I was tired connecting my cellphone with an old cassette adapter to my VW Golf with an Beta 5 in it. I needed something new, something cool. And here it is, a fully functional interface between the Beta 5 and any device you want to play the music with.

This project raised out of an ebay search: I just wanted to know if there is a solution to get an AUX input on the car radio. Yes there is, even complete mp3 players are available. But that’s not what I wanted – at first!

Then I started digging the web for more info about the protocol the radio talks to cd changers. The idea was to build a tiny thingy that fakes a cd changer and simply enables the AUX input (as it is available in several online stores). But during the development my ambition became greater and I wanted to read the keys pressed on the radio to remote control my RPi.

1. Understanding the Protocol

First of all this is the pinout of the radio: rkieslinger.de/steckerbelegungen/vag-stecker.htm The interesting cell is no. 3, the blue one. DATA IN simply is MOSI of an 8bit SPI interface with special timings between the bytes CLOCK is SCK of the SPI DATA OUT is the key code of the pressed key on the radio itself

The radio needs a sequence of bytes to enable the AUX input and display CD# and TR#. It looks likes this:

frame cd#   tr#   time  time  mode  frame frame
0x34, 0xBE, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0x3C

cd# and tr# are sent inverted. So this sequence will display: CD1 TR00 mode sets the playmode (PLAY|SHFFL|SCAN) As the beta doesn’t support time display, I will ignore these bytes.

If cd mode is switched on/off or keys are pressed, 4 byte messages will be sent from the radio. They are coded like RC5 messages. Each message has a startsequence. A low byte has a short low phase, a high byte a longer low phase. Everything you need to know can be found in the pictures here: martinsuniverse.de/projekte/cdc_protokoll/cdc_protokoll.html

2. Remote commands from the radio

As you know 4 byte messages are sent. The following pattern describes the control codes:

head head cmd  !cmd
0x53 0x2C 0xAA 0x55

Here is the complete list of all bytes from all keys:

switch on in cd mode/radio to cd (play)
0x53 0x2C 0xE4 0x1B
0x53 0x2C 0x14 0xEB

switch off in cd mode/cd to radio (pause)
0x53 0x2C 0x10 0xEF
0x53 0x2C 0x14 0xEB

next
0x53 0x2C 0xF8 0x7

prev
0x53 0x2C 0x78 0x87

seek next
0x53 0x2C 0xD8 0x27 hold down
0x53 0x2C 0xE4 0x1B release
0x53 0x2C 0x14 0xEB

seek prev
0x53 0x2C 0x58 0xA7 hold down
0x53 0x2C 0xE4 0x1B release
0x53 0x2C 0x14 0xEB

cd 1
0x53 0x2C 0x0C 0xF3
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

cd 2
0x53 0x2C 0x8C 0x73
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

cd 3
0x53 0x2C 0x4C 0xB3
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

cd 4
0x53 0x2C 0xCC 0x33
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

cd 5
0x53 0x2C 0x2C 0xD3
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

cd 6
0x53 0x2C 0xAC 0x53
0x53 0x2C 0x14 0xEB
0x53 0x2C 0x38 0xC7
send new cd no. to confirm change, else:
0x53 0x2C 0xE4 0x1B beep, no cd (same as play)
0x53 0x2C 0x14 0xEB

scan (in 'play', 'shffl' or 'scan' mode)
0x53 0x2C 0xA0 0x5F

shuffle in 'play' mode
0x53 0x2C 0x60 0x9F

shuffle in 'shffl' mode
0x53 0x2C 0x08 0xF7
0x53 0x2C 0x14 0xEB

The micro controller will only send the 3rd byte after error check over uart. See below.

3. The Hardware I picked

  • Atmel ATmega32A
  • 1117 Regulator (maybe not the best, it’s heating up at >50mA due to the high drop voltage)
  • FTDI FT230X as uart usb bridge
  • ISO connector to fit into the radio

4. Programming the AVR

First thing about the uart communication. I use the library of Peter Fleury: homepage.hispeed.ch/peterfleury

To be independent from clockrates I utilized delay loops instead of timers to create correct timings. Hopefully my comments in the code explains everything very well.

/*
 * VAG_CDC.c
 *
 * Created: 23.06.2013 20:00:51
 *  Author: Dennis Schuett, dev.shyd.de
 */

#define F_CPU 8000000UL

#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#include "uart.h"

#define UART_BAUD_RATE 9600
#define LED_PWR PA0
#define RADIO_OUT PD2
#define FT_CBUS1 PD3
#define RADIO_OUT_IS_HIGH (PIND & (1<<RADIO_OUT))

