Embedded C track

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

LED with RCC & GPIO Registers

CRL/CRH and ODR/BSRR

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

We first put into practice the foundation built in Chapter 13 (CMSIS·RCC·clock tree). The STM32's "Hello, World" — LED blinking — is implemented using only registers without HAL. On Blue Pill series boards, the onboard LED is connected to PC13 as active low, so you can make this pin an output and toggle it to see the LED blink.

The key is the STM32F1's unique GPIO configuration method. Unlike the MODER of the F4/L4 series, F1 uses 4 bits per pin (MODE 2 bits + CNF 2 bits) in CRL (pins 0–7)·CRH (pins 8–15). Understanding this 4-bit combination allows all GPIO settings of the STM32F1 to follow the same pattern.

Learning Objectives

  • Turn on the GPIO clock using RCC and configure the F1 With-CRL/CRH pin mode.
  • Use the 4-bit MODE/CNF combination to create a "push-pull output 2MHz".
  • Toggle the pin with ODR and perform atomic set/reset with BSRR/BRR.
  • Understand the ON/OFF logic of the active low LED (PC13).
  • Confirm pin toggling with the simulator Register/Logic Analyzer.

Target Environment

  • Compiler: Keil MDK-ARM (Arm Compiler 6).
  • Device: STM32F103C8 (μVision simulator).
  • Verification: μVision simulator (GPIOC registers, Logic Analyzer PC13). Bit manipulation uses PC gcc(pc_test.c).

Key Concepts

1) GPIO Configuration of F1 — with-CRL/CRH 4 bits

The STM32F1 sets the mode with 4 bits per pin. Pins 0–7 are in CRL, and pins 8–15 are in CRH. The 4 bits for pin n are placed at (n % 8) * 4.

Bit Name Meaning
[1:0] MODE 00=input, 01=output 10MHz, 10=output 2MHz, 11=output 50MHz
[3:2] CNF(output) 00=push-pull, 01=open-drain, 10=AF push-pull, 11=AF open-drain
[3:2] CNF(input) 00=analog, 01=floating, 10=Pull-up/Pulldown, 11=reserved

2) "Push-pull output 2MHz" = 0x2

Since LED driving does not need to be fast, 2MHz is sufficient. MODE=10 (2MHz output), CNF=00 (general-purpose push-pull) → 4-bit 0010 = 0x2.

PC13 → CRH, position = (13 - 8) * 4 = 4 bits starting from bit 20
Write 0x2 to [23:20] of CRH.

3) Proper setting — clear then set

To change only 4 bits, first clear that spot to 0, then OR in the new value. This prevents affecting the settings of other pins.

GPIOC->CRH &= ~(0xFu << 20);   /*Clear 4-bit at PC13*/
GPIOC->CRH |=  (0x2u << 20);   /*Set 0010*/

4) Pin output — ODR vs BSRR/BRR

  • ODR (Output Data Register): read and write the entire port. Convenient for toggling (ODR ^= (1<<13)).
  • BSRR (Bit Set/Reset): lower 16 bits=set, upper 16 bits=reset. Atomically sets a specific pin to 1 without reading.
  • BRR (Bit Reset): sets a specific pin to 0.
GPIOC->BSRR = (1u << 13);          /*PC13 = 1 (If active low, LED OFF)*/
GPIOC->BRR  = (1u << 13);          /* PC13 = 0 (LED ON) */
GPIOC->ODR ^= (1u << 13);          /*Toggle*/

5) Active Low LED (PC13)

The PC13 LED on the Blue Pill is arranged as VCC—LED—resistor—pin, so the LED is ON when pin=0. Therefore, when toggled, it is ON for half the cycle and OFF for the other half.

Creating a μVision Project

  1. Project → New µVision Project… → name it stm32_gpio_out.
  2. Device: STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8.
  3. Check Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste the code below.
  5. Apply STM32F10X_MD to Options → C/C++ → Define.
  6. In Options → Debug → Use Simulator, set Dialog parameter -pSTM32F103C8.
  7. Rebuild(F7) → Start Debug(Ctrl+F5).

Examples

Example 1 — main.c: PC13 LED Blinking

#include "stm32f10x.h"

/*
 *Lesson 14 Example — Onboard LED (PC13) Blinking.
 *Target: STM32F103C8 (Blue Pill series), Compiler: Keil MDK-ARM.
 *
 *The Blue Pill board has an LED on PC13 with active-low (PIN=0 means ON).
 *GPIO configuration of STM32F1 uses CRL/CRH registers, 4 bits per pin (MODE+CNF).
 */

#define LED_PIN   13u                 /* PC13 */

static void delay(volatile uint32_t count)
{
    while (count--) { }
}

