Repository: collin80/TeslaBMS Branch: master Commit: ab1f2299c09f Files: 14 Total size: 66.6 KB Directory structure: gitextract_ilblwdk_/ ├── BMSModule.cpp ├── BMSModule.h ├── BMSModuleManager.cpp ├── BMSModuleManager.h ├── BMSUtil.h ├── Logger.cpp ├── Logger.h ├── README.md ├── SerialConsole.cpp ├── SerialConsole.h ├── SystemIO.cpp ├── SystemIO.h ├── TeslaBMS.ino └── config.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: BMSModule.cpp ================================================ #include "config.h" #include "BMSModule.h" #include "BMSUtil.h" #include "Logger.h" extern EEPROMSettings settings; BMSModule::BMSModule() { for (int i = 0; i < 6; i++) { cellVolt[i] = 0.0f; lowestCellVolt[i] = 5.0f; highestCellVolt[i] = 0.0f; balanceState[i] = 0; } moduleVolt = 0.0f; temperatures[0] = 0.0f; temperatures[1] = 0.0f; lowestTemperature = 200.0f; highestTemperature = -100.0f; lowestModuleVolt = 200.0f; highestModuleVolt = 0.0f; exists = false; moduleAddress = 0; goodPackets = 0; badPackets = 0; } /* Reading the status of the board to identify any flags, will be more useful when implementing a sleep cycle */ void BMSModule::readStatus() { uint8_t payload[3]; uint8_t buff[8]; payload[0] = moduleAddress << 1; //adresss payload[1] = REG_ALERT_STATUS;//Alert Status start payload[2] = 0x04; BMSUtil::sendDataWithReply(payload, 3, false, buff, 7); alerts = buff[3]; faults = buff[4]; COVFaults = buff[5]; CUVFaults = buff[6]; } uint8_t BMSModule::getFaults() { return faults; } uint8_t BMSModule::getAlerts() { return alerts; } uint8_t BMSModule::getCOVCells() { return COVFaults; } uint8_t BMSModule::getCUVCells() { return CUVFaults; } /* Reading the setpoints, after a reset the default tesla setpoints are loaded Default response : 0x10, 0x80, 0x31, 0x81, 0x08, 0x81, 0x66, 0xff */ /* void BMSModule::readSetpoint() { uint8_t payload[3]; uint8_t buff[12]; payload[0] = moduleAddress << 1; //adresss payload[1] = 0x40;//Alert Status start payload[2] = 0x08;//two registers sendData(payload, 3, false); delay(2); getReply(buff); OVolt = 2.0+ (0.05* buff[5]); UVolt = 0.7 + (0.1* buff[7]); Tset = 35 + (5 * (buff[9] >> 4)); } */ bool BMSModule::readModuleValues() { uint8_t payload[4]; uint8_t buff[50]; uint8_t calcCRC; bool retVal = false; int retLen; float tempCalc; float tempTemp; payload[0] = moduleAddress << 1; readStatus(); Logger::debug("Module %i alerts=%X faults=%X COV=%X CUV=%X", moduleAddress, alerts, faults, COVFaults, CUVFaults); payload[1] = REG_ADC_CTRL; payload[2] = 0b00111101; //ADC Auto mode, read every ADC input we can (Both Temps, Pack, 6 cells) BMSUtil::sendDataWithReply(payload, 3, true, buff, 3); payload[1] = REG_IO_CTRL; payload[2] = 0b00000011; //enable temperature measurement VSS pins BMSUtil::sendDataWithReply(payload, 3, true, buff, 3); payload[1] = REG_ADC_CONV; //start all ADC conversions payload[2] = 1; BMSUtil::sendDataWithReply(payload, 3, true, buff, 3); payload[1] = REG_GPAI; //start reading registers at the module voltage registers payload[2] = 0x12; //read 18 bytes (Each value takes 2 - ModuleV, CellV1-6, Temp1, Temp2) retLen = BMSUtil::sendDataWithReply(payload, 3, false, buff, 22); calcCRC = BMSUtil::genCRC(buff, retLen-1); Logger::debug("Sent CRC: %x Calculated CRC: %x", buff[21], calcCRC); //18 data bytes, address, command, length, and CRC = 22 bytes returned //Also validate CRC to ensure we didn't get garbage data. if ( (retLen == 22) && (buff[21] == calcCRC) ) { if (buff[0] == (moduleAddress << 1) && buff[1] == REG_GPAI && buff[2] == 0x12) //Also ensure this is actually the reply to our intended query { //payload is 2 bytes gpai, 2 bytes for each of 6 cell voltages, 2 bytes for each of two temperatures (18 bytes of data) moduleVolt = (buff[3] * 256 + buff[4]) * 0.002034609f; if (moduleVolt > highestModuleVolt) highestModuleVolt = moduleVolt; if (moduleVolt < lowestModuleVolt) lowestModuleVolt = moduleVolt; for (int i = 0; i < 6; i++) { cellVolt[i] = (buff[5 + (i * 2)] * 256 + buff[6 + (i * 2)]) * 0.000381493f; if (lowestCellVolt[i] > cellVolt[i]) lowestCellVolt[i] = cellVolt[i]; if (highestCellVolt[i] < cellVolt[i]) highestCellVolt[i] = cellVolt[i]; } //Now using steinhart/hart equation for temperatures. We'll see if it is better than old code. tempTemp = (1.78f / ((buff[17] * 256 + buff[18] + 2) / 33046.0f) - 3.57f); tempTemp *= 1000.0f; tempCalc = 1.0f / (0.0007610373573f + (0.0002728524832 * logf(tempTemp)) + (powf(logf(tempTemp), 3) * 0.0000001022822735f)); temperatures[0] = tempCalc - 273.15f; tempTemp = 1.78f / ((buff[19] * 256 + buff[20] + 9) / 33068.0f) - 3.57f; tempTemp *= 1000.0f; tempCalc = 1.0f / (0.0007610373573f + (0.0002728524832 * logf(tempTemp)) + (powf(logf(tempTemp), 3) * 0.0000001022822735f)); temperatures[1] = tempCalc - 273.15f; if (getLowTemp() < lowestTemperature) lowestTemperature = getLowTemp(); if (getHighTemp() > highestTemperature) highestTemperature = getHighTemp(); Logger::debug("Got voltage and temperature readings"); goodPackets++; retVal = true; } } else { Logger::error("Invalid module response received for module %i len: %i crc: %i calc: %i", moduleAddress, retLen, buff[21], calcCRC); badPackets++; } Logger::debug("Good RX: %d Bad RX: %d", goodPackets, badPackets); //turning the temperature wires off here seems to cause weird temperature glitches // payload[1] = REG_IO_CTRL; // payload[2] = 0b00000000; //turn off temperature measurement pins // BMSUtil::sendData(payload, 3, true); // delay(3); // BMSUtil::getReply(buff, 50); //TODO: we're not validating the reply here. Perhaps check to see if a valid reply came back return retVal; } float BMSModule::getCellVoltage(int cell) { if (cell < 0 || cell > 5) return 0.0f; return cellVolt[cell]; } float BMSModule::getLowCellV() { float lowVal = 10.0f; for (int i = 0; i < 6; i++) if (cellVolt[i] < lowVal) lowVal = cellVolt[i]; return lowVal; } float BMSModule::getHighCellV() { float hiVal = 0.0f; for (int i = 0; i < 6; i++) if (cellVolt[i] > hiVal) hiVal = cellVolt[i]; return hiVal; } float BMSModule::getAverageV() { float avgVal = 0.0f; for (int i = 0; i < 6; i++) avgVal += cellVolt[i]; avgVal /= 6.0f; return avgVal; } float BMSModule::getHighestModuleVolt() { return highestModuleVolt; } float BMSModule::getLowestModuleVolt() { return lowestModuleVolt; } float BMSModule::getHighestCellVolt(int cell) { if (cell < 0 || cell > 5) return 0.0f; return highestCellVolt[cell]; } float BMSModule::getLowestCellVolt(int cell) { if (cell < 0 || cell > 5) return 0.0f; return lowestCellVolt[cell]; } float BMSModule::getHighestTemp() { return highestTemperature; } float BMSModule::getLowestTemp() { return lowestTemperature; } float BMSModule::getLowTemp() { return (temperatures[0] < temperatures[1]) ? temperatures[0] : temperatures[1]; } float BMSModule::getHighTemp() { return (temperatures[0] < temperatures[1]) ? temperatures[1] : temperatures[0]; } float BMSModule::getAvgTemp() { return (temperatures[0] + temperatures[1]) / 2.0f; } float BMSModule::getModuleVoltage() { return moduleVolt; } float BMSModule::getTemperature(int temp) { if (temp < 0 || temp > 1) return 0.0f; return temperatures[temp]; } void BMSModule::setAddress(int newAddr) { if (newAddr < 0 || newAddr > MAX_MODULE_ADDR) return; moduleAddress = newAddr; } int BMSModule::getAddress() { return moduleAddress; } bool BMSModule::isExisting() { return exists; } void BMSModule::setExists(bool ex) { exists = ex; } void BMSModule::balanceCells() { uint8_t payload[4]; uint8_t buff[30]; uint8_t balance = 0;//bit 0 - 5 are to activate cell balancing 1-6 payload[0] = moduleAddress << 1; payload[1] = REG_BAL_CTRL; payload[2] = 0; //writing zero to this register resets balance time and must be done before setting balance resistors again. BMSUtil::sendData(payload, 3, true); delay(2); BMSUtil::getReply(buff, 30); for (int i = 0; i < 6; i++) { if ( (balanceState[i] == 0) && (getCellVoltage(i) > settings.balanceVoltage) ) balanceState[i] = 1; if ( /*(balanceState[i] == 1) &&*/ (getCellVoltage(i) < (settings.balanceVoltage - settings.balanceHyst)) ) balanceState[i] = 0; if (balanceState[i] == 1) balance |= (1< 5) return 0; return balanceState[cell]; } ================================================ FILE: BMSModule.h ================================================ #pragma once class BMSModule { public: BMSModule(); void readStatus(); bool readModuleValues(); float getCellVoltage(int cell); float getLowCellV(); float getHighCellV(); float getAverageV(); float getLowTemp(); float getHighTemp(); float getHighestModuleVolt(); float getLowestModuleVolt(); float getHighestCellVolt(int cell); float getLowestCellVolt(int cell); float getHighestTemp(); float getLowestTemp(); float getAvgTemp(); float getModuleVoltage(); float getTemperature(int temp); uint8_t getFaults(); uint8_t getAlerts(); uint8_t getCOVCells(); uint8_t getCUVCells(); void setAddress(int newAddr); int getAddress(); bool isExisting(); void setExists(bool ex); void balanceCells(); uint8_t getBalancingState(int cell); private: float cellVolt[6]; // calculated as 16 bit value * 6.250 / 16383 = volts float lowestCellVolt[6]; float highestCellVolt[6]; float moduleVolt; // calculated as 16 bit value * 33.333 / 16383 = volts float temperatures[2]; // Don't know the proper scaling at this point float lowestTemperature; float highestTemperature; float lowestModuleVolt; float highestModuleVolt; uint8_t balanceState[6]; //0 = balancing off for this cell, 1 = balancing currently on bool exists; int alerts; int faults; int COVFaults; int CUVFaults; int goodPackets; int badPackets; uint8_t moduleAddress; //1 to 0x3E }; ================================================ FILE: BMSModuleManager.cpp ================================================ #include "config.h" #include "BMSModuleManager.h" #include "BMSUtil.h" #include "Logger.h" extern EEPROMSettings settings; BMSModuleManager::BMSModuleManager() { for (int i = 1; i <= MAX_MODULE_ADDR; i++) { modules[i].setExists(false); modules[i].setAddress(i); } lowestPackVolt = 1000.0f; highestPackVolt = 0.0f; lowestPackTemp = 200.0f; highestPackTemp = -100.0f; isFaulted = false; } void BMSModuleManager::balanceCells() { for (int address = 1; address <= MAX_MODULE_ADDR; address++) { if (modules[address].isExisting()) modules[address].balanceCells(); } } /* * Try to set up any unitialized boards. Send a command to address 0 and see if there is a response. If there is then there is * still at least one unitialized board. Go ahead and give it the first ID not registered as already taken. * If we send a command to address 0 and no one responds then every board is inialized and this routine stops. * Don't run this routine until after the boards have already been enumerated.\ * Note: The 0x80 conversion it is looking might in theory block the message from being forwarded so it might be required * To do all of this differently. Try with multiple boards. The alternative method would be to try to set the next unused * address and see if any boards respond back saying that they set the address. */ void BMSModuleManager::setupBoards() { uint8_t payload[3]; uint8_t buff[10]; int retLen; payload[0] = 0; payload[1] = 0; payload[2] = 1; while (1 == 1) { payload[0] = 0; payload[1] = 0; payload[2] = 1; retLen = BMSUtil::sendDataWithReply(payload, 3, false, buff, 4); if (retLen == 4) { if (buff[0] == 0x80 && buff[1] == 0 && buff[2] == 1) { Logger::debug("00 found"); //look for a free address to use for (int y = 1; y < 63; y++) { if (!modules[y].isExisting()) { payload[0] = 0; payload[1] = REG_ADDR_CTRL; payload[2] = y | 0x80; BMSUtil::sendData(payload, 3, true); delay(3); if (BMSUtil::getReply(buff, 10) > 2) { if (buff[0] == (0x81) && buff[1] == REG_ADDR_CTRL && buff[2] == (y + 0x80)) { modules[y].setExists(true); numFoundModules++; Logger::debug("Address assigned"); } } break; //quit the for loop } } } else break; //nobody responded properly to the zero address so our work here is done. } else break; } } /* * Iterate through all 62 possible board addresses (1-62) to see if they respond */ void BMSModuleManager::findBoards() { uint8_t payload[3]; uint8_t buff[8]; numFoundModules = 0; payload[0] = 0; payload[1] = 0; //read registers starting at 0 payload[2] = 1; //read one byte for (int x = 1; x <= MAX_MODULE_ADDR; x++) { modules[x].setExists(false); payload[0] = x << 1; BMSUtil::sendData(payload, 3, false); delay(20); if (BMSUtil::getReply(buff, 8) > 4) { if (buff[0] == (x << 1) && buff[1] == 0 && buff[2] == 1 && buff[4] > 0) { modules[x].setExists(true); numFoundModules++; Logger::debug("Found module with address: %X", x); } } delay(5); } } /* * Force all modules to reset back to address 0 then set them all up in order so that the first module * in line from the master board is 1, the second one 2, and so on. */ void BMSModuleManager::renumberBoardIDs() { uint8_t payload[3]; uint8_t buff[8]; int attempts = 1; for (int y = 1; y < 63; y++) { modules[y].setExists(false); numFoundModules = 0; } while (attempts < 3) { payload[0] = 0x3F << 1; //broadcast the reset command payload[1] = 0x3C;//reset payload[2] = 0xA5;//data to cause a reset BMSUtil::sendData(payload, 3, true); delay(100); BMSUtil::getReply(buff, 8); if (buff[0] == 0x7F && buff[1] == 0x3C && buff[2] == 0xA5 && buff[3] == 0x57) break; attempts++; } setupBoards(); } /* After a RESET boards have their faults written due to the hard restart or first time power up, this clears thier faults */ void BMSModuleManager::clearFaults() { uint8_t payload[3]; uint8_t buff[8]; payload[0] = 0x7F; //broadcast payload[1] = REG_ALERT_STATUS;//Alert Status payload[2] = 0xFF;//data to cause a reset BMSUtil::sendDataWithReply(payload, 3, true, buff, 4); payload[0] = 0x7F; //broadcast payload[2] = 0x00;//data to clear BMSUtil::sendDataWithReply(payload, 3, true, buff, 4); payload[0] = 0x7F; //broadcast payload[1] = REG_FAULT_STATUS;//Fault Status payload[2] = 0xFF;//data to cause a reset BMSUtil::sendDataWithReply(payload, 3, true, buff, 4); payload[0] = 0x7F; //broadcast payload[2] = 0x00;//data to clear BMSUtil::sendDataWithReply(payload, 3, true, buff, 4); isFaulted = false; } /* Puts all boards on the bus into a Sleep state, very good to use when the vehicle is a rest state. Pulling the boards out of sleep only to check voltage decay and temperature when the contactors are open. */ void BMSModuleManager::sleepBoards() { uint8_t payload[3]; uint8_t buff[8]; payload[0] = 0x7F; //broadcast payload[1] = REG_IO_CTRL;//IO ctrl start payload[2] = 0x04;//write sleep bit BMSUtil::sendData(payload, 3, true); delay(2); BMSUtil::getReply(buff, 8); } /* Wakes all the boards up and clears thier SLEEP state bit in the Alert Status Registery */ void BMSModuleManager::wakeBoards() { uint8_t payload[3]; uint8_t buff[8]; payload[0] = 0x7F; //broadcast payload[1] = REG_IO_CTRL;//IO ctrl start payload[2] = 0x00;//write sleep bit BMSUtil::sendData(payload, 3, true); delay(2); BMSUtil::getReply(buff, 8); payload[0] = 0x7F; //broadcast payload[1] = REG_ALERT_STATUS;//Fault Status payload[2] = 0x04;//data to cause a reset BMSUtil::sendData(payload, 3, true); delay(2); BMSUtil::getReply(buff, 8); payload[0] = 0x7F; //broadcast payload[2] = 0x00;//data to clear BMSUtil::sendData(payload, 3, true); delay(2); BMSUtil::getReply(buff, 8); } void BMSModuleManager::getAllVoltTemp() { packVolt = 0.0f; for (int x = 1; x <= MAX_MODULE_ADDR; x++) { if (modules[x].isExisting()) { Logger::debug(""); Logger::debug("Module %i exists. Reading voltage and temperature values", x); modules[x].readModuleValues(); Logger::debug("Module voltage: %f", modules[x].getModuleVoltage()); Logger::debug("Lowest Cell V: %f Highest Cell V: %f", modules[x].getLowCellV(), modules[x].getHighCellV()); Logger::debug("Temp1: %f Temp2: %f", modules[x].getTemperature(0), modules[x].getTemperature(1)); packVolt += modules[x].getModuleVoltage(); if (modules[x].getLowTemp() < lowestPackTemp) lowestPackTemp = modules[x].getLowTemp(); if (modules[x].getHighTemp() > highestPackTemp) highestPackTemp = modules[x].getHighTemp(); } } if (packVolt > highestPackVolt) highestPackVolt = packVolt; if (packVolt < lowestPackVolt) lowestPackVolt = packVolt; if (digitalRead(13) == LOW) { if (!isFaulted) Logger::error("One or more BMS modules have entered the fault state!"); isFaulted = true; } else { if (isFaulted) Logger::info("All modules have exited a faulted state"); isFaulted = false; } } float BMSModuleManager::getPackVoltage() { return packVolt; } float BMSModuleManager::getAvgTemperature() { float avg = 0.0f; for (int x = 1; x <= MAX_MODULE_ADDR; x++) { if (modules[x].isExisting()) avg += modules[x].getAvgTemp(); } avg = avg / (float)numFoundModules; return avg; } float BMSModuleManager::getAvgCellVolt() { float avg = 0.0f; for (int x = 1; x <= MAX_MODULE_ADDR; x++) { if (modules[x].isExisting()) avg += modules[x].getAverageV(); } avg = avg / (float)numFoundModules; return avg; } void BMSModuleManager::printPackSummary() { uint8_t faults; uint8_t alerts; uint8_t COV; uint8_t CUV; Logger::console(""); Logger::console(""); Logger::console(""); Logger::console(" Pack Status:"); if (isFaulted) Logger::console(" FAULTED!"); else Logger::console(" All systems go!"); Logger::console("Modules: %i Voltage: %fV Avg Cell Voltage: %fV Avg Temp: %fC ", numFoundModules, getPackVoltage(),getAvgCellVolt(), getAvgTemperature()); Logger::console(""); for (int y = 1; y < 63; y++) { if (modules[y].isExisting()) { faults = modules[y].getFaults(); alerts = modules[y].getAlerts(); COV = modules[y].getCOVCells(); CUV = modules[y].getCUVCells(); Logger::console(" Module #%i", y); Logger::console(" Voltage: %fV (%fV-%fV) Temperatures: (%fC-%fC)", modules[y].getModuleVoltage(), modules[y].getLowCellV(), modules[y].getHighCellV(), modules[y].getLowTemp(), modules[y].getHighTemp()); SerialUSB.print(" Currently balancing cells: "); for (int i = 0; i < 6; i++) { if (modules[y].getBalancingState(i) == 1) { SerialUSB.print(i); SerialUSB.print(" "); } } SerialUSB.println(); if (faults > 0) { Logger::console(" MODULE IS FAULTED:"); if (faults & 1) { SerialUSB.print(" Overvoltage Cell Numbers (1-6): "); for (int i = 0; i < 6; i++) { if (COV & (1 << i)) { SerialUSB.print(i+1); SerialUSB.print(" "); } } SerialUSB.println(); } if (faults & 2) { SerialUSB.print(" Undervoltage Cell Numbers (1-6): "); for (int i = 0; i < 6; i++) { if (CUV & (1 << i)) { SerialUSB.print(i+1); SerialUSB.print(" "); } } SerialUSB.println(); } if (faults & 4) { Logger::console(" CRC error in received packet"); } if (faults & 8) { Logger::console(" Power on reset has occurred"); } if (faults & 0x10) { Logger::console(" Test fault active"); } if (faults & 0x20) { Logger::console(" Internal registers inconsistent"); } } if (alerts > 0) { Logger::console(" MODULE HAS ALERTS:"); if (alerts & 1) { Logger::console(" Over temperature on TS1"); } if (alerts & 2) { Logger::console(" Over temperature on TS2"); } if (alerts & 4) { Logger::console(" Sleep mode active"); } if (alerts & 8) { Logger::console(" Thermal shutdown active"); } if (alerts & 0x10) { Logger::console(" Test Alert"); } if (alerts & 0x20) { Logger::console(" OTP EPROM Uncorrectable Error"); } if (alerts & 0x40) { Logger::console(" GROUP3 Regs Invalid"); } if (alerts & 0x80) { Logger::console(" Address not registered"); } } if (faults > 0 || alerts > 0) SerialUSB.println(); } } } void BMSModuleManager::printPackDetails() { uint8_t faults; uint8_t alerts; uint8_t COV; uint8_t CUV; int cellNum = 0; Logger::console(""); Logger::console(""); Logger::console(""); Logger::console(" Pack Status:"); if (isFaulted) Logger::console(" FAULTED!"); else Logger::console(" All systems go!"); Logger::console("Modules: %i Voltage: %fV Avg Cell Voltage: %fV Avg Temp: %fC ", numFoundModules, getPackVoltage(),getAvgCellVolt(), getAvgTemperature()); Logger::console(""); for (int y = 1; y < 63; y++) { if (modules[y].isExisting()) { faults = modules[y].getFaults(); alerts = modules[y].getAlerts(); COV = modules[y].getCOVCells(); CUV = modules[y].getCUVCells(); SerialUSB.print("Module #"); SerialUSB.print(y); if (y < 10) SerialUSB.print(" "); SerialUSB.print(" "); SerialUSB.print(modules[y].getModuleVoltage()); SerialUSB.print("V"); for (int i = 0; i < 6; i++) { if (cellNum < 10) SerialUSB.print(" "); SerialUSB.print(" Cell"); SerialUSB.print(cellNum++); SerialUSB.print(": "); SerialUSB.print(modules[y].getCellVoltage(i)); SerialUSB.print("V"); if (modules[y].getBalancingState(i) == 1) SerialUSB.print("*"); else SerialUSB.print(" "); } SerialUSB.print(" Neg Term Temp: "); SerialUSB.print(modules[y].getTemperature(0)); SerialUSB.print("C Pos Term Temp: "); SerialUSB.print(modules[y].getTemperature(1)); SerialUSB.println("C"); } } } void BMSModuleManager::processCANMsg(CAN_FRAME &frame) { uint8_t battId = (frame.id >> 16) & 0xF; uint8_t moduleId = (frame.id >> 8) & 0xFF; uint8_t cellId = (frame.id) & 0xFF; if (moduleId = 0xFF) //every module { if (cellId == 0xFF) sendBatterySummary(); else { for (int i = 1; i <= MAX_MODULE_ADDR; i++) { if (modules[i].isExisting()) { sendCellDetails(i, cellId); delayMicroseconds(500); } } } } else //a specific module { if (cellId == 0xFF) sendModuleSummary(moduleId); else sendCellDetails(moduleId, cellId); } } void BMSModuleManager::sendBatterySummary() { CAN_FRAME outgoing; outgoing.id = (0x1BA00000ul) + ((settings.batteryID & 0xF) << 16) + 0xFFFF; outgoing.rtr = 0; outgoing.priority = 1; outgoing.extended = true; outgoing.length = 8; uint16_t battV = uint16_t(getPackVoltage() * 100.0f); outgoing.data.byte[0] = battV & 0xFF; outgoing.data.byte[1] = battV >> 8; outgoing.data.byte[2] = 0; //instantaneous current. Not measured at this point outgoing.data.byte[3] = 0; outgoing.data.byte[4] = 50; //state of charge int avgTemp = (int)getAvgTemperature() + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[5] = avgTemp; avgTemp = (int)lowestPackTemp + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[6] = avgTemp; avgTemp = (int)highestPackTemp + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[7] = avgTemp; Can0.sendFrame(outgoing); } void BMSModuleManager::sendModuleSummary(int module) { CAN_FRAME outgoing; outgoing.id = (0x1BA00000ul) + ((settings.batteryID & 0xF) << 16) + ((module & 0xFF) << 8) + 0xFF; outgoing.rtr = 0; outgoing.priority = 1; outgoing.extended = true; outgoing.length = 8; uint16_t battV = uint16_t(modules[module].getModuleVoltage() * 100.0f); outgoing.data.byte[0] = battV & 0xFF; outgoing.data.byte[1] = battV >> 8; outgoing.data.byte[2] = 0; //instantaneous current. Not measured at this point outgoing.data.byte[3] = 0; outgoing.data.byte[4] = 50; //state of charge int avgTemp = (int)modules[module].getAvgTemp() + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[5] = avgTemp; avgTemp = (int)modules[module].getLowestTemp() + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[6] = avgTemp; avgTemp = (int)modules[module].getHighestTemp() + 40; if (avgTemp < 0) avgTemp = 0; outgoing.data.byte[7] = avgTemp; Can0.sendFrame(outgoing); } void BMSModuleManager::sendCellDetails(int module, int cell) { CAN_FRAME outgoing; outgoing.id = (0x1BA00000ul) + ((settings.batteryID & 0xF) << 16) + ((module & 0xFF) << 8) + (cell & 0xFF); outgoing.rtr = 0; outgoing.priority = 1; outgoing.extended = true; outgoing.length = 8; uint16_t battV = uint16_t(modules[module].getCellVoltage(cell) * 100.0f); outgoing.data.byte[0] = battV & 0xFF; outgoing.data.byte[1] = battV >> 8; battV = uint16_t(modules[module].getHighestCellVolt(cell) * 100.0f); outgoing.data.byte[2] = battV & 0xFF; outgoing.data.byte[3] = battV >> 8; battV = uint16_t(modules[module].getLowestCellVolt(cell) * 100.0f); outgoing.data.byte[4] = battV & 0xFF; outgoing.data.byte[5] = battV >> 8; int instTemp = modules[module].getHighTemp() + 40; outgoing.data.byte[6] = instTemp; // should be nearest temperature reading not highest but this works too. outgoing.data.byte[7] = 0; //Bit encoded fault data. No definitions for this yet. Can0.sendFrame(outgoing); } //The SerialConsole actually sets the battery ID to a specific value. We just have to set up the CAN filter here to //match. void BMSModuleManager::setBatteryID() { //Setup filter for direct access to our registered battery ID uint32_t canID = (0xBAul << 20) + (((uint32_t)settings.batteryID & 0xF) << 16); Can0.setRXFilter(0, canID, 0x1FFF0000ul, true); } ================================================ FILE: BMSModuleManager.h ================================================ #pragma once #include "config.h" #include "BMSModule.h" #include class BMSModuleManager { public: BMSModuleManager(); void balanceCells(); void setupBoards(); void findBoards(); void renumberBoardIDs(); void clearFaults(); void sleepBoards(); void wakeBoards(); void getAllVoltTemp(); void readSetpoints(); void setBatteryID(); float getPackVoltage(); float getAvgTemperature(); float getAvgCellVolt(); void processCANMsg(CAN_FRAME &frame); void printPackSummary(); void printPackDetails(); private: float packVolt; // All modules added together float lowestPackVolt; float highestPackVolt; float lowestPackTemp; float highestPackTemp; BMSModule modules[MAX_MODULE_ADDR + 1]; // store data for as many modules as we've configured for. int numFoundModules; // The number of modules that seem to exist bool isFaulted; void sendBatterySummary(); void sendModuleSummary(int module); void sendCellDetails(int module, int cell); }; ================================================ FILE: BMSUtil.h ================================================ #include #include "Logger.h" class BMSUtil { public: static uint8_t genCRC(uint8_t *input, int lenInput) { uint8_t generator = 0x07; uint8_t crc = 0; for (int x = 0; x < lenInput; x++) { crc ^= input[x]; /* XOR-in the next input byte */ for (int i = 0; i < 8; i++) { if ((crc & 0x80) != 0) { crc = (uint8_t)((crc << 1) ^ generator); } else { crc <<= 1; } } } return crc; } static void sendData(uint8_t *data, uint8_t dataLen, bool isWrite) { uint8_t orig = data[0]; uint8_t addrByte = data[0]; if (isWrite) addrByte |= 1; SERIAL.write(addrByte); SERIAL.write(&data[1], dataLen - 1); //assumes that there are at least 2 bytes sent every time. There should be, addr and cmd at the least. data[0] = addrByte; if (isWrite) SERIAL.write(genCRC(data, dataLen)); if (Logger::isDebug()) { SERIALCONSOLE.print("Sending: "); SERIALCONSOLE.print(addrByte, HEX); SERIALCONSOLE.print(" "); for (int x = 1; x < dataLen; x++) { SERIALCONSOLE.print(data[x], HEX); SERIALCONSOLE.print(" "); } if (isWrite) SERIALCONSOLE.print(genCRC(data, dataLen), HEX); SERIALCONSOLE.println(); } data[0] = orig; } static int getReply(uint8_t *data, int maxLen) { int numBytes = 0; if (Logger::isDebug()) SERIALCONSOLE.print("Reply: "); while (SERIAL.available() && numBytes < maxLen) { data[numBytes] = SERIAL.read(); if (Logger::isDebug()) { SERIALCONSOLE.print(data[numBytes], HEX); SERIALCONSOLE.print(" "); } numBytes++; } if (maxLen == numBytes) { while (SERIAL.available()) SERIAL.read(); } if (Logger::isDebug()) SERIALCONSOLE.println(); return numBytes; } //Uses above functions to send data then get the response. Will auto retry if response not //the expected return length. This helps to alleviate any comm issues. The Due cannot exactly //match the correct comm speed so sometimes there are data glitches. static int sendDataWithReply(uint8_t *data, uint8_t dataLen, bool isWrite, uint8_t *retData, int retLen) { int attempts = 1; int returnedLength; while (attempts < 4) { sendData(data, dataLen, isWrite); delay(2 * ((retLen / 8) + 1)); returnedLength = getReply(retData, retLen); if (returnedLength == retLen) return returnedLength; attempts++; } return returnedLength; //failed to get a proper response. } }; ================================================ FILE: Logger.cpp ================================================ /* * Logger.cpp * Copyright (c) 2013 Collin Kidder, Michael Neuweiler, Charles Galpin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "Logger.h" Logger::LogLevel Logger::logLevel = Logger::Info; uint32_t Logger::lastLogTime = 0; /* * Output a debug message with a variable amount of parameters. * printf() style, see Logger::log() * */ void Logger::debug(char *message, ...) { if (logLevel > Debug) return; va_list args; va_start(args, message); Logger::log(Debug, message, args); va_end(args); } /* * Output a info message with a variable amount of parameters * printf() style, see Logger::log() */ void Logger::info(char *message, ...) { if (logLevel > Info) return; va_list args; va_start(args, message); Logger::log(Info, message, args); va_end(args); } /* * Output a warning message with a variable amount of parameters * printf() style, see Logger::log() */ void Logger::warn(char *message, ...) { if (logLevel > Warn) return; va_list args; va_start(args, message); Logger::log(Warn, message, args); va_end(args); } /* * Output a error message with a variable amount of parameters * printf() style, see Logger::log() */ void Logger::error(char *message, ...) { if (logLevel > Error) return; va_list args; va_start(args, message); Logger::log(Error, message, args); va_end(args); } /* * Output a comnsole message with a variable amount of parameters * printf() style, see Logger::logMessage() */ void Logger::console(char *message, ...) { va_list args; va_start(args, message); Logger::logMessage(message, args); va_end(args); } /* * Set the log level. Any output below the specified log level will be omitted. */ void Logger::setLoglevel(LogLevel level) { logLevel = level; } /* * Retrieve the current log level. */ Logger::LogLevel Logger::getLogLevel() { return logLevel; } /* * Return a timestamp when the last log entry was made. */ uint32_t Logger::getLastLogTime() { return lastLogTime; } /* * Returns if debug log level is enabled. This can be used in time critical * situations to prevent unnecessary string concatenation (if the message won't * be logged in the end). * * Example: * if (Logger::isDebug()) { * Logger::debug("current time: %d", millis()); * } */ boolean Logger::isDebug() { return logLevel == Debug; } /* * Output a log message (called by debug(), info(), warn(), error(), console()) * * Supports printf() like syntax: * * %% - outputs a '%' character * %s - prints the next parameter as string * %d - prints the next parameter as decimal * %f - prints the next parameter as double float * %x - prints the next parameter as hex value * %X - prints the next parameter as hex value with '0x' added before * %b - prints the next parameter as binary value * %B - prints the next parameter as binary value with '0b' added before * %l - prints the next parameter as long * %c - prints the next parameter as a character * %t - prints the next parameter as boolean ('T' or 'F') * %T - prints the next parameter as boolean ('true' or 'false') */ void Logger::log(LogLevel level, char *format, va_list args) { lastLogTime = millis(); SERIALCONSOLE.print(lastLogTime); SERIALCONSOLE.print(" - "); switch (level) { case Debug: SERIALCONSOLE.print("DEBUG"); break; case Info: SERIALCONSOLE.print("INFO"); break; case Warn: SERIALCONSOLE.print("WARNING"); break; case Error: SERIALCONSOLE.print("ERROR"); break; } SERIALCONSOLE.print(": "); logMessage(format, args); } /* * Output a log message (called by log(), console()) * * Supports printf() like syntax: * * %% - outputs a '%' character * %s - prints the next parameter as string * %d - prints the next parameter as decimal * %f - prints the next parameter as double float * %x - prints the next parameter as hex value * %X - prints the next parameter as hex value with '0x' added before * %b - prints the next parameter as binary value * %B - prints the next parameter as binary value with '0b' added before * %l - prints the next parameter as long * %c - prints the next parameter as a character * %t - prints the next parameter as boolean ('T' or 'F') * %T - prints the next parameter as boolean ('true' or 'false') */ void Logger::logMessage(char *format, va_list args) { for (; *format != 0; ++format) { if (*format == '%') { ++format; if (*format == '\0') break; if (*format == '%') { SERIALCONSOLE.print(*format); continue; } if (*format == 's') { register char *s = (char *) va_arg( args, int ); SERIALCONSOLE.print(s); continue; } if (*format == 'd' || *format == 'i') { SERIALCONSOLE.print(va_arg( args, int ), DEC); continue; } if (*format == 'f') { SERIALCONSOLE.print(va_arg( args, double ), 3); continue; } if (*format == 'x') { SERIALCONSOLE.print(va_arg( args, int ), HEX); continue; } if (*format == 'X') { SERIALCONSOLE.print("0x"); SERIALCONSOLE.print(va_arg( args, int ), HEX); continue; } if (*format == 'b') { SERIALCONSOLE.print(va_arg( args, int ), BIN); continue; } if (*format == 'B') { SERIALCONSOLE.print("0b"); SERIALCONSOLE.print(va_arg( args, int ), BIN); continue; } if (*format == 'l') { SERIALCONSOLE.print(va_arg( args, long ), DEC); continue; } if (*format == 'c') { SERIALCONSOLE.print(va_arg( args, int )); continue; } if (*format == 't') { if (va_arg( args, int ) == 1) { SERIALCONSOLE.print("T"); } else { SERIALCONSOLE.print("F"); } continue; } if (*format == 'T') { if (va_arg( args, int ) == 1) { SERIALCONSOLE.print("TRUE"); } else { SERIALCONSOLE.print("FALSE"); } continue; } } SERIALCONSOLE.print(*format); } SERIALCONSOLE.println(); } ================================================ FILE: Logger.h ================================================ /* * Logger.h * Copyright (c) 2013 Collin Kidder, Michael Neuweiler, Charles Galpin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef LOGGER_H_ #define LOGGER_H_ #include #include "config.h" class Logger { public: enum LogLevel { Debug = 0, Info = 1, Warn = 2, Error = 3, Off = 4 }; static void debug(char *, ...); static void info(char *, ...); static void warn(char *, ...); static void error(char *, ...); static void console(char *, ...); static void setLoglevel(LogLevel); static LogLevel getLogLevel(); static uint32_t getLastLogTime(); static boolean isDebug(); private: static LogLevel logLevel; static uint32_t lastLogTime; static void log(LogLevel, char *format, va_list); static void logMessage(char *format, va_list args); }; #endif /* LOGGER_H_ */ ================================================ FILE: README.md ================================================ Arduino compatible project to interface with the BMS slave board on Tesla Model S modules. The modules are daisy-chained together with a TTL interface. The interface uses a Molex 15-97-5101 connector and runs at 612500 baud. This can be a difficult baud rate to match with arduino compatible processors. The Arduino Due and Teensy 3.5/3.6 boards are confirmed to be able to generate a suitably close baud rate. The factory wiring to each module is comprised of two sets of 5 differently colored wires: * Red = 5V input to the module * Green = Gnd for power and signal * Gray = Fault output * Yellow = UART Wire * Blue = UART Wire The fault output is active low. Use your own pull up to the fault line and if the line is pulled low then a fault has occurred. Here is a PDF that explains how the wiring between modules and the master board is supposed to be: https://cdn.hackaday.io/files/10098432032832/wiring.pdf ================================================ FILE: SerialConsole.cpp ================================================ /* * SerialConsole.cpp * Copyright (c) 2017 EVTV / Collin Kidder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include "SerialConsole.h" #include "Logger.h" #include "BMSModuleManager.h" template inline Print &operator <<(Print &obj, T arg) { obj.print(arg); return obj; } //Lets us stream SerialUSB extern EEPROMSettings settings; extern BMSModuleManager bms; bool printPrettyDisplay; uint32_t prettyCounter; int whichDisplay; SerialConsole::SerialConsole() { init(); } void SerialConsole::init() { //State variables for serial console ptrBuffer = 0; state = STATE_ROOT_MENU; loopcount=0; cancel=false; printPrettyDisplay = false; prettyCounter = 0; whichDisplay = 0; } void SerialConsole::loop() { if (SERIALCONSOLE.available()) { serialEvent(); } if (printPrettyDisplay && (millis() > (prettyCounter + 3000))) { prettyCounter = millis(); if (whichDisplay == 0) bms.printPackSummary(); if (whichDisplay == 1) bms.printPackDetails(); } } void SerialConsole::printMenu() { Logger::console("\n*************SYSTEM MENU *****************"); Logger::console("Enable line endings of some sort (LF, CR, CRLF)"); Logger::console("Most commands case sensitive\n"); Logger::console("GENERAL SYSTEM CONFIGURATION\n"); Logger::console(" E = dump system EEPROM values"); Logger::console(" h = help (displays this message)"); Logger::console(" S = Sleep all boards"); Logger::console(" W = Wake up all boards"); Logger::console(" C = Clear all board faults"); Logger::console(" F = Find all connected boards"); Logger::console(" R = Renumber connected boards in sequence"); Logger::console(" B = Attempt balancing for 5 seconds"); Logger::console(" p = Toggle output of pack summary every 3 seconds"); Logger::console(" d = Toggle output of pack details every 3 seconds"); Logger::console(" LOGLEVEL=%i - set log level (0=debug, 1=info, 2=warn, 3=error, 4=off)", Logger::getLogLevel()); Logger::console(" CANSPEED=%i - set first CAN bus speed", settings.canSpeed); Logger::console(" BATTERYID=%i - Set battery ID for CAN protocol (1-14)", settings.batteryID); Logger::console("\nBATTERY MANAGEMENT CONTROLS\n"); Logger::console(" VOLTLIMHI=%f - High limit for cells in volts", settings.OverVSetpoint); Logger::console(" VOLTLIMLO=%f - Low limit for cells in volts", settings.UnderVSetpoint); Logger::console(" TEMPLIMHI=%f - High limit for cell temperature in degrees C", settings.OverTSetpoint); Logger::console(" TEMPLIMLO=%f - Low limit for cell temperature in degrees C", settings.UnderTSetpoint); Logger::console(" BALVOLT=%f - Voltage at which to begin cell balancing", settings.balanceVoltage); Logger::console(" BALHYST=%f - How far voltage must dip before balancing is turned off", settings.balanceHyst); float OverVSetpoint; float UnderVSetpoint; float OverTSetpoint; float UnderTSetpoint; float balanceVoltage; float balanceHyst; } /* There is a help menu (press H or h or ?) Commands are submitted by sending line ending (LF, CR, or both) */ void SerialConsole::serialEvent() { int incoming; incoming = SERIALCONSOLE.read(); if (incoming == -1) { //false alarm.... return; } if (incoming == 10 || incoming == 13) { //command done. Parse it. handleConsoleCmd(); ptrBuffer = 0; //reset line counter once the line has been processed } else { cmdBuffer[ptrBuffer++] = (unsigned char) incoming; if (ptrBuffer > 79) ptrBuffer = 79; } } void SerialConsole::handleConsoleCmd() { if (state == STATE_ROOT_MENU) { if (ptrBuffer == 1) { //command is a single ascii character handleShortCmd(); } else { //if cmd over 1 char then assume (for now) that it is a config line handleConfigCmd(); } } } /*For simplicity the configuration setting code uses four characters for each configuration choice. This makes things easier for comparison purposes. */ void SerialConsole::handleConfigCmd() { int i; int newValue; float newFloat; bool needEEPROMWrite = false; //Logger::debug("Cmd size: %i", ptrBuffer); if (ptrBuffer < 6) return; //4 digit command, =, value is at least 6 characters cmdBuffer[ptrBuffer] = 0; //make sure to null terminate String cmdString = String(); unsigned char whichEntry = '0'; i = 0; while (cmdBuffer[i] != '=' && i < ptrBuffer) { cmdString.concat(String(cmdBuffer[i++])); } i++; //skip the = if (i >= ptrBuffer) { Logger::console("Command needs a value..ie TORQ=3000"); Logger::console(""); return; //or, we could use this to display the parameter instead of setting } // strtol() is able to parse also hex values (e.g. a string "0xCAFE"), useful for enable/disable by device id newValue = strtol((char *) (cmdBuffer + i), NULL, 0); newFloat = strtof((char *) (cmdBuffer + i), NULL); cmdString.toUpperCase(); if (cmdString == String("CANSPEED")) { if (newValue >= 33000 && newValue <= 1000000) { settings.canSpeed = newValue; Logger::console("Setting CAN speed to %i", newValue); needEEPROMWrite = true; } else Logger::console("Invalid speed. Enter a value between 33000 and 1000000"); } else if (cmdString == String("LOGLEVEL")) { switch (newValue) { case 0: Logger::setLoglevel(Logger::Debug); settings.logLevel = 0; Logger::console("setting loglevel to 'debug'"); break; case 1: Logger::setLoglevel(Logger::Info); settings.logLevel = 1; Logger::console("setting loglevel to 'info'"); break; case 2: Logger::console("setting loglevel to 'warning'"); settings.logLevel = 2; Logger::setLoglevel(Logger::Warn); break; case 3: Logger::console("setting loglevel to 'error'"); settings.logLevel = 3; Logger::setLoglevel(Logger::Error); break; case 4: Logger::console("setting loglevel to 'off'"); settings.logLevel = 4; Logger::setLoglevel(Logger::Off); break; } needEEPROMWrite = true; } else if (cmdString == String("BATTERYID")) { if (newValue > 0 && newValue < 15) { settings.batteryID = newValue; bms.setBatteryID(); needEEPROMWrite = true; Logger::console("Battery ID set to: %i", newValue); } else Logger::console("Invalid battery ID. Please enter a value between 1 and 14"); } else if (cmdString == String("VOLTLIMHI")) { if (newFloat >= 0.0f && newFloat <= 6.00f) { settings.OverVSetpoint = newFloat; needEEPROMWrite = true; Logger::console("Cell Voltage Upper Limit set to: %f", settings.OverVSetpoint); } else Logger::console("Invalid upper cell voltage limit. Please enter a value 0.0 to 6.0"); } else if (cmdString == String("VOLTLIMLO")) { if (newFloat >= 0.0f && newFloat <= 6.0f) { settings.UnderVSetpoint = newFloat; needEEPROMWrite = true; Logger::console("Cell Voltage Lower Limit set to %f", settings.UnderVSetpoint); } else Logger::console("Invalid lower cell voltage limit. Please enter a value 0.0 to 6.0"); } else if (cmdString == String("BALVOLT")) { if (newFloat >= 0.0f && newFloat <= 6.0f) { settings.balanceVoltage = newFloat; needEEPROMWrite = true; Logger::console("Balance voltage set to %f", settings.balanceVoltage); } else Logger::console("Invalid balancing voltage. Please enter a value 0.0 to 6.0"); } else if (cmdString == String("BALHYST")) { if (newFloat >= 0.0f && newFloat <= 1.0f) { settings.balanceHyst = newFloat; needEEPROMWrite = true; Logger::console("Balance hysteresis set to %f", settings.balanceHyst); } else Logger::console("Invalid balance hysteresis. Please enter a value 0.0 to 1.0"); } else if (cmdString == String("TEMPLIMHI")) { if (newFloat >= 0.0f && newFloat <= 100.0f) { settings.OverTSetpoint = newFloat; needEEPROMWrite=true; Logger::console("Module Temperature Upper Limit set to: %f", settings.OverTSetpoint); } else Logger::console("Invalid temperature upper limit please enter a value 0.0 to 100.0"); } else if (cmdString == String("TEMPLIMLO")) { if (newFloat >= -20.00f && newFloat <= 120.0f) { settings.UnderTSetpoint = newFloat; needEEPROMWrite = true; Logger::console("Module Temperature Lower Limit set to: %f", settings.UnderTSetpoint); } else Logger::console("Invalid temperature lower limit please enter a value between -20.0 and 120.0"); } else { Logger::console("Unknown command"); } if (needEEPROMWrite) { EEPROM.write(EEPROM_PAGE, settings); } } void SerialConsole::handleShortCmd() { uint8_t val; switch (cmdBuffer[0]) { case 'h': case '?': case 'H': printMenu(); break; case 'S': Logger::console("Sleeping all connected boards"); bms.sleepBoards(); break; case 'W': Logger::console("Waking up all connected boards"); bms.wakeBoards(); break; case 'C': Logger::console("Clearing all faults"); bms.clearFaults(); break; case 'F': bms.findBoards(); break; case 'R': Logger::console("Renumbering all boards."); bms.renumberBoardIDs(); break; case 'B': bms.balanceCells(); break; case 'p': if (whichDisplay == 1 && printPrettyDisplay) whichDisplay = 0; else { printPrettyDisplay = !printPrettyDisplay; if (printPrettyDisplay) { Logger::console("Enabling pack summary display, 5 second interval"); } else { Logger::console("No longer displaying pack summary."); } } break; case 'd': if (whichDisplay == 0 && printPrettyDisplay) whichDisplay = 1; else { printPrettyDisplay = !printPrettyDisplay; whichDisplay = 1; if (printPrettyDisplay) { Logger::console("Enabling pack details display, 5 second interval"); } else { Logger::console("No longer displaying pack details."); } } break; } } /* if (SERIALCONSOLE.available()) { char y = SERIALCONSOLE.read(); switch (y) { case '1': //ascii 1 renumberBoardIDs(); // force renumber and read out break; case '2': //ascii 2 SERIALCONSOLE.println(); findBoards(); break; case '3': //activate cell balance for 5 seconds SERIALCONSOLE.println(); SERIALCONSOLE.println("Balancing"); cellBalance(); break; case '4': //clear all faults on all boards, required after Reset or FPO (first power on) SERIALCONSOLE.println(); SERIALCONSOLE.println("Clearing Faults"); clearFaults(); break; case '5': //read out the status of first board SERIALCONSOLE.println(); SERIALCONSOLE.println("Reading status"); readStatus(1); break; case '6': //Read out the limit setpoints of first board SERIALCONSOLE.println(); SERIALCONSOLE.println("Reading Setpoints"); readSetpoint(1); SERIALCONSOLE.println(OVolt); SERIALCONSOLE.println(UVolt); SERIALCONSOLE.println(Tset); break; case '0': //Send all boards into Sleep state Serial.println(); Serial.println("Sleep Mode"); sleepBoards(); break; case '9'://Pull all boards out of Sleep state Serial.println(); Serial.println("Wake Boards"); wakeBoards(); break; } } */ ================================================ FILE: SerialConsole.h ================================================ /* * SerialConsole.h * Copyright (c) 2017 EVTV / Collin Kidder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef SERIALCONSOLE_H_ #define SERIALCONSOLE_H_ #include "config.h" class SerialConsole { public: SerialConsole(); void loop(); void printMenu(); protected: enum CONSOLE_STATE { STATE_ROOT_MENU }; private: char cmdBuffer[80]; int ptrBuffer; int state; int loopcount; bool cancel; void init(); void serialEvent(); void handleConsoleCmd(); void handleShortCmd(); void handleConfigCmd(); }; #endif /* SERIALCONSOLE_H_ */ ================================================ FILE: SystemIO.cpp ================================================ #include #include "config.h" #include "Logger.h" #include "SystemIO.h" int DigitalInputs[4] = {DIN1, DIN2, DIN3, DIN4}; int DigitalOutputs[4][2] = { {DOUT1_L, DOUT1_H}, {DOUT2_L, DOUT2_H}, {DOUT3_L, DOUT3_H}, {DOUT4_L, DOUT4_H} }; SystemIO::SystemIO() { } void SystemIO::setup() { for (int x = 0; x < 4; x++) pinMode(DigitalInputs[x], INPUT); for (int y = 0; y < 4; y++) { pinMode(DigitalOutputs[y][0], OUTPUT); digitalWrite(DigitalOutputs[y][0], LOW); pinMode(DigitalOutputs[y][1], OUTPUT); digitalWrite(DigitalOutputs[y][1], LOW); } } bool SystemIO::readInput(int pin) { if (pin < 0 || pin > 3) return false; return !digitalRead(DigitalInputs[pin]); } void SystemIO::setOutput(int pin, OUTPUTSTATE state) { if (pin < 0 || pin > 3) return; //first set it floating digitalWrite(DigitalOutputs[pin][0], LOW); digitalWrite(DigitalOutputs[pin][1], LOW); delayMicroseconds(10); //give mosfets some time to turn off if (state == HIGH_12V) digitalWrite(DigitalOutputs[pin][1], HIGH); if (state == GND) digitalWrite(DigitalOutputs[pin][0], HIGH); } SystemIO systemIO; ================================================ FILE: SystemIO.h ================================================ #pragma once enum OUTPUTSTATE { FLOATING = 0, HIGH_12V = 1, GND = 2 }; class SystemIO { public: SystemIO(); void setup(); bool readInput(int pin); void setOutput(int pin, OUTPUTSTATE state); private: }; extern SystemIO systemIO; ================================================ FILE: TeslaBMS.ino ================================================ #if defined (__arm__) && defined (__SAM3X8E__) #include #endif #include #include "Logger.h" #include "SerialConsole.h" #include "BMSModuleManager.h" #include "SystemIO.h" #include #include #include //#define BMS_BAUD 612500 #define BMS_BAUD 617647 //#define BMS_BAUD 608695 BMSModuleManager bms; EEPROMSettings settings; SerialConsole console; uint32_t lastUpdate; //This code only applicable to Due to fixup lack of functionality in the arduino core. #if defined (__arm__) && defined (__SAM3X8E__) void serialSpecialInit(Usart *pUsart, uint32_t baudRate) { // Reset and disable receiver and transmitter pUsart->US_CR = UART_CR_RSTRX | UART_CR_RSTTX | UART_CR_RXDIS | UART_CR_TXDIS; // Configure mode pUsart->US_MR = US_MR_CHRL_8_BIT | US_MR_NBSTOP_1_BIT | UART_MR_PAR_NO | US_MR_USART_MODE_NORMAL | US_MR_USCLKS_MCK | US_MR_CHMODE_NORMAL | US_MR_OVER; // | US_MR_INVDATA; //Get the integer divisor that can provide the baud rate int divisor = SystemCoreClock / baudRate; int error1 = abs(baudRate - (SystemCoreClock / divisor)); //find out how close that is to the real baud int error2 = abs(baudRate - (SystemCoreClock / (divisor + 1))); //see if bumping up one on the divisor makes it a better match if (error2 < error1) divisor++; //If bumping by one yielded a closer rate then use that instead // Configure baudrate including the optional fractional divisor possible on USART pUsart->US_BRGR = (divisor >> 3) | ((divisor & 7) << 16); // Enable receiver and transmitter pUsart->US_CR = UART_CR_RXEN | UART_CR_TXEN; } #endif void loadSettings() { EEPROM.read(EEPROM_PAGE, settings); if (settings.version != EEPROM_VERSION) //if settings are not the current version then erase them and set defaults { Logger::console("Resetting to factory defaults"); settings.version = EEPROM_VERSION; settings.checksum = 0; settings.canSpeed = 500000; settings.batteryID = 0x01; //in the future should be 0xFF to force it to ask for an address settings.OverVSetpoint = 4.1f; settings.UnderVSetpoint = 2.3f; settings.OverTSetpoint = 65.0f; settings.UnderTSetpoint = -10.0f; settings.balanceVoltage = 3.9f; settings.balanceHyst = 0.04f; settings.logLevel = 2; EEPROM.write(EEPROM_PAGE, settings); } else { Logger::console("Using stored values from EEPROM"); } Logger::setLoglevel((Logger::LogLevel)settings.logLevel); } void initializeCAN() { uint32_t id; Can0.begin(settings.canSpeed); if (settings.batteryID < 0xF) { //Setup filter for direct access to our registered battery ID id = (0xBAul << 20) + (((uint32_t)settings.batteryID & 0xF) << 16); Can0.setRXFilter(0, id, 0x1FFF0000ul, true); //Setup filter for request for all batteries to give summary data id = (0xBAul << 20) + (0xFul << 16); Can0.setRXFilter(1, id, 0x1FFF0000ul, true); } } void setup() { delay(4000); //just for easy debugging. It takes a few seconds for USB to come up properly on most OS's SERIALCONSOLE.begin(115200); SERIALCONSOLE.println("Starting up!"); SERIAL.begin(BMS_BAUD); #if defined (__arm__) && defined (__SAM3X8E__) serialSpecialInit(USART0, BMS_BAUD); //required for Due based boards as the stock core files don't support 612500 baud. #endif SERIALCONSOLE.println("Started serial interface to BMS."); pinMode(13, INPUT); loadSettings(); initializeCAN(); systemIO.setup(); bms.renumberBoardIDs(); //Logger::setLoglevel(Logger::Debug); lastUpdate = 0; bms.clearFaults(); } void loop() { CAN_FRAME incoming; console.loop(); if (millis() > (lastUpdate + 1000)) { lastUpdate = millis(); bms.balanceCells(); bms.getAllVoltTemp(); } if (Can0.available()) { Can0.read(incoming); bms.processCANMsg(incoming); } } ================================================ FILE: config.h ================================================ #pragma once #include //Set to the proper port for your USB connection - SerialUSB on Due (Native) or Serial for Due (Programming) or Teensy #define SERIALCONSOLE SerialUSB //Define this to be the serial port the Tesla BMS modules are connected to. //On the Due you need to use a USART port (Serial1, Serial2, Serial3) and update the call to serialSpecialInit if not Serial1 #define SERIAL Serial1 #define REG_DEV_STATUS 0 #define REG_GPAI 1 #define REG_VCELL1 3 #define REG_VCELL2 5 #define REG_VCELL3 7 #define REG_VCELL4 9 #define REG_VCELL5 0xB #define REG_VCELL6 0xD #define REG_TEMPERATURE1 0xF #define REG_TEMPERATURE2 0x11 #define REG_ALERT_STATUS 0x20 #define REG_FAULT_STATUS 0x21 #define REG_COV_FAULT 0x22 #define REG_CUV_FAULT 0x23 #define REG_ADC_CTRL 0x30 #define REG_IO_CTRL 0x31 #define REG_BAL_CTRL 0x32 #define REG_BAL_TIME 0x33 #define REG_ADC_CONV 0x34 #define REG_ADDR_CTRL 0x3B #define MAX_MODULE_ADDR 0x3E #define EEPROM_VERSION 0x10 //update any time EEPROM struct below is changed. #define EEPROM_PAGE 0 #define DIN1 55 #define DIN2 54 #define DIN3 57 #define DIN4 56 #define DOUT4_H 2 #define DOUT4_L 3 #define DOUT3_H 4 #define DOUT3_L 5 #define DOUT2_H 6 #define DOUT2_L 7 #define DOUT1_H 8 #define DOUT1_L 9 typedef struct { uint8_t version; uint8_t checksum; uint32_t canSpeed; uint8_t batteryID; //which battery ID should this board associate as on the CAN bus uint8_t logLevel; float OverVSetpoint; float UnderVSetpoint; float OverTSetpoint; float UnderTSetpoint; float balanceVoltage; float balanceHyst; } EEPROMSettings;