Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion HANDOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ manager, CAN 2.0B @500k. Docs 01–07 are the spec; read 05 (build plan) first
| PCB — Phase 2 (history) | **(2026-07-25, 27fb2d1).** `gen_board.py` placement green (173 comps, all pour/courtyard/edge assertions pass, 130×90 4-layer); `route_board.py` pass-1 done (power pours, In2 heat patches, both phases' gate fan-outs, Kelvin pairs, disconnect trunk). DRC: **copper down to ~19 clearance + ~11 dangling/mask/hole in 3 known clusters** (see resume section); 243 unconnected = signal nets, autoroute not yet run. Found & fixed a real LM5143 land-pattern bug in the process (see load-bearing decisions) |
| PCB — Phase 3 backplane | **COMPLETE pass-1 (2026-07-25)**: `phase3-backplane/tools/gen_board.py` (single-script: placement + 2oz bus pours + stitching + ALL signal routing) — **0 copper DRC, 0 unconnected**; only lib-bookkeeping (39) + 1 silk nick remain. 8 slots @30mm (XT60PW-F rot-90 mates the module pad-for-pad, socket y60..77.8 = module J5 1:1), M6 lugs -> RS1‖RS2 0.5mΩ Kelvin-sensed by INA228, nested PRESENT L-bus, CAN terminated past both end slots, E-stop chain threaded per docs. Netlist from `tools/wip/bp.net` (regenerate via kicad-cli) |
| PCB — Phase 3 manager | **Routed, 0 unconnected, 0 DRC errors (2026-09-27, draft PR into `development` from `claude/route-manager-shxsg1`)**: placement reworked (15+ passives had been placed by stale refdes, far from their pins; U8 buck re-laid tight round its pinout; U11 turned so CAN faces J1; U12 in the USB path), PGND now poured on both layers (was F.Cu 3V3 / B.Cu PGND), USB + CAN pairs and buck power copper hand-drawn and locked (`route_critical.py`), rest Freerouting 1.9 + `finish_routes.py`, 283 PGND stitch vias. DRC = 5 warnings (4 silk, 1 U8 courtyard-override lib mismatch). Antenna keep-out copper-free. Pipeline: `phase3-manager/tools/README.md` |
| Module firmware | v0.1 builds clean (6.3 KB): full peripheral binding + CAN dispatch around the host-tested `module_core`. Untested on silicon (no board yet) |
| Module firmware | v0.2 builds clean (7.1 KB): full peripheral binding + CAN dispatch around the host-tested `module_core`. 0.2 (2026-09-28) closes the two protection gaps from the bench-test review: OCP backup #2 (`lb_core_ocp_sample`: INA240 ADC or INA228 SOVL alert >110 % i_max for >5 ms) and OVP latch #4 (OVP_TRIP routed to PB4 on the 100 × 80 board; EXTI4 kills EN, tick latches). Untested on silicon (no board yet) |
| Host tests | `cd firmware/tests && make test` — must stay green. **5 suites now**: can, core, manager, scpi, ui |
| Manager firmware | **v0.2 COMPLETE (2026-07-25, commit 174595e)**: `scpi_core` + `ui_core` join `manager_core` as host-tested cores; ESP-IDF shell fully written (display/encoder/USB-SCPI/app_main). **First compile 2026-09-27: builds clean on IDF v5.3.2** (355 KB image, two build fixes). Untested on silicon. See docs/10 |
| Phase-2 circuit design | **complete (docs/08, 2026-07-16)**: all values worked + datasheet-verified; LM5143/LM5069/CSD18540Q5B/CSD19536KTT/XAL1510/TMUX1101/TL431 PDFs now in docs/datasheets/ |
Expand Down
10 changes: 8 additions & 2 deletions docs/04-protection-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ loops are themselves the first protection layer.
| # | Fault | Detected by | Response | Latching | Recovery |
|---|---|---|---|---|---|
| 1 | Output overcurrent (normal) | Analog CC loop (INA240 → EA_I) | Seamless CV→CC transition at I_set | No | Automatic (it's regulation, not a fault) |
| 2 | Output overcurrent (loop failure backup) | MCU ADC watchdog on INA240, >110 % I_max for >5 ms | Output FETs open, LM5143 disabled | Yes | `RESET(0x5A)` after cause cleared |
| 2 | Output overcurrent (loop failure backup) | MCU ADC check on INA240 every 1 ms, plus INA228 ALERT (SOVL at the same limit): >110 % I_max for >5 ms | Output FETs open, LM5143 disabled | Yes | `RESET(0x5A)` after cause cleared |
| 3 | Per-phase overcurrent (shoot-through, saturation) | LM5143 cycle-by-cycle current limit + hiccup | Cycle truncation → hiccup restart | No (hiccup) | Automatic; MCU reports if persistent >100 ms |
| 4 | Output overvoltage | Hardware comparator (TLV7011, fixed 105 % V_max) — independent of MCU | Output disconnect FETs open + controller EN low | Yes | `RESET(0x5A)`; requires V_out below threshold |
| 4 | Output overvoltage | Hardware comparator (TLV7011, fixed 105 % V_max) — independent of MCU | Output disconnect FETs open (comparator, directly) + controller EN low (MCU EXTI on the comparator output, µs) | Yes (firmware latch) | `RESET(0x5A)`; requires V_out below threshold |
| 5 | Output overvoltage (setpoint sanity) | Module firmware clamp | I_set/V_set clamped to envelope: I ≤ min(30 A, 600 W/V) | No | n/a — clamp, report in STATUS warn bits |
| 6 | Battery back-feed into disabled output | Back-to-back output FETs (blocking both directions) | Inherently blocked | n/a | n/a |
| 7 | Reverse current while enabled (battery > V_set) | DEM mode (LM5143 DEMB) — stage cannot sink | Inherently blocked in battery mode; firmware warns if I_meas < −200 mA in non-DEM mode and opens FETs | Yes (non-DEM case) | `RESET(0x5A)` |
Expand All @@ -35,6 +35,12 @@ loops are themselves the first protection layer.
module's absolute V_max, not the current setpoint — it is a catastrophic-
failure backstop (e.g. CV loop open), not a user-range protection. Tight
user-level OVP is a firmware warn/trip configured via LIMITS if desired.
- Phase 1 implements fault 4 as: TLV7011 → Q9 opens the disconnect with no
MCU involvement; the same OVP_TRIP net reaches MCU PB4, whose EXTI handler
sets PS_OFF (EN low, and Q7 also holds the disconnect off) and the 1 ms
tick latches `OVP_HW`. The comparator alone is non-latching; the latch
lives in firmware. If the MCU hangs, the IWDG reset boots into SAFE with
PS_OFF high, so the converter still ends up off.
- Battery work always uses `OUTPUT(on+DEM)` mode: source-only power stage
(fault 7) plus blocking disconnect when off (fault 6) means no path ever
drains or back-feeds the pack.
Expand Down
10 changes: 7 additions & 3 deletions docs/07-module-firmware.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ if the schematic changes, board.h must follow):
| PA5/7 | DAC SCLK/SDI | SPI1 AF5 | PB5 | AUX_PG | in |
| PA6 | VBUS_SNS | ADC2_IN3 | PB6 | HW_EN | in, high=enabled |
| PA8/9 | I2C SDA/SCL | I2C2 AF4 | PB7 | INA_ALERT | in, **internal pull-up** |
| | | | PB4 | OVP_TRIP | in, pull-down, EXTI4 rising |
| PA10 | CAN_STB | out, low=run | PB10 | FAN_PWM | TIM2_CH3 AF1 |
| PA11/12 | CAN RX/TX | FDCAN1 AF9 | PB11-13 | SLOT_ID | in, pull-up, inverted |
| PA15 | PS_PGOOD | in | PB14 | OUT_REQ | out, high=close |
Expand Down Expand Up @@ -78,9 +79,12 @@ FAULT frame on any state/fault-bit change.
Per 1 ms tick:
1. ADC scan; hottest NTC -> `lb_core_set_temp` (derate #11 / OTP #12).
2. `lb_core_tick` (ramp, envelope, comms timeout).
3. Backup latches: V_MEAS > 112 % of v_max -> OVP fault #4 (reporting only —
the comparator already tripped the disconnect); INA_ALERT low in ACTIVE ->
OCP backup #2.
3. Latches: OVP_TRIP (PB4) high, or its EXTI4 edge seen -> OVP fault #4
(the EXTI handler already set PS_OFF and cleared OUT_REQ within µs);
V_MEAS > 112 % of v_max -> OVP #4 as a terminal-side backup;
`lb_core_ocp_sample`: INA240 current > 110 % of i_max, or INA_ALERT low
(INA228 SOVL programmed to the same limit at boot), for > 5 ms while the
output is closed -> OCP backup #2.
4. DAC references written with calibration applied.
5. Outputs: `PS_OFF = !(state ok && mode!=OFF && hw_enable)`;
`OUT_REQ = output_closed && PS_PGOOD`; `PS_FPWM = !DEM`.
Expand Down
40 changes: 22 additions & 18 deletions docs/12-phase1-bench-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ to `docs/test-results/phase1-<board serial>-<date>.md` and fill it in as you
go. Save scope captures and CAN logs next to it. When every row passes, tag
the commit `phase1-pass` (docs/05, repo workflow).

> **Heads-up: predictions from reading the design.** Two exit tests are
> expected to show a gap between the spec and what the board and firmware do
> today (§6.9). Run them anyway and record what happens; they are the point of
> a learning board.
> - Row 2 (OCP backup): firmware relies on the INA228 ALERT pin, but
> `ina228.c` never programs an alert limit, so the backup may never fire.
> - Row 4 (hardware OVP): the TLV7011 opens the disconnect, but in the netlist
> its output reaches only Q9 (the disconnect), not the LM5145 EN pin or the
> MCU. So expect non-latching behaviour with the converter still running,
> where docs/04 says "EN low, latching".
> **Protection gaps closed in firmware 0.2 (2026-09-28).** Two exit tests
> (§6.9) used to be expected to fail; both are now fixed and should pass.
> - Row 2 (OCP backup): the firmware now checks the INA240 current on the ADC
> every 1 ms and also programs the INA228 alert limit (SOVL) to the same
> 8.8 A; either one held for more than 5 ms latches `OCP_BACKUP`.
> - Row 4 (hardware OVP): the TLV7011 output (OVP_TRIP) now also goes to the
> MCU's PB4. Its rising edge interrupts the MCU, which pulls LM5145 EN low
> and latches `OVP_HW`. The comparator still opens the disconnect by itself.
> This needs the board revision that routes OVP_TRIP to U10 pin 41; on an
> older board PB4 is unconnected and row 4 stays non-latching.

## 1. Equipment

Expand Down Expand Up @@ -370,8 +370,10 @@ record. Clear latched faults with `reset clear` and re-enable between rows.
(EAI_INJ → FB). Setpoint 12 V / 4 A. Load in CC, step from 2 A to 9 A.
- Spec: MCU detects > 110 % I_max (8.8 A) for > 5 ms, opens the output and
latches OCP_BACKUP.
- Predicted: no latch (the INA228 alert limit is never configured). The
LM5145 valley current limit (~11 A) remains the backstop.
- Predicted (firmware 0.2): latches `OCP_BACKUP` about 5 ms after the
current passes 8.8 A, from the ADC check or the INA228 alert, whichever
sees it first. The LM5145 valley current limit (~11 A) remains the
backstop behind it.
- Record: whether FAULT `OCP_BACKUP` appears, and the output current and
voltage. Keep this short; do not leave it at 9 A for long. Refit R8.

Expand All @@ -381,12 +383,14 @@ drives the output towards the input voltage.
- Spec: TLV7011 trips at 105 % of 21.2 V ≈ 22.3 V (R45/R46 from VOUT_INT
against the 2.5 V reference R47/R48), the disconnect opens, the controller
is disabled, and the fault latches.
- Predicted: the disconnect opens at ~22.3 V; the controller keeps running
(VOUT_INT stays high until the short is removed); nothing latches, because
the firmware check reads VOUT after the disconnect.
- Record on the scope: VOUT_INT (R1 pad 1), VOUT (J4), OVP_TRIP (Q9 gate).
Trip voltage, time from threshold to disconnect open, and state after the
short is removed.
- Predicted (firmware 0.2, OVP_TRIP routed to PB4): the disconnect opens at
~22.3 V; within microseconds the MCU kills EN, so VOUT_INT falls; FAULT
`OVP_HW` latches and stays latched after the short is removed until
`reset clear`. A `reset clear` while VOUT_INT is still above the threshold
re-latches at once.
- Record on the scope: VOUT_INT (R1 pad 1), VOUT (J4), OVP_TRIP (Q9 gate),
PS_EN (U3 pin 1). Trip voltage, time from threshold to disconnect open,
time from OVP_TRIP to PS_EN low, and state after the short is removed.

**Rows 11 and 12 — overtemperature.** Emulate a hot NTC by clipping a
resistor across RT1 (NTC_FET), rather than heating the board. At ~25 °C the
Expand Down
19 changes: 19 additions & 0 deletions firmware/module/core/module_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ void lb_core_fault(lb_core *c, uint8_t fault_bit)
c->vref_uv = 0;
}

