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 : }
|