Skip to content

Instantly share code, notes, and snippets.

@klalle
Last active July 27, 2024 23:24
Show Gist options
  • Save klalle/1ae1bfec5e2506918a3f89492180565e to your computer and use it in GitHub Desktop.
Save klalle/1ae1bfec5e2506918a3f89492180565e to your computer and use it in GitHub Desktop.
/* 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.
* This 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
* mod by S. Zeller 2016-03-14
*
* 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
*
* pulse train width=120us (15us per clock cycle), high phase between two pulse trains is 540us-600us
*/
#define SERDBG 1
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_CharBytes = 0;
volatile uint8_t melbus_OutByte = 0xFF;
volatile uint8_t melbus_LastReadByte[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
volatile uint8_t melbus_SendBuffer[9] = {0x00,0x02,0x00,0x01,0x80,0x01,0xC7,0x0A,0x02};
volatile uint8_t melbus_SendCnt=0;
volatile uint8_t melbus_DiscBuffer[6] = {0x00,0xFC,0xFF,0x4A,0xFC,0xFF};
volatile uint8_t melbus_DiscCnt=0;
volatile uint8_t melbus_Bitposition = 0x80;
volatile bool InitialSequence_ext = false;
volatile bool ByteIsRead = false;
volatile bool sending_byte = false;
volatile bool melbus_MasterRequested = false;
volatile bool melbus_MasterRequestAccepted = false;
volatile bool testbool = false;
volatile bool AllowInterruptRead = true;
volatile int incomingByte = 0; // for incoming serial data
//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, FALLING);
//Set Clockpin-interrupt to input
pinMode(MELBUS_CLOCKBIT, INPUT_PULLUP);
#ifdef SERDBG
//Initiate serial communication to debug via serial-usb (arduino)
Serial.begin(230400);
Serial.println("Initiating contact with Melbus:");
#endif
//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
#ifdef SERDBG
if (ByteIsRead) {
//Reset bool to enable reading of next byte
ByteIsRead=false;
if (incomingByte == ' ') {
if(melbus_LastReadByte[11] == 0x0 && (melbus_LastReadByte[10] == 0x4A || melbus_LastReadByte[10] == 0x4C || melbus_LastReadByte[10] == 0x4E) && melbus_LastReadByte[9] == 0xEC && melbus_LastReadByte[8] == 0x57 && melbus_LastReadByte[7] == 0x57 && melbus_LastReadByte[6] == 0x49 && melbus_LastReadByte[5] == 0x52 && melbus_LastReadByte[4] == 0xAF && melbus_LastReadByte[3] == 0xE0 && melbus_LastReadByte[2] == 0x0)
{
melbus_CharBytes=8; //print RDS station name
}
if(melbus_LastReadByte[1] == 0x0 && melbus_LastReadByte[0] == 0x4A)
{
Serial.println("\n LCD is master: (no CD init)");
}
else if(melbus_LastReadByte[1] == 0x0 && melbus_LastReadByte[0] == 0x4C)
{
Serial.println("\n LCD is master: (???)");
}
else if(melbus_LastReadByte[1] == 0x0 && melbus_LastReadByte[0] == 0x4E)
{
Serial.println("\n LCD is master: (with CD init)");
}
else if(melbus_LastReadByte[1] == 0x80 && melbus_LastReadByte[0] == 0x4E)
{
Serial.println("\n ???");
}
else if(melbus_LastReadByte[1] == 0xE8 && melbus_LastReadByte[0] == 0x4E)
{
Serial.println("\n ???");
}
else if(melbus_LastReadByte[1] == 0xF9 && melbus_LastReadByte[0] == 0x49)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0x80 && melbus_LastReadByte[0] == 0x49)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0xE8 && melbus_LastReadByte[0] == 0x49)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0xE9 && melbus_LastReadByte[0] == 0x4B)
{
Serial.println("\n HU is master: -> CDC");
}
else if(melbus_LastReadByte[1] == 0x81 && melbus_LastReadByte[0] == 0x4B)
{
Serial.println("\n HU is master: -> CDP");
}
else if(melbus_LastReadByte[1] == 0xF9 && melbus_LastReadByte[0] == 0x4E)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0x50 && melbus_LastReadByte[0] == 0x4E)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0x50 && melbus_LastReadByte[0] == 0x4C)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0x50 && melbus_LastReadByte[0] == 0x4A)
{
Serial.println("\n HU is master: ");
}
else if(melbus_LastReadByte[1] == 0xF8 && melbus_LastReadByte[0] == 0x4C)
{
Serial.println("\n HU is master: ");
}
if (melbus_CharBytes) {
Serial.write(melbus_LastReadByte[1]);
melbus_CharBytes--;
}else
{
Serial.print(melbus_LastReadByte[1],HEX);
Serial.write(' ');
}
}
}
#endif
//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
#ifdef SERDBG
if (melbus_Bitposition != 0x80) {
Serial.println(melbus_Bitposition,HEX);
Serial.println("\n not in sync! ");
}
#endif
if (incomingByte != 'k') {
melbus_Bitposition = 0x80;
melbus_OutByte = 0xFF;
melbus_SendCnt=0;
melbus_DiscCnt=0;
DDRD &= ~(1<<MELBUS_DATA);
PORTD |= (1<<MELBUS_DATA);
}
}
#ifdef SERDBG
if (Serial.available() > 0) {
// read the incoming byte:
incomingByte = Serial.read();
}
if (incomingByte == 'i') {
melbus_Init_CDCHRG();
Serial.println("\n forced init: ");
incomingByte=0;
}
#endif
if ((melbus_Bitposition == 0x80) && (PIND & (1<<MELBUS_CLOCKBIT)))
{
delayMicroseconds(7);
DDRD &= ~(1<<MELBUS_DATA);
PORTD |= (1<<MELBUS_DATA);
}
}
//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);
}
//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(melbus_OutByte & melbus_Bitposition){
DDRD &= (~(1<<MELBUS_DATA));
PORTD |= (1<<MELBUS_DATA);
}
//if bit [i] is "0" - make databpin low
else{
PORTD &= (~(1<<MELBUS_DATA));
DDRD |= (1<<MELBUS_DATA);
}
if (PIND & (1<<MELBUS_DATA)){
melbus_ReceivedByte |= melbus_Bitposition; //set bit nr [melbus_Bitposition] to "1"
}
else {
melbus_ReceivedByte &=~melbus_Bitposition; //set bit nr [melbus_Bitposition] to "0"
}
//if all the bits in the byte are read:
if (melbus_Bitposition==0x01) {
//Move every lastreadbyte one step down the array to keep track of former bytes
for(int i=11;i>0;i--){
melbus_LastReadByte[i] = melbus_LastReadByte[i-1];
}
if (melbus_OutByte != 0xFF) {
melbus_LastReadByte[0] = melbus_OutByte;
melbus_OutByte = 0xFF;
} else {
//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=0x80;
if(melbus_LastReadByte[2] == 0x07 && (melbus_LastReadByte[1] == 0x1A || melbus_LastReadByte[1] == 0x4A) && melbus_LastReadByte[0] == 0xEE)
{
InitialSequence_ext = true;
}
else if(melbus_LastReadByte[2] == 0x0 && (melbus_LastReadByte[1] == 0x1C || melbus_LastReadByte[1] == 0x4C) && melbus_LastReadByte[0] == 0xED)
{
InitialSequence_ext = true;
}
else if((melbus_LastReadByte[0] == 0xE8 || melbus_LastReadByte[0] == 0xE9) && InitialSequence_ext == true){
InitialSequence_ext = 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
melbus_OutByte = 0xEE;
}
else if((melbus_LastReadByte[2] == 0xE8 || melbus_LastReadByte[2] == 0xE9) && (melbus_LastReadByte[1] == 0x1E || melbus_LastReadByte[1] == 0x4E) && melbus_LastReadByte[0] == 0xEF)
{
// CartInfo
melbus_DiscCnt=6;
}
else if((melbus_LastReadByte[2] == 0xE8 || melbus_LastReadByte[2] == 0xE9) && (melbus_LastReadByte[1] == 0x19 || melbus_LastReadByte[1] == 0x49) && melbus_LastReadByte[0] == 0x22)
{
// Powerdown
melbus_OutByte = 0x00; // respond to powerdown;
melbus_SendBuffer[1]=0x02; // STOP
melbus_SendBuffer[8]=0x02; // STOP
}
else if((melbus_LastReadByte[2] == 0xE8 || melbus_LastReadByte[2] == 0xE9) && (melbus_LastReadByte[1] == 0x19 || melbus_LastReadByte[1] == 0x49) && melbus_LastReadByte[0] == 0x52)
{
// RND
}
else if((melbus_LastReadByte[2] == 0xE8 || melbus_LastReadByte[2] == 0xE9) && (melbus_LastReadByte[1] == 0x19 || melbus_LastReadByte[1] == 0x49) && melbus_LastReadByte[0] == 0x29)
{
// FF
}
else if((melbus_LastReadByte[2] == 0xE8 || melbus_LastReadByte[2] == 0xE9) && (melbus_LastReadByte[1] == 0x19 || melbus_LastReadByte[1] == 0x49) && melbus_LastReadByte[0] == 0x2F)
{
// FR
melbus_OutByte = 0x00; // respond to start;
melbus_SendBuffer[1]=0x08; // START
melbus_SendBuffer[8]=0x08; // START
}
else if((melbus_LastReadByte[3] == 0xE8 || melbus_LastReadByte[3] == 0xE9) && (melbus_LastReadByte[2] == 0x1A || melbus_LastReadByte[2] == 0x4A) && melbus_LastReadByte[1] == 0x50 && melbus_LastReadByte[0] == 0x01)
{
// D-
melbus_SendBuffer[3]--;
melbus_SendBuffer[5]=0x01;
}
else if((melbus_LastReadByte[3] == 0xE8 || melbus_LastReadByte[3] == 0xE9) && (melbus_LastReadByte[2] == 0x1A || melbus_LastReadByte[2] == 0x4A) && melbus_LastReadByte[1] == 0x50 && melbus_LastReadByte[0] == 0x41)
{
// D+
melbus_SendBuffer[3]++;
melbus_SendBuffer[5]=0x01;
}
else if((melbus_LastReadByte[4] == 0xE8 || melbus_LastReadByte[4] == 0xE9) && (melbus_LastReadByte[3] == 0x1B || melbus_LastReadByte[3] == 0x4B) && melbus_LastReadByte[2] == 0x2D && melbus_LastReadByte[1] == 0x00 && melbus_LastReadByte[0] == 0x01)
{
// T-
melbus_SendBuffer[5]--;
}
else if((melbus_LastReadByte[4] == 0xE8 || melbus_LastReadByte[4] == 0xE9) && (melbus_LastReadByte[3] == 0x1B || melbus_LastReadByte[3] == 0x4B) && melbus_LastReadByte[2] == 0x2D && melbus_LastReadByte[1] == 0x40 && melbus_LastReadByte[0] == 0x01)
{
// T+
melbus_SendBuffer[5]++;
}
else if((melbus_LastReadByte[4] == 0xE8 || melbus_LastReadByte[4] == 0xE9) && (melbus_LastReadByte[3] == 0x1B || melbus_LastReadByte[3] == 0x4B) && melbus_LastReadByte[2] == 0xE0 && melbus_LastReadByte[1] == 0x01 && melbus_LastReadByte[0] == 0x08 ){
// Playinfo
melbus_SendCnt=9;
}
if (melbus_SendCnt) {
melbus_OutByte=melbus_SendBuffer[9-melbus_SendCnt];
melbus_SendCnt--;
} else if (melbus_DiscCnt) {
melbus_OutByte=melbus_DiscBuffer[6-melbus_DiscCnt];
melbus_DiscCnt--;
}
} else {
//set bitnumber to address of next bit in byte
melbus_Bitposition>>=1;
}
EIFR |= (1 << INTF1);
}
@archi
Copy link

