Skip to content

Chapter 02

Hongjayeong-02 edited this page Oct 4, 2026 · 2 revisions

02. Peripheral Registers

Goal

Control a hardware peripheral without HAL or CMSIS register definitions.

In this chapter:

  • Read the STM32 memory map
  • Calculate peripheral register addresses
  • Map a physical address into C
  • Understand volatile
  • Read and modify register bit fields
  • Control the NUCLEO-F439ZI LD1 LED through GPIOB

Target:

Board : NUCLEO-F439ZI
MCU   : STM32F439ZI
LED   : LD1
Pin   : PB0

1. Memory-Mapped I/O

1.1 What is a Peripheral Register?

GPIO, UART, Timer, ADC, RCC and other peripherals contain internal registers.

From the CPU's point of view, these registers are assigned normal addresses in the memory address space.

This architecture is called:

Memory-Mapped I/O (MMIO)

CPU
 │
 │ address + read/write
 ▼
Bus
 │
 ├── SRAM
 ├── FLASH
 ├── GPIO
 ├── UART
 ├── TIMER
 └── ADC

즉 CPU는 GPIO를 제어하기 위해 특별한 GPIO instruction을 실행하는 것이 아니다.

CPU가 하는 일은 결국:

특정 주소를 읽는다
특정 주소에 값을 쓴다

뿐이다.

예를 들어 PB0의 출력 값을 결정하는 GPIOB_ODR의 주소는:

0x40020414

이다.

CPU가 이 주소의 bit 0을 1로 만들면 GPIOB hardware가 PB0 출력을 High로 변경한다.

CPU
 │
 │ write bit0 = 1
 ▼
0x40020414
 │
 ▼
GPIOB_ODR
 │
 ▼
PB0 = HIGH
 │
 ▼
LD1 ON

2. Where Do the Addresses Come From?

Bare-metal programming에서는 주소를 외우는 것이 아니라 문서에서 찾아 계산해야 한다.

STM32를 사용할 때 주로 보는 문서는 다음과 같다.

Reference Manual

Peripheral의:

  • Base address
  • Register offset
  • Bit field
  • Reset value
  • Register operation

을 확인한다.

STM32F439ZI에서는 RM0090 Reference Manual이 핵심이다.

Datasheet

주로 다음을 확인한다.

  • Pinout
  • Alternate Function
  • Electrical characteristics
  • Package information

Board User Manual / Schematic

MCU pin이 실제 board의 무엇과 연결되었는지 확인한다.

예:

LD1 → PB0
B1  → PC13

즉:

Board Manual
    ↓
Which pin?

Reference Manual
    ↓
Which register?

Memory Map
    ↓
Which address?

Register Description
    ↓
Which bit?

순서로 읽으면 된다.


3. STM32F4 Peripheral Address Mapping

STM32F4 peripheral 영역은 다음 base address에서 시작한다.

PERIPH_BASE = 0x40000000

이번 실습에서 GPIOB와 RCC는 AHB1 bus에 연결되어 있다.

Peripheral Base
0x40000000
     │
     │ + 0x00020000
     ▼
AHB1 Peripheral Base
0x40020000

C에서는:

#define PERIPH_BASE       (0x40000000UL)
#define AHB1PERIPH_BASE   (PERIPH_BASE + 0x00020000UL)

따라서:

AHB1PERIPH_BASE
= 0x40000000 + 0x00020000
= 0x40020000

4. Peripheral Base Address

4.1 GPIOB

GPIOB offset from AHB1 base:

0x0400

Therefore:

GPIOB_BASE
= AHB1PERIPH_BASE + 0x0400

= 0x40020000
+ 0x00000400

= 0x40020400

C:

#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400UL)

4.2 RCC

RCC offset:

0x3800

Therefore:

RCC_BASE
= AHB1PERIPH_BASE + 0x3800

= 0x40020000
+ 0x00003800

= 0x40023800

C:

#define RCC_BASE (AHB1PERIPH_BASE + 0x3800UL)

5. Base Address + Register Offset

