ui: fix stepped breathe at low charge ratios + nexcyber feasibility prereq updates#2
Merged
Conversation
The EVSE_CHARGING cyan-breathe path was pre-multiplying the linear
breathe envelope by ratio_pct (active/advertised current) before
handing it to fill_all, which then ran scaled_gamma. That order
re-introduces the precision-loss the scaled_gamma comment at the
top of the file specifically warns against: scale-before-gamma at
low ratios squashes the input range so hard that gamma can't
recover smooth dynamic range.
Numerical example, ratio_pct=21, brightness_pct=100 (advertised
32A, actively drawing 7A), across the breathe cycle:
breathe v | OLD (scale→gamma) | NEW (gamma→scale)
-------- + ------------------- + ------------------
125 | 26 → 2 → out=2 | 61 → 12 → out=12
200 | 42 → 6 → out=6 | 156 → 32 → out=32
255 | 53 → 11 → out=11 | 255 → 53 → out=53
Old: ~9 distinct output levels across the breathe → visibly stepped.
New: ~41 levels → smooth.
Fix is to compose ratio_pct with brightness_pct into one combined
percent and pass it through fill_all, so gamma sees the full
125-255 breathe range and both percent scales apply after gamma.
This is the same pattern scaled_gamma() was introduced to use; the
EVSE_CHARGING case was the only remaining caller doing it the old
way.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SRAM = 80 KB confirmed by SWD probe (Nations N32G457RBL/REL family) - No external SPI NOR on the board (visual ID) - Persistence: internal-flash ping-pong, ~8 KB carved off top of 128 KB - OTA: single-bank, no self-rollback (SWD recovery if a write fails) - Phase 4 effort drops 1-2 days (no W25Q driver port) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
RAR
added a commit
that referenced
this pull request
May 14, 2026
) * cmake: add OPENEVCHARGER_BOARD selector + M4F toolchain Introduces a board-selection variable so the codebase can host more than one MCU target. Defaults to `rippleon` (the GD32F205VE bench unit that's been the only target through M0-M7), so no behavior change for the current build. `OPENEVCHARGER_BOARD=nexcyber` is reserved for the in-progress port to the Tuya-based Nexcyber AC EVSE / Nations N32G45x; the configure step emits a FATAL_ERROR pointing at boards/nexcyber/README.md until the vendor SDK is vendored and the HAL ported. The companion arm-none-eabi-toolchain-cm4f.cmake mirrors the M3 toolchain but with -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard for the Cortex-M4F core in the N32G45x. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: bring-up skeleton (pin map + linker + porter notes) Scaffolds the Nexcyber / Nations N32G45x port. Skeleton only — no HAL implementation, no vendor SDK vendored yet. Sets up the structure so future bench sessions have a place to land driver code. What's included: - pin_map.h: 27 confirmed pins from the 2026-05-07 SWD firmware dump (USART1 = WBR2 link, USART2 = Nextion HMI, USART3/SPI2 = BL0939 candidate, TIM1_CH1 = J1772 CP PWM, 5-channel ADC, beeper). The 9 silent ULN2003-driven outputs and 3 candidate digital inputs are marked TODO bench-resolve. - n32g457.ld: 120 KB FLASH + 8 KB PERSIST + 80 KB RAM. PERSIST region carved off the top for internal-flash ping-pong (no external SPI NOR on this PCB — feasibility doc Option A). - README.md: porter's checklist — what's done, what needs the bench, and the exact `git submodule add` for the Nations SDK mirror (NationsTechCoreLib/N32G455xx, V3.1.0, 3-clause-BSD). - hal/ + docs/: empty placeholder directories for Phase 2 driver code and per-port engineering notes. BOARDS.md updated to link the skeleton README and move Nexcyber from "Wishlist" to "In progress". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: pin_map mains-on bench update (2026-05-09) 120 V single-leg bench session resolved one of the nine silent OUT_PP pins and refined SRAM cache layout: - PB0 = GFCI CAL pulse relay coil (PIN_GFCI_CAL). Confirmed by audible click of the small relay near the NTC/CP connector when PB0 was pulsed HIGH for 500 ms via SWD. Matches the NOTES.md hypothesis. - PC11 promoted to PIN_PERIPHERAL_EN — only OUT_PP pin stock fw drives HIGH at idle, strongly suggesting a chip-enable for the WBR2 / Nextion / BL0939. Toggling it produced no externally visible response, consistent with a CE rather than an LED drive. - The remaining eight OUT_PP pins (PA0, PA1, PA15, PB8, PB9, PC8, PC10, plus PC11) were silent on bench. Most plausible reasons: bench unit doesn't have all status indicators fitted, contactor drives need a redundant-drive pattern (need stock fw to trigger), or the L2 contactor refuses to close on 120 V single-leg supply. Resolution path documented in pin_map.h — needs J1772 state-walk with stock fw to identify the contactor drives by ODR delta. - SRAM ADC cache layout learnings: 0x20000700-0x20000744 is a 40- halfword circular buffer for waveform analysis (currently railed → consistent with L2 ZMPT107 floating because L2 is dead), 0x20000748+ holds the 4-channel scan cache, 0x2000075c is the CP voltage cache (corrected to int32 mV, was uint16_t in earlier memory; bench shows -300 mV at idle with no plug). No HAL or build changes; documentation/header only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * third_party: vendor Nations N32G455xx SDK V3.1.0 Pulls in the Nations SPL + CMSIS subset needed for the in-progress Nexcyber port (Nations N32G45x main MCU). Vendored from upstream mirror NationsTechCoreLib/N32G455xx, tag V3.1.0, commit 5c43c149bfcde5bc37b90f711071883d3f57b0c5. Layout mirrors third_party/GD32F20x_Firmware_Library/ (cmsis/{core, variants/n32g45x,startup_files} + spl/{inc,src}) so the CMake board branches read symmetrically. ~52 MB upstream subset down to ~2 MB vendored — examples (37 MB), RT-Thread middleware, USB stack, and algo blobs all excluded; layout adaptation + drop list documented in third_party/N32G45x_Firmware_Library/README.md. License is Nations' BSD 2-clause with a non-endorsement clause — GPL-3.0 compatible. Same template as the GD32 vendor lib next door. Companion task #1 of the 4-task M0 bring-up batch; commits #2-#4 follow on this branch (board HAL, CMake wiring, CI matrix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: M0 HAL (clock + uart) + bring-up main Minimal bring-up firmware for the Nations N32G45x: enough to confirm the chip is alive on the bench after a first flash. - boards/nexcyber/hal/clock.c Trusts the Nations SDK's SystemInit() (HSE_PLL → 144 MHz default from the 8 MHz HXTAL) and just publishes the rate via printk. Matches the src/hal/clock.h interface so a future shared main.c can call clock_real_120m_init() / clock_log_status() agnostically. - boards/nexcyber/hal/uart.c USART1 on PA9 (TX) / PA10 (RX) at 115200 8N1 — the same pads the Tuya WBR2 Wi-Fi module sits on (the easiest probe point on the bench). uart_init / uart_write / printk match the rippleon HAL surface but skip the timestamp prefix and FreeRTOS dependency since M0 runs pre-scheduler. Format spec is the same %s/%d/%u/ %x/%c/%% subset. - boards/nexcyber/main.c Drives clock_init → uart_init → printk("M0 bring-up") → heartbeat loop. No FreeRTOS, no tasks, no peripherals beyond the log UART; the rest gets layered on as M1+ HAL files port. Empty `_init` / `_fini` stubs included to satisfy newlib's __libc_init_array — same idiom as src/main.c on the rippleon target. Companion task #2 of the 4-task M0 bring-up batch (SDK vendoring landed in the preceding commit). CMake wiring + CI matrix in #3 / #4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * cmake: wire nexcyber board path → buildable M0 image Replaces the FATAL_ERROR placeholder on the nexcyber branch with the real Nations-SDK wiring. Now `cmake -S . -DOPENEVCHARGER_BOARD=nexcyber -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-toolchain-cm4f.cmake` produces a flashable .elf: - Vendor lib paths point at third_party/N32G45x_Firmware_Library/ (cmsis/core, cmsis/variants/n32g45x, cmsis/startup_files, spl/{inc,src}). - APP_SRCS is the M0 trio: boards/nexcyber/main.c + the two HAL files. No FreeRTOS, no safety core, no persist layer — those come online alongside the matching HAL ports. - Pulls in only the SPL modules touched by the M0 image (misc, gpio, rcc, usart) rather than the full driver set; extend as more HAL modules land. - N32G45X=1, HSE_VALUE=8000000, USE_STDPERIPH_DRIVER=1 mirror the define set the Nations example projects use. - LINKER_SCRIPT switches to boards/nexcyber/n32g457.ld (128 KB flash / 80 KB SRAM / 8 KB persist carve-off). Refactor moves the build-info derive + post-build objcopy + size dump into a shared block above/below the two board branches. Rippleon build path is byte-identical to before — verified locally: rippleon: 52,956 B FLASH / 35,696 B RAM (matches main) nexcyber: 2,792 B FLASH / 2,576 B RAM (M0 only) Companion task #3 of the 4-task M0 bring-up batch. CI matrix wiring in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: matrix firmware build for rippleon + nexcyber (continue-on-error) Promotes the firmware workflow from a single rippleon job to a matrix over [rippleon, nexcyber]. Both rows use the same checkout + arm-none-eabi-gcc setup + configure/build/size/artifact pipeline, just parameterised on board + toolchain file. nexcyber gets `continue-on-error: true` for now: it's an in-flight port (M0 bring-up only — clock + UART + heartbeat printk, no safety core yet) and a hard PR gate would create more friction than it catches. Flip to false once the board ports through M0–M3 on the bench (matching the bench-validated detector + persistence story the rippleon side already has). Artifact names get a board suffix (`openevcharger-rippleon` / `openevcharger-nexcyber`) so both attach to the same workflow run without collision. Companion task #4 of the 4-task M0 bring-up batch — closes the "add nexcyber row with continue-on-error" item from the agreed preference (see feedback memory openevcharger_ci_new_boards). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: refresh README for M0 reality + WBR2-side companion plan README was written pre-M0 and still listed SDK / CMake / HAL items as "not yet here." Sweep: - Status banner: bring-up skeleton → M0 (clock + log UART + heartbeat printk). Buildable; not yet flashable to a real EVSE. - "What's here" table picks up hal/clock.c, hal/uart.c, main.c with their current scopes. - New "Build" block shows the actual M0 cmake invocation + the size ballpark (~2.8 KB FLASH / ~2.6 KB RAM). - Roadmap split into bench-blocked vs code-only items so the next session can see what's actionable on each side. - New "Companion ESPHome side" section documents the path from `esphome/testcharger/testcharger.yaml` (current stock-TuyaMCU bench config on the WBR2 module) to a future `OpenEVCharger/wbr2/openevcharger.yaml` that speaks our TLV protocol — captured as a strip-out / add-in table so the actual adaptation work is mechanical when M2+ arrives. Out of scope until then so the bench unit stays usable against stock firmware. No code changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: M1 FreeRTOS scaffold + heartbeat task Wire ARM_CM4F FreeRTOS port into the nexcyber CMake branch and swap the M0 busy-loop heartbeat in main() for a vTaskDelay-driven one. Smallest possible "scheduler is alive" trace — if printk timing becomes exactly 1 Hz (vs the M0 nop-loop's "tight nop loop"), the scheduler is running. The shared src/FreeRTOSConfig.h is core-independent (configPRIO_BITS=4 matches both GD32F2 Cortex-M3 and N32G45x Cortex-M4F NVIC layouts; SVC/PendSV/SysTick handler aliasing maps to standard Cortex-M vector names that the Nations startup file already references). FPU support auto-handles via the toolchain's -mfpu=fpv4-sp-d16 -mfloat-abi=hard which sets __ARM_FP — no extra config flag needed. Build sizes — nexcyber: Before (M0): 2,792 B FLASH / 2,576 B RAM After (M1): 6,388 B FLASH / 19,232 B RAM (~3.6 KB FreeRTOS code + 16 KB heap_4 reservation) Rippleon target unchanged: still 52,956 B FLASH / 35,696 B RAM. Also generalise .gitignore: `build_*/` covers per-board build dirs (build_nexcyber, build_rippleon) the same way `build/` covered the single-target builds before the board selector landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: M2 GPIO HAL — confirmed pins only Idempotent one-shot config for every confirmed pin in pin_map.h. Same gpio_init_all() / gpio_log_straps() surface as rippleon's src/hal/gpio.c. Pins configured (16 total): Outputs (drive safe-state BEFORE configure, as F1-style mode change could otherwise glitch): PC6 buzzer → LOW (no tone at boot) PB0 GFCI CAL → LOW (relay coil de-energised) PC11 periph_en → HIGH (matches stock idle ODR snapshot) AF (peripheral takes the pad later): PA8 CP_PWM → AF_PP @ 50 MHz (ready for TIM1_CH1 in M3) Inputs with pull-up: PA11/PA12 touch → IPU (TTP223 active-low taps) PC13 GFCI sense → IPU (polarity TBD bench) Input floating: PC3 mains-det A → IN_FLOATING (per stock static decode) Inputs pull-down: PC7/PC9 mains-det B/C → IPD (60 Hz toggle when live) Analog inputs: PB1/PB2/PC0/PC4/PC5 → AIN (J1772 + ZMPT correlation bench-blocked) Pins NOT touched, deliberately: USART2 (PA2/PA3), USART3 (PB10/PB11), SPI2 (PB12-15) — owned by their peripheral drivers, all bench-blocked roles. PA0/PA1/PA15/PB8/PB9/PC8/PC10 — TBD OUT_PP. Don't drive unknown loads. Reset default (input floating) is safe. PC2/PC12 — AF_PP with unidentified remap target. Skip until AFIO_MAPR is decoded on bench. Ports D/E/F/G — entirely unused in stock firmware. main.c calls gpio_init_all() + gpio_log_straps() after uart_init(). Boot log on first flash will now print: clock: SDK default 144000000 Hz (M0 bring-up) openevcharger nexcyber: M2 bring-up (FreeRTOS + GPIO HAL) straps: btn=11 gfci=? mains=??? beat 0 beat 1 ... The strap line gives a single-shot baseline for bench correlation without an oscilloscope. Build sizes — nexcyber: Before (M1): 6,388 B FLASH / 19,232 B RAM After (M2): 6,896 B FLASH / 19,232 B RAM (+508 B for the HAL + Nations SPL GPIO calls) Rippleon target unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: refresh README for M1+M2 reality What's-here table grows from 4 entries to 5 (adds hal/gpio.c) and the status box jumps M0 → M2. Roadmap table flips two rows from "not started" → "✅" (FreeRTOS port, GPIO HAL) and splits the remaining work into M3 (ADC + timer + Nextion USART2) vs M3+ (BL0939 once the variant is bench-confirmed) vs safety-core-port (waits on the HAL preconditions). Boot-time output now also includes a `straps: btn=11 gfci=? mains=???` one-liner from `gpio_log_straps()`, which gives a single-shot bench baseline without an oscilloscope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: lock PC13 = STOP loop, PC11 = safety-loop enable Three-snapshot SWD sweep on the bench 2026-05-11 (mains 120 V single-leg, fault_bitmap=8 latched throughout) resolved two long-standing TBDs: PC13 — E-stop loop sense, NC switch (active-LOW) Snap A: STOP jumpered closed → PC13 HIGH Snap B: jumper momentarily off → PC13 LOW (caught in transition) Snap C: STOP wired normally → PC13 HIGH Original "GFCI fault sense" hypothesis was wrong — GFCI fault path is the separate ADC trace seen as G1 in the telemetry dump (raw ~2195 = mid-rail = healthy), no dedicated digital sense pin. PC11 — safety-loop-driven enable (NOT the contactor permit) Snap A: STOP jumpered (clean short bypass) → PC11 LOW Snap C: STOP wired through physical switch → PC11 HIGH fault_bitmap=8 was latched throughout — PC11 toggled INDEPENDENTLY of the fault state. So PC11 isn't gated on "everything green", it's gated on "the safety loop is hard-wired correctly, not just clean- shorted". Original "always HIGH at idle" finding from 2026-05-09 was partial — that snapshot was taken with the safety loop fully wired. Pin macro renames: PIN_GFCI_SENSE_* → PIN_STOP_SENSE_* PIN_PERIPHERAL_EN_* → PIN_SAFETY_LOOP_EN_* hal/gpio.c follows the renames + adjusts: - PC11 default-state at boot LOW (don't assert downstream enable on potentially-broken hardware; safety task will raise it once safety_state == OK in M5+) - PC13 stays IPU so a disconnected wire reads LOW (= halt) rather than floating "OK" — fail-safe direction - gpio_log_straps boot one-liner now reads `stop=%d` (was gfci=%d) Build sizes — nexcyber: Before: 6,896 B FLASH / 19,232 B RAM After: 6,892 B FLASH / 19,232 B RAM (-4 B from rename rotation) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: lock PA0/PA1 contactors + PC9 button (bench 2026-05-11) Three more pin roles resolved via SWD wiggle + snapshot-diff during a mains-on bench session with the stock fw running: PA1 — MASTER CONTACTOR PERMIT (both line contactors together) Wiggle test: driving PA1 HIGH clicks BOTH large contactors closed and they stay closed for the duration of the HIGH phase. Plain DC drive. Working interpretation: gates the coil-supply rail (12 V?) to both line contactors via a single high-side switch / opto- isolator → both close in parallel. Macro: PIN_CONTACTOR_MAIN_* PA0 — CONTACTOR TEST PULSE (one specific contactor, momentary) Wiggle test: PA0 HIGH clicks ONE of the two contactors briefly (audible click on rising edge), then auto-opens despite the pin staying HIGH. Working interpretation: contactor weld-detect / self-test pulse — fires one coil so firmware can verify it'll close. Same idiom as rippleon's PB0 GFCI CAL pulse. Driver appears to require transitions, not steady DC — fast-wiggle test pending. Macro: PIN_CONTACTOR_TEST_* PC9 — front-panel "tiny button" (IN_PD, active-HIGH) Snapshot-diff confirmed: idle PC9 IDR=0, held-down PC9 IDR=1. Originally hypothesised as a "mains-detect C" photocoupler in the static decode — turned out to be a button. Likely pairing / WiFi-reset / factory-reset trigger in stock fw. Macro: PIN_BUTTON_* (was PIN_MAINS_DETECT_C_*) PA15 — button-LED candidate (hypothesised, needs wiggle to confirm) Same snapshot-diff caught PA15 IDR briefly HIGH (ODR=0) during the PC9 press. Most likely the button-status LED — firmware flashes it on press as user feedback. PA15 is JTAG-TDI in default AFIO_MAPR; stock fw remaps SWJ_CFG to "SWD only, JTAG off" to free it. Macro: PIN_BUTTON_LED_* (candidate; PA15 wiggle test pending) hal/gpio.c updates: - init_outputs_safe_state() now drives PA0+PA1 LOW *before* configuring as OUT_PP. Critical — leaving these in reset default (analog input) on bench-flashed firmware would let stray pulls energise contactors during chip startup before main() runs. Safety task asserts in M5+. - init_inputs_pulldown() configures PIN_BUTTON_* (PC9) the same way it did the old PIN_MAINS_DETECT_C_* — same mode (IN_PD), just a different downstream role. Macro rename is the only semantic change. - gpio_log_straps() boot one-liner now reads `touch=%d%d btn=%d stop=%d mains=%d%d` (was `btn=%d%d stop=%d mains=%d%d%d`). `touch` = the two TTP223 cap- touch buttons; `btn` = the front-panel tiny button. Build sizes — nexcyber: Before: 6,896 B FLASH / 19,232 B RAM After: 6,932 B FLASH / 19,232 B RAM (+36 B for the new pins) Pin map state — pins fully resolved across all sessions: Confirmed via firmware static analysis (2026-05-07): USART1/2/3, SPI2, TIM1_CH1, 5 ADC channels, PA11/PA12 touch, PC2/PC12 AF (remap target TBD), PC6 buzzer, PB0 GFCI CAL, PC3/PC7/PC9 inputs Pinned by bench probing (2026-05-09 + 2026-05-11): PB0 (GFCI CAL relay click), PC11 (safety-loop enable), PC13 (STOP loop sense NC), PA0 (contactor test pulse), PA1 (master contactor permit), PC9 (front-panel button) TBD bench-blocked: PA15 (button-LED candidate), PB8/PB9/PC8/PC10 (still silent), PC2/PC12 (AFIO_MAPR decode needed), mains-detect A/B (60 Hz scope still pending), CC ladder / NTC raw correlation, BL0939 SPI-vs-UART resolution Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber: BL0939 SPI confirmed + PC11 heartbeat model + SWJ remap Three substantive bench-driven updates from the afternoon's snapshot-diff session on 2026-05-11: 1. BL0939 = SPI variant CONFIRMED. Touching the SPI2 data lines coupled into PB14 (MISO) IDR readback, confirming firmware is actively running an SPI2 transfer cycle reading from a slave. UART-variant hypothesis on USART3 ruled out. M3 BL0939 driver can mirror rippleon's src/hal/bl0939.c + src/hal/spi3.c verbatim modulo pin remap to SPI2. 2. USART3 (PB10/PB11) role narrowed to "stock-fw debug log". 8 literal- pool refs make it heaviest of three USARTs; with BL0939 confirmed elsewhere, debug log is the strongest remaining hypothesis. Candidate target for relocating our M2+ printk so USART1 frees up for the WBR2 TLV link. 3. PC11 = safety-supervisor HEARTBEAT (final revision). Earlier-in- session reads showed PC11 in steady states (HIGH/LOW); later bench observation confirmed it toggles continuously while firmware is happy. Snapshot-diff catches different phases at ~1.5 s sample spacing. Working model: PC11 is a dead-man heartbeat the safety supervisor pulses while it's happy. External watchdog timer keeps a peripheral (GFCI module / coil-supply rail / similar) energised only while pulses keep arriving. On an immediate hardware fault, firmware stops pulsing → watchdog times out → peripheral disabled. Bench evidence: - STOP wired through + GFCI healthy: PC11 oscillates - STOP JUMPERED out: pulsing stops, PC11 latches at last state - GFCI driven HIGH (faked fault) OR GFCI connector disconnected: same — pulsing stops - Latched soft fault (fault_bitmap = 8 L2-missing): heartbeat CONTINUES — PC11 is for immediate hardware faults only IMPORTANT for M2+: driving PC11 statically HIGH is NOT sufficient. Our firmware must MATCH the heartbeat rate. Pulse rate not yet measured — likely 50 Hz–1 kHz based on typical external-watchdog- chip timeout windows. hal/gpio.c additions: - swj_disable_jtag() — AFIO_MAPR SWJ_CFG = SW-DP only via GPIO_ConfigPinRemap(GPIO_RMP_SW_JTAG_SW_ENABLE, ENABLE). Frees PA15/PB3/PB4 from JTAG TAP while keeping SWD alive. Must run before any AF init touches those pads. Matches what stock fw does. - PA15 added to init_outputs_safe_state() — driven LOW at boot. PA15 is hypothesised as the front-panel status LED based on the snapshot-diff observations (IDR briefly HIGH during PC9 button press AND during GFCI fault — multi-purpose status indicator). CMakeLists.txt: - Pre-staged 4 SPL src files for M3 work: n32g45x_adc.c — M3 ADC HAL (PB1/PB2/PC0/PC4/PC5 AIN channels) n32g45x_tim.c — M3 TIM1_CH1 CP PWM on PA8 n32g45x_spi.c — M3 BL0939 SPI driver on SPI2 n32g45x_dma.c — M3 ADC scan DMA support All cold dead-code under --gc-sections until M3 driver code refers to them, so zero runtime overhead at M2. Build sizes — nexcyber: Before: 6,932 B FLASH / 19,232 B RAM After: 7,380 B FLASH / 19,232 B RAM (+448 B for swj_disable_jtag, PA15 init, gc-sections-survivor SPL bring-up code) Rippleon target unchanged at 52,956 B / 35,696 B. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber/pin_map.h: DMA probe + revised contactor model Live SWD probe of stock firmware's DMA1/DMA2 register blocks plus a charging-state snapshot of GPIO ODRs forced three pin_map revisions. USART3 (PB10/PB11): role downgraded from "stock-fw debug log" to "third active peer, role TBD". DMA1 channel 3 is configured with RX-DMA (CCR=0x30A1, CNDTR=128, CPAR=USART3_DR) — that rules out a one-way printk path. The literal-pool weight matches a structured- protocol parser, not a printf wrapper. Candidates: RFID reader, internal BLE module, LED-strip controller IC. UART4 / UART5: new block. DMA2 channels 1 and 6 are active with CPARs pointing at 0x40015004 and 0x40015404 — Nations N32G45x relocates UART4 and UART5 from the F1-standard 0x40004C00/0x40005000 APB1 bases. The chip is running 5 active UART RX rings, not 3 as the schematic block diagram suggested. Pinout-to-pad mapping is bench-blocked until the Nations N32G45x reference manual is in hand. PA0 / PA1 contactor model REVERSED: the prior bench interpretation had PA0 as a one-shot test pulse and PA1 as the master contactor permit. A charging-state SWD snapshot (mains live, contactors energised, BL0939 SPI active) showed the opposite: PA0 ODR=1 (held HIGH for the entire charging session), PA1 ODR=0. The new model is that PA1 fires a one-shot close pulse at session start, an external SR latch holds the contactors, and PA0 is the permit/hold line that keeps the latch armed — MCU fault drops PA0, contactors open. This matches a textbook EVSE safety pattern. Backward-compatibility aliases preserved for the old PIN_CONTACTOR_TEST_/MAIN_ names so in-flight code keeps compiling. ADC paragraph added to the ADC AIN block: RCC_APB2ENR reads ADC1/2/3 clock-enable = 0 while the chip is mostly sleeping. ADC1 register block at 0x40012400 reads all-zero in that state. Stock firmware appears to power-cycle the ADC peripheral around each sample for standby-current savings. None of the 5 active DMA channels feeds an ADC. M3 ADC HAL will own clock-enable explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber/pin_map.h: SRAM ADC cache decoded via two-snapshot diff Two SWD peeks of 0x20000700-0x2000077F (steady-state vs mid-charge with EV-sim resistors) pinned 4 of the 6 scan-cache slots: 0x20000750 = CP_RAW (1.99 V no-plug, 1.43 V loaded — compressed read-back range, matches rippleon PB0 pattern; needs 2-point empirical cal for M3 cp_mv()) 0x20000752 = CC raw (flat 1.54 V; no plug = no proximity loading) 0x20000754 = I_L1 (CT mid-rail 1.75 V idle, swings to 0.82 V under load; instantaneous 60 Hz sample, not RMS) 0x20000756 = GFCI sense (analog mid-rail 1.50 V healthy, 1.20 V loaded — confirms GFCI sense routes through ADC, not a digital GPIO; matches prior memory note) 0x2000075A = I_L2 (pairs with I_L1, identical swing pattern) Plus calibrated cache scale confirmed: 0x2000075C = CP_filtered (signed int32 mV). Bench two-point lock: +11,667 mV idle (J1772 state-A +12 V) vs +5,429 mV EV-loaded (state-C +6 V range). M3 should prefer this over the raw at 0x750. 0x20000778 = session-active flag (0 idle, 1 charging — clean edge) Voltage sense channels (0x20000748-0x2000074E) all railed at 3.30 V in both states — ZMPT107 op-amps saturated on the US 120 V single- leg bench supply (L2 leg dead). Need split-phase mains for proper voltage cal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber/pin_map.h: soften 0x756 = GFCI sense claim Three-snapshot SRAM ADC cache test (idle GFCI-unplugged, idle GFCI-plugged, charging attempt with EV-sim resistors) showed 0x756 moves 1.50 V → 1.20 V on charge attempt BUT stays flat at ~1.50 V across GFCI plug/unplug. Rules out the simplest "GFCI subsystem present" reading. Two models stand for 0x756: (a) IS the GFCI sense — but the analog input reads same baseline plugged-vs-unplugged; only real residual current deviates it. The S1 drop would then reflect a true residual event causing the observed 5-second fault on charge attempt. (b) Is an onboard NTC or supply-rail-droop sensor. Disambiguation now bench-blocked on injecting a known residual current. GFCI subsystem health on this board appears to communicate to the MCU through PC11 heartbeat suppression (verified separately in same session: 5 rapid SWD samples of GPIOC ODR with GFCI plugged showed bit-11 toggling), NOT through this ADC channel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber/pin_map.h: 4-snapshot bench locks 0x756 = GFCI sense Fourth snapshot (charging WITH GFCI plugged) provided the disambiguation diff missing in the 3-snapshot version: during the same kind of charging attempt, 0x756 reads 1.43 V with GFCI plugged (close to the 1.50 V healthy mid-rail) vs 1.20 V with GFCI unplugged (floating CT input drifts further). That coupling pattern is the signature of a CT-fed integrator and matches no other candidate. Onboard-NTC alternative ruled out because it doesn't predict the plugged-vs-unplugged differential during the same load condition. Still bench-blocked: a single residual-current injection test (e.g. 6 mA L→ground during charging) would calibrate the swing direction + sensitivity for M5+ GFCI threshold tuning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * boards/nexcyber/pin_map.h: PC3 = mains L1, PC7 = mains L2 (bench-locked) Single GPIOC IDR read on the bench unit running stock fw under US 120 V single-leg supply nailed both mains-detect pin roles + the fault mechanism behind the user's "5-second voltage error": PC3 (IN_FLOATING) = HIGH → L1 present PC7 (IN_PD) = LOW → L2 missing PC7 LOW with mains otherwise alive matches exactly the firmware's "fault_bitmap = 8" (L2 missing) latched-soft-fault behaviour documented earlier in NOTES.md. The MCU rejects charging attempts after a short integration window because it can't see L2 — bench supply only carries one leg. Macros renamed PIN_MAINS_DETECT_L1_* / _L2_* to make the leg explicit. Backward-compat aliases A=L1, B=L2 retained. M5+ implication: track each leg independently; either LOW for > N ticks = FAULT_MAINS_MISSING_L1 / _L2 trips contactor-permit drop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M3: boards/nexcyber/hal/adc_scan — first-cut ADC1 scan + DMA Ports the rippleon adc_scan surface (init/latest/rank) to the Nations N32G45x. Conservative first cut: ADC1 only, 4 ranks (PA6 ch3, PC0 ch6, PC1 ch7, VrefInt ch18). Validates the HAL surface + DMA→SRAM ring + VrefInt cal path before extending to ADC2 (where most signal channels live). Two Nations-specific deviations encountered: 1. ADC1-4 are clocked from AHB (RCC_AHB_PERIPH_ADCx), not APB2. The F1-style RCC_APB2_PERIPH_ADC1 token doesn't exist on this chip. Documented inline. 2. Channel-to-pin mapping is NOT STM32F1-compatible. Per n32g45x_adc.h, PA6 = ADC1 ch3 (not ch6); PC0/PC1 = ADC1 ch6/7 AND ADC2 ch6/7 (shared); PA4/PA5/PA7/PB1/PC4/PC5 = ADC2-only. The header inlines the full Nations map for reference. Wired into main.c heartbeat task — every 5th beat prints "adc pa6=%u pc0=%u pc1=%u vref=%u" so a bench operator can validate ADC1 is alive and VrefInt sits in the expected 1.20 V band (raw ~1490-1540 at 3.3 V Vref, 12-bit right-aligned). s_adc_buf lands at 0x20000010 in this build — easy halt-and-peek target for SWD-based DMA validation before checking the printk output is healthy. Build: 8972 B flash (+~1.3 KB SPL/DMA + this HAL), 19256 B RAM. M3 (a) of 4 — CP PWM, BL0939 SPI, Nextion USART2 remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M3: boards/nexcyber/hal/cp_pwm — TIM1_CH1 J1772 pilot driver Port of rippleon's src/hal/cp_pwm.c to the Nations N32G45x. Pad: PA8 = TIM1_CH1 default AF (no AFIO remap, unlike rippleon which routes TIMER0_CH3 → PE13 via GPIO_TIMER0_FULL_REMAP). Clock math (SDK default chain — HSE 8 MHz × PLL ×18 → SYSCLK 144 MHz; APB2 = /2 = 72 MHz; TIM1 timer clock = APB2 × 2 = 144 MHz): Prescaler 143 → 1 µs tick Period 999 → 1 ms period = 1 kHz ✓ A reconfig of clock_real_120m_init() to lock 120 MHz will need a single-line PSC update (build flag NEXCYBER_CP_PWM_TIM1_CLOCK_HZ). Surface mirrors rippleon: cp_pwm_init / set_idle_high / set_state_f / set_advertise_amps. The J1772 duty-to-amps formula is unchanged (amps × 10 / 6 lower branch, amps × 10 / 25 + 64 upper branch); the earlier amps × 6 / 10 inversion is avoided. Inverting-vs-non-inverting CP buffer status is bench-blocked on this PCB. Defaults to PWM2 mode (non-inverting). Build-flag NEXCYBER_CP_PWM_INVERTING=1 flips to PWM1 mode if a bench scope of PA8 vs CP shows the buffer inverts (the rippleon pattern). Wired into main.c: cp_pwm_init() + cp_pwm_set_idle_high() during boot so the EVSE advertises state-A (+12 V) until M5 safety arbitrates the duty. Build: 10276 B flash (+1304 B for CP PWM + SPL/TIM activation). M3 (b) of 4 — BL0939 SPI and Nextion USART2 remain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M3: boards/nexcyber/hal/{spi2,bl0939} — hardware SPI BL0939 metering Port of rippleon's src/hal/{spi3,bl0939} to the Nations N32G45x. Two transport changes vs rippleon: - Hardware SPI2 (PB13 SCK / PB14 MISO / PB15 MOSI) instead of bit-banged 3-wire. ~562 kHz SCK (APB1 / 64 at the 144 MHz SDK chain) — well under the BL0939's 900 kHz max. - Software-driven NSS on PB12 (SPI_NSS_SOFT). M2 gpio_init_all safe-low init drives it LOW; spi2_init() raises it HIGH after the peripheral is up. Protocol layer unchanged from rippleon: header byte (0x55 read / 0xA5 write), 8-bit addr, 24-bit data, 8-bit checksum (~sum). Bench-confirmed SPI variant per the 2026-05-11 PB14 touch-coupling test recorded in pin_map.h. Calibration constant temporarily hardcoded (BL0939_FREQ_CONST_DEFAULT = 271900). Persist/calibration port to nexcyber lives in M7+; the rippleon-side per-chassis cal model travels directly once that lands. Wired into main.c boot as a one-shot smoke_test() that dumps a few defaulted + runtime registers via printk. Bench operators can verify the link is alive (IA_FAST_RMS_CTRL default 0x00FFFF, TPS_CTRL default 0x0007FF, runtime V_RMS / IA_RMS / IB_RMS / A_WATT non-zero with mains live). Build: 11192 B flash (+916 B for SPI2 + BL0939 + SPL/SPI activation). M3 (c) of 4 — Nextion USART2 driver remains as the last M3 step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M3 (d): boards/nexcyber/hal/nextion — USART2 raw transport Final M3 milestone — USART2 (PA2/PA3) Nextion HMI link at 9600 8N1 with DMA1 channel 6 RX into a 128-byte circular buffer. Surface is intentionally minimal — init / send_cmd (text + 3 × 0xFF) / rx_drain (non-blocking sweep). Protocol parser, frame splitter, page-state model land in M5+ once we know the UI flow needed for OpenEVCharger. Layout matches the stock-fw DMA config decoded earlier (DMA1 ch6, CCR=0x30A1, ~115 B CNDTR; our M3 uses 128 B). Pages observed in stock firmware strings: "setting", "nogun", "chargeing", "waittime". The page-state machine implementing those transitions is M5+ work. Wired into main.c boot to send "page setting" — bench operator can verify TX baud + wiring + display power by watching for the first screen to render. RX path is silent until the user touches the screen, at which point rx_drain() will surface the touch-event bytes for caller-side framing. Build: 11628 B flash (+436 B for Nextion HAL). M3 complete — all four drivers (ADC / CP PWM / SPI2+BL0939 / Nextion) build clean and wire into the boot path. Bench-flash and correlate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tools: flash_nexcyber.sh — manual FPEC programmer for N32G45x OpenOCD 0.12's stm32f1x flash driver auto_probe rejects the Nations N32G45x's DEV_ID 0x511 (no STMicro overlap, unlike rippleon's GD32F205 which happens to share STM32F1 DEV_ID 0x418). No override flag exists. Workaround: drive the FPEC controller manually via direct register pokes from a TCL script. The N32G45x's flash controller is F1-style verbatim (FLASH_KEYR at 0x40022004, KEY1/KEY2 sequence, PG/PER/STRT bits, AR/SR layouts). 2 KB page erase. Word writes from RAM staging to flash via SWD AHB-AP. CRITICAL bench-confirmed gotcha 2026-05-11: ST-Link AHB-AP silently drops `mwh` (16-bit) writes to flash addresses when FLASH_CR.PG=1. No error reported, byte just doesn't land. `mww` (32-bit) writes go through cleanly — flash controller accepts 2-halfword bursts. The TCL uses mww throughout. Validated end-to-end on the bench: M3 build (openevcharger.bin, 11.6 KB, 2907 words) flashed successfully, verified, reset run. PC moves from 0xfffffffe (lockup) to 0x08000d4e (running in flash), s_adc_buf populates with live ADC samples, VrefInt reads 1.19 V (spec 1.16-1.24) confirming ADC + DMA + calibration are operational. flash_nexcyber.sh is a thin wrapper that sets NX_FLASH_BIN env var and invokes openocd with the right interface/target/script. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M3pm: CP polarity fix + ADC2 diag + PB2 = CP_RAW bench-confirmed Three connected findings from a 2026-05-11 evening bench session: 1. CP PWM polarity was wrong — our PWM2-mode default produced CP -12 V instead of +12 V at idle. The nexcyber CP buffer is NON-INVERTING (bench-verified: pin LOW = CP LOW), and PWM2 mode inverts the CCR duty semantic (CCR=ARR+1 → OCxREF=0 → pin LOW). Flipped default to PWM1 mode (OCxREF=1 with CCR=ARR+1 → pin HIGH → CP +12 V idle ✓). Fluke reads +11.72 V at the connector now. The NEXCYBER_CP_PWM_INVERTING build flag stays as an escape hatch for a future inverting-buffer PCB variant. 2. boards/nexcyber include path was resolving to src/hal/adc_scan.h (rippleon's) instead of boards/nexcyber/hal/adc_scan.h (ours) because CMake had `src` before `boards/nexcyber`. main.c worked because its directory has the relative `hal/` path, but adc_scan.c inside boards/nexcyber/hal/ fell through to the -I paths. Reordered: boards/nexcyber first. 3. boards/nexcyber/hal/adc_scan.{h,c} extended with `adc2_diag_scan` — a single-shot ADC2 sweep over the 7 AIN-configured pads ADC1 can't reach (PA4, PA5, PB1, PA7, PC4, PC5, PB2). Fills the exported `adc2_diag_buf` so the bench operator can SWD-peek it during a J1772 state walk to identify CP/CC/NTC etc. J1772 5-point state walk on the bench unit (multimeter at CP connector vs ADC raw) PINPOINTED PB2 = CP_RAW: A CP=+11.72 V → PB2=2172 B CP=+8.50 V → PB2=1662 C CP=+5.71 V → PB2=1081 D CP=+2.81 V → PB2= 506 E CP= ~0 V → PB2= 96 Linear fit: CP_mV ≈ raw * 1000 / 187. 5-point residual < 0.5 V across the positive CP range — plenty good enough for M5 J1772 state thresholds (A/B/C/D bins are each >2 V wide). Slight curve approaching saturation below CP ≈ +1 V. Bonus: PA4 also tracks CP linearly with a SECONDARY slope (~38 counts/V vs PB2's 187). Likely a precision low-current path or differential pair. Use PB2 as primary, PA4 as cross-check. pin_map.h CP_RAW pin moved from PC0 (placeholder) to PB2 (bench-confirmed). VSENSE_L1/L2/CC/NTC still TBD — none of the other ADC2 channels moved meaningfully during the CP walk (different stimulus needed: real EV plug for CC, mains-on charging for I_L1/L2, heat for NTC). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * M4: J1772 state machine + relay/GFCI drivers + monitor task Pulls in shared src/core/j1772.c (board-independent) and adds three nexcyber-specific HAL files plus a monitor task in main.c: - hal/relay.{h,c} — PA0 hold + PA1 close-pulse contactor driver. relay_close() asserts PA0 HIGH first (so the SR latch can latch), delays 1 ms, pulses PA1 HIGH for the configured duration, then releases PA1 LOW. relay_open() drops PA0. Model bench-confirmed via the charging-state SWD snapshot in pin_map.h. - hal/gfci.{h,c} — GFCI CAL pulse on PB0. Active-HIGH (HIGH at MCU → ULN2003 sinks coil → test-pulse relay closes). Default pulse width 500 ms per the 2026-05-09 bench-confirmed click. - hal/adc_scan.{h,c} — adds adc2_cp_raw() / adc2_cp_mv() helpers using the bench-locked PB2 calibration (raw/187 = volts). monitor_task in main.c runs at 10 Hz: - adc2_diag_scan() refresh - j1772_step() with 5-tick debounce (500 ms commit) - printk on state transition + Nextion page switch - Polls STOP (PC13) + mains L1/L2 (PC3/PC7), edges logged - PC11 safety-supervisor heartbeat at 5 Hz (rate bench-blocked) - SWD-triggered g_bench_cmd mailbox: 1 = relay_close, 2 = relay_open 3 = gfci_cal_pulse, 4 = bl0939_smoke_test Build: 13336 B flash (~11 % of 120 KB), 19400 B RAM. Bench-flash validated 2026-05-11: M4 firmware running, ADC2 scan filling adc2_diag_buf, CP raw tracking through J1772 states. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber pin_map: PC4 = GFCI sense — locked via real fault injection Bench-confirmed 2026-05-11 evening. A real residual-current fault injected through the GFCI subsystem produced a +242 raw deflection on PC4 (ADC2 ch5), 18× larger than any other ADC channel's swing (<14 counts). All channels returned to baseline on fault clear with PC4 showing the slow RC-decay characteristic of a CT integrator. | state | PC4 raw | Δ from baseline | |-------|---------|------------------| | baseline (no fault, contactor open) | 2118 | 0 | | GFCI fault active | 2360 | +242 | | just-cleared | 2300 | +182 (decaying) | Polarity note: a CAL pulse on PB0 also moves PC4 but in the OPPOSITE direction (-234 vs +242 for a real fault). The CT is polarity-aware; the CAL relay injects test current the other way around the core. M5 safety task should treat any |PC4 - 2100| > N as a GFCI event, not just upward deflection. PIN_ADC_CC_PORT/_PIN at PC4 was a placeholder that turned out wrong — PC4 is GFCI sense, not CC. Old macro kept as a backward-compat alias to avoid breaking in-flight references; PIN_ADC_GFCI_SENSE_* is the correct name going forward. CC physical pin is still unknown (needs a real plug-in stimulus to identify). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber pin_map: PE continuity has NO dedicated sense pin Bench-confirmed 2026-05-11 via real PE-fault injection. The tester's PE-fault mode reads on this EVSE as "open CP cable" — PB2 jumps to state-A levels (2172) regardless of CP-dial position (was state C = 1080 before fault). PA4 followed (CP secondary). GFCI sense (PC4) showed a small -137 deflection from current-path shifts. Matches the stock-fw SRAM cache layout: no PE_SENSE slot exists (only CP / CC / I_L1 / GFCI / I_L2). The EVSE's PE detection is indirect — likely via the CP behavior change ("CP at state A while plug was known-engaged" = PE fault candidate) plus the small GFCI- sense deflection. M5 safety-task implication: nexcyber's PE fault detection lives in the system_state layer, not the HAL — there's no PE GPIO/ADC pin to read directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: lock PC0+PC1 as redundant gun-NTC reads, PC5 = onboard NTC Bench-confirmed 2026-05-11 pm via ground-short test. Shorting the gun-connector NTC pin collapsed BOTH ADC1 ch6 (PC0) and ADC1 ch7 (PC1) from rail/mid simultaneously to 0 — they are two parallel conditioning paths off the same physical thermistor pad: PC0: high-impedance read, ~3.33 V open (pegged to rail) PC1: divided read, ~2.20 V open (mid-rail, range-compressed) This is a textbook redundant-sensor pattern: M6 over-temp logic gets to detect open/short/mismatch faults by comparing the two reads against each other. PC5 (ADC2 ch12) did NOT respond to the short, so it's not the gun NTC despite sitting at the "1.7 V" value the prior memory entry attributed to NTC1. PC5 reads the onboard PCB thermistor (enclosure / contactor area temp). Adds tools/ntc_peek.py to make halt-peek-resume a one-shot during ADC pin identification: prints labeled table of both ADC buffers, supports baseline save and Δraw/Δmv diff to spot deflections. Renamed PIN_ADC_NTC_PORT/PIN → PIN_ADC_NTC_{GUN_A,GUN_B,BOARD}_* to reflect the three roles. gpio.c now configures all three pads as AIN. Memory entry "OpenEVCharger bench: gun NTCs not populated" was partially wrong: the gun NTC IS populated (or at least the line is unshorted at rest), and the two parallel reads explain the prior "NTC1 floats ~1.7 V, NTC2 = 0 V" observation as a mis-attribution of PC5 (onboard) to one slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: LED ring driver (blue=PC10, green=PC12), bench-validated Bench-confirmed 2026-05-11 pm via tools/led_walk.py with corrected active-HIGH polarity. Topology: 12 V LED rail, three discrete LEDs (R/G/B) on the ring daughter board with no driver IC, each cathode routed back to the main PCB through an NPN/MOSFET buffer. MCU pin HIGH → buffer ON → LED cathode pulled to GND → LED lit. Pin attribution: PC10 = BLUE (active-HIGH, OUT_PP) PC12 = GREEN (active-HIGH, OUT_PP) red = TBD; driver hardware-damaged on this bench unit. 8 V across the LED string at rest, no response to forced PC11 or PC14 drive, consistent with a buffer FET stuck in linear region. Stock fw pulsed red with a PWM breathing pattern, so the original drive is a TIM channel; re-identify on a fresh unit. Tooling trail (kept for future fresh-unit re-ID): led_walk.py — active drive sweep with operator-in-the-loop (drives each candidate HIGH and prompts for color; NPN buffer means LOW won't light) led_passive_probe.py — passive probe by grounding LED pad and reading IDR for the dropped bit. Returned nothing — demo that a buffer-isolated topology hides the connection from the MCU side. pc11_red_test.py — verified PC11 (safety heartbeat) doesn't double as the red driver pc14_red_test.py — unlocks PWR/BDCR so PC14 is GPIO-addressable before driving; ruled out PC14 as red driver State→LED mapping in monitor_task: J1772 A/B → blue ON, green OFF (standby / waiting) J1772 C/D → blue OFF, green ON (charging) J1772 E/F → both OFF (fault — red would normally indicate) Bench-validated walk A→B→C→B→A: all transitions tracked correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: LCD link investigation — bench cmds 5–18 for UART/baud sweeps WIP bench tooling from the 2026-05-11 evening session digging into why the DGUS display doesn't react to commands from our M4 firmware despite being on the right physical UART. What we know for sure: - LCD is DWIN DMT48270C043_04WT, T5 chip, DGUS-II protocol (5A A5 … frames confirmed in stock-mcu-2026-05-07.bin) - Stock firmware inits USART2 at 115200 baud for the LCD link (disasm at 0x8008dd0–0x8008e2a). USART1=9600, USART3=115200, UART7=115200 (Nations base 0x40015400, not STM32F1 0x40004C00). - PA2 (USART2_TX) physically reaches the LCD board's RX pad. Verified with the spam test (cmd 15): voltage drops to ~1.65 V on both PA2 and the LCD-side RX pad while 0x55 streams. - The LCD board has only one UART pair wired (RX/TX); RX1/TX1 pads exist on the connector but aren't routed. - Firmware tight-loop sends DGUS frames back-to-back (no inter-byte gap from openocd round-trips). - BRR sweep through 20 baud × PCLK1 combos via the firmware-side loop produced NO visible LCD reaction (no dim, no page change). What we DON'T know: - Why the LCD ignores our DGUS frames. Top hypotheses: 1. LCD is in a stuck/wedged state from boot-time garbage we sent (our M4 nextion_init sends "page setting" as ASCII at 9600 baud, which the DGUS display would see as nonsense bytes; unlikely to wedge but possible). 2. The actual PCLK1 differs from SPL's belief (SystemCoreClock = 144 MHz, so SPL set BRR=0x0138 for 115200 — but our DWT CYCCNT measurement was contaminated by FreeRTOS WFI sleep and pointed at ~220–250 MHz HCLK, which doesn't match). 3. There's a hardware enable / RESETB line on the LCD that we haven't asserted. Some DWIN displays hold off UART processing until a specific GPIO state. 4. The LCD board has a buffer / level-shifter chip between the connector RX pad and the T5, and that chip isn't powered or enabled. Bench cmds in this commit: 5 = nextion_send_cmd("dims=10") [Nextion-style — wrong protocol] 6 = nextion_send_cmd("dims=100") 7..10 = nextion "page N" 11 = USART3/9600 on PB10 probe 12 = UART4 (wrong base — STM32F1 0x40004C00, not Nations) probe 13 = USART2/115200 probe via nextion_send_cmd 14 = USART3 partial-remap on PC10 probe 15 = spam 0x55 on USART2 for 10 s (used for multimeter trace test) 16 = DGUS dim=0/dim=100 via SPL USART_Init(115200), tight loop 17 = DGUS page0/page1 18 = DGUS dim=0/dim=100 with BRR from g_bench_arg (sweep helper) Mailbox layout shifted: g_bench_arg at 0x2000000c, g_bench_cmd at 0x20000010 (was 0x2000000c). All older bench scripts need to be updated to use 0x20000010 for cmd triggering. Tooling added: tools/baud_sweep.py — old USART2 baud sweep, openocd-only, inter-byte gap issue tools/dgus_test.py — direct openocd byte pokes (had gap issue) tools/dgus_brr_sweep.py — final firmware-side BRR sweep (clean) tools/uart_wiggle.py — 1 Hz wiggle of UART_TX candidates for multimeter-on-LCD-pad ID test Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: stock-fw restore tooling + DGUS cmd 19 (VP=0 init sequence) - tools/restore_stock_nexcyber.sh: 2-pass chunked flasher for the 128 KB stock dump (single-pass load_image overflows 80 KB SRAM) - openocd-n32g45x-flash.tcl: accept NX_FLASH_TARGET_OFFSET + NX_FLASH_NO_RESET_RUN env vars so multi-region flashes can share one TCL helper; default behaviour unchanged - main.c bench cmd 19: full DGUS-II init sequence extracted from stock V1.0.066 dump — VP=0x0000 system-register zero-writes before backlight/page commands; BRR override via g_bench_arg Bench-tested 2026-05-12: SHA-perfect stock restore, but stock fw on this bench unit no longer reaches its UI-init phase (hardware drift from prior M4 work — see project memory). DGUS cmd 19 transmits cleanly but LCD still unresponsive on this unit; saved for retry on a fresh unit per the docs path (CRC mode + RESET-pin hypotheses). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: cmd 20 — docs-driven DGUS test (System_Reset + bidirectional CRC blink) Built from DWIN T5L_DGUSII Application Development Guide v2.9 §4.1/§5.1: - System_Reset via VP=0x0004 (write magic 55AA 5AA5) — guaranteed LCD CPU hard reset. Frame format works in BOTH CRC modes: no-CRC LCDs see the trailing CRC bytes as data beyond the 4 reset bytes (which still trigger reset); CRC LCDs validate the trailer normally. Universal probe. - Backlight blink (VP=0x0082) sent first WITHOUT CRC trailer then WITH CRC trailer — backlight flicker pinpoints which mode the LCD's CFG (byte 0x05 bit 7) is configured for. - Page 0 (VP=0x0084) sent in both variants — content-change probe. CRC algorithm verified bit-exact against doc examples: MODBUS CRC-16 (poly 0xA001, init 0xFFFF) over CMD+VP+DATA, trailer sent LSB first. Doc example 5AA5 06 83 000F 01 ED90 reproduced from `83 00 0F 01` → 0x90ED → on-wire ED 90 ✓. Cmd 19 (stock-fw frame replay) is preserved for reference; cmd 20 supersedes it for LCD bring-up testing. Total sequence runs ~6 sec; trigger via: mww 0x2000000c 0; mww 0x20000010 20 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: host-side DGUS probe tool (USB-UART direct to LCD) Built per DWIN T5L_DGUSII Application Development Guide v2.9. Sweeps common DGUS baud rates and CRC modes via a Version Read at VP=0x000F (cmd 0x83); the LCD's reply is deterministic so a clean RX uniquely identifies (baud, crc-mode). Also offers --reset (universal hard reset via VP=0x0004) and --blink (visible backlight test). Bypasses all MCU/clock uncertainty — talks straight to the LCD over a USB-UART adapter. Wiring + 5V power notes in the module docstring. Use for next bench unit's LCD bring-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * nexcyber: dgus_listen.py — passive LCD UART listener for wire validation Companion to dgus_probe.py. Listens passively for any spontaneous DGUS traffic from the LCD (e.g. touch auto-upload frames if the LCD has a touch panel and CFG enables it). Useful as a wire-validation step when the active sweep returns silence: any received bytes prove the LCD-TX → USB-UART-RX path works, even if baud is wrong. Note: confirmed via dev-guide naming table that DMT48270C043_04WN's "N" suffix means no touch panel — listener will be silent on this specific model unless something else is sending. Kept for use on touch-equipped DGUS variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: multi-MCU board structure design spec Design for reconciling the nexcyber-port-skeleton island model into a real portability boundary: a boards/<board>/ config tree, src/hal/ split into a portable interface + per-chip impls, shared production main.c with Nexcyber's bring-up harness kept as a separate bench target. Six-step migration sequenced so both the rippleon and nexcyber builds stay green throughout and all bench-validated work is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: multi-MCU board structure implementation plan 13-task, 6-phase plan to reconcile the nexcyber-port-skeleton island model into the boards/<board>/ + src/hal/<chip>/ structure. Sequenced pure-moves-first with disassembly-diff regression gates so the GD32 production target and the Nexcyber bench harness stay byte-identical through the refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * cmake: extract bench feature flags into cmake/options.cmake All bench/debug option blocks moved verbatim from the rippleon branch of CMakeLists.txt into cmake/options.cmake; CMakeLists.txt now includes it once via include() in the shared tail, after OPENEVCHARGER_GIT_SHA. Hardware-fact defines (GD32F20X_CL, HXTAL_VALUE, GFCI_CAL_SELF_TEST, PE_CONTINUITY_DETECTOR) remain in the rippleon branch for Task 3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(plan): fix the disassembly regression-gate procedure The sed filter on macro names never worked — objdump -d output carries the disassembled SHA bytes, not the macro names. Replace with a characterize-the-diff GATE: pass iff every differing line is git build-info noise (ASCII-immediate loads / .word data near build_info). * cmake: split if/elseif into per-board board.cmake; adopt PCBA slugs Lifts the rippleon and nexcyber target bodies out of CMakeLists.txt into boards/rippleon-roc001/board.cmake and boards/nexcyber-zbu011k/board.cmake respectively, and replaces the if/elseif block with a single include(). Adopts the new slugs rippleon-roc001 / nexcyber-zbu011k throughout; the FATAL_ERROR guard now checks for the board.cmake file's existence rather than an explicit allowlist. Both firmware targets and host ctest pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(plan): GATE compares instructions only, not raw objdump * boards: relocate pin maps + linker scripts under boards/<board>/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * hal: move GD32-coupled implementations into src/hal/gd32f205/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * drivers: promote w25q SPI-NOR driver to src/drivers/ (chip-independent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * hal: relocate Nexcyber HAL to src/hal/n32g45x/, harness to bench target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * hal: reconcile Nexcyber shadow headers — share cp_pwm/bl0939, isolate divergent adc_scan/gfci/relay Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * hal/n32g45x: add OEVC_HAL_STUB macro + stubs for unported + divergent HAL Adds src/hal/oevc_hal_stub.h with OEVC_HAL_STUB() trap macro (cpsid i + infinite loop), then creates 11 stub .c files in src/hal/n32g45x/: - 8 unported-HAL stubs (rtc, flash, spi3, uart5, rfid, ws2812, wdg, adc_inject) implement every function from the shared src/hal/*.h surface - 3 divergent-peripheral stubs (adc_scan_shared_stub, gfci_shared_stub, relay_shared_stub) implement the SHARED hal interface for peripherals that already have real N32 drivers against board-specific *_nx.h APIs All 11 files pass -fsyntax-only. Existing builds and host tests unchanged (new files are inert until Task 10 wires them into the production target). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * main: extract board-init hooks; make src/main.c board-agnostic src/main.c had chip-specific bring-up inlined at three non-contiguous sites (vendor clock fix-up, debug-pin/AF remap, FC41D power sequence). Extract each behind a board hook — board_early_init / board_debug_pins_init / board_fc41d_release, declared in src/hal/board_init.h, implemented per chip in src/hal/<chip>/board_init.c (GD32 real, N32 stub). Hooks are 1:1 with the original call sites so main()'s call order is preserved. This is the board-hook half of the original Task 10. Wiring the Nexcyber production target is deferred — it is blocked on the shared src/ tree not being chip-clean (raw GD32 SPL in src/ui, src/tasks, src/persist), which a follow-up task addresses first. * docs(plan): split Task 10, insert Task 11 (chip-clean core) + Task 12 (production target) * hal: chip-clean the shared core — GPIO-bit + reset-cause HAL Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * boards/nexcyber: reconcile pin_map.h types + macros; wire production target Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * repo: tighten .gitignore, drop tracked build artifacts + stale vendored OCPP copy Widen the ESPHome build-cache ignore from fc41d/.esphome/ to repo-wide .esphome/ and remove the now-redundant fc41d-scoped line. Step 1 audit found no tracked build artifacts or vendored external_components to un-track (the only regex hit, src/proto/build_info.h, is legitimate source — the substring "build_" in its name is coincidental). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: update firmware matrix for PCBA slugs; nexcyber production target continue-on-error Rename rippleon→rippleon-roc001 and nexcyber→nexcyber-zbu011k to match the board slugs adopted in Task 3. Merge Configure+Build into one step with per-step continue-on-error so a nexcyber link failure doesn't block the size/upload steps unnecessarily. Add bringup artifact paths for nexcyber-zbu011k and switch if-no-files-found to warn (bringup files absent on rippleon-roc001; the job-level continue-on-error already gates PR status). host-tests.yml and fc41d-config.yml are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: describe the as-built multi-MCU board structure Rewrites the stale structural/build-instruction sections in BOARDS.md, README.md, and boards/nexcyber-zbu011k/README.md to match the refactored layout (boards/<board>/ + src/hal/<chip>/ + src/drivers/), updates build commands to the new slugs and build/<board>/ convention, and creates boards/rippleon-roc001/README.md. All hardware/FCC/OEM/safety/roadmap content preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * boards/nexcyber: define PE_CONTINUITY_DETECTOR=0 (final-review fixes) Final holistic review flagged two trivial items: - the nexcyber production target was missing the OPENEVCHARGER_PE_CONTINUITY_DETECTOR board-fact define, producing a -Wundef warning on safety_task.c (spec §2 says board-fact defines live in board.cmake) — added =0 (no PE-continuity sense pin on this PCB); - stale pre-rename 'nexcyber' slug in a toolchain-file comment — corrected to nexcyber-zbu011k. Nexcyber build is now warning-free; both ELFs link. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two unrelated commits, fine to merge together:
1.
cf41e7e— fix stepped cyan breathing at low charge ratiosUser report: "pulsing cyan while charging is sometimes dim and has an off stepped breathing."
Root cause:
EVSE_CHARGINGpre-multiplies the linear breathe envelope byratio_pct(active/advertised current) before callingfill_all, which then runsscaled_gamma. That order re-introduces the precision-loss thescaled_gammacomment at the top of the file specifically warns against: scale-before-gamma at low ratios squashes the input range so hard that gamma can't recover smooth dynamic range.Numerical proof at
ratio_pct=21,brightness_pct=100:Old gives ~9 distinct output levels across the breathe (stepped). New gives ~41 (smooth). +84 bytes flash.
2.
289f298— nexcyber feasibility prereq closureTest plan
🤖 Generated with Claude Code