📚 CMPE2250: Lesson — Timer Output Compare with Interrupts (Multi‑Channel Periodic Events)
🎯 Learning Objectives
By the end of this lesson, you will be able to:
-
Explain how Output Compare works in edge‑aligned up‑counting mode
-
Configure multiple OC channels on the same timer
-
Use OC interrupts to generate independent periodic events
-
Implement incremental scheduling using
CCR += period -
Understand why this method is more stable than resetting the counter
-
Validate timing behavior using an oscilloscope
1️⃣ Output Compare: Quick Review
-
A timer counts upward from
0toARR` at a fixed frequency. -
When the counter equals a compare register:
The timer triggers:
-
An output action (toggle, set, reset), and/or
-
An interrupt (if enabled)
-
Each channel (CH1–CH4) has its own CCR register, allowing multiple independent events.
2️⃣ Hardware Setup (TIM15 Example)
This demo uses:
-
TIM15 (2‑channel advanced timer)
-
PB14 → TIM15_CH1 (AF5)
-
PB15 → TIM15_CH2 (AF5)
Both channels are configured in toggle mode, so each compare event flips the pin. With a 1 MHz timer tick (1 µs per tick):
-
CH1 toggles every
200[µs] -
CH2 toggles every
500[µs]
3️⃣ Why Incremental Scheduling Matters
- A common mistake is to schedule the next event like this:
TIM15->CCR1 = TIM15->CNT + 200;
- This introduces jitter because ISR latency varies. Instead, use:
TIM15->CCR1 += 200;
-
This schedules the next event relative to the previous compare event, not the current time.
-
Benefits:
- No drift
- No jitter
- ISR latency does not affect timing
- Channels remain independent
This is the same scheduling technique used in real‑time kernels.
4️⃣ Full Demo Code (Annotated)
int main(void)
{
// Enable power interface clock
RCC->APBENR1 |= RCC_APBENR1_PWREN;
// Enable GPIO and TIM15 clocks
RCC->IOPENR |= RCC_IOPENR_GPIOAEN
| RCC_IOPENR_GPIOBEN
| RCC_IOPENR_GPIOCEN;
RCC->APBENR2 |= RCC_APBENR2_TIM15EN;
// Configure system clock
printf("SYSCLK = %lu Hz\n", SystemCoreClock);
Clock_InitPll(PLL_40MHZ);
printf("SYSCLK = %lu Hz\n", SystemCoreClock);
FLASH->ACR |= FLASH_ACR_PRFTEN_Msk;
FLASH->ACR &= ~FLASH_ACR_LATENCY;
FLASH->ACR |= FLASH_ACR_LATENCY_1;
// Configure PB14/PB15 as TIM15 CH1/CH2
GPIO_InitAlternateF(GPIOB, 14, 5);
GPIO_InitAlternateF(GPIOB, 15, 5);
// Timer configuration
TIM15->PSC = 40 - 1; // 1 MHz timer tick
TIM15->ARR = 0xFFFF; // free-running counter
// Channels as Output Compare
TIM15->CCMR1 &= ~(TIM_CCMR1_CC2S | TIM_CCMR1_CC1S);
// Initial compare values
TIM15->CCR1 = 200; // 200 µs
TIM15->CCR2 = 500; // 500 µs
// Toggle mode for both channels
TIM15->CCMR1 &= ~(TIM_CCMR1_OC2M | TIM_CCMR1_OC1M);
TIM15->CCMR1 |= TIM_CCMR1_OC2M_1 | TIM_CCMR1_OC2M_0;
TIM15->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_0;
// Enable outputs
TIM15->CCER |= TIM_CCER_CC2E | TIM_CCER_CC1E;
// Enable interrupts
TIM15->DIER |= TIM_DIER_CC2IE | TIM_DIER_CC1IE;
NVIC_EnableIRQ(TIM15_IRQn);
// Master output enable (advanced timers)
TIM15->BDTR |= TIM_BDTR_MOE;
// Start timer
TIM15->CR1 |= TIM_CR1_CEN;
// Optional test pin
GPIO_InitOutput(GPIOC, 10);
while (1) {
// main loop does nothing
}
}
void TIM15_IRQHandler(void)
{
if (TIM15->SR & TIM_SR_CC1IF) {
TIM15->SR &= ~TIM_SR_CC1IF;
TIM15->CCR1 += 200; // schedule next event
}
if (TIM15->SR & TIM_SR_CC2IF) {
TIM15->SR &= ~TIM_SR_CC2IF;
TIM15->CCR2 += 500; // schedule next event
}
NVIC_ClearPendingIRQ(TIM15_IRQn);
}
5️⃣ Timing Diagram (Conceptual)
sequenceDiagram
autonumber
participant CNT as Timer CNT (1 MHz)
participant CH1 as CH1 Toggle Event
participant CH2 as CH2 Toggle Event
Note over CNT: CNT counts up continuously
0 → 200 → 400 → 600 → ...
CNT->>CH1: CNT = 200
OC1 Event
Note right of CH1: Toggle CH1
CNT->>CH1: CNT = 400
OC1 Event
Note right of CH1: Toggle CH1
CNT->>CH1: CNT = 600
OC1 Event
Note right of CH1: Toggle CH1
Note over CNT: CNT continues
0 → 500 → 1000 → 1500 → ...
CNT->>CH2: CNT = 500
OC2 Event
Note right of CH2: Toggle CH2
CNT->>CH2: CNT = 1000
OC2 Event
Note right of CH2: Toggle CH2
CNT->>CH2: CNT = 1500
OC2 Event
Note right of CH2: Toggle CH2
gantt
dateFormat X
axisFormat %L
section CNT (Timer Counter)
CNT: milestone, 0, 0
CNT: milestone, 200, 0
CNT: milestone, 400, 0
CNT: milestone, 600, 0
CNT: milestone, 800, 0
CNT: milestone, 1000, 0
section CH1 (200 µs period)
Toggle @200: milestone, 200, 0
Toggle @400: milestone, 400, 0
Toggle @600: milestone, 600, 0
Toggle @800: milestone, 800, 0
Toggle @1000: milestone, 1000, 0
section CH2 (500 µs period)
Toggle @500: milestone, 500, 0
Toggle @1000: milestone, 1000, 0
- Each channel runs independently, even though they share the same timer.