LCOV - code coverage report
Current view: top level - pwm/src - stm32f4_pwm.c (source / functions) Hit Total Coverage
Test: filtered.info Lines: 116 124 93.5 %
Date: 2025-12-18 00:00:35 Functions: 12 12 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /**
       2             :  * @file stm32f4_pwm.c
       3             :  * @brief STM32F4 implementation of PWM.
       4             :  *
       5             :  * Copyright (c) 2025 Cory McKiel.
       6             :  * Licensed under the MIT License. See LICENSE file in the project root.
       7             :  */
       8             : #include "pwm.h"
       9             : #include "stm32f4_hal.h"
      10             : 
      11             : #ifdef DESKTOP_BUILD
      12             : #include "registers.h"
      13             : #include "nvic.h"
      14             : #else
      15             : #include "stm32f4xx.h"
      16             : #endif
      17             : 
      18             : // @todo Calculate this smartly starting from SYSCLK
      19             : #define TIM1_FREQ_HZ 16000000
      20             : 
      21             : /*********************************************************************************************/
      22             : // Common Output Compare Modes used to configure the pin's behavior when counting events occur.
      23             : /*********************************************************************************************/
      24             : /// @brief PWM Mode 1: In upcounting, channel 1 is active as long as TIM1_CNT<TIM1_CCR1 else inactive.
      25             : /// Classic PWM.
      26             : #define OC_MODE_PWM_1       0b110u
      27             : /// @brief Forced low: Output pin forced low. (0% duty cycle)
      28             : #define OC_MODE_FORCED_LOW  0b100u
      29             : /// @brief Forced high: Output pin forced high (100% duty cycle)
      30             : #define OC_MODE_FORCED_HIGH 0b101u
      31             : 
      32             : static bool pwm_state_enabled = false;
      33             : 
      34             : /*********************************************************************************************/
      35             : // Forward declarations
      36             : /*********************************************************************************************/
      37             : static void configure_gpios();
      38             : static void compute_psc_arr(uint32_t pwm_frequency_hz, uint16_t* psc_out, uint16_t* arr_out);
      39             : 
      40             : /*********************************************************************************************/
      41             : // Inline helpers
      42             : /*********************************************************************************************/
      43             : /**
      44             :  * @brief Force an update event: load preloaded ARR/CCR/PSC
      45             :  */
      46          72 : static inline void tim1_force_update(void)
      47             : {
      48             :     // EGR: Event Generation Register
      49             :     //  UG: Update Generate
      50          72 :     TIM1->EGR = TIM_EGR_UG;
      51          72 : }
      52             : 
      53             : /**
      54             :  * @brief Sets the output compare mode for channel one.
      55             :  * @param ocm Output compare mode - The important modes of
      56             :  * operation are as follows:
      57             :  *   1. @ref OC_MODE_PWM_1 Classic PWM signal. Valid duty cycles of 1% - 99%.
      58             :  *   2. @ref OC_MODE_FORCED_LOW Output forced low. Duty cycle is 0%.
      59             :  *   3. @ref OC_MODE_FORCED_HIGH Output forced high. Duty cycle is 100%.
      60             :  */
      61          40 : static inline void tim1_ch1_set_ocmode(uint32_t ocm)
      62             : {
      63             :     // CCMR: Capture/Compare Mode Register
      64          40 :     TIM1->CCMR1 = (TIM1->CCMR1 & ~TIM_CCMR1_OC1M) | (ocm << TIM_CCMR1_OC1M_Pos);
      65          40 : }
      66             : 
      67             : /**
      68             :  * @brief Apply prescaler (psc) and auto-reload (arr) values to their registers.
      69             :  * Determines the frequency of the PWM signal.
      70             :  */
      71           2 : static inline void apply_psc_arr(uint16_t psc, uint16_t arr)
      72             : {
      73           2 :     TIM1->CR1 &= ~TIM_CR1_CEN;      // stop counter during reprogram (optional, safer)
      74           2 :     TIM1->PSC  = psc;
      75           2 :     TIM1->ARR  = arr;
      76           2 :     tim1_force_update();
      77           2 :     TIM1->CR1 |= TIM_CR1_CEN;
      78           2 : }
      79             : 
      80             : /**
      81             :  * @brief Set PWM Duty Cycle to 0%. (Hold output low)
      82             :  */
      83          28 : static inline void set_forced_inactive(void)
      84             : {
      85          28 :     tim1_ch1_set_ocmode(OC_MODE_FORCED_LOW);
      86          28 :     tim1_force_update();
      87          28 : }
      88             : 
      89             : /**
      90             :  * @brief Set PWM Duty Cycle to 100%. (Hold output high)
      91             :  */
      92           2 : static inline void set_forced_active(void)
      93             : {
      94           2 :     tim1_ch1_set_ocmode(OC_MODE_FORCED_HIGH);
      95           2 :     tim1_force_update();
      96           2 : }
      97             : 
      98             : /**
      99             :  * @brief Set PWM Mode to classic PWM mode (1%-99% duty cycle)
     100             :  */
     101          10 : static inline void set_pwm_mode1(void)
     102             : {
     103          10 :     tim1_ch1_set_ocmode(OC_MODE_PWM_1);
     104          10 :     tim1_force_update();
     105          10 : }
     106             : 
     107             : /*********************************************************************************************/
     108             : // Public Interface
     109             : /*********************************************************************************************/
     110             : 
     111          21 : hal_status_t hal_pwm_init(uint32_t pwm_frequency_hz)
     112             : {
     113             :     // Safe default values for psc and arr.
     114          21 :     uint16_t psc = 0;
     115          21 :     uint16_t arr = 1;
     116             : 
     117          21 :     configure_gpios();
     118          21 :     compute_psc_arr(pwm_frequency_hz, &psc, &arr);
     119             : 
     120          21 :     TIM1->CR1 = 0;
     121          21 :     TIM1->PSC = psc;
     122          21 :     TIM1->ARR = arr;
     123          21 :     tim1_force_update();
     124             : 
     125             :     // Enable preload for ARR and CCR1
     126          21 :     TIM1->CCMR1 |= TIM_CCMR1_OC1PE;
     127          21 :     TIM1->CR1 |= TIM_CR1_ARPE;
     128             : 
     129             :     // Start in a safe state. (0% PWM)
     130          21 :     set_forced_inactive();
     131             : 
     132             :     // Set the polarity to be active high.
     133          21 :     TIM1->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC1NP);
     134             : 
     135             :     // Enable output for channel 1
     136          21 :     TIM1->CCER |= TIM_CCER_CC1E;
     137             : 
     138             :     // MOE: Main output enable. Necessary to get the output out of the timer and
     139             :     // to the configured output pin.
     140          21 :     TIM1->BDTR |= TIM_BDTR_MOE;
     141             : 
     142             :     // Start the counter. Output is still forced low.
     143          21 :     TIM1->CR1 |= TIM_CR1_CEN;
     144             : 
     145          21 :     pwm_state_enabled = false;
     146             : 
     147          21 :     return HAL_STATUS_OK;
     148             : }
     149             : 
     150          16 : void hal_pwm_enable(bool enable)
     151             : {
     152          16 :     if (enable)
     153             :     {
     154          13 :         pwm_state_enabled = true;
     155             :         // Resume the previous setting if there was one.
     156          13 :         if (TIM1->CCR1 != 0)
     157             :         {
     158           1 :             set_pwm_mode1();
     159             :         }
     160             :     }
     161             :     else
     162             :     {
     163           3 :         pwm_state_enabled = false;
     164           3 :         set_forced_inactive();
     165             :     }
     166          16 : }
     167             : 
     168          15 : void hal_pwm_set_duty_cycle(uint8_t percent)
     169             : {
     170             :     // Always set pwm low if called with 0%, regardless of pwm_state_enabled,
     171             :     // for safety.
     172          15 :     if (percent == 0)
     173             :     {
     174           3 :         set_forced_inactive();
     175           3 :         TIM1->CCR1 = 0;
     176           3 :         return;
     177             :     }
     178             : 
     179          12 :     if (pwm_state_enabled)
     180             :     {
     181          10 :         if (percent >= 100)
     182             :         {
     183           2 :             set_forced_active();
     184           2 :             return;
     185             :         }
     186             : 
     187             :         // percent is something between 1%-99%.
     188           8 :         set_pwm_mode1();
     189             : 
     190             :         // Calculate CCR1
     191           8 :         uint32_t arrp1 = (uint32_t)TIM1->ARR + 1u;
     192             :         // CCR = round(percent/100 * (ARR+1))
     193             :         // Common rounding trick for integers:
     194             :         // result = (numerator * scale + divisor/2) / divisor;
     195           8 :         uint32_t ccr = ( (uint32_t)percent * arrp1 + 50u ) / 100u;
     196             : 
     197             :         // Avoid accidental 0%.
     198           8 :         if (ccr == 0u)
     199             :         {
     200           0 :             ccr = 1u;
     201             :         }
     202             : 
     203             :         // Keep within [1..ARR]
     204           8 :         if (ccr > TIM1->ARR)
     205             :         {
     206           0 :             ccr = TIM1->ARR;
     207             :         }
     208             : 
     209             :         // Apply to CCR1
     210           8 :         TIM1->CCR1 = (uint16_t)ccr;
     211             :         // With OC1PE=1, CCR update latches on next UG/overflow. Force UG to apply now:
     212           8 :         tim1_force_update();
     213             :     }
     214             : }
     215             : 
     216           2 : void hal_pwm_set_frequency(uint32_t pwm_frequency_hz)
     217             : {
     218           2 :     if (pwm_state_enabled)
     219             :     {
     220             :         // Compute new PSC/ARR and apply
     221           2 :         uint16_t psc=0;
     222           2 :         uint16_t arr=1;
     223           2 :         compute_psc_arr(pwm_frequency_hz, &psc, &arr);
     224             : 
     225             :         // Capture current ratio before changing ARR
     226           2 :         float ratio = 0.0f;
     227           2 :         if ((TIM1->CCMR1 & TIM_CCMR1_OC1M) == (OC_MODE_PWM_1 << TIM_CCMR1_OC1M_Pos))
     228             :         {
     229           1 :             ratio = (float)TIM1->CCR1 / (float)(TIM1->ARR + 1u);
     230             :         }
     231             : 
     232           2 :         apply_psc_arr(psc, arr);
     233             : 
     234             :         // Restore ratio if applicable
     235           2 :         if (ratio > 0.0f && ratio < 1.0f)
     236           1 :         {
     237           1 :             uint32_t arrp1 = (uint32_t)TIM1->ARR + 1u;
     238           1 :             uint32_t ccr   = (uint32_t)(ratio * (float)arrp1 + 0.5f);
     239             : 
     240             :             // Avoid accidental 0%.
     241           1 :             if (ccr == 0u)
     242             :             {
     243           0 :                 ccr = 1u;
     244             :             }
     245             : 
     246             :             // Keep within [1..ARR]
     247           1 :             if (ccr > TIM1->ARR)
     248             :             {
     249           0 :                 ccr = TIM1->ARR;
     250             :             }
     251             : 
     252           1 :             TIM1->CCR1 = (uint16_t)ccr;
     253           1 :             set_pwm_mode1();
     254           1 :             tim1_force_update();
     255             :         }
     256           1 :         else if (ratio >= 1.0f)
     257             :         {
     258           0 :             set_forced_active();
     259             :         }
     260             :         else
     261             :         { // ratio == 0.0f
     262           1 :             set_forced_inactive();
     263             :         }
     264             :     }
     265           2 : }
     266             : 
     267             : /*********************************************************************************************/
     268             : // Private Functions
     269             : /*********************************************************************************************/
     270             : 
     271          21 : static void configure_gpios()
     272             : {
     273             :     // Clocks
     274          21 :     RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
     275          21 :     RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
     276             : 
     277             :     // Using PA8 as PWM pin. Set alternate function: 0b10.
     278          21 :     GPIOA->MODER |= (BIT_17);
     279          21 :     GPIOA->MODER &= ~(BIT_16);
     280             : 
     281             :     // Set push-pull
     282          21 :     GPIOA->OTYPER &= ~(BIT_8);
     283             : 
     284             :     // No pull-up, no pull-down: 0b00
     285          21 :     GPIOA->PUPDR &= ~(BIT_17);
     286          21 :     GPIOA->PUPDR &= ~(BIT_16);
     287             : 
     288             :     // Set to alternate function 1 (TIM1)
     289          21 :     GPIOA->AFR[1] &= ~(0xFu);
     290          21 :     GPIOA->AFR[1] |= 1;       // [3:0] = 0b0001 for AF1
     291             : 
     292             :     // Optional: Set high speed?
     293          21 : }
     294             : 
     295          23 : static void compute_psc_arr(uint32_t pwm_frequency_hz, uint16_t* psc_out, uint16_t* arr_out)
     296             : {
     297             :     // Ensure frequency is non-zero
     298          23 :     pwm_frequency_hz = pwm_frequency_hz ? pwm_frequency_hz : 1;
     299             :     // Ensure we clamp high side.
     300          23 :     pwm_frequency_hz = (pwm_frequency_hz > TIM1_FREQ_HZ) ? TIM1_FREQ_HZ : pwm_frequency_hz;
     301             : 
     302             :     // target_count: The number of ticks of the timer 1 clock we must count in
     303             :     // order to create a pwm of the requested hz.
     304             :     // Eg: 20 khz pwm request -> 16,000,000 / 20,000 = 800
     305             :     // This is used to generate a 20 khz clock by repeatedly counting from
     306             :     // 0 -> 799 -> 0 -> 799 -> 0 -> 799 -> ...
     307             :     // and counting each time we roll over.
     308             :     // 0        -> 1        -> 2        -> ...
     309             :     // The rollover count will be a 20 khz clock.
     310          23 :     uint32_t target_count = TIM1_FREQ_HZ / pwm_frequency_hz;
     311             : 
     312             :     // Determine the prescaler (psc): For slow pwm signals (say 200 Hz), we would need
     313             :     // to count very high. For a 16,000,000 timer 1 clock, this would mean repeatedly
     314             :     // counting to 16,000,000 / 200 = 80,000 ticks. The problem is that the counting
     315             :     // register (ARR) is only 16 bits. 80,000 = 0x13880 > 0xFFFF means we can't count
     316             :     // high enough using the 16 bit register. In order to solve this, the prescaler
     317             :     // allows us to divide the input clock when counting. For example, if we set psc to
     318             :     // 1, we get: 80,000 / (1 + 1) = 40,000 which is less than 0xFFFF = 65,535. Now we have
     319             :     // a number small enough that we can count to.
     320             :     //
     321             :     // The general formula is based on the following:
     322             :     //     target_count = (psc + 1)(arr + 1)
     323             :     // =>  target_count / (psc + 1) = (arr + 1)
     324             :     // but, arr <= 0xFFFF = 65,535 => (arr + 1) <= 65,536 = 0x10000
     325             :     // so, target_count / (psc + 1) <= 65536
     326             :     // =>  target_count / 65536 <= (psc + 1)
     327             :     //
     328             :     // So, at first, it seems like target_count / 0x10000 would do the trick if we
     329             :     // we remember to subtract 1 later. The problem is that int math rounds down.
     330             :     // Using our example earlier (200 Hz PWM):
     331             :     // target_count = 80,000 => 80,000 / 65,536 = 1.22 => (C rounds down) => 1.
     332             :     // then subtract 1 => 1 - 1 = 0
     333             :     // And psc of 0 implies (arr + 1) = 80,000 / (0 + 1) = 80,000
     334             :     // implies arr = 79,999 which is greater than 65,535 and cannot fit in arr. We
     335             :     // therefore would have a problem with floor(target_count / 0x10000).
     336             :     //
     337             :     // So, we need ceil(target_count / 0x10000) which can be calculated using an
     338             :     // integer math trick: ceil(a/b) = (a + b - 1) / b.
     339             :     // Which can be proved using the fact that any integer `a` can be represented by
     340             :     // a = q*b + r, where r is the remainder. If r is 0, there is no problem, and the
     341             :     // answer is q. If r > 0, then the answer will be ceil(a/b) = q+1.
     342          23 :     uint32_t psc = (target_count + 0xFFFF) / 0x10000;
     343             : 
     344             :     // We need to subtract 1 because under the hood the prescaler performs it's job by
     345             :     // counting, but starting at zero. So if above, we got psc = 2, we subtract 1 so we
     346             :     // get psc = 1. Then it counts: 0, 1, 0, 1, 0, 1, ...
     347             :     // And the rollover happens after 1, but there are 2 states of the counter.
     348          23 :     if (psc != 0)
     349             :     {
     350          23 :         psc -= 1;
     351             :     }
     352             : 
     353             :     // Ensure psc is in 16 bit bounds.
     354          23 :     if (psc > 0xFFFF)
     355             :     {
     356           0 :         psc = 0xFFFF;
     357             :     }
     358             : 
     359             :     // Now derive (arr + 1) from psc:
     360          23 :     uint32_t arr = (target_count / (psc + 1));
     361             : 
     362             :     // Ensure ARR is positive before subtracting to get the true ARR.
     363          23 :     if (arr > 0)
     364             :     {
     365          23 :         arr -= 1;
     366             :     }
     367             : 
     368             :     // Ensure ARR is never zero. Only perform the subtraction if it doesn't take us to zero.
     369             :     // ARR must never be zero because it ruins the counting loop 0 -> ARR -> 0 -> ARR -> ... and
     370             :     // floods the system with constant update events. A genuine PWM cannot be generated at all in
     371             :     // the case ARR = 0.
     372          23 :     if (arr == 0)
     373             :     {
     374           0 :         arr = 1;
     375             :     }
     376             : 
     377             :     // Ensure arr is in 16 bit bounds.
     378          23 :     if (arr > 0xFFFF)
     379             :     {
     380           0 :         arr = 0xFFFF;
     381             :     }
     382             : 
     383          23 :     if (psc_out && arr_out)
     384             :     {
     385          23 :         *psc_out = (uint16_t)psc;
     386          23 :         *arr_out = (uint16_t)arr;
     387             :     }
     388          23 : }

Generated by: LCOV version 1.14