Repository navigation
Chapter 02
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
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
Bare-metal programming에서는 주소를 외우는 것이 아니라 문서에서 찾아 계산해야 한다.
STM32를 사용할 때 주로 보는 문서는 다음과 같다.
Peripheral의:
- Base address
- Register offset
- Bit field
- Reset value
- Register operation
을 확인한다.
STM32F439ZI에서는 RM0090 Reference Manual이 핵심이다.
주로 다음을 확인한다.
- Pinout
- Alternate Function
- Electrical characteristics
- Package information
MCU pin이 실제 board의 무엇과 연결되었는지 확인한다.
예:
LD1 → PB0
B1 → PC13
즉:
Board Manual
↓
Which pin?
Reference Manual
↓
Which register?
Memory Map
↓
Which address?
Register Description
↓
Which bit?
순서로 읽으면 된다.
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
GPIOB offset from AHB1 base:
0x0400
Therefore:
GPIOB_BASE
= AHB1PERIPH_BASE + 0x0400
= 0x40020000
+ 0x00000400
= 0x40020400
C:
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400UL)RCC offset:
0x3800
Therefore:
RCC_BASE
= AHB1PERIPH_BASE + 0x3800
= 0x40020000
+ 0x00003800
= 0x40023800
C:
#define RCC_BASE (AHB1PERIPH_BASE + 0x3800UL)Peripheral base address는 peripheral 전체의 시작 주소다.
실제 register 주소를 얻으려면:
Register Address
=
Peripheral Base Address
+
Register Offset
를 계산한다.
이번 실습에서는 세 개의 register를 사용한다.
RCC_AHB1ENR
GPIOB_MODER
GPIOB_ODR
RCC base:
0x40023800
AHB1ENR offset:
0x30
따라서:
RCC_AHB1ENR
= RCC_BASE + 0x30
= 0x40023800
+ 0x00000030
= 0x40023830
C:
#define RCC_AHB1ENR \
(*(volatile unsigned int *)(RCC_BASE + 0x30UL))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
를 수행하는 것이다.
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
GPIOB base:
0x40020400
MODER offset:
0x00
Therefore:
GPIOB_MODER
= 0x40020400 + 0x00
= 0x40020400
C:
#define GPIOB_MODER \
(*(volatile unsigned int *)(GPIOB_BASE + 0x00UL))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
잘못된 방식:
GPIOB_MODER |= 1;현재 값이 무엇인지 모르는 상태에서는 안전하지 않다.
예를 들어 기존 값이:
11
이면:
11 OR 01 = 11
이므로 Output mode가 되지 않는다.
따라서 일반적으로:
CLEAR
↓
SET
두 단계로 처리한다.
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이 된다.
GPIOB_MODER |= (0x1UL << (0 * 2));01
을 OR한다.
결과:
MODER[1:0] = 01
따라서:
PB0 = General Purpose Output
GPIOB base:
0x40020400
ODR offset:
0x14
Therefore:
GPIOB_ODR
= 0x40020400
+ 0x14
= 0x40020414
C:
#define GPIOB_ODR \
(*(volatile unsigned int *)(GPIOB_BASE + 0x14UL))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
Now look at this expression carefully.
#define GPIOB_ODR \
(*(volatile unsigned int *)0x40020414UL)이 한 줄이 bare-metal register programming에서 가장 중요한 부분 중 하나다.
분해하면 다음과 같다.
0x40020414UL이 단계에서는 단순한 integer constant이다.
Value = 0x40020414
CPU address라는 의미를 C compiler가 자동으로 아는 것은 아니다.
(volatile unsigned int *)0x40020414UL정수 0x40020414를:
pointer to volatile unsigned int
로 변환한다.
즉:
0x40020414
│
▼
"이 주소에는 32-bit register가 있다"
라고 C compiler에게 알려주는 것이다.
앞에 *를 붙인다.
*(volatile unsigned int *)0x40020414ULPointer 자체가 아니라 그 pointer가 가리키는 실제 값에 접근한다.
pointer
0x40020414
│
▼
+------------------+
| GPIOB_ODR |
| 32-bit register |
+------------------+
따라서:
GPIOB_ODR = 1;은 사실상:
*(volatile unsigned int *)0x40020414 = 1;이다.
즉 CPU가 해당 address에 값을 write한다.
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를 읽거나 써라.
라는 의미를 준다.
주소와 bit mask에서:
0x40000000UL
1UL << 1같은 표현을 사용한다.
UL means:
Unsigned Long
즉 상수를 unsigned integer 계열로 취급하도록 명시한다.
Register address와 bit mask에서는 sign extension 등의 문제를 피하고 의도를 명확하게 하기 위해 자주 사용한다.
Register programming에서 가장 자주 사용하는 네 가지 패턴이다.
REG |= (1UL << n);bit n을 1로 만든다.
REG &= ~(1UL << n);bit n을 0으로 만든다.
REG ^= (1UL << n);bit n을 반전한다.
0 → 1
1 → 0
if (REG & (1UL << n))
{
}bit n이 1인지 확인한다.
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만 변경할 수 있다.
이번 실습에 사용된 주소를 하나의 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
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);
}
}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
RCC_AHB1ENR |= GPIOBEN;Hardware:
CPU
↓
AHB1
↓
RCC_AHB1ENR
↓
GPIOB clock enabled
GPIOB_MODER &= ~(0x3UL << 0);
GPIOB_MODER |= (0x1UL << 0);Hardware:
GPIOB_MODER[1:0]
↓
01
↓
PB0 configured as output
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.
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.
volatile unsigned int countis 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.
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.
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.
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
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.
A register is not a special C construct.
It can be represented as:
*(volatile unsigned int *)ADDRESS즉 특정 hardware address를 가리키는 pointer를 만들고 이를 역참조하여 접근한다.
Because STM32 uses clock gating.
RCC_AHB1ENR |= GPIOBEN;Without a peripheral clock, GPIOB cannot operate normally.
Four modes are required.
00 Input
01 Output
10 Alternate Function
11 Analog
Four states require:
2 bits
GPIO output data bits map directly to pin numbers.
PB0 → ODR bit0
PB1 → ODR bit1
...
PB15 → ODR bit15
1 = 0001
shift left 1
0010
Therefore it creates a mask for bit 1.
Read existing register
↓
Modify selected bits
↓
Write result back
Example:
RCC_AHB1ENR |= GPIOBEN;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:
- Find the peripheral in the memory map
- Find its base address
- Find the register offset
- Find the target bit field
- Convert the address into a C register expression
- Modify only the required bits
- Predict the resulting hardware behavior
That is the basic workflow used throughout the remaining chapters.