Skip to content

Repository files navigation

Architecture of STM32F3DISCOVERY

Cortex-M4 core, buses, and Flash interface:

STM32F303 core, buses and ports to Flash

Bus matrix, slaves, and peripherals:

STM32F303 bus matrix, slaves and peripherals

Memory-Mapped I/O — Accessing a GPIO Register

IO pins are controlled using peripheral registers, and those registers are mapped onto processor addressable memory locations — you read and write them exactly like you'd read and write a variable in RAM. This scheme is called memory-mapped I/O.

The system bus (AHB) is what connects the CPU core to memory and to every peripheral: a 32-bit address channel says where, a 32-bit data channel carries what. Because the address channel is 32 bits wide, the processor can produce 2^32 different addresses — 4 gigabytes — ranging from 0x0000_0000 to 0xFFFF_FFFF. Program memory (Flash), data memory (SRAM), and the registers of every peripheral (GPIO, timers, USART, ...) all live somewhere inside that same linear 4 GB address space; each one is just given its own slice of addresses.

0x0000_0000 ┌───────────────────────┐
            │ Code (Flash)          │  program memory
0x2000_0000 ├───────────────────────┤
            │ SRAM                  │  data memory
0x4000_0000 ├───────────────────────┤
            │ Peripherals           │  GPIOx, USART, timers, ... registers
0x6000_0000 ├───────────────────────┤
            │ ...                   │
0xFFFF_FFFF └───────────────────────┘

