From aae23f53a6477ee211a4160a2afed2381a091b31 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Mon, 18 May 2026 18:06:19 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20S12=20=E2=80=94=20vcucmd=203-state=20co?= =?UTF-8?q?mmand=20from=20VCU=20(replaces=20binary=20vcustop)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote 0x212 byte 3 from S11's binary vcustop to the full Dilong-style OBC_ControlCMD vocabulary: 0 = Charging — walk to / hold at EVSEACTIVATE, produce current 1 = Stopped — sit idle in OFF/WAITSTART/ENABLE, no current 2 = Complete — drop to STOP (cycle terminated by VCU) VCU is now authoritative for the charge-cycle lifecycle. The charger keeps hardware-level wisdom (EVSE pilot via CheckStartCondition, plug-out safety via CheckUnplugged, module-fault detection via CheckChargerFaults) but obeys vcucmd for "should I be charging?". FSM changes: - ENABLE→ACTIVATE now gated on vcucmd==Charging (was unconditional). ENABLE on vcucmd==Complete drops to STOP; vcucmd==Stopped holds HV-armed waiting for next Charging command. - EVSEACTIVATE self-decided exits (CheckVoltage, CheckTimeout) are gone. Those were band-aids for the absence of an authoritative VCU stop signal. vcucmd==Stopped now parks back in ENABLE (HV held); vcucmd==Complete drops to STOP. CheckUnplugged + CheckChargerFaults still own hardware-safety exits. - STOP→OFF now also triggers on vcucmd != Complete (was unplug-only). This is the fix for the "raise ChargeLimit mid-COMPLETE doesn't restart charging" wedge — the VCU's vcucmd flip from Complete to Stopped on a limit-raise lets the natural OFF→WAITSTART→ENABLE→ ACTIVATE→EVSEACTIVATE flow resume charging without an unplug cycle. Param rename vcustop→vcucmd. External tooling that reads/writes the S11 vcustop param via the openinverter web UI will see the new vcucmd param with 3-state semantics — single-user bench, acceptable. VERSTR bumped to S12. Co-Authored-By: Claude Opus 4.7 (1M context) --- include/param_prj.h | 33 +++++++++++++++++++++-- src/charger.cpp | 64 +++++++++++++++++++++++++++++++++++++-------- src/chargercan.cpp | 20 +++++++++----- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/include/param_prj.h b/include/param_prj.h index bb59a14..597ea1d 100644 --- a/include/param_prj.h +++ b/include/param_prj.h @@ -109,7 +109,7 @@ VALUE_ENTRY(c3udc, "V", 2035 ) \ VALUE_ENTRY(c3idc, "A", 2036 ) \ VALUE_ENTRY(tmpobcmax, "°C", 2051 ) \ - VALUE_ENTRY(vcustop, OFFON, 2052 ) \ + VALUE_ENTRY(vcucmd, VCUCMDS, 2052 ) \ VALUE_ENTRY(test_time, "s", 3000 ) \ VALUE_ENTRY(test_timer_flag, "X", 3001 ) \ VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \ @@ -119,6 +119,12 @@ #define OPMODES "0=Off, 1=Run" #define CHARGERS "1=Charger1, 2=Charger2, 4=Charger3" #define OFFON "0=Off, 1=On" +// SKUDAK-S12: VCU→charger control command on 0x212 byte 3. Mirrors the Dilong +// path's OBC_ControlCMD vocabulary exactly so VCU code paths can be reused. +// 0 = Charging — VCU wants current to flow; walk to / hold at EVSEACTIVATE +// 1 = Stopped — VCU is not driving a cycle; sit in OFF/WAITSTART/ENABLE +// 2 = Complete — VCU has terminated the cycle; drop to STOP +#define VCUCMDS "0=Charging, 1=Stopped, 2=Complete" #define CHFLAGS "0=None, 1=Enabled, 2=Fault, 4=CheckAlive" #define STATES "0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop" #define INPUTS "0=Type2, 1=Type2-3P, 2=Type1, 3=Manual, 4=Manual-3P, 5=Type2-Auto" @@ -151,7 +157,18 @@ // 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) +// -S12 promotes byte 3 from a binary vcustop flag to the full 3-state +// VCUCMDS vocabulary (0=Charging, 1=Stopped, 2=Complete), matching the +// Dilong OBC_ControlCMD exactly. The internal FSM now obeys vcucmd for +// lifecycle transitions: ENABLE only advances to ACTIVATE on Charging, STOP +// returns to OFF when vcucmd flips back to Charging/Stopped (fixes the +// "raise ChargeLimit mid-COMPLETE doesn't resume" wedge — teslacharger was +// stuck in STOP because STOP only exited on CheckUnplugged). The self- +// decided EVSEACTIVATE exits (CheckVoltage, CheckTimeout) are stripped; the +// VCU is now authoritative for when charging starts and stops. The charger +// retains hardware-level wisdom: CheckUnplugged() still forces OFF from any +// state, and CheckStartCondition() still gates OFF→WAITSTART on EVSE pilot. +#define VERSTR STRINGIFY(4=VER-S12) /***** enums ******/ @@ -175,6 +192,18 @@ enum states STOP }; +// SKUDAK-S12: VCU→charger command vocabulary on 0x212 byte 3. Must match +// the values exposed via VCUCMDS enum string AND the VCU's +// VCU_OIOBCCommand_t enum (stm32-hal-vcu Core/Inc/applications/charger/ +// charger_types.h). Updating one without the others silently breaks the +// charge state machine, so keep them in lock-step. +enum vcucmds +{ + VCUCMD_CHARGING = 0, + VCUCMD_STOPPED = 1, + VCUCMD_COMPLETE = 2 +}; + enum _canspeeds { CAN_PERIOD_100MS = 0, diff --git a/src/charger.cpp b/src/charger.cpp index bf676c1..2dc9f34 100644 --- a/src/charger.cpp +++ b/src/charger.cpp @@ -299,10 +299,26 @@ void CalcAcCurrentLimit() Param::SetFloat(Param::aclim, iacLim); } +// SKUDAK-S12: VCU is authoritative for charge-cycle lifecycle. The charger +// keeps hardware-level wisdom (EVSE pilot detection, plug-out safety, +// fault detection, current ramping) but obeys vcucmd for "should I be +// charging right now?" — exactly mirroring the Dilong OBC_ControlCMD +// pattern so the VCU's two charger paths can share code/state. +// +// VCUCMDS vocabulary (see include/param_prj.h): +// 0 Charging — VCU wants current; walk to / hold at EVSEACTIVATE +// 1 Stopped — VCU has no active cycle; sit idle (no current) +// 2 Complete — VCU has terminated the cycle; drop to STOP +// +// Self-decided exits from EVSEACTIVATE (CheckVoltage, CheckTimeout) are +// gone — those were band-aids for the absence of an authoritative VCU +// stop signal. CheckUnplugged() still owns hardware safety from every +// state; CheckChargerFaults() still bails on real module faults. void ChargerStateMachine() { static states state = OFF; int configuredChargers = Param::GetInt(Param::chargerena); + int vcucmd = Param::GetInt(Param::vcucmd); if (!Param::GetBool(Param::enable)) { @@ -316,6 +332,9 @@ void ChargerStateMachine() Param::SetInt(Param::opmode, 0); DisableAll(); + // OFF→WAITSTART requires hardware-level start condition (EVSE pilot + // or non-EVSE input). vcucmd is checked downstream — we still walk + // up to ENABLE on plug-in so the modules are ready when VCU says go. if (CheckStartCondition()) { startTime = rtc_get_counter_val(); @@ -334,7 +353,15 @@ void ChargerStateMachine() DigIo::ch2ena_out.Set(); if (configuredChargers & 4) DigIo::ch3ena_out.Set(); - state = ACTIVATE; + + // Hold here until VCU says go (vcucmd=Charging) or terminates + // (vcucmd=Complete). Stopped keeps us armed but idle. + if (vcucmd == VCUCMD_COMPLETE) + state = STOP; + else if (vcucmd == VCUCMD_CHARGING) + state = ACTIVATE; + else if (CheckUnplugged()) + state = OFF; break; case ACTIVATE: Param::SetInt(Param::opmode, 1); @@ -353,21 +380,28 @@ void ChargerStateMachine() DigIo::evseact_out.Set(); DigIo::acpres_out.Set(); - // 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)) + // VCU command is the primary exit. CheckUnplugged + CheckChargerFaults + // remain for hardware safety; the self-decided CheckVoltage/CheckTimeout + // exits are gone (S12 — VCU is authoritative). + if (vcucmd == VCUCMD_COMPLETE) { DigIo::acpres_out.Clear(); DigIo::evseact_out.Clear(); state = STOP; } - else if (CheckVoltage() || CheckTimeout()) - state = STOP; + else if (vcucmd == VCUCMD_STOPPED) + { + // VCU revoked the charge command (e.g. user lowered limit below + // current SOC but BMS isn't full yet). Park HV-armed in ENABLE + // ready to resume on the next Charging command. + DigIo::ch1act_out.Clear(); + DigIo::ch2act_out.Clear(); + DigIo::ch3act_out.Clear(); + DigIo::acpres_out.Clear(); + DigIo::evseact_out.Clear(); + Param::SetInt(Param::opmode, 0); + state = ENABLE; + } else if (CheckUnplugged()) { DigIo::acpres_out.Clear(); @@ -386,6 +420,14 @@ void ChargerStateMachine() if (CheckUnplugged()) state = OFF; + // S12 fix for the "raise ChargeLimit mid-COMPLETE wedges in STOP" + // bug: when VCU clears Complete (latch released because user raised + // limit above current SOC), drop to OFF so the natural OFF→WAITSTART + // →ENABLE→ACTIVATE→EVSEACTIVATE flow restarts charging next tick. + // Previously STOP only exited on physical unplug — wedge on every + // limit-raise. + else if (vcucmd != VCUCMD_COMPLETE) + state = OFF; break; } diff --git a/src/chargercan.cpp b/src/chargercan.cpp index 884765c..5264c1d 100644 --- a/src/chargercan.cpp +++ b/src/chargercan.cpp @@ -100,7 +100,7 @@ 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 chglim + S10 udclim + S11 stop *****/ + /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 chglim + S10 udclim + S12 vcucmd *****/ // Byte 0: ChargeLimit_pct (0-100). Param::Change(vcuchglim) translates // % to udcspnt setpoint (see src/main.cpp). // Bytes 1-2: udclim_V (uint16 LE). DEPRECATED in -S11 but mapping kept for @@ -111,17 +111,23 @@ void ChargerCAN::MapMessages(CanMap* can) // 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. + // Byte 3: vcucmd (uint8, S12 — was binary vcustop in S11). Three states + // mirror the Dilong OBC_ControlCMD vocabulary 1:1 so VCU code + // paths can be reused across charger families: + // 0 = Charging — walk to / hold at EVSEACTIVATE, produce current + // 1 = Stopped — sit idle in OFF/WAITSTART/ENABLE, no current + // 2 = Complete — VCU declares cycle done; drop to STOP + // VCU is now authoritative for lifecycle. Self-decided + // EVSEACTIVATE exits (CheckVoltage, CheckTimeout) are gone; + // STOP also exits to OFF when vcucmd flips back from Complete + // to Stopped/Charging so the natural restart path runs (fixes + // the "raise limit mid-COMPLETE doesn't resume" wedge in S11). // Bytes 4-7: reserved 0x00. // VCU broadcasts every 200 ms once teslacharger detected; charger holds // 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); + can->AddRecv(Param::vcucmd, 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)