#include "fan.h" #include "stm32f7xx_hal.h" #include "config.h" #include /* ================================================================ * Fan Hardware Configuration * ================================================================ */ #define FAN_PIN GPIO_PIN_9 #define FAN_PORT GPIOA #define FAN_TIM TIM1 #define FAN_TIM_CHANNEL TIM_CHANNEL_2 #define FAN_PWM_FREQ_HZ 25000 /* 25 kHz for brushless fan */ /* ================================================================ * Temperature Curve Parameters * ================================================================ */ #define TEMP_OFF 40 /* Fan off below this (°C) */ #define TEMP_LOW 50 /* Low speed threshold (°C) */ #define TEMP_HIGH 70 /* High speed threshold (°C) */ #define SPEED_OFF 0 /* Speed at TEMP_OFF (%) */ #define SPEED_LOW 30 /* Speed at TEMP_LOW (%) */ #define SPEED_HIGH 100 /* Speed at TEMP_HIGH (%) */ /* ================================================================ * Internal State * ================================================================ */ typedef struct { uint8_t current_speed; /* Current speed 0-100% */ uint8_t target_speed; /* Target speed 0-100% */ int16_t last_temperature; /* Last temperature reading (°C) */ float ramp_rate_per_ms; /* Speed change rate (%/ms) */ uint32_t last_ramp_time_ms; /* When last ramp update occurred */ bool is_ramping; /* Speed is transitioning */ } FanState_t; static FanState_t s_fan = { .current_speed = 0, .target_speed = 0, .last_temperature = 0, .ramp_rate_per_ms = 0.05f, /* 5% per 100ms default */ .last_ramp_time_ms = 0, .is_ramping = false }; /* ================================================================ * Hardware Initialization * ================================================================ */ void fan_init(void) { /* Enable GPIO and timer clocks */ __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_TIM1_CLK_ENABLE(); /* Configure PA9 as TIM1_CH2 PWM output */ GPIO_InitTypeDef gpio_init = {0}; gpio_init.Pin = FAN_PIN; gpio_init.Mode = GPIO_MODE_AF_PP; gpio_init.Pull = GPIO_NOPULL; gpio_init.Speed = GPIO_SPEED_HIGH; gpio_init.Alternate = GPIO_AF1_TIM1; HAL_GPIO_Init(FAN_PORT, &gpio_init); /* Configure TIM1 for PWM: * Clock: 216MHz / PSC = output frequency * For 25kHz frequency: PSC = 346, ARR = 25 * Duty cycle = CCR / ARR (e.g., 12.5/25 = 50%) */ TIM_HandleTypeDef htim1 = {0}; htim1.Instance = FAN_TIM; htim1.Init.Prescaler = 346 - 1; /* 216MHz / 346 ≈ 624kHz clock */ htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 25 - 1; /* 624kHz / 25 = 25kHz */ htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; HAL_TIM_PWM_Init(&htim1); /* Configure PWM on CH2: 0% duty initially (fan off) */ TIM_OC_InitTypeDef oc_init = {0}; oc_init.OCMode = TIM_OCMODE_PWM1; oc_init.Pulse = 0; /* Start at 0% duty (off) */ oc_init.OCPolarity = TIM_OCPOLARITY_HIGH; oc_init.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim1, &oc_init, FAN_TIM_CHANNEL); /* Start PWM generation */ HAL_TIM_PWM_Start(FAN_TIM, FAN_TIM_CHANNEL); s_fan.current_speed = 0; s_fan.target_speed = 0; s_fan.last_ramp_time_ms = 0; } /* ================================================================ * Temperature Curve Calculation * ================================================================ */ static uint8_t fan_calculate_speed_from_temp(int16_t temp_celsius) { if (temp_celsius < TEMP_OFF) { return SPEED_OFF; /* Off below 40°C */ } if (temp_celsius < TEMP_LOW) { /* Linear ramp from 0% to 30% between 40-50°C */ int32_t temp_offset = temp_celsius - TEMP_OFF; /* 0-10 */ int32_t temp_range = TEMP_LOW - TEMP_OFF; /* 10 */ int32_t speed_range = SPEED_LOW - SPEED_OFF; /* 30 */ uint8_t speed = SPEED_OFF + (temp_offset * speed_range) / temp_range; return (speed > 100) ? 100 : speed; } if (temp_celsius < TEMP_HIGH) { /* Linear ramp from 30% to 100% between 50-70°C */ int32_t temp_offset = temp_celsius - TEMP_LOW; /* 0-20 */ int32_t temp_range = TEMP_HIGH - TEMP_LOW; /* 20 */ int32_t speed_range = SPEED_HIGH - SPEED_LOW; /* 70 */ uint8_t speed = SPEED_LOW + (temp_offset * speed_range) / temp_range; return (speed > 100) ? 100 : speed; } return SPEED_HIGH; /* 100% at 70°C and above */ } /* ================================================================ * PWM Duty Cycle Control * ================================================================ */ static void fan_set_pwm_duty(uint8_t percentage) { /* Clamp to 0-100% */ if (percentage > 100) percentage = 100; /* Convert percentage to PWM counts * ARR = 25 (0-24 counts for 0-96%, scale up to 25 for 100%) * Duty = (percentage * 25) / 100 */ uint32_t duty = (percentage * 25) / 100; if (duty > 25) duty = 25; /* Update CCR2 for TIM1_CH2 */ TIM1->CCR2 = duty; } /* ================================================================ * Public API * ================================================================ */ bool fan_set_speed(uint8_t percentage) { if (percentage > 100) { return false; } s_fan.current_speed = percentage; s_fan.target_speed = percentage; s_fan.is_ramping = false; fan_set_pwm_duty(percentage); return true; } uint8_t fan_get_speed(void) { return s_fan.current_speed; } bool fan_set_target_speed(uint8_t percentage) { if (percentage > 100) { return false; } s_fan.target_speed = percentage; if (percentage == s_fan.current_speed) { s_fan.is_ramping = false; } else { s_fan.is_ramping = true; } return true; } void fan_update_temperature(int16_t temp_celsius) { s_fan.last_temperature = temp_celsius; /* Calculate target speed from temperature curve */ uint8_t new_target = fan_calculate_speed_from_temp(temp_celsius); fan_set_target_speed(new_target); } int16_t fan_get_temperature(void) { return s_fan.last_temperature; } FanState fan_get_state(void) { if (s_fan.current_speed == 0) return FAN_OFF; if (s_fan.current_speed <= 30) return FAN_LOW; if (s_fan.current_speed <= 60) return FAN_MEDIUM; if (s_fan.current_speed <= 99) return FAN_HIGH; return FAN_FULL; } void fan_set_ramp_rate(float percentage_per_ms) { if (percentage_per_ms <= 0) { s_fan.ramp_rate_per_ms = 0.01f; /* Minimum rate */ } else if (percentage_per_ms > 10.0f) { s_fan.ramp_rate_per_ms = 10.0f; /* Maximum rate */ } else { s_fan.ramp_rate_per_ms = percentage_per_ms; } } bool fan_is_ramping(void) { return s_fan.is_ramping; } void fan_tick(uint32_t now_ms) { if (!s_fan.is_ramping) { return; } /* Calculate time elapsed since last ramp */ if (s_fan.last_ramp_time_ms == 0) { s_fan.last_ramp_time_ms = now_ms; return; } uint32_t elapsed = now_ms - s_fan.last_ramp_time_ms; if (elapsed == 0) { return; /* No time has passed */ } /* Calculate speed change allowed in this time interval */ float speed_change = s_fan.ramp_rate_per_ms * elapsed; int32_t new_speed; if (s_fan.target_speed > s_fan.current_speed) { /* Ramp up */ new_speed = s_fan.current_speed + (int32_t)speed_change; if (new_speed >= s_fan.target_speed) { s_fan.current_speed = s_fan.target_speed; s_fan.is_ramping = false; } else { s_fan.current_speed = (uint8_t)new_speed; } } else { /* Ramp down */ new_speed = s_fan.current_speed - (int32_t)speed_change; if (new_speed <= s_fan.target_speed) { s_fan.current_speed = s_fan.target_speed; s_fan.is_ramping = false; } else { s_fan.current_speed = (uint8_t)new_speed; } } /* Update PWM duty cycle */ fan_set_pwm_duty(s_fan.current_speed); s_fan.last_ramp_time_ms = now_ms; } void fan_disable(void) { fan_set_speed(0); }