Skip to content

Commit e56c7c7

Browse files
committed
BMS: implement spec state machine with SN4-aligned charging + safety audit fixes
Reconcile the BMS state machine with the SN5 spec diagram, porting charging behavior from SN4, then fix issues found in a full SN4-vs-SN5 safety audit. State machine: - Re-enable precharge voltage gate (3->4, IVT >= 90% pack), R2D gate (4->5), and R2D-loss return (5->4); BSPD/APPS remain safe-default TODO stubs - Central evaluate_faults() on the 1ms tick: cell V/T violations, sensor timeouts (IVT + cell monitor), overcurrent, continuous IMD, contactor feedback plausibility; drive group -> FAULT_BMS/IMD, charger group -> FAULT_CHARGING (explicit handler faults coerced to match, per SN4/diagram) - All fault states latch until power cycle (updateStateProtected generalized) - Balance: console 6<->8 transitions + auto-return when cells balanced Charging (ported from SN4, FreeRTOS-safe): - New FEB_CAN_Charger module: extended-ID CCS protocol (0x1806E5F4 / 0x18FF50E5), Charging_Status soft/hard limits, trickle charge; charger actively commanded OFF whenever connected and not in CHARGING; done_charging re-arms on charger unplug - New FEB_CAN_Heartbeat module: DASH/PCU presence drives LV_POWER <-> BATTERY_FREE (only-charger-on-CAN / reconnection) Audit fixes: - Lock-free ADBMS pack snapshots so the 1ms SM task never blocks on the ADBMS mutex (temp scans hold it for tens of ms) - CAN filter consolidation: heartbeats and IVT each use one mask filter; boot usage drops from 14/14 banks (dynamic registrations silently dropped) to 4/14; feb_can_filter now reports overflow instead of dropping silently - Saturating violation counters; charging blocked until first cell scan Thresholds marked TUNE must be set from real pack/fuse/charger ratings before track use.
1 parent 1edb7a0 commit e56c7c7

26 files changed

Lines changed: 952 additions & 124 deletions

BMS/Core/User/Inc/FEB_ADBMS6830B.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,33 @@ bool FEB_Cell_Balancing_Status(void);
121121
uint8_t FEB_ADBMS_Get_Error_Type(void);
122122
void FEB_ADBMS_Update_Error_Type(uint8_t error);
123123

