Embedded C track

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

Cortex-M, CMSIS & the Clock Tree

CMSIS, RCC and the clock tree

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

Here, after graduating from the 8051, we move the stage to 32-bit Arm Cortex-M (STM32F103). Compared to the 8-bit 8051, the register width, address space, and the number of peripherals increase a level, and most importantly, thanks to the CMSIS standard, you can access registers of any Cortex-M in a similar way. This chapter focuses on building the three foundations — Cortex-M architecture, CMSIS header, clock tree — that will support the next seven chapters (14~20), rather than writing a lot of code.

This track controls registers directly without using the STM32 HAL/SPL library. This is for the learning purpose of seeing the actions hidden behind abstraction functions directly. Instead, we use the standard symbols provided by CMSIS (RCC->APB2ENR, GPIOC->ODR, SystemCoreClock, etc.) to maintain readability and portability.

Simulator Note: The μVision STM32F103 simulator models the core and basic registers like RCC·GPIO. In the examples in this chapter, you observe RCC register bit changes and variable increments in the Register/Watch window, and clock tree calculations are verified on the PC using pc_test.c.

Learning Objectives

  • Explains structural differences (width, address, bus) between Cortex-M (STM32) and 8051.
  • Understands what CMSIS is and what symbols stm32f10x.h provides.
  • Applies the RCC rule of "turn on the clock before using peripherals."
  • Follows the clock tree from HSE/HSI·PLL·AHB/APB prescalers.
  • Calculate SYSCLK/HCLK/PCLK1/PCLK2 from the settings value.

Target Environment

  • Compiler: Keil MDK-ARM (Arm Compiler 6).
  • Device: STM32F103C8 (μVision simulator).
  • Verification: μVision simulator (Peripherals/Registers, Watch window). Clock calculation is PC gcc(pc_test.c).

Key Concepts

1) Cortex-M vs 8051 — What changes

Item 8051 (AT89C52) Cortex-M3 (STM32F103)
Width 8-bit 32-bit
Clock Usually ~12MHz Up to 72MHz
Register access SFR (P1, TMOD …) Memory-mapped structures (GPIOC->ODR …)
Peripheral clock Always on Individual enable required via RCC
Standard header reg52.h CMSIS stm32f10x.h

The most important new habit is clock gating. In 8051, ports are always on, but in STM32, to save power, all peripheral clocks are off by default, so you need to enable them via RCC before use.

2) CMSIS and stm32f10x.h

CMSIS (Cortex Microcontroller Software Interface Standard) is a standard layer defined by Arm. The parts we use are two-fold.

  • CMSIS-CORE: Core peripherals (NVIC, SysTick) and intrinsics (__NOP, NVIC_EnableIRQ, etc.). core_cm3.h.
  • Device Header stm32f10x.h: Provided by ST, defines all peripherals as C structures.
/*The form (concept) provided by stm32f10x.h*/
RCC->APB2ENR  |= RCC_APB2ENR_IOPCEN;  /*Bit names are defined as macros*/
GPIOC->ODR    ^= (1u << 13);          /*Port = Struct pointer with an address*/

RCC and GPIOC are structure pointers pointing to fixed addresses, and bit masks like RCC_APB2ENR_IOPCEN are also defined in the header, allowing you to avoid magic numbers.

3) RCC — The Gateway to Turning on Clocks

RCC (Reset and Clock Control) manages both the clock sources and peripheral clock gates. The enable registers differ depending on which bus each peripheral is attached to.

Bus Enable Register Representative Peripherals
AHB RCC->AHBENR DMA, CRC
APB2 (High-speed) RCC->APB2ENR GPIOA~E, ADC1/2, USART1, TIM1, AFIO
APB1 (Low-speed) RCC->APB1ENR TIM2~4, USART2/3, I2C, SPI2
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;  /*Turn on GPIOA, GPIOC*/

4) Clock Tree

The STM32 clock starts from the source and flows through Busy/Body-ratio to each bus.

