1 Commits

Author SHA1 Message Date
Bastian de Byl 307853bea8 feat: receive ChargeLimit_pct from VCU on CAN 0x212 (SKUDAK-516)
Build Firmware / Build stm32-teslacharger (pull_request) Successful in 59s
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-09 22:38:01 -04:00
5 changed files with 20 additions and 73 deletions
-1
View File
@@ -4,4 +4,3 @@ stm32_charger*
linker.map
*.layout
*.out
.DS_Store
+3 -4
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: 2051
/* category name unit min max default id */
#define PARAM_LIST \
PARAM_ENTRY(CAT_CHARGER, idclim, "A", 0, 45, 45, 3 ) \
@@ -108,7 +108,6 @@
VALUE_ENTRY(c3iac, "A", 2034 ) \
VALUE_ENTRY(c3udc, "V", 2035 ) \
VALUE_ENTRY(c3idc, "A", 2036 ) \
VALUE_ENTRY(tmpobcmax, "°C", 2051 ) \
VALUE_ENTRY(test_time, "s", 3000 ) \
VALUE_ENTRY(test_timer_flag, "X", 3001 ) \
VALUE_ENTRY(test_timer_icvalue, "X", 3002 ) \
@@ -127,9 +126,9 @@
#define CAT_COMM "Communication"
// 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
// web UI shows a distinct version (e.g. "4=1.19.R-S1") and we can visually confirm
// the right firmware is flashed. Match the stm32-sine -S<N> convention.
#define VERSTR STRINGIFY(4=VER-S6)
#define VERSTR STRINGIFY(4=VER-S1)
/***** enums ******/
+2 -8
View File
@@ -85,15 +85,9 @@ bool CheckTimeout()
bool CheckDelay()
{
uint32_t now = rtc_get_counter_val();
// 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;
uint32_t start = Param::GetInt(Param::timedly) * 60;
return start <= 0 || (now - startTime) > (uint32_t)start;
return start <= 0 || (now - startTime) > start;
}
+1 -4
View File
@@ -113,14 +113,11 @@ void ChargerCAN::MapMessages(CanMap* can)
// 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: tmpobcmax (°C, gain=1, offset=-40) — worst-case across all
// 9 OBC probes; matches Tesla OBC's own CAN offset
// so receivers can decode with the same formula.
// Byte 7: reserved
// 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
}
+4 -46
View File
@@ -77,32 +77,6 @@ static void Ms100Task(void)
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();
}
@@ -114,22 +88,10 @@ static void MapChargerMessages()
float dummyGain;
bool dummyrx;
// SKUDAK: tmpobcmax is the canary for the -S5+ CAN-map schema. If it's
// already mapped, the persistent flash map was built by a firmware that
// included the full current set of mappings (including the OBC temp
// aggregate and all c1*/c2*/c3* receives) — return early so user
// customizations are preserved.
//
// If tmpobcmax is NOT mapped, the saved map is from an older firmware
// that didn't know about it. We must wipe and rebuild from current
// chargercan.cpp — without this the OpenInverter single-shot MapMessages()
// pattern silently leaves newer params un-mapped on every subsequent
// boot, even after flashing fresh firmware (the original guard keyed off
// hwaclim, which has existed since the very first version — so it always
// short-circuited on upgrade and the new entries were dead code).
if (canMap->FindMap(Param::tmpobcmax, dummyId, dummyOfs, dummyLen, dummyGain, dummyAdd, dummyrx)) return;
//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();
//canMap->Clear();
ChargerCAN::MapMessages(canMap);
@@ -196,11 +158,7 @@ extern "C" void tim2_isr(void)
scheduler->Run();
}
// 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 "C" int main(void)
{
extern const TERM_CMD termCmds[];