124+
// ********************************** Fault Flags (SM handoff) *******************
125+
// Sticky flags set by the ADBMS task (under mutex) and read lock-free by the
126+
// state machine task. 32-bit aligned reads are atomic on Cortex-M4.
127+
#define ADBMS_FAULT_FLAG_VOLTAGE (1u << 0)
128+
#define ADBMS_FAULT_FLAG_TEMP (1u << 1)
129+
#define ADBMS_FAULT_FLAG_SENSOR (1u << 2) // reserved: PEC / comm failure
130+
131+
/** @brief Latched cell V/T fault flags (ADBMS_FAULT_FLAG_*). */
132+
uint32_t FEB_ADBMS_Get_Fault_Flags(void);
133+
134+
/** @brief HAL tick of last completed V/T process (0 = never). Used for the
135+
* cell-monitor sensor-timeout check in the state machine. */
136+
uint32_t FEB_ADBMS_Get_Last_Update_Tick(void);
137+
138+
// ********************************** Lock-free Snapshots ************************
139+
// Pack-level values published by the ADBMS task at the end of each scan and
140+
// readable WITHOUT the ADBMS mutex (atomic 32-bit reads). Use these — not the
141+
// FEB_ADBMS_GET_ACC_* getters — from the 1ms state-machine task: the mutex is
142+
// held for tens of ms during a temperature scan and would stall the SM.
143+
144+
/** @brief Pack total voltage [V] from the last scan (0 until first scan). */
145+
float FEB_ADBMS_Snapshot_Total_Voltage(void);
146+
147+
/** @brief Highest cell voltage [V] from the last scan. */
148+
float FEB_ADBMS_Snapshot_Max_Cell_Voltage(void);
149+
150+
/** @brief Highest pack temperature [C] from the last scan (NaN until first scan). */
151+
float FEB_ADBMS_Snapshot_Max_Temp(void);
152+
124153
#endif /* INC_FEB_ADBMS6830B_H_ */
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @file FEB_CAN_Charger.h
3+
* @brief Charger CAN interface (ported from SN4 BMS)
4+
* @author Formula Electric @ Berkeley
5+
*
6+
* Talks to the CCS charger over extended-ID CAN:
7+
* - BMS -> charger : 0x1806E5F4 (max voltage / max current / control)
8+
* - charger -> BMS : 0x18FF50E5 (operating voltage / current / status)
9+
*
10+
* Behaviour mirrors SN4 (FEB_CAN_Charger.c) but uses the SN5 feb_can_lib
11+
* RX/TX API (extended-ID aware, FreeRTOS-safe queues) instead of raw HAL.
12+
*/
13+
14+
#ifndef INC_FEB_CAN_CHARGER_H_
15+
#define INC_FEB_CAN_CHARGER_H_
16+
17+
#include <stdbool.h>
18+
#include <stdint.h>
19+
20+
/**
21+
* @brief Register charger CAN reception. Call from the CAN RX task before
22+
* FEB_CAN_Filter_UpdateFromRegistry().
23+
*/
24+
void FEB_CAN_Charger_Init(void);
25+
26+
/**
27+
* @brief True if a charger frame was received within FEB_CHARGER_RX_TIMEOUT_MS.
28+
* Used to gate BATTERY_FREE -> CHARGER_PRECHARGE (6->7).
29+
*/
30+
bool FEB_CAN_Charger_Received(void);
31+
32+
/**
33+
* @brief SN4 charge-decision function (uses existing cell V/T telemetry).
34+
* @return 1 charge complete / soft V or T limit hit -> stop, return to FREE
35+
* -1 hard pack/cell over-voltage or over-temp -> FAULT_CHARGING
36+
* 0 keep charging
37+
*/
38+
int8_t FEB_CAN_Charging_Status(void);
39+
40+
/** @brief Command the charger to start (control byte = 0). */
41+
void FEB_CAN_Charger_Start_Charge(void);
42+
43+
/** @brief Command the charger to stop (control byte = 1, marks done). */
44+
void FEB_CAN_Charger_Stop_Charge(void);
45+
46+
/**
47+
* @brief Periodic charger command TX + trickle-charge logic.
48+
* No-op unless the state machine is in CHARGING. Call ~every 100 ms.
49+
*/
50+
void FEB_CAN_Charger_Process(void);
51+
52+
#endif /* INC_FEB_CAN_CHARGER_H_ */
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @file FEB_CAN_Heartbeat.h
3+
* @brief CAN-presence (heartbeat) tracking for the BMS
4+
* @author Formula Electric @ Berkeley
5+
*
6+
* Tracks which subsystems are alive on the bus by timestamping their heartbeat
7+
* frames (0xD0-0xD5, defined in the shared CAN library). Drives the
8+
* BATTERY_FREE <-> LV_POWER transitions (spec "Only Charger on CAN" /
9+
* "Other subsystems on CAN"). This is the SN5-idiom equivalent of SN4's
10+
* FEB_COMBINED_STATUS()/FAck scheme: simple last-seen freshness rather than
11+
* failed-ack counters.
12+
*/
13+
14+
#ifndef INC_FEB_CAN_HEARTBEAT_H_
15+
#define INC_FEB_CAN_HEARTBEAT_H_
16+
17+
#include <stdbool.h>
18+
#include <stdint.h>
19+
20+
typedef enum
21+
{
22+
FEB_HB_PCU = 0,
23+
FEB_HB_DASH,
24+
FEB_HB_LVPDB,
25+
FEB_HB_DCU,
26+
FEB_HB_FSN,
27+
FEB_HB_RSN,
28+
FEB_HB_COUNT
29+
} FEB_HB_Device_t;
30+
31+
/**
32+
* @brief Register heartbeat RX. Call from the CAN RX task before
33+
* FEB_CAN_Filter_UpdateFromRegistry().
34+
*/
35+
void FEB_CAN_Heartbeat_Init(void);
36+
37+
/** @brief True if @p dev sent a heartbeat within @p timeout_ms. */
38+
bool FEB_CAN_Heartbeat_DevFresh(FEB_HB_Device_t dev, uint32_t timeout_ms);
39+
40+
/**
41+
* @brief True if "other subsystems" (DASH or PCU) are present on CAN.
42+
* Mirrors SN4 FEB_COMBINED_STATUS(), which keys on DASH/PCU.
43+
*/
44+
bool FEB_CAN_Heartbeat_OthersPresent(uint32_t timeout_ms);
45+
46+
#endif /* INC_FEB_CAN_HEARTBEAT_H_ */

