1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
| // ############################################################################################################
// # This code is meant to interface a 6-wire Renault Twingo / Clio steering wheel remote control with #
// # a JVC car radio equipped with a 'steering wheel remote input'. Hardware used is an Arduino Nano-clone. #
// ############################################################################################################
//
// The steering wheel remote connection on the radio (blue/yellow wire or Tip in case of a TS connector)
// is connected to a pull-up resistor in the radio circuitry.
// Data is sent in the form of pulse interval modulation, meaning the interval following a pulse determines if we're sending a 0 or a 1.
// Pulses are sent by pulling the radio's input to ground.
// I'm driving an optocoupler to pull the radio's input to ground, so a HIGH Arduino output makes for a LOW radio input (= a pulse).
// Whenever I refer to HIGH or LOW, I'm talking about the Arduino output.
// Protocol specifications:
// Pulse width 527.5 µs
// Pulse interval for sending 0 1055.0 µs (HIGH for 1 pulse width, LOW for 1 pulse width)
// Pulse interval for sending 1 2110.0 µs (HIGH for 1 pulse width, LOW for 3 pulse widths)
// Note: since the delayMicroseconds() function accepts only unsigned integers, we're using a pulse width of 527 µs
// Data packets are constructed as follows:
// HEADER always LOW (1 pulse width), HIGH (16 pulse widths), LOW (8 pulse widths)
// START BIT always 1
// ADDRESS 7-bits (0x00 to 0x7F), send LSB first, always 0x47 for JVC KD-R531, probably the same for all JVC car radio's
// COMMAND 7-bits (0x00 to 0x7F), send LSB first, see next section for list of known commands for JVC KD-R531
// STOP BITS always 1, 1
// Note: the ADDRESS, COMMAND and STOP BITS are repeated 3 times to ensure the radio properly receives them.
// Known commands for JVC KD-R531:
// HEX DEC BIN(7b) FUNCTION
// 0x04 4 0000100 Volume +
// 0x05 5 0000101 Volume -
// 0x08 8 0001000 Source cycle
// 0x0D 13 0001101 Equalizer preset cycle
// 0x0E 14 0001110 Mute toggle / Play/pause toggle
// 0x12 18 0010010 Tuner Search + / Track + (and Manual Tune + / Fast Forward with press & hold)
// 0x13 19 0010011 Tuner Search - / Track - (and Manual Tune - / Fast Rewind with press & hold)
// 0x14 20 0010100 Tuner Preset + / USB Folder +
// 0x15 21 0010101 Tuner Preset - / USB Folder -
// 0x37 55 0110111 UNKNOWN, appears to be a sort of reset as well as a display test
// 0x58 88 1011000 UNKNOWN, displays 'SN WRITING' where WRITING is blinking
// Define commands
#define VOLUP 0x04
#define VOLDOWN 0x05
#define SOURCE 0x08
#define EQUALIZER 0x0D
#define MUTE 0x0E
#define TRACKFORW 0x12
#define TRACKBACK 0x13
#define FOLDERFORW 0x14
#define FOLDERBACK 0x15
#define UNKNOWN1 0x37
#define UNKNOWN2 0x58
// Renault Twingo / Clio steering wheel remote wire functions
// pin_cycle_current 1 0 2
// OUTPUTS BLUE GREEN YELLOW
// INPUTS PIN# 3 5 6
// BLACK 2 MUTE TOP RIGHT BTN TOP LEFT BTN
// RED 4 VOL+ BOTTOM BTN VOL-
// HIGH HIGH LOW SCROLL UP (CCW)
// BROWN 7 HIGH LOW HIGH SCROLLWHEEL
// LOW HIGH HIGH SCROLL DN (CW)
// Outputs are set LOW one at a time (the other outputs will be HIGH). Inputs (with internal pull-up) are then evaluated.
// If an input is being pulled LOW this means a button is being pressed. Taking into account which output is currently LOW
// we know which button this is. For example, is output pin 3 (Blue wire) is currently LOW and we also read LOW on
// input pin 2 (Black) we know the MUTE button is being pressed.
// For the scrollwheel we must take into account its last known position in order to determine if there has been a change.
// We can determine the direction based on which pins are being pulled LOW.
// Connect Renault Twingo / Clio steering wheel remote wires to these pins
#define BLACKPIN 2 // D2
#define BLUEPIN 3 // D3
#define REDPIN 4 // D4
#define GREENPIN 5 // D5
#define YELLOWPIN 6 // D6
#define BROWNPIN 7 // D7
// Connect optocoupler input through a 1k resistor to this pin
#define OUTPUTPIN 8 // D8
// On-board LED, useful for debugging
#define LEDPIN 13 // D13
// Pulse width in µs
#define PULSEWIDTH 527
// Address that the radio responds to
#define ADDRESS 0x47
// Set number of output pins and put those pins in an array to cycle through when polling the input pins
#define OUT_PINS 3
unsigned char out_pins[OUT_PINS] = {GREENPIN, BLUEPIN, YELLOWPIN};
void setup() {
pinMode(OUTPUTPIN, OUTPUT); // Set the proper pin as output
digitalWrite(OUTPUTPIN, LOW); // Output LOW to make sure optocoupler is off
// Set the pins connected to the steering wheel remote as input / output
pinMode(BLACKPIN, INPUT_PULLUP);
pinMode(BLUEPIN, OUTPUT);
pinMode(REDPIN, INPUT_PULLUP);
pinMode(GREENPIN, OUTPUT);
pinMode(YELLOWPIN, OUTPUT);
pinMode(BROWNPIN, INPUT_PULLUP);
pinMode(LEDPIN, OUTPUT); // Set pin connected to on-board LED as output...
digitalWrite(LEDPIN, LOW); // ...and turn LED off
for (unsigned char i = 0; i <= 7; i++) { // Flash on-board LED a few times so it's easy to see when the Arduino is ready
delay(100);
digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}
delay(100);
digitalWrite(LEDPIN, LOW); // Make sure LED ends up being off
}
// The steering wheel remote has 6 buttons and a scrollwheel, interfaced via 6 wires.
// This function will cycle through the output pins, setting one pin LOW at a time.
// It will then poll the input pins to see which input pins - if any - are pulled LOW.
unsigned char GetInput(void) {
static unsigned char pin_cycle_current = 0; // To keep track of which output pin is currently LOW
static unsigned char pin_cycle_stored; // To store the last known scrollwheel position
static boolean first_run = true; // After booting, there is no known last position for the scrollwheel
// So on the first poll of the scrollwheel just store the current position and don't send a command
unsigned char i;
if (++pin_cycle_current > (OUT_PINS - 1)) pin_cycle_current = 0; // Reset pin_cycle_current counter after last pin
for (i = 0; i < OUT_PINS; i++) { // Cycle through the output pins, setting one of them LOW and the rest HIGH
if (i == pin_cycle_current)
digitalWrite(out_pins[i], LOW);
else
digitalWrite(out_pins[i], HIGH);
}
if (!digitalRead(BROWNPIN)) { // We're only interested if this pin is being pulled LOW
if (pin_cycle_current != pin_cycle_stored) { // If the output that's currently LOW is different from the one that was LOW the last time
// we came through here, then the scrollwheel has changed position
signed char scrollwheel_current = pin_cycle_current - pin_cycle_stored; // Result of this calculation can range from -2 to 2
pin_cycle_stored = pin_cycle_current; // Store which output pin is currently LOW
if (first_run) { // If this is the first run, don't send a command
first_run = false; // (since there was no previously known scrollwheel position)
return 0;
}
if ((scrollwheel_current == 1) || (scrollwheel_current == -2)) { // If above calculation resulted in 1 or -2 the scrollwheel was rotated up (ccw)
return FOLDERBACK;
}else { // If above calculation resulted in anything else the scrollwheel was rotated down (cw)
return FOLDERFORW;
}
}
}
if (!digitalRead(REDPIN)) { // We're only interested if this pin is being pulled LOW
switch(pin_cycle_current) {
case 0: // RED (input) is LOW while GREEN (output) is LOW: bottom button pressed
return SOURCE;
case 1: // RED (input) is LOW while BLUE (output) is LOW: volume + button pressed
return VOLUP;
case 2: // RED (input) is LOW while YELLOW (output) is LOW: volume - button pressed
return VOLDOWN;
}
}
if (!digitalRead(BLACKPIN)) { // We're only interested if this pin is being pulled LOW
switch(pin_cycle_current) {
case 0: // BLACK (input) is LOW while GREEN (output) is LOW: top right button is pressed
return TRACKFORW;
case 1: // BLACK (input) is LOW while BLUE (output) is LOW: mute button is pressed
return MUTE;
case 2: // BLACK (input) is LOW while YELLOW (output) is LOW: top left button is pressed
return TRACKBACK;
}
}
return 0;
}
void loop() {
unsigned char Key = GetInput(); // If any buttons are being pressed the GetInput() function will return the appropriate command code
if (Key) { // If no buttons are being pressed the function will have returned 0 and no command will be sent
SendCommand(Key);
}
}
// Send a value (7 bits, LSB is sent first, value can be an address or command)
void SendValue(unsigned char value) {
unsigned char i, tmp = 1;
for (i = 0; i < sizeof(value) * 8 - 1; i++) {
if (value & tmp) // Do a bitwise AND on the value and tmp
SendOne();
else
SendZero();
tmp = tmp << 1; // Bitshift left by 1
}
}
// Send a command to the radio, including the header, start bit, address and stop bits
void SendCommand(unsigned char value) {
unsigned char i;
Preamble(); // Send signals to precede a command to the radio
for (i = 0; i < 3; i++) { // Repeat address, command and stop bits three times so radio will pick them up properly
SendValue(ADDRESS); // Send the address
SendValue((unsigned char)value); // Send the command
Postamble(); // Send signals to follow a command to the radio
}
}
// Signals to transmit a '0' bit
void SendZero() {
digitalWrite(OUTPUTPIN, HIGH); // Output HIGH for 1 pulse width
digitalWrite(LEDPIN, HIGH); // Turn on on-board LED
delayMicroseconds(PULSEWIDTH);
digitalWrite(OUTPUTPIN, LOW); // Output LOW for 1 pulse width
digitalWrite(LEDPIN, LOW); // Turn off on-board LED
delayMicroseconds(PULSEWIDTH);
}
// Signals to transmit a '1' bit
void SendOne() {
digitalWrite(OUTPUTPIN, HIGH); // Output HIGH for 1 pulse width
digitalWrite(LEDPIN, HIGH); // Turn on on-board LED
delayMicroseconds(PULSEWIDTH);
digitalWrite(OUTPUTPIN, LOW); // Output LOW for 3 pulse widths
digitalWrite(LEDPIN, LOW); // Turn off on-board LED
delayMicroseconds(PULSEWIDTH * 3);
}
// Signals to precede a command to the radio
void Preamble() {
// HEADER: always LOW (1 pulse width), HIGH (16 pulse widths), LOW (8 pulse widths)
digitalWrite(OUTPUTPIN, LOW); // Make sure output is LOW for 1 pulse width, so the header starts with a rising edge
digitalWrite(LEDPIN, LOW); // Turn off on-board LED
delayMicroseconds(PULSEWIDTH * 1);
digitalWrite(OUTPUTPIN, HIGH); // Start of header, output HIGH for 16 pulse widths
digitalWrite(LEDPIN, HIGH); // Turn on on-board LED
delayMicroseconds(PULSEWIDTH * 16);
digitalWrite(OUTPUTPIN, LOW); // Second part of header, output LOW 8 pulse widths
digitalWrite(LEDPIN, LOW); // Turn off on-board LED
delayMicroseconds(PULSEWIDTH * 8);
// START BIT: always 1
SendOne();
}
// Signals to follow a command to the radio
void Postamble() {
// STOP BITS: always 1
SendOne();
SendOne();
} |