Device Tree 심화

DTS/DTB/FDT 구조, 바인딩, OF API, 오버레이, 주소 변환, 인터럽트 매핑까지 Linux 커널 Device Tree 종합 가이드.

관련 표준: Devicetree Specification v0.4 (devicetree.org) — DT 문법, FDT 바이너리 포맷, 주소 변환 규칙 등 근본 규격. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

DT 개요

Device Tree(DT)는 하드웨어 토폴로지를 트리 구조의 데이터로 기술하는 표준이다. 원래 Open Firmware(IEEE 1275)에서 유래했으며, ARM/RISC-V/PowerPC 등 자가 열거(self-enumeration)가 불가능한 플랫폼에서 커널이 부팅 시 하드웨어 정보를 전달받는 핵심 메커니즘이다.

부팅 흐름

  1. 개발자가 DTS(Device Tree Source)를 작성
  2. DTC(Device Tree Compiler)가 DTS를 DTB(Device Tree Blob, FDT 바이너리)로 컴파일
  3. 부트로더(U-Boot 등)가 DTB를 메모리에 로드하고 커널에 주소 전달
  4. 커널이 unflatten_device_tree()로 FDT를 device_node 트리로 변환
  5. 각 드라이버가 OF API로 노드/프로퍼티를 조회하여 디바이스 초기화
ACPI vs DT: x86 서버는 주로 ACPI를 사용하고, ARM/RISC-V 임베디드는 DT를 사용한다. 커널은 fwnode 추상화로 두 방식을 통합 처리한다.

DTS 문법

DTS는 노드프로퍼티의 계층 구조이다. 각 노드는 name@unit-address 형식을 가진다.

/* 최소 DTS 예제 */
/dts-v1/;

/ {
    compatible = "vendor,board";
    model = "Vendor Board Rev A";
    #address-cells = <2>;
    #size-cells = <2>;

    chosen {
        bootargs = "console=ttyS0,115200";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x40000000>;
    };

    uart0: serial@9000000 {
        compatible = "ns16550a";
        reg = <0x0 0x9000000 0x0 0x1000>;
        interrupts = <0 1 4>;
        clock-frequency = <24000000>;
    };
};

핵심 프로퍼티

프로퍼티설명
compatible드라이버 매칭 문자열 리스트 (가장 구체적 -> 일반적 순서)
#address-cells자식 노드 reg에서 주소 필드가 차지하는 u32 셀 수
#size-cells자식 노드 reg에서 크기 필드가 차지하는 u32 셀 수
regMMIO 영역 (base, size) 튜플
interrupts인터럽트 지정자(specifier) 배열
phandle다른 노드를 참조하는 고유 정수 (DTC가 자동 생성)
status"okay" | "disabled" -- 노드 활성화 여부

레이블과 phandle 참조

gic: interrupt-controller@8000000 {
    compatible = "arm,gic-v3";
    #interrupt-cells = <3>;
    interrupt-controller;
};

timer {
    compatible = "arm,armv8-timer";
    interrupt-parent = <&gic>;  /* phandle 참조 */
    interrupts = <1 13 4>, <1 14 4>;
};

FDT 바이너리 포맷

DTB 파일은 Flattened Device Tree(FDT) 바이너리 형식이다. 부트로더가 커널에 전달하는 실제 데이터 형태이며, 헤더 + 3개 블록으로 구성된다.

+---------------------------+
| fdt_header (40 bytes)     |  magic: 0xD00DFEED
|  totalsize, off_dt_struct |
|  off_dt_strings, version  |
+---------------------------+
| memory reservation block  |  물리 메모리 예약 영역
+---------------------------+
| structure block            |  FDT_BEGIN_NODE / FDT_PROP / FDT_END_NODE 토큰
+---------------------------+
| strings block              |  프로퍼티 이름 문자열 테이블
+---------------------------+

커널의 drivers/of/fdt.c에서 unflatten_device_tree()가 FDT를 순회하며 struct device_node 트리를 생성한다.

DTC 컴파일러

DTC(Device Tree Compiler)는 DTS <-> DTB 간 변환을 수행한다.

# DTS -> DTB 컴파일
dtc -I dts -O dtb -o board.dtb board.dts

# DTB -> DTS 디컴파일 (디버깅)
dtc -I dtb -O dts -o decompiled.dts board.dtb

# 커널 빌드 시스템에서 DTB 빌드
make dtbs              # arch/arm64/boot/dts/ 아래 DTB 생성
make dtbs_check        # dt-schema 검증
주의: dtc 단독으로는 C 전처리기(cpp)를 실행하지 않는다. #include가 포함된 DTS는 커널 빌드 시스템(scripts/dtc/)이 전처리 후 컴파일한다.

커널 OF API

커널 드라이버는 <linux/of.h>의 OF(Open Firmware) API로 Device Tree 노드와 프로퍼티를 조회한다.

주요 함수

#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>

/* 노드 탐색 */
struct device_node *of_find_node_by_path(const char *path);
struct device_node *of_find_compatible_node(
    struct device_node *from, const char *type,
    const char *compat);

/* 프로퍼티 읽기 */
int of_property_read_u32(const struct device_node *np,
    const char *propname, u32 *out_value);
int of_property_read_string(const struct device_node *np,
    const char *propname, const char **out_string);
bool of_property_read_bool(const struct device_node *np,
    const char *propname);

/* 리소스 획득 */
struct resource *of_iomap(struct device_node *np, int index);
int of_irq_get(struct device_node *np, int index);

플랫폼 드라이버 예제

static int my_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    u32 val;
    int irq;

    /* DT 프로퍼티 읽기 */
    if (of_property_read_u32(np, "clock-frequency", &val))
        return -EINVAL;

    irq = platform_get_irq(pdev, 0);  /* OF 기반 IRQ 획득 */

    dev_info(&pdev->dev, "clock-freq=%u irq=%d\n", val, irq);
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-device" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);

fwnode 추상화

struct fwnode_handle은 DT(of_fwnode)와 ACPI(acpi_fwnode)를 통합하는 추상화 레이어이다. 최신 드라이버는 OF API 대신 device_property API를 사용하여 펌웨어 독립적 코드를 작성할 수 있다.

#include <linux/property.h>

/* DT/ACPI 모두 지원하는 통합 API */
int device_property_read_u32(struct device *dev,
    const char *propname, u32 *val);
int device_property_read_string(struct device *dev,
    const char *propname, const char **val);
bool device_property_present(struct device *dev,
    const char *propname);
권장: 새로 작성하는 드라이버는 of_property_read_*() 대신 device_property_read_*()를 사용하라. DT/ACPI 모두에서 동작하며, 소프트웨어 노드(swnode)를 통한 단위 테스트도 가능하다.

바인딩 규격

DT 바인딩(binding)은 특정 하드웨어를 기술하기 위해 어떤 프로퍼티가 필수/선택인지 정의하는 규격 문서이다. 커널 소스의 Documentation/devicetree/bindings/에 위치한다.

YAML dt-schema

기존 텍스트 바인딩에서 YAML 기반 dt-schema로 전환 중이다. JSON Schema를 확장한 형식으로 자동 검증이 가능하다.

# Documentation/devicetree/bindings/serial/ns16550.yaml
$id: http://devicetree.org/schemas/serial/ns16550.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: NS16550 compatible UART

properties:
  compatible:
    enum:
      - ns16550a
      - ns16550
  reg:
    maxItems: 1
  clock-frequency:
    description: Input clock frequency in Hz

required:
  - compatible
  - reg
  - clock-frequency
# 바인딩 검증
make dtbs_check          # 전체 DTB에 대해 dt-schema 검증
make dt_binding_check    # 바인딩 YAML 문법만 검증

주소 변환 (ranges)

DT에서 버스 계층 간 주소 변환은 ranges 프로퍼티로 정의한다. 자식 버스 주소를 부모 버스 주소로 매핑하는 테이블이다.

soc {
    compatible = "simple-bus";
    #address-cells = <1>;  /* 자식 주소: 1 셀 */
    #size-cells = <1>;
    ranges = <0x0 0x0 0x10000000 0x10000000>;
    /*         child_addr  parent_addr  size
     *         0x0000_0000 -> 0x1000_0000, 256MB */

    uart@9000 {
        reg = <0x9000 0x1000>;
        /* 실제 물리 주소: 0x1000_0000 + 0x9000 = 0x1000_9000 */
    };
};