Peripheral base address는 peripheral 전체의 시작 주소다.

실제 register 주소를 얻으려면:

Register Address
=
Peripheral Base Address
+
Register Offset

를 계산한다.

이번 실습에서는 세 개의 register를 사용한다.

RCC_AHB1ENR
GPIOB_MODER
GPIOB_ODR

6. RCC_AHB1ENR

6.1 Address

RCC base:

0x40023800

AHB1ENR offset:

0x30

따라서:

RCC_AHB1ENR
= RCC_BASE + 0x30

= 0x40023800
+ 0x00000030

= 0x40023830

C:

#define RCC_AHB1ENR \
    (*(volatile unsigned int *)(RCC_BASE + 0x30UL))

6.2 GPIOB Clock Enable

RCC_AHB1ENR에서 GPIOB clock은 bit 1이다.

bit 1 = GPIOBEN

Mask:

#define GPIOBEN (1UL << 1)

계산하면:

1 << 1

0000 0001
     ↓ shift

0000 0010

= 0x00000002

Clock enable:

RCC_AHB1ENR |= GPIOBEN;

CPU 관점에서는 사실상:

READ  0x40023830
        ↓
OR    0x00000002
        ↓
WRITE 0x40023830

를 수행하는 것이다.


7. Why Enable the Peripheral Clock?

STM32는 power consumption을 줄이기 위해 사용하지 않는 peripheral의 clock을 기본적으로 차단한다.

이를 Clock Gating이라고 한다.

System Clock
     │
     ▼
RCC
     │
     ├── GPIOA Clock
     ├── GPIOB Clock
     ├── GPIOC Clock
     └── ...

Reset 직후 GPIOB clock이 disabled 상태라면:

CPU
 ↓
GPIOB register write
 ↓
GPIOB clock OFF
 ↓
Peripheral does not operate

따라서 GPIO register를 설정하기 전에 먼저:

RCC_AHB1ENR |= GPIOBEN;

를 수행한다.

Peripheral configuration의 일반적인 순서도 비슷하다.

Enable Peripheral Clock
          ↓
Configure Peripheral Registers
          ↓
Enable Peripheral
          ↓
Run

8. GPIOB_MODER

8.1 Address

GPIOB base:

0x40020400

MODER offset:

0x00

Therefore:

GPIOB_MODER
= 0x40020400 + 0x00
= 0x40020400

C:

#define GPIOB_MODER \
    (*(volatile unsigned int *)(GPIOB_BASE + 0x00UL))

9. GPIO MODER Bit Mapping

Each GPIO pin uses 2 bits in MODER.

Pin     MODER Bits

PB0     [1:0]
PB1     [3:2]
PB2     [5:4]
PB3     [7:6]
...
PB15    [31:30]

General formula:

Pin n
→ bits [(2n+1):(2n)]

For PB0:

n = 0

bit position
= 0 × 2
= 0

PB0 → MODER[1:0]

Mode encoding:

00 = Input
01 = General Purpose Output
10 = Alternate Function
11 = Analog

We need:

PB0 = Output

MODER[1:0] = 01

10. Setting a Multi-Bit Field

잘못된 방식:

GPIOB_MODER |= 1;

현재 값이 무엇인지 모르는 상태에서는 안전하지 않다.

예를 들어 기존 값이:

11

이면:

11 OR 01 = 11

이므로 Output mode가 되지 않는다.

따라서 일반적으로:

CLEAR
  ↓
SET

두 단계로 처리한다.

Step 1 — Clear

GPIOB_MODER &= ~(0x3UL << (0 * 2));

0x3:

binary = 11

PB0 위치는 0이므로:

0x3 << 0
= 00000011

NOT:

~00000011
=
11111100

AND하면:

xxxx xxxx
AND
1111 1100
-----------
xxxx xx00

PB0 field만 00이 된다.


Step 2 — Set

GPIOB_MODER |= (0x1UL << (0 * 2));
01

을 OR한다.

결과:

MODER[1:0] = 01

따라서:

PB0 = General Purpose Output

11. GPIOB_ODR

