The “flutter” you’re seeing is the classic threshold-boundary oscillation of a polling user-space script: the shell reads the temperature every few seconds and writes a PWM value, but with a single trip point the fan flips every time the junction temperature crosses it. During an OpenCV build the big cores alternate between boost and idle, so the temperature naturally oscillates around 40 °C — and the fan follows every single crossing.
Two ways to fix it, depending on how much you want to touch the system:
1. Quick fix — add hysteresis to your fan-control.sh
Keep the systemd service, but make the transitions asymmetric so the fan only changes speed after the temperature has moved a margin past the threshold (plus a small debounce). Multi-level version:
bash
#!/bin/bash
# /usr/local/bin/fan-control.sh — multi-level + 5°C hysteresis
ZONE=/sys/class/thermal/thermal_zone1/temp # big core (compile-heavy)
PWM=/sys/class/hwmon/hwmon6/pwm1
POLL=2
RISE=(55 65 75) # °C where each level engages
PWMV=(64 128 200) # 25% / 50% / 78% duty
HYST=5 # °C hysteresis on the way down
cur=0
while true; do
t=$(($(cat "$ZONE") / 1000))
if [ "$cur" -lt 2 ] && [ "$t" -ge "${RISE[$cur]}" ]; then
cur=$((cur+1))
elif [ "$cur" -gt 0 ] && [ "$t" -le "$(( ${RISE[$((cur-1))]} - HYST ))" ]; then
cur=$((cur-1))
fi
echo "${PWMV[$cur]}" > "$PWM"
sleep "$POLL"
done
With this, a temperature hovering at 40 °C keeps the fan at its current level until the SoC moves 5 °C away from the threshold — no more 50↔100 bouncing every 5 s. Pick the zone that reflects your load: thermal_zone1 (big core) for CPU builds; for mixed workloads you can use the max of zone0 (SoC) and zone1.
2. Proper fix — thermal-zones + pwm-fan in the DT
As loveforcircuits mentioned, Armbian’s RK3588 DTs already model this correctly: cooling-levels on the pwm-fan node plus a multi-trip cooling-map with hysteresis on the thermal zone. A minimal RK3576 equivalent:
dts
&pwm_fan {
cooling-levels = <0 64 128 200 255>;
#cooling-cells = <2>;
};
&thermal_zones {
soc_thermal {
trips {
soc_alert1: soc-alert1 { temperature = <50000>; hysteresis = <5000>; type = "active"; };
soc_alert2: soc-alert2 { temperature = <65000>; hysteresis = <5000>; type = "active"; };
soc_alert3: soc-alert3 { temperature = <80000>; hysteresis = <3000>; type = "active"; };
};
cooling-maps {
map1 { trip = <&soc_alert1>; cooling-device = <&pwm_fan 1 1>; };
map2 { trip = <&soc_alert2>; cooling-device = <&pwm_fan 2 2>; };
map3 { trip = <&soc_alert3>; cooling-device = <&pwm_fan 3 3>; };
};
};
};
The kernel thermal framework then steps the PWM levels with proper hysteresis instead of a binary on/off, with no userspace dependency — and the same behavior carries over to RK3588/RK3576 boards in the Armbian tree. Happy to help draft a full patch against the reComputer DT if you want to push it upstream.
One reliability note from the industrial side: this matters more than it looks. In 24/7 devices (digital signage, payment terminals, industrial HMI), a fan that re-accelerates every few seconds wears its bearings out much faster — industrial fans are typically rated for tens of thousands of hours at constant speed. Also keep in mind that 40 °C is a common ambient temperature inside enclosures, so tune the trip points against box temperature, not just the SoC reading.
Hope that helps!