Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a30cded074 | |||
| 0514940877 | |||
| aae23f53a6 | |||
| d75027b17c | |||
| 4c1f2e1991 | |||
| c905e1236b | |||
| 23716a260b | |||
| 2c9e7a8bbe | |||
| b7fcd2b301 | |||
| e36ba2ccc5 | |||
| 02e3f2643f | |||
| ee59f69890 | |||
| 7acbd66907 | |||
| fb721f7392 | |||
| 5dc715558f | |||
| f566427835 | |||
| 00f359250c | |||
| 7de46b8160 | |||
| 20339b738b | |||
| 3580123a39 | |||
| 877508d047 | |||
| 3d2dbbc709 | |||
| 69822ca08e | |||
| 7f1d417db6 | |||
| 224e7ff286 | |||
| d209a472a6 |
@@ -0,0 +1,36 @@
|
|||||||
|
name: Build Firmware
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build stm32-teslacharger
|
||||||
|
runs-on: fedora
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout with submodules
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build libopencm3
|
||||||
|
run: make get-deps
|
||||||
|
|
||||||
|
- name: Build firmware
|
||||||
|
run: make directories && make images -j$(nproc)
|
||||||
|
|
||||||
|
- name: Show binary size
|
||||||
|
run: ls -lh stm32_charger.bin
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: stm32-teslacharger-${{ github.sha }}
|
||||||
|
path: stm32_charger.bin
|
||||||
|
retention-days: 30
|
||||||
@@ -4,3 +4,4 @@ stm32_charger*
|
|||||||
linker.map
|
linker.map
|
||||||
*.layout
|
*.layout
|
||||||
*.out
|
*.out
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[submodule "libopencm3"]
|
[submodule "libopencm3"]
|
||||||
path = libopencm3
|
path = libopencm3
|
||||||
url = git@github.com:jsphuebner/libopencm3.git
|
url = https://github.com/jsphuebner/libopencm3.git
|
||||||
[submodule "libopeninv"]
|
[submodule "libopeninv"]
|
||||||
path = libopeninv
|
path = libopeninv
|
||||||
url = git@github.com:jsphuebner/libopeninv.git
|
url = https://github.com/jsphuebner/libopeninv.git
|
||||||
|
|||||||
+60
-4
@@ -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(vcucmd, VCUCMDS, 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 ) \
|
||||||
@@ -116,6 +119,12 @@
|
|||||||
#define OPMODES "0=Off, 1=Run"
|
#define OPMODES "0=Off, 1=Run"
|
||||||
#define CHARGERS "1=Charger1, 2=Charger2, 4=Charger3"
|
#define CHARGERS "1=Charger1, 2=Charger2, 4=Charger3"
|
||||||
#define OFFON "0=Off, 1=On"
|
#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 CHFLAGS "0=None, 1=Enabled, 2=Fault, 4=CheckAlive"
|
||||||
#define STATES "0=Off, 1=WaitStart, 2=Enable, 3=Activate, 4=Run, 5=Stop"
|
#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"
|
#define INPUTS "0=Type2, 1=Type2-3P, 2=Type1, 3=Manual, 4=Manual-3P, 5=Type2-Auto"
|
||||||
@@ -124,7 +133,42 @@
|
|||||||
#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.
|
||||||
|
// -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 ******/
|
/***** enums ******/
|
||||||
|
|
||||||
@@ -148,6 +192,18 @@ enum states
|
|||||||
STOP
|
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
|
enum _canspeeds
|
||||||
{
|
{
|
||||||
CAN_PERIOD_100MS = 0,
|
CAN_PERIOD_100MS = 0,
|
||||||
|
|||||||
+121
-7
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -293,10 +299,26 @@ void CalcAcCurrentLimit()
|
|||||||
Param::SetFloat(Param::aclim, iacLim);
|
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()
|
void ChargerStateMachine()
|
||||||
{
|
{
|
||||||
static states state = OFF;
|
static states state = OFF;
|
||||||
int configuredChargers = Param::GetInt(Param::chargerena);
|
int configuredChargers = Param::GetInt(Param::chargerena);
|
||||||
|
int vcucmd = Param::GetInt(Param::vcucmd);
|
||||||
|
|
||||||
if (!Param::GetBool(Param::enable))
|
if (!Param::GetBool(Param::enable))
|
||||||
{
|
{
|
||||||
@@ -310,6 +332,9 @@ void ChargerStateMachine()
|
|||||||
Param::SetInt(Param::opmode, 0);
|
Param::SetInt(Param::opmode, 0);
|
||||||
DisableAll();
|
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())
|
if (CheckStartCondition())
|
||||||
{
|
{
|
||||||
startTime = rtc_get_counter_val();
|
startTime = rtc_get_counter_val();
|
||||||
@@ -328,7 +353,15 @@ void ChargerStateMachine()
|
|||||||
DigIo::ch2ena_out.Set();
|
DigIo::ch2ena_out.Set();
|
||||||
if (configuredChargers & 4)
|
if (configuredChargers & 4)
|
||||||
DigIo::ch3ena_out.Set();
|
DigIo::ch3ena_out.Set();
|
||||||
state = ACTIVATE;
|
|
||||||
|
// Hold here until VCU says go (vcucmd=Charging) or terminates
|
||||||
|
// (vcucmd=Complete). Stopped keeps us armed but idle.
|
||||||
|
if (vcucmd == VCUCMD_COMPLETE)
|
||||||
|
state = STOP;
|
||||||
|
else if (vcucmd == VCUCMD_CHARGING)
|
||||||
|
state = ACTIVATE;
|
||||||
|
else if (CheckUnplugged())
|
||||||
|
state = OFF;
|
||||||
break;
|
break;
|
||||||
case ACTIVATE:
|
case ACTIVATE:
|
||||||
Param::SetInt(Param::opmode, 1);
|
Param::SetInt(Param::opmode, 1);
|
||||||
@@ -347,15 +380,88 @@ void ChargerStateMachine()
|
|||||||
DigIo::evseact_out.Set();
|
DigIo::evseact_out.Set();
|
||||||
DigIo::acpres_out.Set();
|
DigIo::acpres_out.Set();
|
||||||
|
|
||||||
if (CheckVoltage() || CheckTimeout())
|
// VCU command is the primary exit. CheckUnplugged + CheckChargerFaults
|
||||||
state = STOP;
|
// remain for hardware safety; the self-decided CheckVoltage/CheckTimeout
|
||||||
if (CheckUnplugged())
|
// 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;
|
||||||
|
}
|
||||||
|
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::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;
|
||||||
@@ -367,6 +473,14 @@ void ChargerStateMachine()
|
|||||||
|
|
||||||
if (CheckUnplugged())
|
if (CheckUnplugged())
|
||||||
state = OFF;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+88
-42
@@ -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,18 +100,49 @@ 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 + S12 vcucmd *****/
|
||||||
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: 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);
|
||||||
|
|
||||||
/***** 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
|
||||||
|
// 9 OBC probes; matches Tesla OBC's own CAN offset
|
||||||
|
// 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
@@ -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[];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user