Device Tree 심화
DTS/DTB/FDT 구조, 바인딩, OF API, 오버레이, 주소 변환, 인터럽트 매핑까지 Linux 커널 Device Tree 종합 가이드.
핵심 요약
- 단계 분리 — 펌웨어, 부트로더, 커널 초기화 경계를 구분합니다.
- 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
- 신뢰 체인 — Secure Boot 등 검증 체인을 흐름으로 이해합니다.
- 실패 지점 — 부팅 로그에서 단계별 실패 단서를 빠르게 찾습니다.
- 호환성 관점 — 플랫폼 차이에 따른 초기화 분기를 함께 점검합니다.
단계별 이해
- 부팅 단계 식별
현재 이슈가 어느 단계에서 발생하는지 먼저 고정합니다. - 입력 데이터 확인
펌웨어/테이블/이미지 메타데이터를 점검합니다. - 전환 경계 검증
단계 간 인자 전달과 상태 인계를 추적합니다. - 플랫폼별 재검증
다른 하드웨어 조건에서도 동일하게 동작하는지 확인합니다.
Device Tree는 하드웨어 구성을 기술하는 데이터 구조로, PCI/USB처럼 자동 열거(enumeration)가 불가능한 SoC 내장 디바이스의 정보를 커널에 전달합니다. ARM, RISC-V, PowerPC 등 임베디드 플랫폼에서 필수적이며, Open Firmware(IEEE 1275) 표준에서 유래했습니다.
.dts (소스) → dtc (컴파일러) → .dtb (바이너리 블롭) → 부트로더가 메모리에 로드 → 커널이 파싱하여 struct device_node 트리 구축 → 드라이버가 of_* API로 프로퍼티 조회
Device Tree 아키텍처
DTS 문법 상세
/*
* Device Tree Source (.dts) 문법
*
* 기본 구조: 노드(node)와 프로퍼티(property)의 트리
*
* 노드 형식:
* [label:] node-name[@unit-address] {
* [properties];
* [child nodes];
* };
*
* 프로퍼티 데이터 타입:
* - 빈 값: 속성 존재만 의미 (boolean)
* - u32: < 0x1234 >
* - u64: /bits/ 64 < 0x1234567890 >
* - 문자열: "hello"
* - 문자열 목록: "first", "second"
* - 바이트 배열: [00 11 22 33]
* - phandle 참조: <&label>
* - 혼합: < 0x1234 >, "string", [00 ff]
*/
/* ===== 완전한 DTS 예제 ===== */
/dts-v1/;
/* .dtsi 인클루드 — SoC 공통 정의 재사용 */
#include "my-soc.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/my-soc-clk.h>
/ {
/* 루트 노드 — 보드 전체 정보 */
model = "MyVendor MyBoard Rev.A";
compatible = "myvendor,myboard", "myvendor,my-soc";
/* #address-cells / #size-cells:
* 자식 노드의 reg 프로퍼티 해석 방법 지정
* #address-cells = <2> → 주소가 u32 × 2 = 64-bit
* #size-cells = <1> → 크기가 u32 × 1 = 32-bit */
#address-cells = <2>;
#size-cells = <2>;
/* chosen 노드 — 부트로더→커널 런타임 파라미터 */
chosen {
bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = "serial0:115200n8";
};
/* aliases — 노드에 짧은 이름 부여 */
aliases {
serial0 = &uart0;
ethernet0 = ð0;
mmc0 = &sdhci0;
};
/* memory 노드 — 물리 메모리 레이아웃 */
memory@80000000 {
device_type = "memory";
reg = <0x0 0x80000000 0x0 0x40000000>; /* 1 GiB @ 0x80000000 */
};
/* cpus 노드 */
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x0>;
enable-method = "psci";
clocks = <&cpu_clk>;
operating-points-v2 = <&cpu_opp_table>;
};
cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53";
reg = <0x1>;
enable-method = "psci";
};
};
/* SoC 버스 — 주소 공간 정의 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x0 0x40000000>; /* 자식→부모 주소 변환 */
/* 인터럽트 컨트롤러 */
gic: interrupt-controller@1c81000 {
compatible = "arm,gic-400";
interrupt-controller; /* 빈 프로퍼티 (boolean) */
#interrupt-cells = <3>; /* 자식의 interrupts 해석: type irq flags */
reg = <0x1c81000 0x1000>, /* GICD */
<0x1c82000 0x2000>; /* GICC */
};
/* 클럭 컨트롤러 — phandle로 참조 */
ccu: clock-controller@1c20000 {
compatible = "myvendor,my-soc-ccu";
reg = <0x1c20000 0x400>;
clocks = <&osc24m>, <&osc32k>;
clock-names = "hosc", "losc";
#clock-cells = <1>; /* 자식이 참조 시 인덱스 1개 */
#reset-cells = <1>;
};
/* UART — label로 phandle 자동 생성 */
uart0: serial@1c28000 {
compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
reg = <0x1c28000 0x400>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_UART0>;
resets = <&ccu RST_UART0>;
reg-shift = <2>;
reg-io-width = <4>;
status = "okay";
};
/* I2C 컨트롤러 + 자식 디바이스 */
i2c0: i2c@1c2ac00 {
compatible = "myvendor,my-soc-i2c";
reg = <0x1c2ac00 0x400>;
interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_I2C0>;
resets = <&ccu RST_I2C0>;
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* I2C 슬레이브 디바이스 */
sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>; /* I2C 주소 */
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 20 IRQ_TYPE_EDGE_FALLING>;
};
pmic@34 {
compatible = "xpower,axp803";
reg = <0x34>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_LOW>;
/* 서브노드: PMIC 내 레귤레이터 */
regulators {
reg_dcdc1: dcdc1 {
regulator-name = "vcc-3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
};
};
};
};
};
};
표준 프로퍼티 레퍼런스
| 프로퍼티 | 타입 | 설명 | 예시 |
|---|---|---|---|
compatible | string-list | 드라이버 매칭 키. 구체적→일반적 순서 | "vendor,exact", "vendor,fallback" |
reg | prop-encoded | 주소/크기 쌍. 해석은 부모의 #address-cells/#size-cells에 의존 | <0x10000 0x1000> |
interrupts | prop-encoded | 인터럽트 지정자. 해석은 인터럽트 컨트롤러의 #interrupt-cells에 의존 | <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH> |
interrupt-parent | phandle | 인터럽트 컨트롤러 참조 (생략 시 부모 노드에서 상속) | <&gic> |
clocks | phandle+args | 클럭 소스 참조 | <&ccu CLK_UART0> |
clock-names | string-list | 클럭 이름 (clocks와 순서 대응) | "apb", "mod" |
resets | phandle+args | 리셋 컨트롤러 참조 | <&ccu RST_UART0> |
status | string | "okay"=활성, "disabled"=비활성 | "okay" |
#address-cells | u32 | 자식 reg의 주소 u32 개수 | <2> |
#size-cells | u32 | 자식 reg의 크기 u32 개수 (0이면 크기 없음) | <1> |
ranges | prop-encoded | 자식→부모 주소 변환. 빈 값이면 1:1 매핑 | <0x0 0x0 0x10000000 0x1000000> |
dma-ranges | prop-encoded | DMA 주소 변환 (CPU 주소 ≠ DMA 주소일 때) | <0x0 0x0 0x80000000 0x80000000> |
pinctrl-0 | phandle-list | 핀 설정 참조 (상태 0=default) | <&uart0_pins> |
pinctrl-names | string-list | 핀 설정 상태 이름 | "default", "sleep" |
*-gpios | phandle+args | GPIO 참조 (접두사가 이름) | reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW> |
*-supply | phandle | 전원 레귤레이터 참조 | vcc-supply = <®_3v3> |
.dtsi 인클루드 구조와 오버라이드
/*
* .dtsi (Device Tree Source Include) — SoC 공통 정의
* .dts — 보드별 최종 파일, .dtsi를 인클루드하고 오버라이드
*
* 계층 구조 예시:
* arch/arm64/boot/dts/
* ├── myvendor/
* │ ├── my-soc.dtsi ← SoC 공통 (IP 블록, 클럭, 인터럽트)
* │ ├── my-soc-gpu.dtsi ← GPU 관련 (선택적 인클루드)
* │ ├── myboard-rev-a.dts ← 보드 A (오버라이드, 확장)
* │ └── myboard-rev-b.dts ← 보드 B (다른 설정)
*
* 오버라이드 규칙:
* - .dts에서 .dtsi의 노드를 재정의하면 프로퍼티가 병합/덮어쓰기
* - &label 참조로 기존 노드를 수정 (노드 경로 생략 가능)
*/
/* === my-soc.dtsi (SoC 공통) === */
/ {
soc {
uart0: serial@1c28000 {
compatible = "myvendor,my-soc-uart";
reg = <0x1c28000 0x400>;
clocks = <&ccu CLK_UART0>;
status = "disabled"; /* 기본: 비활성 */
};
i2c0: i2c@1c2ac00 {
compatible = "myvendor,my-soc-i2c";
reg = <0x1c2ac00 0x400>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
};
};
/* === myboard-rev-a.dts (보드별) === */
/dts-v1/;
#include "my-soc.dtsi"
/ {
model = "MyBoard Rev.A";
};
/* &label 참조로 기존 노드 오버라이드 */
&uart0 {
status = "okay"; /* 이 보드에서 UART0 활성화 */
pinctrl-0 = <&uart0_pins>; /* 핀 설정 추가 */
pinctrl-names = "default";
};
&i2c0 {
status = "okay";
/* 이 보드에 연결된 센서 추가 */
accelerometer@1d {
compatible = "st,lis3dh";
reg = <0x1d>;
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 25 IRQ_TYPE_EDGE_RISING>;
vdd-supply = <®_3v3>;
};
};
Device Tree Overlay
/*
* Device Tree Overlay (.dtbo):
*
* 런타임에 기존 DTB에 노드/프로퍼티를 추가·수정·삭제합니다.
* 용도:
* - HAT/Cape/Shield 등 확장 보드 자동 인식
* - Raspberry Pi, BeagleBone 등에서 광범위하게 사용
* - 재부팅 없이 하드웨어 구성 변경 (configfs 기반)
*
* Overlay 문법:
* /plugin/; 지시어로 overlay 파일임을 선언
* fragment 또는 __overlay__ 블록으로 수정할 노드 지정
*/
/* === my-hat-overlay.dts === */
/dts-v1/;
/plugin/;
/* &{/path} 또는 &label로 대상 노드 참조 */
&i2c0 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
/* HAT에 장착된 OLED 디스플레이 */
oled@3c {
compatible = "solomon,ssd1306";
reg = <0x3c>;
width = <128>;
height = <64>;
solomon,com-invdir;
};
};
/* fragment 문법 (대체 형식) */
/ {
fragment@0 {
target = <&spi0>;
__overlay__ {
status = "okay";
cs-gpios = <&gpio 8 GPIO_ACTIVE_LOW>;
can0: can@0 {
compatible = "microchip,mcp2515";
reg = <0>;
spi-max-frequency = <10000000>;
clocks = <&can_osc>;
interrupt-parent = <&gpio>;
interrupts = <25 IRQ_TYPE_EDGE_FALLING>;
};
};
};
};
# Overlay 컴파일
$ dtc -I dts -O dtb -@ -o my-hat.dtbo my-hat-overlay.dts
# -@ : __symbols__ 노드 생성 (overlay 심볼 해석에 필요)
# configfs를 통한 런타임 적용 (CONFIG_OF_OVERLAY, CONFIG_OF_CONFIGFS 필요)
# 사전 준비: mount -t configfs none /sys/kernel/config
$ mkdir -p /sys/kernel/config/device-tree/overlays/my-hat
$ cat my-hat.dtbo > /sys/kernel/config/device-tree/overlays/my-hat/dtbo
# → 커널이 overlay를 live DT에 병합, 새 디바이스 probe
# Overlay 제거
$ rmdir /sys/kernel/config/device-tree/overlays/my-hat
# → 관련 디바이스 remove, DT에서 노드 제거
# U-Boot에서 부팅 시 적용
# fdt apply ${fdtoverlay_addr}
Device Tree Bindings
/*
* DT Binding = 특정 하드웨어에 필요한 프로퍼티 규격
*
* 위치: Documentation/devicetree/bindings/
* 형식: YAML schema (dt-schema, v5.2+) 또는 텍스트 문서 (레거시)
*
* 검증 도구:
* make dt_binding_check ← YAML 스키마 자체 검증
* make dtbs_check ← DTB가 바인딩을 준수하는지 검증
*
* compatible 문자열 규칙:
* "vendor,device[-version]"
* vendor: JEDEC 또는 Documentation/devicetree/bindings/vendor-prefixes.yaml
* device: 구체적 칩/IP 이름
*
* 예시:
* "ti,am335x-uart" ← TI AM335x SoC의 UART
* "samsung,exynos4210-i2c" ← Samsung Exynos4210의 I2C
* "snps,dw-apb-uart" ← Synopsys DesignWare APB UART (IP 블록)
*/
# YAML 바인딩 예시: Documentation/devicetree/bindings/serial/snps,dw-apb-uart.yaml
# (간략화)
# $id: http://devicetree.org/schemas/serial/snps,dw-apb-uart.yaml#
# $schema: http://devicetree.org/meta-schemas/core.yaml#
# title: Synopsys DesignWare ABP UART
#
# properties:
# compatible:
# oneOf:
# - items:
# - enum:
# - myvendor,my-soc-uart
# - const: snps,dw-apb-uart
# reg:
# maxItems: 1
# interrupts:
# maxItems: 1
# clocks:
# minItems: 1
# maxItems: 2
# clock-names:
# items:
# - const: baudclk
# - const: apb_pclk
# reg-shift:
# enum: [0, 2]
#
# required:
# - compatible
# - reg
# - interrupts
# - clocks
# 바인딩 검증 실행
$ make dt_binding_check DT_SCHEMA_FILES=serial/snps,dw-apb-uart.yaml
$ make dtbs_check DT_SCHEMA_FILES=serial/snps,dw-apb-uart.yaml
DTS 컴파일과 디컴파일
# ===== DTS → DTB 컴파일 =====
# 커널 빌드 시스템을 통해 (권장)
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# → arch/arm64/boot/dts/myvendor/*.dtb 생성
# 특정 DTB만 빌드
$ make ARCH=arm64 myvendor/myboard-rev-a.dtb
# DTB 설치
$ make ARCH=arm64 INSTALL_DTBS_PATH=/boot/dtbs dtbs_install
# dtc 직접 사용 (단순 DTS 테스트용; 실전 보드 DTS는 make dtbs 권장)
$ dtc -I dts -O dtb -o myboard.dtb myboard.dts
# -I: 입력 형식 (dts, dtb, fs)
# -O: 출력 형식 (dts, dtb, asm)
# ===== DTB → DTS 디컴파일 =====
$ dtc -I dtb -O dts -o decompiled.dts myboard.dtb
# 실행 중인 시스템의 live DT 디컴파일
$ dtc -I fs -O dts -o live-dt.dts /sys/firmware/devicetree/base/
# ===== DTB 정보 조회 =====
$ fdtdump myboard.dtb | head -50 # 구조 덤프
$ fdtget myboard.dtb /soc/serial@1c28000 compatible
myvendor,my-soc-uart snps,dw-apb-uart
$ fdtget -t x myboard.dtb /soc/serial@1c28000 reg
1c28000 400
# DTB 수정 (디버깅/테스트용)
$ fdtput myboard.dtb /soc/serial@1c28000 status -ts "disabled"
# ===== CPP 전처리 =====
# 커널 빌드 시스템은 DTS를 dtc에 전달하기 전에 C 전처리기(cpp)를 먼저 실행합니다.
# 따라서 #include, #define, #ifdef 등 C 전처리 지시어가 DTS에서 동작합니다.
#
# dt-bindings/ 헤더: include/dt-bindings/ 디렉토리의 .h 파일
# → GPIO, 인터럽트, 클럭 등의 숫자 상수를 매크로로 정의
# 예: #include <dt-bindings/gpio/gpio.h>
# GPIO_ACTIVE_HIGH = 0, GPIO_ACTIVE_LOW = 1
커널 OF(Open Firmware) API
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>
/* ===== 프로퍼티 읽기 ===== */
u32 val;
of_property_read_u32(np, "my-prop", &val); /* u32 1개 */
u32 arr[4];
of_property_read_u32_array(np, "my-array", arr, 4); /* u32 배열 */
u64 val64;
of_property_read_u64(np, "my-u64", &val64); /* u64 */
const char *str;
of_property_read_string(np, "label", &str); /* 문자열 */
int count = of_property_read_string_helper( /* 문자열 목록 */
np, "clock-names", NULL, 0, 0);
bool present = of_property_read_bool(np, "big-endian"); /* boolean */
/* ===== 노드 탐색 ===== */
struct device_node *child;
for_each_child_of_node(np, child) { /* 자식 순회 */
/* child 처리... */
}
struct device_node *node;
node = of_find_compatible_node(NULL, NULL,
"myvendor,my-device"); /* compatible로 검색 */
node = of_find_node_by_path("/soc/serial@1c28000"); /* 경로로 검색 */
node = of_parse_phandle(np, "clocks", 0); /* phandle 참조 해석 */
/* ===== 리소스 가져오기 ===== */
struct resource res;
of_address_to_resource(np, 0, &res); /* reg → struct resource */
void __iomem *base = of_iomap(np, 0); /* reg → ioremap */
int irq = of_irq_get(np, 0); /* interrupts → IRQ 번호 */
int irq2 = platform_get_irq(pdev, 0); /* platform 래퍼 (권장) */
/* ===== compatible 매칭 확인 ===== */
bool match = of_device_is_compatible(np, "vendor,dev");
const struct of_device_id *id;
id = of_match_device(my_of_ids, &pdev->dev);
if (id && id->data) {
/* match-specific 데이터 사용 */
const struct my_hw_data *hw = id->data;
}
Device Tree + Platform Driver 통합
/* ===== 완전한 DT 기반 Platform Driver 예제 ===== */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/clk.h>
#include <linux/reset.h>
#include <linux/io.h>
/* 칩 버전별 데이터 */
struct my_hw_data {
int fifo_depth;
bool has_dma;
};
static const struct my_hw_data hw_v1 = { .fifo_depth = 16, .has_dma = false };
static const struct my_hw_data hw_v2 = { .fifo_depth = 64, .has_dma = true };
/* of_device_id: compatible 문자열 → 드라이버 매칭 테이블 */
static const struct of_device_id my_of_ids[] = {
{ .compatible = "myvendor,my-device-v1", .data = &hw_v1 },
{ .compatible = "myvendor,my-device-v2", .data = &hw_v2 },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);
struct my_dev {
void __iomem *base;
struct clk *clk;
struct reset_control *rst;
const struct my_hw_data *hw;
int irq;
};
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_dev *priv;
u32 fifo_thr;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 1. compatible에 연결된 하드웨어 데이터 가져오기 */
priv->hw = of_device_get_match_data(dev);
if (!priv->hw)
return -ENODEV;
/* 2. reg → MMIO 매핑 (devm 관리) */
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
/* 3. interrupts → IRQ 번호 */
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
/* 4. clocks → 클럭 가져오기 + 활성화 */
priv->clk = devm_clk_get_enabled(dev, NULL); /* v6.3+ */
if (IS_ERR(priv->clk))
return dev_err_probe(dev, PTR_ERR(priv->clk),
"failed to get clock\\n");
/* 5. resets → 리셋 제어 */
priv->rst = devm_reset_control_get_exclusive(dev, NULL);
if (IS_ERR(priv->rst))
return PTR_ERR(priv->rst);
reset_control_deassert(priv->rst);
/* 6. 커스텀 프로퍼티 읽기 (선택적, 기본값 지원) */
ret = of_property_read_u32(dev->of_node, "fifo-threshold", &fifo_thr);
if (ret)
fifo_thr = priv->hw->fifo_depth / 2; /* DT에 없으면 기본값 */
platform_set_drvdata(pdev, priv);
dev_info(dev, "probed: fifo=%d dma=%d irq=%d\\n",
priv->hw->fifo_depth, priv->hw->has_dma, priv->irq);
return 0;
}
static void my_remove(struct platform_device *pdev)
{
struct my_dev *priv = platform_get_drvdata(pdev);
reset_control_assert(priv->rst);
}
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my-device",
.of_match_table = my_of_ids, /* DT 매칭 테이블 등록 */
.pm = &my_pm_ops, /* 전원 관리 (선택) */
},
};
module_platform_driver(my_driver);
/*
* 매칭 순서 (우선순위):
* 1. of_match_table — Device Tree compatible 매칭
* 2. acpi_match_table — ACPI _HID 매칭
* 3. id_table — platform_device_id 이름 매칭
* 4. driver.name — platform_device.name 직접 비교 (폴백)
*/
of_* 대신 device_property_read_*(fwnode) API를 사용하면 DT/ACPI 코드를 통일할 수 있습니다. 예: device_property_read_u32(dev, "fifo-depth", &val)
특수 노드와 고급 패턴
/* ===== 주요 특수 노드들 ===== */
/* 1. reserved-memory — 커널이 사용하지 않을 메모리 영역 */
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
/* CMA (Contiguous Memory Allocator) 영역 */
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x0 0x10000000>; /* 256 MiB */
linux,cma-default;
};
/* 펌웨어 전용 영역 */
fw_reserved: framebuffer@be000000 {
reg = <0x0 0xbe000000 0x0 0x2000000>;
no-map; /* 커널이 매핑하지 않음 */
};
};
/* 2. GPIO hog — 부팅 시 GPIO를 고정 상태로 설정 */
&gpio1 {
led-hog {
gpio-hog;
gpios = <10 GPIO_ACTIVE_HIGH>;
output-high;
line-name = "status-led";
};
};
/* 3. 클럭/레귤레이터 고정 정의 (물리 클럭을 DT에서 선언) */
osc24m: oscillator-24m {
compatible = "fixed-clock";
#clock-cells = <0>;
clock-frequency = <24000000>; /* 24 MHz */
clock-output-names = "osc24m";
};
reg_3v3: regulator-3v3 {
compatible = "regulator-fixed";
regulator-name = "vcc-3v3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
};
/* 4. OPP (Operating Performance Points) 테이블 */
cpu_opp_table: opp-table {
compatible = "operating-points-v2";
opp-600000000 {
opp-hz = /bits/ 64 <600000000>;
opp-microvolt = <900000>;
};
opp-1200000000 {
opp-hz = /bits/ 64 <1200000000>;
opp-microvolt = <1100000>;
};
opp-1800000000 {
opp-hz = /bits/ 64 <1800000000>;
opp-microvolt = <1300000>;
opp-suspend; /* suspend 시 이 OPP 사용 */
};
};
/* 5. 인터럽트 매핑 (interrupt-map) — PCI 등 */
pcie@10000000 {
interrupt-map-mask = <0x1800 0 0 7>;
interrupt-map =
<0x0000 0 0 1 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
<0x0000 0 0 2 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
};
Device Tree 디버깅
# ===== 실행 중인 시스템에서 DT 확인 =====
# Live Device Tree (procfs)
$ ls /proc/device-tree/
#address-cells cpus memory@80000000 soc
#size-cells chosen model compatible
# 특정 노드의 프로퍼티 읽기
$ cat /proc/device-tree/model
MyVendor MyBoard Rev.A
$ hexdump -C /proc/device-tree/soc/serial@1c28000/reg
00000000 01 c2 80 00 00 00 04 00
# sysfs를 통한 접근 (동일한 데이터)
$ ls /sys/firmware/devicetree/base/
$ cat /sys/firmware/devicetree/base/compatible
# ===== 커널 로그에서 DT 관련 메시지 =====
$ dmesg | grep -iE 'device.?tree|of_|dts|dtb|compatible'
OF: fdt: Machine model: MyVendor MyBoard Rev.A
OF: fdt: Ignoring memory range 0x0 - 0x80000000
# ===== probe 실패 디버깅 =====
# 매칭되지 않은(드라이버 없는) 디바이스 확인
$ ls /sys/bus/platform/devices/
# 1c28000.serial 1c2ac00.i2c ...
# 특정 디바이스의 드라이버 바인딩 상태
$ ls -la /sys/bus/platform/devices/1c28000.serial/driver
# symlink → 해당 드라이버 (없으면 매칭 실패)
# deferred probe 목록 (의존성 대기 중)
$ cat /sys/kernel/debug/devices_deferred
# 1c2ac00.i2c ← 클럭/레귤레이터 등 의존성 미충족
# 드라이버 강제 바인드/언바인드
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/bind
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/unbind
# ===== Overlay 상태 확인 =====
$ ls /sys/kernel/config/device-tree/overlays/
my-hat/
$ cat /sys/kernel/config/device-tree/overlays/my-hat/status
applied
# ===== ftrace로 DT 매칭 추적 =====
$ echo 1 > /sys/kernel/tracing/events/bus/bus_add_device/enable
$ echo 1 > /sys/kernel/tracing/events/bus/driver_bound/enable
$ cat /sys/kernel/tracing/trace_pipe
# bus_add_device: device 1c28000.serial
# driver_bound: device 1c28000.serial driver my-device
# ===== DT Validation (빌드 시) =====
$ make ARCH=arm64 dt_binding_check # YAML 스키마 검증
$ make ARCH=arm64 dtbs_check # DTB vs 바인딩 검증
$ make ARCH=arm64 W=1 dtbs # 경고 활성화 빌드
compatible문자열은 가장 구체적인 것을 먼저, 일반적인 폴백을 나중에 기술합니다status = "disabled"인 노드는 드라이버가 probe되지 않습니다. .dtsi에서 기본 disabled → .dts에서 필요한 것만 "okay"reg프로퍼티의 해석은 부모의#address-cells/#size-cells에 따라 달라집니다. 실수하면 잘못된 주소로 매핑- phandle 참조(
&label)는 레이블이 정의된 노드를 가리킵니다. 존재하지 않는 레이블은 컴파일 오류 - 새로운 바인딩은 반드시 YAML 스키마를 작성하고
dt_binding_check로 검증해야 합니다 - Overlay 사용 시
dtc -@로 기본 DTB를 컴파일해야__symbols__노드가 포함되어 런타임 심볼 해석이 가능합니다
FDT 바이너리 포맷 (Flattened Device Tree)
DTB 파일은 Flattened Device Tree(FDT) 바이너리 포맷으로 저장됩니다. 부트로더가 이 바이너리를 메모리에 로드하고, 커널의 unflatten_device_tree()가 파싱하여 struct device_node 트리를 구축합니다.
/* ===== FDT 헤더 구조체 (include/linux/libfdt_env.h → scripts/dtc/libfdt/) ===== */
struct fdt_header {
fdt32_t magic; /* 0xD00DFEED (big-endian) */
fdt32_t totalsize; /* DTB 전체 크기 (bytes) */
fdt32_t off_dt_struct; /* Structure Block 시작 오프셋 */
fdt32_t off_dt_strings; /* Strings Block 시작 오프셋 */
fdt32_t off_mem_rsvmap; /* Memory Reservation Block 오프셋 */
fdt32_t version; /* 포맷 버전 (현재 17) */
fdt32_t last_comp_version; /* 호환 가능한 최소 버전 (16) */
fdt32_t boot_cpuid_phys; /* 부팅 CPU의 physical ID */
fdt32_t size_dt_strings; /* Strings Block 크기 */
fdt32_t size_dt_struct; /* Structure Block 크기 */
};
/* FDT는 모두 big-endian으로 저장됨 — cpu_to_fdt32() / fdt32_to_cpu() 로 변환 */
/* ===== Structure Block 토큰 ===== */
#define FDT_BEGIN_NODE 0x00000001 /* 노드 시작 + 이름(NUL종료, 4-byte 정렬) */
#define FDT_END_NODE 0x00000002 /* 노드 종료 */
#define FDT_PROP 0x00000003 /* 프로퍼티: len(u32) + nameoff(u32) + data */
#define FDT_NOP 0x00000004 /* 무시 (편집 시 패딩용) */
#define FDT_END 0x00000009 /* Structure Block 종료 */
/* ===== Memory Reservation Block =====
* 커널이 사용하면 안 되는 물리 메모리 영역 (예: DTB 자체, 펌웨어 영역)
* { uint64_t address; uint64_t size; } 쌍의 배열
* address=0, size=0 엔트리로 종료
*
* 참고: reserved-memory DT 노드와 다름!
* - Memory Reservation Block: FDT 바이너리 레벨, early boot에서 처리
* - reserved-memory 노드: DT 노드 레벨, memblock 서브시스템에서 처리
*/
/* ===== FDT 프로퍼티 인코딩 예시 =====
*
* DTS: compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
*
* Structure Block에 저장되는 바이너리:
* [FDT_PROP] ← 0x00000003
* [len = 39] ← 두 문자열 + NUL 포함 길이
* [nameoff = 0] ← Strings Block에서 "compatible" 오프셋
* "myvendor,my-soc-uart\0snps,dw-apb-uart\0" ← 실제 데이터
* [padding] ← 4-byte 정렬 맞춤
*
* DTS: reg = <0x1c28000 0x400>;
*
* [FDT_PROP]
* [len = 8] ← u32 × 2 = 8 bytes
* [nameoff = 11] ← Strings Block에서 "reg" 오프셋
* [0x01C28000] [0x00000400] ← big-endian u32 값들
*/
/* ===== 커널에서 FDT 직접 접근 (early boot) ===== */
#include <linux/of_fdt.h>
/* early_init_dt_scan(): 부팅 초기에 FDT에서 핵심 정보 추출 */
void __init early_init_dt_scan_nodes(void)
{
/* chosen 노드에서 bootargs, initrd 위치 추출 */
early_init_dt_scan_chosen(boot_command_line);
/* /memory 노드에서 물리 메모리 범위 추출 → memblock에 등록 */
early_init_dt_scan_memory();
/* root 노드에서 #address-cells, #size-cells 가져오기 */
early_init_dt_scan_root();
}
/* unflatten: FDT 바이너리 → struct device_node 트리 변환 */
void __init unflatten_device_tree(void)
{
/* 1차 패스: 필요한 메모리 크기 계산 */
/* 2차 패스: device_node + property 구조체 할당 및 연결 */
__unflatten_device_tree(initial_boot_params, NULL,
&of_root, early_init_dt_alloc_memory_arch, false);
/* of_root: 전역 루트 device_node 포인터 */
/* /proc/device-tree/와 /sys/firmware/devicetree/base/로 노출 */
}
struct device_node / struct property 내부 구조
unflatten_device_tree() 완료 후 커널 메모리에 존재하는 자료구조입니다. 모든 of_* API는 이 구조체를 통해 DT 정보에 접근합니다.
/* include/linux/of.h */
struct device_node {
const char *name; /* 노드 이름 (@ 앞 부분) */
phandle phandle; /* 고유 식별자 (phandle 프로퍼티 값) */
const char *full_name; /* 전체 경로명 또는 name[@unit-address] */
struct fwnode_handle fwnode; /* 펌웨어 노드 추상화 (DT/ACPI 통합) */
struct property *properties; /* 프로퍼티 연결 리스트 헤드 */
struct property *deadprops; /* 제거된 프로퍼티 (overlay undo용) */
/* 트리 탐색 포인터 */
struct device_node *parent; /* 부모 노드 */
struct device_node *child; /* 첫 번째 자식 */
struct device_node *sibling; /* 다음 형제 */
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; /* sysfs 표현 (/sys/firmware/devicetree/) */
#endif
unsigned long _flags; /* OF_POPULATED, OF_DETACHED 등 */
void *data; /* 드라이버 private 데이터 */
};
/* 플래그 상수 */
#define OF_DYNAMIC 1 /* overlay로 동적 생성된 노드 */
#define OF_DETACHED 2 /* 트리에서 분리된 노드 */
#define OF_POPULATED 3 /* platform_device가 이미 생성됨 */
#define OF_POPULATED_BUS 4 /* 자식 디바이스들도 생성됨 */
struct property {
char *name; /* 프로퍼티 이름 ("compatible", "reg" 등) */
int length; /* 값의 바이트 길이 */
void *value; /* 프로퍼티 값 (raw 바이트) */
struct property *next; /* 같은 노드의 다음 프로퍼티 */
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr; /* sysfs 바이너리 속성 */
#endif
};
/* ===== device_node 트리 순회 매크로 ===== */
/* 모든 자식 노드 순회 */
for_each_child_of_node(parent, child) { ... }
/* available(status != "disabled") 자식만 순회 */
for_each_available_child_of_node(parent, child) { ... }
/* 특정 compatible을 가진 노드만 순회 */
for_each_compatible_node(dn, type, compatible) { ... }
/* 특정 프로퍼티를 가진 노드 순회 */
for_each_node_with_property(dn, prop_name) { ... }
/* of_node 참조 카운팅 */
struct device_node *np = of_node_get(node); /* refcount++ */
of_node_put(np); /* refcount-- */
/* for_each_* 매크로는 루프 내에서 자동으로 get/put 처리
* 주의: break로 루프를 탈출하면 of_node_put()을 수동 호출해야 함! */
/* ===== 노드 → platform_device 변환 흐름 =====
*
* 1. unflatten_device_tree() → device_node 트리 구축
* 2. of_platform_default_populate()
* → 루트의 direct children 중 compatible 있는 노드를 platform_device로 생성
* → "simple-bus", "simple-mfd", "isa", "arm,amba-bus" compatible의 노드는
* 재귀적으로 자식도 platform_device로 생성
* 3. 각 platform_device의 compatible과 등록된 platform_driver의 of_match_table 비교
* 4. 매칭 성공 → driver->probe() 호출
* 5. probe 시 의존성(clk, regulator 등) 미충족이면 -EPROBE_DEFER 반환
* → 나중에 재시도 (deferred probe)
*/
주소 변환 (Address Translation) 상세
Device Tree에서 각 버스 레벨마다 독립적인 주소 공간을 가집니다. ranges 프로퍼티가 자식 주소 공간 → 부모 주소 공간으로의 변환 규칙을 정의합니다.
/* ===== ranges 프로퍼티 해석 규칙 =====
*
* ranges = < child_addr parent_addr length >;
*
* - child_addr의 셀 수 = 현재 노드의 #address-cells
* - parent_addr의 셀 수 = 부모 노드의 #address-cells
* - length의 셀 수 = 현재 노드의 #size-cells
* - 빈 ranges (ranges;) → 1:1 매핑 (주소 동일)
* - ranges 없음 → 자식 주소를 부모 주소로 변환 불가 (독립 주소 공간)
*/
/* 예시 1: 단순 SoC 버스 — 오프셋 변환 */
/ {
#address-cells = <2>; /* 루트: 64-bit 주소 */
#size-cells = <2>;
soc {
compatible = "simple-bus";
#address-cells = <1>; /* SoC: 32-bit 주소 */
#size-cells = <1>;
/* child(1 cell) parent(2 cells) size(1 cell)
* 0x0 → 0x0_0000_0000 1 GiB 범위 */
ranges = <0x0 0x0 0x0 0x40000000>;
/* serial의 reg 0x1c28000은:
* child_addr = 0x01c28000
* ranges 적용: 0x01c28000 + 0x0 = 0x0_01c28000 (CPU 물리 주소) */
serial@1c28000 {
reg = <0x1c28000 0x400>;
};
};
};
/* 예시 2: 다중 ranges — 여러 주소 윈도우 */
soc {
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x00000000 0x0 0x00000000 0x20000000>, /* 0~512M: 1:1 */
<0x40000000 0x0 0x40000000 0x20000000>; /* 1G~1.5G */
};
/* 예시 3: PCI 주소 공간 (#address-cells = <3>) */
pcie@10000000 {
compatible = "pci-host-ecam-generic";
/* PCI는 #address-cells = 3: (phys.hi phys.mid phys.lo)
* phys.hi 비트 구조:
* [31] = relocatable
* [30:29] = 프리페치 (01=I/O, 10=32-bit MEM, 11=64-bit MEM)
* [24] = prefetchable
* [23:16] = bus number
* [15:11] = device number
* [10:8] = function number
* [7:0] = register number */
#address-cells = <3>;
#size-cells = <2>;
/* PCI 주소(3 cells) → CPU 주소(2 cells), 크기(2 cells) */
ranges =
/* I/O 공간: PCI I/O 0x0 → CPU 0x1000_0000, 64KiB */
<0x01000000 0x0 0x00000000 0x0 0x10000000 0x0 0x00010000>,
/* 32-bit MEM: PCI MEM 0x2000_0000 → CPU 0x2000_0000, 256MiB */
<0x02000000 0x0 0x20000000 0x0 0x20000000 0x0 0x10000000>,
/* 64-bit MEM (prefetchable): PCI 0x8_0000_0000 → CPU 0x8_0000_0000, 4GiB */
<0x43000000 0x8 0x00000000 0x8 0x00000000 0x1 0x00000000>;
};
/* ===== 커널의 주소 변환 API ===== */
#include <linux/of_address.h>
/* of_translate_address(): DT 주소 → CPU 물리 주소 변환
* ranges 체인을 루트까지 재귀적으로 따라가며 변환 */
u64 cpu_addr = of_translate_address(np, addr_prop);
/* of_address_to_resource(): reg → struct resource 변환
* 내부적으로 of_translate_address() + 크기 정보 포함 */
struct resource res;
of_address_to_resource(np, 0, &res); /* 첫 번째 reg 엔트리 */
/* res.start = 변환된 CPU 물리 주소
* res.end = start + size - 1
* res.flags = IORESOURCE_MEM 또는 IORESOURCE_IO */
/* of_translate_dma_address(): DMA 주소 변환 (dma-ranges 사용) */
u64 dma_addr = of_translate_dma_address(np, addr_prop);
/* dma-ranges: DMA 엔진이 보는 주소 ≠ CPU 물리 주소일 때
* 예: GPU나 DMA 컨트롤러가 IOMMU 없이 다른 주소로 메모리 접근 */
soc {
/* DMA 주소 0x0 → CPU 물리 주소 0x8000_0000 */
dma-ranges = <0x0 0x0 0x80000000 0x80000000>;
};
인터럽트 도메인과 Nexus 노드 심화
Device Tree의 인터럽트 계층은 디바이스 트리 구조(부모-자식)와 독립적입니다. interrupt-parent가 인터럽트 도메인 트리를 형성하고, interrupt-map이 도메인 간 인터럽트 번호 변환을 수행합니다.
/* ===== 인터럽트 처리 핵심 개념 =====
*
* 1. interrupt-controller: 이 노드가 인터럽트 컨트롤러임을 선언 (빈 프로퍼티)
* 2. #interrupt-cells: 자식이 interrupts에 넣는 셀 수 (GIC=3, GPIO=2 등)
* 3. interrupt-parent: 인터럽트를 수신할 컨트롤러 (생략 시 DT 부모에서 상속)
* 4. interrupts: 인터럽트 지정자 (해석은 컨트롤러의 #interrupt-cells에 의존)
* 5. interrupt-map: 인터럽트 도메인 간 변환 (nexus 노드에서 사용)
*/
/* ===== GIC (ARM Generic Interrupt Controller) ===== */
gic: interrupt-controller@1c81000 {
compatible = "arm,gic-400";
interrupt-controller;
#interrupt-cells = <3>;
/* 셀 해석:
* [0] type: 0=SPI(Shared), 1=PPI(Private Per-Processor)
* [1] irq number: SPI=0~987, PPI=0~15 (GIC HW IRQ = SPI+32, PPI+16)
* [2] flags: 1=rising edge, 2=falling edge, 4=level high, 8=level low */
reg = <0x1c81000 0x1000>,
<0x1c82000 0x2000>;
};
/* ===== GPIO 인터럽트 컨트롤러 (계층적) ===== */
gpio0: gpio@1c20800 {
compatible = "myvendor,my-soc-gpio";
reg = <0x1c20800 0x40>;
/* GPIO 컨트롤러이면서 인터럽트 컨트롤러 */
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
/* 셀 해석: [0] GPIO 핀 번호, [1] 트리거 타입 (IRQ_TYPE_*) */
/* 이 GPIO 컨트롤러의 인터럽트가 GIC로 전달됨 */
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
};
/* GPIO 핀을 인터럽트로 사용하는 디바이스 */
button@0 {
compatible = "gpio-keys";
interrupt-parent = <&gpio0>; /* GIC가 아닌 GPIO 컨트롤러로! */
interrupts = <7 IRQ_TYPE_EDGE_FALLING>; /* GPIO 핀 7, 하강 에지 */
};
/* ===== interrupt-map (Nexus 노드) =====
*
* PCI, USB 등의 버스에서 디바이스 인터럽트를 부모 컨트롤러로 변환합니다.
* nexus 노드: interrupt-controller는 아니지만 interrupt-map으로 변환 수행
*/
pcie@10000000 {
/* PCI 인터럽트: INTA=1, INTB=2, INTC=3, INTD=4 */
#interrupt-cells = <1>;
/* interrupt-map-mask: 매칭에 사용할 비트 마스크
* PCI 주소(3 cells) + 인터럽트(1 cell) 총 4 cells
* device 번호(bit 15:11)와 인터럽트 번호만 매칭 */
interrupt-map-mask = <0xf800 0 0 7>;
/* interrupt-map: (child_unit_addr child_irq parent parent_irq)
* child_unit_addr: #address-cells 만큼의 셀 (AND mask 적용 후 비교)
* child_irq: #interrupt-cells 만큼의 셀
* parent: phandle → 부모 인터럽트 컨트롤러
* parent_irq: 부모의 #interrupt-cells 만큼의 셀 */
interrupt-map =
/* Device 0, INTA → GIC SPI 100 */
<0x0000 0 0 1 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
/* Device 0, INTB → GIC SPI 101 */
<0x0000 0 0 2 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
/* Device 0, INTC → GIC SPI 102 */
<0x0000 0 0 3 &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
/* Device 0, INTD → GIC SPI 103 */
<0x0000 0 0 4 &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
/* Device 1, INTA → GIC SPI 104 (rotation: INTB부터 시작) */
<0x0800 0 0 1 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
<0x0800 0 0 2 &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
<0x0800 0 0 3 &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
<0x0800 0 0 4 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;
/* PCI 인터럽트 회전(swizzle):
* IRQ = (device_slot + interrupt_pin - 1) % 4 + 1
* 이를 통해 여러 디바이스의 인터럽트가 4개 GIC IRQ에 분산 */
};
/* ===== interrupts-extended: 여러 컨트롤러의 인터럽트를 한 노드에서 사용 ===== */
my-device {
/* interrupt-parent + interrupts는 하나의 컨트롤러만 가능.
* interrupts-extended는 여러 컨트롤러의 인터럽트를 지정 가능 */
interrupts-extended =
<&gic GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>, /* GIC에서 오는 인터럽트 */
<&gpio0 7 IRQ_TYPE_EDGE_FALLING>; /* GPIO에서 오는 인터럽트 */
interrupt-names = "data-irq", "wakeup-irq";
};
/* ===== 커널 인터럽트 도메인 API (drivers/irqchip/) ===== */
#include <linux/irqdomain.h>
/* irq_domain: HW IRQ 번호 → Linux virq(가상 IRQ) 번호 매핑
* 각 인터럽트 컨트롤러가 자신의 도메인을 등록
* DT의 interrupts 값이 HW IRQ로, irq_domain을 통해 Linux IRQ로 변환 */
struct irq_domain *domain;
domain = irq_domain_add_linear(np, nr_irqs, &my_domain_ops, priv);
/* linear: HW IRQ → virq 직접 테이블 매핑 (소규모)
* hierarchy: 계층적 도메인 (GIC→GPIO 등 cascaded 구조) */
domain = irq_domain_create_hierarchy(parent_domain, 0, nr_irqs,
of_fwnode_handle(np), &my_domain_ops, priv);
/* hierarchy 도메인: 인터럽트 처리가 여러 컨트롤러를 거침
* button → GPIO IRQ 7 → GIC SPI 11 → CPU
* 각 단계의 도메인이 HW IRQ를 변환 */
Pinctrl 서브시스템과 DT 연동
SoC의 핀 다중화(muxing)와 전기적 설정을 DT에서 선언합니다. 드라이버의 probe() 시 자동으로 pinctrl-0이 적용됩니다.
/* ===== 핀 컨트롤러 노드 (SoC .dtsi) ===== */
pio: pinctrl@1c20800 {
compatible = "myvendor,my-soc-pinctrl";
reg = <0x1c20800 0x400>;
clocks = <&ccu CLK_APB1>;
/* UART0 핀 그룹 정의 */
uart0_pins: uart0-pins {
pins = "PA4", "PA5"; /* TX, RX */
function = "uart0"; /* 핀 기능 선택 (mux) */
drive-strength = <10>; /* mA 단위 출력 세기 */
bias-pull-up; /* 풀업 활성화 */
};
uart0_sleep_pins: uart0-sleep-pins {
pins = "PA4", "PA5";
function = "gpio_in"; /* sleep 시 GPIO 입력으로 */
bias-disable;
};
/* I2C0 핀 그룹 */
i2c0_pins: i2c0-pins {
pins = "PA11", "PA12"; /* SDA, SCL */
function = "i2c0";
drive-strength = <10>;
bias-pull-up;
};
/* SPI0 핀 그룹 + CS */
spi0_pins: spi0-pins {
pins = "PC0", "PC1", "PC2", "PC3"; /* CLK, MOSI, MISO, CS */
function = "spi0";
drive-strength = <10>;
};
/* GPIO 키 (외부 풀업, 내부 바이어스 없음) */
key_pins: key-pins {
pins = "PG7";
function = "gpio_in";
bias-disable;
};
};
/* ===== 디바이스에서 pinctrl 참조 ===== */
&uart0 {
pinctrl-names = "default", "sleep";
pinctrl-0 = <&uart0_pins>; /* "default" 상태 (probe 시 적용) */
pinctrl-1 = <&uart0_sleep_pins>; /* "sleep" 상태 (suspend 시 적용) */
status = "okay";
};
/* pinctrl-names와 pinctrl-N은 순서 대응:
* pinctrl-names[0] = "default" → pinctrl-0
* pinctrl-names[1] = "sleep" → pinctrl-1
*
* 커널 PM 시스템이 suspend/resume 시 자동으로 상태 전환:
* probe → "default", suspend → "sleep", resume → "default"
*
* "init" 상태: probe 중에만 사용, probe 완료 후 "default"로 전환 */
/* ===== 핀 설정 바인딩 주요 프로퍼티 (vendor-independent) ===== */
/*
* pins: 핀 이름 목록
* groups: 핀 그룹 이름 (대체)
* function: 핀 기능 (mux 선택)
* bias-disable: 바이어스 없음
* bias-pull-up: 내부 풀업 활성화
* bias-pull-down: 내부 풀다운 활성화
* drive-strength: 출력 드라이브 세기 (mA)
* input-enable: 입력 활성화
* output-high: 출력 High로 설정
* output-low: 출력 Low로 설정
* slew-rate: 슬루율 (0=slow, 1=fast)
*/
IOMMU와 DMA 관련 DT 프로퍼티
/* ===== IOMMU (I/O Memory Management Unit) DT 바인딩 ===== */
/* IOMMU 컨트롤러 노드 */
smmu: iommu@12c00000 {
compatible = "arm,smmu-v2";
reg = <0x12c00000 0x10000>;
#iommu-cells = <1>; /* 자식이 참조 시 stream ID 1개 */
interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
};
/* DMA를 수행하는 디바이스에서 IOMMU 참조 */
gpu@12000000 {
compatible = "vendor,my-gpu";
reg = <0x12000000 0x10000>;
iommus = <&smmu 0x100>; /* SMMU stream ID = 0x100 */
/* 커널이 자동으로 IOMMU 도메인을 설정하여 DMA 주소 변환 수행 */
};
ethernet@1c30000 {
compatible = "vendor,my-eth";
reg = <0x1c30000 0x10000>;
iommus = <&smmu 0x200>; /* 다른 stream ID */
};
/* ===== DMA 관련 프로퍼티 ===== */
my-device@1000 {
compatible = "vendor,my-dev";
/* dma-coherent: 하드웨어가 캐시 코히어런시 보장
* → 커널이 수동 캐시 flush/invalidate 생략 (성능 향상) */
dma-coherent;
/* dma-ranges가 부모에 있으면 DMA 주소 ≠ CPU 주소 */
/* DMA 컨트롤러 참조 (slave DMA 사용 시) */
dmas = <&dma_controller 5>, <&dma_controller 6>;
dma-names = "tx", "rx";
};
/* DMA 컨트롤러 노드 */
dma_controller: dma-controller@1c02000 {
compatible = "myvendor,my-soc-dma";
reg = <0x1c02000 0x1000>;
interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>;
#dma-cells = <1>; /* 자식 참조 시 채널 번호 1개 */
clocks = <&ccu CLK_DMA>;
resets = <&ccu RST_DMA>;
};
/* ===== 커널에서 DMA 채널 가져오기 ===== */
#include <linux/dmaengine.h>
struct dma_chan *tx_chan, *rx_chan;
tx_chan = dma_request_chan(dev, "tx"); /* dma-names의 "tx"에 대응하는 채널 */
rx_chan = dma_request_chan(dev, "rx"); /* dma-names의 "rx"에 대응하는 채널 */
/* restricted-dma-pool: 특정 디바이스용 DMA 메모리 제한 */
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
gpu_dma_pool: dma-pool@90000000 {
compatible = "restricted-dma-pool";
reg = <0x0 0x90000000 0x0 0x10000000>; /* 256MiB */
};
};
gpu@12000000 {
memory-region = <&gpu_dma_pool>; /* 이 디바이스의 DMA는 이 영역만 사용 */
};
Thermal-zones DT 바인딩
/* ===== SoC 온도 센서와 쿨링 제어를 DT에서 정의 ===== */
/* 온도 센서 노드 */
tsensor: thermal-sensor@1c25000 {
compatible = "myvendor,my-soc-thermal";
reg = <0x1c25000 0x400>;
#thermal-sensor-cells = <1>; /* 센서 인덱스 1개 (다중 존) */
clocks = <&ccu CLK_THS>;
resets = <&ccu RST_THS>;
};
/* 쿨링 디바이스: CPU freq 스로틀링 */
/* cpu 노드에 #cooling-cells = <2>; 추가 필요 */
&cpu0 {
#cooling-cells = <2>; /* min_state, max_state */
};
/* thermal-zones 노드 */
thermal-zones {
/* 각 zone = 센서 + 트립 포인트 + 쿨링 맵 */
cpu-thermal {
polling-delay-passive = <250>; /* 트립 후 폴링 주기 (ms) */
polling-delay = <1000>; /* 평상시 폴링 주기 (ms) */
thermal-sensors = <&tsensor 0>; /* 센서 0번 (CPU zone) */
trips {
/* 패시브 쿨링: CPU freq 스로틀 시작 */
cpu_alert: cpu-alert {
temperature = <75000>; /* 75°C (밀리도) */
hysteresis = <2000>; /* 73°C에서 해제 */
type = "passive";
};
/* 크리티컬: 시스템 셧다운 */
cpu_crit: cpu-critical {
temperature = <100000>; /* 100°C */
hysteresis = <0>;
type = "critical";
};
/* 핫: 능동 쿨링(팬) 시작 */
cpu_hot: cpu-hot {
temperature = <85000>; /* 85°C */
hysteresis = <5000>;
type = "hot";
};
};
cooling-maps {
/* 75°C 이상: CPU freq 스로틀 (state 0~최대) */
cpu-throttle {
trip = <&cpu_alert>;
cooling-device = <&cpu0
THERMAL_NO_LIMIT /* min state */
THERMAL_NO_LIMIT>; /* max state */
};
/* 85°C 이상: 팬 활성화 */
fan-cooling {
trip = <&cpu_hot>;
cooling-device = <&fan0 0 3>; /* 팬 레벨 0~3 */
};
};
};
gpu-thermal {
polling-delay-passive = <250>;
polling-delay = <1000>;
thermal-sensors = <&tsensor 1>; /* 센서 1번 (GPU zone) */
trips {
gpu_alert: gpu-alert {
temperature = <80000>;
hysteresis = <2000>;
type = "passive";
};
};
};
};
/* 팬 제어용 PWM 쿨링 디바이스 */
fan0: pwm-fan {
compatible = "pwm-fan";
pwms = <&pwm0 0 25000>; /* PWM 채널 0, 25kHz */
#cooling-cells = <2>;
cooling-levels = <0 64 128 255>; /* state 0~3의 PWM duty */
};
/* sysfs 확인 */
/* /sys/class/thermal/thermal_zone0/temp → 현재 온도 */
/* /sys/class/thermal/thermal_zone0/type → "cpu-thermal" */
/* /sys/class/thermal/thermal_zone0/trip_point_0_temp → 75000 */
/* /sys/class/thermal/cooling_device0/cur_state → 현재 쿨링 레벨 */
전력 도메인 (Power Domain) DT
/* ===== 전력 도메인: SoC 내 독립적으로 전원을 제어할 수 있는 영역 =====
*
* SoC 설계에서 GPU, DSP, ISP 등은 별도 전력 도메인에 배치되어
* 사용하지 않을 때 완전히 전원을 차단(power gating)할 수 있습니다.
* DT에서 이 관계를 선언하면 커널 PM 시스템이 자동 관리합니다.
*/
/* 전력 도메인 컨트롤러 (PMU, Power Management Unit) */
pmu: power-controller@1c20000 {
compatible = "myvendor,my-soc-power";
reg = <0x1c20000 0x100>;
#power-domain-cells = <1>; /* 도메인 인덱스 1개 */
/* 서브노드 형태도 가능 (일부 SoC) */
pd_gpu: power-domain@0 {
reg = <0>;
#power-domain-cells = <0>;
clocks = <&ccu CLK_GPU>;
resets = <&ccu RST_GPU>;
};
pd_dsp: power-domain@1 {
reg = <1>;
#power-domain-cells = <0>;
};
};
/* 디바이스에서 전력 도메인 참조 */
gpu@12000000 {
compatible = "vendor,my-gpu";
reg = <0x12000000 0x10000>;
/* 방법 1: 인덱스 기반 (#power-domain-cells = <1>) */
power-domains = <&pmu 0>; /* 도메인 0 = GPU */
/* 방법 2: 서브노드 phandle (#power-domain-cells = <0>) */
/* power-domains = <&pd_gpu>; */
power-domain-names = "gpu";
};
/* 여러 전력 도메인에 걸친 디바이스 */
isp@14000000 {
compatible = "vendor,my-isp";
reg = <0x14000000 0x10000>;
power-domains = <&pmu 2>, <&pmu 3>;
power-domain-names = "isp-core", "isp-io";
};
/* ===== 커널에서 전력 도메인 관리 =====
*
* Runtime PM과 연동:
* - pm_runtime_get_sync() → 전력 도메인 ON (참조 카운트 기반)
* - pm_runtime_put() → 전력 도메인 OFF (모든 사용자가 put하면)
*
* 커널 내부 흐름:
* 1. DT 파싱 → genpd(Generic Power Domain) 구조체 생성
* 2. pm_genpd_add_device() → 디바이스를 도메인에 연결
* 3. dev_pm_domain_attach() → probe 시 자동 호출
* 4. Runtime PM 콜백에서 genpd_power_on/off() 자동 호출
*
* 디버깅:
* $ cat /sys/kernel/debug/pm_genpd/pm_genpd_summary
* domain status /device runtime status
* gpu_pd on /12000000.gpu active
* dsp_pd off
*/
Device Tree vs ACPI 비교
| 항목 | Device Tree (DT) | ACPI |
|---|---|---|
| 기원 | Open Firmware (IEEE 1275), PowerPC/SPARC | Intel, x86 서버/데스크톱 |
| 주요 플랫폼 | ARM, RISC-V, PowerPC, MIPS | x86, ARM 서버 (SBSA) |
| 데이터 형식 | DTS(텍스트) → DTB(바이너리), 정적 데이터 | ASL(텍스트) → AML(바이코드), 실행 가능 메서드 포함 |
| 하드웨어 기술 | 선언적 (데이터만) | 선언적 + 절차적 (AML 메서드 실행 가능) |
| 런타임 수정 | Overlay (.dtbo, configfs) | 동적 테이블 로드 (SSDT), hotplug |
| 전원 관리 | DT 프로퍼티 + 커널 드라이버에서 직접 구현 | _PS0/_PS3 메서드, _PR0 등 펌웨어가 전원 제어 수행 |
| 인터럽트 | interrupts, interrupt-map | _CRS(Current Resource Settings) 내 IRQ 디스크립터 |
| 열 관리 | thermal-zones DT 노드 | _TMP, _PSV, _CRT, _ACx 메서드 |
| 디바이스 식별 | compatible 문자열 | _HID (Hardware ID), _CID (Compatible ID) |
| 리소스 기술 | reg, interrupts, clocks 등 개별 프로퍼티 | _CRS 버퍼에 Memory32/IRQ/DMA 리소스 패킹 |
| 커널 API | of_*() (DT 전용) | acpi_*() (ACPI 전용) |
| 통합 API | device_property_*() / fwnode_*() — DT/ACPI 양쪽 지원 | |
| 바인딩 문서 | Documentation/devicetree/bindings/ (YAML) | ACPI Spec + DSDT/SSDT (벤더 구현) |
| 검증 도구 | dt_binding_check, dtbs_check | iasl (Intel ASL Compiler), acpidump |
of_*() 대신 device_property_*() 또는 fwnode_property_*()를 사용합니다. 커널이 런타임에 DT/ACPI를 판별하여 적절한 백엔드를 호출합니다.
/* ===== fwnode API: DT/ACPI 통합 드라이버 패턴 ===== */
#include <linux/property.h>
static int my_unified_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
u32 val;
const char *str;
bool flag;
/* of_property_read_u32() 대신 → DT/ACPI 모두 동작 */
device_property_read_u32(dev, "fifo-depth", &val);
device_property_read_string(dev, "label", &str);
flag = device_property_read_bool(dev, "big-endian");
/* fwnode 기반 자식 순회 */
struct fwnode_handle *child;
device_for_each_child_node(dev, child) {
u32 reg;
fwnode_property_read_u32(child, "reg", ®);
}
return 0;
}
/* DT + ACPI 듀얼 매칭 테이블 */
static const struct of_device_id my_of_ids[] = {
{ .compatible = "vendor,my-dev" },
{ }
};
#ifdef CONFIG_ACPI
static const struct acpi_device_id my_acpi_ids[] = {
{ "VNDR0001", 0 }, /* _HID 매칭 */
{ }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
#endif
static struct platform_driver my_driver = {
.probe = my_unified_probe,
.driver = {
.name = "my-device",
.of_match_table = my_of_ids,
.acpi_match_table = ACPI_PTR(my_acpi_ids),
},
};
실제 SoC DTS 분석 (Raspberry Pi / Allwinner)
/* ===== 실제 커널 소스 DTS 구조 분석 =====
*
* 커널 소스 내 DTS 파일 위치:
* arch/arm64/boot/dts/broadcom/ ← Raspberry Pi 4/5
* arch/arm64/boot/dts/allwinner/ ← Allwinner (Pine64, OrangePi)
* arch/arm64/boot/dts/rockchip/ ← Rockchip (Rock5B)
* arch/arm64/boot/dts/amlogic/ ← Amlogic (Odroid)
* arch/arm64/boot/dts/freescale/ ← NXP i.MX
* arch/arm64/boot/dts/qcom/ ← Qualcomm
*
* 일반적인 .dtsi/.dts 계층 구조:
* SoC계열.dtsi ← SoC 공통 (예: sun50i-h5.dtsi)
* └── SoC.dtsi ← 특정 SoC (예: sun50i-h5.dtsi → sun50i-a64.dtsi 포함)
* └── Board.dts ← 보드별 (예: sun50i-h5-orangepi-pc2.dts)
*/
/* ===== Raspberry Pi 4B (BCM2711) DTS 구조 분석 ===== */
/*
* arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dts
* └── #include "bcm2711.dtsi"
* └── #include "bcm283x.dtsi" ← BCM SoC 공통
*
* BCM2711 특징:
* - VideoCore GPU가 주소 공간을 관리 (VC 주소 ≠ ARM 주소)
* - dma-ranges로 VC↔ARM 주소 변환
* - 독자적인 인터럽트 컨트롤러 (GIC-400)
*/
/* bcm283x.dtsi 핵심 구조 (간략화) */
/ {
compatible = "brcm,bcm2835";
#address-cells = <1>;
#size-cells = <1>;
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
/* ARM 주소 0x7E000000이 버스 주소 0xFE000000으로 매핑 (BCM2711) */
ranges = <0x7e000000 0xfe000000 0x01800000>;
/* DMA 엔진은 레거시 주소를 사용 */
dma-ranges = <0xc0000000 0x00000000 0x40000000>;
gpio: gpio@7e200000 {
compatible = "brcm,bcm2711-gpio";
reg = <0x7e200000 0xb4>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-ranges = <&gpio 0 0 58>; /* pinctrl 연동 */
};
uart0: serial@7e201000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x7e201000 0x200>;
clocks = <&clocks BCM2835_CLOCK_UART>,
<&clocks BCM2835_CLOCK_VPU>;
clock-names = "uartclk", "apb_pclk";
arm,primecell-periphid = <0x00241011>;
status = "disabled";
};
};
};
/* bcm2711-rpi-4-b.dts에서 오버라이드 */
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_gpio14>;
status = "okay";
};
/* ===== Allwinner H6 (Pine H64) DTS 구조 분석 ===== */
/*
* arch/arm64/boot/dts/allwinner/sun50i-h6-pine-h64.dts
* └── #include "sun50i-h6.dtsi"
*
* Allwinner 특징:
* - CCU(Clock Control Unit) 드라이버가 클럭 + 리셋 모두 관리
* - R_ 접두사 노드: Always-On 도메인 (대기 전력)
* - MBUS: 메모리 버스 대역폭 제어
*/
/* sun50i-h6.dtsi 핵심 구조 (간략화) */
/ {
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a53";
device_type = "cpu";
reg = <0>;
enable-method = "psci";
clocks = <&ccu CLK_CPUX>;
operating-points-v2 = <&cpu_opp_table>;
#cooling-cells = <2>;
};
};
/* PSCI: ARM 표준 CPU 전원 관리 인터페이스 */
psci {
compatible = "arm,psci-1.0";
method = "smc"; /* Secure Monitor Call */
};
soc@3000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x03000000 0x1000000>;
/* CCU: 클럭 + 리셋 통합 컨트롤러 */
ccu: clock@3001000 {
compatible = "allwinner,sun50i-h6-ccu";
reg = <0x01000 0x1000>;
clocks = <&osc24M>, <&rtc 0>, <&rtc 2>;
clock-names = "hosc", "losc", "iosc";
#clock-cells = <1>;
#reset-cells = <1>;
};
/* EMAC (이더넷) — 완전한 DT 바인딩 예 */
emac: ethernet@5020000 {
compatible = "allwinner,sun50i-h6-emac",
"allwinner,sun50i-a64-emac";
reg = <0x5020000 0x10000>;
interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "macirq";
clocks = <&ccu CLK_BUS_EMAC>;
clock-names = "stmmaceth";
resets = <&ccu RST_BUS_EMAC>;
reset-names = "stmmaceth";
syscon = <&syscon>;
status = "disabled";
mdio: mdio {
compatible = "snps,dwmac-mdio";
#address-cells = <1>;
#size-cells = <0>;
};
};
};
};
/* 보드 .dts에서 활성화 + PHY 추가 */
&emac {
pinctrl-names = "default";
pinctrl-0 = <&ext_rgmii_pins>;
phy-mode = "rgmii-id";
phy-handle = <&ext_rgmii_phy>;
phy-supply = <®_gmac_3v3>;
status = "okay";
};
&mdio {
ext_rgmii_phy: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <1>; /* PHY 주소 */
reset-gpios = <&pio 3 14 GPIO_ACTIVE_LOW>;
reset-assert-us = <15000>;
reset-deassert-us = <40000>;
};
};
arch/arm64/boot/dts/ 디렉토리에서 실제 SoC의 .dtsi 파일을 읽어보면 DT 구조를 빠르게 이해할 수 있습니다. 특히 compatible 문자열로 커널에서 대응하는 드라이버(drivers/)를 검색하면 DT↔드라이버 연결 관계를 파악할 수 있습니다: git grep "allwinner,sun50i-h6-emac" drivers/
일반적인 실수와 올바른 패턴
Device Tree 작성 시 초보자가 자주 범하는 실수와 올바른 접근 방법을 비교합니다.
❌ 실수 1: compatible 순서 잘못
/* 잘못된 예: 일반적인 것을 먼저 나열 */
compatible = "generic-sensor", "myvendor,mysensor-v2";
/* 올바른 예: 구체적 → 일반적 순서 (드라이버 매칭 우선순위) */
compatible = "myvendor,mysensor-v2", "myvendor,mysensor", "generic-sensor";
❌ 실수 2: #address-cells/#size-cells 불일치
/* 잘못된 예: 부모의 #address-cells와 reg 크기 불일치 */
soc {
#address-cells = <2>; /* 주소 2개 u32 필요 */
#size-cells = <1>;
uart@10000 {
reg = <0x10000 0x100>; /* ❌ 주소가 1개 u32만 사용 */
};
};
/* 올바른 예 */
soc {
#address-cells = <2>;
#size-cells = <1>;
uart@10000 {
reg = <0x0 0x10000 0x100>; /* ✓ (주소_상위, 주소_하위, 크기) */
};
};
❌ 실수 3: phandle 참조 오류
/* 잘못된 예: 레이블 없이 참조 시도 */
gpio-controller@1000 {
compatible = "myvendor,gpio";
gpio-controller;
#gpio-cells = <2>;
};
led {
gpios = <&gpio 5 0>; /* ❌ &gpio 레이블이 정의되지 않음 */
};
/* 올바른 예: 레이블 정의 후 참조 */
gpio: gpio-controller@1000 { /* 레이블 정의 */
compatible = "myvendor,gpio";
gpio-controller;
#gpio-cells = <2>;
};
led {
gpios = <&gpio 5 0>; /* ✓ 레이블로 참조 */
};
❌ 실수 4: status 프로퍼티 누락
/* 잘못된 예: .dtsi에서 status 미지정 → 드라이버가 의도치 않게 바인딩됨 */
/* my-soc.dtsi */
uart0: serial@10000 {
compatible = "myvendor,uart";
reg = <0x10000 0x100>;
/* status 없음 → 모든 보드에서 활성화됨 */
};
/* 올바른 예: .dtsi에서는 disabled, 보드 .dts에서 선택적 활성화 */
/* my-soc.dtsi */
uart0: serial@10000 {
compatible = "myvendor,uart";
reg = <0x10000 0x100>;
status = "disabled"; /* 기본값: 비활성 */
};
/* myboard.dts */
&uart0 {
status = "okay"; /* 이 보드에서만 활성화 */
};
❌ 실수 5: interrupt 지정자 잘못된 개수
/* 잘못된 예: 인터럽트 컨트롤러의 #interrupt-cells 무시 */
gic: interrupt-controller@8000000 {
compatible = "arm,gic-400";
#interrupt-cells = <3>; /* (type, number, flags) 필요 */
interrupt-controller;
};
uart@10000 {
interrupts = <42 4>; /* ❌ 2개만 지정 (3개 필요) */
};
/* 올바른 예 */
uart@10000 {
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>; /* ✓ 3개 지정 */
/* = <0 42 4> (GIC_SPI=0, 인터럽트 번호 42, flags=4) */
};
❌ 실수 6: unit-address와 reg 불일치
/* 잘못된 예: 노드명의 @주소와 reg 값이 다름 */
serial@10000 {
reg = <0x20000 0x100>; /* ❌ @10000과 불일치 */
};
/* 올바른 예 */
serial@20000 {
reg = <0x20000 0x100>; /* ✓ 일치 */
};
✅ 모범 사례 체크리스트
| 항목 | 설명 | 검증 방법 |
|---|---|---|
| dtc 경고 제거 | 컴파일 시 모든 warning 해결 | dtc -W all -O dtb foo.dts |
| dt-schema 검증 | YAML 바인딩 스키마 통과 | make dt_binding_check DT_SCHEMA_FILES= |
| 주소 정렬 | reg 주소는 하드웨어 정렬 요구사항 준수 | 데이터시트 확인 |
| 클럭 순서 | clock-names 순서와 clocks phandle 순서 일치 | 바인딩 문서 참조 |
| GPIO active 레벨 | GPIO_ACTIVE_LOW/HIGH 정확히 지정 | 회로도 확인 |
| ranges 1:1 매핑 | 주소 변환 없으면 ranges; (빈 값) 사용 | 불필요한 매핑 제거 |
성능 최적화 가이드
Device Tree는 부팅 성능과 런타임 메모리 사용에 영향을 줍니다. 최적화 기법을 소개합니다.
DTB 크기 최적화
# DTB 크기 확인
ls -lh arch/arm64/boot/dts/myvendor/myboard.dtb
# 노드/프로퍼티 통계 (dtc 디컴파일 후 분석)
dtc -I dtb -O dts myboard.dtb | grep -c '^\s*[a-z].*{' # 노드 개수
dtc -I dtb -O dts myboard.dtb | wc -l # 전체 줄 수
# 압축률 확인 (대부분 부트로더는 gzip 압축 DTB 지원)
gzip -c myboard.dtb | wc -c
- 불필요한 노드 제거 — 사용하지 않는 하드웨어는 .dtsi에서
status="disabled"로 유지하고 .dts에서 활성화하지 않음 - 중복 프로퍼티 정리 — 같은 값이 반복되면 .dtsi 공통 부분으로 이동
- 긴 문자열 축약 — description 같은 문서화 프로퍼티는 커널이 사용하지 않으므로 제거 가능
파싱 시간 최적화
/* ===== unflatten 성능 측정 ===== */
// 커널 부팅 로그에서 확인
dmesg | grep "Unflattening device tree"
// [ 0.123456] Unflattening device tree took 5234us
/* ===== of_platform_populate() 성능 측정 ===== */
dmesg | grep "of_platform_populate"
성능 저하 원인:
- 너무 많은 노드 — 1000개 이상의 노드는 unflatten 시간이 10ms 이상 소요될 수 있음
- 깊은 중첩 — 노드 depth가 10단계 이상이면 재귀 탐색 비용 증가
- 큰 바이너리 프로퍼티 — 펌웨어 이미지 같은 큰 데이터는 별도 파일로 분리 권장
런타임 메모리 사용 최적화
# struct device_node 메모리 사용량 추정
# (노드 개수 × sizeof(device_node) + 프로퍼티 메모리)
cat /proc/meminfo | grep DeviceTree
# DeviceTree: 512 kB
# /proc/device-tree/ procfs 오버헤드 비활성화 (선택)
# CONFIG_PROC_DEVICETREE=n 설정 시 메모리 절약 (디버깅 불편)
of_find_node 캐싱 패턴
/* ❌ 비효율적: 반복 탐색 */
static int my_function(void) {
struct device_node *np;
for (int i = 0; i < 100; i++) {
np = of_find_node_by_path("/soc/i2c@1000");
/* 매번 트리 탐색 발생 */
of_node_put(np);
}
}
/* ✅ 효율적: 한 번만 탐색 후 캐싱 */
struct my_driver_data {
struct device_node *i2c_node;
};
static int my_probe(struct platform_device *pdev) {
struct my_driver_data *data = dev_get_drvdata(&pdev->dev);
data->i2c_node = of_find_node_by_path("/soc/i2c@1000");
/* probe 시 한 번만 탐색 */
}
static int my_remove(struct platform_device *pdev) {
struct my_driver_data *data = dev_get_drvdata(&pdev->dev);
of_node_put(data->i2c_node); /* 참조 카운트 해제 */
}
실전 케이스 스터디
실제 하드웨어를 위한 DTS 작성부터 드라이버 연동까지 단계별로 따라해봅니다.
케이스 1: I2C 온도 센서 추가 (TMP102)
시나리오: TMP102 I2C 온도 센서를 I2C 버스 1, 주소 0x48에 연결한 경우
1단계: 하드웨어 정보 수집
# 데이터시트에서 확인할 정보:
# - I2C 주소: 0x48 (ADD0=GND)
# - 인터럽트 핀: ALERT (옵션)
# - 전원: VCC 1.4V-3.6V
2단계: 커널 드라이버 확인
# drivers/hwmon/lm75.c가 TMP102 지원 (compatible 확인)
git grep -n "ti,tmp102" drivers/hwmon/
# drivers/hwmon/lm75.c:123: { .compatible = "ti,tmp102" },
# 바인딩 문서 확인
cat Documentation/devicetree/bindings/hwmon/lm75.txt
3단계: DTS 작성
/* myboard.dts */
&i2c1 {
status = "okay";
clock-frequency = <100000>; /* 100kHz */
tmp102: temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
/* 옵션: 인터럽트 사용 시 */
interrupt-parent = <&gpio1>;
interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
/* 옵션: 전원 레귤레이터 연결 */
vcc-supply = <®_3v3>;
};
};
4단계: 컴파일 및 검증
# DTS 컴파일
make dtbs
# 보드에 DTB 배포 후 부팅
# dmesg에서 드라이버 로딩 확인
dmesg | grep lm75
# [ 2.345678] lm75 1-0048: hwmon0: sensor 'tmp102'
# sysfs에서 온도 읽기
cat /sys/class/hwmon/hwmon0/temp1_input
# 25000 (섭씨 25도)
# device tree 노드 확인
ls -l /sys/firmware/devicetree/base/soc/i2c@*/temperature-sensor@48/
케이스 2: GPIO LED 추가
시나리오: GPIO5 핀에 연결된 LED (Active Low)
/* myboard.dts */
/ {
leds {
compatible = "gpio-leds";
status_led: led-status {
label = "status";
gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
default-state = "on";
linux,default-trigger = "heartbeat";
};
disk_led: led-disk {
label = "disk";
gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "disk-activity";
};
};
};
/* 검증 */
# LED 수동 제어
echo 0 > /sys/class/leds/status/brightness # OFF
echo 255 > /sys/class/leds/status/brightness # ON (최대)
echo timer > /sys/class/leds/status/trigger
echo 500 > /sys/class/leds/status/delay_on # 500ms ON
echo 500 > /sys/class/leds/status/delay_off # 500ms OFF
케이스 3: SPI LCD 디스플레이 (ILI9341)
시나리오: SPI0에 연결된 2.4" TFT LCD (320x240, ILI9341 컨트롤러)
/* myboard.dts */
&spi0 {
status = "okay";
display: display@0 {
compatible = "ilitek,ili9341";
reg = <0>; /* CS0 */
spi-max-frequency = <32000000>; /* 32MHz */
/* DC (Data/Command), Reset, LED backlight GPIO */
dc-gpios = <&gpio0 24 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
led-gpios = <&gpio0 18 GPIO_ACTIVE_HIGH>;
rotation = <90>; /* 화면 회전 */
bgr; /* BGR 픽셀 순서 (RGB 아님) */
/* 디스플레이 해상도 */
width = <320>;
height = <240>;
buswidth = <8>;
fps = <30>;
};
};
/* 검증 */
# fbdev 드라이버 로딩 확인
dmesg | grep fb
# [ 3.456789] fb0: ili9341 frame buffer, 320x240, 150 KiB video memory
# 프레임버퍼 테스트 (흰색 화면)
cat /dev/zero > /dev/fb0
케이스 4: Pinctrl 설정 (UART + 흐름 제어)
시나리오: UART0를 RTS/CTS 하드웨어 흐름 제어와 함께 사용
/* my-soc.dtsi — 핀 컨트롤러 정의 */
pinctrl: pinctrl@1000 {
compatible = "myvendor,pinctrl";
reg = <0x1000 0x100>;
uart0_default: uart0-default-state {
tx-rx {
pins = "gpio14", "gpio15";
function = "uart0";
bias-disable;
};
};
uart0_rts_cts: uart0-rts-cts-state {
tx-rx {
pins = "gpio14", "gpio15";
function = "uart0";
};
rts-cts {
pins = "gpio16", "gpio17";
function = "uart0";
bias-pull-up;
};
};
};
/* myboard.dts — 보드별 활성화 */
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_rts_cts>; /* RTS/CTS 사용 */
uart-has-rtscts;
status = "okay";
};
문제 해결 FAQ
Device Tree 관련 자주 발생하는 문제와 해결 방법입니다.
Q1: 드라이버가 probe되지 않습니다
증상: dmesg에 드라이버 로딩 메시지가 없고, /sys/bus/platform/drivers/에 디바이스가 바인딩되지 않음
체크리스트:
- compatible 문자열 확인
# 드라이버의 of_device_id 확인 git grep -A3 "of_device_id.*my_device" drivers/ # DTS의 compatible과 정확히 일치해야 함 - status 프로퍼티 확인
cat /proc/device-tree/soc/mydevice@*/status # "okay" 또는 "ok"여야 함 (disabled면 probe 안 됨) - 드라이버 모듈 로딩 확인
lsmod | grep my_driver modprobe my_driver # 수동 로딩 시도 - 디바이스 등록 확인
ls /sys/bus/platform/devices/ | grep mydevice # 노드가 platform_device로 등록되었는지 확인 - probe 실패 로그 확인
dmesg | grep -i "mydevice\|probe\|fail" # EPROBE_DEFER, 리소스 부족, 의존성 문제 등 확인
Q2: EPROBE_DEFER가 계속 발생합니다
증상: dmesg | grep defer에서 같은 디바이스가 반복적으로 defer됨
[ 5.123456] my_device 10000.mydev: probe deferred
[ 6.234567] my_device 10000.mydev: probe deferred
# ... 계속 반복
원인: 의존하는 리소스(클럭, 레귤레이터, GPIO 등)의 드라이버가 로딩되지 않음
해결:
# 1. 의존성 확인 (DTS에서 phandle 참조 추적)
cat /proc/device-tree/soc/mydevice@*/clocks # 바이너리 출력
hexdump -C /proc/device-tree/soc/mydevice@*/clocks
# 2. 클럭/레귤레이터 드라이버 로딩 확인
ls /sys/class/clk/
ls /sys/class/regulator/
# 3. 드라이버 로딩 순서 조정 (Makefile의 obj-y 순서 또는 initcall 우선순위)
# 또는 의존 드라이버를 built-in으로 변경 (=y)
Q3: 인터럽트가 동작하지 않습니다
디버깅 단계:
# 1. 인터럽트 등록 확인
cat /proc/interrupts | grep mydevice
# 출력 없으면 request_irq() 실패
# 2. 인터럽트 번호 확인
# DTS의 interrupts 프로퍼티와 드라이버에서 받은 IRQ 번호 비교
dmesg | grep "IRQ.*mydevice"
# 3. 인터럽트 컨트롤러 확인
cat /proc/device-tree/soc/mydevice@*/interrupt-parent
# phandle 값 확인 후, 해당 노드 찾기
# 4. #interrupt-cells 확인
cat /proc/device-tree/interrupt-controller@*/\#interrupt-cells
# DTS의 interrupts 지정자 개수와 일치해야 함
# 5. 하드웨어 트리거 타입 확인 (실제 HW 동작과 일치해야 함)
# IRQ_TYPE_EDGE_RISING/FALLING/LEVEL_HIGH/LEVEL_LOW
Q4: 디바이스에 접근하면 커널 패닉이 발생합니다
증상: Unable to handle kernel paging request at virtual address ...
원인: 잘못된 reg 주소 또는 주소 변환 오류
# 1. ioremap된 주소 확인
dmesg | grep ioremap
cat /proc/iomem | grep mydevice
# 2. DTS의 reg 주소가 데이터시트와 일치하는지 확인
dtc -I dtb -O dts /boot/myboard.dtb | grep -A2 "mydevice@"
# 3. ranges 프로퍼티 검증 (부모 버스의 주소 변환)
# reg 주소가 CPU 물리 주소인지, 버스 주소인지 확인
# 4. 클럭/전원 활성화 확인
# 클럭이 꺼져있으면 레지스터 접근 시 버스 에러 발생 가능
Q5: Device Tree Overlay가 적용되지 않습니다
검증:
# ConfigFS를 통한 overlay 적용
mount -t configfs none /sys/kernel/config
mkdir /sys/kernel/config/device-tree/overlays/my_overlay
cat my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo
# 적용 상태 확인
cat /sys/kernel/config/device-tree/overlays/my_overlay/status
# "applied" 출력되어야 함
# 에러 발생 시
dmesg | tail -20
# OF: overlay: apply failed 'xxx'
# → fragment target 노드가 존재하지 않거나 phandle 불일치
# overlay fragment target 확인
dtc -I dtb -O dts my_overlay.dtbo | grep "target ="
Q6: "clock not found" 에러가 발생합니다
# 증상
dmesg | grep "clock"
# [ 2.345678] mydevice: failed to get clock 'apb': -2 (ENOENT)
# 해결 1: clock-names 확인
# DTS의 clock-names와 드라이버의 clk_get() 이름이 일치해야 함
# DTS:
clocks = <&ccu CLK_APB>, <&ccu CLK_MOD>;
clock-names = "apb", "mod"; /* 순서 일치 필수 */
# 드라이버:
clk = devm_clk_get(&pdev->dev, "apb"); /* "apb" 일치 */
# 해결 2: 클럭 프로바이더 드라이버 로딩 확인
ls /sys/kernel/debug/clk/ # (CONFIG_DEBUG_FS 필요)
/sys/firmware/devicetree/base/— 런타임 DT 트리 탐색/sys/devices/platform/— platform_device 목록/proc/device-tree/— 심볼릭 링크 (레거시, /sys/firmware/devicetree/base/ 권장)dtc -I fs /proc/device-tree/— 런타임 DT를 DTS로 재구성scripts/dtc/dt-validate— YAML 스키마 검증 도구
Device Tree 검증 플레이북
Device Tree 문제는 "문법은 맞는데 런타임에서 바인딩 실패"하는 경우가 많습니다. 따라서 정적 검증(dtbs_check)과 런타임 검증(/sys/firmware/devicetree/base)을 모두 수행해야 합니다.
- 문법 검증: dtc 경고/오류 제거
- 스키마 검증: 바인딩 YAML과 속성 일치 확인
- 런타임 트리 확인: 실제 로드된 노드/프로퍼티 확인
- 드라이버 바인딩 확인: compatible 매칭과 probe 로그 확인
# 정적 검증
make ARCH=arm64 dtbs
make ARCH=arm64 dtbs_check
# DTB 디컴파일로 결과 확인
dtc -I dtb -O dts -o out.dts arch/arm64/boot/dts/vendor/board.dtb
# 런타임 트리 확인
ls /sys/firmware/devicetree/base
grep -R "my,device" /sys/firmware/devicetree/base 2>/dev/null
# 드라이버 바인딩 확인
dmesg | grep -Ei "of:|probe|mydevice"
| 증상 | 원인 후보 | 대응 |
|---|---|---|
| probe가 호출되지 않음 | compatible 문자열 불일치 | 드라이버 of_match_table와 DTS 비교 |
| irq/clock not found | phandle, 이름, 순서 불일치 | interrupts, clocks, *-names 동시 확인 |
| overlay 적용 실패 | fragment target 누락 | target 경로/phandle 재검증, dmesg 원문 확인 |
관련 문서
Device Tree와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.