int32_t lb_core_ocp_limit_ua(const lb_core_cfg *cfg)
{
return cfg->i_max_ua + cfg->i_max_ua / 10;
}

void lb_core_ocp_sample(lb_core *c, int32_t i_ua, bool ina_alert, uint32_t dt_ms)
{
bool over = i_ua > lb_core_ocp_limit_ua(&c->cfg) || ina_alert;
if (!over || !lb_core_output_closed(c)) {
c->ocp_over_ms = 0;
return;
}
c->ocp_over_ms += dt_ms;
if (c->ocp_over_ms > LB_OCP_BACKUP_MS) {
c->ocp_over_ms = 0;
lb_core_fault(c, LB_FAULT_OCP_BACKUP);
}
}

void lb_core_tick(lb_core *c, uint32_t dt_ms)
{
if (c->ms_since_mgr <= c->cfg.comms_timeout_ms) c->ms_since_mgr += dt_ms;
Expand Down
9 changes: 9 additions & 0 deletions firmware/module/core/module_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ typedef struct {
int32_t iset_ua;
int32_t vref_uv; /* live, ramped V reference */
uint32_t ms_since_mgr;
uint32_t ocp_over_ms; /* consecutive ms above the backup limit */
} lb_core;

void lb_core_init(lb_core *c, const lb_core_cfg *cfg);
Expand All @@ -59,6 +60,14 @@ void lb_core_set_hw_enable(lb_core *c, bool enabled);
void lb_core_set_temp(lb_core *c, int16_t dC);
void lb_core_fault(lb_core *c, uint8_t fault_bit); /* latching, matrix rows */

/* Matrix #2, OCP backup for a failed CC loop: latches OCP_BACKUP once the
* output current has been above 110 % of i_max, or the INA228 alert (same
* limit, programmed by the HAL) has been asserted, for more than
* LB_OCP_BACKUP_MS consecutive ms while the output is closed. */
#define LB_OCP_BACKUP_MS 5u
int32_t lb_core_ocp_limit_ua(const lb_core_cfg *cfg);
void lb_core_ocp_sample(lb_core *c, int32_t i_ua, bool ina_alert, uint32_t dt_ms);

/* -- periodic -------------------------------------------------------------- */
void lb_core_tick(lb_core *c, uint32_t dt_ms);

Expand Down
6 changes: 4 additions & 2 deletions firmware/module/src/board.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
/* PA6 VBUS_SNS ADC2_IN3 PB2 LED_SINK out (low = LED on) */
/* PA7 DAC_SDI SPI1 AF5 PB3 PS_OFF out (high = kill LM5145 EN) */
/* PA8 I2C_SDA I2C2 AF4 PB5 AUX_PG in */
/* PA9 I2C_SCL I2C2 AF4 PB6 HW_EN in (high = enabled) */
/* PA9 I2C_SCL I2C2 AF4 PB4 OVP_TRIP in, EXTI4 (high = HW OVP) */
/* PB6 HW_EN in (high = enabled) */
/* PB7 INA_ALERT in (low = alert) */
/* PB10 FAN_PWM TIM2_CH3 AF1 */
/* PB11..13 SLOT_ID0..2 in, pull-up, */
Expand All @@ -46,6 +47,7 @@
#define AUX_PG_IN() ((GPIOB->IDR >> 5) & 1u)
#define HW_EN_IN() ((GPIOB->IDR >> 6) & 1u)
#define INA_ALERT_IN() ((GPIOB->IDR >> 7) & 1u) /* open-drain, 0 = alert */
#define OVP_TRIP_IN() ((GPIOB->IDR >> 4) & 1u) /* TLV7011, 1 = tripped */
#define SLOT_ID_IN() ((uint8_t)(~(GPIOB->IDR >> 11) & 0x7u))

/* ---- analog scaling (doc 06; ideal values, trimmed by lb_cal) ------------ */
Expand Down Expand Up @@ -96,7 +98,7 @@ static inline int16_t ntc_dC(uint16_t counts)

/* ---- firmware identity ---------------------------------------------------- */
#define FW_MAJOR 0
#define FW_MINOR 1
#define FW_MINOR 2

/* millisecond tick from SysTick */
extern volatile uint32_t g_ms;
Expand Down
1 change: 1 addition & 0 deletions firmware/module/src/drv.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ void dac_write_i(uint16_t counts); /* DAC-B = I_REF */

/* ina228.c — I2C2, addr 0x40 (A1=A0=GND) */
bool ina228_init(void); /* false: not responding (fault #18) */
bool ina228_set_ocp_limit(int32_t i_ua); /* SOVL -> ALERT (matrix #2) */
bool ina228_read(int32_t *vbus_uv, int32_t *cur_ua);
bool ina228_read_energy(int64_t *charge_nAh, int64_t *energy_nWh);

Expand Down
17 changes: 16 additions & 1 deletion firmware/module/src/ina228.c
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#define R_ENERGY 0x09
#define R_CHARGE 0x0A
#define R_DIAGALRT 0x0B
#define R_SOVL 0x0C
#define R_DEVID 0x3F

#define CURRENT_LSB_nA 20000 /* 20 uA in nA for integer math */
Expand Down Expand Up @@ -83,7 +84,21 @@ bool ina228_init(void)
if (!wr16(R_CONFIG, 1u << 4)) return false;
if (!wr16(R_ADCCFG, (0xFu << 12) | (5u << 9) | (5u << 6) | (5u << 3) | 2u))
return false;
return wr16(R_SHUNTCAL, 2097);
if (!wr16(R_SHUNTCAL, 2097)) return false;
/* ALERT: transparent (not latched), active low, compared on every
* non-averaged conversion (ds Table 7-16: ALATCH=CNVR=SLOWALERT=APOL=0).
* The firmware debounces it (module_core OCP backup, >5 ms). */
return wr16(R_DIAGALRT, 0);
}

/* SOVL: 1.25 uV/LSB at ADCRANGE=1 (ds Table 7-17). Across 2 mOhm that is
* 625 uA per LSB, so 8.8 A -> 14080. */
bool ina228_set_ocp_limit(int32_t i_ua)
{
int32_t lsb = i_ua / 625;
if (lsb > 0x7FFF) lsb = 0x7FFF;
if (lsb < 0) lsb = 0;
return wr16(R_SOVL, (uint16_t)lsb);
}

bool ina228_read(int32_t *vbus_uv, int32_t *cur_ua)
Expand Down
Loading
Loading