Change fan to use pure pwm (#2267)

* set cuatro fan settings

* set cuatro to use pure pwm

* use pwm on all boards

* remove fan max rpm configs, use pure pwm

* add comment about cooldown

* change max rpm in python side

* remove reference to max_fan_pwm in fan.h

* fully refactor fan configs into "has_fan"

* increase limits on fan test to 15% either way

* only tres for now

* doesn't matter

* not relevant anymore

---------

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
This commit is contained in:
probablyanasian
2025-09-13 11:09:51 -07:00
committed by GitHub
parent d555c102bb
commit a2064b86f3
9 changed files with 23 additions and 52 deletions

View File

@@ -29,10 +29,9 @@ struct board {
const uint8_t led_pin[3];
const uint8_t led_pwm_channels[3]; // leave at 0 to disable PWM
const bool has_spi;
const uint16_t fan_max_rpm;
const bool has_fan;
const uint16_t avdd_mV;
const uint8_t fan_enable_cooldown_time;
const uint8_t fan_max_pwm;
board_init init;
board_init_bootloader init_bootloader;
board_enable_can_transceiver enable_can_transceiver;

View File

@@ -132,8 +132,7 @@ static harness_configuration cuatro_harness_config = {
board board_cuatro = {
.harness_config = &cuatro_harness_config,
.has_spi = true,
.fan_max_rpm = 12500U,
.fan_max_pwm = 99U, // it can go up to 14k RPM, but 99% -> 100% is very non-linear
.has_fan = true,
.avdd_mV = 1800U,
.fan_enable_cooldown_time = 3U,
.init = cuatro_init,

View File

@@ -115,8 +115,7 @@ board board_red = {
.set_bootkick = unused_set_bootkick,
.harness_config = &red_harness_config,
.has_spi = false,
.fan_max_rpm = 0U,
.fan_max_pwm = 100U,
.has_fan = false,
.avdd_mV = 3300U,
.fan_enable_cooldown_time = 0U,
.init = red_init,

View File

@@ -155,8 +155,7 @@ static harness_configuration tres_harness_config = {
board board_tres = {
.harness_config = &tres_harness_config,
.has_spi = true,
.fan_max_rpm = 6600U,
.fan_max_pwm = 100U,
.has_fan = true,
.avdd_mV = 1800U,
.fan_enable_cooldown_time = 3U,
.init = tres_init,

View File

@@ -5,10 +5,13 @@ struct fan_state_t fan_state;
static const uint8_t FAN_TICK_FREQ = 8U;
void fan_set_power(uint8_t percentage) {
fan_state.target_rpm = ((current_board->fan_max_rpm * CLAMP(percentage, 0U, 100U)) / 100U);
if (percentage > 0U) {
fan_state.power = CLAMP(percentage, 20U, 100U);
} else {
fan_state.power = 0U;
}
}
void llfan_init(void);
void fan_init(void) {
fan_state.cooldown_counter = current_board->fan_enable_cooldown_time * FAN_TICK_FREQ;
llfan_init();
@@ -16,9 +19,7 @@ void fan_init(void) {
// Call this at FAN_TICK_FREQ
void fan_tick(void) {
const float FAN_I = 6.5f;
if (current_board->fan_max_rpm > 0U) {
if (current_board->has_fan) {
// Measure fan RPM
uint16_t fan_rpm_fast = fan_state.tach_counter * (60U * FAN_TICK_FREQ / 4U); // 4 interrupts per rotation
fan_state.tach_counter = 0U;
@@ -31,8 +32,8 @@ void fan_tick(void) {
print("\n");
#endif
// Cooldown counter
if (fan_state.target_rpm > 0U) {
// Cooldown counter to prevent noise on tachometer line.
if (fan_state.power > 0U) {
fan_state.cooldown_counter = current_board->fan_enable_cooldown_time * FAN_TICK_FREQ;
} else {
if (fan_state.cooldown_counter > 0U) {
@@ -40,18 +41,8 @@ void fan_tick(void) {
}
}
// Update controller
if (fan_state.target_rpm == 0U) {
fan_state.error_integral = 0.0f;
} else {
float error = (fan_state.target_rpm - fan_rpm_fast) / ((float) current_board->fan_max_rpm);
fan_state.error_integral += FAN_I * error;
}
fan_state.error_integral = CLAMP(fan_state.error_integral, 0U, current_board->fan_max_pwm);
fan_state.power = fan_state.error_integral;
// Set PWM and enable line
pwm_set(TIM3, 3, fan_state.power);
current_board->set_fan_enabled((fan_state.target_rpm > 0U) || (fan_state.cooldown_counter > 0U));
current_board->set_fan_enabled((fan_state.power > 0U) || (fan_state.cooldown_counter > 0U));
}
}

View File

@@ -3,7 +3,6 @@
struct fan_state_t {
uint16_t tach_counter;
uint16_t rpm;
uint16_t target_rpm;
uint8_t power;
float error_integral;
uint8_t cooldown_counter;

View File

@@ -294,7 +294,7 @@ int main(void) {
microsecond_timer_init();
current_board->set_siren(false);
if (current_board->fan_max_rpm > 0U) {
if (current_board->has_fan) {
fan_init();
}

View File

@@ -131,7 +131,7 @@ class Panda:
MAX_FAN_RPMs = {
HW_TYPE_TRES: 6600,
HW_TYPE_CUATRO: 12500,
HW_TYPE_CUATRO: 5000,
}
HARNESS_STATUS_NC = 0

View File

@@ -4,18 +4,21 @@ import pytest
from panda import Panda
pytestmark = [
pytest.mark.test_panda_types(Panda.INTERNAL_DEVICES)
# TODO: re-enable this once we update the HITL devices
#pytest.mark.test_panda_types(Panda.INTERNAL_DEVICES)
pytest.mark.test_panda_types([Panda.HW_TYPE_TRES])
]
@pytest.mark.timeout(2*60)
def test_fan_controller(p):
def test_fan_curve(p):
# ensure fan curve is (roughly) linear
for power in (30, 50, 80, 100):
p.set_fan_power(0)
while p.get_fan_rpm() > 0:
time.sleep(0.1)
# wait until fan spins up (and recovers if needed),
# then wait a bit more for the RPM to converge
# wait until fan spins up, then wait a bit more for the RPM to converge
p.set_fan_power(power)
for _ in range(20):
time.sleep(1)
@@ -24,7 +27,7 @@ def test_fan_controller(p):
time.sleep(5)
expected_rpm = Panda.MAX_FAN_RPMs[bytes(p.get_type())] * power / 100
assert 0.9 * expected_rpm <= p.get_fan_rpm() <= 1.1 * expected_rpm
assert 0.75 * expected_rpm <= p.get_fan_rpm() <= 1.25 * expected_rpm
def test_fan_cooldown(p):
# if the fan cooldown doesn't work, we get high frequency noise on the tach line
@@ -35,21 +38,3 @@ def test_fan_cooldown(p):
for _ in range(5):
assert p.get_fan_rpm() <= Panda.MAX_FAN_RPMs[bytes(p.get_type())]
time.sleep(0.5)
def test_fan_overshoot(p):
# make sure it's stopped completely
p.set_fan_power(0)
while p.get_fan_rpm() > 0:
time.sleep(0.1)
# set it to 30% power to mimic going onroad
p.set_fan_power(30)
max_rpm = 0
for _ in range(50):
max_rpm = max(max_rpm, p.get_fan_rpm())
time.sleep(0.1)
# tolerate 10% overshoot
expected_rpm = Panda.MAX_FAN_RPMs[bytes(p.get_type())] * 30 / 100
assert max_rpm <= 1.1 * expected_rpm, f"Fan overshoot: {(max_rpm / expected_rpm * 100) - 100:.1f}%"