Arduino as a CD-Changer emulator: 9-databit serial

I want to use my Arduino as a CD-changer emulator for my car stereo (Blaupunkt Travelpilot DX-R52). I sniffed the protocol with a logic analyzer and figured it all out. It's a 9-databit, 1 stopbit, 9800 baud (4800 during startup) RS232 signal without parity. The command structure is pretty straightforward.

Is it possible to do 9-databit serial send/receive with the Arduino; via HardwareSerial or SoftwareSerial? I've found several modified HardwareSerial libraries so far that allow some control over the databits, but none of them seem to be able to send and receive 9 bits of data.

I want to use the buttons on the head unit to control the music on my Android phone via Bluetooth (using the guts of a Bluetooth multimedia keyboard and the Arduino as a bridge). I need to convince the head unit that it's talking to a CD-changer for that to work. The Aux input stays off when no 'CD' is playing.

Here are some captures of the protocol:

A packet up close (bit level):

Head unit to CD-changer: "Load Disk 1, Track 2" (after pressing 'Next Track')

CD-changer to Head unit: "Loaded Disk 1, Track 2"

It shouldn't be hard to replicate to rest of the protocol once I figured the 9-bit communications out.

It should be very over-the-top hard. Since you have to read and write with 16 bits data.
Can you check once more if that is a parity ? I think I can see a change in the last bit according to the previous bits in the pictures or maybe I don't. Is your sampling frequency too low ? Why are the pulses different in timing ? Perhaps it is software generated and the 9th bit is due to timing problems.

P.S.: What makes such nice pictures ?

It's not a parity bit, the packets don't pass the parity test if I configure the decoder with parity. The limited documentation of the CDC protocol mentions the 9 databits, a few other embedded devices like this use it too.

I'm sampling at 50KHz, if I go one step lower (10KHz) it starts to skip bits.

The pictures are actually screenshots :wink:

It won't be easy to write code with 16-bit buffers. Writing can be done without buffers, but I think you need a buffer for incoming data. You can use SoftwareSerial as a start, but it will be very hard.

If the 9-bits serial data is used more, is there some example code ? On this forum or in the projects section of avrfreaks.net ? When I google for it, I can find tons of pages and questions about it. It seems that the ATmega chip USART supports 9-bits.

I've got transmitting 9-bits under control using a special command to set the 9th bit, but the signal needs to be inverted (low=logic 1, high=logic 0) with some circuitry for it to be compatible.

void setup(){
  Serial2.begin(9600);
}
void loop() 
{                                          
  tx(0x0101); //100000001
  delay(5);
  tx(0x00AD); //101101010
  delay(5);
  tx(0x01FF); //111111111
  delay(100);
}
void tx(int value){
  if(value < 256){
    UCSR2B = 0b10011100; //Turn 9th bit off
    Serial2.write(value);
  }
  else if(value > 255){
    UCSR2B = 0b10011101; //Turn 9th bit on
    Serial2.write(value-256);
  }
}

I'm still clueless on the receiving end...

donny007x:
I'm still clueless on the receiving end...

So am I. You need something interrupt driven, with a 16-bit integer ring buffer. I can find many discussions about it and half-baked solutions, but not a nice example for an ATmel chip.

Well, I got it to work. The microcontroller supports 9-bit serial, with the 9th bit as an afterthought ;).

I changed the buffers in HardwareSerial.cpp from unsigned char (8 bit) to unsigned short int (16 bit) and added the register config for 9N1 serial. The 9th bit has to be read from another register before reading the other 8 bits, and has to be set to the register before writing the other 8 bits.

Sources for the lower-level serial stuff:

http://www.mikrocontroller.net/topic/243528

The sketch is now pretty simple. Next up is modifying the Serial.write() routine in HardwareSerial to do the same as tx().

void setup(){
  Serial.begin(115200); //pc
  Serial2.begin(9600, SERIAL_9N1);
  UCSR2B |= ( 1 << UCSZ02 );
}

int incoming = 0;
void loop() 
{
  //RX Routine
  if (Serial2.available() > 0) {
    incoming = Serial2.read();
    Serial.println(incoming, HEX);
    tx(incoming);
  }  
}
//TX Routine
void tx(int value){
  if(value < 256){
    UCSR2B = 0b10011100; //Turn 9th bit off
    Serial2.write(value);
  }
  else if(value > 255){
    UCSR2B = 0b10011101; //Turn 9th bit on
    Serial2.write(value-256);
  }
}

I inverted the signals using a ULN2003A (the only thing close to a transistor that I had lying around) and successfully communicated with the head unit, writing 13:37 as the current playtime on the display:

Yellow: Head unit, Blue: Inverted signal going to Arduino, Green: Arduino repeating the same signal back:

