Embedded C track

Lesson 17 · Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)

General-Purpose Timers & PWM Output

PSC/ARR/CCR and PWM

Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)6 min readFree lesson

If SysTick provides "accurate time," the general-purpose timer (TIM) builds on that with functions such as channels, PWM, and input capture. Among them, the most commonly used is PWM (Pulse Width Modulation). Digital pins can only output 0 or 1, but by quickly adjusting the proportion of time they are on (duty), you can control LED brightness or motor speed "like analog."

In this section, we create a 1kHz PWM on TIM2 channel 1 (PA0) and adjust the duty to control the output ratio. By understanding the relationship among the timer’s three main registers: PSC (prescaler)·ARR (period)·CCR (duty), you can freely calculate the desired frequency and duty.

Learning Objectives

  • Calculate the time axis (PSC·ARR) and counter clock of the general-purpose timer.
  • Understand the operation of PWM mode 1 (comparison of CNT vs CCR).
  • Set the registers to output PWM on TIM2 channel 1.
  • Adjust duty with CCR and frequency with ARR.
  • Know the rule that the APB1 timer clock becomes twice PCLK1.

Target Environment

  • Compiler: Keil MDK-ARM (Arm Compiler 6).
  • Device: STM32F103C8 (μVision simulator).
  • Verification: μVision simulator (TIM2 registers, Logic Analyzer PA0). Calculations on PC.

Key Concepts

1) Timer Time Base — PSC and ARR

The timer divides the input clock with the PSC (prescaler) to create the counter clock, and when the counter reaches ARR (auto-reload), it resets to 0 (update event).

counter_clk = TIMxCLK / (PSC + 1)
pwm_freq    = counter_clk / (ARR + 1)

Example: TIM2CLK=72MHz, PSC=71 → counter 1MHz. ARR=999 → 1MHz/1000 = 1kHz PWM.

2) Timer Clock Trap — APB1 ×2

TIM2~4 are on the APB1 bus. If the APB1 prescaler is not 1, the timer clock becomes twice PCLK1 (STM32 design rule). In the standard 72MHz setting, PCLK1=36MHz, but TIM2 clock is 72MHz. That's why we used 72MHz in the example above.

3) PWM Mode 1 — CNT vs CCR

In PWM Mode 1, the output is High while CNT < CCR, and Low afterward. Therefore, the ratio of being High in one period (ARR+1 clocks) is the duty cycle.

duty(%) = CCR * 100 / (ARR + 1)

CCR=250, ARR=999 → 250/1000 = 25%. Increasing CCR makes it brighter (High for longer).

4) PWM Setup Sequence

Step Register Description
Clock RCC->APB1ENR, APB2ENR Turn on TIM2, GPIOA
Pin GPIOA->CRL PA0 = AF Push-Pull (0xB)
Timing PSC, ARR Counter clock & period
Duty CCR1 Compare value
Mode CCMR1 OC1M=110(PWM1), OC1PE (preload)
Output CCER CC1E (channel output enable)
Start CR1, EGR ARPE, UG (forced load), CEN (start)

5) Alternate Function (AF) Pin

Timer output is the GPIO Alternate Function. PA0 must be set to AF push-pull (CNF=10, MODE=11 → 0xB) instead of general output for the timer signal to come out on the pin.

Creating a μVision Project

  1. Project → New µVision Project… → name it stm32_pwm.
  2. Device: STM32F103C8.
  3. Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste.
  5. Apply STM32F10X_MD to C/C++ → Define.
  6. Debug → Use Simulator, -pSTM32F103C8.
  7. Rebuild(F7) → Debug(Ctrl+F5).

Examples

Example 1 — main.c: TIM2 CH1 PWM (1kHz, 25%)

#include "stm32f10x.h"

/*
 *Lesson 17 Example — TIM2 CH1 (PA0) PWM Output.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *Divide the timer clock (here 72MHz) by PSC to get 1MHz, set the period with ARR,
 *and set the duty cycle with CCR1. PWM Mode 1: Output High while CNT < CCR1.
 *- PSC=71 → 72MHz/(71+1) = 1MHz counter
 *   - ARR=999 → 1MHz/(999+1) = 1kHz PWM
 *- CCR1=250 → Duty 25%
 */

