*** 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 = CD
The 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