커널 함수 of_translate_address() (drivers/of/address.c)가 ranges를 순회하며 최종 CPU 물리 주소를 계산한다.

인터럽트 매핑

DT 인터럽트 시스템은 interrupt-parent, interrupts, interrupt-controller, #interrupt-cells로 구성된다. 복잡한 인터럽트 라우팅은 interrupt-map으로 처리한다.

pcie@40000000 {
    interrupt-map-mask = <0x1800 0 0 7>;
    interrupt-map =
        /* dev  pin  parent  parent-irq */
        <0x0000 0 0 1 &gic 0 0 32 4>,  /* INTA -> SPI 32 */
        <0x0000 0 0 2 &gic 0 0 33 4>,  /* INTB -> SPI 33 */
        <0x0000 0 0 3 &gic 0 0 34 4>,  /* INTC -> SPI 34 */
        <0x0000 0 0 4 &gic 0 0 35 4>;  /* INTD -> SPI 35 */
};
프로퍼티설명
interrupt-controller이 노드가 인터럽트 컨트롤러임을 선언 (빈 프로퍼티)
#interrupt-cells인터럽트 지정자의 u32 셀 수 (GIC-v3: 3)
interrupt-parent인터럽트를 전달할 부모 컨트롤러 (phandle)
interrupt-map자식 인터럽트를 부모 인터럽트로 변환하는 테이블
interrupt-map-mask매핑 조회 전 적용할 마스크

Device Tree Overlay

DT Overlay는 런타임(또는 부트 시)에 기존 DTB 위에 노드/프로퍼티를 추가/수정하는 메커니즘이다. 확장 보드(HAT, Cape) 지원에 필수적이다.

/* my-overlay.dts */
/dts-v1/;
/plugin/;

&{/soc} {
    my_sensor@50000 {
        compatible = "vendor,temp-sensor";
        reg = <0x50000 0x100>;
        status = "okay";
    };
};
# DTBO 컴파일
dtc -I dts -O dtb -o my-overlay.dtbo my-overlay.dts

# ConfigFS를 통한 런타임 적용 (지원 플랫폼)
mkdir /sys/kernel/config/device-tree/overlays/my-overlay
cat my-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-overlay/dtbo

# 오버레이 제거
rmdir /sys/kernel/config/device-tree/overlays/my-overlay
주의: 런타임 오버레이 적용/제거는 드라이버 상태와의 일관성 문제가 있다. 커널 메인라인에서는 제한적으로 지원하며, 안전한 제거를 위해 CONFIG_OF_OVERLAY + CONFIG_OF_DYNAMIC이 필요하다.

DT 디버깅

procfs / sysfs 확인

# 런타임 DT 트리 확인
ls /proc/device-tree/              # FDT unflattened 노드
cat /proc/device-tree/compatible   # 루트 compatible 문자열

# sysfs에서 디바이스-DT 노드 연결 확인
ls -la /sys/devices/platform/*/of_node

DTB 분석

# DTB를 사람이 읽을 수 있는 DTS로 변환
dtc -I dtb -O dts /sys/firmware/fdt

# fdtdump으로 바이너리 구조 확인
fdtdump board.dtb | head -50

커널 로그 디버깅

# DT 관련 커널 메시지 필터링
dmesg | grep -i "of:"
dmesg | grep -i "device.tree"

# 부팅 파라미터로 DT 디버그 활성화
# earlycon으로 unflatten 과정 확인 가능
팁: /proc/device-tree/는 커널이 실제 사용 중인 DT 스냅샷이다. 오버레이 적용 후에도 이 경로에서 최종 상태를 확인할 수 있다.

흔한 오류 패턴

증상원인해결
드라이버 probe 안 됨compatible 불일치DTS와 of_match_table 문자열 정확히 비교
IRQ 매핑 실패#interrupt-cells 불일치인터럽트 컨트롤러의 #interrupt-cells와 specifier 셀 수 일치 확인
주소 매핑 오류ranges 미지정 또는 오류부모 노드의 #address-cells/#size-cellsreg 형식 확인
오버레이 적용 실패대상 노드 경로 오류/proc/device-tree/에서 실제 경로 확인