#define CDC_PREFIX1 0x53
#define CDC_PREFIX2 0x2C

#define CDC_END_CMD 0x14
#define CDC_PLAY 0xE4
#define CDC_STOP 0x10
#define CDC_NEXT 0xF8
#define CDC_PREV 0x78
#define CDC_SEEK_FWD 0xD8
#define CDC_SEEK_RWD 0x58
#define CDC_CD1 0x0C
#define CDC_CD2 0x8C
#define CDC_CD3 0x4C
#define CDC_CD4 0xCC
#define CDC_CD5 0x2C
#define CDC_CD6 0xAC
#define CDC_SCAN 0xA0
#define CDC_SFL 0x60
#define CDC_PLAY_NORMAL 0x08

#define MODE_PLAY 0xFF
#define MODE_SHFFL 0x55
#define MODE_SCAN 0x00

uint16_t captimehi = 0;
uint16_t captimelo = 0;
uint8_t capturingstart = 0;
uint8_t capturingbytes = 0;
uint32_t cmd = 0;
uint8_t cmdbit = 0;
uint8_t newcmd = 0;
uint8_t shutdownpending = 0;

uint8_t getCommand(uint32_t cmd);
void shutdown();
void softreset();

volatile uint8_t cd;
volatile uint8_t tr;
volatile uint8_t mode;

ISR(INT0_vect) //remote signals
{
	if(RADIO_OUT_IS_HIGH)
	{
		if (capturingstart || capturingbytes)
		{
			captimelo = TCNT1;
		}
		else
			capturingstart = 1;
		TCNT1 = 0;

		//eval times
		if (captimehi > 8300 && captimelo > 3500)
		{
			capturingstart = 0;
			capturingbytes = 1;
			//uart_puts("startseq found\r\n");
		}
		else if(capturingbytes && captimelo > 1500)
		{
			//uart_puts("bit 1\r\n");
			cmd = (cmd<<1) | 0x00000001;
			cmdbit++;
		}
		else if (capturingbytes && captimelo > 500)
		{
			//uart_puts("bit 0\r\n");
			cmd = (cmd<<1);
			cmdbit++;
		}
		else
		{
			//uart_puts("nothing found\r\n");
		}
		if(cmdbit == 32)
		{
			//uart_puts("new cmd\r\n");
			newcmd = 1;
			cmdbit = 0;
			capturingbytes = 0;
		}
	}
	else
	{
		captimehi = TCNT1;
		TCNT1 = 0;
	}
}

ISR(INT1_vect) //ft230x cbus1
{
	if (PIND & (1<<FT_CBUS1)) //reset radio display to 'PLAY' CD01 TR00
	{
		//PORTA &= ~(1<<LED_PWR);
		cd = 0xBE;
		tr = 0xFF;
		mode = 0xFF;
	}
	else //usb connect
	{
		PORTA |= (1<<LED_PWR);
		_delay_ms(50);
		PORTA &= ~(1<<LED_PWR);
		cd = 0xB0;
		tr = 0x00;
	}
}

uint8_t spi_xmit(uint8_t val) {
	SPDR = val;
	while(!(SPSR & (1<<SPIF)));
	return SPDR;
}

void send_package(uint8_t c0, uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4, uint8_t c5, uint8_t c6, uint8_t c7)
{
	spi_xmit(c0);
	_delay_us(874);
	spi_xmit(c1);
	_delay_us(874);
	spi_xmit(c2);
	_delay_us(874);
	spi_xmit(c3);
	_delay_us(874);
	spi_xmit(c4);
	_delay_us(874);
	spi_xmit(c5);
	_delay_us(874);
	spi_xmit(c6);
	_delay_us(874);
	spi_xmit(c7);
}