I couldn't have done it without the logic analyzer, seeing the bits flow through the wire is a must for stuff like this.

I'm very impressed!
When you have working code you might make a page for it in the Playground section. I have read about many questions for 9-bit serial, but no solution yet.

Donny,

Do you have a reference for the Blaupunkt CDC protocol? i.e., which commands are sent, and responses.

I'm working on a similar project, interfacing a Blaupunkt radio from 2010 (a Bremen MP78) to an old Alpine CD changer that uses Alpine's M-bus protocol.

btw, what logic analyzer did you use?

Hi, can I try to reopen this thread ? I'd like to setup a cdc faker for my Bosch/Blaupunkt HU. I collected contributions in these topics.........

http://forum.arduino.cc/index.php?topic=39755.0

http://forum.arduino.cc/index.php?topic=91377.0

i'm not pretty good in electronics and protocol sniffing but good in programming... pls if anyone can help to setup the right sketch to "init" the fake cdc and fool the HU. That's in order to add an additional external audio source.

THANKS GUYS

Hello. A question for a friend donny007x. I successfully communicated via arduino to the radio but I have a small problem. Did you manage to display some text on the display on the radio? The protocol is the command used when browsing through the changer. 0x10B and eight times 0x020 are sent with each viewed disc. Maybe it's some way to send text to the display. Secondly, what delays do you use between the characters you send?

I don't have this radio anymore, but I do have all the protocol logs/captures that I made back then.

0x10B followed by eight times 0x20 is a timing synchronization thing (ping/reply).

For the display: on this particular radio you can only change the current track/disk number and set the play time, text input is not supported.

Here is the code that I hacked together back then:

// This code emulates a Blaupunkt CDC-A08
// Protocol: 9-databits, no parity, 1 stopbit, 0-5V TTL (inverted) serial (requires a modified HardwareSerial)
// Tested with: Blaupunkt Travelpilot DX-R52 (Warning: Blaupunkt may use different protocols across generations of radio's)
// Arduino boards: Mega 2560 R3, Micro
// Arduino has to be reset before radio is turned on (use the switched 12v line from the CDC connector with a relay)

// Status:
// Initialization and mag scan: Fully implemented, except for mag present/not present status (not required)
// Playback controls (play/pause/track change/disk change): Fully implemented
// Special controls (mix/repeat/fast forward/rewind): Not implemented
// Position report (time): Implemented, but counting up goes wrong (gives strange times but doesn't affect anything)
// Serial sync/ping: Implemented, but doesn't actually check if pongs are received
// Cold/Warm start: Fully implemented
// Far from pretty, we're using the send-and-hope-for-the-best approach here...

////////////////////////
//  CD CHANGER SETUP  //
////////////////////////

//Disk Magazine (always contains 10 disks thanks to laziness)
int numTracks = 0x99; //Number of tracks per disk (1-99)
int runtimeMin = 0x25; //Disk Runtime: Minutes
int runtimeSec = 0x1; //Disk Runtime: Seconds
int trackLength = 0x05; //Length of each track in minutes

//Currently Playing
int currentDisk = 0x1; //Disk currently playing
int currentTrack = 0x1; //Track currently playing
int currentMinutes = 0x0; //Position: minutes
int currentSeconds = 0x0; //Position: Seconds
int playStatus = 0xA;

void setup(){
  //Serial.begin(115200); //Pc, for debugging
  Serial1.begin(4800, SERIAL_9N1); //start with 4800 baud, later we switch to 9600
  UCSR1B |= ( 1 << UCSZ11 );
}

//Command buffer and states
boolean c = false;
int cmdCapture[16];
int cmdCaptureIndex=0;
long m = 0;
long g = 0;
boolean huLastWord = false;