BMS/Core/User/Inc/FEB_CAN_State.h

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* @brief BMS state machine states (aligned with SN4)
1313
* @note Values match CAN bms_state signal (5-bit, 0-31 valid range)
1414
* @note Values must match FEB_SM_ST_t in PCU/Core/User/Inc/FEB_CAN_BMS.h
15+
* @note SN5 keeps an extra CHARGER_PRECHARGE (7) state that the spec diagram
16+
* does not show, so diagram state numbers >= 7 (Charging, Balance, the
17+
* fault states) are offset by +1 here. Do NOT renumber to "match" the
18+
* diagram: PCU mirrors these exact values and compares state == DRIVE (5).
1519
*/
1620
typedef enum
1721
{
@@ -71,11 +75,11 @@ const char *FEB_CAN_State_GetStateName(BMS_State_t state);
7175

7276
/**
7377
* @brief Process automatic state transitions based on R2D signal
74-
* @note Call from 1ms timer callback
7578
*
76-
* Handles:
77-
* - ENERGIZED -> DRIVE when R2D active
78-
* - DRIVE -> ENERGIZED when R2D inactive/timeout
79+
* @warning DO NOT CALL. The ENERGIZED<->DRIVE R2D logic now lives in
80+
* FEB_SM.c (EnergizedTransition / DriveTransition). Calling this would
81+
* double-drive those transitions. Retained only to avoid touching the
82+
* generated wiring; remove once confirmed unused everywhere.
7983
*/
8084
void FEB_CAN_State_ProcessTransitions(void);
8185

BMS/Core/User/Inc/FEB_Const.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,37 @@
8686
#define FEB_VOLTAGE_ERROR_THRESH 3 // Trigger fault after 3 consecutive voltage violations
8787
#define FEB_TEMP_ERROR_THRESH 5 // Trigger fault after 5 consecutive temp violations
8888

89+
// ********************************** Charging Limits (SN4-derived) **************
90+
// Used by FEB_CAN_Charging_Status(). Soft limit -> stop charging and return to
91+
// BATTERY_FREE; hard limit -> FAULT_CHARGING. SN5 values (do not copy SN4's
92+
// pack-specific thermal numbers verbatim).
93+
#define FEB_CONFIG_CELL_HARD_MAX_VOLTAGE_mV 4200 // Hard cell over-voltage -> FAULT_CHARGING
94+
#define FEB_CONFIG_CELL_SOFT_MAX_VOLTAGE_mV 4180 // TUNE: soft target -> stop charge
95+
#define FEB_CONFIG_CELL_HARD_MAX_TEMP_dC 600 // 60.0C hard over-temp -> FAULT_CHARGING
96+
// (soft charge temp limit FEB_CONFIG_CELL_SOFT_MAX_TEMP_dC defined above = 550)
97+
98+
// Pack hard-max voltage in VOLTS (compared against pack/IVT voltage in volts).
99+
#define FEB_CONFIG_PACK_HARD_MAX_VOLTAGE_V \
100+
((FEB_NBANKS * FEB_NUM_CELLS_PER_BANK * FEB_CONFIG_CELL_HARD_MAX_VOLTAGE_mV) / 1000.0f)
101+
102+
// ********************************** Charger CAN (SN4-derived) ******************
103+
// Charger command (BMS -> charger) targets, in charger units (deci-amps / deci-volts).
104+
#define FEB_CHARGE_CURRENT_dA 40 // TUNE: 4.0 A nominal charge current
105+
#define FEB_TRICKLE_CHARGE_CURRENT_dA (FEB_CHARGE_CURRENT_dA / 2)
106+
#define FEB_TRICKLE_CHARGE_INTERVAL_MS 5000 // toggle interval near full charge
107+
#define FEB_CHARGE_TARGET_VOLTAGE_dV ((uint16_t)(FEB_CONFIG_PACK_HARD_MAX_VOLTAGE_V * 10.0f * 0.99f)) // TUNE
108+
#define FEB_TRICKLE_CHARGE_START_VOLTAGE_dV ((uint16_t)(FEB_CONFIG_PACK_HARD_MAX_VOLTAGE_V * 10.0f * 0.98f)) // TUNE
109+
#define FEB_CHARGER_RX_TIMEOUT_MS 1000 // charger considered absent after this
110+
111+
// ********************************** Fault Evaluation Thresholds ****************
112+
#define FEB_DISCHARGE_OVERCURRENT_A 350.0f // TUNE: drive-side limit (fuse/AIR rating)
113+
#define FEB_CHARGE_OVERCURRENT_A 60.0f // TUNE: charge-side limit (charger rating)
114+
#define FEB_OVERCURRENT_CONFIRM_MS 50 // |I| must exceed limit this long before fault
115+
#define FEB_IVT_FAULT_TIMEOUT_MS 1000 // IVT CAN staleness -> sensor timeout fault
116+
#define FEB_ADBMS_DATA_TIMEOUT_MS 1000 // cell-monitor staleness -> sensor timeout fault
117+
#define FEB_IMD_FAULT_CONFIRM_MS 100 // debounce IMD-open before faulting
118+
#define FEB_CONTACTOR_FEEDBACK_TIMEOUT_MS 200 // cmd vs sense mismatch -> weld/stuck fault
119+
89120
// ********************************** isoSPI Communication Mode ******************
90121

91122
// isoSPI Mode Selection - Choose ONE of the following:

BMS/Core/User/Src/FEB_ADBMS6830B.c

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ uint16_t balancing_mask = 0xAAAA;
4242

4343
uint8_t ERROR_TYPE = 0; // HEXDIGIT 1 voltage faults; HEXDIGIT 2 temp faults; HEXDIGIT 3 relay faults
4444

45+
/* Sticky fault flags + freshness tick for the state machine.
46+
* Written here under ADBMSMutexHandle (validators run while the task holds it);
47+
* read lock-free by the SM task via FEB_ADBMS_Get_Fault_Flags() /
48+
* FEB_ADBMS_Get_Last_Update_Tick(). 32-bit aligned access is atomic on M4. */
49+
static volatile uint32_t adbms_fault_flags = 0;
50+
static volatile uint32_t adbms_last_update_tick = 0;
51+
52+
/* Lock-free pack-level snapshots for the 1ms SM task. Written at the end of
53+
* each process pass (writer holds ADBMSMutexHandle); read without the mutex
54+
* (aligned 32-bit float reads are atomic on Cortex-M4). The mutex-taking
55+
* FEB_ADBMS_GET_ACC_* getters MUST NOT be called from the SM task hot path:
56+
* a temperature scan holds the mutex for tens of ms and would stall it. */
57+
static volatile float adbms_snap_total_V = 0.0f;
58+
static volatile float adbms_snap_max_cell_V = 0.0f;
59+
static volatile float adbms_snap_max_temp_C = NAN;
60+
4561
// ********************************** Config Bits ********************************
4662

4763
static bool refon = 1;
@@ -224,15 +240,22 @@ static void validate_voltages()
224240
// Check redundant S-code measurement to confirm violation
225241
if (voltageS > vMax || voltageS < vMin)
226242
{
227-
FEB_ACC.banks[bank].cells[cell].violations += 1;
228-
DEBUG_VOLTAGE_PRINT("Both C and S codes confirm violation: violations=%d",
229-
FEB_ACC.banks[bank].cells[cell].violations);
230-
if (FEB_ACC.banks[bank].cells[cell].violations == FEB_VOLTAGE_ERROR_THRESH)
243+
/* Saturating increment (counter is uint8_t — a free-running += would
244+
* wrap at 255); actions fire exactly once, on the crossing scan. */
245+
if (FEB_ACC.banks[bank].cells[cell].violations < FEB_VOLTAGE_ERROR_THRESH)
231246
{
232-
printf("[ADBMS] FAULT: Cell voltage out of range - Bank %d Cell %d: %.3fV (limits: %.3f-%.3fV)\r\n", bank,
233-
cell, voltageC / 1000.0f, vMin / 1000.0f, vMax / 1000.0f);
234-
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_VOLTAGE_VIOLATION);
235-
// FEB_SM_Transition(FEB_SM_ST_FAULT_BMS);
247+
FEB_ACC.banks[bank].cells[cell].violations += 1;
248+
DEBUG_VOLTAGE_PRINT("Both C and S codes confirm violation: violations=%d",
249+
FEB_ACC.banks[bank].cells[cell].violations);
250+
if (FEB_ACC.banks[bank].cells[cell].violations >= FEB_VOLTAGE_ERROR_THRESH)
251+
{
252+
printf("[ADBMS] FAULT: Cell voltage out of range - Bank %d Cell %d: %.3fV (limits: %.3f-%.3fV)\r\n", bank,
253+
cell, voltageC / 1000.0f, vMin / 1000.0f, vMax / 1000.0f);
254+
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_VOLTAGE_VIOLATION);
255+
/* Latch for the SM task; evaluate_faults() in FEB_SM.c routes this
256+
* to FAULT_BMS (drive group) or FAULT_CHARGING (charger group). */
257+
adbms_fault_flags |= ADBMS_FAULT_FLAG_VOLTAGE;
258+
}
236259
}
237260
}
238261
else
@@ -449,13 +472,18 @@ static void validate_temps()
449472
{
450473
DEBUG_TEMP_PRINT("Temperature violation: Bank %d Sensor %d Temp=%.1fC violations=%d", bank, sensor,
451474
temp / 10.0f, FEB_ACC.banks[bank].temp_violations[sensor] + 1);
452-
FEB_ACC.banks[bank].temp_violations[sensor]++;
453-
if (FEB_ACC.banks[bank].temp_violations[sensor] == FEB_TEMP_ERROR_THRESH)
475+
/* Saturating increment (uint8_t would wrap); actions fire once. */
476+
if (FEB_ACC.banks[bank].temp_violations[sensor] < FEB_TEMP_ERROR_THRESH)
454477
{
455-
printf("[ADBMS] FAULT: Cell temperature out of range - Bank %d Sensor %d: %.1fC (limits: %.1f-%.1fC)\r\n",
456-
bank, sensor, temp / 10.0f, tMin / 10.0f, tMax / 10.0f);
457-
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_TEMP_VIOLATION);
458-
// FEB_SM_Transition(FEB_SM_ST_FAULT_BMS);
478+
FEB_ACC.banks[bank].temp_violations[sensor]++;
479+
if (FEB_ACC.banks[bank].temp_violations[sensor] >= FEB_TEMP_ERROR_THRESH)
480+
{
481+
printf("[ADBMS] FAULT: Cell temperature out of range - Bank %d Sensor %d: %.1fC (limits: %.1f-%.1fC)\r\n",
482+
bank, sensor, temp / 10.0f, tMin / 10.0f, tMax / 10.0f);
483+
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_TEMP_VIOLATION);
484+
/* Latch for the SM task; evaluate_faults() routes per state group. */
485+
adbms_fault_flags |= ADBMS_FAULT_FLAG_TEMP;
486+
}
459487
}
460488
}
461489
else
@@ -473,7 +501,10 @@ static void validate_temps()
473501
{
474502
DEBUG_TEMP_PRINT("WARNING: Low temperature read ratio (%.1f%%)", read_ratio * 100.0f);
475503
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_LOW_TEMP_READS);
476-
// FEB_SM_Transition(FEB_SM_ST_FAULT_BMS);
504+
/* NOTE: chronic low temp-read ratio is a sensor-health warning, not a cell
505+
* over-temp event; intentionally NOT latched as a hard fault (would
506+
* false-trip on a flaky harness). A truly dead cell-monitor is caught by
507+
* the staleness timeout (adbms_last_update_tick) in evaluate_faults(). */
477508
}
478509
DEBUG_TEMP_PRINT("Temperature validation complete");
479510
}
@@ -582,6 +613,10 @@ void FEB_ADBMS_Voltage_Process()
582613
read_cell_voltages();
583614
store_cell_voltages();
584615
validate_voltages();
616+
/* Publish lock-free snapshots for the SM task (we hold the mutex here) */
617+
adbms_snap_total_V = FEB_ACC.total_voltage_V;
618+
adbms_snap_max_cell_V = FEB_ACC.pack_max_voltage_V;
619+
adbms_last_update_tick = HAL_GetTick(); /* freshness for SM sensor-timeout check */
585620
DEBUG_VOLTAGE_PRINT("=== Voltage Process Completed ===");
586621
}
587622

