Bare metal C 00 필수개념 - 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 구조로 동작한다.

⚠️ **GitHub.com Fallback** ⚠️