feat: S12 — vcucmd 3-state command from VCU (replaces binary vcustop)

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) <noreply@anthropic.com>
This commit is contained in:
Bastian de Byl
2026-05-18 18:06:19 -04:00
parent d75027b17c
commit aae23f53a6
3 changed files with 97 additions and 20 deletions
+53 -11
View File
@@ -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;
}