int main(void)
{
	cd = 0xBE;
	tr = 0xFF;
	mode = 0xFF;
	//LEDs
	DDRA |= (1<<LED_PWR);

	//pullup
	PORTD |= (1<<FT_CBUS1);

	uart_init(UART_BAUD_SELECT(UART_BAUD_RATE, F_CPU));

	//init SPI
	DDRB |= (1<<PB5) | (1<<PB7)| (1<<DDB4);

	// SPI Type: Master
	// SPI Clock Rate: 62,500 kHz
	// SPI Clock Phase: Cycle Start
	// SPI Clock Polarity: Low
	// SPI Data Order: MSB First
	SPCR=0x57;//at 8MHz
	//SPCR=0x56;//at 4MHz
	SPSR=0x00;

	//beta commands -> cdc
	TCCR1B |= (1<<CS11);    // no prescaler 8 -> 1 timer clock tick is 1us long
	GICR |= (1<<INT0) | (1<<INT1);
	MCUCR |= (1<<ISC00) | (1<<ISC10); //any change on INT0 and INT1
	sei();

	//init led on
	PORTA |= (1<<LED_PWR);
	_delay_ms(500);
	//uart_puts("VAG_CDC ready...\r\n");
	uart_putc(0xAA);
	uart_putc(0x55);
	PORTA &= ~(1<<LED_PWR);

	send_package(0x74,0xBE,0xFE,0xFF,0xFF,0xFF,0x8F,0x7C); //idle
	_delay_ms(10);
	send_package(0x34,0xFF,0xFE,0xFE,0xFE,0xFF,0xFA,0x3C); //load disc
	_delay_ms(100);
	send_package(0x74,0xBE,0xFE,0xFF,0xFF,0xFF,0x8F,0x7C); //idle
	_delay_ms(10);

    while(1)
    {
		int r = uart_getc();
		//r has new data
		if(r <= 0xFF)
		{
			//send inverted CD No.
			if((r & 0xC0) == 0xC0)
			{
				if (r == 0xCA)
					mode = MODE_SCAN;
				else if (r == 0xCB)
					mode = MODE_SHFFL;
				else if (r == 0xCC)
					mode = MODE_PLAY;
				else
					cd = 0xFF^(r & 0x0F);
			}
			//send inverted TR No.
			else
				tr = 0xFF^r;
		}
		//                disc  trk  min  sec
		//send_package(0x34,cd,tr,0xFF,0xFF,0xFF,0xCF,0x3C);
		send_package(0x34,cd,tr,0xFF,0xFF,mode,0xCF,0x3C);
        _delay_ms(41);
		if (newcmd)
		{
			newcmd = 0;
			uint8_t c = getCommand(cmd);
			if (c)
			{
				uart_putc(c);
				/*if (c == CDC_STOP)
				{
					shutdownpending = 1;
				}
				else if (shutdownpending && c == CDC_END_CMD)
				{
					shutdownpending = 0;

					PORTA &= ~(1<<LED_PWR);
					_delay_ms(100);
					shutdown();
					PORTA |= (1<<LED_PWR);
				}*/
			}
		}
    }
}

uint8_t getCommand(uint32_t cmd)
{
	if (((cmd>>24) & 0xFF) == CDC_PREFIX1 && ((cmd>>16) & 0xFF) == CDC_PREFIX2)
		if (((cmd>>8) & 0xFF) == (0xFF^((cmd) & 0xFF)))
			return (cmd>>8) & 0xFF;
	return 0;
}

Additional info can be found here: <www.mikrocontroller.net/topic/28549> (in German)

5. The Circuit

Nothing special here, just an ISP header for programming the AVR, USB connector, voltage regulator, LED,… For feature circuits in the glovebox I provided 12V, 5V and GND through further pins. I was thinking of powering the RPi directly from my faker. But I don’t know if the radio is able to provide around 400mAmps. Even the regulator would heat up like hell at these currents.

The FT230X is programmed as selfpowered and CBUS1 as output for the #POWER_ON singnal.

6. Bringing all together: MPD and RPi

Now we need a wrapper for mpc to let mpd act as our cd changer. I’ve written a python script to evaluate the radio commands and send cd and track numbers back periodically. The script starts at startup by the rc.local and checks for the connected faker. The last listened cd no. will be stored in a file to send it to the faker if something is restarted or reconnected. I let the script stop mpd to save power and make sure the current settings are synced with the filesystem, because a powerfail will cause a corrupted playlist. The music is stored in /var/vag_cdc/cdX where X is the cd no. 1 to 6. You can put up to 99 tracks in each folder due to radio display limitations.

#!/usr/bin/python

import os
import serial
import string
import time

# bytes sent when radio keys are pressed
CDC_END_CMD = chr(0x14)
CDC_PLAY = chr(0xE4)
CDC_STOP = chr(0x10)
CDC_NEXT = chr(0xF8)
CDC_PREV = chr(0x78)
CDC_SEEK_FWD = chr(0xD8)
CDC_SEEK_RWD = chr(0x58)
CDC_CD1 = chr(0x0C)
CDC_CD2 = chr(0x8C)
CDC_CD3 = chr(0x4C)
CDC_CD4 = chr(0xCC)
CDC_CD5 = chr(0x2C)
CDC_CD6 = chr(0xAC)
CDC_CDSET = chr(0x38)
CDC_SCAN = chr(0xA0)
CDC_SFL = chr(0x60)
CDC_PLAY_NORMAL = chr(0x08)

# control bytes to set the display
CMD_SCAN = chr(0xCA)
CMD_SHFFL = chr(0xCB)
CMD_PLAY = chr(0xCC)

