Skip to content

Commit

Permalink
Merge pull request #728 from dalathegreat/feature/bmw-sbox
Browse files Browse the repository at this point in the history
Feature/bmw sbox
  • Loading branch information
dalathegreat authored Jan 2, 2025
2 parents 608b082 + 1f74953 commit 184d929
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Software/Software.ino
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ void setup() {
setup_battery();
#ifdef EQUIPMENT_STOP_BUTTON
init_equipment_stop_button();
#endif
#ifdef CAN_SHUNT_SELECTED
setup_can_shunt();
#endif
// BOOT button at runtime is used as an input for various things
pinMode(0, INPUT_PULLUP);
Expand Down
3 changes: 2 additions & 1 deletion Software/USER_SETTINGS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ volatile CAN_Configuration can_config = {
.battery = CAN_NATIVE, // Which CAN is your battery connected to?
.inverter = CAN_NATIVE, // Which CAN is your inverter connected to? (No need to configure incase you use RS485)
.battery_double = CAN_ADDON_MCP2515, // (OPTIONAL) Which CAN is your second battery connected to?
.charger = CAN_NATIVE // (OPTIONAL) Which CAN is your charger connected to?
.charger = CAN_NATIVE, // (OPTIONAL) Which CAN is your charger connected to?
.shunt = CAN_NATIVE // (OPTIONAL) Which CAN is your shunt connected to?
};

std::string ssid = WIFI_SSID; // Set in USER_SECRETS.h
Expand Down
4 changes: 4 additions & 0 deletions Software/USER_SETTINGS.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
//#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM for CONTACTOR_CONTROL, which lowers power consumption and heat generation. CONTACTOR_CONTROL must be enabled.
//#define NC_CONTACTORS //Enable this line to control normally closed contactors. CONTACTOR_CONTROL must be enabled for this option. Extremely rare setting!

/* Shunt/Contactor settings */
//#define BMW_SBOX // SBOX relay control & battery current/voltage measurement

/* Other options */
//#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs (WARNING, raises CPU load, do not use for production)
//#define DEBUG_VIA_WEB //Enable this line to log diagnostic data while program runs, which can be viewed via webpage (WARNING, slightly raises CPU load, do not use for production)
Expand Down Expand Up @@ -143,6 +146,7 @@ typedef struct {
CAN_Interface inverter;
CAN_Interface battery_double;
CAN_Interface charger;
CAN_Interface shunt;
} CAN_Configuration;
extern volatile CAN_Configuration can_config;
extern volatile uint8_t AccessPointEnabled;
Expand Down
7 changes: 7 additions & 0 deletions Software/src/battery/BATTERIES.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
#define BATTERIES_H
#include "../../USER_SETTINGS.h"

#ifdef BMW_SBOX
#include "BMW-SBOX.h"
void handle_incoming_can_frame_shunt(CAN_frame rx_frame);
void transmit_can_shunt();
void setup_can_shunt();
#endif

#ifdef BMW_I3_BATTERY
#include "BMW-I3-BATTERY.h"
#endif
Expand Down
218 changes: 218 additions & 0 deletions Software/src/battery/BMW-SBOX.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include "../include.h"
#ifdef BMW_SBOX
#include "../datalayer/datalayer.h"
#include "BMW-SBOX.h"

#define MAX_ALLOWED_FAULT_TICKS 1000

enum SboxState { DISCONNECTED, PRECHARGE, NEGATIVE, POSITIVE, PRECHARGE_OFF, COMPLETED, SHUTDOWN_REQUESTED };
SboxState contactorStatus = DISCONNECTED;

unsigned long prechargeStartTime = 0;
unsigned long negativeStartTime = 0;
unsigned long positiveStartTime = 0;
unsigned long timeSpentInFaultedMode = 0;
unsigned long LastMsgTime = 0; // will store last time a 20ms CAN Message was send
unsigned long LastAvgTime = 0; // Last current storage time
unsigned long ShuntLastSeen = 0;

uint32_t avg_mA_array[10];
uint32_t avg_sum;

uint8_t k; //avg array pointer

uint8_t CAN100_cnt = 0;

CAN_frame SBOX_100 = {.FD = false,
.ext_ID = false,
.DLC = 4,
.ID = 0x100,
.data = {0x55, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00}}; // Byte 0: relay control, Byte 1: counter 0-E, Byte 4: CRC

CAN_frame SBOX_300 = {.FD = false,
.ext_ID = false,
.DLC = 4,
.ID = 0x300,
.data = {0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}}; // Static frame

uint8_t reverse_bits(uint8_t byte) {
uint8_t reversed = 0;
for (int i = 0; i < 8; i++) {
reversed = (reversed << 1) | (byte & 1);
byte >>= 1;
}
return reversed;
}

/** CRC8, both inverted, poly 0x31 **/
uint8_t calculateCRC(CAN_frame CAN) {
uint8_t crc = 0;
for (size_t i = 0; i < CAN.DLC; i++) {
uint8_t reversed_byte = reverse_bits(CAN.data.u8[i]);
crc ^= reversed_byte;
for (int j = 0; j < 8; j++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc <<= 1;
}
crc &= 0xFF;
}
}
crc = reverse_bits(crc);
return crc;
}

