From d75027b17cea39e00baa5b59431e21b31d31300b Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 18 May 2026 15:39:36 -0400 Subject: [PATCH] feat: S11 - explicit VCU stop command on 0x212 byte 3 (replaces udclim hack) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -S10 udclim=200V mechanism was a hack — VCU lowered udclim below pack to fire CheckVoltage() udc>udclim and force STOP. It didn't work reliably because once VCU opens HV contactors, the OBC per-module voltage probes (c1udc/c2udc/c3udc) collapse below 200V and udc = MAX(c1,c2,c3) never accumulates 10 ticks above udclim. Bench symptom: VCU reaches CHARGE_COMPLETE, opens contactors, but teslacharger stays in EVSEACTIVATE, OBC modules keep drawing ~0.5kW AC, water pumps run, CP signal stays charging, EVSE doesn't shut down. S11 adds an explicit vcustop byte on 0x212 byte 3 — VCU sets it to 1 when it wants charging stopped. ChargerStateMachine checks vcustop FIRST in the EVSEACTIVATE case and transitions to STOP immediately. Mirrors how the Dilong OBC obeys OBC_ControlCMD = Stopped. Wire format (additive, no breaking change to S10): Byte 0: vcuchglim_pct (existing) Bytes 1-2: udclim_V (existing, DEPRECATED — VCU sends 398V no-op) Byte 3: vcustop (NEW, 0=normal / 1=force stop) Bytes 4-7: reserved Defensive: Param::Change(udclim) now clamps received <50V values to the 398V flash default. Protects against a pre-PR47 VCU sending all-zeros in bytes 1-2 (which would otherwise make CheckVoltage() fire constantly and brick charging). After this VCU also flashed: the WPUMP issue resolves naturally because VCU_IsCharging() reads OIOBC.State < ACTIVATE once teslacharger goes to STOP. The VCU/teslacharger ownership becomes symmetric with the Dilong path — VCU is authoritative for "should we charge." Pool: 43 → 44 / 50 slots used. #patch Co-Authored-By: Claude Opus 4.7 (1M context) --- include/param_prj.h | 21 ++++++++++++++------- src/charger.cpp | 19 ++++++++++++++++--- src/chargercan.cpp | 34 +++++++++++++++++++--------------- src/main.cpp | 12 ++++++++++++ 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index cd565e6..bb59a14 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -47,7 +47,7 @@ 3. Display values */ //Next param id (increase when adding new parameter!): 24 -//Next value Id: 2052 +//Next value Id: 2053 /* category name unit min max default id */ #define PARAM_LIST \ PARAM_ENTRY(CAT_CHARGER, idclim, "A", 0, 45, 45, 3 ) \ @@ -109,6 +109,7 @@ VALUE_ENTRY(c3udc, "V", 2035 ) \ VALUE_ENTRY(c3idc, "A", 2036 ) \ VALUE_ENTRY(tmpobcmax, "°C", 2051 ) \ + VALUE_ENTRY(vcustop, OFFON, 2052 ) \ VALUE_ENTRY(test_time, "s", 3000 ) \ VALUE_ENTRY(test_timer_flag, "X", 3001 ) \ VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \ @@ -139,12 +140,18 @@ // -S9 restores 8 c2/c3 RX entries (uac/flag/idc/udc per module). -S8 over- // trimmed multi-module Tesla OBC support: CalcTotals was summing only // module 1's contribution, so reported pack power was 1/3 of actual delivery. -// -S10 adds udclim RX on 0x212 bytes 1-2. With this, the VCU can force a -// hard stop by sending udclim=200V (below pack), which fires the existing -// ChargerStateMachine CheckVoltage() exit (udc > udclim → STOP). Closes the -// gap where Fix B's contactor-open left the OBC modules drawing ~0.55 kW AC -// indefinitely because EVSEACTIVATE had no other exit path. -#define VERSTR STRINGIFY(4=VER-S10) +// -S10 added udclim RX on 0x212 bytes 1-2. The VCU sent udclim=200V to force +// CheckVoltage() to fire (udc > udclim → STOP). DEPRECATED in -S11: the +// per-module DC voltage readings (c1udc/c2udc/c3udc) collapse below 200V +// once VCU opens HV contactors, so the trigger never accumulates 10 ticks. +// udclim RX is kept mapped but the VCU now always sends 398V (no-op). +// +// -S11 adds vcustop RX on 0x212 byte 3 — an explicit VCU command lever that +// mirrors the Dilong path's OBC_ControlCMD. When VCU enters CHARGE_COMPLETE +// it sets vcustop=1; ChargerStateMachine's EVSEACTIVATE case transitions to +// STOP immediately, the J1772 CP signal drops, EVSE stops delivering AC, +// VCU_IsCharging() returns 0, water pumps power down naturally. +#define VERSTR STRINGIFY(4=VER-S11) /***** enums ******/ diff --git a/src/charger.cpp b/src/charger.cpp index b3cd21d..bf676c1 100644 --- a/src/charger.cpp +++ b/src/charger.cpp @@ -353,15 +353,28 @@ void ChargerStateMachine() DigIo::evseact_out.Set(); DigIo::acpres_out.Set(); - if (CheckVoltage() || CheckTimeout()) + // SKUDAK-S11: explicit VCU stop command takes priority over every + // other exit condition. When the VCU's BMS-side gate determines the + // charge session is complete (or the user lowered ChargeLimit below + // current SOC), it sets vcustop=1 on 0x212 byte 3. We obey + // immediately — drop AC pres + EVSE-active GPIOs and transition to + // STOP. Mirrors how the Dilong path's OBC_ControlCMD = Stopped works. + // Replaces the brittle udclim=200V mechanism from -S10. + if (Param::GetInt(Param::vcustop)) + { + DigIo::acpres_out.Clear(); + DigIo::evseact_out.Clear(); state = STOP; - if (CheckUnplugged()) + } + else if (CheckVoltage() || CheckTimeout()) + state = STOP; + else if (CheckUnplugged()) { DigIo::acpres_out.Clear(); DigIo::evseact_out.Clear(); state = OFF; } - if (CheckChargerFaults()) + else if (CheckChargerFaults()) { DigIo::acpres_out.Clear(); state = OFF; diff --git a/src/chargercan.cpp b/src/chargercan.cpp index bb90cb4..884765c 100644 --- a/src/chargercan.cpp +++ b/src/chargercan.cpp @@ -100,24 +100,28 @@ void ChargerCAN::MapMessages(CanMap* can) can->AddSend(Param::aclim, 0x44c, 16, 16, 1500); can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100); - /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit + S10 stop *****/ + /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 chglim + S10 udclim + S11 stop *****/ // Byte 0: ChargeLimit_pct (0-100). Param::Change(vcuchglim) translates // % to udcspnt setpoint (see src/main.cpp). - // Bytes 1-2: udclim_V (uint16 LE, 50-420 V). SKUDAK-S10: VCU drives this - // dynamically so it can force-stop the OBC modules when its - // BMS-side SOC gate trips. Normally 398 V (= default udclim, no - // effect). On the VCU's CHARGE_COMPLETE state it drops to 200 V, - // which fires the existing ChargerStateMachine CheckVoltage() - // path (udc > udclim → STOP within 1 s). Pre-S10 firmware - // ignored these bytes; safe upgrade. NOTE: udclim updates here - // are RAM-only — `save` command WHILE the VCU is forcing 200 - // would persist that value and brick subsequent charging. Don't - // issue `save` mid-stop. - // Bytes 3-7: reserved 0x00. + // Bytes 1-2: udclim_V (uint16 LE). DEPRECATED in -S11 but mapping kept for + // backward compat with -S10 VCU TX. New VCU always sends 398V + // (= the flash default), so this RX is a no-op. Old -S10 trick + // (VCU sends 200V to force CheckVoltage() STOP) was unreliable + // because c1udc/c2udc/c3udc collapse < 200V once VCU opens HV + // contactors, never accumulating 10 ticks > udclim. Defensive + // clamp in Param::Change(udclim) ignores values < 50 V (would + // otherwise turn an old-VCU bench into a never-charges brick). + // Byte 3: vcustop (uint8, NEW in -S11). 0 = no override / normal state + // machine. 1 = VCU commands STOP. Checked at the top of + // EVSEACTIVATE in ChargerStateMachine; transition to STOP fires + // within one 100 ms tick. Mirrors how the Dilong path uses + // OBC_ControlCMD = Stopped to authoritatively halt charging. + // Bytes 4-7: reserved 0x00. // VCU broadcasts every 200 ms once teslacharger detected; charger holds - // last received values if VCU goes silent (held udclim defaults to 398 V). - can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1); - can->AddRecv(Param::udclim, 0x212, 8, 16, 1); + // last received values if VCU goes silent. + can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1); + can->AddRecv(Param::udclim, 0x212, 8, 16, 1); + can->AddRecv(Param::vcustop, 0x212, 24, 8, 1); /***** SKUDAK VCU telemetry (0x211) — SKUDAK-448 expanded charger telemetry *****/ // Byte 0: state (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop) diff --git a/src/main.cpp b/src/main.cpp index 1f6ff69..c393ad9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -161,6 +161,18 @@ void Param::Change(Param::PARAM_NUM paramNum) spnt = MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim)); dcCurController.SetRef(spnt); break; + case Param::udclim: + // SKUDAK-S11: defensive clamp. udclim is now reverse-mapped from CAN + // (chargercan.cpp), and a pre-PR47 VCU sends 0 in 0x212 bytes 1-2. + // udclim=0 would make CheckVoltage() fire on every tick (udc > 0) + // and prevent any charging. If we see a value below the documented + // min (50V), restore the safe default. Trivial cost — fires only on + // CAN RX of udclim (200 ms cadence) and only ever does work when + // the value is bogus. + if (Param::GetInt(Param::udclim) < 50) { + Param::SetInt(Param::udclim, 398); + } + break; case Param::vcuchglim: { // SKUDAK-516: VCU sends user-set ChargeLimit_pct on CAN 0x212. Translate