BatterySpace LiFePO4 BMS Code for Arduino / by Paul Sammut

The BMS from www.BatterySpace.com for LiFePO4 batteries comes with an optional CAN interface. This CAN interface is useful for two reasons:

  • Allows you to monitor the BMS and all its diagnostics information from another computer, such as the main computer for the robot you are building
  • CAN connects to the charger system and tells the charger to stop charging during over voltage conditions. This eliminates the need of an extra charging protection relay.  
www.BatterySpace.com BMS for LiFePO4 packs

www.BatterySpace.com BMS for LiFePO4 packs

So for you to make use of the data coming out of the CAN bus on the BMS, you need something that reads it! There are many, many, many ways to do this such as:

  1. Get a USB to CAN adapter and plug it straight into a computer/laptop. Then use software on that computer to parse the CAN frames. The cheapest ones are still a bit pricey at $50. Here's one from amazon
  2. Get a fancy dev-board that has on board CAN such as a Digilent MX7
  3. Use a cheap arduino with a cheap CAN shield

Because I wanted a dedicated embedded computer inside the battery box that will do some more stuff like handle relays to give me a remote shut-off of the battery box, I chose the 3rd option. 

The ElecFreaks CAN-BUS Shield uses a Microchip MCP2515 CAN Controller with an SPI Interface with an MCP2551 CAN Transceiver. 

The CAN-BUS Shield from www.ElecFreaks.com is absolutely fantastic. It costs under $24 dollars and is very well designed using quality CAN chips from Mircochip. It also has a convenient terminal block to connect the two bare wires of a CAN bus along with a standard DB9. 

The way it works is simple. It serves as an interface that takes CAN in from one end, and gives SPI out the other. Arduino has a built in SPI interface so you talk to the CAN bus from there.

So here's what our system looks like: 

System overview diagram of the Arduino with CAN-Shield bus and how it connects to the BMS. Note the 120k termination resistors at the ends of the bus. All standard easy-peasy stuff!

Nice and simple! The arduino now has access to the CAN bus data that's being spit out by the BMS and we have a connection to the arduino via serial link to a remote computer sitting far far away. Now remember there are many ways of doing this. I could have just extended the CAN Bus to the monitoring compute and had the monitoring computer talk to both the arduino and the BMS via that one bus. But because I didn't have the need for a CAN bus anywhere else in the vehicle, I opted to just keep a simple point to point serial connection to the battery pack. This also gives me the slight benefit of having a very discrete battery box with one input-output through that serial port that handles all functionality of the device. 

The Arduino Stack-up containing the CAN-Shield and custom proto-board with relay for remote battery shut-off and undervoltage shut-off protection.

So now that we have everything set up and wired, we just need code for the Arduino to parse the CAN frames spit out from the BMS. So this should be easy right? Just look up the comms-spec of the BMS published by the manufacturer and parse the data as necessary. Unfortunately this spec doc is written in chinglish. And the kind of chinglish where it's not just bad English, but also extremely convoluted ways of specifying stuff and absolute heinous typos (0-10 vs 1-11) that make you frustrated for HOURS. Good news for you is that I decoded, parsed, and scaled this crap so you don't have to. 

Finally seeing the CAN data from the BMS being properly decoded and parsed into their proper values!

And without further adoiu here's the Arduino code that reads the CAN Bus and spits out the information at a rate of 50 Hz. I built upon the CAN-BUS Shield example's found on ElecFreak's wiki page.  

**Note that my code has extra functionality that shuts down a relay based on the status of a digital input. This is extra functionality that you don't need if you just want to read the BMS, and has been commented out. 

Happy building!

