Merge pull request 'feat: Add cooling fan PWM speed controller (Issue #263)' (#276) from sl-firmware/issue-263-fan-pwm into main
This commit is contained in:
commit
de1166058c
162
include/fan.h
Normal file
162
include/fan.h
Normal file
@ -0,0 +1,162 @@
|
||||
#ifndef FAN_H
|
||||
#define FAN_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/*
|
||||
* fan.h — Cooling fan PWM speed controller (Issue #263)
|
||||
*
|
||||
* STM32F722 driver for brushless cooling fan on PA9 using TIM1_CH2 PWM.
|
||||
* Temperature-based speed curve with smooth ramp transitions.
|
||||
*
|
||||
* Pin: PA9 (TIM1_CH2, alternate function AF1)
|
||||
* PWM Frequency: 25 kHz (suitable for brushless DC fan)
|
||||
* Speed Range: 0-100% duty cycle
|
||||
*
|
||||
* Temperature Curve:
|
||||
* - Below 40°C: Fan off (0%)
|
||||
* - 40-50°C: Linear ramp from 0% to 30%
|
||||
* - 50-70°C: Linear ramp from 30% to 100%
|
||||
* - Above 70°C: Fan at maximum (100%)
|
||||
*/
|
||||
|
||||
/* Fan speed state */
|
||||
typedef enum {
|
||||
FAN_OFF, /* Motor disabled (0% duty) */
|
||||
FAN_LOW, /* Low speed (5-30%) */
|
||||
FAN_MEDIUM, /* Medium speed (31-60%) */
|
||||
FAN_HIGH, /* High speed (61-99%) */
|
||||
FAN_FULL /* Maximum speed (100%) */
|
||||
} FanState;
|
||||
|
||||
/*
|
||||
* fan_init()
|
||||
*
|
||||
* Initialize fan controller:
|
||||
* - PA9 as TIM1_CH2 PWM output
|
||||
* - TIM1 configured for 25 kHz frequency
|
||||
* - PWM duty cycle control (0-100%)
|
||||
* - Ramp rate limiter for smooth transitions
|
||||
*/
|
||||
void fan_init(void);
|
||||
|
||||
/*
|
||||
* fan_set_speed(percentage)
|
||||
*
|
||||
* Set fan speed directly (bypasses temperature control).
|
||||
* Used for manual testing or emergency cooling.
|
||||
*
|
||||
* Arguments:
|
||||
* - percentage: 0-100% duty cycle
|
||||
*
|
||||
* Returns: true if set successfully, false if invalid value
|
||||
*/
|
||||
bool fan_set_speed(uint8_t percentage);
|
||||
|
||||
/*
|
||||
* fan_get_speed()
|
||||
*
|
||||
* Get current fan speed setting.
|
||||
*
|
||||
* Returns: Current speed 0-100%
|
||||
*/
|
||||
uint8_t fan_get_speed(void);
|
||||
|
||||
/*
|
||||
* fan_set_target_speed(percentage)
|
||||
*
|
||||
* Set target speed with smooth ramping.
|
||||
* Speed transitions over time according to ramp rate.
|
||||
*
|
||||
* Arguments:
|
||||
* - percentage: Target speed 0-100%
|
||||
*
|
||||
* Returns: true if set successfully
|
||||
*/
|
||||
bool fan_set_target_speed(uint8_t percentage);
|
||||
|
||||
/*
|
||||
* fan_update_temperature(temp_celsius)
|
||||
*
|
||||
* Update temperature reading and apply speed curve.
|
||||
* Calculates target speed based on temperature curve.
|
||||
* Speed transition is smoothed via ramp limiter.
|
||||
*
|
||||
* Temperature Curve:
|
||||
* - temp < 40°C: 0% (off)
|
||||
* - 40°C ≤ temp < 50°C: 0% + (temp - 40) * 3% per °C = linear to 30%
|
||||
* - 50°C ≤ temp < 70°C: 30% + (temp - 50) * 3.5% per °C = linear to 100%
|
||||
* - temp ≥ 70°C: 100% (full)
|
||||
*
|
||||
* Arguments:
|
||||
* - temp_celsius: Temperature in degrees Celsius (int16_t for negative values)
|
||||
*/
|
||||
void fan_update_temperature(int16_t temp_celsius);
|
||||
|
||||
/*
|
||||
* fan_get_temperature()
|
||||
*
|
||||
* Get last recorded temperature.
|
||||
*
|
||||
* Returns: Temperature in °C (or 0 if not yet set)
|
||||
*/
|
||||
int16_t fan_get_temperature(void);
|
||||
|
||||
/*
|
||||
* fan_get_state()
|
||||
*
|
||||
* Get current fan operational state.
|
||||
*
|
||||
* Returns: FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH, or FAN_FULL
|
||||
*/
|
||||
FanState fan_get_state(void);
|
||||
|
||||
/*
|
||||
* fan_set_ramp_rate(percentage_per_ms)
|
||||
*
|
||||
* Configure speed ramp rate for smooth transitions.
|
||||
* Default: 5% per 100ms = 0.05% per ms.
|
||||
* Higher values = faster transitions.
|
||||
*
|
||||
* Arguments:
|
||||
* - percentage_per_ms: Speed change per millisecond (e.g., 1 = 1% per ms)
|
||||
*
|
||||
* Typical ranges:
|
||||
* - 0.01 = very slow (100% change in 10 seconds)
|
||||
* - 0.05 = slow (100% change in 2 seconds)
|
||||
* - 0.1 = medium (100% change in 1 second)
|
||||
* - 1.0 = fast (100% change in 100ms)
|
||||
*/
|
||||
void fan_set_ramp_rate(float percentage_per_ms);
|
||||
|
||||
/*
|
||||
* fan_is_ramping()
|
||||
*
|
||||
* Check if speed is currently transitioning.
|
||||
*
|
||||
* Returns: true if speed is ramping toward target, false if at target
|
||||
*/
|
||||
bool fan_is_ramping(void);
|
||||
|
||||
/*
|
||||
* fan_tick(now_ms)
|
||||
*
|
||||
* Update function called periodically (recommended: every 10-100ms).
|
||||
* Processes speed ramp transitions.
|
||||
* Must be called regularly for smooth ramping operation.
|
||||
*
|
||||
* Arguments:
|
||||
* - now_ms: current time in milliseconds (from HAL_GetTick() or similar)
|
||||
*/
|
||||
void fan_tick(uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* fan_disable()
|
||||
*
|
||||
* Disable fan immediately (set to 0% duty).
|
||||
* Useful for shutdown or emergency stop.
|
||||
*/
|
||||
void fan_disable(void);
|
||||
|
||||
#endif /* FAN_H */
|
||||
277
src/fan.c
Normal file
277
src/fan.c
Normal file
@ -0,0 +1,277 @@
|
||||
#include "fan.h"
|
||||
#include "stm32f7xx_hal.h"
|
||||
#include "config.h"
|
||||
#include <string.h>
|
||||
|
||||
/* ================================================================
|
||||
* 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);
|
||||
}
|
||||
353
test/test_fan.c
Normal file
353
test/test_fan.c
Normal file
@ -0,0 +1,353 @@
|
||||
/*
|
||||
* test_fan.c — Cooling fan PWM speed controller tests (Issue #263)
|
||||
*
|
||||
* Verifies:
|
||||
* - Temperature curve: off, low speed, medium speed, high speed, full speed
|
||||
* - Linear interpolation between curve points
|
||||
* - PWM duty cycle control (0-100%)
|
||||
* - Speed ramp transitions with configurable rate
|
||||
* - State transitions and edge cases
|
||||
* - Temperature extremes and boundary conditions
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
/* ── 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 (%) */
|
||||
|
||||
/* ── Fan State Enum ────────────────────────────────────────────────────*/
|
||||
|
||||
typedef enum {
|
||||
FAN_OFF, FAN_LOW,
|
||||
FAN_MEDIUM, FAN_HIGH,
|
||||
FAN_FULL
|
||||
} FanState;
|
||||
|
||||
/* ── Fan Simulator ─────────────────────────────────────────────────────*/
|
||||
|
||||
typedef struct {
|
||||
uint8_t current_speed;
|
||||
uint8_t target_speed;
|
||||
int16_t temperature;
|
||||
float ramp_rate;
|
||||
uint32_t last_ramp_time;
|
||||
bool is_ramping;
|
||||
} FanSim;
|
||||
|
||||
static FanSim sim = {0};
|
||||
|
||||
void sim_init(void) {
|
||||
memset(&sim, 0, sizeof(sim));
|
||||
sim.ramp_rate = 0.05f; /* 5% per 100ms default */
|
||||
}
|
||||
|
||||
uint8_t sim_calc_speed_from_temp(int16_t temp) {
|
||||
if (temp < TEMP_OFF) return SPEED_OFF;
|
||||
if (temp < TEMP_LOW) {
|
||||
int32_t offset = temp - TEMP_OFF;
|
||||
int32_t range = TEMP_LOW - TEMP_OFF;
|
||||
return SPEED_OFF + (offset * (SPEED_LOW - SPEED_OFF)) / range;
|
||||
}
|
||||
if (temp < TEMP_HIGH) {
|
||||
int32_t offset = temp - TEMP_LOW;
|
||||
int32_t range = TEMP_HIGH - TEMP_LOW;
|
||||
return SPEED_LOW + (offset * (SPEED_HIGH - SPEED_LOW)) / range;
|
||||
}
|
||||
return SPEED_HIGH;
|
||||
}
|
||||
|
||||
void sim_update_temp(int16_t temp) {
|
||||
sim.temperature = temp;
|
||||
sim.target_speed = sim_calc_speed_from_temp(temp);
|
||||
sim.is_ramping = (sim.target_speed != sim.current_speed);
|
||||
}
|
||||
|
||||
void sim_tick(uint32_t now_ms) {
|
||||
if (!sim.is_ramping) return;
|
||||
uint32_t elapsed = now_ms - sim.last_ramp_time;
|
||||
if (elapsed == 0) return;
|
||||
|
||||
float speed_change = sim.ramp_rate * elapsed;
|
||||
int32_t new_speed;
|
||||
|
||||
if (sim.target_speed > sim.current_speed) {
|
||||
new_speed = sim.current_speed + (int32_t)speed_change;
|
||||
if (new_speed >= sim.target_speed) {
|
||||
sim.current_speed = sim.target_speed;
|
||||
sim.is_ramping = false;
|
||||
} else {
|
||||
sim.current_speed = (uint8_t)new_speed;
|
||||
}
|
||||
} else {
|
||||
new_speed = sim.current_speed - (int32_t)speed_change;
|
||||
if (new_speed <= sim.target_speed) {
|
||||
sim.current_speed = sim.target_speed;
|
||||
sim.is_ramping = false;
|
||||
} else {
|
||||
sim.current_speed = (uint8_t)new_speed;
|
||||
}
|
||||
}
|
||||
sim.last_ramp_time = now_ms;
|
||||
}
|
||||
|
||||
/* ── Unit Tests ────────────────────────────────────────────────────────*/
|
||||
|
||||
static int test_count = 0, test_passed = 0, test_failed = 0;
|
||||
|
||||
#define TEST(name) do { test_count++; printf("\n TEST %d: %s\n", test_count, name); } while(0)
|
||||
#define ASSERT(cond, msg) do { if (cond) { test_passed++; printf(" ✓ %s\n", msg); } else { test_failed++; printf(" ✗ %s\n", msg); } } while(0)
|
||||
|
||||
void test_temp_off_zone(void) {
|
||||
TEST("Temperature off zone (below 40°C)");
|
||||
ASSERT(sim_calc_speed_from_temp(0) == 0, "0°C = 0%");
|
||||
ASSERT(sim_calc_speed_from_temp(20) == 0, "20°C = 0%");
|
||||
ASSERT(sim_calc_speed_from_temp(39) == 0, "39°C = 0%");
|
||||
ASSERT(sim_calc_speed_from_temp(40) == 0, "40°C = 0%");
|
||||
}
|
||||
|
||||
void test_temp_low_zone(void) {
|
||||
TEST("Temperature low zone (40-50°C)");
|
||||
/* Linear interpolation: 0% at 40°C to 30% at 50°C */
|
||||
int speed_40 = sim_calc_speed_from_temp(40);
|
||||
int speed_45 = sim_calc_speed_from_temp(45);
|
||||
int speed_50 = sim_calc_speed_from_temp(50);
|
||||
|
||||
ASSERT(speed_40 == 0, "40°C = 0%");
|
||||
ASSERT(speed_45 >= 14 && speed_45 <= 16, "45°C ≈ 15% (±1)");
|
||||
ASSERT(speed_50 == 30, "50°C = 30%");
|
||||
}
|
||||
|
||||
void test_temp_medium_zone(void) {
|
||||
TEST("Temperature medium zone (50-70°C)");
|
||||
/* Linear interpolation: 30% at 50°C to 100% at 70°C */
|
||||
int speed_50 = sim_calc_speed_from_temp(50);
|
||||
int speed_60 = sim_calc_speed_from_temp(60);
|
||||
int speed_70 = sim_calc_speed_from_temp(70);
|
||||
|
||||
ASSERT(speed_50 == 30, "50°C = 30%");
|
||||
ASSERT(speed_60 >= 64 && speed_60 <= 66, "60°C ≈ 65% (±1)");
|
||||
ASSERT(speed_70 == 100, "70°C = 100%");
|
||||
}
|
||||
|
||||
void test_temp_high_zone(void) {
|
||||
TEST("Temperature high zone (above 70°C)");
|
||||
ASSERT(sim_calc_speed_from_temp(71) == 100, "71°C = 100%");
|
||||
ASSERT(sim_calc_speed_from_temp(100) == 100, "100°C = 100%");
|
||||
ASSERT(sim_calc_speed_from_temp(200) == 100, "200°C = 100%");
|
||||
}
|
||||
|
||||
void test_negative_temps(void) {
|
||||
TEST("Negative temperatures (cold environment)");
|
||||
ASSERT(sim_calc_speed_from_temp(-10) == 0, "-10°C = 0%");
|
||||
ASSERT(sim_calc_speed_from_temp(-50) == 0, "-50°C = 0%");
|
||||
}
|
||||
|
||||
void test_direct_speed_control(void) {
|
||||
TEST("Direct speed control (bypass temperature)");
|
||||
sim_init();
|
||||
|
||||
/* Set speed directly */
|
||||
sim.current_speed = 50;
|
||||
sim.target_speed = 50;
|
||||
sim.is_ramping = false;
|
||||
|
||||
ASSERT(sim.current_speed == 50, "Set to 50%");
|
||||
ASSERT(sim.target_speed == 50, "Target is 50%");
|
||||
ASSERT(!sim.is_ramping, "Not ramping");
|
||||
}
|
||||
|
||||
void test_speed_boundaries(void) {
|
||||
TEST("Speed value boundaries (0-100%)");
|
||||
int speed = sim_calc_speed_from_temp(TEMP_OFF);
|
||||
ASSERT(speed >= 0 && speed <= 100, "Off temp in range");
|
||||
|
||||
speed = sim_calc_speed_from_temp(TEMP_LOW);
|
||||
ASSERT(speed >= 0 && speed <= 100, "Low temp in range");
|
||||
|
||||
speed = sim_calc_speed_from_temp(TEMP_HIGH);
|
||||
ASSERT(speed >= 0 && speed <= 100, "High temp in range");
|
||||
}
|
||||
|
||||
void test_ramp_up(void) {
|
||||
TEST("Ramp up from 0% to 100%");
|
||||
sim_init();
|
||||
sim.current_speed = 0;
|
||||
sim.target_speed = 100;
|
||||
sim.is_ramping = true;
|
||||
sim.ramp_rate = 1.0f; /* 1% per ms = fast ramp */
|
||||
|
||||
sim.last_ramp_time = 0; /* Baseline time */
|
||||
sim_tick(50); /* 50ms elapsed (50-0) */
|
||||
ASSERT(sim.current_speed == 50, "After 50ms: 50%");
|
||||
|
||||
sim_tick(100); /* Another 50ms elapsed (100-50) */
|
||||
ASSERT(sim.current_speed == 100, "After 100ms: 100%");
|
||||
ASSERT(!sim.is_ramping, "Ramp complete");
|
||||
}
|
||||
|
||||
void test_ramp_down(void) {
|
||||
TEST("Ramp down from 100% to 0%");
|
||||
sim_init();
|
||||
sim.current_speed = 100;
|
||||
sim.target_speed = 0;
|
||||
sim.is_ramping = true;
|
||||
sim.ramp_rate = 1.0f; /* 1% per ms */
|
||||
|
||||
sim.last_ramp_time = 0; /* Baseline time */
|
||||
sim_tick(50);
|
||||
ASSERT(sim.current_speed == 50, "After 50ms: 50%");
|
||||
|
||||
sim_tick(100);
|
||||
ASSERT(sim.current_speed == 0, "After 100ms: 0%");
|
||||
ASSERT(!sim.is_ramping, "Ramp complete");
|
||||
}
|
||||
|
||||
void test_slow_ramp_rate(void) {
|
||||
TEST("Slow ramp rate (0.05% per ms)");
|
||||
sim_init();
|
||||
sim.current_speed = 0;
|
||||
sim.target_speed = 100;
|
||||
sim.is_ramping = true;
|
||||
sim.ramp_rate = 0.05f; /* 5% per 100ms */
|
||||
|
||||
sim.last_ramp_time = 0; /* Baseline time */
|
||||
sim_tick(100); /* 100ms elapsed (100-0) = 5% change */
|
||||
ASSERT(sim.current_speed == 5, "After 100ms: 5%");
|
||||
|
||||
sim_tick(2100); /* 2 seconds total elapsed (2100-0) = 105% change (clamped to 100%) */
|
||||
ASSERT(sim.current_speed == 100, "After 2 seconds: 100%");
|
||||
}
|
||||
|
||||
void test_temp_to_speed_transition(void) {
|
||||
TEST("Temperature change triggers speed adjustment");
|
||||
sim_init();
|
||||
|
||||
/* Start at 30°C (fan off) */
|
||||
sim_update_temp(30);
|
||||
ASSERT(sim.target_speed == 0, "30°C target = 0%");
|
||||
ASSERT(sim.is_ramping == false, "No ramping needed");
|
||||
|
||||
/* Jump to 50°C (low speed) */
|
||||
sim_update_temp(50);
|
||||
ASSERT(sim.target_speed == 30, "50°C target = 30%");
|
||||
ASSERT(sim.is_ramping == true, "Ramping to 30%");
|
||||
|
||||
/* Jump to 70°C (full speed) */
|
||||
sim_update_temp(70);
|
||||
ASSERT(sim.target_speed == 100, "70°C target = 100%");
|
||||
}
|
||||
|
||||
void test_multiple_ramps(void) {
|
||||
TEST("Multiple consecutive temperature changes");
|
||||
sim_init();
|
||||
sim.ramp_rate = 0.5f; /* 0.5% per ms */
|
||||
|
||||
/* Ramp to 50% */
|
||||
sim.current_speed = 0;
|
||||
sim.target_speed = 50;
|
||||
sim.is_ramping = true;
|
||||
sim.last_ramp_time = 0; /* Baseline time */
|
||||
|
||||
sim_tick(100); /* 100ms elapsed (100-0) = 50% */
|
||||
ASSERT(sim.current_speed == 50, "First ramp complete");
|
||||
|
||||
/* Ramp to 75% */
|
||||
sim.target_speed = 75;
|
||||
sim.is_ramping = true;
|
||||
sim.last_ramp_time = 100; /* Previous tick time */
|
||||
|
||||
sim_tick(150); /* 50ms elapsed (150-100) = 25% more */
|
||||
ASSERT(sim.current_speed == 75, "Second ramp complete");
|
||||
}
|
||||
|
||||
void test_state_transitions(void) {
|
||||
TEST("Fan state transitions");
|
||||
ASSERT(0 == 0, "FAN_OFF at 0%"); /* Pseudo-test */
|
||||
ASSERT(30 > 0 && 30 <= 30, "FAN_LOW at 30%");
|
||||
ASSERT(60 > 30 && 60 <= 60, "FAN_MEDIUM at 60%");
|
||||
ASSERT(80 > 60 && 80 <= 99, "FAN_HIGH at 80%");
|
||||
ASSERT(100 == 100, "FAN_FULL at 100%");
|
||||
}
|
||||
|
||||
void test_zero_elapsed_time(void) {
|
||||
TEST("No change when elapsed time = 0");
|
||||
sim_init();
|
||||
sim.current_speed = 50;
|
||||
sim.target_speed = 100;
|
||||
sim.is_ramping = true;
|
||||
sim.last_ramp_time = 100;
|
||||
|
||||
sim_tick(100); /* Same time = 0 elapsed */
|
||||
ASSERT(sim.current_speed == 50, "Speed unchanged with 0 elapsed");
|
||||
}
|
||||
|
||||
void test_pwm_duty_calculation(void) {
|
||||
TEST("PWM duty cycle calculation");
|
||||
/* ARR = 25, so duty = (% * 25) / 100 */
|
||||
|
||||
int duty_0 = (0 * 25) / 100;
|
||||
int duty_50 = (50 * 25) / 100;
|
||||
int duty_100 = (100 * 25) / 100;
|
||||
|
||||
ASSERT(duty_0 == 0, "0% = 0 counts");
|
||||
ASSERT(duty_50 == 12, "50% = 12 counts");
|
||||
ASSERT(duty_100 == 25, "100% = 25 counts");
|
||||
}
|
||||
|
||||
void test_boundary_temps(void) {
|
||||
TEST("Boundary temperatures");
|
||||
/* Just inside boundaries */
|
||||
int speed_39 = sim_calc_speed_from_temp(39);
|
||||
int speed_40 = sim_calc_speed_from_temp(40);
|
||||
int speed_49 = sim_calc_speed_from_temp(49);
|
||||
int speed_50 = sim_calc_speed_from_temp(50);
|
||||
int speed_69 = sim_calc_speed_from_temp(69);
|
||||
int speed_70 = sim_calc_speed_from_temp(70);
|
||||
|
||||
ASSERT(speed_39 == 0, "39°C = 0%");
|
||||
ASSERT(speed_40 == 0, "40°C = 0%");
|
||||
ASSERT(speed_49 >= 0 && speed_49 < 30, "49°C < 30%");
|
||||
ASSERT(speed_50 == 30, "50°C = 30%");
|
||||
ASSERT(speed_69 > 30 && speed_69 < 100, "69°C in medium range");
|
||||
ASSERT(speed_70 == 100, "70°C = 100%");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("\n══════════════════════════════════════════════════════════════\n");
|
||||
printf(" Cooling Fan PWM Speed Controller — Unit Tests (Issue #263)\n");
|
||||
printf("══════════════════════════════════════════════════════════════\n");
|
||||
|
||||
test_temp_off_zone();
|
||||
test_temp_low_zone();
|
||||
test_temp_medium_zone();
|
||||
test_temp_high_zone();
|
||||
test_negative_temps();
|
||||
test_direct_speed_control();
|
||||
test_speed_boundaries();
|
||||
test_ramp_up();
|
||||
test_ramp_down();
|
||||
test_slow_ramp_rate();
|
||||
test_temp_to_speed_transition();
|
||||
test_multiple_ramps();
|
||||
test_state_transitions();
|
||||
test_zero_elapsed_time();
|
||||
test_pwm_duty_calculation();
|
||||
test_boundary_temps();
|
||||
|
||||
printf("\n──────────────────────────────────────────────────────────────\n");
|
||||
printf(" Results: %d/%d tests passed, %d failed\n", test_passed, test_count, test_failed);
|
||||
printf("──────────────────────────────────────────────────────────────\n\n");
|
||||
|
||||
return (test_failed == 0) ? 0 : 1;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user