Embedded System/Bootloader & System Startup

임베디드 개발자를 위한 U-Boot 드라이버 모델(DM) 핵심 분석과 포팅 가이드

임베디드 친구 2025. 12. 5. 20:47
반응형

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

Generated by Gemini AI.

핵심 요약

  • 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는 부팅 시 다음 절차를 통해 하드웨어를 인식하고 준비합니다.

  1. DTB 로딩: 하드웨어 명세가 담긴 데이터 블록을 읽습니다.
  2. UCLASS 스캔: 시스템에 필요한 장치 클래스를 확인합니다.
  3. 매칭 (Matching): Device Tree의 compatible 필드와 드라이버의 of_match를 대조합니다.
  4. 바인딩 및 프로브 (Binding & Probe): 장치와 드라이버를 결합하고 probe() 함수를 호출하여 활성화합니다.

4. 실습: RK3399 UART 드라이버 포팅

최소한의 기능을 갖춘 UART 드라이버 구현 예제입니다.

드라이버 소스 구현 (drivers/serial/serial_rk3399_simple.c)

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 등 다른 드라이버 모델로 영역을 확장해 보시기 바랍니다. 체계적으로 설계된 드라이버 코드는 시스템의 안정성을 보장하는 가장 확실한 방법입니다.

반응형