CD_MASK = 0xC0

cd_filename = "/var/vag_cdc/last_cd"
global ser
ser = None
timeout_seek = 0.5
timeout_normal = 2

# send current cd no. to radio and save it for next reboot
def set_cd_no(val):
	ser.write(val)
	fo = open(cd_filename, "wb")
	fo.write(val)
	fo.close()

# load new cd-dir
def play_cd(cd_no):
	if ser.read() == CDC_END_CMD and ser.read() == CDC_CDSET:
		r = os.popen("mpc ls cd" + str(cd_no)).read()
		if r != "":
			set_cd_no(chr(CD_MASK + cd_no))
			os.popen("mpc clear")
			os.popen("mpc ls cd" + str(cd_no) + " | mpc add")
			os.popen("mpc play")

# init serial and last cd no. for correct radio display
def connect():
	while(1):
		try:
			global ser
			ser = serial.Serial('/dev/ttyUSB0', 9600)
			ser.timeout = timeout_normal #we need a timeout to update the track no.
			ser.writeTimeout = timeout_normal
			try:
				fo = open(cd_filename, "rb")
				cd = fo.read(1)
				fo.close()
				ser.write(cd)
			except:
				print "no last cd no. [not sending cd#]"
			return
		except:
			time.sleep(2) #wait before retrying

try:
	os.popen("/etc/init.d/mpd restart") #restart mpd due to alsa-equalizer
	connect()

	# read cmds from the radio and act like a cd changer
	while(1):
		try:
			c = ser.read()

			if c == CDC_PLAY:
				if ser.read() == CDC_END_CMD:
					os.popen("/etc/init.d/mpd start")
					os.popen("mpc play")
					os.popen("mpc repeat on")

			elif c == CDC_STOP:
				if ser.read() == CDC_END_CMD:
					#os.popen("mpc pause")
					# stop service to sync current status
					os.popen("/etc/init.d/mpd stop")

			elif c == CDC_NEXT:
				os.popen("mpc next")

			elif c == CDC_PREV:
				os.popen("mpc prev")

			elif c == CDC_SEEK_FWD:
				ser.timeout = timeout_seek
				while(1):
					os.popen("mpc seek +00:00:10")
					if ser.read() == CDC_PLAY and ser.read() == CDC_END_CMD:
						break
				ser.timeout = timeout_normal

			elif c == CDC_SEEK_RWD:
				ser.timeout = timeout_seek
				while(1):
					os.popen("mpc seek -00:00:10")
					if ser.read() == CDC_PLAY and ser.read() == CDC_END_CMD:
						break
				ser.timeout = timeout_normal

			elif c == CDC_CD1:
				play_cd(1)

			elif c == CDC_CD2:
				play_cd(2)

			elif c == CDC_CD3:
				play_cd(3)

			elif c == CDC_CD4:
				play_cd(4)

			elif c == CDC_CD5:
				play_cd(5)

			elif c == CDC_CD6:
				play_cd(6)

			elif c == CDC_SCAN:
				os.popen("mpc update")

			elif c == CDC_SFL:
				os.popen("mpc random on")

			elif c == CDC_PLAY_NORMAL:
				if ser.read() == CDC_END_CMD:
					os.popen("mpc random off")

			#get current track no. (radio display will be delayed up to {timeout_normal})
			mpc = os.popen("mpc |grep ] #").read()
			try:
				mpc = mpc.split("/", 1)
				mpc = mpc[0].split("#", 1)
				tr = chr(string.atoi(mpc[1], 16))
				ser.write(tr)
			except:
				print "not playing"

			#get current playmode
			mpc = os.popen("mpc | grep random:").read()
			try:
				mpc = mpc.split("random: ", 1)
				if mpc[1].startswith("on"):
					ser.write(CMD_SHFFL)
				elif mpc[1].startswith("off"):
					ser.write(CMD_PLAY)
			except:
				print "not playing"
		except (serial.SerialException, serial.SerialTimeoutException):
			print "serial port unavailable, reconnecting..."
			ser.close()
			connect()
		except:
			raise
except (KeyboardInterrupt):
	if ser is not None:
		ser.close()
		print "port closed!"
	print "KeyboardInterrupt detected, exiting..."

7. Some further ideas

The project is mostly complete - for now. But there are some things I have in mind that could be quite nice to have. It would be awesome if it had bluetooth to remote control my android phone. Or mute the radio at incoming calls and act as a hands free set. But what I’ve done lately is preparing the RPi with Wifi, so it connects automatically to my Wifi, when parking in the garage to sync the music folders with rsync.