int main(void)
{
    /*1) Turn on GPIOC clock (APB2 bus)*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    /*2) Set PC13 as push-pull output (2MHz).
     *PC13 is in pin 8~15 range, so use CRH. 4-bit position = (13-8)*4 = 20.
     *MODE=10 (2MHz output), CNF=00 (general-purpose push-pull) → 4-bit value 0b0010 = 0x2.*/
    GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));   /*Clear the corresponding 4 bits*/
    GPIOC->CRH |=  (0x2u << ((LED_PIN - 8u) * 4u));   /*Set 0010*/

    while (1) {
        GPIOC->ODR ^= (1u << LED_PIN);   /*Toggle PC13*/
        delay(200000u);
    }
}

Example 2 — pc_test.c: CRH Bit Manipulation Check

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

/*
 *Lecture 14 PC Check — Imitating CRL/CRH Pin Setting Bit Manipulation of STM32F1.
 *Calculates read-modify-write to change a single pin to push-pull output (2MHz, 4-bit value 0x2) without hardware.
 *The reset value of CRH is 0x44444444 (all pins floating input).
 */

/*Returns the new CRH value with pin(8~15) changed to mode4 (4-bit setting).*/
static uint32_t set_pin_config(uint32_t crh, unsigned int pin, uint32_t mode4)
{
    unsigned int shift = (pin - 8u) * 4u;
    crh &= ~(0xFu << shift);     /*Clear the existing 4 bits.*/
    crh |=  (mode4 << shift);    /*Insert the new 4 bits.*/
    return crh;
}

int main(void)
{
    uint32_t crh = 0x44444444u;     /*CRH reset value.*/

    printf("reset CRH = 0x%08lX\n", (unsigned long)crh);
    crh = set_pin_config(crh, 13u, 0x2u);   /*PC13 = push-pull output 2MHz.*/
    printf("after PC13 output = 0x%08lX\n", (unsigned long)crh);

    return 0;
}

Checking with Simulator

  • Confirm in Peripherals → General Purpose I/O → GPIOC (or Registers) that CRH becomes 0x44244444 (only bit 0x2 for PC13).
  • Add PORTC.13 to the Logic Analyzer in Bit mode and run it; you will see a square wave on PC13 toggling periodically.
  • Expected behavior: PC13 toggles 0↔1 at a regular interval (if on a real board, the onboard LED blinks).

Verification on a PC

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

Expected output (stdout):

reset CRH = 0x44444444
after PC13 output = 0x44244444

Common Mistakes

Q. I set it clearly, but the pin doesn’t move.

A. Make sure you turn on the GPIO clock (RCC->APB2ENR |= RCC_APB2ENR_IOPCEN) first. Without the clock, using to-CRH/ODR will not have any effect.

Q. Why is PC13 (13-8)*4 if it’s in CRH?

A. CRL holds pins 07, and CRH holds pins 815. Within CRH, pin 8 corresponds to the first 4 bits, so pin 13 is the (13-8)=5-th one → starting from bit 20.

Q. Changing this also changes the adjacent pin configuration.

A. If you overwrite with = as a whole, the other pins will be affected. Always clear that spot with &= ~(0xF<<shift) and insert the new value with |=.

Q. The LED is always on or off.

A. Remember that it is active low (PC13=0 is ON). Also, if delay is not used, it may seem to turn on too quickly. To check the toggle itself, using a Logic Analyzer is accurate.

Summary

  • F1 GPIO uses 4 bits per pin (MODE+CNF) in in-CRL/CRH. Push-pull output 2MHz = 0x2.
  • When changing only 4 bits, use the "clear and set" pattern to preserve the other pins.
  • Output is done with ODR (toggle) and BSRR/BRR (atomic set/reset).
  • PC13 LED is active low — pin=0 is ON.
  • The prerequisite for all operations is RCC clock enable.

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

/*
 *Lesson 14 Example — Onboard LED (PC13) Blinking.
 *Target: STM32F103C8 (Blue Pill series), Compiler: Keil MDK-ARM.
 *
 *The Blue Pill board has an LED on PC13 with active-low (PIN=0 means ON).
 *GPIO configuration of STM32F1 uses CRL/CRH registers, 4 bits per pin (MODE+CNF).
 */

#define LED_PIN   13u                 /* PC13 */

static void delay(volatile uint32_t count)
{
    while (count--) { }
}

int main(void)
{
    /*1) Turn on GPIOC clock (APB2 bus)*/
    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;

    /*2) Set PC13 as push-pull output (2MHz).
     *PC13 is in pin 8~15 range, so use CRH. 4-bit position = (13-8)*4 = 20.
     *MODE=10 (2MHz output), CNF=00 (general-purpose push-pull) → 4-bit value 0b0010 = 0x2.*/
    GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));   /*Clear the corresponding 4 bits*/
    GPIOC->CRH |=  (0x2u << ((LED_PIN - 8u) * 4u));   /*Set 0010*/

    while (1) {
        GPIOC->ODR ^= (1u << LED_PIN);   /*Toggle PC13*/
        delay(200000u);
    }
}
src/stm32_gpio_out/pc_test.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Lecture 14 PC Check — Imitating CRL/CRH Pin Setting Bit Manipulation of STM32F1.
 *Calculates read-modify-write to change a single pin to push-pull output (2MHz, 4-bit value 0x2) without hardware.
 *The reset value of CRH is 0x44444444 (all pins floating input).
 */