HSI (internal 8MHz) ─┐
├─► (PLL × multiplier) ─► SYSCLK ─► AHB(/HPRE) ─► HCLK ─┬─► APB1(/PPRE1) ─► PCLK1
HSE (external 8MHz) ─┘                                                  └─► APB2(/PPRE2) ─► PCLK2
  • HSI: Internal RC oscillator (~8MHz), inaccurate but no external components needed.
  • HSE: External crystal (usually 8MHz), precise. Combined with PLL to generate high-speed clocks.
  • PLL: Multiplies the input by an integer. For F103, 8MHz × 9 = 72MHz is standard.
  • Prescaler: Divides SYSCLK by each bus to fit within the limits.

5) Bus Clock Limits — Why APB1 is part/2-people

The limits for F103 are SYSCLK/HCLK ≤ 72MHz, PCLK1 (APB1) ≤ 36MHz, PCLK2 (APB2) ≤ 72MHz. Therefore, with the standard 72MHz setting, APB1 is /2 (=36MHz), APB2 is /1 (=72MHz). We directly calculate this in pc_test.c.

Note: If the APB prescaler is not 1, the timer clock on that bus becomes 2 times the PCLK (covered in Timer Chapter 17). For simplification, this calculation only deals with the bus clock.

Creating a μVision Project

  1. Project → New µVision Project… → Folder·Name stm32_intro.
  2. Device: STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8.
  3. In the Manage Run-Time Environment window, check CMSIS → CORE, check Device → Startup. (This automatically includes stm32f10x.h along with startup and system_stm32f10x.c.)
  4. Add main.c to the project and copy-paste the code below.
  5. Add device macro STM32F10X_MD to Options for Target → C/C++ → Define. (C8 is 64KB flash = Medium Density)
  6. Select Options for Target → Debug → Use Simulator. Enter -pSTM32F103C8 in the Dialog DLL parameters.
  7. Rebuild(F7) → Start Debug(Ctrl+F5).

Examples

Example 1 — main.c: Turning On Peripheral Clocks

This is a CMSIS standard idiom for turning on the clocks of GPIOA and GPIOC. The goal is to visually confirm the changes in the RCC register bits rather than the operation itself.

#include "stm32f10x.h"

/*
 *Lesson 13 Example — Basic Form of CMSIS Register Access.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM (Arm Compiler 6).
 *
 *The goal of this section is 'understanding the structure.' Actually turning on an LED with GPIO output will be done in Lesson 14.
 *Here, we show the standard idiom for enabling peripheral clocks using CMSIS headers,
 *and observe the result (RCC register bits) changing in the simulator register window.
 *
 *STM32's rule: "Enable the peripheral clock first before using the peripheral."
 *A peripheral with its clock off will not respond even if you write to its registers.
 */

int main(void)
{
    volatile uint32_t heartbeat = 0;   /*A counter showing it is alive.*/

    /*GPIOA and GPIOC are attached to the APB2 bus. Enable the clocks for both ports.
     *Before enabling, the GPIOA->/GPIOC-> registers do not operate.*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;

    /*SystemCoreClock is a global variable managed by CMSIS representing the current core clock (Hz).
     *If added to the Watch window, the system_stm32f10x.c setting (usually 72000000) can be seen.*/

    while (1) {
        heartbeat++;       /*Observe the value increasing in the Watch window.*/
    }
}

Example 2 — pc_test.c: Clock Tree Calculation

Calculate the frequency of each bus from the settings (HSE·PLL multiplier·prescaler). This is an exercise in tracing the clock tree by hand without hardware.

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

/*
 *Lesson 13 PC Check — Manually calculating the STM32F103 clock tree.
 *
 *HSE (external 8MHz) → PLL (× multiplier) → SYSCLK → AHB (/HPRE) → HCLK
 *                                          → APB1(/PPRE1) → PCLK1
 *                                          → APB2(/PPRE2) → PCLK2
 *
 *Determine the frequency of each bus using only the configuration values, without hardware.
 *(F103 constraints: SYSCLK/HCLK ≤ 72MHz, PCLK1 ≤ 36MHz, PCLK2 ≤ 72MHz)
 */

typedef struct {
    uint32_t hse_hz;     /*External crystal frequency*/
    uint32_t pll_mul;    /*PLL multiplication factor (2~16)*/
    uint32_t ahb_div;    /*AHB prescaler (1,2,...,512)*/
    uint32_t apb1_div;   /*APB1 prescaler*/
    uint32_t apb2_div;   /*APB2 prescaler*/
} clock_cfg_t;

