Introduction: Custom Arduino to Keep CAN Steering Wheel Buttons With New Car Stereo

I decided to replace the original car stereo in my Volvo V70 -02 with a new stereo so I will be able to enjoy stuff like mp3, bluetooth and handsfree.

My car has some steering wheel controls for the stereo that I would like to still be able to use. I didn't expect that to be a problem because there are several adapters on the market that are supposed to be compatible with my car. However I soon found out that they were not! (It seems like the adapters for V70 might have problems with -02 cars due to a slightly different CAN protocol.)

So what to do then? Keep the old stereo? Live a life with non-working buttons? Of course not! If there is no working adapter on the market then we will have to build one!

This instructable can be applied (with some adaptations) to cars where the steering wheel buttons communicates over the CAN bus.

Step 1: Find Out How to Send Commands to the Stereo

The first thing you should do is to find out what type of remote input the stereo expects. Typically the manufacturers will not tell you that and you probably don't have access to working remote controls for reverse engineering either.

The remote in for my new stereo (Kenwood) consists of a single wire and I have not been able to find out any information about how it works. However it has also a 3.5 mm jack for remote input. I couldn't find out anything about that either. But there are some information about a 3.5 mm jack for other brands suggesting that different commands are identified by applying a specific resistance between tip and sleeve (and optionally between ring and sleeve). E.g. https://forum.arduino.cc/index.php?topic=230068.0. So I decided to give that a try, equipped with a breadboard, a bunch of resistors and a 3.5 mm plug plugged in to the stereo and connected to the breadboard. Nothing was recognized at first, but the stereo has a "learning mode" menu and there the commands could be successfully set up while applying various resistance. Success!

However I later found out that I made a mistake here: Not all of the commands that the stereo seemed to learn would actually work. E.g. 30 kOhm was found in learning mode but did not work later and for some of the commands I set up the resistance difference was so small that later the wrong command was triggered.

So I recommend that you use a breadboard with resistors and switch buttons for all the remote commands that you want to handle and actually test that all of them will work.

If your car stereo can not receive input in the same way then you will have to figure out how it works so you can adapt this solution. If you can't figure out at all then you have a problem.

Step 2: Find Out Where to Connect to the CAN Bus

You need to locate a good place to connect to the CAN bus. Since you are replacing an old stereo that communicates over CAN you should be able to find that behind the stereo. The CAN bus consists of a pair of twisted wires (CAN-L and CAN_H). Consult a wiring diagram for your car to be sure.

Step 3: Reverse Engineering of CAN Messages