archi commented Mar 21, 2017

@nenkovarna take a look at http://gizmosnack.blogspot.de/2015/11/aux-in-volvo-hu-xxxx-radio.html

The code also has the wiring in the variables:

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

Just connect the Clock to Pin D3, Data to D4 and Busy to D5.
The link has a diagram on which you can see which pins on the radio connector are Clock/Data/Busy (I think originally it is either from festlv [who also runs wot.lv], or some forum).

Also: Take a look at the websites mentioned in the gist comment.
I also have a project on https://github.com/archi/melbus - but haven't got around to updating it (I think this gist is better functionality-wise).

Good luck with your project :)

@VincentGijsen
Copy link

I also started tinkering with the HU (850). your code works very nice.

I was wondering if you already figured out how to address the sending of text. I find it quite a shame the those who have figured it out (by sniffing the melbus with the original equipment), keep the results to themselves... If i would have the sat-tuner I would definitly sniff the connection and at least share the whole session, either via decoding the protocol, or the logical analyser dumps.

Just thinking out loud: I see that there is some code to show RDS information, have you tried using the same format and sending the same packages while tuned to the SAT channel? i'll try this myself in the coming days, but perhaps you've already tried?

I also might write some 'brute-force' code to just send a truckload of combinations to the radio, and see if I can get some garbage on the display...

