7 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
Bastian de Byl d75027b17c feat: S11 - explicit VCU stop command on 0x212 byte 3 (replaces udclim hack)
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 58s
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) <noreply@anthropic.com>
2026-05-18 15:39:36 -04:00
Bastian de Byl 4c1f2e1991 feat: S10 - udclim RX on 0x212 bytes 1-2 for VCU-driven force-stop
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 58s
Fix B on the VCU side (b24864e) opens HV contactors when BMS reports
cell voltage above the user's ChargeLimit_pct target. That isolates
the pack but doesn't tell the OBC modules to stop drawing AC. Result:
~0.55 kW continuous EVSE draw dissipated as conversion loss in the OBC,
water pumps stay engaged, app reports "Charging <1 kW" indefinitely.

Root cause: ChargerStateMachine's EVSEACTIVATE state has only four
exits (CheckVoltage udc>udclim, CheckTimeout, CheckUnplugged,
CheckChargerFaults). With contactors open the VCU can't drive pack
voltage above udclim, so none of the exits fire — state machine wedges.

Fix: make udclim CAN-driven on 0x212 bytes 1-2 (uint16 LE, range 50-420V).
The VCU normally sends 398 V (no change in behavior). When VCU is in
CHARGE_COMPLETE state it drops to 200 V — well below any realistic
pack voltage — which fires the existing CheckVoltage() path within
1 second (10 × 100 ms ticks) and transitions EVSEACTIVATE → STOP → OFF.
OBC modules disable, AC draw drops to zero.

Elegant because we reuse the existing CheckVoltage() logic instead of
adding new state-machine code, new params, or new exit paths. Pre-S10
firmware silently ignored bytes 1-2; pre-S10 VCU with S10 teslacharger
leaves udclim at its persistent flash default (398V). Safe upgrade in
both directions.

Pool: 42 → 43 / 50 slots used, still comfortable headroom.

Pairs with stm32-hal-vcu PR #47's matching dynamic-udclim TX commit.

CAUTION: udclim updates here are RAM-only; running `save` while VCU
is forcing 200V would persist that value and require canclear-and-
rebuild to recover. Documented in chargercan.cpp comment block.

#patch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:39:13 -04:00
Bastian de Byl c905e1236b feat: S8/S9 - trim chargercan.cpp + restore c2/c3 RX for multi-module Tesla OBC
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
S8 trimmed the 73-entry chargercan.cpp down to 37 entries to fit
libopeninv's 50-slot CANPOS pool — needed because the upstream firmware
silently overflowed the pool and dropped the SKUDAK 0x211 broadcast at
the tail. Without that broadcast the VCU couldn't see idc/udc/tmpobcmax.

S9 restores 8 of the entries S8 dropped: c2/c3 uac, flag, idc, udc.
These turn out to be NECESSARY for 3-module Tesla OBC operation:

- CalcTotals (charger.cpp:41/48) sums c1+c2+c3 idc and takes the max
  udc. Without c2/c3 idc mapped, idc reports only module 1's
  contribution. Bench observed 2.7 kW reported vs 9.7 kW actual EVSE
  draw — exactly the 1/3 ratio expected.
- CheckChargerFaults (charger.cpp:226-261) gates active2/active3
  detection on c2uac/c3uac > 70V. Without those mapped, the active
  bits short-circuit to false — which masked the c2flag/c3flag
  CheckAlive timeout. We were getting away with the trim by accident.

Kept dropped (still need pool headroom for future):
- c2/c3 iac (AC current — c1iac × 3 is a fine proxy, not used in
  aggregation anywhere)
- c2/c3 stt (state — all modules report same value for healthy operation)
- c2/c3 tmp1/2/in (temps — tmpobcmax from c1's 3 probes is sufficient;
  full 9-probe worst-case is a nice-to-have)
- CHAdeMO RX/TX (0x102/0x108/0x109) — Polarity VCU doesn't use CHAdeMO
- 0x368 idle frame — cosmetic only
- hwaclim duplicates on 0x209/0x20B — all modules report identical limits

Pool math: 42 / 50 slots used, 8-slot headroom for future additions.

VERSTR S8 → S9 so any bench reporting -S8 in the openinverter UI is
visibly the regressed build and needs canclear + reflash.

After flash: user must issue `canclear` from the openinverter Commands
tab so the persisted map gets rebuilt from the new chargercan.cpp.
Power-cycle, verify `can p` shows the 8 new c2/c3 lines, then EVSE
test should report ~9.5 kW in the app (was 2.7 kW).