void loop() 
{
  ////////////////////////////
  //  CD CHANGER FUNCTIONS  //
  ////////////////////////////
  if (Serial1.available() > 0) {
    int incoming = Serial1.read();
    //if(incoming != 0x14F){tx(incoming);}
    //capture command; read every byte back to the HU, except for the end of command byte
    if (incoming == 0x180){c=true;delayMicroseconds(400);tx(incoming);g = millis();} //start of command
    else if (c){
      if(incoming == 0x14F){c=false;interpret(cmdCapture, cmdCaptureIndex);cmdCaptureIndex=0;} //end of command
      else{cmdCapture[cmdCaptureIndex]=incoming;cmdCaptureIndex++;tx(incoming);} //store the value
    }
    
  }
  else if(playStatus == 0x9 && millis()-m > 1500){
    m = millis();
    currentSeconds += 0x1;
    if (currentSeconds == 0x60){currentSeconds = 0x0; currentMinutes += 0x1;}
    int t[]={0x109,currentMinutes,currentSeconds,0x14F}; transmit(t,4); //transmit the current playback position, counting goes wrong but that doesn't matter, as long as the HU gets two numbers at regular intervals it's ok
    
    if(currentMinutes >= trackLength){ //reset the timer when this track ends, we aren't going to actually load the next track
        currentSeconds = 0x0;
        currentMinutes = 0x0;
    }
  }

////////////////////////////
//  CD CHANGER FUNCTIONS  //
////////////////////////////

//TX Routine
void tx(int value){
  if(value < 256){
    UCSR1B = 0b10011100; //Turn 9th bit off
    Serial1.write(value);
  }
  else if(value > 255){
    UCSR1B = 0b10011101; //Turn 9th bit on
    Serial1.write(value-256);
  }
}
//Send a formatted command sequence
void transmit(int args[], int length){
  if(huLastWord){
    tx(args[0]);
    huLastWord = false;
    delay(46);
  }
  for (int i=0; i<length; i++){
    delay(8); 
    tx(args[i]); //transmit the byte
  }
}
//Process a received command
void interpret(int args[], int length){
  huLastWord = true;
  // ====================== PLAYBACK ====================== //
  if(args[0] == 0x99){ //track change
    int prevDisk = currentDisk;
    int prevTrack = currentTrack;
    
    currentDisk = args[1]; 
    if(args[1] > 0x9){currentDisk = 0x1;} //catch a disk select to an impossible slot, happens from time to time
    currentTrack = args[2];
    if(args[2] == 0x0){currentTrack = 0x1;} //catch a track select to track 0, happens from time to time
    currentMinutes = 0x0;
    currentSeconds = 0x0;
    Serial.print("Loading Disk: "); Serial.print(currentDisk,HEX); Serial.print(" Track: "); Serial.println(currentTrack,HEX);
    delay(30);
      int t1[]={0x101,currentDisk,currentTrack,0x14F}; transmit(t1,4); //tell the HU that we're loading the selected track
    delay(30);
      int t2[]={0x10D,0x1,numTracks,runtimeMin,runtimeSec,0x14F}; transmit(t2,6); //give the HU some info about the current disk
    delay(30);
      int t3[]={0x109,currentMinutes,currentSeconds,0x14F}; transmit(t3,4); //set the timer to 0:00
    delay(30);
      int t4[]={0x101,currentDisk,currentTrack,0x14F}; transmit(t4,4); //tell the HU that we've loaded the selected track
    delay(30);
    //  int t5[]={0x10B,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x14F}; transmit(t5,10); //Do the timing/ping thing (not really needed)
    //delay(30);

   // Do something with the play/pause/next/previous command here
    
  }
  else if(args[0] == 0x21){ //Pause
    Serial.println("Pause");
    playStatus = 0xA;
    delay(30);
    int t1[]={0x103,0x20,playStatus,0x20,0x0,0x14F}; transmit(t1,6); delay(40); transmit(t1,6);//tell the HU that we've paused playback, twice (sometimes bytes get lost when switching to FM radio, the HU doesn't care how many times you send this)
    delay(30);
  }
  else if(args[0] == 0xA5){ //Play
    Serial.println("Play");
    playStatus = 0x9;
    delay(30);
    int t1[]={0x101,currentDisk,currentTrack,0x14F}; transmit(t1,4); //tell the HU what track we're playing
    delay(32);
    int t2[]={0x103,0x20,playStatus,0x20,0x0,0x14F}; transmit(t2,6); //tell the HU that we've resumed playback
    delay(30);
    int t4[]={0x10B,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x14F}; transmit(t4,10); //Do the timing/ping thing
    delay(30);
    int t3[]={0x10D,0x1,numTracks,runtimeMin,runtimeSec,0x14F}; transmit(t3,6); //give the HU some info about the current disk
    delay(30);

  }
  // ====================== MISC ====================== //
  else if(args[0] == 0x48){ //Baud rate change on startup
    if(args[1] == 0x1){ //4800
      Serial.println("Baudrate: 4800");
      delay(30);
      int t1[]={0x10F,0x48,0x1,0x14F}; transmit(t1,4);
    }
    else if(args[1] == 0x2){ //4800
      Serial.println("Baudrate: 9600");
      Serial1.end();
      Serial1.begin(9600, SERIAL_9N1);
    }
  }
  else if(args[0] == 0xA7){ //Status report
      Serial.println("Status report");
      delay(30);
      int t1[]={0x10E,0x8,0x1,0x14F}; transmit(t1,4); //No idea, HU expects this response...
  }
  else if(args[0] == 0xA9){ //Report the magazine status
      Serial.println("MAG status report");
      delay(30);
      int t1[]={0x10C,0x0,0x14F}; transmit(t1,3); //Mag status 0x0 (scanned), 0x1 (mag just inserted, needs to be scanned), 0x2 (no mag inserted), HU always scans the mag on cold start
  }                  
                     // ???             //Mix mag/off      //Repeat track     //Repeat disk      //Repeat off       //Mix disk         // ???
  else if(args[0] == 0x11 || args[0] == 0x96 || args[0] == 0x93 || args[0] == 0x94 || args[0] == 0x95 || args[0] == 0xA3 || args[0] == 0x87){ //All of these expect a playback and disk status report
      Serial.println("Playback status report");
      delay(30);
      int t1[]={0x103,0x20,playStatus,0x20,0x0,0x14F}; transmit(t1,6); //tell the HU the play status
      delay(30);
      int t2[]={0x10B,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x14F}; transmit(t2,10); //Do the timing/ping thing
      delay(30);
      int t3[]={0x10D,0x1,numTracks,runtimeMin,runtimeSec,0x14F}; transmit(t3,6); //give the HU some info about the current disk
      delay(30);
  }
  else{
     Serial.print("Unknown: ");
     for(int i=0; i < sizeof(args); i++){
       Serial.print(args[i],HEX);
       Serial.print(", "); 
     }
     Serial.println(";");
  }
}
1 Like

donny007x: Thanks for posting this - great info!

I am trying to do a very similar project with my van stereo/cassette/CD. I want to replace the cassette circuit with a Bluetooth AVRCP chip and use the front panel buttons to control playback (play/pause, prev/next, and receive/end calls).

My unit is a Visteon DM100i that uses SPI to communicate between the main board and the two peripherals (cassette and CD), Visteon isn't talking, and the internet is lacking info on this system. The PCB for the cassette/CD has the pins clearly labeled so I can connect to them easily enough, but I'm a relative beginner with Arduino and would really appreciate any insight you could provide as to:

  • how did you set up the logic analyzer (did you use an Arduino?),
  • did the analyzer sit between the HU and the CDC? and
  • how did you get the display output (Arduino Serial Plotter?).

I really appreciate your posts in this thread, and hope you'll have time for one more.

Thanks!
Rick

donny007x:
Here is the code that I hacked together back then:

Do you have your modified libraries still?

Hello. It's been a long time since my post. Admittedly, I've dealt with the emulator myself and now it works as expected but thanks for placing the source code. It will be useful to someone. I can add from my thrones how to enter the position (time) of the song. For a long time I could not understand what's going on, and finally I came up with something like that ... the radio does not read the counterparts in hex in turn. Let go of seven items from time to time and counts further. So I created a matrix and by referencing the appropriate index in the matrix I get the corresponding number on the displays.

int _HEX[] =
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
{0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8,0x9,0xA,0xB,0xC,0xD,0xE,0xF,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D

// 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 0x1E,0x1F,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,

// 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 0x3F,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,

// 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 0x66,0x76,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F,0x86,0x87,

// 90 91 92 93 94 95 96 97 98 99 100
0x89,0x8A,0x8B,0x8C,0x8d,0x8E,0x8F,0x96,0x97,0x98,0x99
};

void setTime(int m, int s){
mySerial.write9(0x109);
delay(1);
mySerial.write9(_HEX[m]);
delay(1);
mySerial.write9(_HEX~~);~~
~~ delay(1);~~
~~ mySerial.write9(frameEndMarker);~~
}
it may be useful to someone.
Sorry for my googleEnglish :wink:

1 Like

WallK. I used SoftwareSerial9 library on atmega328(uno) and its work perfect.

Friends, help me figure out what I'm doing wrong.
The task is simple. I have a blaupunkt head unit that, by its specification, accurately supports an external cd changer. I also decided to make an emulator. But since there was only an arduino nano board as the platform, I ran into a serial port problem. At first I was looking for the possibility of implementing a software serial port with 9-bit transmission, but nothing came of it. Could not find at least an example of how this can be done. I am silent about ready-made libraries, etc. Then I decided to sacrifice the only serial port on the board. I found libraries for the hardware serial port of 9 bits, adjusted the sketch from donny007x (thanks a lot for doing this) - changed UCSR1B to UCSR0B and, accordingly, UCSZ11 to UCSZ02.
But nothing came of it.
Please give advice, did I do the right thing? If someone managed to do it using the software serial port, share how you can do it.
Or will I have to purchase Mega for these purposes?
For information. When I tried to understand what was happening, I wrote a simple sketch that simply accepted what came to him from the head unit and wrote it to the port monitor, then I saw that when I turned on the radio, it sent four pings to the port, but every time I turned it on different meanings. No logic or pattern could be traced.
When I used the hardware serial port with a donny007x sketch, then when I turn on the radio, the "RX" LED on the board lights up and continues to glow until I turn off the head unit.

I would be very grateful if someone would help. :slight_smile:
For almost a month now I can’t solve this problem and has come to a standstill.