Configurable per H-bridge ETB duty ceiling (#9799) - #10086
Conversation
f61be27 to
b1820c9
Compare
ETB_DUTY_LIMIT was a hard-coded 0.9, applied through the ETB_PERCENT_TO_DUTY macro at the single point where the controller writes to the motor. The reporter's H-bridge trips its own overcurrent/thermal protection as the PID pushes duty toward that ceiling, on a throttle which is mechanically wide open around 45% duty - so the top half of the range is not merely unused, it is actively harmful on that hardware. The limit is now engineConfiguration->etbMaxDutyCycle[ETB_COUNT], default 90. 90 stays both the default and the upper bound. This issue is about lowering the ceiling; letting a tune raise it past what the hardware was always limited to is a separate call with hardware-protection implications, so the field range stops at 90 rather than 100. Easy to relax if that is not the preference. 0 means "not configured". Tunes predating the field read 0, which would otherwise mean "never open the throttle". applyDefaultsOrFixAfterBurn maps it to 90 per docs/calibration-compatibility.md, and getMaxDutyCycle() independently falls back to 90 for anything outside the valid range, so even a config reaching the controller before migration runs cannot shut the throttle. Two layers on purpose, given what this path controls. init() keeps its signature - it is a pure virtual with roughly two dozen call sites in the tests. The H-bridge index is instead recovered by finding this controller's function in etbFunctions[], which is the array that defines the mapping in the first place. Full unit test suite: 1164 pass, up from 1159. Host build only; no ARM firmware build was run locally, and this is not tested on hardware. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b1820c9 to
1248959
Compare
startBenchTest() and doAutocal() drive the motor with a fixed 0.5 duty rather than going through setOutput(), so they ignored the configurable ceiling entirely. On the hardware which motivated this issue that is the worst case: 50% is above the limit the user lowered the ceiling to in order to stop the driver faulting, so the two paths most likely to be run on the bench were the two that could still trip it. New clampToMaxDutyCycle() is used by all three call sites. Trade-off worth flagging: autocal can be less accurate on a throttle which needs more than the configured ceiling to reach its mechanical stops. Exceeding a limit the user set for hardware protection is the worse of the two. Full suite: 1172 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed a follow-up commit after tracing the subsystem properly rather than just the call site — the first version had a hole in it.
motor->set(0.5f); // startBenchTest
motor->set(0.5f); // autocal open
motor->set(-0.5f); // autocal closeOn the hardware that motivated this issue that is the worst case: 50% is above the limit the user lowered the ceiling to in order to stop the driver faulting. So the two routines most likely to be run on a bench were the two that could still trip it — while the PR claimed to have fixed exactly that. All three now go through a new Trade-off worth your judgement: autocal can be less accurate on a throttle that needs more than the configured ceiling to reach its mechanical stops. I took the view that exceeding a limit the user set for hardware protection is the worse of the two, but if you'd rather autocal keep full authority and only the runtime path be limited, that's a one-line change. Full suite: 1172 tests pass. |
…#9123 review) Review rework per FDSoftware: 1. The valve now parks at a dedicated idleSolenoidParkPosition instead of following the idle loop's calculated position - "I want the off position to be X place instead of 0" is now literally a field. 2. The parked position is only held for idleSolenoidParkTimeout seconds after the engine stops turning, then the outputs switch off anyway - holding a solenoid energized with the engine off drains the battery and can overheat the coil. The timeout check uses Timer::hasElapsedSec on the trigger event timer rather than comparing getSecondsSinceTriggerEvent against the setting, because elapsed readings saturate at 2^32 ticks - under 26 seconds on the kinetis/cypress ports - which would make any larger timeout never expire and leave the valve powered forever. hasElapsedSec treats a saturated delta as expired. It also means a freshly booted ECU which has never seen the engine turn parks nothing. Both new fields are appended at the end of engine_configuration_s so no offsets move. Tunes which predate the timeout carry 0, which applyDefaultsOrFixAfterBurn migrates to the 60 second default so enabling the flag cannot silently do nothing - same pattern as etbMaxDutyCycle in rusefi#10086. Full suite: 1170 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUaUv4kBBZhtCkMpZnh8So
…TY_CYCLE
default_base_engine.cpp uses the constant but never included electronic_throttle.h,
where it lives. The unit test pch happens to pull the header in, which is why the
test build passed while all 51 firmware/simulator CI jobs failed with
default_base_engine.cpp:152: error: 'ETB_DEFAULT_MAX_DUTY_CYCLE' was not declared
The header is already compiled in every build context via engine_controller.cpp
and engine_configuration.cpp, so including it here is safe everywhere.
Local verification: full unit suite passes (1172 tests). The 32-bit MinGW
simulator toolchain CI uses is not available on this machine, so the simulator
build itself is left to CI - the include is what the error asked for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUaUv4kBBZhtCkMpZnh8So
…review) Review rework per FDSoftware: 1. The valve now parks at a dedicated idleSolenoidParkPosition instead of following the idle loop's calculated position - "I want the off position to be X place instead of 0" is now literally a field. 2. The parked position is only held for idleSolenoidParkTimeout seconds after the engine stops turning, then the outputs switch off anyway - holding a solenoid energized with the engine off drains the battery and can overheat the coil. The timeout check uses Timer::hasElapsedSec on the trigger event timer rather than comparing getSecondsSinceTriggerEvent against the setting, because elapsed readings saturate at 2^32 ticks - under 26 seconds on the kinetis/cypress ports - which would make any larger timeout never expire and leave the valve powered forever. hasElapsedSec treats a saturated delta as expired. It also means a freshly booted ECU which has never seen the engine turn parks nothing. Both new fields are appended at the end of engine_configuration_s so no offsets move. Tunes which predate the timeout carry 0, which applyDefaultsOrFixAfterBurn migrates to the 60 second default so enabling the flag cannot silently do nothing - same pattern as etbMaxDutyCycle in #10086. Full suite: 1170 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUaUv4kBBZhtCkMpZnh8So
Fixes #9799.
Problem
ETB_DUTY_LIMITwas a hard-coded0.9, reaching the motor through one macro at one call site:Per the report, that H-bridge trips its own overcurrent/thermal protection as the PID pushes duty toward the ceiling, on a throttle that is mechanically wide open around 45% duty. The top half of the range isn't merely unused on that hardware — it's actively driving the part into a fault state.
Now
engineConfiguration->etbMaxDutyCycle[ETB_COUNT], default 90.Two decisions I'd particularly like checked
90 is both the default and the upper bound. The field range is 10–90, not 10–100. This issue is about lowering the ceiling; letting a tune raise it past what the hardware has always been limited to is a separate call with hardware-protection implications, and not mine to make. One-character change if you'd rather allow 100.
0 means "not configured", and there are two independent fallbacks. Tunes predating the field read 0, which would otherwise mean never open the throttle.
applyDefaultsOrFixAfterBurnmaps 0 → 90 perdocs/calibration-compatibility.md, andgetMaxDutyCycle()separately falls back to 90 for anything outside the valid range. So a config that somehow reaches the controller before migration runs still can't shut the throttle. Belt and braces is deliberate here given what this path controls — happy to drop one layer if you consider it noise.Implementation note
init()keeps its signature. It's a pure virtual with roughly two dozen call sites across the tests, and churning all of them to thread an index through seemed worse than recovering the index frometbFunctions[]— the array that defines the function → H-bridge mapping in the first place.rusefi_config.txtuint8_t[ETB_COUNT iterate] etbMaxDutyCycle, appended at the end ofengine_configuration_selectronic_throttle.hETB_DEFAULT_MAX_DUTY_CYCLE(90),ETB_MIN_MAX_DUTY_CYCLE(10)electronic_throttle.cppgetHBridgeIndex()/getMaxDutyCycle()/percentToDuty()default_base_engine.cppsetDefaultBaseEngine, 0 → 90 migration inapplyDefaultsOrFixAfterBurntunerstudio.template.inietbFunctions2 != 0test_etb.cppGenerated artifacts (
*_generated_structures_*.h, per-board.ini,VariableRegistryValues.java) are intentionally not committed, per CLAUDE.md.Validation
setDefaultBaseEnginerather than only testing my own assignment.applyDefaultsOrFixAfterBurnreports a change and fills 90, then asserts a user-chosen 45 survives a second call.Two things I did not do, stated plainly:
.iniregeneration are on CI, not verified by me. My host toolchain is MinGW GCC 16.1, ahead of CI's, so warning-level parity isn't guaranteed either.While looking for this one I checked a number of other open issues against the code and found several already fixed — I left notes on #3040, #8070, #9101, #9103, #9638 and #9641 with the implementing commits, in case they're closable.
🤖 Generated with Claude Code