#patch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:07:19 -04:00
Bastian de Byl 23716a260b fix: unbrick boot — revert -S6 Clear(), add reentrancy guard, S7
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 1m1s
PR #8 (-S6) caused chip boot fault: MapChargerMessages() uncommented
canMap->Clear(). Clear() ends with canHardware->ClearUserMessages(),
which fires the HandleClear callback (main.cpp:170-173) — and
HandleClear calls MapChargerMessages(). Recursive chain:

  main() -> MapChargerMessages
         -> Clear() -> ClearUserMessages -> HandleClear
                                         -> MapChargerMessages (re-entry)
                                            -> Clear() (recurse...)
                                            -> ... stack overflow -> HardFault

Bench symptom (Kyle): ESP8266 web UI stops responding after flashing
-S6. Recovery required re-flashing an older firmware (3-5 attempts).

This commit:
1. Re-commented canMap->Clear() inside MapChargerMessages — the safe
   path. The upgrade rebuild now requires a manual "canclear" via the
   openinverter terminal/web UI, which fires HandleClear once and
   triggers an additive MapMessages + Save.
2. Added a static `in_progress` reentrancy guard so any future
   accidental Clear() inside this function or its callees turns into
   a no-op re-entry instead of a chip-bricking fault.
3. Kept the canary as Param::tmpobcmax (from PR #8) — useful as a
   schema marker, just no longer auto-triggers a destructive rebuild.
4. Bumped VERSTR S5 -> S7 (skips S6 so any bench reporting "S6" in
   the openinverter UI is visibly the broken build and gets flashed).

Note: this does NOT solve the underlying "73 entries trying to fit in
50-slot CANPOS pool" issue — chargercan.cpp still has more AddRecv +
AddSend calls than MAX_ITEMS. Step 2 (reduce entries) is a separate
follow-up PR.

#patch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:30:56 -04:00
4 changed files with 276 additions and 62 deletions
+53 -2
View File
@@ -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(vcucmd, VCUCMDS, 2052 ) \
VALUE_ENTRY(test_time, "s", 3000 ) \
VALUE_ENTRY(test_timer_flag, "X", 3001 ) \
VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \
@@ -118,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"
@@ -129,7 +136,39 @@
// SKUDAK customization suffix: bump on every Skudak-side change so the OpenInverter
// web UI shows a distinct version (e.g. "4=1.20.R-S4") and we can visually confirm
// the right firmware is flashed. Match the stm32-sine -S<N> convention.
#define VERSTR STRINGIFY(4=VER-S5)
// -S6 was withdrawn (PR #8 closed): the canary-rebuild attempt called
// canMap->Clear() inside MapChargerMessages, which recursed through the
// HandleClear callback and stack-overflowed at boot before the terminal
// came up. Jumping straight to -S7 so any bench that still reports -S6
// in the openinverter UI is visibly the broken build and gets re-flashed.
// -S8 trims chargercan.cpp from 73 → 37 entries so the libopeninv 50-slot
// pool doesn't silently drop the SKUDAK 0x211 broadcast on rebuild.
// -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 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.
// -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 ******/
@@ -153,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,
+111 -3
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,15 +380,88 @@ void ChargerStateMachine()
DigIo::evseact_out.Set();
DigIo::acpres_out.Set();
if (CheckVoltage() || CheckTimeout())
// 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;
if (CheckUnplugged())
}
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();
DigIo::evseact_out.Clear();
state = OFF;
}
if (CheckChargerFaults())
else if (CheckChargerFaults())
{
DigIo::acpres_out.Clear();
state = OFF;
@@ -373,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;
}
+71 -49
View File
@@ -18,47 +18,62 @@
*/
#include "chargercan.h"
// SKUDAK-S8/S9: trimmed entry list to fit libopeninv's MAX_ITEMS=50 CANPOS pool.
// The upstream teslacharger registers ~73 AddRecv+AddSend mappings, which
// silently overflows the 50-slot pool and drops the last ~23 entries
// (including the SKUDAK 0x211 broadcast). We dropped things we don't use:
//
// - hwaclim duplicates on 0x209/0x20B (keep just 0x207; the three modules
// broadcast identical hardware AC limit values) -2
// - c2*/c3* iac, stt, tmp1/2/in (12 entries — full per-module diagnostics
// not needed; CheckChargerFaults only uses uac+flag, CalcTotals only
// uses idc+udc, and tmpobcmax aggregates fine from c1's 3 probes) -12
// - CHAdeMO RX on 0x102 (canenable/idcspnt/udclim/soc — Polarity VCU
// doesn't use CHAdeMO at all) -4
// - 0x368 version-only idle frame (purely cosmetic for stock OI UI) -7
// - CHAdeMO TX on 0x108/0x109 (version, idclim, udc, idc, opmode —
// unused on this build) -5
//
// Total dropped: 30 entries. From 73 → 42, with 8-slot headroom.
//
// -S9 RESTORED 8 entries the -S8 trim wrongly killed for 3-module Tesla OBCs:
// c2/c3 uac (CheckChargerFaults active2/active3 detection),
// c2/c3 flag (clears CheckAlive timeout under fault-check logic),
// c2/c3 idc (CalcTotals current SUM — was reporting 1/3 of true delivery),
// c2/c3 udc (CalcTotals voltage MAX). Without these the bench reported
// ~2.7kW for what was actually 9.7kW going to the pack.
//
// 0x43c/0x44c TX kept on purpose — they're commands to OBC sub-modules 2/3,
// required for multi-module operation. Drop them too only if pool pressure
// returns and you've confirmed the bench is single-module.
void ChargerCAN::MapMessages(CanMap* can)
{
can->AddRecv(Param::hwaclim, 0x207, 32, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::hwaclim, 0x209, 32, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::hwaclim, 0x20B, 32, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::c1iac, 0x207, 41, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::c2iac, 0x209, 41, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::c3iac, 0x20B, 41, 9, 0.06666f); //gain 0.06666
can->AddRecv(Param::c1uac, 0x207, 8, 8, 1);
can->AddRecv(Param::c2uac, 0x209, 8, 8, 1);
can->AddRecv(Param::c3uac, 0x20B, 8, 8, 1);
can->AddRecv(Param::c1flag, 0x207, 17, 2, 1);
can->AddRecv(Param::c2flag, 0x209, 17, 2, 1);
can->AddRecv(Param::c3flag, 0x20B, 17, 2, 1);
can->AddRecv(Param::c1stt, 0x217, 0, 8, 1);
can->AddRecv(Param::c2stt, 0x219, 0, 8, 1);
can->AddRecv(Param::c3stt, 0x21B, 0, 8, 1);
can->AddRecv(Param::c1idc, 0x227, 32, 16, 0.000839233f); //gain 0.000839233
can->AddRecv(Param::c2idc, 0x229, 32, 16, 0.000839233f); //gain 0.000839233
can->AddRecv(Param::c3idc, 0x22B, 32, 16, 0.000839233f); //gain 0.000839233
can->AddRecv(Param::c1udc, 0x227, 16, 16, 0.01052856f); //gain 0.01052856
can->AddRecv(Param::c2udc, 0x229, 16, 16, 0.01052856f); //gain 0.01052856
can->AddRecv(Param::c3udc, 0x22B, 16, 16, 0.01052856f); //gain 0.01052856
can->AddRecv(Param::c1tmp1, 0x237, 0, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c2tmp1, 0x239, 0, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c3tmp1, 0x23B, 0, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c1tmp2, 0x237, 8, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c2tmp2, 0x239, 8, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c3tmp2, 0x23B, 8, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c1tmpin, 0x237, 40, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c2tmpin, 0x239, 40, 8, 1, -40); //offset -40°C
can->AddRecv(Param::c3tmpin, 0x23B, 40, 8, 1, -40); //offset -40°C
//We don't have enough space for all messages, so we discard these
//can->AddRecv(Param::c1tmplim, 0x247, 0, 8, 7); //gain 0.234375
//can->AddRecv(Param::c2tmplim, 0x249, 0, 8, 7); //gain 0.234375
//can->AddRecv(Param::c3tmplim, 0x24B, 0, 8, 7); //gain 0.234375
/***** CHAdeMO RX *****/
can->AddRecv(Param::canenable, 0x102, 40, 1, 1);
can->AddRecv(Param::idcspnt, 0x102, 24, 8, 1);
can->AddRecv(Param::udclim, 0x102, 8, 16, 1);
can->AddRecv(Param::soc, 0x102, 48, 8, 1);
// SKUDAK-S9: module 2 RX. uac/flag prevent the CheckChargerFaults false-
// positive that would silently mask module-2 failure (active2 = (chargerena
// & 2) && (c2uac > 70V); without c2uac mapped, active2==false short-circuits
// the flag check). idc/udc are required so CalcTotals reports the SUM
// current and MAX voltage across all three Tesla OBC sub-modules.
can->AddRecv(Param::c2uac, 0x209, 8, 8, 1);
can->AddRecv(Param::c2flag, 0x209, 17, 2, 1);
can->AddRecv(Param::c2idc, 0x229, 32, 16, 0.000839233f);
can->AddRecv(Param::c2udc, 0x229, 16, 16, 0.01052856f);
// SKUDAK-S9: module 3 RX, same rationale.
can->AddRecv(Param::c3uac, 0x20B, 8, 8, 1);
can->AddRecv(Param::c3flag, 0x20B, 17, 2, 1);
can->AddRecv(Param::c3idc, 0x22B, 32, 16, 0.000839233f);
can->AddRecv(Param::c3udc, 0x22B, 16, 16, 0.01052856f);
/***** Charger TX ******/
can->AddSend(Param::udcspnt, 0x45c, 0, 16, 100); //gain 100
@@ -85,27 +100,34 @@ void ChargerCAN::MapMessages(CanMap* can)
can->AddSend(Param::aclim, 0x44c, 16, 16, 1500);
can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100);
can->AddSend(Param::version, 0x368, 0, 8, 0, 0x03);
can->AddSend(Param::version, 0x368, 8, 8, 0, 0x49);
can->AddSend(Param::version, 0x368, 16, 8, 0, 0x29);
can->AddSend(Param::version, 0x368, 24, 8, 0, 0x11);
can->AddSend(Param::version, 0x368, 40, 8, 0, 0x0c);
can->AddSend(Param::version, 0x368, 48, 8, 0, 0x40);
can->AddSend(Param::version, 0x368, 56, 8, 0, (int8_t)0xff);
/***** CHAdeMO TX *****/
can->AddSend(Param::version, 0x108, 8, 16, 107); //output 428V max = 4*107
can->AddSend(Param::idclim, 0x108, 24, 8, 1);
can->AddSend(Param::udc, 0x109, 8, 16, 1);
can->AddSend(Param::idc, 0x109, 24, 16, 1);
can->AddSend(Param::opmode, 0x109, 40, 3, 5); //Set charging and connlock at once
/***** SKUDAK VCU command RX (0x212) — SKUDAK-516 user ChargeLimit *****/
// Byte 0: ChargeLimit_pct (0-100). Bytes 1-7 reserved 0x00.
// Param::Change(vcuchglim) translates % to udcspnt setpoint (see src/main.cpp).
// VCU broadcasts every 200 ms once teslacharger detected; charger holds last
// value if VCU goes silent.
/***** 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
// 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: 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::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)
+38 -5
View File
@@ -108,20 +108,41 @@ static void Ms100Task(void)
static void MapChargerMessages()
{
// SKUDAK: Reentrancy guard. canMap->Clear() (libopeninv canmap.cpp) ends
// with canHardware->ClearUserMessages(), which fires the HandleClear
// callback below — which calls back into THIS function. If this function
// ever calls Clear() itself, the chain becomes infinite recursion → stack
// overflow → HardFault before the terminal/web UI come up (this is what
// bricked -S6 on Kyle's bench, see PR #8 postmortem). The static guard
// makes any future "accidental Clear() inside the rebuild path" a no-op
// re-entry rather than a chip-bricking fault. Safe because this function
// is only invoked from main() and from the HandleClear callback, both
// single-threaded contexts; never from an ISR.
static bool in_progress = false;
if (in_progress) return;
uint32_t dummyId;
uint8_t dummyOfs;
int8_t dummyAdd, dummyLen;
float dummyGain;
bool dummyrx;
//check sample value, if it is mapped assume valid CAN map
if (canMap->FindMap(Param::hwaclim, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return;
//canMap->Clear();
// SKUDAK: tmpobcmax is the canary for the -S5+ CAN-map schema. If it's
// already mapped, the persistent flash map is up-to-date — preserve it
// and any user customizations. If it's NOT mapped, the saved map is
// from an older firmware that didn't broadcast OBC temp on 0x211 byte 7.
//
// We deliberately do NOT call canMap->Clear() here. Clear() triggers
// HandleClear, and the recursion is fatal (see guard comment above). The
// upgrade path is: user issues "canclear" via the openinverter terminal
// or web UI to nuke the stale map manually. That single Clear fires
// HandleClear once, this function re-runs additively, and Save() persists.
if (canMap->FindMap(Param::tmpobcmax, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return;
in_progress = true;
ChargerCAN::MapMessages(canMap);
canMap->Save();
in_progress = false;
}
/** This function is called when the user changes a parameter */
@@ -140,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