@@ -604,6 +639,9 @@ void FEB_ADBMS_Temperature_Process()
604639
}
605640
compute_pack_temp_stats();
606641
validate_temps();
642+
/* Publish lock-free snapshot for the SM task (we hold the mutex here) */
643+
adbms_snap_max_temp_C = FEB_ACC.pack_max_temp;
644+
adbms_last_update_tick = HAL_GetTick(); /* freshness for SM sensor-timeout check */
607645

608646
DEBUG_TEMP_PRINT("=== Temperature Process Completed ===");
609647
}
@@ -918,3 +956,33 @@ void FEB_ADBMS_Update_Error_Type(uint8_t error)
918956
{
919957
ERROR_TYPE = error;
920958
}
959+
960+
// ********************************** Fault Flags (SM handoff) *******************
961+
962+
uint32_t FEB_ADBMS_Get_Fault_Flags(void)
963+
{
964+
return adbms_fault_flags;
965+
}
966+
967+
uint32_t FEB_ADBMS_Get_Last_Update_Tick(void)
968+
{
969+
return adbms_last_update_tick;
970+
}
971+
972+
// ********************************** Lock-free Snapshots ************************
973+
// For the 1ms SM task / charger logic. Never blocks on ADBMSMutexHandle.
974+
975+
float FEB_ADBMS_Snapshot_Total_Voltage(void)
976+
{
977+
return adbms_snap_total_V;
978+
}
979+
980+
float FEB_ADBMS_Snapshot_Max_Cell_Voltage(void)
981+
{
982+
return adbms_snap_max_cell_V;
983+
}
984+
985+
float FEB_ADBMS_Snapshot_Max_Temp(void)
986+
{
987+
return adbms_snap_max_temp_C;
988+
}

0 commit comments

Comments
 (0)