BSP와 Linux Kernel을 함께 공부하며 정리한 것들

이 글은 “BSP와 커널을 공부하고 싶지만 어디서부터 연결해야 할지 모르겠다”는 사람을 위한 학습 기록이다. 용어를 먼저 외우기보다, 전원을 켠 뒤 실제 하드웨어가 동작하기까지의 흐름을 따라가며 정리했다.

이 글에서 사용하는 학습 관점 / The learning viewpoint

어려운 용어가 나오면 바로 옆에서 뜻을 설명한다. 한 번에 너무 큰 주제가 나오면 “이 글에서는 큰 그림만 보고, 세부 내용은 별도 글로 나눈다”는 원칙을 적용했다.

1. BSP란 무엇인가? / What is a BSP?

**BSP(Board Support Package)**는 특정 보드에서 운영체제가 부팅되고 주변 장치를 사용할 수 있도록 준비한 소프트웨어 묶음이다. 부트로더, 커널, Device Tree, 보드 설정, 드라이버, 빌드 레시피가 서로 맞아야 한다.

보드 회로도 / 데이터시트

  핀·클록·전원·메모리 설정

  Bootloader + Device Tree + Kernel driver

  실제 장치 동작

따라서 BSP 개발자는 “코드 한 파일”보다 하드웨어 설명이 소프트웨어를 거쳐 실제 신호가 되는 과정을 봐야 한다.

2. 전원 인가부터 Linux 시작까지 / From power-on to Linux

임베디드 보드의 부팅은 PC의 전원 버튼과 비슷해 보이지만, 중간 단계가 더 분명하게 나뉜다.

Power On / Reset
  → SoC Boot ROM
  → SPL 또는 TF-A 등 초기 부트 단계
  → U-Boot
  → Linux Kernel + DTB 로드
  → 커널 초기화
  → init / systemd
  → 사용자 공간 애플리케이션

보드와 부트 체인에 따라 SPL·TF-A·U-Boot의 순서와 역할은 달라질 수 있다. 여기서는 여러 ARM 임베디드 보드에서 공통으로 만나는 개념적 순서를 사용한다.

Boot ROM : SoC 안에 고정된 코드다. 부트 모드 핀이나 eFuse를 보고 eMMC, SD, SPI, USB 복구 모드 중 어디서 다음 이미지를 받을지 결정한다.

SPL(Secondary Program Loader) : DRAM이 준비되기 전에 실행되는 작은 부트로더다. 클록, 핀mux, DDR 초기화와 같은 최소 작업을 담당한다.

U-Boot : 메모리·저장장치·네트워크를 초기화하고 커널과 DTB를 메모리에 올린 뒤 커널로 제어를 넘긴다.

DTB(Device Tree Blob) : 커널이 읽는 하드웨어 설명의 바이너리 형태다. 어떤 장치가 어느 주소와 인터럽트, 클록, 전원을 사용하는지 담는다.

공부 포인트 / Study point

부팅 로그에서 Starting kernel ...만 찾지 말고, 그 앞에서 누가 DRAM을 준비했고, 누가 DTB를 전달했으며, 어떤 저장장치에서 이미지를 읽었는지 질문해 보자.

3. Device Tree: 커널에게 보드 설명서 전달하기

Device Tree는 하드웨어를 코드로 직접 하드코딩하지 않고 데이터로 설명하는 방식이다. 같은 커널 드라이버를 유지하면서 보드마다 주소·핀·IRQ·전원 연결을 바꿀 수 있다는 장점이 있다.

my-device@40000000 {
    compatible = "example,my-device-v1";
    reg = <0x40000000 0x1000>;
    interrupts = <42>;
    clocks = <&clk 3>;
    vdd-supply = <&reg_3v3>;
    status = "okay";
};
속성 / Property쉽게 말하면 / Plain meaning
compatible“이 장치와 맞는 드라이버를 찾아 달라”는 식별 문자열
reg레지스터 영역의 시작 주소와 크기. 0x1000은 4096바이트(4KB)다.
interrupts장치가 CPU의 주의를 요청할 때 사용하는 IRQ 정보
clocks, *-supply장치에 필요한 클록과 전원 연결
statusokay면 활성화, disabled면 보통 사용하지 않음

예를 들어 base가 0x40000000이고 STATUS 레지스터 offset이 0x20이면 실제 레지스터 주소는 0x40000020이다. 하지만 커널 드라이버는 주소를 일반 포인터처럼 직접 역참조하지 않고 MMIO API를 사용한다. 이 예제의 reg 셀 수와 해석은 부모 노드의 #address-cells/#size-cells 설정에 따라 달라질 수 있으므로, 실제 DTS에서는 해당 버스의 binding을 함께 확인해야 한다.