/*Returns the new CRH value with pin(8~15) changed to mode4 (4-bit setting).*/
static uint32_t set_pin_config(uint32_t crh, unsigned int pin, uint32_t mode4)
{
    unsigned int shift = (pin - 8u) * 4u;
    crh &= ~(0xFu << shift);     /*Clear the existing 4 bits.*/
    crh |=  (mode4 << shift);    /*Insert the new 4 bits.*/
    return crh;
}

int main(void)
{
    uint32_t crh = 0x44444444u;     /*CRH reset value.*/

    printf("reset CRH = 0x%08lX\n", (unsigned long)crh);
    crh = set_pin_config(crh, 13u, 0x2u);   /*PC13 = push-pull output 2MHz.*/
    printf("after PC13 output = 0x%08lX\n", (unsigned long)crh);

    return 0;
}

Assignment

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


Problem 1 — F1 GPIO 4-bit Setting Code Calculation

  • File name: homework_01/main.c
  • Key concept: MODE/CNF bit combination, CRL/CRH 4-bit
  • Platform: Common (PC gcc)

Requirements

  • Calculate and output the 4-bit pin setting code (CNF << 2) | MODE from the (MODE, CNF) pair.
  • Output the following 5 combinations in order. The format mode=M cnf=C -> 0xN must match the expected output character by character.

Expected output

mode=0 cnf=1 -> 0x4
mode=2 cnf=0 -> 0x2
mode=3 cnf=0 -> 0x3
mode=2 cnf=1 -> 0x6
mode=0 cnf=2 -> 0x8

Hint

  • The lower 2 bits are MODE, the upper 2 bits are CNF: code = (cnf << 2) | mode;.
  • First line (0x4) is floating input, second (0x2) is 2MHz push-pull output.

Problem 2 — PA5 Output Blinking

  • File name: homework_02/main.c
  • Key concept: GPIO clock, CRL setting, ODR toggle
  • Platform: MDK (STM32F103C8)

Requirements

  • Turn on GPIOA clock, and set PA5 as a 2MHz push-pull output.
  • Toggle PA5 in while(1) with delay.

Expected Operation (Simulator)

  • Since PA5 is in the pin0~7 area, use CRL (position 5*4=20). After setting, [23:20] of GPIOA->CRL becomes 0x2 and the value becomes 0x44244444.
  • If you add PORTA.5 to the Logic Analyzer, you can see a periodic toggling square wave.

Hint

  • RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
  • GPIOA->CRL &= ~(0xFu << (5*4)); GPIOA->CRL |= (0x2u << (5*4));
  • Toggle: GPIOA->ODR ^= (1u << 5);

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 — F1 GPIO 4-bit Configuration Code Calculation
 *
 *Key Points:
 *- 4 bits for one pin configuration = (CNF << 2) | MODE.
 *- MODE: 00 input, 01/10/11 output (10/2/50MHz). The meaning of CNF varies depending on input/output.
 *
 *Common mistake:
 *- In case of swapping MODE and CNF positions (lower 2 bits are MODE, upper 2 bits are CNF).
 */

static unsigned int gpio_code(unsigned int mode, unsigned int cnf)
{
    return (cnf << 2) | mode;     /* [3:2]=CNF, [1:0]=MODE */
}

int main(void)
{
    unsigned int mode[5] = { 0u, 2u, 3u, 2u, 0u };
    unsigned int cnf[5]  = { 1u, 0u, 0u, 1u, 2u };
    int i;

    for (i = 0; i < 5; i++) {
        printf("mode=%u cnf=%u -> 0x%X\n",
               mode[i], cnf[i], gpio_code(mode[i], cnf[i]));
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Correct answer 2 (MDK, STM32F103C8) — PA5 output blinking
 *
 *Key Points:
 *- PA5 is pin 0~7, so use CRL. 4-bit position = 5*4 = 20.
 *- Push-pull output 2MHz = 0x2. Preserve adjacent pins by clearing first then setting.
 *
 *Common mistake:
 *- If PA5 is set in CRH (pins 8~15 go to CRH, pins 0~7 go to CRL).
 *- If you forget to enable the clock (IOPAEN).
 *
 *Expected operation (simulator): GPIOA->CRL = 0x44244444 (not PC13 position, PA5 position = 0x2),
 *PA5 (PORTA.5) toggles periodically.
 */

#define LED_PIN   5u

static void delay(volatile uint32_t count)
{
    while (count--) { }
}

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;                 /*GPIOA clock*/

    GPIOA->CRL &= ~(0xFu << (LED_PIN * 4u));            /*Clear PA5 position*/
    GPIOA->CRL |=  (0x2u << (LED_PIN * 4u));            /*Push-pull output 2MHz*/

    while (1) {
        GPIOA->ODR ^= (1u << LED_PIN);                  /*Toggle PA5*/
        delay(200000u);
    }
}

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