diff --git a/CMakeLists.txt b/CMakeLists.txt index 76ace64f..08710a34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,8 +54,9 @@ if(EOS_BUILD_TESTS AND NOT EOS_PRODUCT) EOS_ENABLE_SENSOR=1 EOS_ENABLE_FILESYSTEM=1 EOS_ENABLE_MOTOR_CTRL=1 + EOS_ENABLE_PWM=1 ) - message(STATUS " Test features: multicore net power ota sensor filesystem motor_ctrl") + message(STATUS " Test features: multicore net power ota sensor filesystem motor_ctrl pwm") endif() # Global include for eos_config.h and product profiles @@ -70,6 +71,7 @@ include_directories( add_library(eos_hal STATIC hal/src/hal_common.c hal/src/hal_extended_stubs.c + hal/src/hal_pwm.c ) # Which backend source is compiled, and which one eos_hal_init() falls back to, diff --git a/STATUS.md b/STATUS.md index 3fd717c1..733a5341 100644 --- a/STATUS.md +++ b/STATUS.md @@ -35,7 +35,8 @@ is an admission of an unmeasured claim, and should be driven to zero. | Kernel (task, sync, ipc, multicore, heap) | `libeos_kernel.a` | Experimental | Builds host and Cortex-M4; `test_kernel` passes. Mutex priority inheritance is transitive and recomputed from an invariant. No preemption or latency measurement. | | HAL — dispatch layer | `libeos_hal.a` | Experimental | `hal_common.c` dispatches to a registered backend; `test_drivers` passes. | | HAL — host backend | (in `libeos_hal.a`) | **Planned** | `hal_linux.c` implements a Linux backend, but `eos_hal_linux_register()` is **never called and is declared in no public header**. On a host build no backend registers, `eos_hal_init()` returns `-1`, and every HAL call silently no-ops. | -| HAL — extended peripherals (ADC, PWM, I²C, SPI, …) | (in `libeos_hal.a`) | **Planned** | `hal_extended_stubs.c` is 740 of the HAL's 3,185 lines and contains 144 `(void)param;` casts. The API exists; the peripherals do not. | +| HAL — extended peripherals (host) | (in `libeos_hal.a`) | Experimental | `hal_extended_stubs.c` is 707 lines and contains 268 `(void)param;` casts; ADC, I²C, SPI and most other extended peripherals remain stub-only. PWM now has a real host backend (`hal_pwm.c`) — a state-recording stub, not a waveform generator — with `test_pwm` **28/28 pass**. | +| HAL — extended peripherals (STM32F4) | (in `libeos_hal.a`, cross build) | **Planned** | PWM and ADC each have STM32F4 register-level code (`hal_pwm.c`, `hal_extended.c`), but neither is compiled by any build target in this repository: the ARM CI job (`.github/workflows/build.yml`) compiles an explicit source list that omits both files, and the CMake cross-build (`toolchains/arm-cortex-m4.cmake`) sets `EOS_MCU_STM32F4` as a cache variable that is never converted into a compiler `-D` flag either. No test exercises this code, and no ADC test exists at all. | | Architecture — ARM Cortex-M4 / R5 | 23 `.a` targets | Experimental | Cross-compiles clean via `arm-cortex-m4`, `arm-none-eabi-stm32f4` and `arm-none-eabi-r5`; artifacts verified `armv7e-m` / `armv7` with `objdump -f`. **Never executed** — no board, no QEMU. | | Architecture — RISC-V, Cortex-A/AArch64 | — | **Unknown** | Toolchain files exist (`riscv64-linux-gnu.cmake`, `aarch64-linux-gnu.cmake`). Neither toolchain was installed, so neither was built. **NOT RUN.** | | Boards | `boards/*.yaml` | **Planned** | 84 descriptors, of which **71 are named `generic-*`**. A descriptor is not a port; none has been validated on hardware. | diff --git a/hal/include/eos/hal_extended.h b/hal/include/eos/hal_extended.h index 37026fc5..5e5c501b 100644 --- a/hal/include/eos/hal_extended.h +++ b/hal/include/eos/hal_extended.h @@ -77,7 +77,7 @@ void eos_pwm_deinit(uint8_t channel); int eos_pwm_set_duty(uint8_t channel, uint16_t duty_pct_x10); int eos_pwm_set_freq(uint8_t channel, uint32_t frequency_hz); int eos_pwm_start(uint8_t channel); -int eos_pwm_stop(uint8_t channel); +int eos_pwm_stop(uint8_t channel); /* idempotent: 0 if channel is initialized but not running */ #endif /* EOS_ENABLE_PWM */ diff --git a/hal/src/hal_extended.c b/hal/src/hal_extended.c index d22b4d0c..72f23d3a 100644 --- a/hal/src/hal_extended.c +++ b/hal/src/hal_extended.c @@ -13,7 +13,7 @@ #if defined(EOS_MCU_STM32F4) -#define REG32(addr) (*(volatile uint32_t *)(addr)) +#include "hal_stm32f4_regs.h" /* ================================================================ * Flash — STM32F4 Internal Flash (FLASH_CR/SR at 0x40023C00) @@ -207,7 +207,6 @@ int eos_wdt_set_callback(eos_wdt_callback_t cb, void *ctx) { #define RTC_CR REG32(0x40002808U) #define RTC_ISR REG32(0x4000280CU) #define RTC_WPR REG32(0x40002824U) -#define RCC_APB1ENR_REG REG32(0x40023840U) #define RCC_BDCR REG32(0x40023870U) static uint8_t bcd_to_bin(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0xF); } diff --git a/hal/src/hal_extended_stubs.c b/hal/src/hal_extended_stubs.c index 6691b8b9..1a36c01c 100644 --- a/hal/src/hal_extended_stubs.c +++ b/hal/src/hal_extended_stubs.c @@ -89,42 +89,9 @@ int eos_dac_write_mv(uint8_t channel, uint32_t millivolts) /* ---- PWM ---- */ #if EOS_ENABLE_PWM - -int eos_pwm_init(const eos_pwm_config_t *cfg) -{ - (void)cfg; - return -1; -} - -void eos_pwm_deinit(uint8_t channel) -{ - (void)channel; -} - -int eos_pwm_set_duty(uint8_t channel, uint16_t duty_pct_x10) -{ - (void)channel; (void)duty_pct_x10; - return -1; -} - -int eos_pwm_set_freq(uint8_t channel, uint32_t frequency_hz) -{ - (void)channel; (void)frequency_hz; - return -1; -} - -int eos_pwm_start(uint8_t channel) -{ - (void)channel; - return -1; -} - -int eos_pwm_stop(uint8_t channel) -{ - (void)channel; - return -1; -} - +/* PWM implementation moved to hal_pwm.c - platform-specific implementations + * for STM32F4 and Linux are provided there + */ #endif /* EOS_ENABLE_PWM */ /* ---- CAN ---- */ diff --git a/hal/src/hal_pwm.c b/hal/src/hal_pwm.c new file mode 100644 index 00000000..964a88de --- /dev/null +++ b/hal/src/hal_pwm.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project + +/** + * @file hal_pwm.c + * @brief PWM implementation for STM32F4 and host backends + * + * Provides platform-specific PWM implementations: + * - STM32F4: Hardware PWM using TIM2-TIM5 timers + * - Host: State-recording stub (no waveform generation) for API-level testing + */ + +#include +#include + +/* ================================================================ + * STM32F4 Hardware PWM Implementation + * ================================================================ */ +#if defined(EOS_MCU_STM32F4) && EOS_ENABLE_PWM + +#include "hal_stm32f4_regs.h" + +/* Timer base addresses */ +#define TIM2_BASE 0x40000000UL +#define TIM3_BASE 0x40000400UL +#define TIM4_BASE 0x40000800UL +#define TIM5_BASE 0x40000C00UL + +typedef struct { + volatile uint32_t CR1; + volatile uint32_t CR2; + volatile uint32_t SMCR; + volatile uint32_t DIER; + volatile uint32_t SR; + volatile uint32_t EGR; + volatile uint32_t CCMR1; + volatile uint32_t CCMR2; + volatile uint32_t CCER; + volatile uint32_t CNT; + volatile uint32_t PSC; + volatile uint32_t ARR; + volatile uint32_t RCR; + volatile uint32_t CCR1; + volatile uint32_t CCR2; + volatile uint32_t CCR3; + volatile uint32_t CCR4; + volatile uint32_t BDTR; + volatile uint32_t DCR; + volatile uint32_t DMAR; +} TIM_TypeDef; + +/* Timer control bits */ +#define TIM_CR1_CEN (1U << 0) +#define TIM_CR1_ARPE (1U << 7) +#define TIM_EGR_UG (1U << 0) +#define TIM_CCMR1_OC1PE (1U << 3) +#define TIM_CCMR2_OC3PE (1U << 11) +#define TIM_CCER_CC1E (1U << 0) + +/* RCC control */ +#define RCC_APB1ENR_TIM2 (1U << 0) + +static TIM_TypeDef *const tim_bases[] = { + (TIM_TypeDef *)TIM2_BASE, + (TIM_TypeDef *)TIM3_BASE, + (TIM_TypeDef *)TIM4_BASE, + (TIM_TypeDef *)TIM5_BASE, +}; + +/* PWM channel state */ +static struct { + bool initialized; + bool running; + uint32_t frequency_hz; + uint16_t duty_pct_x10; +} pwm_state[16] = {0}; + +static uint8_t channel_to_timer[] = {0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3}; +static uint8_t channel_to_tim_ch[] = {0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3}; + +/* Assuming 84MHz APB1 clock for STM32F4. + * PWM_freq = 84MHz / (PSC + 1) / (ARR + 1) + */ +#define TIMER_CLOCK_HZ 84000000UL + +/* Caller must have already rejected frequency_hz == 0. Chooses the smallest + * prescaler that keeps the period within the timer's 16-bit ARR range; + * rejects frequencies too high for this timer clock to represent (fewer + * than 2 counter ticks per period) instead of wrapping PSC/ARR. */ +static int compute_prescaler_period(uint32_t frequency_hz, uint32_t *out_prescaler, uint32_t *out_period) +{ + uint32_t ticks = TIMER_CLOCK_HZ / frequency_hz; + if (ticks < 2) return -1; + + uint32_t prescaler = (ticks + 0xFFFFu) / 0x10000u; + if (prescaler < 1) prescaler = 1; + + *out_prescaler = prescaler; + *out_period = ticks / prescaler - 1; + return 0; +} + +static bool timer_has_active_channel(uint8_t timer_idx, uint8_t except_channel) +{ + for (uint8_t i = 0; i < 16; i++) { + if (i != except_channel && channel_to_timer[i] == timer_idx && + pwm_state[i].initialized && pwm_state[i].running) { + return true; + } + } + return false; +} + +int eos_pwm_init(const eos_pwm_config_t *cfg) +{ + if (!cfg || cfg->channel >= 16 || cfg->frequency_hz == 0 || + cfg->duty_pct_x10 > 1000) { + return -1; + } + + uint8_t timer_idx = channel_to_timer[cfg->channel]; + uint8_t tim_ch = channel_to_tim_ch[cfg->channel]; + + if (timer_idx >= 4) return -1; + + uint32_t prescaler, period; + if (compute_prescaler_period(cfg->frequency_hz, &prescaler, &period) != 0) { + return -1; + } + + TIM_TypeDef *tim = tim_bases[timer_idx]; + + /* Enable timer clock */ + RCC_APB1ENR_REG |= (RCC_APB1ENR_TIM2 << timer_idx); + + /* Configure timer for PWM */ + tim->CR1 = TIM_CR1_ARPE; /* Auto-reload preload enable */ + tim->CR2 = 0; + tim->SMCR = 0; + tim->PSC = prescaler - 1; + tim->ARR = period; + + /* Configure PWM mode for the channel */ + if (tim_ch < 2) { + /* Channels 1-2 in CCMR1 */ + uint32_t shift = tim_ch * 8; + tim->CCMR1 &= ~(0xFFU << shift); + tim->CCMR1 |= (0x60U << shift) | (TIM_CCMR1_OC1PE << shift); + } else { + /* Channels 3-4 in CCMR2 */ + uint32_t shift = (tim_ch - 2) * 8; + tim->CCMR2 &= ~(0xFFU << shift); + tim->CCMR2 |= (0x60U << shift) | (TIM_CCMR2_OC3PE << shift); + } + + /* Set initial duty cycle */ + uint32_t ccr_val = (period * cfg->duty_pct_x10) / 1000; + switch (tim_ch) { + case 0: tim->CCR1 = ccr_val; break; + case 1: tim->CCR2 = ccr_val; break; + case 2: tim->CCR3 = ccr_val; break; + case 3: tim->CCR4 = ccr_val; break; + } + + /* Enable output for the channel */ + tim->CCER |= (TIM_CCER_CC1E << (tim_ch * 4)); + + /* Force an update event so the ARR/CCR preload values just written + * take effect on this period instead of the next overflow. */ + tim->EGR = TIM_EGR_UG; + + /* Store state */ + pwm_state[cfg->channel].initialized = true; + pwm_state[cfg->channel].running = false; + pwm_state[cfg->channel].frequency_hz = cfg->frequency_hz; + pwm_state[cfg->channel].duty_pct_x10 = cfg->duty_pct_x10; + + return 0; +} + +void eos_pwm_deinit(uint8_t channel) +{ + if (channel >= 16) return; + + uint8_t timer_idx = channel_to_timer[channel]; + uint8_t tim_ch = channel_to_tim_ch[channel]; + + if (timer_idx >= 4) return; + + TIM_TypeDef *tim = tim_bases[timer_idx]; + + /* Disable channel output */ + tim->CCER &= ~(TIM_CCER_CC1E << (tim_ch * 4)); + + /* Stop the timer only if no other channel on this same timer is + * still active. */ + if (!timer_has_active_channel(timer_idx, channel)) { + tim->CR1 &= ~TIM_CR1_CEN; + } + + pwm_state[channel].initialized = false; +} + +int eos_pwm_set_duty(uint8_t channel, uint16_t duty_pct_x10) +{ + if (channel >= 16 || duty_pct_x10 > 1000) return -1; + if (!pwm_state[channel].initialized) return -1; + + uint8_t timer_idx = channel_to_timer[channel]; + uint8_t tim_ch = channel_to_tim_ch[channel]; + + if (timer_idx >= 4) return -1; + + TIM_TypeDef *tim = tim_bases[timer_idx]; + uint32_t period = tim->ARR; + uint32_t ccr_val = (period * duty_pct_x10) / 1000; + + switch (tim_ch) { + case 0: tim->CCR1 = ccr_val; break; + case 1: tim->CCR2 = ccr_val; break; + case 2: tim->CCR3 = ccr_val; break; + case 3: tim->CCR4 = ccr_val; break; + } + + pwm_state[channel].duty_pct_x10 = duty_pct_x10; + return 0; +} + +int eos_pwm_set_freq(uint8_t channel, uint32_t frequency_hz) +{ + if (channel >= 16 || frequency_hz == 0) return -1; + if (!pwm_state[channel].initialized) return -1; + + uint8_t timer_idx = channel_to_timer[channel]; + if (timer_idx >= 4) return -1; + + uint32_t prescaler, period; + if (compute_prescaler_period(frequency_hz, &prescaler, &period) != 0) { + return -1; + } + + TIM_TypeDef *tim = tim_bases[timer_idx]; + + /* Stop timer temporarily */ + bool was_running = pwm_state[channel].running; + tim->CR1 &= ~TIM_CR1_CEN; + + tim->PSC = prescaler - 1; + tim->ARR = period; + + /* Restore duty cycle with new period */ + uint16_t duty = pwm_state[channel].duty_pct_x10; + uint8_t tim_ch = channel_to_tim_ch[channel]; + uint32_t ccr_val = (period * duty) / 1000; + + switch (tim_ch) { + case 0: tim->CCR1 = ccr_val; break; + case 1: tim->CCR2 = ccr_val; break; + case 2: tim->CCR3 = ccr_val; break; + case 3: tim->CCR4 = ccr_val; break; + } + + /* Force an update event so the new ARR/CCR take effect immediately */ + tim->EGR = TIM_EGR_UG; + + /* Restart if it was running */ + if (was_running) { + tim->CR1 |= TIM_CR1_CEN; + } + + pwm_state[channel].frequency_hz = frequency_hz; + return 0; +} + +int eos_pwm_start(uint8_t channel) +{ + if (channel >= 16) return -1; + if (!pwm_state[channel].initialized) return -1; + + uint8_t timer_idx = channel_to_timer[channel]; + if (timer_idx >= 4) return -1; + + TIM_TypeDef *tim = tim_bases[timer_idx]; + tim->CR1 |= TIM_CR1_CEN; + pwm_state[channel].running = true; + + return 0; +} + +int eos_pwm_stop(uint8_t channel) +{ + if (channel >= 16) return -1; + if (!pwm_state[channel].initialized) return -1; + + uint8_t timer_idx = channel_to_timer[channel]; + if (timer_idx >= 4) return -1; + + TIM_TypeDef *tim = tim_bases[timer_idx]; + + if (!timer_has_active_channel(timer_idx, channel)) { + tim->CR1 &= ~TIM_CR1_CEN; + } + + pwm_state[channel].running = false; + return 0; +} + +#endif /* EOS_MCU_STM32F4 && EOS_ENABLE_PWM */ + +/* ================================================================ + * Host PWM Implementation (state-recording stub) + * ================================================================ */ +#if !defined(EOS_MCU_STM32F4) && EOS_ENABLE_PWM + +static struct { + bool initialized; + bool running; + uint32_t frequency_hz; + uint16_t duty_pct_x10; +} host_pwm_state[16] = {0}; + +int eos_pwm_init(const eos_pwm_config_t *cfg) +{ + if (!cfg || cfg->channel >= 16 || cfg->frequency_hz == 0 || + cfg->duty_pct_x10 > 1000) { + return -1; + } + + host_pwm_state[cfg->channel].initialized = true; + host_pwm_state[cfg->channel].running = false; + host_pwm_state[cfg->channel].frequency_hz = cfg->frequency_hz; + host_pwm_state[cfg->channel].duty_pct_x10 = cfg->duty_pct_x10; + + return 0; +} + +void eos_pwm_deinit(uint8_t channel) +{ + if (channel >= 16) return; + host_pwm_state[channel].initialized = false; + host_pwm_state[channel].running = false; +} + +int eos_pwm_set_duty(uint8_t channel, uint16_t duty_pct_x10) +{ + if (channel >= 16 || duty_pct_x10 > 1000) return -1; + if (!host_pwm_state[channel].initialized) return -1; + + host_pwm_state[channel].duty_pct_x10 = duty_pct_x10; + return 0; +} + +int eos_pwm_set_freq(uint8_t channel, uint32_t frequency_hz) +{ + if (channel >= 16 || frequency_hz == 0) return -1; + if (!host_pwm_state[channel].initialized) return -1; + + host_pwm_state[channel].frequency_hz = frequency_hz; + return 0; +} + +int eos_pwm_start(uint8_t channel) +{ + if (channel >= 16) return -1; + if (!host_pwm_state[channel].initialized) return -1; + + host_pwm_state[channel].running = true; + return 0; +} + +int eos_pwm_stop(uint8_t channel) +{ + if (channel >= 16) return -1; + if (!host_pwm_state[channel].initialized) return -1; + + host_pwm_state[channel].running = false; + return 0; +} + +#endif /* !EOS_MCU_STM32F4 && EOS_ENABLE_PWM */ diff --git a/hal/src/hal_stm32f4_regs.h b/hal/src/hal_stm32f4_regs.h new file mode 100644 index 00000000..b572b794 --- /dev/null +++ b/hal/src/hal_stm32f4_regs.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project + +/** + * @file hal_stm32f4_regs.h + * @brief Shared STM32F4 register-access primitives for hal/src translation units + * + * Not a public header: included only by hal/src/*.c files that need direct + * register access on STM32F4. Do not add to hal/include/eos/. + */ + +#ifndef EOS_HAL_STM32F4_REGS_H +#define EOS_HAL_STM32F4_REGS_H + +#define REG32(addr) (*(volatile uint32_t *)(addr)) + +#define RCC_APB1ENR_REG REG32(0x40023840U) + +#endif /* EOS_HAL_STM32F4_REGS_H */ diff --git a/products/vbox_test.h b/products/vbox_test.h index d0bbee45..cd61c492 100644 --- a/products/vbox_test.h +++ b/products/vbox_test.h @@ -11,6 +11,7 @@ #define EOS_ENABLE_SPI 1 #define EOS_ENABLE_I2C 1 #define EOS_ENABLE_TIMER 1 +#define EOS_ENABLE_PWM 1 #define EOS_ENABLE_USB 1 #define EOS_ENABLE_ETHERNET 1 #define EOS_ENABLE_DISPLAY 1 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bfc0e049..412200af 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,6 +45,11 @@ add_executable(test_hal test_hal.c) target_link_libraries(test_hal PRIVATE eos_hal) add_test(NAME test_hal COMMAND test_hal) +# --- test_pwm: PWM peripheral --- +add_executable(test_pwm test_pwm.c) +target_link_libraries(test_pwm PRIVATE eos_hal) +add_test(NAME test_pwm COMMAND test_pwm) + # --- test_kernel: RTOS kernel (tasks, sync, IPC) --- add_executable(test_kernel test_kernel.c) target_link_libraries(test_kernel PRIVATE eos_kernel) diff --git a/tests/test_pwm.c b/tests/test_pwm.c new file mode 100644 index 00000000..d00cb1d8 --- /dev/null +++ b/tests/test_pwm.c @@ -0,0 +1,515 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project +// ISO/IEC 25000 | ISO/IEC/IEEE 15288:2023 + +/** + * @file test_pwm.c + * @brief Unit tests for PWM (Pulse Width Modulation) peripheral + */ + +#include +#include +#include +#include + +static int tests_run = 0; +static int tests_passed = 0; + +/* Every channel is deinitialized before each test so a test's outcome never + * depends on what a previous test left initialized. */ +static void setup(void) +{ + for (uint8_t i = 0; i < 16; i++) { + eos_pwm_deinit(i); + } +} + +#define TEST(name) \ + static void name(void); \ + static void run_##name(void) { \ + printf(" %-50s ", #name); \ + name(); \ + tests_passed++; \ + printf("[PASS]\n"); \ + } \ + static void name(void) + +#define ASSERT(cond) do { \ + if (!(cond)) { \ + printf("[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + exit(1); \ + } \ +} while(0) + +/* ============================================================ + * Basic PWM API Tests + * ============================================================ */ + +TEST(test_pwm_init_invalid_channel) +{ + setup(); + /* Test invalid channel numbers */ + eos_pwm_config_t cfg = { + .channel = 16, /* Invalid - max is 15 */ + .frequency_hz = 1000, + .duty_pct_x10 = 500 /* 50.0% */ + }; + ASSERT(eos_pwm_init(&cfg) == -1); + + cfg.channel = 255; /* Way out of range */ + ASSERT(eos_pwm_init(&cfg) == -1); +} + +TEST(test_pwm_init_null_config) +{ + setup(); + /* Test NULL configuration */ + ASSERT(eos_pwm_init(NULL) == -1); +} + +TEST(test_pwm_init_valid) +{ + setup(); + /* Test valid initialization for channel 0 */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 /* 50.0% */ + }; + ASSERT(eos_pwm_init(&cfg) == 0); + eos_pwm_deinit(0); + + /* Test valid initialization for channel 15 */ + cfg.channel = 15; + ASSERT(eos_pwm_init(&cfg) == 0); + eos_pwm_deinit(15); +} + +TEST(test_pwm_init_multiple_channels) +{ + setup(); + /* Test initializing multiple channels */ + for (uint8_t i = 0; i < 4; i++) { + eos_pwm_config_t cfg = { + .channel = i, + .frequency_hz = 1000 + (i * 100), + .duty_pct_x10 = 250 + (i * 100) + }; + ASSERT(eos_pwm_init(&cfg) == 0); + } + + /* Clean up */ + for (uint8_t i = 0; i < 4; i++) { + eos_pwm_deinit(i); + } +} + +TEST(test_pwm_init_all_channels) +{ + setup(); + /* Every channel 0-15 must be independently initializable and usable. + * Regression coverage for the STM32F4 array being sized to 4 instead + * of 16 (eos#136 review finding 1) -- this only protects the backend + * actually compiled and linked into this test binary; see PR body for + * the STM32F4 coverage gap. */ + for (uint8_t ch = 0; ch < 16; ch++) { + eos_pwm_config_t cfg = { + .channel = ch, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + } + + /* Read back through the API: every channel must independently accept + * duty/freq updates and start/stop without disturbing its neighbours. */ + for (uint8_t ch = 0; ch < 16; ch++) { + ASSERT(eos_pwm_set_duty(ch, 250) == 0); + ASSERT(eos_pwm_set_freq(ch, 2000) == 0); + ASSERT(eos_pwm_start(ch) == 0); + ASSERT(eos_pwm_stop(ch) == 0); + } + + for (uint8_t ch = 0; ch < 16; ch++) { + eos_pwm_deinit(ch); + } +} + +TEST(test_pwm_deinit_invalid_channel) +{ + setup(); + /* Test deinit on invalid channel - should not crash */ + eos_pwm_deinit(16); + eos_pwm_deinit(255); + /* Just ensure it doesn't crash */ +} + +TEST(test_pwm_set_duty_invalid_channel) +{ + setup(); + /* Test setting duty on invalid channel */ + ASSERT(eos_pwm_set_duty(16, 500) == -1); + ASSERT(eos_pwm_set_duty(255, 500) == -1); +} + +TEST(test_pwm_set_duty_invalid_value) +{ + setup(); + /* Test setting invalid duty cycle values */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Duty cycle > 100% */ + ASSERT(eos_pwm_set_duty(0, 1001) == -1); + ASSERT(eos_pwm_set_duty(0, 5000) == -1); + + eos_pwm_deinit(0); +} + +TEST(test_pwm_set_duty_valid) +{ + setup(); + /* Test setting valid duty cycles */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Test various duty cycles */ + ASSERT(eos_pwm_set_duty(0, 0) == 0); /* 0% */ + ASSERT(eos_pwm_set_duty(0, 250) == 0); /* 25% */ + ASSERT(eos_pwm_set_duty(0, 500) == 0); /* 50% */ + ASSERT(eos_pwm_set_duty(0, 750) == 0); /* 75% */ + ASSERT(eos_pwm_set_duty(0, 1000) == 0); /* 100% */ + + eos_pwm_deinit(0); +} + +TEST(test_pwm_set_duty_not_initialized) +{ + setup(); + /* Test setting duty on uninitialized channel */ + ASSERT(eos_pwm_set_duty(0, 500) == -1); +} + +TEST(test_pwm_set_freq_invalid_channel) +{ + setup(); + /* Test setting frequency on invalid channel */ + ASSERT(eos_pwm_set_freq(16, 1000) == -1); + ASSERT(eos_pwm_set_freq(255, 1000) == -1); +} + +TEST(test_pwm_set_freq_invalid_value) +{ + setup(); + /* Test setting invalid frequency values */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Zero frequency */ + ASSERT(eos_pwm_set_freq(0, 0) == -1); + + eos_pwm_deinit(0); +} + +TEST(test_pwm_set_freq_valid) +{ + setup(); + /* Test setting valid frequencies */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Test various frequencies */ + ASSERT(eos_pwm_set_freq(0, 100) == 0); /* 100 Hz */ + ASSERT(eos_pwm_set_freq(0, 1000) == 0); /* 1 kHz */ + ASSERT(eos_pwm_set_freq(0, 10000) == 0); /* 10 kHz */ + ASSERT(eos_pwm_set_freq(0, 50000) == 0); /* 50 kHz */ + + eos_pwm_deinit(0); +} + +TEST(test_pwm_set_freq_not_initialized) +{ + setup(); + /* Test setting frequency on uninitialized channel */ + ASSERT(eos_pwm_set_freq(0, 1000) == -1); +} + +TEST(test_pwm_start_invalid_channel) +{ + setup(); + /* Test starting invalid channel */ + ASSERT(eos_pwm_start(16) == -1); + ASSERT(eos_pwm_start(255) == -1); +} + +TEST(test_pwm_start_stop_cycle) +{ + setup(); + /* Test start/stop cycle */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Start PWM */ + ASSERT(eos_pwm_start(0) == 0); + + /* Stop PWM */ + ASSERT(eos_pwm_stop(0) == 0); + + /* Start again */ + ASSERT(eos_pwm_start(0) == 0); + + /* Stop again */ + ASSERT(eos_pwm_stop(0) == 0); + + eos_pwm_deinit(0); +} + +TEST(test_pwm_start_not_initialized) +{ + setup(); + /* Test starting uninitialized channel */ + ASSERT(eos_pwm_start(0) == -1); +} + +TEST(test_pwm_stop_invalid_channel) +{ + setup(); + /* Test stopping invalid channel */ + ASSERT(eos_pwm_stop(16) == -1); + ASSERT(eos_pwm_stop(255) == -1); +} + +TEST(test_pwm_stop_not_initialized) +{ + setup(); + /* Test stopping uninitialized channel */ + ASSERT(eos_pwm_stop(0) == -1); +} + +TEST(test_pwm_stop_when_not_running) +{ + setup(); + /* eos_pwm_stop() is idempotent: stopping a channel that is initialized + * but not running succeeds, matching hal_extended.h's documented + * contract for eos_pwm_stop(). */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + ASSERT(eos_pwm_stop(0) == 0); + + eos_pwm_deinit(0); +} + +/* ============================================================ + * PWM Configuration Tests + * ============================================================ */ + +TEST(test_pwm_different_frequencies) +{ + setup(); + /* Test multiple channels with different frequencies */ + eos_pwm_config_t cfg[] = { + { .channel = 0, .frequency_hz = 100, .duty_pct_x10 = 500 }, + { .channel = 1, .frequency_hz = 1000, .duty_pct_x10 = 500 }, + { .channel = 2, .frequency_hz = 10000, .duty_pct_x10 = 500 }, + }; + + for (int i = 0; i < 3; i++) { + ASSERT(eos_pwm_init(&cfg[i]) == 0); + } + + for (int i = 0; i < 3; i++) { + eos_pwm_deinit(i); + } +} + +TEST(test_pwm_different_duty_cycles) +{ + setup(); + /* Test multiple channels with different duty cycles */ + eos_pwm_config_t cfg[] = { + { .channel = 0, .frequency_hz = 1000, .duty_pct_x10 = 0 }, /* 0% */ + { .channel = 1, .frequency_hz = 1000, .duty_pct_x10 = 250 }, /* 25% */ + { .channel = 2, .frequency_hz = 1000, .duty_pct_x10 = 500 }, /* 50% */ + { .channel = 3, .frequency_hz = 1000, .duty_pct_x10 = 750 }, /* 75% */ + { .channel = 4, .frequency_hz = 1000, .duty_pct_x10 = 1000 }, /* 100% */ + }; + + for (int i = 0; i < 5; i++) { + ASSERT(eos_pwm_init(&cfg[i]) == 0); + } + + for (int i = 0; i < 5; i++) { + eos_pwm_deinit(i); + } +} + +TEST(test_pwm_reinitialize_channel) +{ + setup(); + /* Test reinitializing a channel */ + eos_pwm_config_t cfg1 = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg1) == 0); + eos_pwm_deinit(0); + + /* Reinitialize with different parameters */ + eos_pwm_config_t cfg2 = { + .channel = 0, + .frequency_hz = 2000, + .duty_pct_x10 = 750 + }; + ASSERT(eos_pwm_init(&cfg2) == 0); + eos_pwm_deinit(0); +} + +/* ============================================================ + * PWM Edge Case Tests + * ============================================================ */ + +TEST(test_pwm_high_frequency) +{ + setup(); + /* Test high frequency PWM */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 100000, /* 100 kHz */ + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + eos_pwm_deinit(0); +} + +TEST(test_pwm_low_frequency) +{ + setup(); + /* Test low frequency PWM */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1, /* 1 Hz */ + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + eos_pwm_deinit(0); +} + +TEST(test_pwm_duty_cycle_boundaries) +{ + setup(); + /* Test duty cycle at boundaries */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == 0); + + /* Test boundary values */ + ASSERT(eos_pwm_set_duty(0, 1) == 0); /* 0.1% */ + ASSERT(eos_pwm_set_duty(0, 999) == 0); /* 99.9% */ + ASSERT(eos_pwm_set_duty(0, 1000) == 0); /* 100% */ + + eos_pwm_deinit(0); +} + +TEST(test_pwm_init_rejects_zero_frequency) +{ + setup(); + /* eos_pwm_set_freq() has always rejected 0; eos_pwm_init() must reject + * it too instead of accepting a config that later divides by it. */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 0, + .duty_pct_x10 = 500 + }; + ASSERT(eos_pwm_init(&cfg) == -1); +} + +TEST(test_pwm_init_rejects_duty_over_limit) +{ + setup(); + /* eos_pwm_set_duty() has always rejected > 1000; eos_pwm_init() must + * reject it too instead of programming an out-of-range duty cycle. */ + eos_pwm_config_t cfg = { + .channel = 0, + .frequency_hz = 1000, + .duty_pct_x10 = 1001 + }; + ASSERT(eos_pwm_init(&cfg) == -1); + + cfg.duty_pct_x10 = 65535; + ASSERT(eos_pwm_init(&cfg) == -1); +} + +/* ============================================================ + * Main Test Runner + * ============================================================ */ + +int main(void) +{ + printf("=== EoS: PWM Unit Tests ===\n\n"); + + /* Basic API tests */ + run_test_pwm_init_invalid_channel(); + run_test_pwm_init_null_config(); + run_test_pwm_init_valid(); + run_test_pwm_init_multiple_channels(); + run_test_pwm_init_all_channels(); + run_test_pwm_deinit_invalid_channel(); + run_test_pwm_set_duty_invalid_channel(); + run_test_pwm_set_duty_invalid_value(); + run_test_pwm_set_duty_valid(); + run_test_pwm_set_duty_not_initialized(); + run_test_pwm_set_freq_invalid_channel(); + run_test_pwm_set_freq_invalid_value(); + run_test_pwm_set_freq_valid(); + run_test_pwm_set_freq_not_initialized(); + run_test_pwm_start_invalid_channel(); + run_test_pwm_start_stop_cycle(); + run_test_pwm_start_not_initialized(); + run_test_pwm_stop_invalid_channel(); + run_test_pwm_stop_not_initialized(); + run_test_pwm_stop_when_not_running(); + + /* Configuration tests */ + run_test_pwm_different_frequencies(); + run_test_pwm_different_duty_cycles(); + run_test_pwm_reinitialize_channel(); + + /* Edge case tests */ + run_test_pwm_high_frequency(); + run_test_pwm_low_frequency(); + run_test_pwm_duty_cycle_boundaries(); + run_test_pwm_init_rejects_zero_frequency(); + run_test_pwm_init_rejects_duty_over_limit(); + + tests_run = 28; + printf("\n%d/%d tests passed\n", tests_passed, tests_run); + return (tests_passed == tests_run) ? 0 : 1; +}