void handle_incoming_can_frame_shunt(CAN_frame rx_frame) {
unsigned long currentTime = millis();
if (rx_frame.ID == 0x200) {
ShuntLastSeen = currentTime;
datalayer.shunt.measured_amperage_mA =
((rx_frame.data.u8[2] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[0] << 8)) / 256;
datalayer.shunt.measured_amperage_dA = datalayer.shunt.measured_amperage_mA / 100;

/** Calculate 1S avg current **/
if (LastAvgTime + 100 < currentTime) {
LastAvgTime = currentTime;
if (k > 9) {
k = 0;
}
avg_mA_array[k] = datalayer.shunt.measured_amperage_mA;
k++;
avg_sum = 0;
for (uint8_t i = 0; i < 10; i++) {
avg_sum = avg_sum + avg_mA_array[i];
}
datalayer.shunt.measured_avg1S_amperage_mA = avg_sum / 10;
}
} else if (rx_frame.ID == 0x210) //SBOX input (battery side) voltage
{
ShuntLastSeen = currentTime;
datalayer.shunt.measured_voltage_mV =
((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[1] << 8) | (rx_frame.data.u8[0]));
} else if (rx_frame.ID == 0x220) //SBOX output voltage
{
ShuntLastSeen = currentTime;
datalayer.shunt.measured_outvoltage_mV =
((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[1] << 8) | (rx_frame.data.u8[0]));
datalayer.shunt.available = true;
}
}

