Skip to content

Commit 695efe6

Browse files
committed
BMS: FSAE-safe accumulator temperature monitoring
Make the cell-temperature path meet FSAE AMS rules: react to over/under-temp and to loss of temperature data within the ~1 s window, and latch. Over/under-temp fault: - Was unreachable: FEB_TEMP_ERROR_THRESH had drifted to a pack-wide 0.8*N aggregate (=336) compared against a per-sensor uint8_t counter. Restore a small per-sensor consecutive-violation count so any single cell over/under limit latches. - Was too slow even if reached: the temp scan ran at 2 Hz, so any debounce blew the FSAE budget. Drive the scan from FEB_TEMP_SCAN_PERIOD_MS and run it at 100 ms (work is ~38 ms); phase-offset it from the voltage scan. Worst-case latch is now ~400 ms. A _Static_assert ties (THRESH+1)*period to a 900 ms budget so the timing can't silently regress. Temperature-telemetry-loss fail-safe (FSAE: a disconnected temp sense wire must open the shutdown circuit within 1 s): - validate_temps() now measures valid reads against the POPULATED sensor count (FEB_TEMP_SENSORS_POPULATED_PER_BANK, not the 42-wide array) and latches ADBMS_FAULT_FLAG_SENSOR when below FEB_TEMP_MIN_VALID_FRACTION for longer than FEB_TEMP_TELEMETRY_TIMEOUT_MS (800 ms). Time-confirmed so a flaky scan won't trip it; gated under !FEB_BMS_DISABLE_TEMP_CHECKS so bench boards don't fault. - evaluate_faults() routes the SENSOR flag to FAULT_BMS/FAULT_CHARGING. Charging thermal limits: hard 60 -> 45 C, soft 55 -> 40 C (Li-ion charge accept is stricter than the 60 C discharge ceiling). Verify against the cell datasheet. Bench-mode safety: - FEB_BMS_DISABLE_ADBMS_CHECKS default back to 0; bench enabled via the new BMS_BENCH_MODE CMake option (-D), not by editing the source default. - scripts/check-bench-flags.sh fails if any bench override is committed enabled; wired into pre-commit and the quality CI workflow. FEB_NBANKS 1 -> 10. TUNE/verify against hardware: FEB_TEMP_SENSORS_POPULATED_PER_BANK (41) must match the real harness; FEB_TEMP_MIN_VALID_FRACTION (0.50); >=30% coverage.
1 parent ce38bfe commit 695efe6

11 files changed

Lines changed: 193 additions & 20 deletions

File tree

.github/workflows/quality.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ on:
77
branches: [main]
88

99
jobs:
10+
bench-flags:
11+
runs-on: ubuntu-latest
12+
name: bench-flag guard
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v6
17+
18+
- name: BMS ADBMS safety overrides must not be enabled in source
19+
run: ./scripts/check-bench-flags.sh
20+
1021
clang-format:
1122
runs-on: ubuntu-latest
1223
name: clang-format check

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ repos:
4949
pass_filenames: false
5050
always_run: true
5151

52+
# Bench-flag guard - BMS ADBMS safety overrides must never ship enabled.
53+
# Enable bench mode via the build flag (cmake -DBMS_BENCH_MODE=ON) instead.
54+
- id: check-bench-flags
55+
name: check-bench-flags
56+
entry: ./scripts/check-bench-flags.sh
57+
language: script
58+
pass_filenames: false
59+
always_run: true
60+
5261
# Auto-increment patch version on each commit for affected boards + repo.
5362
# No-ops if a VERSION file is already staged (so /version stays authoritative).
5463
- id: auto-bump-patch

BMS/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ add_compile_definitions(
2424
$<$<CONFIG:Debug>:DEBUG>
2525
)
2626

27+
# ── Bench-mode override (NEVER for a real pack / scrutineering) ───────
28+
# Sanctioned, *uncommitted* way to disable ADBMS safety checks for bench
29+
# bring-up with no functional cell monitor attached:
30+
# cmake --preset Debug -DBMS_BENCH_MODE=ON (or pass -DBMS_BENCH_MODE=ON)
31+
# The in-source default in FEB_Const.h MUST stay 0 — scripts/check-bench-flags.sh
32+
# enforces that — so this build flag is the only approved way to flip it.
33+
option(BMS_BENCH_MODE "Disable ADBMS safety checks for bench bring-up (NEVER for a real pack)" OFF)
34+
if(BMS_BENCH_MODE)
35+
add_compile_definitions(FEB_BMS_DISABLE_ADBMS_CHECKS=1)
36+
message(WARNING "BMS_BENCH_MODE=ON: ADBMS safety checks DISABLED — bench only, NEVER a real pack")
37+
endif()
38+
2739
# ══════════════════════════════════════════════════════════════════════
2840
# SOURCE COLLECTION
2941
# ══════════════════════════════════════════════════════════════════════