static void report(const char *name, clock_cfg_t c)
{
    uint32_t sysclk = c.hse_hz * c.pll_mul;
    uint32_t hclk   = sysclk / c.ahb_div;
    uint32_t pclk1  = hclk / c.apb1_div;
    uint32_t pclk2  = hclk / c.apb2_div;

    printf("[%s]\n", name);
    printf("  SYSCLK = %lu Hz\n", (unsigned long)sysclk);
    printf("  HCLK   = %lu Hz\n", (unsigned long)hclk);
    printf("  PCLK1  = %lu Hz\n", (unsigned long)pclk1);
    printf("  PCLK2  = %lu Hz\n", (unsigned long)pclk2);
}

int main(void)
{
    /*Standard 72MHz configuration: HSE 8MHz × 9 = 72MHz, AHB/1, APB1/2, APB2/1*/
    clock_cfg_t std72 = { 8000000u, 9u, 1u, 2u, 1u };
    /*Another example: HSE 8MHz × 4 = 32MHz, AHB/1, APB1/1, APB2/1*/
    clock_cfg_t cfg32 = { 8000000u, 4u, 1u, 1u, 1u };

    report("72MHz standard", std72);
    report("32MHz example", cfg32);

    return 0;
}

Checking with Simulator

  • After starting debugging, look at APB2ENR in Peripherals → System Viewer → RCC (or the Registers window). After passing one line of main, IOPAEN (bit 2)·IOPCEN (bit 4) become 1, and the value becomes 0x00000014.
  • If you add heartbeat to the Watch window and run it, the value keeps increasing.
  • If you add SystemCoreClock to Watch, the configured core clock (usually 72000000) is visible.
  • Expected behavior: Clock enable bits are set, and counters increase in an infinite loop.

Verification on a PC

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

Expected output (stdout):

[72MHz standard]
  SYSCLK = 72000000 Hz
  HCLK   = 72000000 Hz
  PCLK1  = 36000000 Hz
  PCLK2  = 72000000 Hz
[32MHz example]
  SYSCLK = 32000000 Hz
  HCLK   = 32000000 Hz
  PCLK1  = 32000000 Hz
  PCLK2  = 32000000 Hz

Common Mistakes

Q. I wrote a value to the GPIO register, but nothing happens.

A. In most cases, the clock is not turned on. STM32 peripherals are off by default, so you need to do RCC->APB2ENR |= RCC_APB2ENR_IOPxEN; first for the registers to work. This is the biggest difference from 8051.

Q. I can't find stm32f10x.h, or it says RCC is not defined.

A. Check whether CMSIS:CORE and Device:Startup are checked in Manage Run-Time Environment, and whether a device macro like STM32F10X_MD is added in C/C++ Defines. Without the density macro, the header doesn't know which chip it is, and compilation fails.

Q. I always get confused about which peripheral is APB1 and which is APB2.

A. Roughly remember 'fast ones (high-speed) are APB2, slow ones are APB1', but the clock tree diagram in the datasheet is the correct reference. GPIO, ADC, USART1, TIM1 are on APB2, while TIM2~4, USART2/3, I2C, SPI2 are on APB1.

Q. HSI is 8MHz, so how does it become 72MHz?

A. The PLL multiplies it. If you input HSE (or HSI/2) and multiply by 9, you get 72MHz. In system_stm32f10x.c, SystemInit() applies this setting at the startup stage and records the result in SystemCoreClock.

Summary

  • STM32 is a 32-bit Cortex-M, and each peripheral requires the clock to be enabled via RCC to operate.
  • CMSIS stm32f10x.h provides registers as structures and bit macros to help direct control.
  • Clock tree: source (HSI/HSE) → PLL → SYSCLK → AHB/APB prescaler → each bus.
  • Due to F103 limitations, APB1 is divided by 2 (36MHz), and APB2 is divided by 1 (72MHz) as standard.
  • Without HAL/SPL, directly manipulate registers while maintaining readability using CMSIS standard symbols.

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_intro/main.cC
#include "stm32f10x.h"