void transmit_can_shunt() {
unsigned long currentTime = millis();

/** Shunt can frames seen? **/
if (ShuntLastSeen + 1000 < currentTime) {
datalayer.shunt.available = false;
} else {
datalayer.shunt.available = true;
}
// Send 20ms CAN Message
if (currentTime - LastMsgTime >= INTERVAL_20_MS) {
LastMsgTime = currentTime;
// First check if we have any active errors, incase we do, turn off the battery
if (datalayer.battery.status.bms_status == FAULT) {
timeSpentInFaultedMode++;
} else {
timeSpentInFaultedMode = 0;
}

//handle contactor control SHUTDOWN_REQUESTED
if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS) {
contactorStatus = SHUTDOWN_REQUESTED;
SBOX_100.data.u8[0] = 0x55; // All open
}

if (contactorStatus == SHUTDOWN_REQUESTED) {
datalayer.shunt.contactors_engaged = false;
return; // A fault scenario latches the contactor control. It is not possible to recover without a powercycle (and investigation why fault occured)
}

// After that, check if we are OK to start turning on the contactors
if (contactorStatus == DISCONNECTED) {
datalayer.shunt.contactors_engaged = false;
SBOX_100.data.u8[0] = 0x55; // All open

if (datalayer.system.status.battery_allows_contactor_closing &&
datalayer.system.status.inverter_allows_contactor_closing &&
!datalayer.system.settings.equipment_stop_active &&
(datalayer.shunt.measured_voltage_mV > MINIMUM_INPUT_VOLTAGE * 1000)) {
contactorStatus = PRECHARGE;
}
}
// In case the inverter requests contactors to open, set the state accordingly
if (contactorStatus == COMPLETED) {
//Incase inverter (or estop) requests contactors to open, make state machine jump to Disconnected state (recoverable)
if (!datalayer.system.status.inverter_allows_contactor_closing ||
datalayer.system.settings.equipment_stop_active) {
contactorStatus = DISCONNECTED;
}
}
// Handle actual state machine. This first turns on Precharge, then Negative, then Positive, and finally turns OFF precharge
switch (contactorStatus) {
case PRECHARGE:
SBOX_100.data.u8[0] = 0x86; // Precharge relay only
prechargeStartTime = currentTime;
contactorStatus = NEGATIVE;
#ifdef DEBUG_VIA_USB
Serial.println("S-BOX Precharge relay engaged");
#endif
break;
case NEGATIVE:
if (currentTime - prechargeStartTime >= CONTACTOR_CONTROL_T1) {
SBOX_100.data.u8[0] = 0xA6; // Precharge + Negative
negativeStartTime = currentTime;
contactorStatus = POSITIVE;
datalayer.shunt.precharging = true;
#ifdef DEBUG_VIA_USB
Serial.println("S-BOX Negative relay engaged");
#endif
}
break;
case POSITIVE:
if (currentTime - negativeStartTime >= CONTACTOR_CONTROL_T2 &&
(datalayer.shunt.measured_voltage_mV * MAX_PRECHARGE_RESISTOR_VOLTAGE_PERCENT <
datalayer.shunt.measured_outvoltage_mV)) {
SBOX_100.data.u8[0] = 0xAA; // Precharge + Negative + Positive
positiveStartTime = currentTime;
contactorStatus = PRECHARGE_OFF;
datalayer.shunt.precharging = false;
#ifdef DEBUG_VIA_USB
Serial.println("S-BOX Positive relay engaged");
#endif
}
break;
case PRECHARGE_OFF:
if (currentTime - positiveStartTime >= CONTACTOR_CONTROL_T3) {
SBOX_100.data.u8[0] = 0x6A; // Negative + Positive
contactorStatus = COMPLETED;
#ifdef DEBUG_VIA_USB
Serial.println("S-BOX Precharge relay released");
#endif
datalayer.shunt.contactors_engaged = true;
}
break;
case COMPLETED:
SBOX_100.data.u8[0] = 0x6A; // Negative + Positive
default:
break;
}
CAN100_cnt++;
if (CAN100_cnt > 0x0E) {
CAN100_cnt = 0;
}
SBOX_100.data.u8[1] = CAN100_cnt << 4 | 0x01;
SBOX_100.data.u8[3] = 0x00;
SBOX_100.data.u8[3] = calculateCRC(SBOX_100);
transmit_can_frame(&SBOX_100, can_config.shunt);
transmit_can_frame(&SBOX_300, can_config.shunt);
}
}

void setup_can_shunt() {
strncpy(datalayer.system.info.shunt_protocol, "BMW SBOX", 63);
datalayer.system.info.shunt_protocol[63] = '\0';
}
#endif
22 changes: 22 additions & 0 deletions Software/src/battery/BMW-SBOX.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#ifndef BMW_SBOX_CONTROL_H
#define BMW_SBOX_CONTROL_H
#include "../include.h"
#define CAN_SHUNT_SELECTED
void transmit_can(CAN_frame* tx_frame, int interface);

/** Minimum input voltage required to enable relay control **/
#define MINIMUM_INPUT_VOLTAGE 250

/** Minimum required percentage of input voltage at the output port to engage the positive relay. **/
/** Prevents engagement of the positive contactor if the precharge resistor is faulty. **/
#define MAX_PRECHARGE_RESISTOR_VOLTAGE_PERCENT 0.99