11.1 Address

GPIOB base:

0x40020400

ODR offset:

0x14

Therefore:

GPIOB_ODR
= 0x40020400
+ 0x14

= 0x40020414

C:

#define GPIOB_ODR \
    (*(volatile unsigned int *)(GPIOB_BASE + 0x14UL))

12. ODR Bit Mapping

ODR uses one bit per output pin.

ODR bit 0  → PB0
ODR bit 1  → PB1
ODR bit 2  → PB2
...
ODR bit 15 → PB15

For LD1:

LD1 = PB0

Mask:

#define LD1_PIN (1UL << 0)

which becomes:

0x00000001

13. C Pointer Mapping

Now look at this expression carefully.

#define GPIOB_ODR \
    (*(volatile unsigned int *)0x40020414UL)

이 한 줄이 bare-metal register programming에서 가장 중요한 부분 중 하나다.

분해하면 다음과 같다.


13.1 Address Constant

0x40020414UL

이 단계에서는 단순한 integer constant이다.

Value = 0x40020414

CPU address라는 의미를 C compiler가 자동으로 아는 것은 아니다.


13.2 Pointer Cast

(volatile unsigned int *)0x40020414UL

정수 0x40020414를:

pointer to volatile unsigned int

로 변환한다.

즉:

0x40020414
     │
     ▼
"이 주소에는 32-bit register가 있다"

라고 C compiler에게 알려주는 것이다.


13.3 Dereference

앞에 *를 붙인다.

*(volatile unsigned int *)0x40020414UL

Pointer 자체가 아니라 그 pointer가 가리키는 실제 값에 접근한다.

pointer
0x40020414
     │
     ▼
+------------------+
|   GPIOB_ODR      |
| 32-bit register  |
+------------------+

따라서:

GPIOB_ODR = 1;

은 사실상:

*(volatile unsigned int *)0x40020414 = 1;

이다.

즉 CPU가 해당 address에 값을 write한다.


14. Why volatile?

Hardware register에는 volatile이 매우 중요하다.

일반 변수:

unsigned int value;

는 compiler 입장에서 프로그램 코드가 변경하지 않으면 값이 바뀌지 않는다고 가정할 수 있다.

하지만 peripheral register는 다르다.

CPU가 직접 값을 쓰지 않아도 hardware가 바꿀 수 있다.

Examples:

UART receives byte
        ↓
RX flag changes

Timer counts
        ↓
Counter changes

ADC conversion completes
        ↓
Status flag changes

Button pressed
        ↓
GPIO input changes

따라서:

volatile unsigned int

는 compiler에게:

이 값은 프로그램 코드 외부의 원인으로 바뀔 수 있으므로 접근할 때마다 실제 memory를 읽거나 써라.

라는 의미를 준다.


15. UL Suffix

주소와 bit mask에서:

0x40000000UL
1UL << 1

같은 표현을 사용한다.

UL means:

Unsigned Long

즉 상수를 unsigned integer 계열로 취급하도록 명시한다.

Register address와 bit mask에서는 sign extension 등의 문제를 피하고 의도를 명확하게 하기 위해 자주 사용한다.


16. Bit Manipulation

Register programming에서 가장 자주 사용하는 네 가지 패턴이다.

Set

REG |= (1UL << n);

bit n을 1로 만든다.


Clear

REG &= ~(1UL << n);

bit n을 0으로 만든다.


Toggle

REG ^= (1UL << n);

bit n을 반전한다.

0 → 1
1 → 0

Read

if (REG & (1UL << n))
{
}

bit n이 1인지 확인한다.


17. Read-Modify-Write

Expression:

REG |= MASK;

은 한 번의 단순 write처럼 보이지만 개념적으로는:

1. READ register
2. MODIFY value
3. WRITE register

이다.

Example:

RCC_AHB1ENR |= GPIOBEN;

Conceptually:

tmp = RCC_AHB1ENR;
tmp = tmp | GPIOBEN;
RCC_AHB1ENR = tmp;

이 방식을 Read-Modify-Write라고 한다.

