Device Tree 심화
DTS/DTB/FDT 구조, 바인딩, OF API, 오버레이, 주소 변환, 인터럽트 매핑까지 Linux 커널 Device Tree 종합 가이드.
DT 개요
Device Tree(DT)는 하드웨어 토폴로지를 트리 구조의 데이터로 기술하는 표준이다. 원래 Open Firmware(IEEE 1275)에서 유래했으며, ARM/RISC-V/PowerPC 등 자가 열거(self-enumeration)가 불가능한 플랫폼에서 커널이 부팅 시 하드웨어 정보를 전달받는 핵심 메커니즘이다.
부팅 흐름
- 개발자가 DTS(Device Tree Source)를 작성
- DTC(Device Tree Compiler)가 DTS를 DTB(Device Tree Blob, FDT 바이너리)로 컴파일
- 부트로더(U-Boot 등)가 DTB를 메모리에 로드하고 커널에 주소 전달
- 커널이
unflatten_device_tree()로 FDT를device_node트리로 변환 - 각 드라이버가 OF API로 노드/프로퍼티를 조회하여 디바이스 초기화
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 셀 수 |
reg | MMIO 영역 (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-cells와 reg 형식 확인 |
| 오버레이 적용 실패 | 대상 노드 경로 오류 | /proc/device-tree/에서 실제 경로 확인 |