Twizy-Virtual-BMS/examples/BlazejBMS/BlazejBMS.ino
Michael Balzer ce7c6e8311 - New API calls stateName(), stateName(state)
- No out of bounds error messages in TWIZY_DEBUG_LEVEL 0
- Donors update
2017-08-17 22:59:15 +02:00

421 lines
12 KiB
C++

/**
* ==========================================================================
* Blazej's Twizy Lead Acid "BMS"
* ==========================================================================
*
* Hardware setup:
* - Arduino Nano + NiRen MCP2515_CAN (16 MHz) + relay + power regulator
* - HC-06 Bluetooth module (AltSoftSerial pin 8+9)
* Sensors:
* - 1x LM35D temperature sensor (PORT_TEMP)
* - 4x voltage divider at 60/45/30/15 V (PORT_C1…4)
*
* Authors:
* - Michael Balzer <dexter@dexters-web.de>
* - Błażej Błaszczyk <blazej.blaszczyk@pascal-engineering.com>
*
* Libraries used:
* - TwizyVirtualBMS https://github.com/dexterbg/Twizy-Virtual-BMS
* - MCP_CAN https://github.com/coryjfowler/MCP_CAN_lib
* - FlexiTimer2 https://github.com/PaulStoffregen/FlexiTimer2
* - AltSoftSerial https://github.com/PaulStoffregen/AltSoftSerial
*
* License:
* This is free software under GNU Lesser General Public License (LGPL)
* https://www.gnu.org/licenses/lgpl.html
*
*/
#define BLAZEJ_BMS_VERSION "V2.1.0 (2017-07-05)"
#include "TwizyVirtualBMS_config.h"
#include "TwizyVirtualBMS.h"
#include "AltSoftSerial.h"
#include "BlazejBMS_config.h"
TwizyVirtualBMS twizy;
// Bluetooth software serial port:
// Note: AltSoftSerial uses fixed pins!
// i.e. Arduino Nano: RX = pin 8, TX = pin 9
AltSoftSerial bt;
// --------------------------------------------------------------------------
// State variables
//
float temp = 20.0;
float
vpack = VMAX_DRV;
float
c1 = VMAX_DRV / 4,
c2 = VMAX_DRV / 4,
c3 = VMAX_DRV / 4,
c4 = VMAX_DRV / 4;
float
soc = 99.0;
float curr = 0.0;
int drvpwr = MAX_DRIVE_POWER;
int recpwr = MAX_RECUP_POWER;
int chgcur = MAX_CHARGE_CURRENT;
unsigned long error = TWIZY_OK;
// --------------------------------------------------------------------------
// Callback: handle state transition for BMS
// - called by twizyEnterState() after Twizy handling
// Note: avoid complex operations, this needs to be fast.
//
void bmsEnterState(TwizyState currentState, TwizyState newState) {
// lower SOC at switch-on to prevent immediate charge stop:
if (currentState == Init && newState == Ready) {
if (soc > 99) {
soc -= 1;
twizy.setSOC(soc);
twizy.setChargeCurrent(5);
Serial.print(F("bmsEnterState: soc lowered to "));
Serial.println(soc, 1);
}
}
}
// --------------------------------------------------------------------------
// Callback: timer ticker
// - called every 10 ms by twizyTicker() after twizy handling
// - clockCnt cyclic range: 0 .. 2999 = 30 seconds (reset to 0 on Off/Init)
// Note: avoid complex operations, this needs to be fast.
//
void bmsTicker(unsigned int clockCnt) {
// full second?
if (clockCnt % 100 != 0) {
return;
}
if (twizy.state() != Off) {
Serial.println(F("\nbmsTicker:"));
error = TWIZY_OK;
// ----------------------------------------------------------------------
// bmsTicker: Read stacked cell voltages
//
c1 = analogRead(PORT_C1) / 1024.0 * SCALE_C1; // 60 V
c2 = analogRead(PORT_C2) / 1024.0 * SCALE_C2; // 45 V
c3 = analogRead(PORT_C3) / 1024.0 * SCALE_C3; // 30 V
c4 = analogRead(PORT_C4) / 1024.0 * SCALE_C4; // 15 V
#if INPUT_CALIBRATION >= 1
// raw output for calibration:
Serial.print(F("< raw c1 = ")); Serial.println(c1, 2);
Serial.print(F("< c2 = ")); Serial.println(c2, 2);
Serial.print(F("< c3 = ")); Serial.println(c3, 2);
Serial.print(F("< c4 = ")); Serial.println(c4, 2);
#endif
// derive single cell voltages from stacked voltages:
vpack = c1;
c1 -= c2;
c2 -= c3;
c3 -= c4;
// ----------------------------------------------------------------------
// bmsTicker: Derive SOC from voltage
//
// - newsoc = SOC in operation mode voltage range
// - soc = smoothed operation mode SOC
// (used to derive drive & recup power & charge current)
//
float newsoc;
// voltage range depends on operation mode:
if (twizy.state() == Charging) {
newsoc = (vpack - VMIN_CHG) / (VMAX_CHG - VMIN_CHG) * 100.0;
}
else {
newsoc = (vpack - VMIN_DRV) / (VMAX_DRV - VMIN_DRV) * 100.0;
}
// smooth...
if (newsoc < soc) {
// slow adaption to lower voltages:
soc = ((soc * (SMOOTH_SOC_DOWN-1)) + newsoc) / SMOOTH_SOC_DOWN;
}
else {
if (twizy.state() == Charging) {
// fast adaption while charging:
soc = ((soc * (SMOOTH_SOC_UP_CHG-1)) + newsoc) / SMOOTH_SOC_UP_CHG;
}
else {
// slow adaption while driving:
soc = ((soc * (SMOOTH_SOC_UP-1)) + newsoc) / SMOOTH_SOC_UP;
}
}
// sanitize...
soc = constrain(soc, 0.0, 100.0);
// ----------------------------------------------------------------------
// bmsTicker: Derive power limits & charge current from SOC
//
// scale down drive power for low SOC:
// 100% at FULL → 100% at <SOC1> → <LVL2> at <SOC2> → 0% at EMPTY
#define SOC2_DRIVE_POWER ((DRV_CUTBACK_LVL2 / 100.0f) * MAX_DRIVE_POWER)
if (soc <= DRV_CUTBACK_SOC2) {
float factor = soc / DRV_CUTBACK_SOC2;
drvpwr = factor * SOC2_DRIVE_POWER;
}
else if (soc <= DRV_CUTBACK_SOC1) {
float factor = ((soc - DRV_CUTBACK_SOC2) / (DRV_CUTBACK_SOC1 - DRV_CUTBACK_SOC2));
drvpwr = SOC2_DRIVE_POWER + (factor * (MAX_DRIVE_POWER - SOC2_DRIVE_POWER));
}
else {
drvpwr = MAX_DRIVE_POWER;
}
// scale down recuperation power & charge current for high SOC:
// 0% at FULL → 100% at <CHG_CUTBACK_SOC> → 100% at EMPTY
if (soc > 99.99) {
// stop charge & reduce recuperation at 100% SOC:
recpwr = 500; // TODO: should be 0 when driving, but affects D/R change
chgcur = 0;
}
else if (soc >= CHG_CUTBACK_SOC) {
// keep min 1000W / 5A below 100% SOC:
float factor = ((100 - soc) / (100 - CHG_CUTBACK_SOC));
recpwr = 1000 + (factor * (MAX_RECUP_POWER - 1000));
chgcur = 5 + (factor * (MAX_CHARGE_CURRENT - 5));
}
else {
recpwr = MAX_RECUP_POWER;
chgcur = MAX_CHARGE_CURRENT;
}
// ------------------------------------------------------------
// bmsTicker: Check cell voltage difference (min - max)
//
float cmin = min(c1, min(c2, min(c3, c4)));
float cmax = max(c1, max(c2, max(c3, c4)));
float cdif = cmax - cmin;
#if INPUT_CALIBRATION >= 1
Serial.print(F("> cmin = ")); Serial.println(cmin, 2);
Serial.print(F("> cmax = ")); Serial.println(cmax, 2);
Serial.print(F("> cdif = ")); Serial.println(cdif, 2);
#endif
if (cdif >= VOLT_DIFF_SHUTDOWN) {
// cell difference is critical: emergency shutdown
Serial.println(F("!!! VOLT_SHUTDOWN"));
bt.println(F("!!! VOLT_SHUTDOWN"));
error |= TWIZY_SERV_BATT | TWIZY_SERV_STOP;
twizy.enterState(Error);
}
else if (cdif >= VOLT_DIFF_ERROR) {
// cell difference is high: set STOP signal, reduce drive power, stop recuperation & charge:
Serial.println(F("!!! VOLT_ERROR"));
bt.println(F("!!! VOLT_ERROR"));
error |= TWIZY_SERV_BATT | TWIZY_SERV_STOP;
drvpwr /= 4;
recpwr /= 4;
chgcur = 0;
}
else if (cdif >= VOLT_DIFF_WARN) {
// cell difference detected: reduce power & charge levels:
Serial.println(F("!!! VOLT_WARN"));
bt.println(F("!!! VOLT_WARN"));
error |= TWIZY_SERV_BATT;
drvpwr /= 2;
recpwr /= 2;
chgcur = min(chgcur, 5);
}
// ----------------------------------------------------------------------
// bmsTicker: Read & check battery temperature
//
float newtemp;
// dual read _seems_ to yield better results (LM35D issue?)
newtemp = analogRead(PORT_TEMP);
newtemp = BASE_TEMP + analogRead(PORT_TEMP) / 1024.0 * SCALE_TEMP;
// raw output for calibration:
#if INPUT_CALIBRATION >= 1
Serial.print(F("< temp = ")); Serial.println(newtemp, 2);
#endif
// smooth...
temp = (temp * (SMOOTH_TEMP-1) + newtemp) / SMOOTH_TEMP;
if (temp > TEMP_SHUTDOWN) {
// battery is burning: emergency shutdown
Serial.println(F("!!! TEMP_SHUTDOWN"));
bt.println(F("!!! TEMP_SHUTDOWN"));
error |= TWIZY_SERV_TEMP | TWIZY_SERV_STOP;
twizy.enterState(Error);
}
else if (temp > TEMP_ERROR) {
// battery very hot: set STOP signal, stop recuperation, stop charge:
Serial.println(F("!!! TEMP_ERROR"));
bt.println(F("!!! TEMP_ERROR"));
error |= TWIZY_SERV_TEMP | TWIZY_SERV_STOP;
drvpwr /= 4;
recpwr = 0;
chgcur = 0;
}
else if (temp > TEMP_WARN) {
// battery hot, show warning, reduce recuperation, reduce charge current:
Serial.println(F("!!! TEMP_WARN"));
bt.println(F("!!! TEMP_WARN"));
error |= TWIZY_SERV_TEMP;
drvpwr /= 2;
recpwr /= 2;
chgcur = min(chgcur, 5);
}
// ----------------------------------------------------------------------
// bmsTicker: Estimate current level
//
if (twizy.state() == Charging) {
curr = chgcur;
}
else {
// OPTION: derive current estimation from voltage difference:
//curr = ((newsoc - soc) / 100) * SCALE_CURRENT + OFFSET_CURRENT;
//curr = constrain(curr, -500.0, 500.0);
curr = 0.0;
}
// ----------------------------------------------------------------------
// bmsTicker: Update Twizy state
//
#if INPUT_CALIBRATION >= 1
Serial.println();
#endif
twizy.setVoltage(vpack, true);
twizy.setCurrent(curr);
twizy.setTemperature(temp, temp, true);
twizy.setSOC(soc);
twizy.setPowerLimits(drvpwr, recpwr);
twizy.setChargeCurrent(chgcur);
twizy.setError(error);
// ----------------------------------------------------------------------
// bmsTicker: Output state to serial port
//
Serial.println();
Serial.print(F("- volt = ")); Serial.println(vpack, 1);
Serial.print(F("- ...c1 = ")); Serial.println(c1, 1);
Serial.print(F("- ...c2 = ")); Serial.println(c2, 1);
Serial.print(F("- ...c3 = ")); Serial.println(c3, 1);
Serial.print(F("- ...c4 = ")); Serial.println(c4, 1);
Serial.print(F("- temp = ")); Serial.println(temp, 1);
Serial.print(F("- curr = ")); Serial.println(curr, 1);
Serial.println();
Serial.print(F("- soc% = ")); Serial.println(soc, 1);
Serial.print(F("- drvpwr = ")); Serial.println(drvpwr);
Serial.print(F("- recpwr = ")); Serial.println(recpwr);
Serial.print(F("- chgcur = ")); Serial.println(chgcur);
// ----------------------------------------------------------------------
// bmsTicker: Output state to bluetooth port
//
bt.print(twizy.stateName());
bt.print(F(" -- "));
bt.print(temp, 1); bt.print(F(" °C -- "));
bt.print(soc, 1); bt.print(F(" %SOC -- "));
bt.println();
if (error != TWIZY_OK) {
bt.print(F("ERROR: "));
bt.println(error & 0x0fff, HEX);
}
bt.print(vpack, 1); bt.print(F(" V -- "));
bt.print(curr, 1); bt.print(F(" A -- "));
bt.print(cdif, 1); bt.print(F(" CD"));
bt.println();
bt.print(c1, 1); bt.print(F(" C1 -- "));
bt.print(c2, 1); bt.print(F(" C2 -- "));
bt.print(c3, 1); bt.print(F(" C3 -- "));
bt.print(c4, 1); bt.print(F(" C4 -- "));
bt.println();
bt.println();
} // if (twizy.state() != Off)
} // bmsTicker()
// -----------------------------------------------------
// SETUP
//
void setup() {
Serial.begin(115200);
Serial.println(F("Blazej-BMS " BLAZEJ_BMS_VERSION));
bt.begin(BT_BAUD);
twizy.begin();
twizy.attachTicker(bmsTicker);
twizy.attachEnterState(bmsEnterState);
// Init:
twizy.setPowerLimits(drvpwr, recpwr);
twizy.setChargeCurrent(chgcur);
twizy.setSOC(soc);
twizy.setTemperature(temp, temp, true);
twizy.setVoltage(vpack, true);
twizy.setError(error);
twizy.setSOH(100);
twizy.setCurrent(0.0);
#if TWIZY_CAN_SEND == 0
// Dry run:
twizy.enterState(Ready);
#endif
}
// -----------------------------------------------------
// MAIN LOOP
//
void loop() {
twizy.looper();
}