Unless Google can tell you what CAN messages you should listen for then you will need to connect to the CAN bus and do some reverse engineering. I used an Arduino Uno and a CAN shield. (You don't really need the CAN shield, as you will see later you can use some cheap components on a breadboard instead.)

Consult Google to find out what baud rate you should use when
connecting to your car. (Typically you will find that there is a high speed and a low speed CAN net. You are connecting to the low speed net.)

You will also need to program the Arduino to log all CAN messages over the serial interface so you can save them to a log file on your computer. The standard Arduino IDE will not save data to a log file but you can use e.g. Putty instead.

Before you start writing your program you need to install the CAN_BUS_Shield library.

Here are some pseudo code to help you getting started with your program:

setup() 
{
  init serial connection
  init CAN library
}


loop()
{
  if CAN message is received
  {
    read CAN message
    format log entry
    write log entry to serial
  }
}

Hints:

You will use an instance of class MCP_CAN to access the CAN library functionality:

MCP_CAN m_can;

Init CAN:

while (m_can.begin(<correct baud rate for your car here>) != CAN_OK)
{
  delay(1000);
}

Check for and read CAN messages:

while(m_can.checkReceive() == CAN_MSGAVAIL)
{
  // Get CAN id, message length and message data
  m_can.readMsgBufID(&m_canId, &m_msgLen, m_msgBuf);
  // Do something with the message data here
}

If you need more help you can find a link to my program in a later step. The CAN shield library also includes an example. Or check mviljoen2's instructable that includes a similar step.

First you will need a reference file to help you filter out data. Switch ignition to radio mode and log everything for a couple of minutes without touching any buttons.

Then for each of the buttons, start logging, push the button and stop logging.

When you are done you need to filter out everything that is in your reference log from your button logs to find your candidates. I found out that there was still a lot of messages left so I made more logs and then required that "candidates for command A must be in all button-A-files and in none of the reference files". That left me with only a few possibilities to try.

The logs will contain a lot of messages so you will need to write some program for this or possibly use Excel. (I used a program with very hard coded conditions for my needs so I'm afraid I can't offer a program you can use.)

A word of warning: You can not be sure that a button will always produce an identical message. Some of the bits might contain incrementing counters etc. (You can except the message id to be the same however.)

If you happen to have a Volvo V70 -02 this is what you are after:

  • Message id: 0x0400066
    Byte0: 0x00, 0x40, 0x80 or 0xc0 (don't care)
  • Byte1: 0x00 (don't care)
  • Byte2: 0x00 (don't care)
  • Byte3: 0x00-0x07 (don't care)
  • Byte4: 0x1f (don't care)
  • Byte5: 0x40 (don't care)
  • Byte6: 0x40 (don't care)
  • Byte7: Button identifier: 0x77 = volume up, 0x7b = volume down, 0x7d = next track, 0x7e = previous track.

When you believe that you have found the commands it might be a good idea to modify the program so that it only logs the interesting messages. Look at the serial log window while you press the buttons to verify that you have identified the correct messages.

Step 4: The Hardware Prototype

Your hardware needs to be able to:

  1. Identify commands received on the CAN bus
  2. Send commands in another format to the stereo

If you have enough space you can use an Arduino and a CAN shield for the first part and attach some additional hardware for the second. However there are some drawbacks:

  • Cost of the CAN shield
  • Size
  • The Arduino power supply will not be happy if it is connected directly to your cars 12V (it will probably work but it's life will likely be shortened).

So instead I used the following:

  • Atmega 328, the "Arduino brain". (There are some variants, get the one that is equal to the one on Arduino Uno. You can buy it with or without Arduino boot loader.)
  • 16 MHz crystal + capacitors for clock signal.
  • MCP2551 CAN transceiver.
  • MCP2515 CAN controller.
  • TSR1-2450, converts 6.5-36V to 5V. (Not used in the prototype because the software will not care about the power supply.)
  • CD4066B switch that will be used when sending commands to the stereo.
  • A couple of resistors. (The values can be found in the Eagle schematics in a later step.)

A good thing with this configuration is that it is fully compatible with the Arduino and the CAN shield library.

If you want to handle more than four buttons you might want to consider to use something else than the CD4066B. The CD4066B can be described as four switches in one, each controlled by one of the Atmegas GPIO pins. To each switch there is a resistor connected that can be used to control the resistance used as input to the stereo. So this can easily be used to send four different commands. If they are combined then additional resistance values can be obtained. This is where the mistake I mentioned earlier comes in. I have four buttons, but I planned to implement two of them as long and short press to give me six different commands. But in the end I found out that I could not find a combination of resistors that would give me six working combinations. It would probably be possible to connect an analog out signal to the stereo (3.5 mm tip) instead. (Note that the Atmega has no true analog out pins so some additional hardware would be required.)

For testing purposes I also made a simple "car and stereo" simulator to connect to my prototype. It makes debugging easier and unless you enjoy to sit in your car and program I can recommend that.

The prototype is illustrated by the bottom breadboard in the image. For power supply, programming and serial logging it is attached to an Arduino Uno where the Atmega chip has been removed.

The upper breadboard is the car + stereo simulator that will be used for initial testing of the prototype.

The prototype + simulator is intended to work like this:

  • Press one of the switch buttons on the simulator board. (Those are your steering wheel buttons.)
  • When the simulator program detects a button press it will send the corresponding CAN message each 70 ms as long as the button is pushed. (Because the logs I took earlier indicated that is how it is working in my car.) It will also send lots of "junk" CAN messages to simulate other traffic on the bus.
  • CAN messages are sent on the CAN bus.
  • CAN messages are received by the prototype.
  • The MCP2515 throws all unrelated messages based on the message id.
  • When the MCP2515 receives a message that should be handled it will indicate that it has a message enqueued.
  • The Atmega will read the message and decide which button that should be considered active.
  • The Atmega will also keep track on when the last message was received, after a certain time the button will be considered released. (The CAN messages only indicates that a button is down, not that it has become pushed or released.)
  • If a button is considered active then one or more switches in the CD4066B will be activated.
  • The simulator (now acting as your stereo) will detect that a resistance is applied between tip and sleeve. (The tip is connected to 3.3V and through a resistor to an analog input pin. When no command is active this pin will read 3.3V, when a command is active the value will become lower and identify the command.
  • While a command is active the corresponding led will also be activated. (There are six leds because I planned to use different long / short press for two of my buttons.)

For more details about the prototype hardware, see Eagle schematics in a later step.

Additional details about the simulator board hardware:

(Or you can use the CAN shield for the "CAN part" of the simulator if you prefer.)

It is important that you know how the Atmega pins are mapped to Arduino pins when you design the hardware.

(Do not connect any leds directly to the CD 4066B, it can only handle a low current. I tried that when I first tested the output and the chip become useless. The good thing is that I had bought a couple of them just because they are so cheap.)

Step 5: Fuse Programming

Maybe you noticed in the previous step that the prototype has no separate components to generate the clock signal to the MCP2515. That is because there is already a 16 MHz crystal used as the Atmega clock signal that we can use. But we cannot simply connect it directly to the MCP2515 and by default there is no clock out signal on the Atmega.

(If you prefer you can skip this step and add the extra clock hardware instead.)

However we can use something called "fuse programming" to enable a clock out signal on one of the GPIO pins.

First you will need to locate a file named "boards.txt" used by your Arduino IDE. You will need to copy the entry for Arduino Uno, give it a new name and change the value for low_fuses.

My new board looks like this:

##############################################################<br>
# Based on Arduino Uno
# Changes:
# low_fuses changed from 0xff to 0xbf to enable 16 MHz clock
# out on Atmega PB0/pin 14 = Arduino D8
clkuno.name=Clock out (Arduino Uno)
clkuno.upload.protocol=arduino
clkuno.upload.maximum_size=32256
clkuno.upload.speed=115200
clkuno.bootloader.low_fuses=0xbf
clkuno.bootloader.high_fuses=0xde
clkuno.bootloader.extended_fuses=0xfd
clkuno.bootloader.path=optiboot
clkuno.bootloader.file=optiboot_atmega328.hex
clkuno.bootloader.unlock_bits=0xff
clkuno.bootloader.lock_bits=0xcf
clkuno.build.mcu=atmega328p
clkuno.build.f_cpu=16000000L
clkuno.build.core=arduino
clkuno.build.variant=standard
##############################################################

Note that the clock out is activated by setting its control bit to 0.

When you have created the new board in the boards configuration file you will have to burn a new boot loader to the Atmega. There are various ways to do this, I used the method described in https://www.arduino.cc/en/Tutorial/ArduinoToBreadboard.

After you have done this, remember to select your new board type and not the Arduino Uno when you upload a program to the Atmega.

Step 6: The Software

Time to make the dumb hardware smart by adding some software.

Here are some pseudo code for the prototype:

lastReceivedTime = 0
lastReceivedCmd = none
cmdTimeout = 100

setup()
{
  enable watchdog
  configure pins D4-D7 as output pins
  init CAN
  setup CAN filter
}

loop()
{
  reset watchdog
  if (CAN message is received)
  {
    for each button command
    {
      if CAN message belongs to the button command
      {
        lastReceivedTime = now
        lastReceivedCmd = cmd
      }
    }
  }
  if now > lastReceivedTime + cmdTimeout
  {
    lastReceivedCmd = none
  }
  for each button command
  {
    if lastReceivedCmd is button command
    {
      set command pin output = on
    }
    else
    {
      set command pin output = off
    }
  }
}

cmdTimeout decides how long we should wait before we consider the last active button released. Because button CAN message commands are sent approximately each 70 ms it needs to be larger than that with some margin. But if it is to large there will be a lag experience. So 100 ms seems like a good candidate.

But what is a watchdog? It is a useful little hardware feature that can save us in case of a crash. Imagine that we have a bug causing the program to crash while the volume up command is active. Then we would end up with the stereo on max volume! But if the watchdog is not reset for the specific time it will decide that something unexpected has happened and simply issue a reset.

void setup()
{    
  // allow max 250 ms for the loop
  wdt_enable(WDTO_250MS);
  // other init stuff
}

void loop() 
{  
  wdt_reset();
  // do stuff
}

CAN filter? Well, you can configure the CAN controller to discard all messages that does not match the filter so the software does not have to waste time on messages we do not care about.

unsigned long mask = 0x1fffffff; // Include all 29 header bits in the mask
unsigned long filterId = 0x0400066; // We only care about this CAN message id

m_can.init_Mask(0, CAN_EXTID, mask); // Mask 0 applies to filter 0-1
m_can.init_Mask(1, CAN_EXTID, mask); // Mask 1 applies to filter 2-5
 
m_can.init_Filt(0, CAN_EXTID, filterId);
m_can.init_Filt(1, CAN_EXTID, filterId);
m_can.init_Filt(2, CAN_EXTID, filterId);
m_can.init_Filt(3, CAN_EXTID, filterId);
m_can.init_Filt(4, CAN_EXTID, filterId);
m_can.init_Filt(5, CAN_EXTID, filterId);

Check the CAN library code and the CAN controller documentation for more details about how to set up filter + mask.

You can also set up the CAN controller to raise an interrupt when a message (that is not filtered out) is received. (Not included in the example above but there is some code for it in my program.) In this case it doesn't really add any value and it might be confusing if you are not used to programming.

So that was the prototype software in summary. But we need some code for the simulator board as well:


lastSentTime = 0
minDelayTime = 70

setup()
{
  configure pins A0-A5 as output pins
  configure pins D4-D7 as input pins with internal pullup.
  init CAN
}

loop()
{
  send "junk" can msg
  set activeButton = none
  for each button
  {
    if button is pushed
    {
      set activeButton = button
    }
  }
  if activeButton != none
  {
    if now > lastSentTime + minDelayTime
    {
      send button command can message
    }
    set lastSentTime = now
  }
  inval = read pin A7
  foreach(cmd)
  {
    if (min < inval < max)
    {
      led on
    }
    else
    {
      led off
    }
  }
  wait for 1 ms
}

This will continuously send "junk" CAN messages approximately every ms and while a button is pushed the corresponding command each 70 ms.

You might need to log the input on pin A7 while pressing the different buttons to find out suitable values for the min and max variables belonging to each button. (Or you can calculate it, but actually reading the input will give you more precise values.)

You need to be a little careful when you are programming the pin modes. If you accidentally set the pins intended to use internal pullup as output pins instead then you will create a potential shortcut that will damage your Arduino when you set the output high.

If you want to check my programs they can be downloaded here:

You should be aware that those programs doesn't really match the pseudo code here, they contains a lot of "extra" stuff that is not really needed and if you are not familiar with object oriented programming it can probably be a little difficult to read.

Step 7: The Final Hardware

When you are happy with your program (remember to test the prototype in the car after final testing with the simulator board) it is time to construct the real hardware.

You have three options here:

  • Quick and dirty - solder the stuff together on a PCB prototype board.
  • Hardcore DIY - etch your own PCB.
  • The lazy way - order a professional PCB to solder the components on.

If you are not in a hurry I can recommend the last option. If you only need a small PCB like this it is very cheap to order it from China. (And then you will probably get ten pieces or so so you can afford some soldering mistakes.)

To order PCBs you will need to send your design in Gerber format. There are various software for this. I used Eagle which I can recommend. You can expect a few hours to learn it but then it works fine. For small boards like this you can use it for free.

Be careful when you make the design. You don't want to wait four weeks for delivery just to find out that you did something wrong.

(If you have good soldering skills you can design for surface mounted components and get a really small adapter. I did not.)

Then order at e.g. https://www.seeedstudio.com/fusion_pcb.html. Follow the instructions for how to generate the Gerber files from your design. You can also get a preview of the result to ensure that it is ok.

(In the end I had to select other resistors for R4-R7 than listed in the schematics picture. Instead I used 2k, 4.7k, 6.8k and 14.7k.)

And remember - do not confuse the Atmega pin numbering with the Arduino pin numbering!

I recommend that you do not solder the Atmega chip directly but use a socket. Then you can easily remove it in case you need to reprogram it.

Step 8: Car Mounting

Now to the most fun part - mount it in your car and start using it! (After you have made / bought a case for it.)

If you have already fully tested the prototype in your car everything should work perfectly.

(As I mentioned earlier I did not so I had to replace some resistors and do some changes in my program.)

Also consider if you should mount it behind the stereo or somewhere else. I found a good place above my glove box where I can reach it from inside the glove box without taking anything apart. That might be useful if I decide to upgrade it later.

Finally my buttons are working again! How could I survive for two months without them?

Step 9: Future Improvements

As mentioned, if I make a version 2.0 of this I will replace the 4066B with something else (probably a digital potentiometer) for greater flexibility.

There are also a lot of other things you can do. E.g. add a bluetooth module and make a remote control app for your phone. Or a gps module, then when you are close to home you can automatically raise volume and send the "windows down" CAN message so that all your neighbors can enjoy your wonderful music.

Arduino Contest 2017

Participated in the
Arduino Contest 2017

First Time Author Contest 2018

Participated in the
First Time Author Contest 2018