BMS/Core/User/Inc/FEB_ADBMS6830B.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ void FEB_ADBMS_Update_Error_Type(uint8_t error);
126126
// state machine task. 32-bit aligned reads are atomic on Cortex-M4.
127127
#define ADBMS_FAULT_FLAG_VOLTAGE (1u << 0)
128128
#define ADBMS_FAULT_FLAG_TEMP (1u << 1)
129-
#define ADBMS_FAULT_FLAG_SENSOR (1u << 2) // reserved: PEC / comm failure
129+
#define ADBMS_FAULT_FLAG_SENSOR (1u << 2) // temperature telemetry lost (too few valid sensor reads)
130130

131131
/** @brief Latched cell V/T fault flags (ADBMS_FAULT_FLAG_*). */
132132
uint32_t FEB_ADBMS_Get_Fault_Flags(void);

BMS/Core/User/Inc/FEB_Const.h

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#define FEB_NUM_ICPBANK 1
88

99
// Number of banks in the system
10-
#define FEB_NBANKS 1
10+
#define FEB_NBANKS 10
1111

1212
// Total number of ICs in the daisy chain
1313
#define FEB_NUM_IC (FEB_NUM_ICPBANK * FEB_NBANKS)
@@ -80,20 +80,52 @@
8080
// Cell temperature limits (in deci-Celsius, 1 dC = 0.1°C)
8181
#define FEB_CELL_MAX_TEMP_DC 600 // 60.0°C maximum cell temperature
8282
#define FEB_CELL_MIN_TEMP_DC -200 // -20.0°C minimum cell temperature
83-
#define FEB_CONFIG_CELL_SOFT_MAX_TEMP_dC 550 // 55.0°C soft limit for charging
83+
#define FEB_CONFIG_CELL_SOFT_MAX_TEMP_dC 400 // 40.0°C soft limit (stop charging; also gates balancing)
8484

8585
// Error thresholds (consecutive violations before triggering fault)
8686
#define FEB_VOLTAGE_ERROR_THRESH 3 // Trigger fault after 3 consecutive voltage violations
87-
#define FEB_TEMP_ERROR_THRESH 5 // Trigger fault after 5 consecutive temp violations
87+
// Per-sensor consecutive out-of-range scans before latching a temp fault. This
88+
// is checked PER SENSOR against the uint8_t temp_violations[] counter, so it
89+
// must stay small: ANY single cell over/under limit must fault — we never wait
90+
// for a fraction of the pack to overheat. (A pack-wide 0.8*N aggregate here was
91+
// both wrong semantically and unreachable: 0.8*42*10=336 > uint8_t max.)
92+
#define FEB_TEMP_ERROR_THRESH 3 // Trigger fault after 3 consecutive temp violations
93+
94+
// Temperature scan cadence and FSAE fault-timing budget.
95+
// The temp scan (FEB_ADBMS_Temperature_Process) is gated to this period in
96+
// FEB_Task_ADBMS.c. FSAE requires the AMS to open the shutdown circuit within
97+
// ~1 s of an out-of-range cell temperature (and within 1 s of a temp sense-wire
98+
// disconnect). The per-sensor debounce above latches in at most
99+
// (FEB_TEMP_ERROR_THRESH + 1) scans; the static assertion proves that stays
100+
// under budget, so it cannot silently regress if the period or threshold changes.
101+
#define FEB_TEMP_SCAN_PERIOD_MS 100 // temperature scan cadence (10 Hz)
102+
#define FEB_TEMP_FAULT_BUDGET_MS 900 // margin under the 1 s FSAE temperature window
103+
_Static_assert((FEB_TEMP_ERROR_THRESH + 1) * FEB_TEMP_SCAN_PERIOD_MS <= FEB_TEMP_FAULT_BUDGET_MS,
104+
"Over/under-temp fault must latch within the FSAE temperature window");
105+
106+
// Temperature-telemetry-loss fail-safe (FSAE: a disconnected temperature sense
107+
// wire must open the shutdown circuit within 1 s). If fewer than
108+
// FEB_TEMP_MIN_VALID_FRACTION of the POPULATED sensors read in-range for longer
109+
// than FEB_TEMP_TELEMETRY_TIMEOUT_MS, validate_temps() latches a sensor fault.
110+
// NOTE: *_POPULATED_PER_BANK is a HARDWARE FACT — set it to the number of
111+
// thermistors actually wired per bank. The array holds FEB_NUM_TEMP_SENSORS=42,
112+
// but index 39 (MUX6[4]) is unconnected by design, so 41 are expected. A wrong
113+
// value here either false-trips or under-protects.
114+
#define FEB_TEMP_SENSORS_POPULATED_PER_BANK 41 // VERIFY against the harness
115+
#define FEB_TEMP_MIN_VALID_FRACTION 0.50f // TUNE: fault below this fraction valid
116+
#define FEB_TEMP_TELEMETRY_TIMEOUT_MS 800 // sustained low reads this long -> fault (< 1 s)
88117

