Skip to content

fan: latch HBR tach above max_rpm/2 to absorb soft-PWM transients - #185

Open
DRracer wants to merge 1 commit into
vintagepc:MINI404from
DRracer:feat/fan-always-emit-tach
Open

fan: latch HBR tach above max_rpm/2 to absorb soft-PWM transients#185
DRracer wants to merge 1 commit into
vintagepc:MINI404from
DRracer:feat/fan-always-emit-tach

Conversation

@DRracer

@DRracer DRracer commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • New always_emit_tach bool property on the fan device. When set, fan_tach_expire ignores tach_blocked and fan_pwm_change latches current_rpm at max_rpm once it has been above max_rpm/2.
  • Enabled only on fans[FAN_HBR] in prusa-mk4.c (print fan behaviour unchanged).
  • The standalone Fan Test still passes both phases because the print-fan-test phase runs with HBR PWM=0 (no pulses emitted regardless of the latch), and the HBR-100% phase reads cfg.f_rpms[1] = 7500, inside the FW's [6800, 8700] acceptance window.

Motivation

On the MK4 board, PF13 is multiplexed between the HBR and print fan tach lines (HBR.tach-disable <- PF13, PRINT.tach-disable <- PF13 inverted in prusa-mk4.c:920-934) and is also bit-banged by the at21csxx loveboard EEPROM. The firmware's Fans::tick() toggles PF13 at 1 kHz to scan both fans; on real silicon the mux is a passive input read by a glue chip, but in QEMU there is no HiZ — the GPIO holds whatever the FW last drove — so the sim's tach_blocked path masks the HBR tach roughly half the time during normal operation.

This is invisible during the standalone fan selftest (the FW puts the fan into selftest_mode, which short-circuits CFanCtlCommon::is_fan_ok()), but after the heater test, autoFan re-engages and the software_pwm device's 256-tick window averaging yields small-but-nonzero intermediate values during the PWM=0 → PWM=255 transition. is_fan_ok() treats zero RPM as OK (short-circuit) but small RPM as a failure when commanded PWM is up, firing WarningType::HotendFanError ("Hotend fan not spinning") about 25 s into the cool-down — which kills the rest of the selftest snake.

The proper long-term fix is probably to honour STM32 GPIOx_MODER bits so the mux-select GPIO models HiZ correctly. This latch is a targeted workaround that unblocks the full first-boot selftest in sim.

Test plan

  • Build clean on top of MINI404.
  • Standalone Fan Test still passes on prusa-mk4-027c with mk4_release_noboot.bin (Buddy 6.6.0+14739): Print 6552/2658 RPM in range, Heatbreak 7500 RPM in range at both 100% and 40% PWM phases.
  • No spurious HotendFanError after the heater test on the same FW build.
  • Full first-boot selftest snake runs to completion (Fans → XYCheck → ZAlign → Loadcell → ZCheck → Heaters → Gears [skipped] → FS calibration → home), exercised end-to-end via a harness walker. No set_warning(0) in the CDC log.

On the MK4 board, PF13 is multiplexed between the hotend (HBR) and print
fan tach lines (HBR.tach-disable <- PF13, PRINT.tach-disable <- PF13
inverted in prusa-mk4.c) and is also bit-banged by the at21csxx loveboard
EEPROM (read once at boot, then idle LOW). The firmware's Fans::tick()
toggles PF13 at 1 kHz to scan both fans; on real silicon the mux is a
passive input read by a glue chip, but in QEMU there is no HiZ — the
GPIO sticks at whatever the FW last drove — so the sim's tach_blocked
path masks the HBR tach roughly half the time during normal operation.

This is invisible during the standalone fan selftest (FW puts the fan
into selftest_mode, which short-circuits is_fan_ok()). After the heater
test, autoFan re-engages and the software_pwm device's 256-tick window
averaging yields small-but-nonzero intermediate values during the
PWM=0 → PWM=255 transition. is_fan_ok() treats *zero* RPM as OK
(short-circuit) but *small* RPM as a failure when commanded PWM is up,
firing WarningType::HotendFanError ("Hotend fan not spinning") about
25 s into the cool-down. That kills the rest of the selftest snake.

Add a per-device `always_emit_tach` bool. When set:

1. fan_tach_expire ignores tach_blocked — pulses always reach the GPIO.
2. fan_pwm_change latches current_rpm at max_rpm whenever the new
   computed value drops below max_rpm/2 AND the previous value was
   above max_rpm/2. So once the fan has spun up, it stays at full RPM
   through PWM transients (rotor-inertia approximation).

Enable on fans[FAN_HBR] in prusa-mk4.c; print fan is unchanged. The
standalone Fan Test still passes both phases: HBR-100% reads max_rpm
(= cfg.f_rpms[1] = 7500), inside the FW's [6800, 8700] window; HBR-40%
reads max_rpm, inside the wide [10, 10000] window. The print-fan tach
phase runs with HBR PWM=0, so HBR emits no pulses regardless of the
latch and doesn't contaminate the print-fan reading.

The proper long-term fix is to honour STM32 GPIOx_MODER bits so the
mux-select GPIO models HiZ correctly; this latch is a targeted
workaround that unblocks the full first-boot selftest in sim.

Verified end-to-end on Buddy 6.6.0+14739 with mk4_release_noboot.bin:
sim/harness/walk_full_selftest.py drives Fans -> XYCheck -> ZAlign ->
Loadcell -> ZCheck -> Heaters -> Gears (skipped) -> FS calibration to
the home screen, no spurious HotendFanError.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant