📚 CMPE1250: Lesson: Output Compare and Pulse Width Modulation (PWM)
🎯 Introduction
-
In our previous lessons, we used Basic Timers to generate Update Events when a timer’s counter (CNT) reached its maximum value (ARR) and rolls over. This is great for triggering periodic interrupts.
-
What if we want to change the state of an output pin precisely when a specific time has passed, without relying on the CPU to handle an interrupt? Or, what if we want to control the speed of a motor or dim an LED? For this, we use Output Compare and Pulse Width Modulation (PWM).
1️⃣ Concept: Output Compare (OC)
-
Output Compare allows the hardware to automatically control an output pin when the timer’s Counter (CNT) matches a specific value you set in the Capture/Compare Register (CCR).
-
When
CNT == CCR, a “Match Event” occurs. The hardware can be configured to automatically perform one of the following actions on the assigned GPIO pin:-
Setthe pin HIGH. -
Resetthe pin LOW. -
Togglethe pin state.
-
Reviewing the Output Compare Demo
-
Let’s look at the logic in the demo code provided. We configured TIM16 to toggle a pin every 400 μs.
-
The Setup:
-
Prescaler (PSC): We set it to 40 - 1 with a 40 MHz clock, meaning the timer ticks every 1 μs.
-
Period (ARR): Set to maximum (0xFFFF), so the counter runs continuously.
-
Target Time (CCR1): Set to 400. When CNT reaches 400, a match occurs.
-
-
The Action Register (CCMR1):
- To tell the microcontroller what to do on a match, we configured the Output Compare Mode bits in CCMR1:
TIM16->CCMR1 &= ~TIM_CCMR1_OC1M; // Clear settings
TIM16->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0; // Set to 0b011 (Toggle Mode)
-
The Software Loop (while(1)):
- In Toggle Mode, once the timer hits 400, the pin flips. But the timer keeps counting up to 0xFFFF. To make it toggle again after another 400 μs, we must intercept the flag in the while(1) loop and advance our target:
if(TIM16->SR & TIM_SR_CC1IF)
{
TIM16->SR &= ~TIM_SR_CC1IF; // Clear flag
TIM16->CCR1 += 400; // Re-arm event for the next 400us
}
2️⃣ Concept: Pulse Width Modulation (PWM)
- Toggling pins is useful, but it requires CPU intervention to constantly update the CCR register. What if we want a repeating waveform where the HIGH time is different from the LOW time, completely handled by hardware? This is PWM. PWM Fundamentals
A PWM signal is a square wave characterized by two main properties:
-
Period (T) / Frequency (f): The period is how long it takes for one complete cycle, and frequency is how many cycles occur per second.
\[f = \frac{1}{T}\] -
Duty Cycle: The percentage of the period where the signal is HIGH.
\[\text{Duty Cycle} = \left( \frac{t_{\text{high}}}{T} \right) \times 100\%\]
How Timers Generate PWM
-
In the STM32, PWM is just a special configuration of Output Compare. Instead of letting the counter run to 0xFFFF and constantly updating CCR, we use ARR and CCR together:
-
ARR(Auto-Reload Register) controls the Frequency/Period. -
CCR(Capture/Compare Register) controls the Duty Cycle.
-
-
How PWM Mode 1 ( Positive Polarity) works (Edge-Aligned up-counting):
-
While
CNT < CCR, the output pin is drivenHIGH. -
When
CNT == CCR, the output pin is drivenLOW. -
When
CNT == ARR, the counter resets to 0, and the pin goesHIGHagain.
-
Notice the magic here: No CPU intervention is required inside the while(1) loop!
-
How PWM Mode 2 works (Edge-Aligned up-counting):
-
While
CNT < CCR, the output pin is drivenLOW(Inactive). -
When
CNT == CCR, the output pin is drivenHIGH(Active). -
When
CNT == ARR, the counter resets to 0, and the pin goes LOW again.
-
-
In Mode 1, CCR defines how long the signal stays HIGH. In Mode 2, CCR defines how long the signal stays LOW. Code Walkthrough: Changing to PWM Mode 2
-
To switch our previous 1 kHz, 25% duty cycle example to PWM Mode 2, we only need to change the Output Compare Mode bits in CCMR1.
-
Instead of PWM Mode 1
(0b110), we set the bits to PWM Mode 2(0b111).
/* Change Output Compare Mode to PWM Mode 2 */ TIM16->CCMR1 &= ~TIM_CCMR1_OC1M; // 0b111 means Bit 2, Bit 1, and Bit 0 are all HIGH TIM16->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0;
Note: If you leave CCR1 = 250 and switch to Mode 2, the pin will be LOW for the first 250 ticks and HIGH for the remaining 750 ticks. Your duty cycle just flipped from 25% to 75%! PWM Mode 2 Math
-
The frequency calculation remains exactly the same as Mode 1. However, because the active (HIGH) time now happens after the counter passes CCR, the formal math for a standard Duty Cycle (D) changes.
-
Calculating PWM Duty Cycle (Mode 2):
- The active HIGH time is the total period minus the compare value. $\text{D} = \left(\frac{ARR + 1 - CCR}{ARR + 1} \right) \times 100$
3️⃣ Code Walkthrough: Converting the Demo to PWM
- To convert your existing Toggle demo into a 1 kHz PWM signal with a 25% duty cycle, we only need to change a few lines of code.
Step 1: Set the Period (Frequency)
- With a 1 μs tick (PSC = 39), we want a 1 kHz frequency (a 1ms / 1000 μs period).
TIM16->ARR = 1000 - 1; // Period = 1000 ticks (1ms)
Step 2: Set the Duty Cycle
- We want a 25% duty cycle. 25% of 1000 is 250.
TIM16->CCR1 = 250; // Pin stays HIGH for 250 ticks, then goes LOW
Step 3: Change Output Compare Mode to PWM Mode 1
- Instead of Toggle
(0b011), we set the bits to PWM Mode 1 (0b110).
TIM16->CCMR1 &= ~TIM_CCMR1_OC1M; // 0b110 is Bit 2 and Bit 1 HIGH TIM16->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;
Note: Because we want the hardware to handle everything continuously, you must also enable the Preload register (OC1PE bit in CCMR1) when working with PWM to ensure glitch-free updates.
4️⃣ Empty the main loop
- Because PWM is entirely hardware-driven, you can completely remove the if statement and the CCR1 += 400 logic from your while(1) loop!
Summary of Timer Roles for PWM
| Register | Purpose in PWM | Analogy |
|---|---|---|
| PSC | Sets the speed of the timer “tick”. | How fast the clock’s second hand moves. |
| ARR | Sets the Total Period (T). | How many ticks make up one full wave. |
| CCR | Sets the Duty Cycle (thigh). | The exact tick where the signal switches off. |