You wouldn't happen to know the hardware used in the grom/imiv? I might fire up the hex-editor or IDA and try to wrestle through the hex files, looking for bytes similar to the public availble id's used by melbus... however I rather not. I would really like to send some text or a phone-number to my display using my arduino + bluetooth module.

@Dainiulis
Copy link

Hello, I tried this code for my mitsubishi w142 head unit but it doesn't work. Has anyone managed to get it working on this head unit?

@VincentGijsen
Copy link

Hi,
I started sniffing text-exchange on the melbus, to implement on my car-pc project. checkout https://github.com/VincentGijsen/melbus-sniffed-traffic/blob/master/README.md for more details and captures on the bus

@Jonatan-S
Copy link

Hey,
I wanted to use this code in my Volvo S80 with a HU-801 Radio. The car RTI by default but I don't use it so I just disconnected it and connected my Arduino Uno (had it lying around) directly to the back of the radio. Thing is: It is not working. It's always telling me:
"40

not in sync!"
Sadly that doesn't really tell me anything about what is wrong. Could anyone help me out here, please?

@bingo-bango
Copy link

@Jonatan-S
I expect your HU-801 is coded from the factory to expect a connection to the RTI unit. I don't know for sure, but I'd expect you'd have to flash the unit with the Volvo software to disable RTI.

@bingo-bango
Copy link

bingo-bango commented Dec 9, 2017

Update: you may want to try plugging the RTI unit back in, and pulgging the UNO into the RTI. See here for more details

@aryangangurde
Copy link

Hi, I just bought a volvo v70 2004 with a hu-803 and I would like to keep the head unit but I need the ability to play music from my own source via aux, usb or Bluetooth. I came across this project of yours which sounds great but I have spent a couple of weeks trying this myself with no luck. Initially with an arduino nano every, then a arduino mega then today with a regular nano clone. My first two attempts were with just jamming wires in the din plug at the back as a proof of concept but then I bought a din cable to splice to the arduino nano that I got for this which also had no luck. I have double checked the wiring connections (I also put a voltage regulator between run and vin for protection) but I haven't been able to make the cd changer appear. All that happens is a cd error message and then the internal cd player becomes unelectable. Not sure what to do from here, if anyone else has had a similar experience and has solved it and gotten it to work, please do let me know. I'll be trying again tomorrow to see

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment