- Added BlazejBMS example

This commit is contained in:
Michael Balzer 2017-07-15 11:29:56 +02:00
parent 365ee37fa2
commit 71550230e7
3 changed files with 553 additions and 0 deletions

View file

@ -0,0 +1,421 @@
/**
* ==========================================================================
* 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_C14)
*
* 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 to prevent immediate charge stop:
if (newState == Init) {
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(FS(twizyStateName[twizy.state()]));
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();
}

View file

@ -0,0 +1,88 @@
/**
* ==========================================================================
* Blazej's Twizy Lead Acid "BMS": Configuration
* ==========================================================================
*/
#ifndef _BlazejBMS_config_h
#define _BlazejBMS_config_h
// Bluetooth baud rate: (i.e. 57600 / 38400 / 19200 / 9600)
#define BT_BAUD 9600
// Maximum charge current to use [A]:
#define MAX_CHARGE_CURRENT 30
// Charge current → power drawn from socket:
// 35 A = 2,2 kW
// 30 A = 2,1 kW
// 25 A = 1,7 kW
// 20 A = 1,4 kW
// 15 A = 1,0 kW
// 10 A = 0,7 kW
// 5 A = 0,4 kW
// Maximum driving & recuperation power limits to use [W]:
#define MAX_DRIVE_POWER 18000
#define MAX_RECUP_POWER 10000
// Drive power cutback [%]:
// (100% at FULL → 100% at <SOC1>% → <LVL2>% at <SOC2>% → 0% at EMPTY)
#define DRV_CUTBACK_SOC1 90
#define DRV_CUTBACK_SOC2 45
#define DRV_CUTBACK_LVL2 50
// Charge power cutback [%]:
// (100% at EMPTY → 100% at <SOC>% → 0% at FULL)
#define CHG_CUTBACK_SOC 85
// Lead acid voltage range for discharging [V]:
#define VMIN_DRV (4 * 11.2)
#define VMAX_DRV (4 * 13.0)
// Lead acid voltage range for charging [V]:
#define VMIN_CHG (4 * 12.0)
#define VMAX_CHG (4 * 14.4)
// Analog input port assignment:
#define PORT_TEMP A0 // temperature sensor LM35D
#define PORT_C1 A1 // voltage divider 60 V
#define PORT_C2 A2 // voltage divider 45 V
#define PORT_C3 A3 // voltage divider 30 V
#define PORT_C4 A4 // voltage divider 15 V
// Set to 1 to enable input port calibration outputs:
#define INPUT_CALIBRATION 0
// Voltage analog input scaling:
#define SCALE_C1 (60.0 / 3.8202247191 * 5.0 * 1.02652)
#define SCALE_C2 (45.0 / 4.0909090909 * 5.0 * 1.01857)
#define SCALE_C3 (30.0 / 3.7918215613 * 5.0 * 1.02534)
#define SCALE_C4 (15.0 / 3.8059701492 * 5.0 * 1.02344)
// Voltage warning/error thresholds [V]:
#define VOLT_DIFF_WARN 3.0
#define VOLT_DIFF_ERROR 5.0
#define VOLT_DIFF_SHUTDOWN 10.0
// SOC smoothing [samples]:
#define SMOOTH_SOC_DOWN 700 // adaption to lower voltage
#define SMOOTH_SOC_UP 400 // adaption to higher voltage
#define SMOOTH_SOC_UP_CHG 50 // adaption to higher voltage while charging
// OPTION: Scale voltage to SOC difference to current [A]:
//#define SCALE_CURRENT 150
//#define OFFSET_CURRENT -70
// Temperature analog input scaling:
// LM35D: +2 .. +100°, 10 mV / °C => 100 °C = 1.0 V
#define BASE_TEMP 2.0
#define SCALE_TEMP 500.0
// Temperature smoothing [samples]:
#define SMOOTH_TEMP 30
// Temperature warning/error thresholds [°C]:
#define TEMP_WARN 40
#define TEMP_ERROR 45
#define TEMP_SHUTDOWN 50
#endif // _BlazejBMS_config_h

View file

@ -0,0 +1,44 @@
/**
* ==========================================================================
* Twizy Virtual BMS: Configuration
* ==========================================================================
*/
#ifndef _TwizyVirtualBMS_config_h
#define _TwizyVirtualBMS_config_h
// Serial debug output:
// Level 0 = none, only output init & error messages
// Level 1 = log state transitions & CAN statistics
// Level 2 = log CAN frame dumps (10 second interval)
#define TWIZY_DEBUG_LEVEL 1
// Set to 0 to disable CAN transmissions for testing:
#define TWIZY_CAN_SEND 1
// CAN send timing is normally 10 ms (10.000 us).
// You may need to lower this if your Arduino is too slow.
#define TWIZY_CAN_CLOCK_US 10000
// VirtualBMS can use Timer1 (16 bit), Timer2 (8 bit) or Timer3 (16bit)
// Select Timer2/3 if you need Timer1 for e.g. AltSoftSerial
#define TWIZY_USE_TIMER 2
// Timer2: precise resolutions depend on CPU type & clock frequency
// i.e. Arduino Nano 16 MHz: 1000 = 1 ms / 2000 = 0.5 ms / 5000 = 0.2 ms / 10000 = 0.1 ms
// Use lowest resolution possible to minimize side effects on AltSoftSerial
#define TWIZY_TIMER2_RESOLUTION 1000
// Set your MCP clock frequency here:
#define TWIZY_CAN_MCP_FREQ MCP_16MHZ
// Set your SPI CS pin number here:
#define TWIZY_CAN_CS_PIN 10
// If you've connected the CAN module's IRQ pin:
#define TWIZY_CAN_IRQ_PIN 2
// Set your 3MW control pin here:
#define TWIZY_3MW_CONTROL_PIN 3
#endif // _TwizyVirtualBMS_config_h