89118
// ********************************** Charging Limits (SN4-derived) **************
90119
// Used by FEB_CAN_Charging_Status(). Soft limit -> stop charging and return to
91120
// BATTERY_FREE; hard limit -> FAULT_CHARGING. SN5 values (do not copy SN4's
92121
// pack-specific thermal numbers verbatim).
93122
#define FEB_CONFIG_CELL_HARD_MAX_VOLTAGE_mV 4200 // Hard cell over-voltage -> FAULT_CHARGING
94123
#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)
124+
#define FEB_CONFIG_CELL_HARD_MAX_TEMP_dC 450 // 45.0C hard charge over-temp -> FAULT_CHARGING
125+
// Charge thermal limits are stricter than the 60.0C discharge ceiling
126+
// (FEB_CELL_MAX_TEMP_DC): Li-ion charge accept tops out ~45C. Soft limit
127+
// (FEB_CONFIG_CELL_SOFT_MAX_TEMP_dC, 40.0C above) stops charge with margin
128+
// before this hard fault.
97129

98130
// Pack hard-max voltage in VOLTS (compared against pack/IVT voltage in volts).
99131
#define FEB_CONFIG_PACK_HARD_MAX_VOLTAGE_V \
@@ -189,7 +221,7 @@
189221
// readings and diagnostics remain visible; only enforcement is suppressed.
190222
// Logs warnings at boot while enabled. NEVER enable with a real pack.
191223
#ifndef FEB_BMS_DISABLE_ADBMS_CHECKS
192-
#define FEB_BMS_DISABLE_ADBMS_CHECKS 1
224+
#define FEB_BMS_DISABLE_ADBMS_CHECKS 0
193225
#endif
194226

195227
#if FEB_BMS_DISABLE_ADBMS_CHECKS

BMS/Core/User/Src/FEB_ADBMS6830B.c

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ uint8_t ERROR_TYPE = 0; // HEXDIGIT 1 voltage faults; HEXDIGIT 2 temp faults; HE
4949
static volatile uint32_t adbms_fault_flags = 0;
5050
static volatile uint32_t adbms_last_update_tick = 0;
5151

