Bare metal C Ch00 필수개념 - Hongjayeong-02/Basic_SoC_implementation GitHub Wiki

Ch00. Bare-Metal C 필수 개념

목표
이후 모든 Chapter에서 반복해서 등장하는 Bare-Metal C의 공통 개념을 먼저 정리한다.
Memory-Mapped I/O, volatile, pointer, bit manipulation, build flow, startup/vector table을 이해하는 것이 핵심이다.


1. Bare-Metal이란?

Bare-Metal programming은 운영체제나 HAL 같은 고수준 추상화 계층에 의존하지 않고, CPU가 MCU의 peripheral register를 직접 제어하는 방식이다.

Application
    │
    ▼
C code
    │
    ▼
Memory-Mapped Register
    │
    ▼
Peripheral Hardware

HAL 방식:

HAL_GPIO_WritePin(...);

Bare-Metal 방식:

GPIOB->ODR |= (1UL << 0);

둘 다 결국 hardware register를 제어하지만, Bare-Metal에서는 register와 bit를 직접 다룬다.


2. Memory-Mapped I/O

MCU 내부의 GPIO, UART, Timer, ADC 같은 peripheral은 CPU 관점에서 memory address를 가진 register 집합으로 보인다.

예를 들어 NUCLEO-F439ZI의 GPIOB:

GPIOB Base Address : 0x4002_0400
ODR Offset         : 0x14

GPIOB_ODR
= 0x4002_0400 + 0x14
= 0x4002_0414

CPU가 이 주소를 읽거나 쓰는 것이 곧 GPIO hardware를 제어하는 것이다.

CPU
 │
 │ Load / Store
 ▼
AHB/APB Bus
 │
 ▼
Address Decoder
 │
 ▼
Peripheral Register
 │
 ▼
Hardware

3. C에서 Register Address를 표현하는 방법

직접 주소 방식의 대표적인 표현:

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

이를 순서대로 해석하면:

0x40020414UL
    │
    └─ 주소값

(volatile unsigned int *)0x40020414UL
    │
    └─ 이 숫자를 32-bit register pointer로 취급

*(...)
    │
    └─ pointer가 가리키는 실제 memory location 접근

따라서:

GPIOB_ODR |= (1UL << 0);

은 실제로 0x40020414 주소의 bit0을 set하는 것이다.


4. volatile

Hardware register는 CPU code 외부의 hardware에 의해 값이 바뀔 수 있다.

예:

  • GPIO input
  • UART RX data
  • ADC conversion complete flag
  • Timer counter
  • Interrupt status flag

따라서 compiler가 값을 register/cache에 보관하고 memory read를 생략하면 안 된다.

volatile unsigned int *reg;

volatile은 compiler에게:

이 값은 예측할 수 없는 시점에 바뀔 수 있으므로 실제 memory access를 생략하지 마라.

라는 의미를 전달한다.


5. Bit Manipulation 기본 패턴

Bit Set

REG |= (1UL << n);

특정 bit를 1로 만든다.

Bit Clear

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

특정 bit만 0으로 만든다.

Bit Toggle

REG ^= (1UL << n);

해당 bit를 0 ↔ 1로 반전한다.

Bit Test

if ((REG & (1UL << n)) != 0U)
{
    ...
}

특정 bit가 1인지 확인한다.

여러 bit field 설정

REG &= ~(0x3UL << shift);
REG |=  (value << shift);

먼저 field를 clear한 뒤 원하는 값을 넣는다.


6. GPIO 예시

PB0을 output으로 설정:

GPIOB->MODER &= ~(0x3UL << (0 * 2));
GPIOB->MODER |=  (0x1UL << (0 * 2));

MODER[1:0]:

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

7. Build Flow

main.c
  │
  ▼
Preprocessing
  │
  ▼
main.i
  │
  ▼
Compilation
  │
  ▼
main.s
  │
  ▼
Assembly
  │
  ▼
main.o
  │
  ▼
Linking
  │
  ▼
firmware.elf
  │
  ▼
objcopy
  │
  ▼
firmware.hex / firmware.bin

대표 도구:

arm-none-eabi-gcc
arm-none-eabi-objdump
arm-none-eabi-objcopy
arm-none-eabi-size
arm-none-eabi-nm

8. Linker Script와 Startup File

Linker Script

FLASH
 ├─ Vector Table
 ├─ .text
 └─ .data 초기값

RAM
 ├─ .data
 ├─ .bss
 ├─ Heap
 └─ Stack

Startup File

Reset
  │
  ▼
Initial MSP 설정
  │
  ▼
.data : Flash → RAM
  │
  ▼
.bss : 0으로 초기화
  │
  ▼
SystemInit()
  │
  ▼
main()

9. Interrupt 기본 개념

ISR

void EXTI15_10_IRQHandler(void)
{
    ...
}

Vector Table

각 exception/interrupt의 handler 주소를 저장한다.

NVIC

NVIC_EnableIRQ(EXTI15_10_IRQn);

으로 특정 IRQ를 활성화할 수 있다.


10. 이 프로젝트의 보드 기준

항목 NUCLEO-F439ZI
MCU STM32F439ZIT6
CPU Core ARM Cortex-M4F
User LED LD1 = PB0
User Button B1 = PC13
VCP UART USART3
UART TX/RX PD8 / PD9
Flash 2 MB
SRAM 128 KB + CCM 64 KB

11. 이후 Chapter에서 반복되는 공통 패턴

1. RCC에서 peripheral clock enable
2. GPIO mode / Alternate Function 설정
3. Peripheral configuration register 설정
4. Peripheral enable
5. Status flag polling 또는 interrupt
6. Data register read/write

12. 핵심 정리

  • Peripheral register는 memory address에 존재한다.
  • Bare-Metal C는 그 주소를 C pointer/구조체로 직접 접근한다.
  • volatile은 hardware register 접근에서 중요하다.
  • Bit set/clear/toggle/test 패턴을 반복적으로 사용한다.
  • Firmware build는 preprocessing → compilation → assembly → linking 순서로 진행된다.
  • main() 전에 startup code가 실행된다.
  • Interrupt는 Vector Table + NVIC + ISR 구조로 동작한다.

네비게이션: 전체 목차 · 다음: Ch01. Toolchain 설정