다른 bit 값을 유지하면서 특정 bit만 변경할 수 있다.


18. Complete Address Map

이번 실습에 사용된 주소를 하나의 tree로 보면 다음과 같다.

PERIPH_BASE
0x40000000
│
└── AHB1PERIPH_BASE
    0x40020000
    │
    ├── GPIOB_BASE
    │   0x40020400
    │   │
    │   ├── MODER + 0x00
    │   │   = 0x40020400
    │   │
    │   └── ODR + 0x14
    │       = 0x40020414
    │
    └── RCC_BASE
        0x40023800
        │
        └── AHB1ENR + 0x30
            = 0x40023830

정리하면:

RCC_AHB1ENR = 0x40023830
GPIOB_MODER = 0x40020400
GPIOB_ODR   = 0x40020414

19. Full Practice Code

The following code directly controls LD1 on PB0 without HAL register APIs.

#define PERIPH_BASE        (0x40000000UL)
#define AHB1PERIPH_BASE    (PERIPH_BASE + 0x00020000UL)

#define RCC_BASE           (AHB1PERIPH_BASE + 0x3800UL)
#define GPIOB_BASE         (AHB1PERIPH_BASE + 0x0400UL)

/* Register mapping */
#define RCC_AHB1ENR   (*(volatile unsigned int *)(RCC_BASE + 0x30UL))
#define GPIOB_MODER   (*(volatile unsigned int *)(GPIOB_BASE + 0x00UL))
#define GPIOB_ODR     (*(volatile unsigned int *)(GPIOB_BASE + 0x14UL))

/* Bit definitions */
#define GPIOBEN       (1UL << 1)
#define LD1_PIN       (1UL << 0)

static void delay(volatile unsigned int count)
{
    while (count--)
    {
        __asm__("nop");
    }
}

int main(void)
{
    /* Enable GPIOB peripheral clock */
    RCC_AHB1ENR |= GPIOBEN;

    /* PB0 -> General Purpose Output mode */
    GPIOB_MODER &= ~(0x3UL << (0 * 2));
    GPIOB_MODER |=  (0x1UL << (0 * 2));

    while (1)
    {
        /* Toggle PB0 */
        GPIOB_ODR ^= LD1_PIN;

        delay(800000);
    }
}

20. Code Execution Flow

The program executes in this order.

main()
 │
 ▼
Enable GPIOB Clock
RCC_AHB1ENR bit1 = 1
 │
 ▼
Configure PB0
GPIOB_MODER[1:0] = 01
 │
 ▼
while(1)
 │
 ├── Toggle GPIOB_ODR[0]
 │
 ├── Delay
 │
 └── Repeat

21. What Happens at Hardware Level?

Step 1

RCC_AHB1ENR |= GPIOBEN;

Hardware:

CPU
 ↓
AHB1
 ↓
RCC_AHB1ENR
 ↓
GPIOB clock enabled

Step 2

GPIOB_MODER &= ~(0x3UL << 0);
GPIOB_MODER |=  (0x1UL << 0);

Hardware:

GPIOB_MODER[1:0]
       ↓
      01
       ↓
PB0 configured as output

Step 3

GPIOB_ODR ^= LD1_PIN;

First execution:

ODR bit0

0 XOR 1
   ↓
   1

PB0 becomes High.

Next execution:

1 XOR 1
   ↓
   0

PB0 becomes Low.

Therefore:

HIGH → LOW → HIGH → LOW ...

and the LED blinks.


22. Why Use a Delay?

Without delay:

while (1)
{
    GPIOB_ODR ^= LD1_PIN;
}

the CPU toggles the pin extremely quickly.

The human eye cannot observe this directly.

Therefore the practice uses:

delay(800000);

to slow down the toggle rate.

static void delay(volatile unsigned int count)
{
    while (count--)
    {
        __asm__("nop");
    }
}

nop means:

No Operation

The CPU executes an instruction without performing useful work.


23. Why Is count Volatile?

volatile unsigned int count

is used because an optimizing compiler may determine that an empty delay loop has no externally visible effect.

