BSP와 Linux Kernel을 함께 공부하며 정리한 것들
What I Have Learned While Studying BSP and the Linux Kernel
이 글은 “BSP와 커널을 공부하고 싶지만 어디서부터 연결해야 할지 모르겠다”는 사람을 위한 학습 기록이다. 용어를 먼저 외우기보다, 전원을 켠 뒤 실제 하드웨어가 동작하기까지의 흐름을 따라가며 정리했다.
This is a study log for anyone who wants to learn BSP and the Linux kernel but is unsure how the pieces fit together. Instead of memorizing terms first, we follow the path from power-on to real hardware activity.
이 글에서 사용하는 학습 관점 / The learning viewpoint
어려운 용어가 나오면 바로 옆에서 뜻을 설명한다. 한 번에 너무 큰 주제가 나오면 “이 글에서는 큰 그림만 보고, 세부 내용은 별도 글로 나눈다”는 원칙을 적용했다.
Every difficult term is explained when it first appears. When a topic becomes too large, this article keeps the map and leaves the detailed lab or theory to a separate article.
1. BSP란 무엇인가? / What is a BSP?
BSP(Board Support Package)는 특정 보드에서 운영체제가 부팅되고 주변 장치를 사용할 수 있도록 준비한 소프트웨어 묶음이다. 부트로더, 커널, Device Tree, 보드 설정, 드라이버, 빌드 레시피가 서로 맞아야 한다.
BSP (Board Support Package) is the collection of software needed for an operating system to boot and use a particular board. Bootloader, kernel, Device Tree, board configuration, drivers, and build recipes must agree with one another.
보드 회로도 / 데이터시트
↓
핀·클록·전원·메모리 설정
↓
Bootloader + Device Tree + Kernel driver
↓
실제 장치 동작
따라서 BSP 개발자는 “코드 한 파일”보다 하드웨어 설명이 소프트웨어를 거쳐 실제 신호가 되는 과정을 봐야 한다.
A BSP engineer therefore follows how a hardware description becomes a real signal through software, rather than looking at one source file in isolation.
2. 전원 인가부터 Linux 시작까지 / From power-on to Linux
임베디드 보드의 부팅은 PC의 전원 버튼과 비슷해 보이지만, 중간 단계가 더 분명하게 나뉜다.
Embedded boot resembles a PC power-on sequence, but its stages are more explicit.
Power On / Reset
→ SoC Boot ROM
→ SPL 또는 TF-A 등 초기 부트 단계
→ U-Boot
→ Linux Kernel + DTB 로드
→ 커널 초기화
→ init / systemd
→ 사용자 공간 애플리케이션
보드와 부트 체인에 따라 SPL·TF-A·U-Boot의 순서와 역할은 달라질 수 있다. 여기서는 여러 ARM 임베디드 보드에서 공통으로 만나는 개념적 순서를 사용한다.
The exact order and responsibilities of SPL, TF-A, and U-Boot vary by board and boot chain. The diagram is a conceptual sequence shared by many ARM embedded boards.
Boot ROMSoC 안에 고정된 코드다. 부트 모드 핀이나 eFuse를 보고 eMMC, SD, SPI, USB 복구 모드 중 어디서 다음 이미지를 받을지 결정한다.
Immutable SoC code that selects the boot source and may enter USB recovery. SPL(Secondary Program Loader)DRAM이 준비되기 전에 실행되는 작은 부트로더다. 클록, 핀mux, DDR 초기화와 같은 최소 작업을 담당한다.
A small first-stage loader that initializes clocks, pinmux, and DRAM before the full bootloader. U-Boot메모리·저장장치·네트워크를 초기화하고 커널과 DTB를 메모리에 올린 뒤 커널로 제어를 넘긴다.
The full bootloader that initializes hardware, loads the kernel/DTB, and transfers control to Linux. DTB(Device Tree Blob)커널이 읽는 하드웨어 설명의 바이너리 형태다. 어떤 장치가 어느 주소와 인터럽트, 클록, 전원을 사용하는지 담는다.
The binary hardware description consumed by the kernel.
공부 포인트 / Study point
부팅 로그에서 Starting kernel ...만 찾지 말고, 그 앞에서 누가 DRAM을 준비했고, 누가 DTB를 전달했으며, 어떤 저장장치에서 이미지를 읽었는지 질문해 보자.
When you see Starting kernel ..., ask who initialized DRAM, who passed the DTB, and which storage path supplied the image.
3. Device Tree: 커널에게 보드 설명서 전달하기
Device Tree는 하드웨어를 코드로 직접 하드코딩하지 않고 데이터로 설명하는 방식이다. 같은 커널 드라이버를 유지하면서 보드마다 주소·핀·IRQ·전원 연결을 바꿀 수 있다는 장점이 있다.
Device Tree describes hardware as data instead of hard-coding a board into the driver. The same driver can then serve boards with different addresses, pins, IRQs, and power connections.
my-device@40000000 {
compatible = "example,my-device-v1";
reg = <0x40000000 0x1000>;
interrupts = <42>;
clocks = <&clk 3>;
vdd-supply = <®_3v3>;
status = "okay";
};
속성 / Property쉽게 말하면 / Plain meaning
| compatible | “이 장치와 맞는 드라이버를 찾아 달라”는 식별 문자열 The identifier used for Device–Driver matching. |
| reg | 레지스터 영역의 시작 주소와 크기. 0x1000은 4096바이트(4KB)다. Register base and size; 0x1000 is 4096 bytes. |
| interrupts | 장치가 CPU의 주의를 요청할 때 사용하는 IRQ 정보 The interrupt description. |
| clocks, *-supply | 장치에 필요한 클록과 전원 연결 Clock and power dependencies. |
| status | okay면 활성화, disabled면 보통 사용하지 않음 Whether the node is enabled. |
예를 들어 base가 0x40000000이고 STATUS 레지스터 offset이 0x20이면 실제 레지스터 주소는 0x40000020이다. 하지만 커널 드라이버는 주소를 일반 포인터처럼 직접 역참조하지 않고 MMIO API를 사용한다. 이 예제의 reg 셀 수와 해석은 부모 노드의 #address-cells/#size-cells 설정에 따라 달라질 수 있으므로, 실제 DTS에서는 해당 버스의 binding을 함께 확인해야 한다.
If the base is 0x40000000 and STATUS has offset 0x20, the register is at 0x40000020. A kernel driver still accesses it through MMIO APIs, not as an ordinary pointer. The number and meaning of reg cells depend on the parent bus’s #address-cells/#size-cells, so check the binding in a real DTS.
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을 붙이는 것보다 이런 전용 접근자를 사용해야 한다.
MMIO maps device registers into an address space so the kernel can access them with I/O helpers such as readl() and writel(). In kernel code, use the I/O accessors rather than treating an MMIO pointer as a generic volatile pointer.
4. 전원과 USB Regulator / Power and USB regulators
USB Host 포트는 데이터선만 필요한 것이 아니라 외부 장치에 공급할 VBUS 전원도 필요하다. 이 전원을 GPIO나 Power IC로 켜고 끄는 회로를 커널에서 공통적으로 다루기 위해 Regulator Framework를 사용한다.
A USB host needs VBUS power as well as data lines. Linux uses the Regulator Framework to control a GPIO- or PMIC-driven power switch through a common interface.
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을 분리해 볼 수 있다.
The USB driver can then enable or disable the supply without knowing the board-specific GPIO number. This separation helps distinguish data, VBUS, reset, and pinctrl problems during bring-up.
5. Process, Thread, Scheduler: 커널이 실행을 나누는 방법
Linux는 Process와 Thread를 모두 task_struct라는 공통 구조로 관리한다. Process는 주소 공간·힙·열린 파일 같은 자원을 가지고, Thread는 각자의 스택·레지스터·프로그램 카운터를 가진다.
Linux represents both processes and threads with the common task_struct. A process owns resources such as an address space and files; each thread has its own stack, registers, and program counter.
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를 빠르게 교체해 여러 작업이 동시에 진행되는 것처럼 보이게 한다.
The scheduler chooses the next runnable task. One logical CPU executes one instruction stream at a time, so Linux switches runnable tasks quickly to share the CPU.
Context switch는 현재 task의 레지스터와 실행 위치를 저장하고 다음 task의 상태를 복원하는 과정이다. 전환이 잦으면 레지스터 저장뿐 아니라 캐시와 TLB 효율도 떨어질 수 있다.
A context switch saves the current task’s CPU state and restores another’s. Frequent switches also affect cache and TLB locality.
CPU마다 run queue(실행 가능한 task 대기열)를 두면 전역 큐 하나를 놓고 경쟁하는 비용을 줄일 수 있다. 대신 한 CPU에 task가 몰리면 load balancing으로 다른 CPU로 이동시킨다. 최근 일반 스케줄링을 이해할 때는 CFS의 vruntime 개념과 함께 EEVDF 방식으로의 변화를 구분해 보는 것이 좋다.
Per-CPU run queues reduce contention compared with one global queue; load balancing moves tasks when the queues become uneven. When studying modern Linux scheduling, relate the historical CFS/vruntime model to the newer EEVDF design.
6. 동기화: IRQ, Spinlock, Mutex, Atomic
공유 데이터를 보호할 때는 “누가 동시에 들어올 수 있는가”와 “얼마나 오래 기다리는가”를 함께 생각해야 한다.
When protecting shared data, consider both who can enter concurrently and how long a waiter may need to wait.
문제 / Problem주요 도구 / Tool핵심 의미 / Core idea
| 현재 CPU의 IRQ가 끼어듦 | local_irq_disable() | 현재 CPU의 인터럽트만 막음 Disables local interrupts only. |
| 여러 CPU가 critical section에 동시 진입 | Spinlock | 짧은 구간을 잠그고 바쁜 대기로 기다림 Busy-waits for a short critical section. |
| 오래 기다리거나 sleep 가능 | Mutex | 기다리는 task도 잠들 수 있음 The waiter can sleep. |
| 단순한 카운터 연산 | Atomic | 하나의 연산을 쪼개지지 않게 처리 Makes a simple operation indivisible. |
| 메모리 접근 관찰 순서 | Barrier / acquire-release | 다른 CPU가 보는 순서를 보장 Constrains observation order across CPUs. |
spin_lock_irqsave()는 현재 인터럽트 상태를 저장한 뒤 IRQ를 제어하고 spinlock을 획득하는 패턴이다. IRQ 핸들러와 일반 코드가 같은 lock을 사용할 수 있는 상황에서 재진입 교착을 피하는 데 도움이 된다.
spin_lock_irqsave() saves the interrupt state, disables local interrupts as needed, and takes the spinlock. It is useful when an IRQ handler and normal code can touch the same lock.
Spinlock에서 잠들면 안 되는 이유 / Why sleeping while holding a spinlock is wrong
CPU0이 lock을 잡은 채 sleep()하면 CPU0의 task는 실행을 멈춘다. CPU1은 lock이 풀릴 때까지 계속 spin하지만, lock을 풀 주체는 잠들어 있다. CPU를 낭비하고 심하면 deadlock이 된다. 오래 걸리는 작업이나 sleep 가능한 작업은 mutex 영역으로 옮겨야 한다.
If CPU0 sleeps while holding the lock, CPU1 spins while the only unlocker is not runnable. This wastes a CPU and can deadlock; use a mutex for operations that may sleep.
Atomicity와 Memory Ordering은 다르다. atomic_inc()가 count 자체의 증가를 안전하게 만든다고 해서 다른 데이터가 원하는 순서로 보인다는 뜻은 아니다. 또한 volatile은 MMIO처럼 컴파일러가 접근을 제거하지 않아야 하는 경우와 관련이 있지만, 멀티코어 동기화 도구는 아니다.
Atomicity is not the same as memory ordering. atomic_inc() protects the counter operation; it does not automatically order unrelated data. volatile is not a multi-core synchronization primitive.
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(한 작업이 다른 작업보다 먼저 관찰되어야 한다는 관계)를 만든다.
Release publishes prior work before the flag; acquire observes the flag before consuming the data. When the acquire observes the value published by the release, together they create a happens-before relationship.
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 등을 수행한다.
For raw NAND, MTD provides the raw flash access layer, UBI manages logical volumes, bad blocks, and wear leveling, and UBIFS provides the filesystem. An eMMC packages NAND with a controller; its internal FTL performs address translation, wear leveling, ECC, and garbage collection.
Application
→ write() / pwrite()
→ VFS / filesystem / page cache
→ block layer
→ eMMC controller + FTL
→ NAND cells
write()는 현재 파일 offset에서 쓰고 offset을 이동시킨다. pwrite()는 지정한 offset에 쓰지만 파일의 현재 offset은 바꾸지 않는다. 둘 중 어느 것을 사용했는지는 애플리케이션 관점의 위치 제어 차이이고, eMMC 수명은 그 호출 횟수만으로 판단할 수 없다.
write() uses and advances the current file offset; pwrite() writes at an explicit offset without changing it. This is an API difference, not a direct measure of NAND wear.
파일시스템 저널·메타데이터, 블록 정렬, FTL의 garbage collection과 wear leveling 때문에 실제 NAND 쓰기는 호스트가 요청한 양보다 커질 수 있다. 이를 write amplification이라고 한다. eMMC의 소모 상태는 호스트 write 양과 함께 EXT_CSD의 lifetime estimate 같은 장치 정보를 확인해야 한다.
Filesystem metadata, alignment, garbage collection, and wear leveling can make physical NAND writes larger than host writes. This is write amplification; combine host-write accounting with eMMC EXT_CSD lifetime information.
8. 공부한 내용을 실제 BSP 업무와 연결하기
지금까지의 개념은 따로 떨어져 있지 않다. 예를 들어 USB가 동작하지 않는 문제를 조사할 때도 다음 층을 차례로 내려갈 수 있다.
These concepts are not isolated. For a USB failure, move down the layers in order:
- 전원이 실제로 들어오는가? — regulator/VBUS
- 핀과 클록이 맞는가? — pinctrl/clock
- Device Tree 노드가 활성화됐는가? — status, compatible
- 드라이버가 매칭됐는가? — probe와 로그
- 레지스터 접근이 맞는가? — MMIO, IRQ, DMA
- 동시 접근 문제가 없는가? — lock, atomic, memory ordering
이런 식으로 “현상 → 하드웨어 자원 → Device Tree → 드라이버 → 커널 동기화”의 방향으로 질문을 좁혀 가면, 막연한 BSP 디버깅이 재현 가능한 조사 과정으로 바뀐다.
Moving from symptom to hardware resource, Device Tree, driver, and synchronization turns vague BSP debugging into a repeatable investigation.
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 버스 파형은 각각 한 편 이상의 설명이 필요한 주제다.
The kernel path from system calls through tasks, scheduling, sleep/wakeup, interrupts, locks, atomics, and memory ordering is covered in a separate deep-dive article. Device Tree and platform-driver writing has its own deep-dive article. PCIe/DMA/IOMMU, ARM64 exceptions, Kconfig/Yocto builds, perf/ftrace, MCTP/PLDM, and eMMC waveforms each deserve further posts.
그중 먼저 실습으로 연결하기 좋은 주제는 UUU → U-Boot → eMMC mmc read → CMD17 → CLK/CMD/DAT 파형이다. 이 흐름은 명령 한 줄이 MMC 계층과 SoC 호스트 컨트롤러, DMA, 실제 핀 신호로 이어지는 모습을 보여 준다.
A particularly useful next lab is UUU → U-Boot → eMMC mmc read → CMD17 → CLK/CMD/DAT waveform, which connects one command to the MMC stack, host controller, DMA, and pins.
마무리 / Closing thoughts
BSP와 커널 공부의 핵심은 용어를 많이 아는 것이 아니라, 한 현상을 여러 계층에서 설명할 수 있게 되는 것이다.
The goal of BSP and kernel study is not to collect terms, but to explain one symptom across multiple layers.
보드 회로
↕
Boot ROM / U-Boot
↕
Device Tree
↕
Kernel driver / MMIO / DMA
↕
Scheduler / synchronization / filesystem
↕
실제 장치 동작과 측정 결과
다음 글에서는 이 지도에서 저장장치 경로를 골라, UUU로 실행 중인 U-Boot에 읽기 명령을 보내고 eMMC 파형을 관찰하는 과정을 별도로 정리한다.
The next article follows the storage path: send a read-only command to U-Boot through UUU and observe the eMMC waveform.
#BSP
#LinuxKernel
#EmbeddedLinux
#DeviceTree
#DeviceDriver
#eMMC
'임베디드' 카테고리의 다른 글
| Device Tree와 Linux 플랫폼 드라이버 작성 입문 (0) | 2026.09.11 |
|---|---|
| UUU 명령 하나로 eMMC 파형과 드라이버 코드까지 따라가기 (0) | 2026.09.11 |
| 디바이스 트리, 리눅스 보드가 하드웨어를 보는 방법 (0) | 2026.09.08 |
| 임베디드 리눅스 부팅 과정, 부트로더부터 유저랜드까지 (0) | 2026.08.28 |
| mcp251xfd FIFO overflow, can0에서 RX가 떨어질 때 먼저 볼 것 (0) | 2026.08.26 |
