17 Commits

Author SHA1 Message Date
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
bastian 2c9e7a8bbe Merge pull request 'feat: broadcast worst-case OBC temperature on 0x211 byte 7' (#7) from feat/skudak-broadcast-obc-temp into main
Build Firmware / Build stm32-teslacharger (push) Successful in 58s
2026-05-14 15:53:52 -04:00
Bastian de Byl b7fcd2b301 feat: broadcast worst-case OBC temperature on 0x211 byte 7
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
The Tesla OBC hardware already publishes 9 temperature probes (3
modules × {tmp1, tmp2, tmpin}) on CAN 0x237/0x239/0x23B which the
teslacharger receives as c[123]tmp[12,in]. Until now none of these
were forwarded downstream — the Polarity VCU's STATUS_SCREEN OBC-temp
and DCDC-temp fields rendered 0 °C on OI-OBC cars.

This commit:

- Adds Param::tmpobcmax — derived value computed each Ms100Task tick
  as the max across the 9 probes, skipping any that read -40 °C
  (sensor absent / module not present).
- Maps it to 0x211 byte 7 (previously reserved, the last unused byte
  of the 7-byte frame). Wire encoding uses the Tesla OBC convention
  of offset 40 (wire = °C + 40), matching c[123]tmp* upstream so
  receivers can decode with the same formula.
- Bumps VERSTR to -S5 so the right firmware is visually confirmable
  in the OpenInverter web UI.

The new byte is purely additive — existing 0x211 consumers that read
only bytes 0-6 continue to work unchanged.

The Polarity VCU's 0x211 RX handler grows a corresponding decode +
display path in a paired stm32-hal-vcu commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:48:40 -04:00
bastian e36ba2ccc5 Merge pull request 'fix: drop extern "C" on main to clear -Wpedantic warning (-S4)' (#6) from fix/main-extern-c-pedantic-warning into main
Build Firmware / Build stm32-teslacharger (push) Successful in 1m0s
2026-05-12 15:41:43 -04:00
Bastian de Byl 02e3f2643f fix: drop extern "C" on main to clear -Wpedantic warning
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 1m1s
src/main.cpp:161:16: warning: cannot declare '::main' with a linkage
specification [-Wpedantic]

Long-standing upstream OpenInverter code (predates SKUDAK). C++ forbids
linkage specifications on main; with -pedantic in CPPFLAGS this fires
every build.

Confirmed safe to drop:
- libopencm3 vector.c:35 forward-declares int main(void) with default
  linkage and invokes it at boot.
- Linker resolves main by symbol name, not by linkage attribute.
- stm32-sine has the same upstream issue; same fix applies if we ever
  push upstream.

Bump VERSTR -S3 -> -S4 so the OI web UI version field shows the new
build. Verified in the built binary as "4=1.20.R-S4".

Build: clean, no compiler warnings (only the long-standing linker RWX
notice remains, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:41:06 -04:00
bastian ee59f69890 Merge pull request 'feat: receive ChargeLimit_pct from VCU on CAN 0x212 (SKUDAK-516)' (#4) from feat/skudak-516-receive-vcu-chargelimit into main
Build Firmware / Build stm32-teslacharger (push) Successful in 1m19s
2026-05-10 10:50:20 -04:00
Bastian de Byl 7acbd66907 feat: receive ChargeLimit_pct from VCU on CAN 0x212 (SKUDAK-516)
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 58s
Pairs with stm32-hal-vcu PR #42. Adds a runtime "vcuchglim" param that
the VCU broadcasts every 200 ms; on receipt, the param's Change()
callback translates 20-100% to the charger's udcspnt (DC voltage
setpoint) using a linear cell-voltage ramp.

Without this, the teslacharger had no path for the user's app-set
charge limit to actually limit charging — udcspnt was effectively
a compile-time setpoint.

What's added
------------

- Param::vcuchglim in include/param_prj.h:
    PARAM_ENTRY(CAT_CHARGER, vcuchglim, "%", 20, 100, 80, 23)
  - Range 20-100 (matches VCU validation)
  - Default 80% if VCU never connects
  - ID 23 (next free per the comment, bumped to 24)
  - Saveable so a flashed charger paired with a silent VCU still
    behaves predictably across reboots

- can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1) registration in
  ChargerCAN::MapMessages() — libopeninv's CanMap dispatches to the
  Change() callback on each received frame.

- Param::Change(vcuchglim) handler in src/main.cpp:
    cell_target = 3.30 + (4.15 - 3.30) * (pct - 20) / 80
    udcspnt     = cell_target * 96
  Endpoints chosen to be safe across Tesla LDU / VW MEB / Volt2:
    20%  → 3.30 V/cell × 96 = 316.8 V (deep-cycle storage floor)
    80%  → 3.94 V/cell × 96 = 378.0 V (recommended daily ceiling)
    100% → 4.15 V/cell × 96 = 398.4 V (matches existing udclim default)
  Tuneable here as constants if a particular pack needs a tighter range.

- VER bump 1.19.R → 1.20.R. Combined with the pre-existing -S1 suffix
  the OI web UI shows "4=1.20.R-S1", visually distinct from upstream
  and from the previous Skudak build.

Build
-----

text=25288, links clean. The "RWX LOAD segment" linker warning is
pre-existing on this template, unrelated. No new warnings from this
change.

Version string in the binary verified as "4=1.20.R-S1".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:45:24 -04:00
bastian fb721f7392 Merge pull request 'fix: CheckDelay returns false forever when timedly is negative (-S2)' (#5) from fix/checkdelay-negative-timedly into main
Build Firmware / Build stm32-teslacharger (push) Successful in 1m0s
2026-05-10 09:34:44 -04:00
Bastian de Byl 5dc715558f fix: CheckDelay returns false forever when timedly is negative
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 1m1s
Upstream openinverter bug surfaced after recent SKUDAK PRs.

Symptom (reported by Kyle on the bench): teslacharger detects EVSE,
shows the AC limit, but stays stuck in WaitStart and never proceeds
to Enable / Activate / Run. State machine looks like it's the OFF
state but it's actually one step further on.

Root cause in src/charger.cpp:85-91:

    bool CheckDelay()
    {
       uint32_t now = rtc_get_counter_val();
       uint32_t start = Param::GetInt(Param::timedly) * 60;
       return start <= 0 || (now - startTime) > start;
    }

With timedly = -1 (the param's documented "no delay" sentinel and its
default value), the math is:

    Param::GetInt(timedly) -> -1 (signed int)
    -1 * 60 -> -60 (signed)
    assigned to uint32_t -> 0xFFFFFFC4 (~4.29 billion)
    start <= 0 -> false (uint32_t can't be negative)
    (now - startTime) > 4294967236 -> false for ~136 years

So CheckDelay() returns false forever; charger wedges in WaitStart.

Why "it was working before": every SKUDAK PR that bumped the param
table (uaux broadcast, expanded 0x210, 0x210->0x211, version suffix)
invalidates the saved-params CRC and resets timedly to its -1 default.
Kyle had a non-default value (probably 0) saved before.

Fix: signed math for the short-circuit so timedly <= 0 still means
"start immediately." Cast to uint32_t only on the right operand, which
is only evaluated when start > 0 — can't reinterpret negative as huge.

The companion CheckTimeout() at line 75 has the same uint32_t pattern
but works "correctly by accident" because timelim = -1 makes timeout
huge and (now - startTime) > timeout is false, which matches the
intended "no timeout" semantics. Leaving that alone — fixing it would
change behaviour, not bugs.

VER suffix bumped -S1 -> -S2 so the OI web UI clearly shows the
deployed firmware (post-fix). Confirmed in built binary as
"4=1.19.R-S2".

Build: text=25192, links clean, pre-existing pedantic warning unchanged.

This bug is also present upstream (johanneshuebner/stm32-charger);
worth a PR upstream once we've confirmed the fix on the bench.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:34:17 -04:00
bastian f566427835 Merge pull request 'fix: move SKUDAK telemetry to 0x211 + add -S1 version suffix' (#3) from fix/skudak-448-move-telemetry-to-0x211 into main
Build Firmware / Build stm32-teslacharger (push) Successful in 59s
2026-05-09 17:59:39 -04:00
Bastian de Byl 00f359250c chore: add SKUDAK -S1 version suffix to VERSTR
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
The OpenInverter web UI displays the firmware version from the 'version'
parameter (entry 2001), which expands VERSTR. Out of the box this reads
'4=1.19.R' (upstream version, no Skudak indicator). With this change it
reads '4=1.19.R-S1' so we can visually confirm we're running the Skudak
fork and not stock OpenInverter firmware.

Matches the convention already used in stm32-sine ('-sine-S6' / '-foc-S6')
where the -S<N> suffix bumps on each Skudak-side change.

Verified: 'strings stm32_charger.bin | grep S1' shows '4=1.19.R-S1' in the
built artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:57:45 -04:00
Bastian de Byl 7de46b8160 fix: move SKUDAK telemetry frame from 0x210 → 0x211 (avoid bus contention)
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
Bench capture on the green car showed a non-Polarity module also
broadcasting at 0x210 (DLC=8 frame with mostly-zero payload, byte 5 = 0x85
static — likely a Tesla OEM DI module). Even after a clean reflash + 'can c'
to clear persisted maps, the rogue 0x210 frame keeps appearing on the wire,
racing our intended frame and intermittently corrupting the VCU's read.

Confirmed via 'can p' on the OBC that this firmware's CanMap has exactly
one 0x210 entry with the 5 expected fields — the conflict is external.

Moves all five AddSends to 0x211, an adjacent free ID. The VCU's
CHARGER_CAN_ID_OI_OBC_AUX constant is updated to match in a paired
stm32-hal-vcu PR. Same 7-byte expanded layout (state/udc/idc/uaux@5/soc),
same 100ms cadence — only the arbitration ID changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:54:54 -04:00
bastian 20339b738b Merge pull request 'feat: expand 0x210 telemetry layout (state/udc/idc/uaux@5/soc)' (#2) from feat/skudak-448-expanded-0x210-telemetry into main
Build Firmware / Build stm32-teslacharger (push) Successful in 59s
2026-05-08 17:00:30 -04:00
Bastian de Byl 3580123a39 feat: expand 0x210 telemetry layout (state/udc/idc/uaux@5/soc) for SKUDAK-448
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
Replaces the single uaux-at-byte-0 broadcast (commit d209a47) with a
7-byte expanded telemetry frame. Bench capture on green car showed dual
0x210 frames colliding (DLC=8 simple + DLC=7 expanded) because Kyle's
flashed image had both AddSend sets active; standardising on the
expanded layout eliminates that ambiguity.

Byte layout:
  byte 0     state    (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop)
  bytes 1-2  udc      (V, gain=1)
  bytes 3-4  idc      (0.1A, gain=10)
  byte 5     uaux     (decivolts, gain=10) — 12V aux battery rail
  byte 6     soc      (%, gain=1)
  byte 7     reserved

Consumed by stm32-hal-vcu Core/Src/main.c CHARGER_CAN_ID_OI_OBC_AUX
handler. The state byte enables future VCU_IsCharging integration via
state == 4 (Run) without requiring a second CAN ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:48:48 -04:00
5 changed files with 228 additions and 60 deletions
+1
View File
@@ -4,3 +4,4 @@ stm32_charger*
linker.map linker.map
*.layout *.layout
*.out *.out
.DS_Store
+31 -4
View File
@@ -39,15 +39,15 @@
*/ */
//Define a version string of your firmware here //Define a version string of your firmware here
#define VER 1.19.R #define VER 1.20.R
/* Entries must be ordered as follows: /* Entries must be ordered as follows:
1. Saveable parameters (id != 0) 1. Saveable parameters (id != 0)
2. Temporary parameters (id = 0) 2. Temporary parameters (id = 0)
3. Display values 3. Display values
*/ */
//Next param id (increase when adding new parameter!): 23 //Next param id (increase when adding new parameter!): 24
//Next value Id: 2051 //Next value Id: 2053
/* category name unit min max default id */ /* category name unit min max default id */
#define PARAM_LIST \ #define PARAM_LIST \
PARAM_ENTRY(CAT_CHARGER, idclim, "A", 0, 45, 45, 3 ) \ PARAM_ENTRY(CAT_CHARGER, idclim, "A", 0, 45, 45, 3 ) \
@@ -63,6 +63,7 @@
PARAM_ENTRY(CAT_CHARGER, enablepol, POLARITIES,0, 1, 0, 18 ) \ PARAM_ENTRY(CAT_CHARGER, enablepol, POLARITIES,0, 1, 0, 18 ) \
PARAM_ENTRY(CAT_CHARGER, idckp, "", 0, 10000, 1, 20 ) \ PARAM_ENTRY(CAT_CHARGER, idckp, "", 0, 10000, 1, 20 ) \
PARAM_ENTRY(CAT_CHARGER, idcki, "", 0, 10000, 10, 21 ) \ PARAM_ENTRY(CAT_CHARGER, idcki, "", 0, 10000, 10, 21 ) \
PARAM_ENTRY(CAT_CHARGER, vcuchglim, "%", 20, 100, 80, 23 ) \
VALUE_ENTRY(state, STATES, 2043 ) \ VALUE_ENTRY(state, STATES, 2043 ) \
VALUE_ENTRY(uptime, "s", 2048 ) \ VALUE_ENTRY(uptime, "s", 2048 ) \
VALUE_ENTRY(lasterr, errorListString, 2002 ) \ VALUE_ENTRY(lasterr, errorListString, 2002 ) \
@@ -107,6 +108,8 @@
VALUE_ENTRY(c3iac, "A", 2034 ) \ VALUE_ENTRY(c3iac, "A", 2034 ) \
VALUE_ENTRY(c3udc, "V", 2035 ) \ VALUE_ENTRY(c3udc, "V", 2035 ) \
VALUE_ENTRY(c3idc, "A", 2036 ) \ VALUE_ENTRY(c3idc, "A", 2036 ) \
VALUE_ENTRY(tmpobcmax, "°C", 2051 ) \
VALUE_ENTRY(vcustop, OFFON, 2052 ) \
VALUE_ENTRY(test_time, "s", 3000 ) \ VALUE_ENTRY(test_time, "s", 3000 ) \
VALUE_ENTRY(test_timer_flag, "X", 3001 ) \ VALUE_ENTRY(test_timer_flag, "X", 3001 ) \
VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \ VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \
@@ -124,7 +127,31 @@
#define CAT_CHARGER "Charger" #define CAT_CHARGER "Charger"
#define CAT_COMM "Communication" #define CAT_COMM "Communication"
#define VERSTR STRINGIFY(4=VER) // 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.
// -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.
#define VERSTR STRINGIFY(4=VER-S11)
/***** enums ******/ /***** enums ******/
+24 -5
View File
@@ -85,9 +85,15 @@ bool CheckTimeout()
bool CheckDelay() bool CheckDelay()
{ {
uint32_t now = rtc_get_counter_val(); uint32_t now = rtc_get_counter_val();
uint32_t start = Param::GetInt(Param::timedly) * 60; // Upstream uses uint32_t here, which wraps a negative timedly (the param's
// documented -1 "no delay" sentinel) into a ~4 billion-second timeout that
// never expires — leaving the state machine wedged in WaitStart forever.
// Use signed math for the short-circuit so timedly <= 0 still means
// "start immediately." The cast on the elapsed-comparison side is only
// reached when start > 0, so it can't reinterpret a negative as huge.
int start = Param::GetInt(Param::timedly) * 60;
return start <= 0 || (now - startTime) > start; return start <= 0 || (now - startTime) > (uint32_t)start;
} }
@@ -347,15 +353,28 @@ void ChargerStateMachine()
DigIo::evseact_out.Set(); DigIo::evseact_out.Set();
DigIo::acpres_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; state = STOP;
if (CheckUnplugged()) }
else if (CheckVoltage() || CheckTimeout())
state = STOP;
else if (CheckUnplugged())
{ {
DigIo::acpres_out.Clear(); DigIo::acpres_out.Clear();
DigIo::evseact_out.Clear(); DigIo::evseact_out.Clear();
state = OFF; state = OFF;
} }
if (CheckChargerFaults()) else if (CheckChargerFaults())
{ {
DigIo::acpres_out.Clear(); DigIo::acpres_out.Clear();
state = OFF; state = OFF;
+82 -45
View File
@@ -18,47 +18,62 @@
*/ */
#include "chargercan.h" #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) void ChargerCAN::MapMessages(CanMap* can)
{ {
can->AddRecv(Param::hwaclim, 0x207, 32, 9, 0.06666f); //gain 0.06666 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::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::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::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::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::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::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::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::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::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 // SKUDAK-S9: module 2 RX. uac/flag prevent the CheckChargerFaults false-
//We don't have enough space for all messages, so we discard these // positive that would silently mask module-2 failure (active2 = (chargerena
//can->AddRecv(Param::c1tmplim, 0x247, 0, 8, 7); //gain 0.234375 // & 2) && (c2uac > 70V); without c2uac mapped, active2==false short-circuits
//can->AddRecv(Param::c2tmplim, 0x249, 0, 8, 7); //gain 0.234375 // the flag check). idc/udc are required so CalcTotals reports the SUM
//can->AddRecv(Param::c3tmplim, 0x24B, 0, 8, 7); //gain 0.234375 // current and MAX voltage across all three Tesla OBC sub-modules.
/***** CHAdeMO RX *****/ can->AddRecv(Param::c2uac, 0x209, 8, 8, 1);
can->AddRecv(Param::canenable, 0x102, 40, 1, 1); can->AddRecv(Param::c2flag, 0x209, 17, 2, 1);
can->AddRecv(Param::idcspnt, 0x102, 24, 8, 1); can->AddRecv(Param::c2idc, 0x229, 32, 16, 0.000839233f);
can->AddRecv(Param::udclim, 0x102, 8, 16, 1); can->AddRecv(Param::c2udc, 0x229, 16, 16, 0.01052856f);
can->AddRecv(Param::soc, 0x102, 48, 8, 1);
// 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 ******/ /***** Charger TX ******/
can->AddSend(Param::udcspnt, 0x45c, 0, 16, 100); //gain 100 can->AddSend(Param::udcspnt, 0x45c, 0, 16, 100); //gain 100
@@ -85,21 +100,43 @@ void ChargerCAN::MapMessages(CanMap* can)
can->AddSend(Param::aclim, 0x44c, 16, 16, 1500); can->AddSend(Param::aclim, 0x44c, 16, 16, 1500);
can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100); can->AddSend(Param::opmode, 0x44c, 32, 8, 154, 100);
can->AddSend(Param::version, 0x368, 0, 8, 0, 0x03); /***** SKUDAK VCU command RX (0x212) — SKUDAK-516 chglim + S10 udclim + S11 stop *****/
can->AddSend(Param::version, 0x368, 8, 8, 0, 0x49); // Byte 0: ChargeLimit_pct (0-100). Param::Change(vcuchglim) translates
can->AddSend(Param::version, 0x368, 16, 8, 0, 0x29); // % to udcspnt setpoint (see src/main.cpp).
can->AddSend(Param::version, 0x368, 24, 8, 0, 0x11); // Bytes 1-2: udclim_V (uint16 LE). DEPRECATED in -S11 but mapping kept for
can->AddSend(Param::version, 0x368, 40, 8, 0, 0x0c); // backward compat with -S10 VCU TX. New VCU always sends 398V
can->AddSend(Param::version, 0x368, 48, 8, 0, 0x40); // (= the flash default), so this RX is a no-op. Old -S10 trick
can->AddSend(Param::version, 0x368, 56, 8, 0, (int8_t)0xff); // (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.
can->AddRecv(Param::vcuchglim, 0x212, 0, 8, 1);
can->AddRecv(Param::udclim, 0x212, 8, 16, 1);
can->AddRecv(Param::vcustop, 0x212, 24, 8, 1);
/***** CHAdeMO TX *****/ /***** SKUDAK VCU telemetry (0x211) — SKUDAK-448 expanded charger telemetry *****/
can->AddSend(Param::version, 0x108, 8, 16, 107); //output 428V max = 4*107 // Byte 0: state (0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop)
can->AddSend(Param::idclim, 0x108, 24, 8, 1); // Bytes 1-2: udc (V, gain=1)
can->AddSend(Param::udc, 0x109, 8, 16, 1); // Bytes 3-4: idc (0.1A, gain=10)
can->AddSend(Param::idc, 0x109, 24, 16, 1); // Byte 5: uaux (decivolts, gain=10) — 12V aux battery rail
can->AddSend(Param::opmode, 0x109, 40, 3, 5); //Set charging and connlock at once // Byte 6: soc (%, gain=1)
// Byte 7: tmpobcmax (°C, gain=1, offset=-40) — worst-case across all
/***** Auxiliary TX *****/ // 9 OBC probes; matches Tesla OBC's own CAN offset
can->AddSend(Param::uaux, 0x210, 0, 8, 10); //12V aux battery: byte 0, gain=10 (0.1V/decivolts, SKUDAK-448) // so receivers can decode with the same formula.
// Consumed by stm32-hal-vcu Core/Src/main.c CHARGER_CAN_ID_OI_OBC_AUX handler.
can->AddSend(Param::state, 0x211, 0, 8, 1);
can->AddSend(Param::udc, 0x211, 8, 16, 1);
can->AddSend(Param::idc, 0x211, 24, 16, 10);
can->AddSend(Param::uaux, 0x211, 40, 8, 10);
can->AddSend(Param::soc, 0x211, 48, 8, 1);
can->AddSend(Param::tmpobcmax, 0x211, 56, 8, 1, 40); // offset 40 → wire byte = temp + 40
} }
+90 -6
View File
@@ -77,25 +77,72 @@ static void Ms100Task(void)
EvseRead(); EvseRead();
// SKUDAK: derive a single "worst case" OBC temperature for downstream
// consumers (Polarity VCU broadcasts it on STATUS_SCREEN bytes 23/26).
// Aggregates the 9 per-module probes the Tesla OBC publishes on CAN
// 0x237/0x239/0x23B — max wins. Modules without a sensor read -40 °C
// (the DBC offset; raw byte 0); we ignore those so an absent module
// doesn't drag the worst-case down. If no probes are valid we leave
// tmpobcmax at -40 °C, which the VCU will display as "—".
{
const Param::PARAM_NUM probes[] = {
Param::c1tmp1, Param::c1tmp2, Param::c1tmpin,
Param::c2tmp1, Param::c2tmp2, Param::c2tmpin,
Param::c3tmp1, Param::c3tmp2, Param::c3tmpin,
};
int worst = -40;
bool any_ok = false;
for (unsigned i = 0; i < sizeof(probes) / sizeof(probes[0]); i++) {
int t = Param::GetInt(probes[i]);
if (t <= -40) continue; // No sensor / module not present
if (!any_ok || t > worst) {
worst = t;
any_ok = true;
}
}
Param::SetInt(Param::tmpobcmax, any_ok ? worst : -40);
}
canMap->SendAll(); canMap->SendAll();
} }
static void MapChargerMessages() 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; uint32_t dummyId;
uint8_t dummyOfs; uint8_t dummyOfs;
int8_t dummyAdd, dummyLen; int8_t dummyAdd, dummyLen;
float dummyGain; float dummyGain;
bool dummyrx; bool dummyrx;
//check sample value, if it is mapped assume valid CAN map // SKUDAK: tmpobcmax is the canary for the -S5+ CAN-map schema. If it's
if (canMap->FindMap(Param::hwaclim, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return; // 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
//canMap->Clear(); // 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); ChargerCAN::MapMessages(canMap);
canMap->Save(); canMap->Save();
in_progress = false;
} }
/** This function is called when the user changes a parameter */ /** This function is called when the user changes a parameter */
@@ -114,6 +161,39 @@ void Param::Change(Param::PARAM_NUM paramNum)
spnt = MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim)); spnt = MIN(Param::Get(Param::idcspnt), Param::Get(Param::idclim));
dcCurController.SetRef(spnt); dcCurController.SetRef(spnt);
break; 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
// percentage to udcspnt (DC voltage setpoint) using a linear cell-voltage
// ramp. 96-cell pack (Tesla LDU / VW MEB compatible):
// pct=20 → 3.30 V/cell × 96 = 316.8 V (storage / deep-cycle floor)
// pct=80 → 3.94 V/cell × 96 = 378.0 V (recommended daily ceiling)
// pct=100 → 4.15 V/cell × 96 = 398.4 V (matches existing udclim default)
// Endpoints chosen to be safe across Tesla/VW/Volt2 chemistries — adjust
// here if a particular pack needs a tighter range. The VCU validates 20-100
// before transmit so out-of-range values never arrive.
int pct = Param::GetInt(Param::vcuchglim);
if (pct < 20) pct = 20;
if (pct > 100) pct = 100;
const float CELL_LOW_V = 3.30f;
const float CELL_HIGH_V = 4.15f;
const float CELL_COUNT = 96.0f;
float cell_target = CELL_LOW_V + (CELL_HIGH_V - CELL_LOW_V) * (pct - 20) / 80.0f;
Param::SetFloat(Param::udcspnt, cell_target * CELL_COUNT);
break;
}
default: default:
//Handle general parameter changes here. Add paramNum labels for handling specific parameters //Handle general parameter changes here. Add paramNum labels for handling specific parameters
break; break;
@@ -137,7 +217,11 @@ extern "C" void tim2_isr(void)
scheduler->Run(); scheduler->Run();
} }
extern "C" int main(void) // C++ forbids a linkage specification on `main` (-Wpedantic). The reset_handler
// in libopencm3's vector.c forward-declares `int main(void)` with default
// linkage and resolves it at link time by symbol name, so dropping `extern "C"`
// here doesn't affect how main is invoked at boot.
int main(void)
{ {
extern const TERM_CMD termCmds[]; extern const TERM_CMD termCmds[];