regs = devm_platform_ioremap_resource(pdev, 0);
status = readl(regs + 0x20);
writel(value, regs + CTRL_OFFSET);

**MMIO(Memory-Mapped I/O)**는 장치 레지스터를 메모리 주소처럼 매핑해 읽고 쓰는 방식이다. ioremap() 또는 플랫폼 헬퍼가 물리 주소를 커널 가상 주소로 연결하고, readl()/writel()이 올바른 I/O 접근을 표현한다. 커널에서는 MMIO 포인터에 임의로 volatile을 붙이는 것보다 이런 전용 접근자를 사용해야 한다.

4. 전원과 USB Regulator / Power and USB regulators

USB Host 포트는 데이터선만 필요한 것이 아니라 외부 장치에 공급할 VBUS 전원도 필요하다. 이 전원을 GPIO나 Power IC로 켜고 끄는 회로를 커널에서 공통적으로 다루기 위해 Regulator Framework를 사용한다.

SoC GPIO → load switch / PMIC → USB VBUS

      regulator_enable()

Device Tree에는 보통 regulator 노드를 만들고 USB 노드에서 vbus-supply로 연결한다. USB 드라이버는 보드마다 GPIO 번호를 알 필요 없이 regulator를 enable/disable한다. 이 구조를 이해하면 “USB Host는 인식되는데 외부 장치가 동작하지 않는다”는 문제에서 데이터, VBUS, reset, pinctrl을 분리해 볼 수 있다.

5. Process, Thread, Scheduler: 커널이 실행을 나누는 방법

Linux는 Process와 Thread를 모두 task_struct라는 공통 구조로 관리한다. Process는 주소 공간·힙·열린 파일 같은 자원을 가지고, Thread는 각자의 스택·레지스터·프로그램 카운터를 가진다.

Process (공유)
 ├─ code / heap / global data
 ├─ file descriptors
 └─ address space
     ├─ Thread A: stack + registers + PC
     └─ Thread B: stack + registers + PC

Scheduler는 다음에 CPU를 사용할 task를 고르는 커널 서브시스템이다. 하나의 논리 CPU는 한 순간에 하나의 instruction stream을 실행하므로, Linux는 실행 가능한 task를 빠르게 교체해 여러 작업이 동시에 진행되는 것처럼 보이게 한다.

Context switch는 현재 task의 레지스터와 실행 위치를 저장하고 다음 task의 상태를 복원하는 과정이다. 전환이 잦으면 레지스터 저장뿐 아니라 캐시와 TLB 효율도 떨어질 수 있다.

CPU마다 run queue(실행 가능한 task 대기열)를 두면 전역 큐 하나를 놓고 경쟁하는 비용을 줄일 수 있다. 대신 한 CPU에 task가 몰리면 load balancing으로 다른 CPU로 이동시킨다. 최근 일반 스케줄링을 이해할 때는 CFS의 vruntime 개념과 함께 EEVDF 방식으로의 변화를 구분해 보는 것이 좋다.

6. 동기화: IRQ, Spinlock, Mutex, Atomic

공유 데이터를 보호할 때는 “누가 동시에 들어올 수 있는가”와 “얼마나 오래 기다리는가”를 함께 생각해야 한다.

문제 / Problem주요 도구 / Tool핵심 의미 / Core idea
현재 CPU의 IRQ가 끼어듦local_irq_disable()현재 CPU의 인터럽트만 막음
여러 CPU가 critical section에 동시 진입Spinlock짧은 구간을 잠그고 바쁜 대기로 기다림
오래 기다리거나 sleep 가능Mutex기다리는 task도 잠들 수 있음
단순한 카운터 연산Atomic하나의 연산을 쪼개지지 않게 처리
메모리 접근 관찰 순서Barrier / acquire-release다른 CPU가 보는 순서를 보장

spin_lock_irqsave()는 현재 인터럽트 상태를 저장한 뒤 IRQ를 제어하고 spinlock을 획득하는 패턴이다. IRQ 핸들러와 일반 코드가 같은 lock을 사용할 수 있는 상황에서 재진입 교착을 피하는 데 도움이 된다.

Spinlock에서 잠들면 안 되는 이유 / Why sleeping while holding a spinlock is wrong

CPU0이 lock을 잡은 채 sleep()하면 CPU0의 task는 실행을 멈춘다. CPU1은 lock이 풀릴 때까지 계속 spin하지만, lock을 풀 주체는 잠들어 있다. CPU를 낭비하고 심하면 deadlock이 된다. 오래 걸리는 작업이나 sleep 가능한 작업은 mutex 영역으로 옮겨야 한다.

AtomicityMemory Ordering은 다르다. atomic_inc()가 count 자체의 증가를 안전하게 만든다고 해서 다른 데이터가 원하는 순서로 보인다는 뜻은 아니다. 또한 volatile은 MMIO처럼 컴파일러가 접근을 제거하지 않아야 하는 경우와 관련이 있지만, 멀티코어 동기화 도구는 아니다.