To toggle a physical pin, you find the memory address of the register that controls it, and read/write that address like any other pointer. For example, to set pin PD15 (bit 15 of port D's output data register) high:

  1. Peripheral base address — port D (GPIOD) sits on the AHB2 bus at base address 0x4800_0C00 (reference manual §3.2.2, memory map: 0x4800_0C00 - 0x4800_0FFF).
  2. Register offset — the output data register (ODR) is at offset 0x14 within a GPIO port's register block.
  3. Register address — base + offset = 0x4800_0C00 + 0x14 = 0x4800_0C14.
  4. Bit within the register — pin 15 corresponds to bit 15 of that 32-bit register.
#define GPIOD_ODR (*(volatile uint32_t *)0x48000C14)

GPIOD_ODR |= (1 << 15);   // drive PD15 high
GPIOD_ODR &= ~(1 << 15);  // drive PD15 low

The volatile qualifier tells the compiler this memory location can change outside the normal flow of the program (or that writes to it have a side effect), so it must never optimize the read/write away. In practice, this raw pointer is what a vendor HAL/CMSIS struct like GPIOD->ODR expands to under the hood.

GPIO Peripheral Registers

ODR is only one of several registers in a GPIO port's block — each one is a 32-bit register offset from the port's base address (e.g. 0x4800_0C00 for GPIOD), where every pin gets 1 or 2 bits of space within the register:

Offset Register Purpose
0x00 MODER (mode register) 2 bits per pin — selects input, general-purpose output, alternate function, or analog mode
0x04 OTYPER (output type register) 1 bit per pin — push-pull (drives high and low) or open-drain (drives low only)
0x08 OSPEEDR (output speed register) 2 bits per pin — sets the output slew rate (low/medium/high/very high), trading speed for noise/EMI
0x0C PUPDR (pull-up/pull-down register) 2 bits per pin — enables an internal pull-up, pull-down, or neither
0x10 IDR (input data register) 1 bit per pin, read-only — the current logic level actually present on the pin
0x14 ODR (output data register) 1 bit per pin, read/write — the logic level the pin drives when configured as output
0x18 BSRR (bit set/reset register) write-only — writing a 1 to bit n sets pin n, writing a 1 to bit n+16 resets it, atomically (no read-modify-write, so no race with an interrupt)
0x1C LCKR (configuration lock register) locks MODER/OTYPER/OSPEEDR/PUPDR/AFR for the selected pins until the next reset
0x20/0x24 AFRL / AFRH (alternate function low/high) 4 bits per pin — picks which alternate function (0-15), e.g. USART/SPI/timer, a pin is routed to
			┌----------------------------------------┐
0x4800_0C24 |31	|	GPIOD AFRL/AFRH register      |	0|	
			└----------------------------------------┘			
	...
			┌----------------------------------------┐
0x4800_0C08 |31	|	GPIOD output speed register	  |	0|	
			└----------------------------------------┘
	+4
			┌----------------------------------------┐
0x4800_0C04 |31	|	GPIOD output type register	  |	0|	
			└----------------------------------------┘
	+4
			┌----------------------------------------┐
0x4800_0C00 |31	|	GPIOD port mode register	  |	0|	
			└----------------------------------------┘

For example, before ODR can drive PD15 high, MODER bits 30-31 must be set to 01 (general-purpose output) — otherwise the pin is left in its default input/analog state and writing to ODR has no effect on the physical pin. To actually drive the pin, write to the output data register (ODR), not the output type register — OTYPER only chooses push-pull vs. open-drain, it doesn't hold the logic level:

					GPIOD output data register (ODR)
			┌-------------------------------------------┐
0x4800_0C14 |31	|	|	|	| 15|	|12	|	|	|1	|0	|		  
			└-------------------------------------------┘
							  |		 |			  |	 └-------PD0	
							  |		 |			  └-------PD1
							  |		 └-------PD12
							  └-------PD15

Procedure — Turning On an LED

Putting the pieces above together, here's the general procedure for driving one GPIO pin high, e.g. an LED wired to PD12:

  1. Identify the GPIO peripheral — GPIOD.
  2. Identify the pin the LED is connected to — pin 12.
  3. Enable the peripheral clock. On reset, the clock to every peripheral is off to save power, so GPIOD's registers won't respond until its clock is turned on. Clock control lives in the RCC (Reset and Clock Control) peripheral:
    • RCC is mapped at 0x4002_1000 - 0x4002_13FF on the AHB1 bus.
    • The AHB peripheral clock enable register (RCC_AHBENR) is at offset 0x14 → address 0x4002_1000 + 0x14 = 0x4002_1014.
    • Bit 20 of that register is IOPDEN (I/O port D clock enable).
    #define RCC_AHBENR (*(volatile uint32_t *)0x40021014)
    RCC_AHBENR |= (1 << 20);  // enable GPIOD's clock
  4. Configure the pin as output. GPIOD_MODER is at offset 0x00 → address 0x4800_0C00 + 0x00 = 0x4800_0C00. Each pin gets 2 bits, at position pin * 2, so pin 12 uses bits 24-25; 01 selects general-purpose output:
straight from the reference manual's table for MODER:
	┌──────┬────────────────────────┐
	│ Bits │          Mode          │
	├──────┼────────────────────────┤
	│ 00   │ Input                  │
	├──────┼────────────────────────┤
	│ 01   │ General-purpose output │
	├──────┼────────────────────────┤
	│ 10   │ Alternate function     │
	├──────┼────────────────────────┤
	│ 11   │ Analog                 │
	└──────┴────────────────────────┘
#define GPIOD_MODER (*(volatile uint32_t *)0x48000C00)
GPIOD_MODER &= ~(0b11 << (12 * 2));  // clear bits 24-25
GPIOD_MODER |=  (0b01 << (12 * 2));  // set mode = output
  1. Write to the pin. GPIOD_ODR is at offset 0x14 → address 0x4800_0C00 + 0x14 = 0x4800_0C14. Setting bit 12 drives PD12 (and the LED) high:

    #define GPIOD_ODR (*(volatile uint32_t *)0x48000C14)
    GPIOD_ODR |= (1 << 12);  // turn the LED on

    Or the atomic way

    #define GPIOD_BSRR (*(volatile uint32_t *)0x48000C18)
    GPIOD_BSRR = (1 << 12); 

Bit Extraction — Reading Back the Pin's Mode

Writing a field uses a clear-then-set: &= ~(mask) then |= (value). Reading a field back out goes the other direction — shift the field down to bit 0, then mask off everything that came down with it:

uint32_t mode = (GPIOD_MODER >> (12 * 2)) & 0b11;

Say GPIOD_MODER currently holds 0x0100_0000 — every pin is 00 (input) except pin 12, whose field (bits 25:24) is 01 (general-purpose output):

  0000 0001 0000 0000 0000 0000 0000 0000   MODER (pin 12's field = bits 25:24 = 01)
>> 24                                        shift right by (12 * 2): move that field down to bits 1:0
------------------------------------------
  0000 0000 0000 0000 0000 0000 0000 0001   pin 12's field is now in bits 1:0, but bits above it also shifted down
& 0000 0000 0000 0000 0000 0000 0000 0011   mask (0b11): strip everything except those 2 bits
------------------------------------------
  0000 0000 0000 0000 0000 0000 0000 0001   mode = 0b01 -> General-purpose output

The shift alone isn't enough — without the & 0b11, mode would also carry whatever bits 13-31's fields shifted down into position 2 and above. The mask is what isolates pin 12's 2 bits from all the neighboring fields that rode along with the shift.

Useful Commands

arm-none-eabi-objdump -h *.elf

Displays the section headers of the compiled ELF file (e.g. .text, .data, .bss), including their size and memory address — useful for checking how much Flash and RAM a build actually uses.

arm-none-eabi-objdump -d *.elf

Disassembles the executable sections of the ELF file into assembly instructions — useful for inspecting the actual machine code generated for a function or debugging at the instruction level.

Bitwise Operations in Embedded C

Embedded programming works directly with hardware registers, where each individual bit can control or reflect the state of a peripheral (e.g. a GPIO pin). Bitwise operators let you set, clear, toggle, or check specific bits without disturbing the others in the same register.

AND (&) — clear a bit / check a bit

// Clear bit 5 of a register (e.g. turn off GPIOx pin 5)
GPIOx->ODR &= ~(1 << 5);
  00101000   ODR before (bit 5 set)
& 11011111   ~(1 << 5)
----------
  00001000   ODR after (bit 5 cleared)

OR (|) — set a bit

// Set bit 5 of a register (e.g. turn on GPIOx pin 5)
GPIOx->ODR |= (1 << 5);
  00001000   ODR before (bit 5 clear)
| 00100000   1 << 5
----------
  00101000   ODR after (bit 5 set)

XOR (^) — toggle a bit

// Toggle bit 5 of a register (e.g. blink an LED on GPIOx pin 5)
GPIOx->ODR ^= (1 << 5);
  00001000   ODR before (bit 5 clear)
^ 00100000   1 << 5
----------
  00101000   ODR after (bit 5 toggled on)

NOT (~) — invert bits (build a mask)

// Build a mask with every bit set except bit 5, used to clear that bit
uint32_t mask = ~(1 << 5);
~ 00100000   1 << 5
----------
  11011111   mask (every bit set except bit 5)

Left shift (<<) — position a value into a bit field

// Move the value 0b11 into bits 4-5 of a mode register (e.g. set GPIO pin 2 to alternate function mode)
GPIOx->MODER |= (0b10 << 4);
  00000010   0b10
<< 4         shift left by 4 positions
----------
  00100000   result placed into bits 4-5

Right shift (>>) — read a bit field out of a register

// Read bits 4-5 back out of the mode register into the lowest two bits
uint32_t mode = (GPIOx->MODER >> 4) & 0b11;
  00100000   MODER (bits 4-5 hold the mode)
>> 4         shift right by 4 positions
----------
  00000010   bits 4-5 now in bit positions 0-1
& 00000011   mask off everything else
----------
  00000010   mode

Bit Masking

A bit mask is a value where the bits you care about are set to 1 and everything else is 0. Combining a register with a mask through the right operator lets you test, set, clear, or toggle specific bits while leaving the rest of the register untouched.

Testing a bit — &

AND the register with a mask that has only the target bit set. The result is non-zero only if that bit was set.

// Check whether bit 5 of IDR is set (e.g. is the button pressed)
if (GPIOx->IDR & (1 << 5)) {
    // bit 5 is set
}
  00101000   IDR
& 00100000   mask (1 << 5)
----------
  00100000   non-zero -> bit 5 is set

Setting a bit — |

OR the register with the mask. Bits set in the mask are forced to 1; every other bit keeps its value.

// Set bit 5 of ODR (e.g. turn on GPIOx pin 5)
GPIOx->ODR |= (1 << 5);
  00001000   ODR before
| 00100000   mask (1 << 5)
----------
  00101000   ODR after — bit 5 forced to 1

Clearing a bit — & ~

AND the register with the inverted mask. Bits set in the mask are forced to 0; every other bit keeps its value.

// Clear bit 5 of ODR (e.g. turn off GPIOx pin 5)
GPIOx->ODR &= ~(1 << 5);
  00101000   ODR before
& 11011111   ~mask (~(1 << 5))
----------
  00001000   ODR after — bit 5 forced to 0

Toggling a bit — ^

XOR the register with the mask. Bits set in the mask are flipped; every other bit keeps its value.

// Toggle bit 5 of ODR (e.g. blink an LED on GPIOx pin 5)
GPIOx->ODR ^= (1 << 5);
  00001000   ODR before
^ 00100000   mask (1 << 5)
----------
  00101000   ODR after — bit 5 flipped

About

Embedded C projects on STM32F3DISCOVERY microcontroller

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages