Chapter 10: Timer Output Compare with Interrupts

📚 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 0to ARR` 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.