Exported from Notepad++
#define PIN_SHUTDOWN 7 #define PIN_UC_SIGNAL A0 // demo: CAN-BUS Shield, receive data with check mode // send data coming to fast, such as less than 10ms, you can use this way // loovee, 2014-6-13 #include <SPI.h> #include "mcp_can.h" // the cs pin of the version after v1.1 is default to D9 // v0.9b and v1.0 is default D10 const int SPI_CS_PIN = 9; MCP_CAN CAN(SPI_CS_PIN); // Set CS pin char readCmd[255]; int readCmdIndex = 0; unsigned long signalTimer = 0; float packVoltage = 0; float packCurrent = 0; float maxCellTemp = 0; float minCellTemp = 0; float minCellVoltage = 0; float maxCellVoltage = 0; float packCapacity = 0; int packStatus = 0; unsigned long sampleTimer = millis(); unsigned long samplePeriod = 20; //20 milliseconds. void setup() { initSerial(); pinMode(PIN_SHUTDOWN, OUTPUT); digitalWrite(PIN_SHUTDOWN, LOW); postStatus(); } void loop() { //Serial.println(analogRead(PIN_UC_SIGNAL)); serialHandler(); //checkSystem(); //Code for the undervoltage check system checkCAN(); postStatus(); } void postStatus() { if((millis()-samplePeriod)>=sampleTimer){ sampleTimer = millis(); //reset the timer Serial1.print("$BTBS,"); Serial1.print(packStatus);Serial1.print(","); Serial1.print(packVoltage, 3);Serial1.print(","); Serial1.print(packCurrent, 3);Serial1.print(","); Serial1.print(packCapacity, 3);Serial1.print(","); Serial1.print(maxCellTemp, 3);Serial1.print(","); Serial1.print(minCellTemp, 3);Serial1.print(","); Serial1.print(maxCellVoltage, 3);Serial1.print(","); Serial1.print(minCellVoltage, 3);Serial1.println(); } } void checkSystem() { if (analogRead(PIN_UC_SIGNAL) < 200) { Serial1.println("$BTBF,UC_DETECT_1"); //UC signal detected, wait 30 seconds and check again. delay(30000); if (analogRead(PIN_UC_SIGNAL) < 200) { Serial1.println("$BTBF,UC_DETECT_2"); //UC signal detected, wait 30 seconds and check again. delay(30000); if (analogRead(PIN_UC_SIGNAL) < 200) { shutDown(); } } } } void serialHandler() { if (Serial1.available() > 0) { // get incoming byte: readCmd[readCmdIndex] = Serial1.read(); if (readCmd[readCmdIndex] == '\n') { //carriage return found meaning that is the end of our command. readCmdIndex = 0; processCmd(readCmd); //send the readCmd to be processed memset(readCmd, 0, 255); //rest the readCmd char array to all nulls } else readCmdIndex++; } } void processCmd(char incomingCmd[]) { if (strcmp(incomingCmd, "$BTBC,SHUTDOWN\n") == 0) { shutDown(); } } void shutDown() { Serial1.println("$BTBC,SHUTTING_DOWN"); int timerShutdown = 60; while(timerShutdown != 0){ Serial1.print("Shutting down in "); Serial1.print(timerShutdown); Serial1.println("seconds."); delay(1000); timerShutdown--; } digitalWrite(PIN_SHUTDOWN, HIGH); delay(5000); //this is in the case the arduino remains powered during a shutdown bench test digitalWrite(PIN_SHUTDOWN, LOW); } void initSerial() { //Initialize serial and wait for port to open: Serial1.begin(115200); // prints title with ending line break Serial1.println("=============================================="); Serial1.println("==== SAMMUT TECH L.L.C ======================="); Serial1.println("========================== © June,2016 ======="); Serial1.println("=============================================="); Serial1.println("BTB Battery Computer System Online!"); while (CAN_OK != CAN.begin(CAN_500KBPS)) // init can bus : baudrate = 500k { Serial1.println("CAN BUS Shield init fail"); Serial1.println(" Init CAN BUS Shield again"); delay(100); } Serial1.println("CAN BUS Shield init ok!"); } void checkCAN() { unsigned char len = 0; unsigned char buf[8]; if (CAN_MSGAVAIL == CAN.checkReceive()) // check if data coming { CAN.readMsgBuf(&len, buf); // read data, len: data length, buf: data buf unsigned char canId = CAN.getCanId(); //Serial1.println("-----------------------------"); //Serial1.print("Get data from ID: "); // Serial1.println(canId, HEX); //Serial1.println(buf[2], HEX); if (buf[2] == 0x25) { // we have a percent sign //Serial1.print(buf[0], HEX); //Serial1.println(buf[1], HEX); //pack voltage if (buf[0] == 0x30 && buf[1] == 0x42) { processPackVoltage(buf[3], buf[4], buf[5], buf[6], buf[7]); } //pack current else if (buf[0] == 0x30 && buf[1] == 0x43) { processPackCurrent(buf[3], buf[4], buf[5], buf[6], buf[7]); } //pack capacity else if (buf[0] == 0x30 && buf[1] == 0x44) { processPackCapacity(buf[3], buf[4], buf[5], buf[6], buf[7]); } //min cell temp else if (buf[0] == 0x31 && buf[1] == 0x31) { processMinCellTemp(buf[3], buf[4], buf[5], buf[6], buf[7]); } //max cell temp else if (buf[0] == 0x30 && buf[1] == 0x35) { processMaxCellTemp(buf[3], buf[4], buf[5], buf[6], buf[7]); } //min cell voltage else if (buf[0] == 0x30 && buf[1] == 0x39) { processMinCellVoltage(buf[3], buf[4], buf[5], buf[6], buf[7]); } //max cell voltage else if (buf[0] == 0x30 && buf[1] == 0x37) { processMaxCellVoltage(buf[3], buf[4], buf[5], buf[6], buf[7]); } //pack status THER IS A TYPO IN THE DOC. I can guarantee you that it is 0-10 and not 1-11 else if (buf[0] == 0x30 && buf[1] == 0x45) { processPackStatus(buf[3], buf[4], buf[5], buf[6], buf[7]); } } } } void processPackVoltage(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); packVoltage = (float)number / 1000; //Serial1.print(packVoltage, 3); //Serial1.println(); } void processPackCurrent(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); packCurrent = (float)number / 10; //Serial1.println(packCurrent, 3); //Serial1.println(); } void processPackCapacity(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); packCapacity = (float)number; //Serial1.println(packCapacity, 3); //Serial1.println(); } void processMinCellTemp(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); minCellTemp = (float)number; //Serial1.println(minCellTemp, 3); //Serial1.println(); } void processMaxCellTemp(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); maxCellTemp = (float)number; //Serial1.println(maxCellTemp, 3); //Serial1.println(); } void processMinCellVoltage(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); minCellVoltage = (float)number / 1000; //Serial1.println(minCellVoltage, 3); //Serial1.println(); } void processMaxCellVoltage(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); maxCellVoltage = (float)number / 1000; //Serial1.println(maxCellVoltage, 3); //Serial1.println(); } void processPackStatus(char buff0, char buff1, char buff2, char buff3, char buff4) { char hexstring[] = {buff0, buff1, buff2, buff3, buff4}; int number = (int)strtol(hexstring, NULL, 16); packStatus = number; //Serial1.println(packStatus); //Serial1.println(); }