*** Project: Arduino & BMW K/I-Bus Interface ***
** Intro located here ***
The examples below are from version 0.0.1-alpha. This is an older version of the code, and requires and outdated Arduino IDE, as well as some modifications to the IDE core files. I suggest looking at the latest versions (which do not require core modifications and can use the latest Arduino IDE), available: here.
All of the Arduino I/K-Bus code is available on my BitBucket page, as well as Google Drive (embedded below).
Like I mentioned earlier, the coding was mostly done by ian332isport, and only slightly modified for projects’ my purposes.
Most of the code is fairly well commented, but I will provide a general description here anyway.
There are two slight modifications that need to be made to the Arduino core files in order for this branch of code to work properly.
The files are “HardwareSerial.cpp” and “HardwareSerial.h”, located in the following directory:
“C:\Program Files (x86)\Arduino\hardware\arduino\cores\arduino”
This is due to the modification of function “Serial.peekn(n)”. Both of the modified files are included on my BitBucket page.
The Arduino IDE version that must be used is version 1.0.5 – this is due to modifying the two core files mentioned above.
I have not checked if these files have changed in any versions after 1.0.5.
⚠ Module cannot be rendered as the requested content is not (longer) accessible. Contact the administrator to get access.
Code description:
• E46_KBus.ino
This is the main program file (The one that must be opened with the Arduino IDE). The first section below is simply setting up variables we will use throughout the code.
E46_KBus.ino
// v0.0.1-alpha branch
#include <SoftwareSerial.h>
// Setting up our variables
volatile boolean clearToSend = false;
boolean goodPacket; //boolean value set true if good IBUS message detected
int ibusEvent = 0;
int EventID = 0; // Event number (for case select)
byte ibusByte[40]; // Byte array to store incoming messages.. Max message length per BMW design should be 32, plus the source and length bytes, makes a total of 34. Sticking to 40 for safety - don't want to overrun the array.
byte inByte[40];
byte source;
byte length;
byte destination;
byte databytes[36]; // Byte array to store message data bytes. ibusByte array of 40 - 4 for (Source, length, destination, checksumByte) = 36
byte checksumByte;
byte outgoingMsg[32];
int outgoingSize;
const int EN = 4; // HIGH enables Melexix TH3122 chip
const int senSta = 3; // SEN/STA output from Melexix TH3122 - Needs to be on pin 3 for interrupt 1 to work (on UNO/NANO - probably different on Mega)
const byte packetGap = 10; // Length of gap between messages we send
unsigned long currentTime;
unsigned long packetTimer;
unsigned long ibusSleepTimer;
// Debug - Used for sending debug messages to terminal window.
SoftwareSerial mySerial(7, 8);
// 7 is Rx, 8 is Tx
// All these messages are either compared with incoming messages, or can be sent out onto the bus. Add or
// remove as required, but you need to make the necessary changes further down as well.
byte SEND_MESSAGE[32] = {
0xC8 , 0x00 , 0x80 , 0x23 , 0x42 , 0x32 };
byte KEY_IN [7] PROGMEM = {
0x44 , 0x05 , 0xBF , 0x74 , 0x04 , 0x00 , 0x8E }; // Ignition key in
byte KEY_OUT [7] PROGMEM = {
0x44 , 0x05 , 0xBF , 0x74 , 0x00 , 0xFF , 0x75 }; // Ignition key out
byte CD_STOP [7] PROGMEM = {
0x68 , 0x05 , 0x18 , 0x38 , 0x01 , 0x00 , 0x4C }; // CD Stop command
byte CD_PLAY [7] PROGMEM = {
0x68 , 0x05 , 0x18 , 0x38 , 0x03 , 0x00 , 0x4E }; // CD Play command
byte CD_PAUSE [7] PROGMEM = {
0x68 , 0x05 , 0x18 , 0x38 , 0x02 , 0x00 , 0x4F }; // CD Pause command
byte MFL_VOL_UP [6] PROGMEM = {
0x50 , 0x04 , 0x68 , 0x32, 0x11 , 0x1F }; // Steering wheel Volume Up
byte MFL_VOL_DOWN [6] PROGMEM = {
0x50 , 0x04 , 0x68 , 0x32, 0x10 , 0x1E }; // Steering wheel Volume Down
byte MFL_RT [5] PROGMEM = {
0x50 , 0x03 , 0xC8 , 0x01, 0x9A }; // Steering wheel R/T
byte DSP_VOL_UP_1 [6] PROGMEM = {
0x68 , 0x04 , 0x6A , 0x32, 0x11 , 0x25 }; // Rotary Volume Up 1 step
byte DSP_VOL_DOWN_1 [6] PROGMEM = {
0x68 , 0x04 , 0x6A , 0x32, 0x10 , 0x24 }; // Rotary Volume Down 1 step
byte DSP_SRCE_CD [6] PROGMEM = {
0x68 , 0x04 , 0x6A , 0x36, 0xA0 , 0x90 }; // DSP Source = CDThe section below is the setup and the main loop functions. The main loop runs millions of times per second – it first checks if the I/K-Bus has been idle for over 60 seconds in order to put the TH3122 IC into sleep mode, and shutdown the Arduino. The TH3122 will wake up automatically when it receives K-Bus voltage once again. The main loop then sends any messages that are awaiting in the circular buffer. And finally, the loop reads the serial buffer and passes the bytes to the readIbus() function.
void setup()
{
Serial.begin(9600, SERIAL_8E1); // Set hardware serial for 9600 baud, Even parity and one stop bit
mySerial.begin(9600); // Set debug serial speed
mySerial.println(F("--IBUS reader--"));
pinMode (EN, OUTPUT); // initialize pin.
pinMode(senSta, INPUT); // Configure pin as input
digitalWrite (EN, HIGH); // write pin high to enable TH3122.
senStaTimer(); // Start contention timer
packetTimer = currentTime;
ibusSleepTimer = millis();
}
void loop()
{
// Sleep Timer - Check if I-Bus has been inactive for 60 seconds
if (millis() - ibusSleepTimer >= 60000) {
digitalWrite (EN, LOW); // Shutdown TH3122
}
// Check if we have packets waiting to be sent onto the I-bus in our circular Buffer.. and send them.
// Inter packet gap timer on send
if(clearToSend && cbAvailable() > 0 ) { // If clear to send, and messages waiting in circular buffer, send them.
//if(cbAvailable() > 0 ) { // BENCH TESTING - NO clearToSend
if (millis() - packetTimer >= packetGap) {
sendIbusPacket();
packetTimer = millis();
}
}
if (Serial.available() >= 2) { // if 2 or more bytes in serial buffer, go and check for bus message
readIbus();
}
}The next section is the readIbus() function. It’s pretty self explanatory and well commented 😛
// This is where all the I/K-bus reading happens
// Message/Variable format example:
// 0xE8 0x05 0xD0 0x59 0x11 0x02 0x77
// source length destination databytes[0-2] checksumByte
void readIbus() {
goodPacket = false;
source = Serial.peekn(0);
length = Serial.peekn(1);
if (length >= 0x03 && length <= 0x24) { // Check if length byte between decimal 3 & 36
if (Serial.available() >= length + 2) { // Check if enough bytes in buffer - based on length byte
for (int i = 0; i <= length + 1; i++) {
inByte[i] = Serial.peekn(i);
}
// Calculate checksum.
checksumByte = 0;
for (int i = 0; i <= length; i++){
checksumByte ^= inByte[i];
}
if (inByte[length + 1] == checksumByte){ // Test for good checksum.
ibusSleepTimer = millis(); // Restart sleep timer. Need to do this every time a message is detected
// only process messages we're interested in. Add or remove from list as required
if (source != 0x50 && // MFL
source != 0x68 && // RAD
source != 0x18 && // CDC
source != 0x6A && // DSP
source != 0xC8 && // TEL
source != 0x44 && // EWS
source != 0xE8 // RLS
) {
Serial.remove(length + 2); // discard message if it's not one we want
return;
}
// Populate our incoming message byte array
for (int i = 0; i <= length + 1; i++){
ibusByte[i] = Serial.read();
}
goodPacket = true;
// Identify destination byte
destination = ibusByte[2];
// Identify message databytes - populate our databytes array
for (int i = 0, s = 3; i <= length - 3; i++, s++) {
databytes[i] = ibusByte[s];
}
printDebug("rx"); //Print to SoftwareSerial for debug purposes
comparePacket(); // Do stuff with the message
}
else {
printDebug("rx"); // Print to SoftwareSerial for debug purposes
Serial.remove(1); // Checksum was bad.. Delete first byte in buffer and restart on next byte
}
}
else { // Not enough data.. Loop around and see if more bytes have arrived
return;
}
}
else { // Length not in range.. Delete first byte in buffer and restart on next byte
Serial.remove(1);
return;
}
}And then we have the comparePacket() function which is called from the readIbus() function whenever we receive a verified valid I/K-Bus message. This is where you “do stuff” when you receive a message you’re interested in. You can either compare it to the list of variables set-up at the top of the code, or you can use a few if (x) {y} statements to do what you need. Some examples included in v0.0.1-alpha are: Printing text on the business radio LCD display, sending a “CD Stop” button command, and printing text onto the software serial (for debug) when the rain/light sensor detects darkness.
// This is where we compare the received message with all the message bytes
// at the top of the code, or to known source/destination, and respond accordingly.
// To send data back onto the I-Bus, we first write the packets into our circularBuffer
// TODO:
// 1. Simplify i-Bus message sending into single functions
// 2. Implement timer to remove written text from radio display after X seconds
void comparePacket() {
if(goodPacket == true) {
// Message from MFL_RT
if(memcmp_P(ibusByte, MFL_RT, 5) == 0 ) {
// This is how to send a known i-Bus message, from list at top of code..
// TODO - simplify this into single function. example: sendMSG(CD_STOP);
// It's sending the CD_STOP message whenever you press the
// R/T button on the steering wheel. This is just an example, and probably
// not something you would ever want to do.
cbWrite(sizeof(CD_STOP));
for(int i = 0; i < sizeof(CD_STOP); i++) {
cbWrite(pgm_read_byte(&CD_STOP[i]));
}
}
// Message from MFL_VOL_UP
if(memcmp_P(ibusByte, MFL_VOL_UP, 6) == 0 ) {
// This is how to send text message to the Business CD/Radio display
// TODO - simplify this into single function. example: textRadio("Example");
char strng[] = "Vol UP";
printText(strng, strlen(strng));
}
// Message from MFL_VOL_DOWN
if(memcmp_P(ibusByte, MFL_VOL_DOWN, 6) == 0 ) {
char strng[] = "Vol DOWN";
printText(strng, strlen(strng));
}
// Message from RLS to LCM
if ((source == 0xE8) && (destination == 0xD0)){
if (databytes[1] == 0x11) {
mySerial.println("It's dark out, lights commanded on");
// TODO:
// Blinking alert LED wire will come into arduino as a digital input (Radar detector alert LED).
// Implement dimming feature here. If this I-Bus message is present, find a method to dim this LED
// Arduino output to actual LED == Increase resistance to ground? PWM? Or activate a relay that includes resistor?
}
}
}
}And finally we have the sendIbusPacket() function which reads the circular buffer and writes any awaiting messages onto the K-Bus via the TH3122 IC.
// This is the code that removes messages from the circular buffer and does the actual sending.
void sendIbusPacket() {
outgoingSize = cbRead();
if(digitalRead(senSta) == LOW && outgoingSize <= 32) { // Double checks we are still clear to send
//if(outgoingSize <= 32) { // BENCH TESTING - NO clearToSend
for ( int i = 0; i < outgoingSize; i++) {
outgoingMsg[i] = cbRead();
Serial.write(outgoingMsg[i]); // write byte to IBUS.
}
printDebug("tx");
}
else {
cbRemove(outgoingSize);
return;
}
}• auxFunctions.ino
This file contains auxiliary functions. The first one is the printText() function used to create a message that will display text on the business radio LCD display. The function builds the message and sends it to the circular buffer.
// This function is used to print text onto the single line display of a BMW Business CD/Radio.
// It builds the message and calculates the checksum before it writes it to the outgoing circular buffer.
void printText(char * instrng , size_t messageLen) {
for(int i = 0, s = 0; i <= messageLen; i++, s++) {
SEND_MESSAGE[i + 6] = instrng[s];
}
SEND_MESSAGE[1] = 5 + messageLen;
int checksumPos = SEND_MESSAGE[1] + 1;
int checksumByte = 0;
for (int i = 0; i <= SEND_MESSAGE[1]; i++){
checksumByte ^= SEND_MESSAGE[i];
}
SEND_MESSAGE[checksumPos] = checksumByte;
cbWrite(SEND_MESSAGE[1] + 2);
for(int i = 0; i < SEND_MESSAGE[1] + 2; i++) {
cbWrite(SEND_MESSAGE[i]);
}
}The next function is printDebug() – it is used along with software serial to display I/K-Bus messages into a serial monitor window (Such as PuTTy). I am using pins 7 (Rx) and 8(Tx) on the Arduino for the software serial, with a USB to TTL converter (These wires must be crossed to the converter).
// Print HEX I-BUS message to SoftwareSerial - for debug
void printDebug(char debugType[3]) {
if (debugType == "rx") {
mySerial.print("Rx: ");
for(int i = 0; i <= length + 1; i++) {
mySerial.print(ibusByte[i], HEX);
mySerial.print(" ");
}
if(goodPacket == true) {
mySerial.println();
}
else {
mySerial.print(" ");
mySerial.println("Message Bad");
}
}
if (debugType == "tx") {
mySerial.print("Tx: ");
for ( int i = 0; i < outgoingSize; i++) {
mySerial.print(outgoingMsg[i], HEX);
mySerial.print(" ");
}
mySerial.println();
}
}• circularBuffer.ino
This is the circular buffer (FiFo) that holds messages in queue to be written to the I/K-Bus when it is clear to send.
// This code is basically a circular (FiFo) buffer used to store outgoing messages until the bus is clear to send.
// As soon as we detect a suitable gap in bus activity, we send the messages that are stored in the buffer.
//=====================================
// variables - not accessible by user
const byte cbBufSize = 18;
byte cbBufTail = 0;
byte cbBufHead = 0;
char cbBuffer[cbBufSize]; // use same buffer for cb and receive
//=====================================
// user accessible functions
//=====================================
int cbAvailable() {
byte cbByteCount = (cbBufSize + cbBufHead - cbBufTail) % cbBufSize;
return cbByteCount;
}
//============
byte cbRead() {
if (cbBufHead == cbBufTail) {
return -1;
}
else {
byte c = cbBuffer[cbBufTail];
cbBufTail = (cbBufTail + 1) % cbBufSize;
return c;
}
}
//============
char cbWrite( byte cbInByte ) { // User calls this
if ((cbBufHead + 1) % cbBufSize == cbBufTail) {
return -1;
}
cbBuffer[cbBufHead] = cbInByte;
cbBufHead = (cbBufHead + 1) % cbBufSize;
return 0;
}
//============
char cbWrite( byte cbInByte[], byte cbNumInBytes ) { // User calls this
if ((cbBufHead + 1) % cbBufSize == cbBufTail) {
return -1;
}
int i = cbBufHead;
for (byte n = 0; n < cbNumInBytes; n++) {
cbBuffer[(n + i) % cbBufSize] = cbInByte[n];
cbBufHead = (cbBufHead + 1) % cbBufSize;
}
return 0;
}
void cbRemove(int n) {
if(cbBufHead != cbBufTail) {
cbBufTail = (cbBufTail + n) % cbBufSize;
}
}
//============
int cbPeek(void) {
if (cbBufHead == cbBufTail) {
return -1;
}
else {
return cbBufTail;
}
}
//========END=============• senStaTimer.ino
This code is a form of collision prevention – it ensures it’s “clear to send” on the I/K-Bus, by using the SEN/STA signal from the TH3122. It monitors the signal via an interrupt and sets the flag “clearToSend” to true or false depending on that signal.
// This is all to do with making sure the bus is clear to send messages.
// It monitors the SEN/STA signal from the TH3122. As soon as the signal goes
// high, it sets a flag that it's 'not clear to send'. As soon as the signal goes low,
// it starts a timer for approx 1.5 milliseconds to check the signal is staying low,
// and then changes the flag state to 'clear to send'. No need to make changes here.
byte gapTimerCount = 94; // gives a test interval of 94 * 16 = 1504 usecs
void senStaTimer() {
// this sets the appropriate timer mode and then attaches an interrupt to Pin 3
TCCR2A = B00000000; // set normal mode (not fast PWM)
TCCR2B |= ((1 << CS22) | (1 << CS21)); // Start timer 2 at Fcpu/256
attachInterrupt(1, startTimer, FALLING);
}
//========================
void startTimer() {
// when a falling pulse is detected on pin3 the clock is started to trigger another
// interrupt after NN microseconds (determined by the value on gapTimerCount)
clearToSend = false;
OCR2A = TCNT2 + gapTimerCount; // sets up Timer2A to run for the sample period
TIMSK2 |= (1 << OCIE2A); // Enable Timer2 Compare Match A Interrupt
TIFR2 |= (1 << OCF2A); // Clear Timer2 Compare Match A Flag
detachInterrupt(1);
attachInterrupt(1, clearToSendReset, RISING);
}
//========================
void clearToSendReset() {
// when a rising pulse is detected on pin3 clearToSend = false
clearToSend = false;
detachInterrupt(1);
attachInterrupt(1, startTimer, FALLING);
}
//========================
ISR(TIMER2_COMPA_vect) { // this function runs when the Timer2A compare expires
TIMSK2 &= ~(1 << OCIE2A); // Disable Timer2 Compare Match A Interrupt
clearToSend = true;
}Project Continued:
• Intro
• Technical Details
• Schematic Description
• Resources & Downloads
• Programming
• Integration
• Messages