임베디드 시스템 개발에서 U-Boot는 단순히 운영체제를 로딩하는 부트로더를 넘어, 하드웨어 초기화와 시스템 제어를 담당하는 핵심 소프트웨어입니다. 특히 최근의 U-Boot는 리눅스 커널과 유사한 드라이버 모델(Driver Model, DM)을 도입하여 하드웨어 추상화와 코드 재사용성을 획기적으로 개선했습니다. 이번 포스팅에서는 U-Boot DM의 기본 개념부터 이를 활용한 Rockchip RK3399 UART 드라이버 포팅 실습까지 체계적으로 살펴보겠습니다.

핵심 요약
- U-Boot Driver Model(DM)은 장치(udevice)와 드라이버, 클래스(uclass)를 구분하여 하드웨어 제어 코드를 구조화합니다.
- Device Tree를 기반으로 장치를 스캔하고 적절한 드라이버를 할당(Binding)하여 하드웨어를 활성화하는 방식을 취합니다.
- 커널 드라이버와 유사한 구조를 가지고 있어, 한번 이해하면 다양한 주변 장치 드라이버 포팅에 즉시 응용 가능합니다.
1. U-Boot Driver Model(DM)의 핵심
과거의 U-Boot는 보드별로 코드가 흩어져 있어 관리가 어려웠습니다. DM은 이를 프레임워크로 표준화하여 유지보수성을 극대화했습니다.
| 구성 요소 | 역할 |
| udevice (Device) | 하드웨어 장치를 나타내는 데이터 노드 |
| driver | 하드웨어를 제어하는 실제 로직 (Ops 포함) |
| uclass | 동일한 기능의 드라이버 그룹 (예: UCLASS_SERIAL, UCLASS_SPI) |
| Device Tree (DT) | 하드웨어 사양을 정의하는 데이터 구조 |
2. U_BOOT_DRIVER 매크로 분석
드라이버는 U_BOOT_DRIVER 매크로를 통해 등록됩니다. 이 매크로는 드라이버의 이름, 클래스, 매칭 정보, 그리고 실행 함수 등을 포함합니다.
- probe 함수: 장치가 사용되기 직전 호출되어 클럭 설정, 레지스터 맵핑, 리셋 등의 초기화를 수행합니다.
- ops (Operations): 드라이버가 외부로 노출하는 기능 함수들을 모아놓은 구조체입니다.
3. DM의 장치 활성화 과정
U-Boot는 부팅 시 다음 절차를 통해 하드웨어를 인식하고 준비합니다.
- DTB 로딩: 하드웨어 명세가 담긴 데이터 블록을 읽습니다.
- UCLASS 스캔: 시스템에 필요한 장치 클래스를 확인합니다.
- 매칭 (Matching): Device Tree의 compatible 필드와 드라이버의 of_match를 대조합니다.
- 바인딩 및 프로브 (Binding & Probe): 장치와 드라이버를 결합하고 probe() 함수를 호출하여 활성화합니다.
4. 실습: RK3399 UART 드라이버 포팅
최소한의 기능을 갖춘 UART 드라이버 구현 예제입니다.
드라이버 소스 구현 (drivers/serial/serial_rk3399_simple.c)
#include <common.h>
#include <dm.h>
#include <serial.h>
#include <asm/io.h>
struct rk_uart_priv {
void __iomem *base;
};
static int rk_uart_probe(struct udevice *dev) {
struct rk_uart_priv *priv = dev_get_priv(dev);
priv->base = (void *)dev_read_addr(dev); // DT에서 주소 정보 획득
if (!priv->base) return -EINVAL;
return 0;
}
static int rk_uart_putc(struct udevice *dev, const char ch) {
struct rk_uart_priv *priv = dev_get_priv(dev);
writel(ch, priv->base + 0x00); // TX 레지스터에 데이터 쓰기
return 0;
}
static const struct dm_serial_ops rk_uart_ops = {
.putc = rk_uart_putc,
};
static const struct udevice_id rk_uart_ids[] = {
{ .compatible = "rockchip,rk3399-simple-uart" },
{}
};
U_BOOT_DRIVER(rk3399_simple_uart) = {
.name = "rk3399_simple_uart",
.id = UCLASS_SERIAL,
.of_match = rk_uart_ids,
.probe = rk_uart_probe,
.ops = &rk_uart_ops,
.priv_auto = sizeof(struct rk_uart_priv),
};
5. 주요 드라이버 디렉토리 구조
포팅 작업 시 아래 경로의 코드들을 참고하면 표준적인 구현 방식을 배울 수 있습니다.
| 디렉토리 | 담당 기능 |
| drivers/serial/ | UART 등 직렬 통신 드라이버 |
| drivers/spi/ | SPI 컨트롤러 및 메모리 드라이버 |
| drivers/mmc/ | SD/eMMC 카드 제어 드라이버 |
| drivers/net/ | 이더넷 컨트롤러 및 PHY 드라이버 |
| drivers/clk/ | 시스템 클럭 프레임워크 |
6. 개발을 위한 팁
- 기존 코드 분석: 새로운 드라이버를 작성하기 전, drivers/ 하위에서 동일한 클래스(예: serial/)의 기존 드라이버를 열어보십시오. 구현 패턴이 거의 일치합니다.
- 디버그 로그 활용: 드라이버가 로드되지 않는다면 debug() 함수를 probe 내부 곳곳에 배치하여 어느 단계에서 매칭이 실패하는지 확인하십시오.
7. 흔히 하는 실수
- Compatible 문자열 불일치: Device Tree의 compatible 문자열과 드라이버의 of_match에 정의된 문자열이 한 글자라도 다르면 장치는 프로브되지 않습니다.
- 주소 할당 오류: dev_read_addr를 통해 가져온 주소가 실제 SoC 데이터시트의 메모리 맵과 일치하는지 다시 확인하십시오.
결론
U-Boot 드라이버 모델은 리눅스 커널 드라이버 아키텍처와 매우 유사합니다. DM의 구조를 깊이 이해하면 새로운 하드웨어를 브링업할 때 겪는 시행착오를 크게 줄일 수 있습니다. 본 글의 UART 예제를 시작으로, SPI나 I2C 등 다른 드라이버 모델로 영역을 확장해 보시기 바랍니다. 체계적으로 설계된 드라이버 코드는 시스템의 안정성을 보장하는 가장 확실한 방법입니다.
'Embedded System > Bootloader & System Startup' 카테고리의 다른 글
| 임베디드 개발자를 위한 U-Boot 환경 변수 완벽 가이드: 부팅 제어와 자동화 (0) | 2025.12.08 |
|---|---|
| 임베디드 개발자를 위한 U-Boot 커스텀 명령어 추가 및 활용 가이드 U_BOOT_CMD 완벽 분석 (0) | 2025.12.07 |
| U-Boot 부팅 시퀀스 완벽 분석: 다단계 부팅 구조와 로그 해석 (0) | 2025.12.04 |
| U-Boot 포팅 가이드: 환경 설정부터 보드 초기화까지 (0) | 2025.12.03 |
| U-Boot 디렉토리 구조와 빌드 과정 분석 (이미지 파일 생성 원리) (0) | 2025.12.02 |