volatile prevents aggressive removal of accesses to count.

However, this method is only useful for simple experiments.

It is not an accurate timer.

The actual delay depends on:

  • CPU clock
  • Compiler optimization
  • Instruction timing
  • Pipeline behavior

Precise timing should use:

SysTick
or
Hardware Timer

which will be covered in later chapters.


24. ODR vs BSRR

This practice intentionally uses:

GPIOB_ODR ^= LD1_PIN;

because it clearly demonstrates register read-modify-write.

For production GPIO set/reset operations, STM32 also provides BSRR.

Why?

ODR |= or ODR &= performs:

READ → MODIFY → WRITE

If another execution context changes the same register between the read and write, a race can occur.

BSRR provides atomic bit set/reset operation.

Concept:

ODR
READ → MODIFY → WRITE

BSRR
single WRITE

For this single-threaded LED experiment, ODR ^= ... is sufficient.


25. Register-Level View

The entire example can be reduced to three registers.

RCC_AHB1ENR
     │
     └── Enable GPIOB clock

GPIOB_MODER
     │
     └── Configure PB0 as output

GPIOB_ODR
     │
     └── Drive PB0 High / Low

This pattern appears repeatedly in embedded programming:

Clock
  ↓
Configuration
  ↓
Data / Control

UART, ADC, Timer and I2C also follow similar initialization patterns.


26. Important Difference: MCU vs Board

The MCU is:

STM32F439ZI

The board is:

NUCLEO-F439ZI

The MCU reference manual tells us:

What is GPIOB?
Where is GPIOB?
What does MODER mean?

The board schematic/user manual tells us:

Which MCU pin is connected to LD1?

For this board:

LD1 → PB0

Therefore both documents are required.

MCU Reference Manual
        +
Board Schematic
        ↓
Correct Bare-Metal Code

27. Build Result

The original Ch02 practice was successfully built with the GNU ARM toolchain.

Recorded result:

.text : 252 bytes
.data : 0 bytes
.bss  : 0 bytes

At this stage, temporary startup and linker files were reused.

The linker script and startup sequence are covered separately in Chapter 04.

Verification status: build verified. The original Ch02 note did not record an actual board-flash verification for this specific experiment.


28. Key Questions

Q1. What is a register in C?

A register is not a special C construct.

It can be represented as:

*(volatile unsigned int *)ADDRESS

즉 특정 hardware address를 가리키는 pointer를 만들고 이를 역참조하여 접근한다.


Q2. Why is GPIOB clock enabled first?

Because STM32 uses clock gating.

RCC_AHB1ENR |= GPIOBEN;

Without a peripheral clock, GPIOB cannot operate normally.


Q3. Why does one GPIO pin use two MODER bits?

Four modes are required.

00 Input
01 Output
10 Alternate Function
11 Analog

Four states require:

2 bits

Q4. Why is PB0 controlled by ODR bit 0?

GPIO output data bits map directly to pin numbers.

PB0 → ODR bit0
PB1 → ODR bit1
...
PB15 → ODR bit15

Q5. What does 1UL << 1 mean?

1 = 0001

shift left 1

0010

Therefore it creates a mask for bit 1.


Q6. What is Read-Modify-Write?

Read existing register
        ↓
Modify selected bits
        ↓
Write result back

Example:

RCC_AHB1ENR |= GPIOBEN;

29. Key Takeaways

Peripheral
    ↓
Base Address
    ↓
Register Offset
    ↓
Physical Address
    ↓
Pointer Cast
    ↓
Dereference
    ↓
Bit Mask
    ↓
Hardware Control

Bare-metal register programming can be summarized as:

Address + Offset + Bit Field

The important point is not memorizing hexadecimal addresses.

The important skill is being able to:

  1. Find the peripheral in the memory map
  2. Find its base address
  3. Find the register offset
  4. Find the target bit field
  5. Convert the address into a C register expression
  6. Modify only the required bits
  7. Predict the resulting hardware behavior

That is the basic workflow used throughout the remaining chapters.

Clone this wiki locally