int main(void)
{
    /*1) Clock: GPIOA is APB2, TIM2 is APB1*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    /*2) PA0 = TIM2_CH1 → Alternate function push-pull 50MHz (CNF=10, MODE=11 → 0xB)*/
    GPIOA->CRL &= ~(0xFu << (0u * 4u));
    GPIOA->CRL |=  (0xBu << (0u * 4u));

    /*3) Timer time axis*/
    TIM2->PSC  = 71u;        /*72MHz / 72 = 1MHz counter*/
    TIM2->ARR  = 999u;       /* 1MHz / 1000 = 1kHz PWM */
    TIM2->CCR1 = 250u;       /*Duty 25%*/

    /*4) Channel 1 PWM Mode 1 (110) + CCR preload, enable output*/
    TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;  /* OC1M = 110 */
    TIM2->CCMR1 |= TIM_CCMR1_OC1PE;                      /*CCR1 preload*/
    TIM2->CCER  |= TIM_CCER_CC1E;                        /*Turn on CH1 output*/

    /*5) ARR preload + Force update load (UG) + Start counter*/
    TIM2->CR1 |= TIM_CR1_ARPE;
    TIM2->EGR |= TIM_EGR_UG;
    TIM2->CR1 |= TIM_CR1_CEN;

    while (1) {
        /*PWM is automatically generated by hardware. Changing CCR1 immediately changes the duty cycle.*/
    }
}

Example 2 — pc_test.c: Frequency·Duty Calculation

#include <stdio.h>
#include <stdint.h>

/*
 *Lesson 17 PC Check — Calculation of PWM Frequency and Duty from PSC/ARR/CCR.
 *   counter_clk = tim_clk / (PSC+1)
 *   pwm_freq    = counter_clk / (ARR+1)
 *   duty(%)     = CCR * 100 / (ARR+1)
 */

static void report(uint32_t tim_clk, uint32_t psc, uint32_t arr, uint32_t ccr)
{
    uint32_t counter_clk = tim_clk / (psc + 1u);
    uint32_t pwm_freq    = counter_clk / (arr + 1u);
    uint32_t duty        = (ccr * 100u) / (arr + 1u);

    printf("PSC=%lu ARR=%lu CCR=%lu -> %lu Hz, duty %lu%%\n",
           (unsigned long)psc, (unsigned long)arr, (unsigned long)ccr,
           (unsigned long)pwm_freq, (unsigned long)duty);
}

int main(void)
{
    report(72000000u, 71u, 999u, 250u);   /* 1kHz, 25% */
    report(72000000u, 71u, 999u, 500u);   /* 1kHz, 50% */
    report(72000000u, 71u, 499u, 250u);   /* 2kHz, 50% */
    return 0;
}

Checking with Simulator

  • Check PSC=71, ARR=999, CCR1=250, CEN=1 in CR1 on Peripherals → Timers → TIM2.
  • Adding PORTA.0 to the Logic Analyzer shows a 1kHz square wave with 25% duty cycle (High 0.25ms, Low 0.75ms).
  • During debugging, if TIM2->CCR1 is changed to 500, the duty cycle changes to 50%.
  • Expected behavior: 1kHz PWM on PA0, duty proportional to CCR1.

Verification on a PC

gcc pc_test.c -o pc_test && ./pc_test

Expected output (stdout):

PSC=71 ARR=999 CCR=250 -> 1000 Hz, duty 25%
PSC=71 ARR=999 CCR=500 -> 1000 Hz, duty 50%
PSC=71 ARR=499 CCR=250 -> 2000 Hz, duty 50%

Common Mistakes

Q. PWM is not coming out of the pin.

A. Check three things: ① if PA0 is set to AF push-pull (0xB), ② if output is enabled with CC1E on CCER, ③ if counter is started with CEN on CR1.

Q. Frequency differs from the calculated Half/2-times.

A. This is because the timer clock is set incorrectly. If the APB1 prescaler is not 1, TIM2 clock is 2 times PCLK1 (72MHz). Calculating with 36MHz will result in a 2x mismatch.

Q. I changed the settings, but they don't reflect in the first cycle.

