3 Commits

Author SHA1 Message Date
Bastian de Byl a30cded074 fix: restore PI reference on final ramp tick (iteration O regression)
The iteration O 3-tick ramp ended with dcCurController.SetRef(0) before
transitioning to STOP. Param::Change only fires when idcspnt or idclim
parameter values change — a normal STOP → OFF → WAITSTART → ENABLE →
ACTIVATE → RUN → EVSEACTIVATE restart cycle never touches those, so
nothing restored the PI reference. On the next entry to EVSEACTIVATE
the controller targeted 0 A and the charger sipped ~0.1 kW.

Bench-confirmed 2026-05-21 on iteration O firmware: after lowering the
ChargeLimit_pct to drop into COMPLETE and then raising it back, charging
restarted at 0.1 kW instead of ramping to 9 kW.

Fix: on the final ramp tick, restore SetRef to MIN(idcspnt, idclim)
instead of zeroing it. The GPIO clears (acpres_out, evseact_out) still
cut AC enable so the output current decays through the filter caps;
the reference value is irrelevant during STOP but matters for the next
EVSEACTIVATE entry.

The ramp-restore block in the vcucmd != Complete branch (charger.cpp:431
in iteration O) only handles the mid-ramp reversal case (counter ≠ 0);
it didn't help when the ramp completed cleanly. This commit closes that
gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:23:47 -04:00
Bastian de Byl 0514940877 feat: ramp DC current to zero over 3 ticks before RUN → STOP
When vcucmd transitions to COMPLETE while in EVSEACTIVATE the previous
behavior was to clear acpres_out / evseact_out and transition to STOP
in a single 100 ms tick — cutting the OBC AC enable immediately. The
VCU's HV contactor opens at roughly the same moment; under active 9 kW
charging (~23 A) this hard disconnect transient floods the Tesla BMS
state-change CAN path and starves the VCU's task scheduler past the
IWDG window. Bench-confirmed 2026-05-21 as the trigger for the
iteration N limit-lower 0x002A reset loop.

Add a 3-tick (300 ms) ramp inside EVSEACTIVATE before the STOP
transition: stash the original DC-current reference, scale it 2/3 →
1/3 → 0 across the remaining ticks, and only then clear the AC enable
GPIOs and go STOP. The PI controller's natural response walks the AC
current limit down so output current decays smoothly rather than
hard-stopping.

If vcucmd reverts to Charging mid-ramp (user raised limit again),
restore the original reference cleanly before the existing branches
take over.

The VCU also defers its own HV-contactor open by 1500 ms (see VCU
iteration O commit 5781f47). Belt-and-suspenders: either fix alone
substantially reduces the disconnect transient; together they make
RUN → STOP graceful on any controller that interoperates with this
charger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:33:29 -04:00
Bastian de Byl aae23f53a6 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>
2026-05-18 18:06:19 -04:00
3 changed files with 151 additions and 21 deletions
+31 -2
View File
@@ -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,
+105 -10
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();
// 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,81 @@ 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).
//
// SKUDAK: ramp the DC-current reference toward zero across 3 ticks
// (300 ms) before transitioning to STOP. Without this, the VCU
// opening its HV contactor at the moment we go STOP triggers a hard
// 23 A → 0 disconnect transient that floods the Tesla BMS state-
// change CAN path and starves the VCU's task scheduler past its
// IWDG window (bench-confirmed 2026-05-21 limit-lower 0x002A loop).
// The VCU also defers its contactor open by ~1500 ms; together
// they make the disconnect graceful even on a hard limit-lower
// mid-RUN. completeRampTicks counts DOWN from 3 → 0; the original
// reference is stashed so if vcucmd reverts to Charging mid-ramp
// we restore cleanly.
static uint8_t completeRampTicks = 0;
static s32fp completeRampOriginalRef = 0;
if (vcucmd == VCUCMD_COMPLETE)
{
if (completeRampTicks == 0)
{
completeRampOriginalRef = MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim));
completeRampTicks = 3;
}
if (completeRampTicks > 1)
{
// Scale 2/3 → 1/3 → 0 across remaining ticks.
s32fp scaled = (completeRampOriginalRef * (completeRampTicks - 1)) / 3;
dcCurController.SetRef(scaled);
completeRampTicks--;
// Stay in EVSEACTIVATE this tick; current is decaying.
}
else
{
// Final ramp tick: clear GPIOs (cuts AC enable; output decays
// through filter caps) and restore the PI reference to the
// current idcspnt/idclim so the NEXT EVSEACTIVATE entry
// doesn't sip at 0 A. Param::Change only fires on parameter
// changes; a normal restart cycle never touches idcspnt or
// idclim, so without this restore the PI controller stays
// pinned at 0 across the cycle. Iteration O bug — bench-
// confirmed 0.1 kW sip on limit-raise from COMPLETE.
DigIo::acpres_out.Clear();
DigIo::evseact_out.Clear();
dcCurController.SetRef(MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim)));
completeRampTicks = 0;
completeRampOriginalRef = 0;
state = STOP;
}
else if (CheckVoltage() || CheckTimeout())
state = STOP;
break;
}
// vcucmd not Complete — if we were mid-ramp the user reversed
// (raised limit again); restore reference before the existing
// branches take over.
if (completeRampTicks != 0)
{
dcCurController.SetRef(completeRampOriginalRef);
completeRampTicks = 0;
completeRampOriginalRef = 0;
}
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 +473,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;
}
+13 -7
View File
@@ -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)