/* NOTE: modify the T2 time constant below to account for the resistance and capacitance of the target system.
* t=3RC at minimum, t=5RC ideally
*/

#define CONTACTOR_CONTROL_T1 5000 // Time before negative contactor engages and precharging starts
#define CONTACTOR_CONTROL_T2 5000 // Precharge time before precharge resistor is bypassed by positive contactor
#define CONTACTOR_CONTROL_T3 2000 // Precharge relay lead time after positive contactor has been engaged

#endif
9 changes: 9 additions & 0 deletions Software/src/communication/can/comm_can.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ void transmit_can() {
#ifdef CHARGER_SELECTED
transmit_can_charger();
#endif // CHARGER_SELECTED

#ifdef CAN_SHUNT_SELECTED
transmit_can_shunt();
#endif // CAN_SHUNT_SELECTED
}

void transmit_can_frame(CAN_frame* tx_frame, int interface) {
Expand Down Expand Up @@ -339,6 +343,11 @@ void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) {
if (interface == can_config.charger) {
#ifdef CHARGER_SELECTED
map_can_frame_to_variable_charger(*rx_frame);
#endif
}
if (interface == can_config.shunt) {
#ifdef CAN_SHUNT_SELECTED
handle_incoming_can_frame_shunt(*rx_frame);
#endif
}
}
16 changes: 16 additions & 0 deletions Software/src/datalayer/datalayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,29 @@ typedef struct {
uint16_t measured_voltage_dV = 0;
/** measured amperage in deciAmperes. 300 = 30.0 A */
uint16_t measured_amperage_dA = 0;
/** measured battery voltage in mV (S-BOX) **/
uint32_t measured_voltage_mV = 0;
/** measured output voltage in mV (eg. S-BOX) **/
uint32_t measured_outvoltage_mV = 0;
/** measured amperage in mA (eg. S-BOX) **/
int32_t measured_amperage_mA = 0;
/** Average current from last 1s **/
int32_t measured_avg1S_amperage_mA = 0;
/** True if contactors are precharging state */
bool precharging = false;
/** True if the contactor controlled by battery-emulator is closed */
bool contactors_engaged = false;
/** True if shunt communication ok **/
bool available = false;
} DATALAYER_SHUNT_TYPE;

typedef struct {
/** array with type of battery used, for displaying on webserver */
char battery_protocol[64] = {0};
/** array with type of inverter used, for displaying on webserver */
char inverter_protocol[64] = {0};
/** array with type of battery used, for displaying on webserver */
char shunt_protocol[64] = {0};
/** array with incoming CAN messages, for displaying on webserver */
char logged_can_messages[15000] = {0};
size_t logged_can_messages_offset = 0;
Expand Down
5 changes: 5 additions & 0 deletions Software/src/devboard/webserver/settings_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ String settings_processor(const String& var) {
content += "<h4 style='color: white;'>Inverter interface: RS485<span id='Inverter'></span></h4>";
#endif

#ifdef CAN_SHUNT_SELECTED
content += "<h4 style='color: white;'>Shunt Interface: <span id='Shunt'>" +
String(getCANInterfaceName(can_config.shunt)) + "</span></h4>";
#endif //CAN_SHUNT_SELECTED

// Close the block
content += "</div>";

Expand Down
6 changes: 6 additions & 0 deletions Software/src/devboard/webserver/webserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,12 @@ String processor(const String& var) {
}
content += "</h4>";

#ifdef CAN_SHUNT_SELECTED
content += "<h4 style='color: white;'>Shunt protocol: ";
content += datalayer.system.info.shunt_protocol;
content += "</h4>";
#endif

#if defined CHEVYVOLT_CHARGER || defined NISSANLEAF_CHARGER
content += "<h4 style='color: white;'>Charger protocol: ";
#ifdef CHEVYVOLT_CHARGER
Expand Down

0 comments on commit 184d929

Please sign in to comment.