52+
#if !FEB_BMS_DISABLE_TEMP_CHECKS
53+
/* Arm tick for the temperature-telemetry-loss fault (0 = disarmed). Set when the
54+
* valid-read fraction first drops below FEB_TEMP_MIN_VALID_FRACTION; a sensor
55+
* fault latches once it stays low for FEB_TEMP_TELEMETRY_TIMEOUT_MS. */
56+
static uint32_t temp_telemetry_loss_tick = 0;
57+
#endif
58+
5259
/* Lock-free pack-level snapshots for the 1ms SM task. Written at the end of
5360
* each process pass (writer holds ADBMSMutexHandle); read without the mutex
5461
* (aligned 32-bit float reads are atomic on Cortex-M4). The mutex-taking
@@ -518,17 +525,45 @@ static void validate_temps()
518525
DEBUG_TEMP_PRINT("Bank %d: tempRead=%d", bank, FEB_ACC.banks[bank].tempRead);
519526
}
520527

521-
float read_ratio = totalReads / (float)(FEB_NUM_TEMP_SENSORS * FEB_NBANKS);
522-
DEBUG_TEMP_PRINT("Total reads: %d/%d (%.1f%%)", totalReads, FEB_NUM_TEMP_SENSORS * FEB_NBANKS, read_ratio * 100.0f);
523-
if (read_ratio < 0.2)
528+
/* Valid-read fraction measured against the POPULATED sensor count (NOT the
529+
* 42-wide array: index 39 / MUX6[4] is unconnected by design), so a healthy
530+
* pack reads ~100%, not ~98%, and the fault threshold below is meaningful. */
531+
const int expected_reads = FEB_TEMP_SENSORS_POPULATED_PER_BANK * FEB_NBANKS;
532+
float read_ratio = totalReads / (float)expected_reads;
533+
DEBUG_TEMP_PRINT("Total reads: %d/%d (%.1f%%)", totalReads, expected_reads, read_ratio * 100.0f);
534+
bool telemetry_low = (read_ratio < FEB_TEMP_MIN_VALID_FRACTION);
535+
if (telemetry_low)
524536
{
525537
DEBUG_TEMP_PRINT("WARNING: Low temperature read ratio (%.1f%%)", read_ratio * 100.0f);
526538
FEB_ADBMS_Update_Error_Type(ERROR_TYPE_LOW_TEMP_READS);
527-
/* NOTE: chronic low temp-read ratio is a sensor-health warning, not a cell
528-
* over-temp event; intentionally NOT latched as a hard fault (would
529-
* false-trip on a flaky harness). A truly dead cell-monitor is caught by
530-
* the staleness timeout (adbms_last_update_tick) in evaluate_faults(). */
531539
}
540+
541+
#if !FEB_BMS_DISABLE_TEMP_CHECKS
542+
/* FSAE fail-safe: the AMS must open the shutdown circuit within ~1 s if it
543+
* loses the temperature data it depends on (e.g. a temperature sense wire
544+
* disconnects). Latch a sensor fault when too few populated sensors read valid
545+
* for longer than FEB_TEMP_TELEMETRY_TIMEOUT_MS. Time-confirmed so a single
546+
* flaky scan does not trip it; evaluate_faults() routes ADBMS_FAULT_FLAG_SENSOR
547+
* to FAULT_BMS/FAULT_CHARGING like the over-temp fault. */
548+
if (telemetry_low)
549+
{
550+
uint32_t now = HAL_GetTick();
551+
if (temp_telemetry_loss_tick == 0)
552+
{
553+
temp_telemetry_loss_tick = (now == 0) ? 1u : now; /* 0 is the disarmed sentinel */
554+
}
555+
else if ((now - temp_telemetry_loss_tick) >= FEB_TEMP_TELEMETRY_TIMEOUT_MS)
556+
{
557+
printf("[ADBMS] FAULT: temperature telemetry lost - %d/%d sensors valid (%.0f%%)\r\n", totalReads, expected_reads,
558+
(double)(read_ratio * 100.0f));
559+
adbms_fault_flags |= ADBMS_FAULT_FLAG_SENSOR;
560+
}
561+
}
562+
else
563+
{
564+
temp_telemetry_loss_tick = 0; /* healthy: disarm the loss timer */
565+
}
566+
#endif
532567
DEBUG_TEMP_PRINT("Temperature validation complete");
533568
}
534569

BMS/Core/User/Src/FEB_SM.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,13 @@ static void evaluate_faults(void)
313313
s == BMS_STATE_BALANCE);
314314
BMS_State_t grp_fault = charger_group ? BMS_STATE_FAULT_CHARGING : BMS_STATE_FAULT_BMS;
315315

316-
/* (a) Cell voltage / temperature violations latched by the ADBMS task. */
316+
/* (a) Cell voltage / temperature violations and temperature-telemetry loss
317+
* latched by the ADBMS task (SENSOR = too few valid temp reads, a required
318+
* FSAE fail-safe — treated as a hard fault, same as an over-temp cell). */
317319
uint32_t af = FEB_ADBMS_Get_Fault_Flags();
318-
if (af & (ADBMS_FAULT_FLAG_VOLTAGE | ADBMS_FAULT_FLAG_TEMP))
320+
if (af & (ADBMS_FAULT_FLAG_VOLTAGE | ADBMS_FAULT_FLAG_TEMP | ADBMS_FAULT_FLAG_SENSOR))
319321
{
320-
LOG_E(TAG_SM, "Cell V/T violation (flags=0x%02lX)", (unsigned long)af);
322+
LOG_E(TAG_SM, "Cell V/T/sensor violation (flags=0x%02lX)", (unsigned long)af);
321323
fault_begin(grp_fault);
322324
return;
323325
}

BMS/Core/User/Src/FEB_Task_ADBMS.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,11 @@ void StartADBMSTask(void *argument)
7575

7676
/* === Main Task Loop === */
7777
uint32_t voltage_tick = osKernelGetTickCount();
78-
uint32_t temp_tick = osKernelGetTickCount();
78+
/* Phase-offset the temperature scan by half a period so it doesn't fire on the
79+
* same tick as the (now equal-rate) voltage scan and serialize their isoSPI
80+
* work under the shared mutex. Unsigned tick wrap is intentional and matches
81+
* the rollover-safe `now - tick` comparison idiom used below. */
82+
uint32_t temp_tick = osKernelGetTickCount() - pdMS_TO_TICKS(FEB_TEMP_SCAN_PERIOD_MS / 2);
7983
uint32_t balance_tick = osKernelGetTickCount();
8084

8185
for (;;)

BMS/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.7.18
1+
1.7.19

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.7.34
1+
1.7.35

0 commit comments

Comments
 (0)