A. If you force an update event with the UG bit of EGR, PSC/ARR/CCR will load immediately. If you miss this, it will use the old value until the next update.

Q. I want to set the duty cycle to 100%.

A. If it's CCR ≥ ARR+1, CNT < CCR is always true, so the output stays high (100%). When ARR=999, CCR=1000 corresponds to 100%.

Summary

  • Timer: The counter clock is set by PSC, period by ARR, and duty by CCR.
  • PWM Mode 1 is High during CNT < CCR → duty = CCR/(ARR+1).
  • The timer output is sent to the AF pin (PA0=0xB).
  • Setup order: Clock → Pin → Time base → Mode/Output → UG → CEN.
  • Note that the APB1 timer clock is twice PCLK1 (72MHz).

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/stm32_pwm/main.cC
#include "stm32f10x.h"

/*
 *Lesson 17 Example — TIM2 CH1 (PA0) PWM Output.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *Divide the timer clock (here 72MHz) by PSC to get 1MHz, set the period with ARR,
 *and set the duty cycle with CCR1. PWM Mode 1: Output High while CNT < CCR1.
 *- PSC=71 → 72MHz/(71+1) = 1MHz counter
 *   - ARR=999 → 1MHz/(999+1) = 1kHz PWM
 *- CCR1=250 → Duty 25%
 */

int main(void)
{
    /*1) Clock: GPIOA is APB2, TIM2 is APB1*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    /*2) PA0 = TIM2_CH1 → Alternate function push-pull 50MHz (CNF=10, MODE=11 → 0xB)*/
    GPIOA->CRL &= ~(0xFu << (0u * 4u));
    GPIOA->CRL |=  (0xBu << (0u * 4u));

    /*3) Timer time axis*/
    TIM2->PSC  = 71u;        /*72MHz / 72 = 1MHz counter*/
    TIM2->ARR  = 999u;       /* 1MHz / 1000 = 1kHz PWM */
    TIM2->CCR1 = 250u;       /*Duty 25%*/

    /*4) Channel 1 PWM Mode 1 (110) + CCR preload, enable output*/
    TIM2->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1;  /* OC1M = 110 */
    TIM2->CCMR1 |= TIM_CCMR1_OC1PE;                      /*CCR1 preload*/
    TIM2->CCER  |= TIM_CCER_CC1E;                        /*Turn on CH1 output*/

    /*5) ARR preload + Force update load (UG) + Start counter*/
    TIM2->CR1 |= TIM_CR1_ARPE;
    TIM2->EGR |= TIM_EGR_UG;
    TIM2->CR1 |= TIM_CR1_CEN;

    while (1) {
        /*PWM is automatically generated by hardware. Changing CCR1 immediately changes the duty cycle.*/
    }
}
src/stm32_pwm/pc_test.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Lesson 17 PC Check — Calculation of PWM Frequency and Duty from PSC/ARR/CCR.
 *   counter_clk = tim_clk / (PSC+1)
 *   pwm_freq    = counter_clk / (ARR+1)
 *   duty(%)     = CCR * 100 / (ARR+1)
 */

static void report(uint32_t tim_clk, uint32_t psc, uint32_t arr, uint32_t ccr)
{
    uint32_t counter_clk = tim_clk / (psc + 1u);
    uint32_t pwm_freq    = counter_clk / (arr + 1u);
    uint32_t duty        = (ccr * 100u) / (arr + 1u);

    printf("PSC=%lu ARR=%lu CCR=%lu -> %lu Hz, duty %lu%%\n",
           (unsigned long)psc, (unsigned long)arr, (unsigned long)ccr,
           (unsigned long)pwm_freq, (unsigned long)duty);
}

int main(void)
{
    report(72000000u, 71u, 999u, 250u);   /* 1kHz, 25% */
    report(72000000u, 71u, 999u, 500u);   /* 1kHz, 50% */
    report(72000000u, 71u, 499u, 250u);   /* 2kHz, 50% */
    return 0;
}

Assignment

Problem 1 is pure logic (PC gcc), Problem 2 is MDK (STM32F103C8, μVision simulator).


Problem 1 — Calculating CCR from Duty (%)

  • File name: homework_01/main.c
  • Key concept: CCR = duty × (ARR+1) / 100
  • Platform: Common (PC gcc)