int ready;

/* producer */
data = 100;
smp_store_release(&ready, 1);

/* consumer */
if (smp_load_acquire(&ready) == 1)
    use(data);

Release는 “준비한 내용을 공개하기 전에 정리한다”는 쪽이고, Acquire는 “준비됐다는 표시를 확인한 뒤 내용을 읽는다”는 쪽이다. Acquire가 Release가 공개한 값을 실제로 관찰하는 조건에서 이 관계가 happens-before(한 작업이 다른 작업보다 먼저 관찰되어야 한다는 관계)를 만든다.

7. Raw NAND와 eMMC, 그리고 write 관찰

Raw NAND에서는 MTD가 원시 플래시 접근 계층을 제공하고, 그 위의 UBI가 논리 볼륨·bad block·wear leveling을 관리하며, UBIFS가 파일시스템 역할을 한다. eMMC는 NAND와 컨트롤러가 한 패키지에 들어 있어 내부 FTL(Flash Translation Layer)이 논리 블록 주소를 실제 NAND 위치로 바꾸고 wear leveling, ECC, garbage collection 등을 수행한다.

Application
  → write() / pwrite()
  → VFS / filesystem / page cache
  → block layer
  → eMMC controller + FTL
  → NAND cells

write()는 현재 파일 offset에서 쓰고 offset을 이동시킨다. pwrite()는 지정한 offset에 쓰지만 파일의 현재 offset은 바꾸지 않는다. 둘 중 어느 것을 사용했는지는 애플리케이션 관점의 위치 제어 차이이고, eMMC 수명은 그 호출 횟수만으로 판단할 수 없다.

파일시스템 저널·메타데이터, 블록 정렬, FTL의 garbage collection과 wear leveling 때문에 실제 NAND 쓰기는 호스트가 요청한 양보다 커질 수 있다. 이를 write amplification이라고 한다. eMMC의 소모 상태는 호스트 write 양과 함께 EXT_CSD의 lifetime estimate 같은 장치 정보를 확인해야 한다.

8. 공부한 내용을 실제 BSP 업무와 연결하기

지금까지의 개념은 따로 떨어져 있지 않다. 예를 들어 USB가 동작하지 않는 문제를 조사할 때도 다음 층을 차례로 내려갈 수 있다.

  1. 전원이 실제로 들어오는가? — regulator/VBUS
  2. 핀과 클록이 맞는가? — pinctrl/clock
  3. Device Tree 노드가 활성화됐는가? — status, compatible
  4. 드라이버가 매칭됐는가? — probe와 로그
  5. 레지스터 접근이 맞는가? — MMIO, IRQ, DMA
  6. 동시 접근 문제가 없는가? — lock, atomic, memory ordering

이런 식으로 “현상 → 하드웨어 자원 → Device Tree → 드라이버 → 커널 동기화”의 방향으로 질문을 좁혀 가면, 막연한 BSP 디버깅이 재현 가능한 조사 과정으로 바뀐다.

9. 한 글에 담지 않은 큰 주제 / Topics intentionally split out

System Call부터 Process/Thread, Scheduler, Sleep/Wake-up, Interrupt, Spinlock, Atomic, Memory Ordering까지의 커널 기초 흐름은 별도 심화 글로 정리했다. Device Tree와 플랫폼 드라이버 작성 흐름은 또 다른 심화 글로 분리했다. PCIe·DMA·IOMMU, ARM64 예외 처리, Kconfig/Yocto 빌드, perf/ftrace, MCTP/PLDM, 그리고 eMMC 버스 파형은 각각 한 편 이상의 설명이 필요한 주제다.

그중 먼저 실습으로 연결하기 좋은 주제는 UUU → U-Boot → eMMC mmc read → CMD17 → CLK/CMD/DAT 파형이다. 이 흐름은 명령 한 줄이 MMC 계층과 SoC 호스트 컨트롤러, DMA, 실제 핀 신호로 이어지는 모습을 보여 준다.

마무리 / Closing thoughts

BSP와 커널 공부의 핵심은 용어를 많이 아는 것이 아니라, 한 현상을 여러 계층에서 설명할 수 있게 되는 것이다.

보드 회로

Boot ROM / U-Boot

Device Tree

Kernel driver / MMIO / DMA

Scheduler / synchronization / filesystem

실제 장치 동작과 측정 결과

다음 글에서는 이 지도에서 저장장치 경로를 골라, UUU로 실행 중인 U-Boot에 읽기 명령을 보내고 eMMC 파형을 관찰하는 과정을 별도로 정리한다.

#BSP

#LinuxKernel

#EmbeddedLinux

#DeviceTree

#DeviceDriver

#eMMC