Arduino LTC6804 BMS - Part 3: Telemetry
by dcaditz in Circuits > Arduino
5642 Views, 3 Favorites, 0 Comments
Arduino LTC6804 BMS - Part 3: Telemetry
Part 1 is here
Part 2 is here
The BMS system described in Parts 1 and 2 of this series can happily operate as a stand-alone system, monitoring, balancing and protecting your battery pack. But what if you want to know what is going on within the pack? Furthermore, what if the system is in a remote location? For example, the BMS described in Part 1 was originally designed to manage the battery pack of a solar powered race vehicle, with the monitoring team following in a chase vehicle.
This Instructable describes a wireless telemetry system that can optionally be added to the BMS to transmit detailed information about the status of the battery pack in real time to a base station. The system is based on popular XBee RF modules (https://www.digi.com/products/embedded-systems/digi-xbee/rf-modules), which can easily be interfaced with the Arduino MCU board.
The system described here allows for one or more BMSs to report to a single base station such as a laptop computer. This is useful when there are multiple battery packs requiring multiple BMSs and you want to monitor them all from a single location.
Supplies
1) An Arduino BMS from Part 1 of this series: https://www.instructables.com/id/Arduino-LTC6804-B...
2) Two XBee SC2's (or later version) RF modules https://www.sparkfun.com/products/15126
3) Seeed Studio XBee shield http://wiki.seeedstudio.com/XBee_Shield_V2.0/
4) Sparkfun XBee Explorer USB https://www.sparkfun.com/products/11812
5) USB Mini B cable
6) XTCU configuration software https://www.digi.com/products/embedded-systems/dig...
Configure Your XBees
XBees can be set up to use either Transparent Mode and API Mode. Transparent Mode is often easiest to set up. In Transparent mode, serial data is sent as ASCII characters with no additional packet framing. This allows the user to view the incoming data on a Serial terminal program such as PuTTY or Realterm, with no additional programming. In API mode, on the other hand, the BMS data is 'framed' in a packet containing additional information, including identifying information about the sending XBee node. API mode therefore allows multiple BMS units to be monitored by a single base station with each being packet tagged with the sender's identifier. However, additional programming is required to create the frames before sending to the XBee and to interpret the received frames in a useful way.
Use the XCTU Configuration and Testing Utility to configure your XBees. XCTU can be downloaded from Digi here: https://www.digi.com/products/embedded-systems/di... (Note: a newer version of XCTU is available since this Instructable was written.)
There are numerous postings that describe how to use XCTU and I will not repeat the detailed configuration instructions here.
The following configuration parameters are used for the BMS system in Transparent Mode:
1) Set Transparent mode
2) Set both XBees to use the same PAN ID
3) Set one XBee as Coordinator and the other as Router. The Router attaches to the BMS and the coordinator to the monitoring computer.
4) On the router enable JV - Channel Verification
5) Leave all other settings at their default values
With these settings, the XBee's should be able to communicate in Transparent mode.
Assemble the Hardware
The various parts for the BMS unit and the base unit are shown in the accompanying images.
1) The BMS Unit:
Place an XBee that has been configured as a router on the XBee shield from Seeed Studio https://www.seeedstudio.com/XBee-Shield-V2-0-p-137... (Other equivalent shields could be used as well.) Note that the XBee shield has a series of jumper pins that are designed to route the serial RX and TX pins of the XBee to selected pins on the Arduino board. Connect XB_RX to Arduino pin 4 and XB_TX to Arduino pin 5 as shown in the accompanying image. These pins will also be specified in the Arduino code file to communicate with the XBee.
Next, place the shield onto the Arduino Uno and then place the BMS board that you built in Part 1 onto the XBee shield. The assembled BMS unit is shown in the image. Make sure all of the pins have seated properly. You may apply power to the Arduino using a 12V wall adapter... no need to connect the BMS to the battery pack at this time.
2) The Base Station Unit:
Place the XBee that has been configured as a Coordinator onto the XBee explorer board https://www.sparkfun.com/products/11812 Next, connect the Explorer board to your computer using a USB cable.
BMS Arduino Code - Transparent Mode
The Arduino code for the BMS is provided in Part 1 of this series of
Instructables. Here, I extend the code to allow wireless serial communication through XBees. I will draw attention to the parts of the code that are needed for XBee communication in Transparent mode.The complete program is attached below.
First, we create a SoftwareSerial object to manage the XBee serial data stream
/***** Xbee serial *****/ #define xbRxD 4 #define xbTxD 5 SoftwareSerial XbeeSerial(xbRxD, xbTxD);
The send and receive pins (4 and 5 in my code) have to match the physical pins on the XBee shield that were connected to the XBee RX and TX pins.
In setup() we add a line:
XbeeSerial.begin(9600);
Then we add 'XbeeSerial.print()' lines in the serialPrint() function as follows (do this for each overload of serialPrint()):
void serialPrint(String val) { Serial.print(val); Serial.print("\t"); XbeeSerial.print(val); XbeeSerial.print("\t"); }
That's it for transparent mode.
The full code is provided below for you to cut and paste into your Arduino IDE. Note that you may have to download the specified libraries and you my have to tweak the code a bit since libraries may have been updated since this Instructable was posted.
#include "Arduino.h" #include "stdint.h" #include "SoftwareSerial.h" #include "Linduino.h" #include "LT_SPI.h" #include "UserInterface.h" #include "LTC68042.h" #include "Average.h" #define TOTAL_IC 1 // Number of ICs in the isoSPI network LTC6804-2 ICs must be addressed in ascending order starting at 0. /***** Pack and sensor characteristics *****/ const float MAX_CURRENT = 5000000000.; // Maximum battery current(amps) before relay opens const float MAX_TEMP = 50. ; // Maximum pack temperature (deg C) before relay opens const float LEM_RANGE = 50.; // Rated range of LEM hall sensor. const float MIN_CELL_V = 2.20; // Minimum allowable cell voltage. Depends on battery chemistry. const float MAX_CELL_V = 3.60; // Maximum allowable cell voltage. Depends on battery chemistry. const float CELL_BALANCE_THRESHOLD_V = 3.3; // Cell balancing occurrs when voltage is above this value /***** Xbee serial *****/ #define xbRxD 4 #define xbTxD 5 SoftwareSerial XbeeSerial(xbRxD, xbTxD); /******** Arduino pin definitions ********/ int chargeRelayPin = 8; // Relay output for overcharge conditions int dischargeRelayPin = 9; // Relay output for undercharge conditions int currentPin = A0; // LEM Input should be Vcc/2 + I * 1.667 / LEM_RANGE int currentBiasPin = A1; // For comparing with LEM output (currentPin) since Vcc/2 may change as aux battery discharges. int tempPins[] = {A2}; // Array of arduino pins used for temperature sensor inpout /******** Variables for tracking cell voltages and states ***************/ int overCharge_state = LOW; // Over charge state. HIGH = relay on, LOW = relay off int underCharge_state = LOW; // Under charge state. HIGH = relay on, LOW = relay off int overTemp_state = LOW; // Over temperature state. HIGH = relay on, LOW = relay off int overCurrent_state = LOW; // Over current state. HIGH = relay on, LOW = relay off int chargeRelay_state; int dischargeRelay_state; int cellMax_i; // Temporary variable for holding index of cell with max voltage int cellMin_i; // Temporary variable for holding index of cell with min voltage float cellMin_V; // Temporary variable for holding min measured cell voltage float cellMax_V; // Temporary variable for holding max measured cell voltage float minV1 ; float maxV1 ; /******** Current and temperature variables ***********************/ const uint16_t imax = 100; // Size of arrays for averaging read measurements Average lemHistory(imax); Average lemBiasHistory(imax); float lem = 0; float lemBias = 0; float lemZeroCal = 0; float current = 0; float temp[sizeof(tempPins)]; int error = 0; unsigned long tstart; /****************************************************** Global Battery Variables received from 6804 commands These variables store the results from the LTC6804 register reads and the array lengths must be based on the number of ICs on the stack ******************************************************/ uint16_t cell_codes[TOTAL_IC][12]; /*!< The cell codes will be stored in the cell_codes[][12] array in the following format: | cell_codes[0][0]| cell_codes[0][1] | cell_codes[0][2]| ..... | cell_codes[0][11]| cell_codes[1][0] | cell_codes[1][1]| ..... | |------------------|------------------|------------------|--------------|-------------------|-------------------|-----------------|----------| |IC1 Cell 1 |IC1 Cell 2 |IC1 Cell 3 | ..... | IC1 Cell 12 |IC2 Cell 1 |IC2 Cell 2 | ..... | ****/ uint16_t aux_codes[TOTAL_IC][6]; /*!< The GPIO codes will be stored in the aux_codes[][6] array in the following format: | aux_codes[0][0]| aux_codes[0][1] | aux_codes[0][2]| aux_codes[0][3]| aux_codes[0][4]| aux_codes[0][5]| aux_codes[1][0] |aux_codes[1][1]| ..... | |-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|---------------|-----------| |IC1 GPIO1 |IC1 GPIO2 |IC1 GPIO3 |IC1 GPIO4 |IC1 GPIO5 |IC1 Vref2 |IC2 GPIO1 |IC2 GPIO2 | ..... | */ uint8_t tx_cfg[TOTAL_IC][6]; /*!< The tx_cfg[][6] stores the LTC6804 configuration data that is going to be written to the LTC6804 ICs on the daisy chain. The LTC6804 configuration data that will be written should be stored in blocks of 6 bytes. The array should have the following format: | tx_cfg[0][0]| tx_cfg[0][1] | tx_cfg[0][2]| tx_cfg[0][3]| tx_cfg[0][4]| tx_cfg[0][5]| tx_cfg[1][0] | tx_cfg[1][1]| tx_cfg[1][2]| ..... | |--------------|--------------|--------------|--------------|--------------|--------------|--------------|--------------|--------------|-----------| |IC1 CFGR0 |IC1 CFGR1 |IC1 CFGR2 |IC1 CFGR3 |IC1 CFGR4 |IC1 CFGR5 |IC2 CFGR0 |IC2 CFGR1 | IC2 CFGR2 | ..... | */ uint8_t rx_cfg[TOTAL_IC][8]; /*!< the rx_cfg[][8] array stores the data that is read back from a LTC6804-1 daisy chain. The configuration data for each IC is stored in blocks of 8 bytes. Below is an table illustrating the array organization: |rx_config[0][0]|rx_config[0][1]|rx_config[0][2]|rx_config[0][3]|rx_config[0][4]|rx_config[0][5]|rx_config[0][6] |rx_config[0][7] |rx_config[1][0]|rx_config[1][1]| ..... | |---------------|---------------|---------------|---------------|---------------|---------------|-----------------|----------------|---------------|---------------|-----------| |IC1 CFGR0 |IC1 CFGR1 |IC1 CFGR2 |IC1 CFGR3 |IC1 CFGR4 |IC1 CFGR5 |IC1 PEC High |IC1 PEC Low |IC2 CFGR0 |IC2 CFGR1 | ..... | */ /*!********************************************************************** \brief Inititializes hardware and variables ***********************************************************************/ void setup() { pinMode(chargeRelayPin, OUTPUT); pinMode(dischargeRelayPin, OUTPUT); pinMode(currentPin, INPUT); pinMode(currentBiasPin, INPUT); for (int i = 0; i < sizeof(tempPins) / sizeof(int); i++) { pinMode(tempPins[i], INPUT); } digitalWrite(dischargeRelayPin, LOW); // turn off relays during setup digitalWrite(chargeRelayPin, LOW); // turn off relays during setup overCharge_state = HIGH; underCharge_state = HIGH; Serial.begin(9600); XbeeSerial.begin(9600); LTC6804_initialize(); //Initialize LTC6804 hardware init_cfg(); //initialize the 6804 configuration array to be written delay(1000); lemZeroCal = zeroCurrentCalibrate(); // Calibrates LEM sensor at zero current overCurrent_state = HIGH; tstart = millis(); } /*!********************************************************************* \brief main loop ***********************************************************************/ void loop() { // while (overCurrent_state == LOW) { // Serial.println("RESET TO CONTINUE"); // delay(10000); // } // read current: //overCurrent_state = HIGH; current = readCurrent(); // read temperatures: overTemp_state = HIGH; for (int i = 0; i < sizeof(tempPins) / sizeof(int); i++) { temp[i] = (analogRead(tempPins[i]) * 5. / 1024 - 0.5) / 0.01; if (temp[i] > MAX_TEMP) { overTemp_state = LOW; Serial.println("OVER TEMPERATURE STATE DETECTED."); } } // read cells: wakeup_idle(); LTC6804_adcv(); // do cell AD conversion and fill cell registers delay(10); wakeup_idle(); error = LTC6804_rdcv(0, TOTAL_IC, cell_codes); // read cell voltages from registers if (error == -1) { Serial.println("A PEC error was detected in the received data"); } // print to serial outputs: print_cells(); // test for over charge/undercharge states: minV1 = MIN_CELL_V; maxV1 = MAX_CELL_V; if (overCharge_state == LOW) { // add hysteresis maxV1 = maxV1 - .2; } if (underCharge_state == LOW) { // add hysteresis minV1 = minV1 + .2; } // get maximum and minimum cells: cellMax_i = -1; cellMin_i = -1; cellMin_V = 100.; cellMax_V = 0.; for (int i = 0; i < 12; i++) { float V = cell_codes[0][i] * 0.0001; if (V < cellMin_V) { cellMin_V = V; cellMin_i = i; } if (V > cellMax_V) { cellMax_V = V; cellMax_i = i; } } underCharge_state = HIGH; overCharge_state = HIGH; overCurrent_state = HIGH; if (cellMin_V <= minV1) { underCharge_state = LOW; // Serial.println("V <= MIN_CELL_V"); } if (cellMax_V >= maxV1) { overCharge_state = LOW; //Serial.println("V >= MAX_CELL_V"); } if (abs(current) > MAX_CURRENT) { overCurrent_state = LOW; } // set relay states: chargeRelay_state = overCurrent_state && underCharge_state && overCharge_state && overTemp_state ; dischargeRelay_state = overCurrent_state && overCharge_state && underCharge_state && overTemp_state; digitalWrite(chargeRelayPin, chargeRelay_state ); digitalWrite(dischargeRelayPin, dischargeRelay_state); //while (chargeRelay_state == LOW || dischargeRelay_state== LOW ) { // Serial.println("RESET TO CONTINUE"); // delay(10000); //} // if (abs(current) > MAX_CURRENT) { // overCurrent_state = LOW; // digitalWrite(chargeRelayPin, overCurrent_state ); // digitalWrite(dischargeRelayPin, overCurrent_state ); // Serial.println("OVER CURRENT STATE DETECTED. PRESS RESET TO CONTINUE"); // delay(10000); // } else { // overCurrent_state = HIGH; // // chargeRelay_state = overCharge_state && overTemp_state ; // // dischargeRelay_state = underCharge_state && overTemp_state; // chargeRelay_state = underCharge_state && overCharge_state && overTemp_state ; // dischargeRelay_state = overCharge_state && underCharge_state && overTemp_state; // digitalWrite(chargeRelayPin, chargeRelay_state ); // digitalWrite(dischargeRelayPin, dischargeRelay_state); // } // // if (underCharge_state == LOW ) { // Serial.println("UNDER VOLTAGE STATE DETECTED."); // digitalWrite(dischargeRelayPin, dischargeRelay_state); // } // // if (overCharge_state == LOW ) { // Serial.println("OVER VOLTAGE STATE DETECTED."); // digitalWrite(chargeRelayPin, chargeRelay_state ); // } // take advantage of open relay to recalibrate LEM zero current setting: if (underCharge_state == LOW or overCharge_state == LOW ) { lemZeroCal = zeroCurrentCalibrate(); } // cell balancing: // Turn on switch Sx for highest cell x if voltage is above threshold // Note: DCP is set to 0 in initialize() This turns off discharge when cell voltages are read. // set values in tx_cfg //cellMax_i = 5; if (cellMax_V >= CELL_BALANCE_THRESHOLD_V) { balance_cfg(0, cellMax_i); //Serial.print("Balance "); //Serial.println(cellMax_i); } else { balance_cfg(0, -1); } // write tx_cfg to LTC6804. This sets the LTC6804 DCCx registers which control the S pins for balancing: LTC6804_wrcfg( TOTAL_IC, tx_cfg); delay(5000); } /*!*********************************** \brief Initializes the configuration array **************************************/ void init_cfg() { for (int i = 0; i < TOTAL_IC; i++) { tx_cfg[i][0] = 0xFE; //tx_cfg[i][1] = 0x04 ; tx_cfg[i][1] = 0x4E1 ; // 2.0V //tx_cfg[i][2] = 0xE1 ; tx_cfg[i][2] = 0x8CA; // 3.6V tx_cfg[i][3] = 0x00 ; tx_cfg[i][4] = 0x00 ; // discharge switches 0->off 1-> on. S0 = 0x01, S1 = 0x02, S2 = 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 // tx_cfg[i][5] = 0x00 ; tx_cfg[i][5] = 0x20 ; // sets the software timer to 1 minute } } /*!*********************************** \brief sets the configuration array for cell balancing uses CFGR4 and lowest 4 bits of CGFR5 **************************************/ void balance_cfg(int ic, int cell) { tx_cfg[ic][4] = 0x00; // clears S1-8 tx_cfg[ic][5] = tx_cfg[ic][5] & 0xF0; // clears S9-12 and sets software timer to 1 min //Serial.println(tx_cfg[ic][5] & 0xF0,BIN); if (cell >= 0 and cell <= 7) { tx_cfg[ic][4] = tx_cfg[ic][4] | 1 << cell; } if ( cell > 7) { tx_cfg[ic][5] = tx_cfg[ic][5] | ( 1 << (cell - 8)); } } /*!************************************************************ \brief Prints Cell Voltage Codes to the serial port *************************************************************/ void print_cells() { unsigned long elasped = millis() - tstart; float moduleV; serialPrint(elasped); //ELAPSED TIME: //INDIVIDUAL CELL VOLTAGES: for (int current_ic = 0 ; current_ic < TOTAL_IC; current_ic++) { moduleV = 0.; for (int i = 0; i < 12; i++) { moduleV = moduleV + cell_codes[current_ic][i] * 0.0001; serialPrint(cell_codes[current_ic][i] * 0.0001); } } serialPrint(moduleV); // TOTAL MODULE VOLTAGE: serialPrint(current); //MODULE CURRENT: //TEMPERATURES: for (int i = 0; i < sizeof(tempPins) / sizeof(int) ; i++) { serialPrint( temp[i]); } //RELAY STATES: serialPrint( chargeRelay_state); serialPrint( dischargeRelay_state); serialPrint("\r\n"); } /*!**************************************************************************** \brief print function overloads: *****************************************************************************/ void serialPrint(String val) { Serial.print(val); Serial.print("\t"); XbeeSerial.print(val); XbeeSerial.print("\t"); } void serialPrint(unsigned long val) { Serial.print(val); Serial.print("\t"); XbeeSerial.print(val); XbeeSerial.print("\t"); } void serialPrint(double val) { Serial.print(val, 4); Serial.print("\t"); XbeeSerial.print(val, 4); XbeeSerial.print("\t"); } void serialPrint(int val) { Serial.print(val); Serial.print("\t"); XbeeSerial.print(val); XbeeSerial.print("\t"); } /*!**************************************************************************** \brief Prints GPIO Voltage Codes and Vref2 Voltage Code onto the serial port *****************************************************************************/ void print_aux() { for (int current_ic = 0 ; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); for (int i = 0; i < 5; i++) { Serial.print(" GPIO-"); Serial.print(i + 1, DEC); Serial.print(":"); Serial.print(aux_codes[current_ic][i] * 0.0001, 4); Serial.print(","); } Serial.print(" Vref2"); Serial.print(":"); Serial.print(aux_codes[current_ic][5] * 0.0001, 4); Serial.println(); } Serial.println(); } /*!****************************************************************************** \brief Prints the Configuration data that is going to be written to the LTC6804 to the serial port. ********************************************************************************/ void print_config() { int cfg_pec; Serial.println("Written Configuration: "); for (int current_ic = 0; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); Serial.print(": "); Serial.print("0x"); serial_print_hex(tx_cfg[current_ic][0]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][1]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][2]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][3]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][4]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][5]); Serial.print(", Calculated PEC: 0x"); cfg_pec = pec15_calc(6, &tx_cfg[current_ic][0]); serial_print_hex((uint8_t)(cfg_pec >> 8)); Serial.print(", 0x"); serial_print_hex((uint8_t)(cfg_pec)); Serial.println(); } Serial.println(); } /*!***************************************************************** \brief Prints the Configuration data that was read back from the LTC6804 to the serial port. *******************************************************************/ void print_rxconfig() { Serial.println("Received Configuration "); for (int current_ic = 0; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); Serial.print(": 0x"); serial_print_hex(rx_cfg[current_ic][0]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][1]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][2]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][3]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][4]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][5]); Serial.print(", Received PEC: 0x"); serial_print_hex(rx_cfg[current_ic][6]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][7]); Serial.println(); } Serial.println(); } void serial_print_hex(uint8_t data) { if (data < 16) { Serial.print("0"); Serial.print((byte)data, HEX); } else Serial.print((byte)data, HEX); } /*!*********************************** \brief Reads current input from LEM sensor **************************************/ float readCurrent() { for (int i = 0 ; i < imax; i++) { lem = lemHistory.rolling(analogRead(currentPin)); lemBias = lemBiasHistory.rolling(analogRead(currentBiasPin)); } current = ((lem - lemBias / 2. - lemZeroCal) * 3. * 5. / 1024 * LEM_RANGE / 1.667) * 8.9 / 8.0;// assumes lem and bias are on 10k/5k voltage divider. Calibration fudge factor added. if (abs(current) < .2) current = 0; return current; } /*!*********************************** \brief Reads LEM sensor value when current is zero. Used to calibrates to zero output for zero current **************************************/ float zeroCurrentCalibrate() { // get initial readings for current and set the zero calibration int iSample = 500; for (int i = 0 ; i < iSample; i++) { lemHistory.push(analogRead(currentPin)); lemBiasHistory.push(analogRead(currentBiasPin)); } lem = lemHistory.mean(); lemBias = lemBiasHistory.mean(); return lem - lemBias / 2.; }
BMS Arduino Code - API Mode
You may want to go further and get the added functionality of using the XBee API mode. This and the next step describe API mode. If you are satisfied with Transparent mode you can ignore these steps.
First, use XCTU and your XBee explorer board to change the configuration of each XBee from Transparent to API mode.
The arduino code will also have to be modified. Instead of just printing data to the XBee serial stream, we temporarily store the data in a Byte array called _data[], embed the _data[] array in a frame, and then transmit the frame.
The _data array is built in the print_cells() function. The frame is then sent using XBee_sendFrame(_data, p) where p is the length of _data. The entire Arduino code is given below.
#include <Arduino.h> #include <stdint.h> #include <SoftwareSerial.h> //#include <SPI.h> #include "Linduino.h" #include "LT_SPI.h" #include "UserInterface.h" #include "LTC68042.h" #include "Average.h" #define TOTAL_IC 1 // Number of ICs in the isoSPI network LTC6804-2 ICs must be addressed in ascending order starting at 0. /***** Pack and sensor characteristics *****/ const float MAX_CURRENT = 5000000000.; // Maximum battery current(amps) before relay opens const float MAX_TEMP = 50. ; // Maximum pack temperature (deg C) before relay opens const float LEM_RANGE = 50.; // Rated range of LEM hall sensor. const float MIN_CELL_V = 2.20; // Minimum allowable cell voltage. Depends on battery chemistry. const float MAX_CELL_V = 3.60; // Maximum allowable cell voltage. Depends on battery chemistry. const float CELL_BALANCE_THRESHOLD_V = 3.3; // Cell balancing occurrs when voltage is above this value /***** Xbee serial *****/ #define xbRxD 4 #define xbTxD 5 SoftwareSerial XbeeSerial(xbRxD, xbTxD); /******** Arduino pin definitions ********/ int chargeRelayPin = 8; // Relay output for overcharge conditions int dischargeRelayPin = 9; // Relay output for undercharge conditions int currentPin = A0; // LEM Input should be Vcc/2 + I * 1.667 / LEM_RANGE int currentBiasPin = A1; // For comparing with LEM output (currentPin) since Vcc/2 may change as aux battery discharges. int tempPins[] = {A2, A3}; // Array of arduino pins used for temperature sensor inpout /******** variables for tracking cell voltages and states ***************/ int overCharge_state = LOW; // Over charge state. HIGH = relay on, LOW = relay off int underCharge_state = LOW; // Under charge state. HIGH = relay on, LOW = relay off int overTemp_state = LOW; // Over temperature state. HIGH = relay on, LOW = relay off int overCurrent_state = LOW; // Over current state. HIGH = relay on, LOW = relay off int chargeRelay_state; int dischargeRelay_state; int cellMax_i; // Temporary variable for holding index of cell with max voltage int cellMin_i; // Temporary variable for holding index of cell with min voltage float cellMin_V; // Temporary variable for holding min measured cell voltage float cellMax_V; // Temporary variable for holding max measured cell voltage float minV1 ; float maxV1 ; /******** Current and temperature variables ***********************/ const uint16_t imax = 100; // Size of arrays for averaging read measurements Average<float> lemHistory(imax); Average<float> lemBiasHistory(imax); float lem = 0; float lemBias = 0; float lemZeroCal = 0; float current = 0; float temp[sizeof(tempPins)]; int error = 0; unsigned long tstart; /****************************************************** XBee API configuration variables ******************************************************/ //uint16_t addr16 = 0xFFFE; // broadcast //uint8_t rad = 0x00; // allow max hops //uint8_t options = 0x00; // no options ////uint8_t destAddr16[] = { 0x00, 0x00}; //uint8_t FrameType = 10; //uint8_t FrameID = 01; //uint8_t payload[6]; // 1 more than needed - to allow for null termination //XBeeAddress64 addr64 = XBeeAddress64(0x00000000, 0x00000000); ////ZBTxRequest zbTx = ZBTxRequest(addr64, payload, sizeof(payload)); //ZBTxRequest zbTx = ZBTxRequest(addr64,addr16,rad,options,payload, sizeof(payload),FrameID); //ZBTxStatusResponse txStatus = ZBTxStatusResponse(); /****************************************************** Global Battery Variables received from 6804 commands These variables store the results from the LTC6804 register reads and the array lengths must be based on the number of ICs on the stack ******************************************************/ uint16_t cell_codes[TOTAL_IC][12]; /*!< The cell codes will be stored in the cell_codes[][12] array in the following format: | cell_codes[0][0]| cell_codes[0][1] | cell_codes[0][2]| ..... | cell_codes[0][11]| cell_codes[1][0] | cell_codes[1][1]| ..... | |------------------|------------------|------------------|--------------|-------------------|-------------------|-----------------|----------| |IC1 Cell 1 |IC1 Cell 2 |IC1 Cell 3 | ..... | IC1 Cell 12 |IC2 Cell 1 |IC2 Cell 2 | ..... | ****/ uint16_t aux_codes[TOTAL_IC][6]; /*!< The GPIO codes will be stored in the aux_codes[][6] array in the following format: | aux_codes[0][0]| aux_codes[0][1] | aux_codes[0][2]| aux_codes[0][3]| aux_codes[0][4]| aux_codes[0][5]| aux_codes[1][0] |aux_codes[1][1]| ..... | |-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|---------------|-----------| |IC1 GPIO1 |IC1 GPIO2 |IC1 GPIO3 |IC1 GPIO4 |IC1 GPIO5 |IC1 Vref2 |IC2 GPIO1 |IC2 GPIO2 | ..... | */ uint8_t tx_cfg[TOTAL_IC][6]; /*!< The tx_cfg[][6] stores the LTC6804 configuration data that is going to be written to the LTC6804 ICs on the daisy chain. The LTC6804 configuration data that will be written should be stored in blocks of 6 bytes. The array should have the following format: | tx_cfg[0][0]| tx_cfg[0][1] | tx_cfg[0][2]| tx_cfg[0][3]| tx_cfg[0][4]| tx_cfg[0][5]| tx_cfg[1][0] | tx_cfg[1][1]| tx_cfg[1][2]| ..... | |--------------|--------------|--------------|--------------|--------------|--------------|--------------|--------------|--------------|-----------| |IC1 CFGR0 |IC1 CFGR1 |IC1 CFGR2 |IC1 CFGR3 |IC1 CFGR4 |IC1 CFGR5 |IC2 CFGR0 |IC2 CFGR1 | IC2 CFGR2 | ..... | */ uint8_t rx_cfg[TOTAL_IC][8]; /*!< the rx_cfg[][8] array stores the data that is read back from a LTC6804-1 daisy chain. The configuration data for each IC is stored in blocks of 8 bytes. Below is an table illustrating the array organization: |rx_config[0][0]|rx_config[0][1]|rx_config[0][2]|rx_config[0][3]|rx_config[0][4]|rx_config[0][5]|rx_config[0][6] |rx_config[0][7] |rx_config[1][0]|rx_config[1][1]| ..... | |---------------|---------------|---------------|---------------|---------------|---------------|-----------------|----------------|---------------|---------------|-----------| |IC1 CFGR0 |IC1 CFGR1 |IC1 CFGR2 |IC1 CFGR3 |IC1 CFGR4 |IC1 CFGR5 |IC1 PEC High |IC1 PEC Low |IC2 CFGR0 |IC2 CFGR1 | ..... | */ /*!********************************************************************** \brief Inititializes hardware and variables ***********************************************************************/ void setup() { pinMode(chargeRelayPin, OUTPUT); pinMode(dischargeRelayPin, OUTPUT); pinMode(currentPin, INPUT); pinMode(currentBiasPin, INPUT); for (int i = 0; i < sizeof(tempPins) / sizeof(int); i++) { pinMode(tempPins[i], INPUT); } digitalWrite(dischargeRelayPin, LOW); // turn off relays during setup digitalWrite(chargeRelayPin, LOW); // turn off relays during setup overCharge_state = HIGH; underCharge_state = HIGH; Serial.begin(9600); XbeeSerial.begin(9600); LTC6804_initialize(); //Initialize LTC6804 hardware init_cfg(); //initialize the 6804 configuration array to be written delay(1000); lemZeroCal = zeroCurrentCalibrate(); // Calibrates LEM sensor at zero current overCurrent_state = HIGH; tstart = millis(); } /*!********************************************************************* \brief main loop ***********************************************************************/ void loop() { //Serial.println(sizeof(tempPins)); //Serial.println(sizeof(int)); // while (overCurrent_state == LOW) { // Serial.println("RESET TO CONTINUE"); // delay(10000); // } // read current: //overCurrent_state = HIGH; current = readCurrent(); // read temperatures: overTemp_state = LOW; for (int i = 0; i < sizeof(tempPins) / sizeof(int); i++) { temp[i] = (analogRead(tempPins[i]) * 5. / 1024 - 0.5) / 0.01; Serial.println(temp[i] ); if (temp[i] > MAX_TEMP) { overTemp_state = HIGH; Serial.println("OVER TEMPERATURE STATE DETECTED."); } } // read cells: wakeup_idle(); LTC6804_adcv(); // do cell AD conversion and fill cell registers delay(10); wakeup_idle(); error = LTC6804_rdcv(0, TOTAL_IC, cell_codes); // read cell voltages from registers if (error == -1) { Serial.println("A PEC error was detected in the received data"); } // print to serial outputs: print_cells(); // test for over charge/undercharge states: minV1 = MIN_CELL_V; maxV1 = MAX_CELL_V; if (overCharge_state == HIGH) { // add hysteresis maxV1 = maxV1 - .2; } if (underCharge_state == HIGH) { // add hysteresis minV1 = minV1 + .2; } // get maximum and minimum cells: cellMax_i = -1; cellMin_i = -1; cellMin_V = 100.; cellMax_V = 0.; for (int i = 0; i < 12; i++) { float V = cell_codes[0][i] * 0.0001; if (V < cellMin_V) { cellMin_V = V; cellMin_i = i; } if (V > cellMax_V) { cellMax_V = V; cellMax_i = i; } } underCharge_state = LOW; overCharge_state = LOW; overCurrent_state = LOW; if (cellMin_V <= minV1) { underCharge_state = LOW; // Serial.println("V <= MIN_CELL_V"); } if (cellMax_V >= maxV1) { overCharge_state = HIGH; //Serial.println("V >= MAX_CELL_V"); } if (abs(current) > MAX_CURRENT) { overCurrent_state = HIGH; } //*** set relay states: chargeRelay_state = !(overCurrent_state || overCharge_state || overTemp_state); // may change this logic dischargeRelay_state = !(overCurrent_state || overCharge_state || overTemp_state);// may change this logic digitalWrite(chargeRelayPin, chargeRelay_state); digitalWrite(dischargeRelayPin, dischargeRelay_state); //while (chargeRelay_state == LOW || dischargeRelay_state== LOW ) { // Serial.println("RESET TO CONTINUE"); // delay(10000); //} // if (abs(current) > MAX_CURRENT) { // overCurrent_state = LOW; // digitalWrite(chargeRelayPin, overCurrent_state ); // digitalWrite(dischargeRelayPin, overCurrent_state ); // Serial.println("OVER CURRENT STATE DETECTED. PRESS RESET TO CONTINUE"); // delay(10000); // } else { // overCurrent_state = HIGH; // // chargeRelay_state = overCharge_state && overTemp_state ; // // dischargeRelay_state = underCharge_state && overTemp_state; // chargeRelay_state = underCharge_state && overCharge_state && overTemp_state ; // dischargeRelay_state = overCharge_state && underCharge_state && overTemp_state; // digitalWrite(chargeRelayPin, chargeRelay_state ); // digitalWrite(dischargeRelayPin, dischargeRelay_state); // } // // if (underCharge_state == LOW ) { // Serial.println("UNDER VOLTAGE STATE DETECTED."); // digitalWrite(dischargeRelayPin, dischargeRelay_state); // } // // if (overCharge_state == LOW ) { // Serial.println("OVER VOLTAGE STATE DETECTED."); // digitalWrite(chargeRelayPin, chargeRelay_state ); // } // take advantage of open relay to recalibrate LEM zero current setting: if (underCharge_state == LOW or overCharge_state == LOW ) { lemZeroCal = zeroCurrentCalibrate(); } // cell balancing: // Turn on switch Sx for highest cell x if voltage is above threshold // Note: DCP is set to 0 in initialize() This turns off discharge when cell voltages are read. // set values in tx_cfg //cellMax_i = 5; if (cellMax_V >= CELL_BALANCE_THRESHOLD_V) { balance_cfg(0, cellMax_i); //Serial.print("Balance "); //Serial.println(cellMax_i); } else { balance_cfg(0, -1); } // write tx_cfg to LTC6804. This sets the LTC6804 DCCx registers which control the S pins for balancing: LTC6804_wrcfg( TOTAL_IC, tx_cfg); delay(5000); } /*!*********************************** \brief Initializes the configuration array **************************************/ void init_cfg() { for (int i = 0; i < TOTAL_IC; i++) { tx_cfg[i][0] = 0xFE; //tx_cfg[i][1] = 0x04 ; tx_cfg[i][1] = 0x4E1 ; // 2.0V //tx_cfg[i][2] = 0xE1 ; tx_cfg[i][2] = 0x8CA; // 3.6V tx_cfg[i][3] = 0x00 ; tx_cfg[i][4] = 0x00 ; // discharge switches 0->off 1-> on. S0 = 0x01, S1 = 0x02, S2 = 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 // tx_cfg[i][5] = 0x00 ; tx_cfg[i][5] = 0x20 ; // sets the software timer to 1 minute } } /*!*********************************** \brief sets the configuration array for cell balancing uses CFGR4 and lowest 4 bits of CGFR5 **************************************/ void balance_cfg(int ic, int cell) { tx_cfg[ic][4] = 0x00; // clears S1-8 tx_cfg[ic][5] = tx_cfg[ic][5] & 0xF0; // clears S9-12 and sets software timer to 1 min //Serial.println(tx_cfg[ic][5] & 0xF0,BIN); if (cell >= 0 and cell <= 7) { tx_cfg[ic][4] = tx_cfg[ic][4] | 1 << cell; } if ( cell > 7) { tx_cfg[ic][5] = tx_cfg[ic][5] | ( 1 << (cell - 8)); } } /*!************************************************************ \brief Prints Cell Voltage Codes to the serial port *************************************************************/ void print_cells() { int nCells = 12; unsigned long elasped = millis() - tstart; uint16_t elapsed16 = (uint16_t)elasped; float moduleV; uint8_t _data[39]; // holds values to send to XBee int p = 0; //*** elapsed time: _data[p] = (uint8_t) (elapsed16 >> 8) & 0xff; p++; _data[p] = (uint8_t) elapsed16 & 0xff; p++; //*** cell voltages: for (int current_ic = 0 ; current_ic < TOTAL_IC; current_ic++) { moduleV = 0.; for (int i = 0; i < nCells; i++) { moduleV = moduleV + cell_codes[current_ic][i] * 0.0001; Serial.print(cell_codes[current_ic][i] * 0.0001, 4); Serial.print(" "); _data[p] = (uint8_t) (cell_codes[0][i] >> 8) & 0xff; p++; _data[p] = (uint8_t) cell_codes[0][i] & 0xff; p++; } } Serial.print(" V: "); Serial.print(moduleV); //*** current: Serial.print(" A: "); Serial.print(current); int bcurrent = (int) (current * 1000.); // current may take negative values so use signed integet type _data[p] = (bcurrent >> 8) & 0xff; p++; _data[p] = (bcurrent) & 0xff; p++; //*** temperatures: for (int i = 0; i < sizeof(tempPins) / sizeof(int) ; i++) { Serial.print(" T:"); Serial.print( temp[i]); uint16_t btemp = (uint16_t) (temp[i] * 1000.); _data[p] = (btemp >> 8) & 0xff; p++; _data[p] = (btemp) & 0xff; p++; } //*** fault flags and relay states: Serial.print(" "); Serial.print( underCharge_state); Serial.print(" "); Serial.print( overCharge_state); Serial.print(" "); Serial.print( overCurrent_state); Serial.print(" "); Serial.print( overTemp_state); Serial.print(" "); Serial.print( chargeRelay_state); Serial.print(" "); Serial.print( dischargeRelay_state); _data[p] = underCharge_state; p++; _data[p] = overCharge_state; p++; _data[p] = overCurrent_state; p++; _data[p] = overTemp_state; p++; _data[p] = chargeRelay_state; p++; _data[p] = dischargeRelay_state; p++; XBee_sendFrame(_data, p); Serial.println(); } /*!**************************************************************************** \brief Prints GPIO Voltage Codes and Vref2 Voltage Code onto the serial port *****************************************************************************/ void print_aux() { for (int current_ic = 0 ; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); for (int i = 0; i < 5; i++) { Serial.print(" GPIO-"); Serial.print(i + 1, DEC); Serial.print(":"); Serial.print(aux_codes[current_ic][i] * 0.0001, 4); Serial.print(","); } Serial.print(" Vref2"); Serial.print(":"); Serial.print(aux_codes[current_ic][5] * 0.0001, 4); Serial.println(); } Serial.println(); } /*!****************************************************************************** \brief Prints the Configuration data that is going to be written to the LTC6804 to the serial port. ********************************************************************************/ void print_config() { int cfg_pec; Serial.println("Written Configuration: "); for (int current_ic = 0; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); Serial.print(": "); Serial.print("0x"); serial_print_hex(tx_cfg[current_ic][0]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][1]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][2]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][3]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][4]); Serial.print(", 0x"); serial_print_hex(tx_cfg[current_ic][5]); Serial.print(", Calculated PEC: 0x"); cfg_pec = pec15_calc(6, &tx_cfg[current_ic][0]); serial_print_hex((uint8_t)(cfg_pec >> 8)); Serial.print(", 0x"); serial_print_hex((uint8_t)(cfg_pec)); Serial.println(); } Serial.println(); } /*!***************************************************************** \brief Prints the Configuration data that was read back from the LTC6804 to the serial port. *******************************************************************/ void print_rxconfig() { Serial.println("Received Configuration "); for (int current_ic = 0; current_ic < TOTAL_IC; current_ic++) { Serial.print(" IC "); Serial.print(current_ic + 1, DEC); Serial.print(": 0x"); serial_print_hex(rx_cfg[current_ic][0]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][1]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][2]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][3]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][4]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][5]); Serial.print(", Received PEC: 0x"); serial_print_hex(rx_cfg[current_ic][6]); Serial.print(", 0x"); serial_print_hex(rx_cfg[current_ic][7]); Serial.println(); } Serial.println(); } void serial_print_hex(uint8_t data) { if (data < 16) { Serial.print("0"); Serial.print((byte)data, HEX); } else Serial.print((byte)data, HEX); } /*!*********************************** \brief Reads current input from LEM sensor **************************************/ float readCurrent() { for (int i = 0 ; i < imax; i++) { lem = lemHistory.rolling(analogRead(currentPin)); lemBias = lemBiasHistory.rolling(analogRead(currentBiasPin)); } current = ((lem - lemBias / 2. - lemZeroCal) * 3. * 5. / 1024 * LEM_RANGE / 1.667) * 8.9 / 8.0;// assumes lem and bias are on 10k/5k voltage divider. Calibration fudge factor added. if (abs(current) < .2) current = 0; return current; } /*!*********************************** \brief Reads LEM sensor value when current is zero. Used to calibrates to zero output for zero current **************************************/ float zeroCurrentCalibrate() { // get initial readings for current and set the zero calibration int iSample = 500; for (int i = 0 ; i < iSample; i++) { lemHistory.push(analogRead(currentPin)); lemBiasHistory.push(analogRead(currentBiasPin)); } lem = lemHistory.mean(); lemBias = lemBiasHistory.mean(); return lem - lemBias / 2.; } void XBee_sendFrame(uint8_t payload[], int psize) { uint8_t checksum = 0; uint8_t len = 0x0E + psize; uint8_t msbLen = (len >> 8) & 0xff; uint8_t lsbLen = len & 0xff; uint8_t rad = 0x00; uint8_t options = 0x00; uint8_t destAddr64[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t destAddr16[] = { 0xFF, 0xFE}; uint8_t FrameType = 0x10; uint8_t FrameID = 0x01; sendByte(0x7E); // start byte sendByte(msbLen); sendByte(lsbLen); sendByte(FrameType); // Frame Type TxRequest checksum += FrameType; sendByte(FrameID); //Frame ID checksum += FrameID; for (int i = 0; i < 8; i++) { sendByte(destAddr64[i]); checksum += destAddr64[i]; } for (int i = 0; i < 2; i++) { sendByte(destAddr16[i]); checksum += destAddr16[i]; } sendByte(rad); checksum += rad; sendByte(options); checksum += options; for (int i = 0; i < psize; i++) { sendByte(payload[i]); checksum += payload[i]; } checksum = 0xff - checksum; sendByte(checksum); } void XBee_sendFrame_test() { // sends 'Hello' uint8_t frame[] = {0x7E, 0x00, 0x13, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, 0x00, 0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xFD}; for (int i = 0; i < 23; i++) { sendByte(frame[i]); } } void sendByte(uint8_t b) { XbeeSerial.write(b); }
Base Station Code - API Mode
The Coordinator XBee is attached to a USB port on the base station computer using an XBee explorer and USB cable. You can view the incoming data using XCTU on the Console tab. XCTU can parse the API mode data frames and extract the data portion of each frame, which is probably what you are mainly interested in. If you are familiar with coding, you can also write your own program to receive and interpret the incoming data. This is more work, but may provide a better solution because reading frame data in XCTU is a bit inconvenient. In the following I describe the XCTU Console and give some general direction for developing your own code.
1) XCTU:
Connect to the Coordinator XBee using the Search or Add XBee buttons on the upper left of XCTU. Once the device is found and selected, switch to the console tab and connect the serial port. You will see the API mode frames appear in the frames list. You can view frame details by clicking on the frame. Scroll down in the details to the RF data section and switch to the HEX tab. (ASCII will be useless since we put the data into a Byte array. ). You will have to click frame by frame to view the incoming data. This is probably not the most convenient UI for viewing BMS data, but it is a good option for testing whether the system is working and whether data frames are being sent and received as expected.
2) Roll your own:
A good place to start if you want to provide your own BMS UI is on the Digi's open source library on GitHub:
I was most familiar with C# and Visual Studio .NET and used the XBee.core library by Jeff Haynes:
https://github.com/jefffhaynes/XBee
There you will find a good quick start guide and example project to get you well on your way. After you receive the XBee API frams using the code library, you will have to parse the data to retreive the battery voltages, module current, temperatures, etc. To do this I created a class, RxFrame, in my .NET form. This class fills its properties with the associated values from the data byte array:
public class RxFrame { private int _nTemps = 1; // set to the number of temperature sensors used. Must be identical to the number of sensors specified by tempPins[] in the Arduino code. private int _elapsed; private double[] _VCells; private double[] _temps; private double _VModule; private double _current; private int _overCharge; private int _underCharge; private int _overTemp; private int _overCurrent; private int _chargeRelayState; private int _disChargeRelayState; int p = 0; public RxFrame(byte[] data) { _elapsed = (int)(data[p] << 8 | data[p + 1]); p = p + 2; _VCells = new double[12]; _temps = new double[_nTemps]; _VModule = 0; for (int i = 0; i < 12; i++) { _VCells[i] = (double)(data[p] << 8 | data[p + 1]) * .0001; p = p + 2; _VModule += _VCells[i]; } _current = (double)(data[p] << 8 | data[p + 1]) * .0001; p = p + 2; for (int i = 0; i < _nTemps; i++) { _temps[i] = (double)(data[p] << 8 | data[p + 1]) * .001; p = p + 2; } _underCharge = data[p]; p++; _overCharge = data[p]; p++; _overCurrent = data[p]; p++; _overTemp = data[p]; p++; _chargeRelayState = data[p]; p++; _disChargeRelayState = data[p]; p++; }
I set up a data received handler:
eventArgs.Node.DataReceived += (node, data) => { this.BeginInvoke(new SetDataReceivedEventArgsDeleg(si_DataReceived), new object[] { data }); };
and populated an RxFrame instance from the received frame in the si_DataReceived() function:
private void si_DataReceived(DataReceivedEventArgs data) { RxFrame rxFrame = new RxFrame(data.Data); // do what you want with the data contained in rxFrame here // For example: tbCurrent.Text = rxFrame.current.ToString(); }
This may seem cryptic at this point, but it will become more clear when you study the examples on GitHub. Feel free to message me with questions if you get stuck. Happy coding!