Requirements

  • Based on ARR=999 (period 1000), calculate and output CCR values for duty 0·25·50·75·100%.
  • CCR = duty × (ARR+1) / 100.
  • The format duty N% -> CCR R must match the expected output exactly, character by character.

Expected output

duty 0% -> CCR 0
duty 25% -> CCR 250
duty 50% -> CCR 500
duty 75% -> CCR 750
duty 100% -> CCR 1000

Hint

  • ccr = duty * (arr + 1u) / 100u; (pay attention to integer operation order — multiply first, then divide).
  • When ARR=999, CCR=1000 means duty 100% (always High).

Problem 2 — TIM2 CH2(PA1) PWM 1kHz 75%

  • File name: homework_02/main.c
  • Key concept: Setting other channels (CCMR1 upper byte, CCER CC2E, CCR2)
  • Platform: MDK (STM32F103C8)

Requirements

  • Output 1kHz, 75% duty PWM on TIM2 Channel 2 (PA1).
  • Set PSC=71, ARR=999, CCR2=750.

Expected Operation (Simulator)

  • Logic Analyzer PORTA.1 shows 1kHz, 75% duty (High 0.75ms) square wave.
  • Check TIM2 registers: CCR2=750, CCER CC2E=1, CR1 CEN=1.

Hint

  • Set PA1 to AF push-pull (0xB): GPIOA->CRL bits[7:4].
  • Channel 2 is the upper byte of CCMR1: TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1, TIM_CCMR1_OC2PE.
  • Output enable: TIM_CCER_CC2E. Duty: TIM2->CCR2 = 750;.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — Calculating CCR from Duty (%)
 *
 *Key Points:
 *- CCR = duty × (ARR+1) / 100. Multiply first then divide to reduce integer error.
 *- ARR=999 → period 1000. Duty 100% means CCR=1000 (always High).
 *
 *Common mistake:
 *- When duty/100 is calculated first, it can become 0 in integers (so multiply first).
 */

int main(void)
{
    unsigned int arr = 999u;
    unsigned int duties[5] = { 0u, 25u, 50u, 75u, 100u };
    int i;

    for (i = 0; i < 5; i++) {
        uint32_t ccr = (uint32_t)duties[i] * (arr + 1u) / 100u;
        printf("duty %u%% -> CCR %lu\n", duties[i], (unsigned long)ccr);
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Answer 2 (MDK, STM32F103C8) — TIM2 CH2(PA1) PWM 1kHz 75%
 *
 *Key Points:
 *- Channel 2 uses the high byte of CCMR1 (OC2M/OC2PE) and CCER's CC2E, duty cycle is CCR2.
 *- Time base (PSC/ARR) is common for the timer, same as the channel 1 example.
 *
 *Common mistake:
 *- For channel 2, if you manipulate OC1M/CC1E/CCR1 (needs correct channel number).
 *- If PA1 is set as general output instead of AF push-pull (0xB).
 *
 *Expected behavior (simulator): 1kHz, 75% duty PWM on PORTA.1. CCR2=750, CC2E=1.
 */

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;

    /*PA1 = TIM2_CH2 → AF push-pull (bits[7:4] = 0xB)*/
    GPIOA->CRL &= ~(0xFu << (1u * 4u));
    GPIOA->CRL |=  (0xBu << (1u * 4u));

    TIM2->PSC  = 71u;        /*1MHz counter*/
    TIM2->ARR  = 999u;       /* 1kHz */
    TIM2->CCR2 = 750u;       /*75% duty*/

    TIM2->CCMR1 |= TIM_CCMR1_OC2M_2 | TIM_CCMR1_OC2M_1;  /*CH2 PWM mode 1*/
    TIM2->CCMR1 |= TIM_CCMR1_OC2PE;                      /*CCR2 preload*/
    TIM2->CCER  |= TIM_CCER_CC2E;                        /*Enable CH2 output*/

    TIM2->CR1 |= TIM_CR1_ARPE;
    TIM2->EGR |= TIM_EGR_UG;
    TIM2->CR1 |= TIM_CR1_CEN;

    while (1) {
    }
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now