/*
 *Lesson 13 Example — Basic Form of CMSIS Register Access.
 *Target: STM32F103C8, Compiler: Keil MDK-ARM (Arm Compiler 6).
 *
 *The goal of this section is 'understanding the structure.' Actually turning on an LED with GPIO output will be done in Lesson 14.
 *Here, we show the standard idiom for enabling peripheral clocks using CMSIS headers,
 *and observe the result (RCC register bits) changing in the simulator register window.
 *
 *STM32's rule: "Enable the peripheral clock first before using the peripheral."
 *A peripheral with its clock off will not respond even if you write to its registers.
 */

int main(void)
{
    volatile uint32_t heartbeat = 0;   /*A counter showing it is alive.*/

    /*GPIOA and GPIOC are attached to the APB2 bus. Enable the clocks for both ports.
     *Before enabling, the GPIOA->/GPIOC-> registers do not operate.*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN;

    /*SystemCoreClock is a global variable managed by CMSIS representing the current core clock (Hz).
     *If added to the Watch window, the system_stm32f10x.c setting (usually 72000000) can be seen.*/

    while (1) {
        heartbeat++;       /*Observe the value increasing in the Watch window.*/
    }
}
src/stm32_intro/pc_test.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Lesson 13 PC Check — Manually calculating the STM32F103 clock tree.
 *
 *HSE (external 8MHz) → PLL (× multiplier) → SYSCLK → AHB (/HPRE) → HCLK
 *                                          → APB1(/PPRE1) → PCLK1
 *                                          → APB2(/PPRE2) → PCLK2
 *
 *Determine the frequency of each bus using only the configuration values, without hardware.
 *(F103 constraints: SYSCLK/HCLK ≤ 72MHz, PCLK1 ≤ 36MHz, PCLK2 ≤ 72MHz)
 */

typedef struct {
    uint32_t hse_hz;     /*External crystal frequency*/
    uint32_t pll_mul;    /*PLL multiplication factor (2~16)*/
    uint32_t ahb_div;    /*AHB prescaler (1,2,...,512)*/
    uint32_t apb1_div;   /*APB1 prescaler*/
    uint32_t apb2_div;   /*APB2 prescaler*/
} clock_cfg_t;

static void report(const char *name, clock_cfg_t c)
{
    uint32_t sysclk = c.hse_hz * c.pll_mul;
    uint32_t hclk   = sysclk / c.ahb_div;
    uint32_t pclk1  = hclk / c.apb1_div;
    uint32_t pclk2  = hclk / c.apb2_div;

    printf("[%s]\n", name);
    printf("  SYSCLK = %lu Hz\n", (unsigned long)sysclk);
    printf("  HCLK   = %lu Hz\n", (unsigned long)hclk);
    printf("  PCLK1  = %lu Hz\n", (unsigned long)pclk1);
    printf("  PCLK2  = %lu Hz\n", (unsigned long)pclk2);
}

int main(void)
{
    /*Standard 72MHz configuration: HSE 8MHz × 9 = 72MHz, AHB/1, APB1/2, APB2/1*/
    clock_cfg_t std72 = { 8000000u, 9u, 1u, 2u, 1u };
    /*Another example: HSE 8MHz × 4 = 32MHz, AHB/1, APB1/1, APB2/1*/
    clock_cfg_t cfg32 = { 8000000u, 4u, 1u, 1u, 1u };

    report("72MHz standard", std72);
    report("32MHz example", cfg32);

    return 0;
}

Assignment

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


Problem 1 — SYSCLK by PLL multiplier and limit check

  • File name: homework_01/main.c
  • Key concepts: clock tree, PLL multiplication, F103 72MHz limit
  • Platform: Common (PC gcc)

Requirements

  • Assuming HSE = 8MHz, run PLL multiplier mul from 2 to 16 and output SYSCLK(8MHz × mul).
  • Append (OK) at the end of each line if SYSCLK is 72MHz or less; append (OVER) if exceeded.
  • Output format PLLMUL xN -> ... Hz (...) must match the expected output character by character. (Multiplier is left-aligned in %-2u)

Expected output

