Background
Why, why, why isn't there an AUX input on my car stereo from 2007?Yes 2007 was before the big era of smartphones, but everyone owned a couple of dirt cheap mp3 players and iPod was a big thing.
The HU that my car is fitted with has two super retro 8-pin DIN-connections on the back. One of which is for connecting a CD-changer "CD-CHGR" that you could have installed in the boot of the car - but who uses CDs these days?
It didn't take allot of research to find out there is already a product out there that lets you add an AUX to you HU-xxxx. The only drawback is that it sets you back $80 and most of all: It doesn't come with the awesome feeling that you get when you have hacked the stereo yourself.
Research
It wasn't an easy thing finding information on how to hack the HU, but after allot of research I finally found some really good pages that made the hack easy peasy.This is how it works:
- The source knob on the HU lacks the ability to choose the CD-CHGR until you connect the CD-changer.
- To trick the HU that you have a connected a CD-CHGR is not as easy as to shorten two of the pins on the DIN-connector, but it has to be done in code via a protocol named MELBUS
- MELBUS is a protocol that utilizes a clock pin and a single bi directional data line to transfer the data between the units and the HU.
- MELBUS uses three lines: Clock, Data and Busy see blue lines on picture below: ("Run" is just 12V from battery)
Picture source |
- In the picture above you can also see the Left and Right Audio signals that I tapped into for the AUX-input (Red).
- Focus on the left DIN-Socket (female) on the picture, that's the back female socket on the HU.
Hardware
- Arduino Nano (clone) with USB-cable - $2.5
- 8-Pin DIN male plug (pins exactly like the pic above) - $1
- 0.5m network cable - free
- 1m 3.5 mm audio cable - free (who doesn't have a drawer full of them?)
Hacking together some code
Edit 2016-11-04
A guy named Sebastian has modified my code and his version is more stable, and it also works on Mitsubishi HU and supports displaying track numbers! -Here's a link to his code!
A guy named Sebastian has modified my code and his version is more stable, and it also works on Mitsubishi HU and supports displaying track numbers! -Here's a link to his code!
Finally some programming!
I found this awesome write-up that contained almost everything I needed to fool the HU that the Arduino is a CD-CHGR. I will talk you through the process:
- Set BUSY to low for 1000 ms to make the HU run its initialization routine
- The HU init routine starts by sending three bytes: 0x07 0x1A 0xEE
- Then two bytes per optional connection device (~30 of them), the first byte being the predefined ID of the device (ex. CD-CHGR = 0x8E), followed by either an empty byte 0xFF, if the device is not connected, or the answering ID of the device (CD-CHGR = 0xEE)
Example: External CD-CHGR: ID = 0x8E and its return ID is 0xEE
Example: Internal CD-player: ID = 0x80 and its return ID is 0x86
See list of predefined addresses and returns on bottom of this site. - I.e. to simulate a connected CD-CHGR we have to:
- Trigger the HU init routine
- Wait until HU is sending out our ID (0x8E)
- Respond to that by sending back 0xEE
- Now the HU has registered that we have a CD-CHGR and it is now available through the source-knob, and will listen to the Left and Right Audio pins on the DIN-plug.
- Every time the car ignition is turned on, the HU will automatically run a secondary initialization routine:
- Starting by sending four bytes: 0x00 0x00 0x1C 0xED
- Then two bytes per already registered connection devices from the first init routine, the two bytes follows the same pattern as in the first init routine, first ID, than expecting the same answering byte as before.
- If the HU won't get an answer from the CD-CHGR, the device will be removed, and we have to set BUSY low again to call the first init-routine.
As a bonus the 12V is only active when the ignition is turned on => no extra load on battery caused by the arduino when car is not used)
The code I wrote (below) is well documented and fairly easy to understand...
I had a hard time getting the function "SendByteToMelbus(uint8_t byteToSend)" to work.
It turned out that the Arduino functions: digitalWrite(MELBUS_DATA, HIGH); and LOW are too slow for this code, (MELBUS runs at 10-12MHz) and CLK had returned to high state before Databit was changed.
To solve this, I had to go back to the old AVR-GCC technique and use
PORTD |= (1<<MELBUS_DATA); and
PORTD &= ~(1<<MELBUS_DATA);
in place of the Arduino functions.
I tried this setup without any form of ground-loop isolation and it works like a charm! Even when the engine is running and my smartphone is charging through the cigaret-socket the sound is crisp and no disturbing noise (alternator suppose to cause ground noise)
I guess one could buy an external Ground Loop Isolator ($5) to be on the safe side....
I had a hard time getting the function "SendByteToMelbus(uint8_t byteToSend)" to work.
It turned out that the Arduino functions: digitalWrite(MELBUS_DATA, HIGH); and LOW are too slow for this code, (MELBUS runs at 10-12MHz) and CLK had returned to high state before Databit was changed.
To solve this, I had to go back to the old AVR-GCC technique and use
PORTD |= (1<<MELBUS_DATA); and
PORTD &= ~(1<<MELBUS_DATA);
in place of the Arduino functions.
I tried this setup without any form of ground-loop isolation and it works like a charm! Even when the engine is running and my smartphone is charging through the cigaret-socket the sound is crisp and no disturbing noise (alternator suppose to cause ground noise)
I guess one could buy an external Ground Loop Isolator ($5) to be on the safe side....
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Melbus CDCHGR Emulator | |
* Program that emulates the MELBUS communication from a CD-changer (CD-CHGR) in a Volvo V70 (HU-xxxx) to enable AUX-input through the 8-pin DIN-contact. | |
* Tis setup is using an Arduino Nano 5v clone | |
* | |
* The HU enables the CD-CHGR in its source-menue after a successful initialization procedure is accomplished. | |
* The HU will remove the CD-CHGR everytime the car starts if it wont get an response from CD-CHGR (second init-procedure). | |
* | |
* Karl Hagström 2015-11-04 | |
* | |
* This project went realy smooth thanks to these sources: | |
* http://volvo.wot.lv/wiki/doku.php?id=melbus | |
* https://github.com/festlv/screen-control/blob/master/Screen_control/melbus.cpp | |
* http://forums.swedespeed.com/showthread.php?50450-VW-Phatbox-to-Volvo-Transplant-(How-To)&highlight=phatbox | |
*/ | |
const uint8_t MELBUS_CLOCKBIT_INT = 1; //interrupt numer (INT1) on DDR3 | |
const uint8_t MELBUS_CLOCKBIT = 3; //Pin D3 - CLK | |
const uint8_t MELBUS_DATA = 4; //Pin D4 - Data | |
const uint8_t MELBUS_BUSY = 5; //Pin D5 - Busy | |
volatile uint8_t melbus_ReceivedByte = 0; | |
volatile uint8_t melbus_LastReadByte[8] = {0, 0, 0, 0 ,0, 0, 0, 0}; | |
volatile uint8_t melbus_Bitposition = 7; | |
uint8_t ByteToSend = 0; | |
volatile long Counter = 0; | |
volatile bool InitialSequence = false; | |
volatile bool ByteIsRead = false; | |
volatile bool sending_byte = false; | |
volatile bool melbus_MasterRequested = false; | |
volatile bool melbus_MasterRequestAccepted = false; | |
volatile bool Connected = false; | |
volatile bool testbool = false; | |
volatile bool AllowInterruptRead = true; | |
//Startup seequence | |
void setup() { | |
//Data is deafult input high | |
pinMode(MELBUS_DATA, INPUT_PULLUP); | |
//Activate interrupt on clock pin (INT1, D3) | |
attachInterrupt(MELBUS_CLOCKBIT_INT, MELBUS_CLOCK_INTERRUPT, RISING); | |
//Set Clockpin-interrupt to input | |
pinMode(MELBUS_CLOCKBIT, INPUT_PULLUP); | |
//Initiate serial communication to debug via serial-usb (arduino) | |
Serial.begin(115200); | |
Serial.println("Initiating contact with Volvo-Melbus:"); | |
//Call function that tells HU that we want to register a new device | |
melbus_Init_CDCHRG(); | |
} | |
//Main loop | |
void loop() { | |
//Waiting for the clock interrupt to trigger 8 times to read one byte before evaluating the data | |
if (ByteIsRead) { | |
//Reset bool to enable reading of next byte | |
ByteIsRead=false; | |
Counter++; | |
//If we failed to connect, reset and retry the init procedure | |
if(Counter>100 && Connected == false){ | |
Serial.print("\nRetrying to connect...\n"); | |
Counter=0; | |
melbus_MasterRequested = false; | |
melbus_MasterRequestAccepted = false; | |
melbus_Init_CDCHRG(); | |
} | |
if(Counter>100) | |
Counter = 0; | |
//Check if this is the very first initial sequence (07 1A EE ....) | |
if(melbus_LastReadByte[2] == 0x07 && melbus_LastReadByte[1] == 0x1A && melbus_LastReadByte[0] == 0xEE) | |
{ | |
InitialSequence = true; | |
Serial.println("Initiating CD-CHGR..."); | |
} | |
//Now check if this is the car ignition startup sequence. | |
//The HU performes a check everytime the car starts, to evaluate if the initiated applications are still present: (00 00 1C ED ....) | |
//This function is not necessary since the Arduino reconnects if not connected by calling the first init-procedure! | |
else if(melbus_LastReadByte[3] == 0x00 && melbus_LastReadByte[2] == 0x00 && melbus_LastReadByte[1] == 0x1C && melbus_LastReadByte[0] == 0xED){ | |
Serial.println("Initiating ignition CD-CHGR..."); | |
InitialSequence = true; | |
} | |
//If this is the initial sequense and the HU is now asking if the CD-CHGR (0xE8) is prsent? | |
if(melbus_LastReadByte[0] == 0xE8 && InitialSequence == true){ | |
InitialSequence = false; | |
//Returning the expected byte to the HU, to confirm that the CD-CHGR is present (0xEE)! see "ID Response"-table here http://volvo.wot.lv/wiki/doku.php?id=melbus | |
SendByteToMelbus(0xEE); | |
Serial.println("\nConnected!"); | |
//Make sure the bit-counter is reset before we go back to reading bytes... | |
melbus_Bitposition=7; | |
} | |
//Check if the HU is asking for current track information (E9 1B E0 01 08 .......) - about once a second | |
//The HU is writing out CD ERROR if it wont get a response on this... the AUX works anyway! | |
else if(melbus_LastReadByte[4] == 0xE9 && melbus_LastReadByte[3] == 0x1B && melbus_LastReadByte[2] == 0xE0 && melbus_LastReadByte[1] == 0x01 && melbus_LastReadByte[0] == 0x08 ){ | |
Connected = true; | |
Serial.println("\nTrack info requested!"); | |
/* This is where you could request master mode and send the HU your cartridge and track info to display instead of "CD ERROR": | |
* 1. Wait for Busy-pin to go high again (HU not currently using melbus) | |
* 2. Pull datapin low for 2ms | |
* 3. listen for the "Master request broadcast" and then respond to your address followed by 8 bits (track info) | |
* see http://volvo.wot.lv/wiki/doku.php?id=melbus for further info | |
* | |
* For some reson I didn't get the HU to send out the Master request broadcast... | |
* I gave up since I don't care that the display sais CD Error (legit message since my smartphone lacks a CD!) | |
*/ | |
} | |
} | |
//If BUSYPIN is HIGH => HU is in between transmissions | |
if (digitalRead(MELBUS_BUSY)==HIGH) | |
{ | |
//Make sure we are in sync when reading the bits by resetting the clock reader | |
melbus_Bitposition = 7; | |
} | |
} | |
//Notify HU that we want to trigger the first initiate procedure to add a new device (CD-CHGR) by pulling BUSY line low for 1s | |
void melbus_Init_CDCHRG() { | |
//Disabel interrupt on INT1 quicker then: detachInterrupt(MELBUS_CLOCKBIT_INT); | |
EIMSK &= ~(1<<INT1); | |
// Wait untill Busy-line goes high (not busy) before we pull BUSY low to request init | |
while(digitalRead(MELBUS_BUSY)==LOW){} | |
delayMicroseconds(10); | |
pinMode(MELBUS_BUSY, OUTPUT); | |
digitalWrite(MELBUS_BUSY, LOW); | |
delay(1200); | |
digitalWrite(MELBUS_BUSY, HIGH); | |
pinMode(MELBUS_BUSY, INPUT_PULLUP); | |
//Enable interrupt on INT1, quicker then: attachInterrupt(MELBUS_CLOCKBIT_INT, MELBUS_CLOCK_INTERRUPT, RISING); | |
EIMSK |= (1<<INT1); | |
} | |
//This is a function that sends a byte to the HU - (not using interrupts) | |
void SendByteToMelbus(uint8_t byteToSend){ | |
//Disabel interrupt on INT1 quicker then: detachInterrupt(MELBUS_CLOCKBIT_INT); | |
EIMSK &= ~(1<<INT1); | |
//Convert datapin to output | |
//pinMode(MELBUS_DATA, OUTPUT); //To slow, use DDRD instead: | |
DDRD |= (1<<MELBUS_DATA); | |
//For each bit in the byte | |
for(int i=7;i>=0;i--) | |
{ | |
//If bit [i] is "1" - make datapin high | |
if(byteToSend & (1<<i)){ | |
//digitalWrite(, HIGH); //To slow, use AVR-style instead: | |
PORTD |= (1<<MELBUS_DATA); | |
} | |
//if bit [i] is "0" - make databpin low | |
else{ | |
PORTD &= ~(1<<MELBUS_DATA); | |
} | |
//dummyloop until clk goes low and then high again (I think HU is recording the value on datapin when clk goes back high again) | |
while(PIND & (1<<MELBUS_CLOCKBIT)){} | |
while(!(PIND & (1<<MELBUS_CLOCKBIT))){} | |
//while(digitalRead(MELBUS_CLOCKBIT)==HIGH){} | |
//while(digitalRead(MELBUS_CLOCKBIT)==LOW){} | |
} | |
//Reset datapin to high and return it to an input | |
pinMode(MELBUS_DATA, INPUT_PULLUP); | |
//Enable interrupt on INT1, quicker then: attachInterrupt(MELBUS_CLOCKBIT_INT, MELBUS_CLOCK_INTERRUPT, RISING); | |
EIMSK |= (1<<INT1); | |
} | |
//Global external interrupt that triggers when clock pin goes high after it has been low for a short time => time to read datapin | |
void MELBUS_CLOCK_INTERRUPT() { | |
//Read status of Datapin and set status of current bit in recv_byte | |
if (digitalRead(MELBUS_DATA)==HIGH){ | |
melbus_ReceivedByte |= (1<<melbus_Bitposition); //set bit nr [melbus_Bitposition] to "1" | |
} | |
else { | |
melbus_ReceivedByte &=~(1<<melbus_Bitposition); //set bit nr [melbus_Bitposition] to "0" | |
} | |
//if all the bits in the byte are read: | |
if (melbus_Bitposition==0) { | |
//Move every lastreadbyte one step down the array to keep track of former bytes | |
for(int i=7;i>0;i--){ | |
melbus_LastReadByte[i] = melbus_LastReadByte[i-1]; | |
} | |
//Insert the newly read byte into first position of array | |
melbus_LastReadByte[0] = melbus_ReceivedByte; | |
//set bool to true to evaluate the bytes in main loop | |
ByteIsRead = true; | |
//Reset bitcount to first bit in byte | |
melbus_Bitposition=7; | |
} | |
else { | |
//set bitnumber to address of next bit in byte | |
melbus_Bitposition--; | |
} | |
} |