PLLMUL x2  -> 16000000 Hz (OK)
PLLMUL x3  -> 24000000 Hz (OK)
PLLMUL x4  -> 32000000 Hz (OK)
PLLMUL x5  -> 40000000 Hz (OK)
PLLMUL x6  -> 48000000 Hz (OK)
PLLMUL x7  -> 56000000 Hz (OK)
PLLMUL x8  -> 64000000 Hz (OK)
PLLMUL x9  -> 72000000 Hz (OK)
PLLMUL x10 -> 80000000 Hz (OVER)
PLLMUL x11 -> 88000000 Hz (OVER)
PLLMUL x12 -> 96000000 Hz (OVER)
PLLMUL x13 -> 104000000 Hz (OVER)
PLLMUL x14 -> 112000000 Hz (OVER)
PLLMUL x15 -> 120000000 Hz (OVER)
PLLMUL x16 -> 128000000 Hz (OVER)

Hint

  • sysclk = 8000000u * mul; — unsigned is sufficient (128MHz < 2^32).
  • Limit comparison: sysclk <= 72000000u ? "OK" : "OVER".
  • Output: printf("PLLMUL x%-2u -> %lu Hz (%s)\n", mul, (unsigned long)sysclk, tag);.

Problem 2 — Enable clocks for GPIOB and TIM2

  • File name: homework_02/main.c
  • Key concepts: RCC clock gating, APB1 vs APB2
  • Platform: MDK (STM32F103C8)

Requirements

  • Turn on the clocks for GPIOB (APB2 bus) and TIM2 (APB1 bus) respectively.
  • Show in code that the two peripherals are in different enable registers.
  • End with while(1) (STM32 main does not return).

Expected Operation (Simulator)

  • When debugging and looking at the RCC registers, the IOPBEN (bit 3) of APB2ENR becomes 1 (value 0x08), and the TIM2EN (bit 0) of APB1ENR becomes 1 (value 0x01).
  • Step through each line (F11) and check that the corresponding bit is set immediately after each |=.

Hint

  • RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; (GPIOB is high-speed APB2).
  • RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; (TIM2 is low-speed APB1).
  • The bit names are defined in stm32f10x.h, so do not use magic numbers.

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 — SYSCLK by PLL multiplier and limit check
 *
 *Key Points:
 *- SYSCLK = HSE × PLL multiplier. Based on HSE 8MHz, ×9 is 72MHz (standard).
 *- The core clock limit of F103 is 72MHz. OK if below this, OVER if exceeded.
 *
 *Common mistake:
 *- Case where 72MHz is classified as OVER. Since it is 'less than or equal to (<=)', ×9 (72MHz) is OK.
 *- Missing %-2u for alignment can cause arrows to misalign for two-digit multipliers (10~16).
 */

int main(void)
{
    unsigned int mul;

    for (mul = 2; mul <= 16; mul++) {
        uint32_t sysclk = 8000000u * mul;
        const char *tag = (sysclk <= 72000000u) ? "OK" : "OVER";
        printf("PLLMUL x%-2u -> %lu Hz (%s)\n",
               mul, (unsigned long)sysclk, tag);
    }

    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Answer 2 (MDK, STM32F103C8) — Turning on GPIOB and TIM2 clocks
 *
 *Key Points:
 *- The enable register differs because each peripheral is attached to a different bus.
 *GPIOB → High-speed APB2 → IOPBEN in RCC->APB2ENR
 *TIM2  → Low-speed APB1 → TIM2EN in RCC->APB1ENR
 *- Use macros in stm32f10x.h for bit names to avoid magic numbers.
 *
 *Common mistake:
 *- Mistaking GPIOB for APB1 and TIM2 for APB2 (the buses are opposite).
 *- No response occurs if GPIOB/TIM2 registers are accessed without turning on the clock.
 *
 *Expected behavior (simulator): IOPBEN (bit 3) = 1 (0x08) in RCC->APB2ENR,
 *TIM2EN (bit 0) = 1 (0x01) in RCC->APB1ENR can be checked in the register window.
 */

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;   /*GPIOB clock (APB2)*/
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;   /*TIM2 clock (APB1)*/

    while (1) {
        /*This part covers clock enable verification. Actual GPIO/timer operation is in the following parts.*/
    }
}

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