Device Tree
DTS/DTB/FDT 구조, 바인딩, OF API, 오버레이(Overlay), 주소 변환(Address Translation), 인터럽트(Interrupt) 매핑(Mapping)까지 Linux 커널 Device Tree 종합 가이드.
핵심 요약
- 단계 분리 — 펌웨어(Firmware), 부트로더(Bootloader), 커널 초기화 경계를 구분합니다.
- 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
- 신뢰 체인(Chain of Trust) — 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 Controller)의 #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 디버깅(Debugging)
# ===== 실행 중인 시스템에서 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는 이 구조체(Struct)를 통해 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에서 각 버스(Bus) 레벨마다 독립적인 주소 공간(Address Space)을 가집니다. 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의 인터럽트 계층은 디바이스 트리(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 |
| 전원 관리(Power Management) | DT 프로퍼티 + 커널 드라이버에서 직접 구현 | _PS0/_PS3 메서드, _PR0 등 펌웨어가 전원 제어 수행 |
| 인터럽트 | interrupts, interrupt-map | _CRS(Current Resource Settings) 내 IRQ 디스크립터 |
| 열 관리(Thermal Management) | thermal-zones DT 노드 | _TMP, _PSV, _CRT, _ACx 메서드 |
| 디바이스 식별 | compatible 문자열 | _HID (Hardware ID), _CID (Compatible ID) |
| 리소스 기술 | reg, interrupts, clocks 등 개별 프로퍼티 | _CRS 버퍼(Buffer)에 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 + 흐름 제어(Flow Control))
시나리오: 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: 디바이스에 접근하면 커널 패닉(Kernel Panic)이 발생합니다
증상: 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 원문 확인 |
FDT 부팅 로드 흐름
펌웨어/부트로더가 DTB를 메모리에 로드하고, 커널이 이를 파싱하여 디바이스 트리를 구축하는 전체 과정을 살펴봅니다. 이 흐름을 이해하면 부팅 실패 시 어느 단계에서 문제가 발생했는지 빠르게 진단할 수 있습니다.
struct device_node 트리를 구축합니다.
/* ===== FDT 헤더 검증 (drivers/of/fdt.c) ===== */
int __init early_init_dt_verify(void *params)
{
/* 1. magic number 확인 */
if (fdt_check_header(params))
return false;
/* 2. 전역 FDT 포인터 설정 */
initial_boot_params = params;
/* 3. CRC32 계산 (나중에 검증용) */
of_fdt_crc32 = crc32_be(0, initial_boot_params,
fdt_totalsize(initial_boot_params));
return true;
}
/* ===== early_init_dt_scan_chosen(): bootargs 추출 ===== */
int __init early_init_dt_scan_chosen(char *cmdline)
{
int l;
const char *p;
const void *rng_seed;
unsigned long node = fdt_path_offset(
initial_boot_params, "/chosen");
if (node < 0)
node = fdt_path_offset(initial_boot_params, "/chosen@0");
if (node < 0)
return -ENOENT;
/* bootargs 프로퍼티 → boot_command_line 복사 */
p = of_fdt_get_property(initial_boot_params, node, "bootargs", &l);
if (p != NULL && l > 0)
strscpy(cmdline, p, min(l, COMMAND_LINE_SIZE));
/* initrd 위치: linux,initrd-start / linux,initrd-end */
early_init_dt_check_for_initrd(node);
/* rng-seed: 하드웨어 RNG 시드 (보안상 읽은 후 메모리에서 제거) */
rng_seed = of_fdt_get_property(initial_boot_params, node,
"rng-seed", &l);
if (rng_seed && l > 0) {
add_bootloader_randomness(rng_seed, l);
fdt_nop_property(initial_boot_params, node, "rng-seed");
}
return 0;
}
/* ===== unflatten_device_tree() 2-pass 알고리즘 상세 ===== */
static void *__unflatten_device_tree(
const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
int size;
void *mem;
/* Pass 1: NULL 메모리로 호출 → 필요한 총 크기만 계산 */
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
if (size <= 0)
return NULL;
/* 4-byte 정렬 + __alignof__(struct device_node) 보장 */
size = ALIGN(size, 4);
/* 메모리 할당 (early boot: memblock, 이후: kmalloc) */
mem = dt_alloc(size + 4, __alignof__(struct device_node));
if (!mem)
return NULL;
memset(mem, 0, size);
/* 끝에 sentinel 마커 (디버깅용) */
*((u32 *)(mem + size)) = 0xDEADBEEF;
/* Pass 2: 실제 device_node + property 구조체 생성 */
unflatten_dt_nodes(blob, mem, dad, mynodes);
/* sentinel 검증 */
pr_debug("unflattening %p...done\n", mem);
if (*((u32 *)(mem + size)) != 0xDEADBEEF)
pr_warn("End of tree marker overwritten\n");
return mem;
}
/* unflatten 완료 후 호출 순서:
* 1. of_alias_scan() — /aliases 노드 파싱, of_aliases 전역 설정
* 2. unittest_unflatten_overlay_base() — CONFIG_OF_UNITTEST 시
* 3. of_core_init() — /sys/firmware/devicetree/base/ sysfs 생성
* 4. of_platform_default_populate_init() — platform_device 생성 시작
*/
OF API 실전 코드 예제
커널 드라이버에서 Device Tree 정보에 접근하는 of_* API의 실전 패턴입니다. 단순한 프로토타입 나열이 아니라, 에러 처리와 리소스 관리를 포함한 실용적인 코드를 제시합니다.
of_find_*로 얻은 device_node는 반드시 of_node_put()으로 해제해야 합니다. for_each_* 매크로(Macro)에서 중간에 break하는 경우에도 마찬가지입니다. dev->of_node는 드라이버 프레임워크가 관리하므로 별도 해제 불필요합니다.
/* ===== 1. of_find_node_by_name — 이름으로 노드 검색 ===== */
struct device_node *np;
/* 전체 트리에서 "memory" 이름의 노드 검색 (첫 번째 매치) */
np = of_find_node_by_name(NULL, "memory");
if (!np) {
pr_err("memory node not found\n");
return -ENODEV;
}
pr_info("found: %pOF\n", np); /* %pOF = full path 출력 */
of_node_put(np); /* 반드시 refcount 해제 */
/* 동일 이름 노드가 여러 개인 경우 순회 */
np = NULL;
while ((np = of_find_node_by_name(np, "memory")) != NULL) {
/* 각 memory 노드 처리 */
pr_info("memory node: %pOF\n", np);
/* of_find_node_by_name이 이전 np를 자동 put하고 다음을 get */
}
/* ===== 2. of_property_read_u32 — 정수 프로퍼티 읽기 (에러 처리 포함) ===== */
static int my_parse_dt(struct device *dev)
{
struct device_node *np = dev->of_node;
u32 fifo_depth, bus_width;
int ret;
/* 필수 프로퍼티 — 없으면 probe 실패 */
ret = of_property_read_u32(np, "fifo-depth", &fifo_depth);
if (ret) {
dev_err(dev, "missing required 'fifo-depth' property\n");
return ret; /* -EINVAL 또는 -ENODATA */
}
/* 선택적 프로퍼티 — 없으면 기본값 사용 */
ret = of_property_read_u32(np, "bus-width", &bus_width);
if (ret)
bus_width = 32; /* 기본값 */
/* 배열 프로퍼티: 먼저 크기 확인 후 읽기 */
int count = of_property_count_u32_elems(np, "voltage-ranges");
if (count > 0 && count % 2 == 0) {
u32 *ranges = devm_kcalloc(dev, count, sizeof(u32), GFP_KERNEL);
if (!ranges)
return -ENOMEM;
of_property_read_u32_array(np, "voltage-ranges", ranges, count);
}
/* boolean 프로퍼티 — 존재 여부만 확인 */
bool big_endian = of_property_read_bool(np, "big-endian");
dev_info(dev, "fifo=%u bus=%u be=%d\n", fifo_depth, bus_width, big_endian);
return 0;
}
/* ===== 3. of_get_child_count / 자식 순회 패턴 ===== */
static int my_parse_channels(struct device *dev)
{
struct device_node *np = dev->of_node;
struct device_node *child;
int nchannels;
/* status="disabled" 제외한 자식 수 */
nchannels = of_get_available_child_count(np);
if (nchannels == 0) {
dev_err(dev, "no available channel nodes\n");
return -ENODEV;
}
/* available 자식만 순회 (status="okay" 또는 없는 것) */
for_each_available_child_of_node(np, child) {
u32 reg;
if (of_property_read_u32(child, "reg", ®)) {
dev_warn(dev, "%pOF: missing reg\n", child);
continue;
}
dev_info(dev, "channel %u: %pOFn\n", reg, child);
/* %pOFn = 노드 이름만 출력 */
}
/* for_each_* 매크로가 루프 종료 시 자동 of_node_put() */
/* 단, break로 빠져나오면 직접 of_node_put(child) 필요! */
return 0;
}
/* ===== 4. of_parse_phandle — phandle 참조 해석 ===== */
static int my_parse_clocks(struct device *dev)
{
struct device_node *np = dev->of_node;
struct device_node *clk_np;
struct of_phandle_args clkspec;
int ret, i, count;
/* 단순 phandle 해석 (첫 번째 clocks 항목) */
clk_np = of_parse_phandle(np, "clocks", 0);
if (clk_np) {
dev_info(dev, "clock provider: %pOF\n", clk_np);
of_node_put(clk_np);
}
/* phandle + args 해석 (clocks = <&ccu CLK_BUS_UART0>;) */
count = of_count_phandle_with_args(np, "clocks", "#clock-cells");
for (i = 0; i < count; i++) {
ret = of_parse_phandle_with_args(np, "clocks",
"#clock-cells", i, &clkspec);
if (ret)
continue;
dev_info(dev, "clock[%d]: provider=%pOF args[0]=%d\n",
i, clkspec.np, clkspec.args[0]);
of_node_put(clkspec.np);
}
return 0;
}
/* ===== 5. of_address_to_resource — reg → struct resource 변환 ===== */
static int my_map_registers(struct device *dev)
{
struct device_node *np = dev->of_node;
struct resource res;
void __iomem *base;
int ret;
/* 방법 1: of_address_to_resource (수동 매핑) */
ret = of_address_to_resource(np, 0, &res);
if (ret) {
dev_err(dev, "failed to get reg[0]: %d\n", ret);
return ret;
}
dev_info(dev, "reg: %pR\n", &res); /* %pR = [mem 0x1c28000-0x1c283ff] */
/* 방법 2: of_iomap (resource 해석 + ioremap 한번에) */
base = of_iomap(np, 0);
if (!base)
return -ENOMEM;
/* 사용 후 iounmap(base) 필요 */
/* 방법 3: devm_platform_ioremap_resource (권장 — 자동 관리) */
/* platform_driver의 probe에서: */
/* base = devm_platform_ioremap_resource(pdev, 0); */
iounmap(base);
return 0;
}
/* ===== 6. of_match_device — compatible 기반 하드웨어 분기 ===== */
struct my_hw_data {
u32 fifo_depth;
bool has_dma;
const char *variant;
};
static const struct my_hw_data hw_v1 = { .fifo_depth = 32, .has_dma = false, .variant = "v1" };
static const struct my_hw_data hw_v2 = { .fifo_depth = 128, .has_dma = true, .variant = "v2" };
static const struct of_device_id my_of_ids[] = {
{ .compatible = "myvendor,uart-v1", .data = &hw_v1 },
{ .compatible = "myvendor,uart-v2", .data = &hw_v2 },
{ .compatible = "myvendor,uart", .data = &hw_v1 }, /* 폴백 */
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);
static int my_probe(struct platform_device *pdev)
{
const struct my_hw_data *hw;
/* of_device_get_match_data: match된 of_device_id의 .data 반환 */
hw = of_device_get_match_data(&pdev->dev);
if (!hw) {
dev_err(&pdev->dev, "no match data\n");
return -ENODEV;
}
dev_info(&pdev->dev, "variant=%s fifo=%u dma=%d\n",
hw->variant, hw->fifo_depth, hw->has_dma);
if (hw->has_dma) {
/* DMA 초기화 */
}
return 0;
}
Clock 바인딩
SoC의 클록 트리는 PLL, 분주기(divider), 멀티플렉서(mux), 게이트(gate)의 계층 구조로 이루어집니다. Device Tree에서 클록 공급자(provider)와 소비자(consumer)의 바인딩을 정확히 기술해야 드라이버가 올바른 클록을 획득하고 제어할 수 있습니다.
/* ===== Clock Provider (CCU) DTS 정의 ===== */
/* SoC .dtsi 파일 — Clock Control Unit */
ccu: clock-controller@1c20000 {
compatible = "allwinner,sun50i-h5-ccu";
reg = <0x01c20000 0x400>;
clocks = <&osc24M>, <&rtc 0>; /* 입력: 24MHz OSC, 32kHz RTC */
clock-names = "hosc", "losc";
#clock-cells = <1>; /* 소비자가 clock ID 1개 지정 */
#reset-cells = <1>; /* 리셋 컨트롤러 기능 겸용 */
};
/* 외부 오실레이터 — fixed-clock 바인딩 */
osc24M: osc24M {
compatible = "fixed-clock";
#clock-cells = <0>; /* 출력 클록이 1개뿐이면 0 */
clock-frequency = <24000000>; /* 24 MHz */
clock-output-names = "osc24M";
};
/* ===== Clock Consumer DTS 정의 ===== */
&uart0 {
clocks = <&ccu CLK_BUS_UART0>; /* phandle + clock-id */
clock-names = "apb"; /* 드라이버에서 이 이름으로 검색 */
resets = <&ccu RST_BUS_UART0>; /* 리셋 라인 */
status = "okay";
};
/* 여러 클록을 사용하는 디바이스 */
&mmc0 {
clocks = <&ccu CLK_BUS_MMC0>, <&ccu CLK_MMC0>;
clock-names = "ahb", "mmc"; /* 순서 대응: clocks[0]=ahb, [1]=mmc */
resets = <&ccu RST_BUS_MMC0>;
status = "okay";
};
/* ===== Clock Consumer 드라이버 코드 ===== */
#include <linux/clk.h>
static int my_probe(struct platform_device *pdev)
{
struct clk *clk_ahb, *clk_mod;
int ret;
/* devm_clk_get: clock-names로 클록 획득 (devm = 자동 해제) */
clk_ahb = devm_clk_get(&pdev->dev, "ahb");
if (IS_ERR(clk_ahb))
return dev_err_probe(&pdev->dev, PTR_ERR(clk_ahb),
"failed to get ahb clock\n");
clk_mod = devm_clk_get(&pdev->dev, "mmc");
if (IS_ERR(clk_mod))
return dev_err_probe(&pdev->dev, PTR_ERR(clk_mod),
"failed to get mmc clock\n");
/* prepare + enable: 클록 활성화 (sleep context 가능) */
ret = clk_prepare_enable(clk_ahb);
if (ret) {
dev_err(&pdev->dev, "failed to enable ahb clock\n");
return ret;
}
ret = clk_prepare_enable(clk_mod);
if (ret) {
clk_disable_unprepare(clk_ahb);
return ret;
}
/* 클록 주파수 설정 (가능한 경우) */
ret = clk_set_rate(clk_mod, 50000000); /* 50 MHz */
if (ret)
dev_warn(&pdev->dev, "failed to set clock rate\n");
dev_info(&pdev->dev, "ahb=%lu Hz, mod=%lu Hz\n",
clk_get_rate(clk_ahb), clk_get_rate(clk_mod));
/* devm_clk_get_enabled(): clk_get + prepare_enable 한번에 (6.3+) */
/* struct clk *clk = devm_clk_get_enabled(&pdev->dev, "ahb"); */
return 0;
}
/* ===== Assigned Clocks — DT에서 부팅 시 클록 설정 ===== */
/* 드라이버 코드 없이 DT만으로 클록 주파수/부모 지정 가능 */
&mmc0 {
assigned-clocks = <&ccu CLK_MMC0>;
assigned-clock-rates = <50000000>; /* 50 MHz로 설정 */
};
/* 클록 부모 변경 */
&uart0 {
assigned-clocks = <&ccu CLK_UART0>;
assigned-clock-parents = <&ccu CLK_PLL_PERIPH0>; /* 부모 PLL 변경 */
};
/* assigned-clocks 처리 순서:
* 1. of_clk_set_defaults() — 드라이버 probe 전에 플랫폼 코드가 호출
* 2. assigned-clock-parents 먼저 적용 (clk_set_parent)
* 3. assigned-clock-rates 적용 (clk_set_rate)
* 4. 이후 드라이버 probe()가 실행
*/
Regulator/전원 DT 바인딩
PMIC(Power Management IC)의 레귤레이터를 DT로 정의하면, 드라이버가 devm_regulator_get()으로 전원 레일을 제어할 수 있습니다. 전압/전류 범위, 부팅 시 기본값, 부하 조건을 DT에서 선언적으로 관리합니다.
/* ===== PMIC Regulator DTS 정의 ===== */
/* I2C 버스에 연결된 PMIC 노드 */
&i2c0 {
pmic: pmic@34 {
compatible = "x-powers,axp803";
reg = <0x34>;
interrupt-parent = <&nmi_intc>;
interrupts = <0 IRQ_TYPE_LEVEL_LOW>;
regulators {
/* DCDC 레귤레이터: 고효율 벅 컨버터 */
reg_dcdc1: dcdc1 {
regulator-name = "vcc-3v3";
regulator-min-microvolt = <3300000>; /* 3.3V 고정 */
regulator-max-microvolt = <3300000>;
regulator-always-on; /* 항상 켜짐 */
regulator-boot-on; /* 부팅 시 켜짐 */
};
/* CPU 코어 전원: DVFS용 가변 전압 */
reg_dcdc2: dcdc2 {
regulator-name = "vdd-cpux";
regulator-min-microvolt = <800000>; /* 0.8V */
regulator-max-microvolt = <1100000>; /* 1.1V */
regulator-ramp-delay = <2500>; /* uV/us 전압 변경 속도 */
regulator-always-on;
};
/* LDO 레귤레이터: 저노이즈 아날로그 전원 */
reg_ldo1: ldo1 {
regulator-name = "vcc-wifi";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <1800000>;
/* regulator-always-on 없음 → 소비자가 제어 */
};
};
};
};
/* ===== 소비자에서 regulator 참조 ===== */
&mmc0 {
vmmc-supply = <®_dcdc1>; /* 카드 전원 (3.3V) */
vqmmc-supply = <®_ldo1>; /* I/O 전원 (1.8/3.3V) */
};
/* CPU DVFS: operating-points-v2 + regulator */
&cpu0 {
cpu-supply = <®_dcdc2>; /* cpufreq 드라이버가 DVFS 시 전압 조절 */
operating-points-v2 = <&cpu_opp_table>;
};
/* ===== Regulator Consumer 드라이버 코드 ===== */
#include <linux/regulator/consumer.h>
static int my_camera_probe(struct platform_device *pdev)
{
struct regulator *avdd, *dvdd;
int ret;
/* DTS에서 AVDD-supply = <®_ldo2>; 으로 연결 */
avdd = devm_regulator_get(&pdev->dev, "AVDD");
if (IS_ERR(avdd))
return dev_err_probe(&pdev->dev, PTR_ERR(avdd),
"failed to get AVDD supply\n");
dvdd = devm_regulator_get(&pdev->dev, "DVDD");
if (IS_ERR(dvdd))
return dev_err_probe(&pdev->dev, PTR_ERR(dvdd),
"failed to get DVDD supply\n");
/* 전원 켜기 (참조 카운트 기반 — 여러 소비자 공유 가능) */
ret = regulator_enable(avdd);
if (ret)
return ret;
/* 전압 설정 (DTS 범위 내에서만 가능) */
ret = regulator_set_voltage(avdd, 2800000, 2800000);
if (ret)
dev_warn(&pdev->dev, "failed to set AVDD voltage\n");
/* Bulk API: 여러 레귤레이터를 한번에 관리 */
/*
* struct regulator_bulk_data supplies[] = {
* { .supply = "AVDD" },
* { .supply = "DVDD" },
* };
* ret = devm_regulator_bulk_get(&pdev->dev, ARRAY_SIZE(supplies), supplies);
* ret = regulator_bulk_enable(ARRAY_SIZE(supplies), supplies);
*/
return 0;
}
GPIO/IRQ 바인딩 실전
GPIO 컨트롤러와 인터럽트 컨트롤러의 DT 바인딩은 임베디드 시스템에서 가장 빈번하게 사용됩니다. 올바른 셀 수 지정, 플래그 의미, 그리고 interrupt-map을 통한 인터럽트 도메인 변환을 정확히 이해해야 합니다.
/* ===== GPIO 컨트롤러 DTS 정의 ===== */
pio: gpio@1c20800 {
compatible = "allwinner,sun50i-h5-pinctrl";
reg = <0x01c20800 0x400>;
/* GPIO 컨트롤러 선언 */
gpio-controller; /* 이 노드가 GPIO provider */
#gpio-cells = <2>; /* <pin_number flags> */
gpio-ranges = <&pio 0 0 224>; /* pinctrl 매핑: GPIO 0-223 */
/* 인터럽트 컨트롤러 겸용 */
interrupt-controller;
#interrupt-cells = <2>; /* <pin_number irq_type> */
interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>, /* GPIOA */
<GIC_SPI 17 IRQ_TYPE_LEVEL_HIGH>, /* GPIOG */
<GIC_SPI 21 IRQ_TYPE_LEVEL_HIGH>; /* GPIOH */
};
/* ===== GPIO 소비자 바인딩 예제 ===== */
/* LED */
leds {
compatible = "gpio-leds";
status-led {
label = "status";
gpios = <&pio 7 0 GPIO_ACTIVE_HIGH>; /* PA7, active high */
linux,default-trigger = "heartbeat";
};
power-led {
gpios = <&pio 10 0 GPIO_ACTIVE_LOW>; /* PA10, active low */
default-state = "on";
};
};
/* 버튼 (GPIO 인터럽트) */
gpio-keys {
compatible = "gpio-keys";
power-button {
label = "Power";
gpios = <&pio 3 0 GPIO_ACTIVE_LOW>;
linux,code = <KEY_POWER>;
wakeup-source; /* suspend에서 깨어남 */
};
};
/* 디바이스 리셋 (named GPIO) */
&usb_phy {
reset-gpios = <&pio 3 6 GPIO_ACTIVE_LOW>; /* PD6, active low */
};
/* ===== interrupt-map: PCI 브리지 인터럽트 변환 ===== */
pci@10000000 {
compatible = "vendor,pcie-host";
#address-cells = <3>;
#size-cells = <2>;
#interrupt-cells = <1>; /* 자식: INTx (1~4) */
interrupt-map-mask = <0 0 0 7>; /* INTx 비트만 매스킹 */
interrupt-map =
<0 0 0 1 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>, /* INTA → SPI 100 */
<0 0 0 2 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>, /* INTB → SPI 101 */
<0 0 0 3 &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>, /* INTC → SPI 102 */
<0 0 0 4 &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>; /* INTD → SPI 103 */
/* interrupt-map 해석:
* <child_addr child_irq parent_phandle parent_irq_spec>
* child_addr: #address-cells 만큼의 주소 (PCI는 3)
* child_irq: #interrupt-cells 만큼 (PCI는 1)
* 실제 매칭: (child_unit & mask) == map_entry
*/
};
/* ===== GPIO/IRQ 드라이버 코드 ===== */
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
static irqreturn_t my_irq_handler(int irq, void *data)
{
pr_info("interrupt fired!\n");
return IRQ_HANDLED;
}
static int my_probe(struct platform_device *pdev)
{
struct gpio_desc *reset_gpio, *enable_gpio;
int irq, ret;
/* devm_gpiod_get: DTS의 reset-gpios 프로퍼티에서 GPIO 획득 */
/* con_id "reset" → DTS에서 "reset-gpios" 프로퍼티를 찾음 */
reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(reset_gpio))
return dev_err_probe(&pdev->dev, PTR_ERR(reset_gpio),
"failed to get reset GPIO\n");
/* 선택적 GPIO (없어도 에러 아님) */
enable_gpio = devm_gpiod_get_optional(&pdev->dev, "enable", GPIOD_OUT_LOW);
/* GPIO를 인터럽트로 사용 */
irq = platform_get_irq(pdev, 0); /* DTS interrupts 프로퍼티에서 IRQ 번호 */
if (irq < 0)
return irq;
ret = devm_request_irq(&pdev->dev, irq, my_irq_handler,
IRQF_TRIGGER_FALLING, "my-device", pdev);
if (ret)
return ret;
/* 리셋 시퀀스: Low → 지연 → High */
gpiod_set_value_cansleep(reset_gpio, 1); /* assert (active) */
usleep_range(1000, 2000);
gpiod_set_value_cansleep(reset_gpio, 0); /* deassert */
usleep_range(5000, 10000);
/* gpiod API는 ACTIVE_LOW를 자동 처리:
* gpiod_set_value(desc, 1) → GPIO_ACTIVE_LOW면 물리적 Low 출력
* 즉, 논리적 "active" = 1로 통일 */
return 0;
}
DMA 바인딩과 채널 매핑
DMA 컨트롤러와 주변장치 간의 채널 매핑을 DT에서 정의합니다. SoC의 DMA 컨트롤러는 여러 채널을 가지며, 각 주변장치가 특정 request line에 연결됩니다. 정확한 바인딩이 없으면 DMA 전송이 실패하거나 잘못된 채널로 데이터가 전달됩니다.
/* ===== DMA 컨트롤러 DTS ===== */
dma: dma-controller@1c02000 {
compatible = "allwinner,sun50i-a64-dma";
reg = <0x01c02000 0x1000>;
interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_DMA>;
resets = <&ccu RST_BUS_DMA>;
#dma-cells = <1>; /* 소비자가 request line 번호 1개 지정 */
};
/* ===== DMA 소비자 DTS ===== */
&uart0 {
dmas = <&dma 6>, <&dma 7>; /* TX request=6, RX request=7 */
dma-names = "tx", "rx"; /* 드라이버에서 이름으로 검색 */
};
&spi0 {
dmas = <&dma 22>, <&dma 23>; /* TX=22, RX=23 */
dma-names = "tx", "rx";
};
/* #dma-cells = <2>인 DMA 컨트롤러 (채널 + 설정)
* 예: STM32 DMA
* dmas = <&dma1 4 0x400>;
* ^^^^ ^ ^^^^
* phandle 채널 설정 플래그
*
* 설정 플래그는 SoC마다 다름: 우선순위, FIFO 모드, 버스트 크기 등
*/
/* ===== DMA Consumer 드라이버 코드 ===== */
#include <linux/dmaengine.h>
static int my_dma_probe(struct platform_device *pdev)
{
struct dma_chan *tx_chan, *rx_chan;
struct dma_slave_config cfg = {};
/* dma-names로 DMA 채널 획득 */
tx_chan = dma_request_chan(&pdev->dev, "tx");
if (IS_ERR(tx_chan))
return dev_err_probe(&pdev->dev, PTR_ERR(tx_chan),
"failed to request TX DMA channel\n");
rx_chan = dma_request_chan(&pdev->dev, "rx");
if (IS_ERR(rx_chan)) {
dma_release_channel(tx_chan);
return PTR_ERR(rx_chan);
}
/* DMA slave 설정 */
cfg.direction = DMA_MEM_TO_DEV;
cfg.dst_addr = res.start + 0x00; /* 주변장치 FIFO 주소 */
cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
cfg.dst_maxburst = 8; /* 버스트 전송 크기 */
dmaengine_slave_config(tx_chan, &cfg);
/* DMA 전송 시작 */
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_sg(tx_chan, sg, nents,
DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!desc)
return -ENOMEM;
desc->callback = my_dma_complete;
desc->callback_param = priv;
dmaengine_submit(desc);
dma_async_issue_pending(tx_chan);
return 0;
}
런타임 Overlay 적용
Device Tree Overlay는 기본 DTB를 수정하지 않고 런타임에 노드를 추가/변경할 수 있는 메커니즘입니다. 특히 Raspberry Pi의 HAT 자동 감지, 산업용 모듈러 시스템, Cape(BeagleBone) 등에서 핵심적으로 사용됩니다.
/* ===== Overlay DTS 소스 (my-sensor.dtso) ===== */
/dts-v1/;
/plugin/;
/* fragment: 기존 DT 노드를 수정하거나 자식을 추가 */
&i2c1 { /* target: 기존 i2c1 노드의 레이블 */
#address-cells = <1>;
#size-cells = <0>;
/* 새 I2C 디바이스 추가 */
temperature-sensor@48 {
compatible = "ti,tmp102";
reg = <0x48>;
interrupt-parent = <&pio>; /* __fixups__로 해석됨 */
interrupts = <7 IRQ_TYPE_EDGE_FALLING>;
#thermal-sensor-cells = <0>;
};
};
/* 기존 노드의 프로퍼티 변경 */
&uart2 {
status = "okay"; /* disabled → okay로 활성화 */
pinctrl-names = "default";
pinctrl-0 = <&uart2_pins>;
};
/* 참고: 새 레이블 정의도 가능
* 단, overlay 내 phandle은 __local_fixups__로 처리됨 */
# ===== dtoverlay 명령 (Raspberry Pi) =====
# 사용 가능한 overlay 목록
dtoverlay -l
# overlay 적용
sudo dtoverlay my-sensor.dtbo
# 현재 적용된 overlay 확인
dtoverlay -l
# overlay 제거 (LIFO 순서)
sudo dtoverlay -r my-sensor
# ===== Raspberry Pi config.txt =====
# /boot/config.txt에 추가하면 부팅 시 자동 적용
dtoverlay=my-sensor
dtoverlay=i2c-rtc,ds3231 # 파라미터 전달
dtparam=i2c_arm=on # 기본 DT 파라미터 변경
# ===== configfs를 통한 런타임 overlay 적용 =====
# overlay 디렉토리 생성
sudo mkdir /sys/kernel/config/device-tree/overlays/my-sensor
# .dtbo 바이너리를 직접 기록
sudo cp my-sensor.dtbo \
/sys/kernel/config/device-tree/overlays/my-sensor/dtbo
# 적용 상태 확인
cat /sys/kernel/config/device-tree/overlays/my-sensor/status
# → "applied" 또는 에러 메시지
# overlay 제거
sudo rmdir /sys/kernel/config/device-tree/overlays/my-sensor
# 커널 로그에서 overlay 적용 결과 확인
dmesg | grep -i overlay
/* ===== 커널 API: of_overlay_fdt_apply() ===== */
#include <linux/of.h>
#include <linux/of_fdt.h>
static int ovcs_id; /* overlay changeset ID */
static int my_module_apply_overlay(const void *dtbo, size_t dtbo_size)
{
int ret;
/* FDT overlay를 device_node 트리에 적용
* - phandle fixup 자동 수행
* - 새 device_node 생성 + 기존 프로퍼티 업데이트
* - platform_device 자동 생성 (compatible 매칭 시)
* - ovcs_id: 나중에 제거할 때 사용하는 changeset ID */
ret = of_overlay_fdt_apply(dtbo, dtbo_size, &ovcs_id, NULL);
if (ret) {
pr_err("overlay apply failed: %d\n", ret);
return ret;
}
pr_info("overlay applied, changeset id=%d\n", ovcs_id);
return 0;
}
static void my_module_remove_overlay(void)
{
/* changeset ID로 overlay 제거
* - 역순으로 device_node 삭제
* - platform_device 자동 제거 → driver remove() 호출
* - 프로퍼티 복원 */
of_overlay_remove(&ovcs_id);
}
멀티플랫폼 DTS 전략
하나의 SoC를 여러 보드에서 사용할 때, .dtsi(SoC 공통)와 .dts(보드 고유)를 분리하는 계층 구조가 필수입니다. 이 전략은 DTS 중복을 최소화하고, SoC 벤더의 업스트림 변경을 쉽게 반영할 수 있게 합니다.
SoC.dtsi→ 모든 주변장치를status = "disabled"로 정의SoC-variant.dtsi→ SoC 변형(패키지, 메모리 등) 추가 정의board.dts→ 실제 사용하는 주변장치만"okay"로 활성화- 프로퍼티 오버라이드: 나중에 포함된 파일이 우선 (last-one-wins)
/* ===== SoC DTSI 기본 (예: sun50i-h5.dtsi) ===== */
/* SoC 수준: 모든 IP 블록을 disabled로 선언 */
/ {
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&gic>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a53";
device_type = "cpu";
reg = <0>;
enable-method = "psci";
operating-points-v2 = <&cpu_opp_table>;
};
/* cpu@1, cpu@2, cpu@3 ... */
};
soc {
compatible = "simple-bus"; /* 자동으로 자식 platform_device 생성 */
#address-cells = <1>;
#size-cells = <1>;
ranges; /* 1:1 주소 매핑 */
uart0: serial@1c28000 {
compatible = "snps,dw-apb-uart";
reg = <0x01c28000 0x400>;
interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_BUS_UART0>;
resets = <&ccu RST_BUS_UART0>;
reg-shift = <2>;
reg-io-width = <4>;
status = "disabled"; /* ← 기본 비활성 */
};
uart1: serial@1c28400 {
/* ... 동일 패턴 ... */
status = "disabled";
};
mmc0: mmc@1c0f000 {
compatible = "allwinner,sun50i-a64-mmc";
/* ... */
status = "disabled";
};
};
};
/* ===== 보드 DTS (예: sun50i-h5-nanopi-neo2.dts) ===== */
/dts-v1/;
#include "sun50i-h5.dtsi"
#include "sunxi-common-regulators.dtsi" /* 공통 레귤레이터 */
/ {
model = "FriendlyElec NanoPi NEO2";
compatible = "friendlyarm,nanopi-neo2", "allwinner,sun50i-h5";
/* compatible 순서: 보드 → SoC (가장 구체적 → 일반적) */
aliases {
serial0 = &uart0; /* /dev/ttyS0 → uart0 매핑 */
ethernet0 = &emac;
};
chosen {
stdout-path = "serial0:115200n8"; /* earlycon 대상 */
};
leds {
compatible = "gpio-leds";
status-led {
label = "nanopi:green:status";
gpios = <&pio 0 10 GPIO_ACTIVE_HIGH>;
linux,default-trigger = "heartbeat";
};
};
};
/* 보드에서 사용하는 주변장치만 okay로 활성화 */
&uart0 {
pinctrl-names = "default";
pinctrl-0 = <&uart0_pa_pins>;
status = "okay"; /* ← 활성화 */
};
&mmc0 {
vmmc-supply = <®_vcc3v3>;
bus-width = <4>;
cd-gpios = <&pio 5 6 GPIO_ACTIVE_LOW>; /* 보드 고유: 카드 감지 핀 */
status = "okay";
};
&emac {
pinctrl-names = "default";
pinctrl-0 = <&emac_rgmii_pins>;
phy-mode = "rgmii-id"; /* 보드 PHY 인터페이스 */
phy-handle = <&ext_rgmii_phy>;
status = "okay";
mdio {
ext_rgmii_phy: ethernet-phy@7 {
compatible = "ethernet-phy-ieee802.3-c22";
reg = <7>;
};
};
};
# ===== arch/arm64/boot/dts/allwinner/Makefile =====
# SoC별로 그룹화, 보드 DTS를 나열
dtb-$(CONFIG_ARCH_SUNXI) += \
sun50i-h5-nanopi-neo2.dtb \
sun50i-h5-orangepi-pc2.dtb \
sun50i-h5-orangepi-zero-plus.dtb \
sun50i-a64-pine64.dtb \
sun50i-a64-pinephone-1.2.dtb
# dtb-y 사용 시 항상 빌드
# dtb-$(CONFIG_...) 사용 시 Kconfig 조건부 빌드
# 빌드 명령:
# make dtbs # 전체 DTB 빌드
# make sun50i-h5-nanopi-neo2.dtb # 단일 DTB 빌드
# make ARCH=arm64 dtbs_check # dt-schema 검증 포함
dt-schema/yamllint 워크플로
커널 5.x부터 Device Tree 바인딩은 YAML 스키마로 정의됩니다. dt-schema 도구와 make dtbs_check를 통해 DTS가 바인딩 규격을 준수하는지 자동 검증할 수 있습니다. 업스트림에 DT 바인딩을 제출할 때 이 검증을 통과해야 합니다.
# ===== YAML 바인딩 스키마 예제 =====
# Documentation/devicetree/bindings/serial/snps,dw-apb-uart.yaml
%YAML 1.2
---
$id: http://devicetree.org/schemas/serial/snps,dw-apb-uart.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
title: Synopsys DesignWare ABP UART
maintainers:
- "Andy Shevchenko <andriy.shevchenko@linux.intel.com>"
properties:
compatible:
oneOf:
- enum:
- snps,dw-apb-uart
- items:
- enum:
- allwinner,sun50i-h5-uart
- rockchip,rk3399-uart
- const: snps,dw-apb-uart # 폴백 compatible
reg:
maxItems: 1
interrupts:
maxItems: 1
clocks:
minItems: 1
maxItems: 2
items:
- description: Bus clock
- description: Baud clock (optional)
clock-names:
minItems: 1
items:
- const: apb
- const: baudclk
reg-shift:
enum: [0, 2] # 레지스터 간격: 1 또는 4 bytes
reg-io-width:
enum: [1, 4] # I/O 접근 폭
resets:
maxItems: 1
required:
- compatible
- reg
- interrupts
- clocks
additionalProperties: false # 정의되지 않은 프로퍼티 금지
examples:
- |
serial@1c28000 {
compatible = "allwinner,sun50i-h5-uart", "snps,dw-apb-uart";
reg = <0x01c28000 0x400>;
interrupts = <0 0 4>;
clocks = <&ccu 68>;
reg-shift = <2>;
reg-io-width = <4>;
};
...
# ===== dt-schema 검증 명령 =====
# 1. dt-schema 설치 (pip 또는 패키지 매니저)
pip3 install dtschema yamllint
# 2. 바인딩 YAML 스키마 자체 검증
make dt_binding_check
# 특정 바인딩만 검증:
make dt_binding_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/serial/snps,dw-apb-uart.yaml
# 3. DTB를 스키마에 대해 검증 (전체)
make ARCH=arm64 dtbs_check
# 특정 DTB만:
make ARCH=arm64 dtbs_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/serial/
# 4. 단독 dt-validate 실행
dt-validate -s Documentation/devicetree/bindings/processed-schema.json \
arch/arm64/boot/dts/allwinner/sun50i-h5-nanopi-neo2.dtb
# 5. yamllint로 YAML 문법만 확인
yamllint Documentation/devicetree/bindings/serial/snps,dw-apb-uart.yaml
# ===== 자주 발생하는 dt-schema 에러와 수정 방법 =====
# 에러 1: 'additionalProperties' 위반
# → 스키마에 정의되지 않은 프로퍼티 사용
# 수정: 스키마에 프로퍼티 추가 또는 DTS에서 제거
# 예: "my-custom-prop" is not valid under any of the given schemas
# 에러 2: 'required' 프로퍼티 누락
# → compatible, reg, interrupts 등 필수 프로퍼티 빠짐
# 수정: DTS에 누락된 프로퍼티 추가
# 예: 'clocks' is a required property
# 에러 3: compatible 문자열 불일치
# → 스키마의 enum/pattern과 DTS의 compatible이 불일치
# 수정: 정확한 compatible 문자열 사용
# 예: 'myvendor,my-uart' is not one of ['snps,dw-apb-uart']
# 에러 4: #*-cells 값 불일치
# → provider의 #clock-cells과 consumer의 인자 수 불일치
# 수정: cells 수에 맞게 인자 조정
# 팁: W=1로 빌드하면 dtc 경고도 표시
make W=1 ARCH=arm64 dtbs
# ===== dt-schema를 활용한 개발 워크플로 =====
# 새 바인딩 개발 시 권장 순서:
# 1. YAML 바인딩 작성
vim Documentation/devicetree/bindings/mysubsys/vendor,my-device.yaml
# 2. 바인딩 자체 검증 → 통과할 때까지 반복
make dt_binding_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/mysubsys/vendor,my-device.yaml
# 3. DTS 작성
vim arch/arm64/boot/dts/vendor/my-board.dts
# 4. DTB 빌드 + 스키마 검증
make ARCH=arm64 my-board.dtb
make ARCH=arm64 dtbs_check DT_SCHEMA_FILES=Documentation/devicetree/bindings/mysubsys/
# 5. 패치 제출 전 전체 검증
make ARCH=arm64 dt_binding_check
make ARCH=arm64 dtbs_check
커널 소스 DT 코드 참조
Device Tree의 핵심 처리 코드는 drivers/of/ 디렉토리에 집중되어 있습니다. 이 섹션에서는 드라이버 개발자가 알아야 할 핵심 함수들의 실제 구현을 분석합니다.
| 파일 | 역할 |
|---|---|
drivers/of/base.c | of_find_*, of_property_read_*, of_node_get/put 핵심 API |
drivers/of/fdt.c | FDT 파싱, unflatten, early_init_dt_* 함수 |
drivers/of/platform.c | of_platform_populate, platform_device 생성 |
drivers/of/address.c | 주소 변환 (of_translate_address, of_address_to_resource) |
drivers/of/irq.c | 인터럽트 파싱, 도메인 해석 |
drivers/of/overlay.c | Overlay 적용/제거, changeset 관리 |
drivers/of/dynamic.c | 동적 노드 추가/제거, notifier |
drivers/of/property.c | 프로퍼티 그래프 (ports/endpoints), fwnode 연동 |
/* ===== drivers/of/base.c — 핵심 검색/읽기 함수 ===== */
/* of_find_property: 노드에서 프로퍼티 검색 (내부 핵심 함수) */
struct property *of_find_property(
const struct device_node *np,
const char *name,
int *lenp)
{
struct property *pp;
unsigned long flags;
raw_spin_lock_irqsave(&devtree_lock, flags);
/* 프로퍼티 연결 리스트를 순회하며 이름 비교 */
pp = __of_find_property(np, name, lenp);
raw_spin_unlock_irqrestore(&devtree_lock, flags);
return pp;
}
static struct property *__of_find_property(
const struct device_node *np,
const char *name, int *lenp)
{
struct property *pp;
if (!np)
return NULL;
/* deadprops: overlay 제거 시 이동된 프로퍼티 리스트 */
for (pp = np->properties; pp; pp = pp->next) {
if (of_prop_cmp(pp->name, name) == 0) {
if (lenp)
*lenp = pp->length;
return pp;
}
}
return NULL;
}
/* of_property_read_u32_index: u32 프로퍼티의 N번째 요소 읽기 */
int of_property_read_u32_index(
const struct device_node *np,
const char *propname,
u32 index, u32 *out_value)
{
const u32 *val = of_find_property_value_of_size(
np, propname, ((index + 1) * sizeof(*out_value)),
0, NULL);
if (IS_ERR(val))
return PTR_ERR(val);
*out_value = be32_to_cpup(((__be32 *)val) + index);
/* FDT는 big-endian → CPU endian 변환 필수 */
return 0;
}
/* ===== drivers/of/platform.c — DT에서 platform_device 생성 ===== */
/* of_platform_default_populate_init: 부팅 시 자동 호출 */
static int __init of_platform_default_populate_init(void)
{
/* 이 함수가 arch_initcall_sync 우선순위로 호출됨 */
if (!of_have_populated_dt())
return -ENODEV;
/* 루트 노드의 자식들을 순회하며 platform_device 생성
* "simple-bus", "simple-mfd", "isa", "arm,amba-bus" 등은
* 재귀적으로 자식도 platform_device로 생성 */
of_platform_default_populate(NULL, NULL, NULL);
return 0;
}
arch_initcall_sync(of_platform_default_populate_init);
/* of_platform_device_create_pdata: 실제 device 생성 핵심 */
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np,
const char *bus_id,
void *platform_data,
struct device *parent)
{
struct platform_device *dev;
/* status = "disabled"이면 건너뛰기 */
if (!of_device_is_available(np) ||
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;
/* platform_device 할당 + DT의 reg/interrupts → resource 변환 */
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;
/* bus type, DMA 설정 */
dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
if (!dev->dev.dma_mask)
dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
dev->dev.bus = &platform_bus_type;
dev->dev.platform_data = platform_data;
of_msi_configure(&dev->dev, dev->dev.of_node);
/* device_add → 버스 매칭 → compatible 일치하는 드라이버의 probe() 호출 */
if (of_device_add(dev) != 0) {
platform_device_put(dev);
goto err_clear_flag;
}
return dev;
err_clear_flag:
of_node_clear_flag(np, OF_POPULATED);
return NULL;
}
/* ===== of_platform_populate — 재귀적 디바이스 생성 흐름 ===== */
int of_platform_populate(
struct device_node *root,
const struct of_device_id *matches, /* 재귀 대상 판별 */
const struct of_dev_auxdata *lookup,
struct device *parent)
{
struct device_node *child;
int rc = 0;
root = root ? root : of_find_node_by_path("/");
/* 루트의 각 자식에 대해 */
for_each_child_of_node(root, child) {
/* 1. platform_device 생성 (reg, interrupts → resource) */
rc = of_platform_bus_create(child, matches, lookup,
parent, true);
if (rc) {
of_node_put(child);
break;
}
}
/* of_platform_bus_create 내부:
* - compatible이 matches (simple-bus 등)에 해당하면 자식도 재귀 생성
* - "arm,primecell" → amba_device로 생성 (별도 경로)
* - 그 외 → platform_device로 생성
*
* 중요: I2C/SPI 버스 하위 디바이스는 여기서 생성되지 않음!
* → 해당 버스 드라이버(i2c-core, spi-core)의 probe에서
* of_register_child_devices()로 별도 생성
*/
of_node_set_flag(root, OF_POPULATED_BUS);
return rc;
}
EXPORT_SYMBOL_GPL(of_platform_populate);
unflatten_device_tree() 콜 체인 분석
부팅 초기화 과정에서 DTB(Device Tree Blob)를 커널의 device_node 트리로 변환하는 핵심 경로를 소스 코드 수준에서 분석합니다. unflatten_device_tree()부터 of_platform_device_create_pdata()까지의 전체 콜 체인은 DT 기반 플랫폼 초기화의 근간입니다.
unflatten_dt_nodes() — FDT 노드 순회 핵심
unflatten_dt_nodes()는 unflatten_device_tree() 내부에서 두 번 호출됩니다. 첫 번째 pass에서는 필요한 메모리 크기를 계산하고, 두 번째 pass에서는 실제로 device_node를 할당합니다.
/* drivers/of/fdt.c */
static int unflatten_dt_nodes(const void *blob,
void *mem,
struct device_node *dad,
struct device_node **nodepp)
{
struct device_node *root;
int offset = 0, depth = 0, initial_depth = 0;
#define FDT_MAX_DEPTH 64
struct device_node *nps[FDT_MAX_DEPTH]; /* 최대 64단계 스택 */
void *base = mem;
bool dryrun = !mem; /* mem==NULL → pass 1 (크기 계산) */
if (nodepp)
*nodepp = NULL;
/* FDT 토큰 기반 순회: BEGIN_NODE, END_NODE, PROP, NOP */
do {
u32 tag;
int next_offset;
const char *pathp;
offset = fdt_next_node(blob, offset, &depth);
if (offset < 0 && offset != -FDT_ERR_NOTFOUND)
return offset;
if (offset > 0 && depth >= initial_depth) {
/* populate_node: dryrun 시 크기만 누적, 실 run 시 노드 할당 */
if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH - 1))
continue;
populate_node(blob, offset, &mem,
nps[depth - 1], &nps[depth], dryrun);
}
} while (offset > 0);
if (!dryrun) {
/* 할당된 총 바이트 반환: 다음 pass의 kmalloc 크기 결정 */
if (nodepp)
*nodepp = nps[initial_depth];
return ((void *)ALIGN((unsigned long)mem, 4) - base);
}
return ((void *)ALIGN((unsigned long)mem, 4) - base);
}
코드 설명
- 3행
blob: DTB 바이너리 포인터 (물리 주소에서 fixmap으로 매핑된 가상 주소),mem: pass 2에서만 유효한 할당 버퍼,dad: 부모device_node(루트 호출 시 NULL) - 9행
nps[FDT_MAX_DEPTH]: 현재 깊이별device_node포인터 스택. DT 트리를 깊이 우선(DFS)으로 순회하며 부모-자식 연결에 사용 - 11행
dryrun = !mem:mem == NULL이면 pass 1(크기 계산 전용). 이 플래그로 같은 함수에서 두 가지 역할을 분리함 - 18행
fdt_next_node(): libfdt 함수. FDT 토큰 스트림에서 다음BEGIN_NODE/END_NODE토큰을 찾아 오프셋과 깊이를 반환. 음수 반환 시 오류 또는 순회 완료 - 26행
populate_node(): 실제struct device_node할당 및 초기화 담당. dryrun 시에는 필요 크기만 계산하고 포인터 전진, 실 run 시에는 할당된 버퍼에 노드를 구성 - 32행순회 종료 후 전진된
mem포인터와 시작점base의 차이를 반환. pass 1 결과값이unflatten_device_tree()에서kmalloc()크기로 사용됨
populate_node() — device_node 할당 및 초기화
populate_node()는 FDT의 단일 노드를 struct device_node와 연결된 struct property 리스트로 변환합니다. pass 1에서는 포인터를 전진해 크기를 측정하고, pass 2에서는 실제 필드를 채웁니다.
/* drivers/of/fdt.c — populate_node (simplified) */
static void *populate_node(const void *blob, int offset,
void **mem,
struct device_node *dad,
struct device_node **nodepp,
bool dryrun)
{
struct device_node *np;
const char *pathp;
unsigned int l, allocl;
/* FDT에서 노드 이름 포인터 획득 (e.g., "uart@10000000") */
pathp = fdt_get_name(blob, offset, &l);
if (!pathp) {
*nodepp = NULL;
return *mem;
}
allocl = ALIGN(l + 1, 4); /* 이름 문자열 크기 (4바이트 정렬) */
/* device_node + 이름 문자열을 연속 블록으로 할당 */
np = unflatten_dt_alloc(mem,
sizeof(struct device_node) + allocl, __alignof__(struct device_node));
if (!dryrun) {
/* full_name: device_node 구조체 바로 뒤 메모리 영역 가리킴 */
np->full_name = np->data;
memcpy(np->data, pathp, l);
np->data[l] = '\0';
/* 부모-자식-형제 연결: 트리 포인터 설정 */
if (dad != NULL) {
np->parent = dad;
/* 자식 연결 리스트의 맨 앞에 삽입 (역순이지만 후속 리버스로 교정) */
np->sibling = dad->child;
dad->child = np;
}
}
/* 이 노드의 프로퍼티들을 순회하여 struct property 리스트 구성 */
populate_properties(blob, offset, mem, np, pathp, dryrun);
if (!dryrun) {
np->name = of_get_property(np, "name", NULL);
if (!np->name)
np->name = "<NULL>";
}
*nodepp = np;
return *mem;
}
코드 설명
- 12행
fdt_get_name(): libfdt 함수. FDT 오프셋에서 노드 이름 문자열과 그 길이를 반환. 반환된 포인터는 DTB 바이너리 내부를 직접 가리키므로 복사가 필요 - 18행
allocl = ALIGN(l+1, 4): 노드 이름 저장에 필요한 4바이트 정렬된 크기. 이 크기가 pass 1에서 누적되어 최종kmalloc크기를 결정 - 21행
unflatten_dt_alloc(): dryrun 시에는 포인터만 전진(메모리 계산), 실 run 시에는 미리 할당된 버퍼에서 슬라이스를 반환하는 bump allocator 패턴 - 24행
np->full_name = np->data:device_node구조체 끝에 이름 문자열이 인접하게 배치됨. 추가kmalloc없이 단일 연속 블록으로 관리하는 최적화 기법 - 31행자식 노드를 부모의
child헤드에 삽입하는 스택 방식. FDT 순회 순서상 자식이 역순으로 삽입되며, 이후reverse_nodes()로 DTS 정의 순서로 복원 - 37행
populate_properties(): FDT의PROP토큰을 순회하여 각 프로퍼티마다struct property를 할당하고np->properties연결 리스트에 추가
struct device_node / struct property / struct of_device_id 필드 주석
세 구조체는 DT 기반 드라이버 개발의 핵심입니다. device_node는 트리 노드, property는 키-값 속성, of_device_id는 드라이버의 compatible 매칭 테이블을 나타냅니다.
/* include/linux/of.h — struct device_node 전체 필드 */
struct device_node {
const char *name; /* DTS 노드 이름 (@ 앞 부분, e.g. "uart") */
phandle phandle; /* DT phandle 값: 다른 노드에서 <&label>로 참조할 때 사용 */
const char *full_name; /* 노드 전체 이름 (name@unit-addr, e.g. "uart@10000000") */
struct fwnode_handle fwnode; /* 펌웨어 노드 추상화: DT·ACPI를 동일 인터페이스로 접근 */
struct property *properties; /* 이 노드의 프로퍼티 연결 리스트 헤드 */
struct property *deadprops; /* overlay 제거 시 무효화된 프로퍼티 (undo 지원용) */
struct device_node *parent; /* 부모 노드 포인터 (루트 노드는 NULL) */
struct device_node *child; /* 첫 번째 자식 노드 포인터 */
struct device_node *sibling; /* 다음 형제 노드 포인터 (단방향 연결 리스트) */
#if defined(CONFIG_OF_KOBJ)
struct kobject kobj; /* sysfs kobject: /sys/firmware/devicetree/ 하위 항목 노출 */
#endif
unsigned long _flags; /* OF_DYNAMIC·OF_DETACHED·OF_POPULATED·OF_POPULATED_BUS */
void *data; /* full_name 문자열 인접 저장 (bump allocator 최적화) */
};
코드 설명
- 3행 nameDTS 노드 이름에서
@unit-address앞 부분만 가리킵니다.uart@10000000이면name은"uart".of_find_node_by_name()에서 검색 키로 사용 - 4행 phandleDTS의
phandle = <N>또는&label참조 시 자동 할당되는 32비트 식별자.of_find_node_by_phandle()로 O(1) 조회 가능 (해시 테이블 캐시) - 6행 fwnodeDT와 ACPI를 통합하는 추상화 계층.
device_fwnode(dev),fwnode_get_named_child_node()등 fwnode API는 이 필드를 통해 DT/ACPI 무관하게 동작 - 9행 properties단방향 연결 리스트.
of_find_property()는 이 리스트를 선형 탐색. 대부분의 노드는 프로퍼티 수가 적어 O(N) 탐색이 충분히 빠름 - 10행 deadpropsDT Overlay 제거 시 살아있는
properties에서 분리된 프로퍼티들이 이곳으로 이동. overlay 재적용 시 복원 가능한 체크포인트 역할 - 17행 _flags
OF_POPULATED(bit 3): 이 노드로부터platform_device가 이미 생성됨을 표시.of_platform_device_create_pdata()에서 중복 생성 방지에 사용 - 18행 data
full_name이 실제로 가리키는 문자열 공간.populate_node()의 bump allocator가sizeof(device_node)바로 뒤에 이름 문자열을 배치하여 별도 할당 없이 관리
/* include/linux/of.h — struct property 전체 필드 */
struct property {
char *name; /* 프로퍼티 이름 문자열 (e.g. "compatible", "reg", "clocks") */
int length; /* 값 데이터의 바이트 길이 (빈 프로퍼티면 0) */
void *value; /* raw 바이트 포인터: FDT big-endian 그대로 보존 */
struct property *next; /* 같은 노드 내 다음 프로퍼티 (단방향 연결 리스트) */
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags; /* overlay changeset에서 상태 추적 (ADDED, REMOVED 등) */
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr; /* sysfs 바이너리 속성: /sys/firmware/devicetree/…/compatible 접근 */
#endif
};
코드 설명
- 3행 nameDTS의 프로퍼티 키.
of_find_property()는 이 필드와 입력 문자열을of_prop_cmp()(strcasecmp 계열)로 비교하여 프로퍼티를 탐색 - 4행 length값 바이트 수.
of_property_read_u32_array()등에서 요청 크기가length를 초과하면-EOVERFLOW반환. 빈 프로퍼티(불리언 flag)는length == 0 - 5행 valueFDT 명세상 모든 다중-셀 값은 big-endian(BE)으로 저장.
of_property_read_u32()가 내부적으로be32_to_cpup()을 호출해 CPU 네이티브 엔디안으로 변환 - 8행 _flagsDT Overlay changeset 추적 플래그.
OF_DYNAMIC노드에서만 유효하며, overlay 적용/제거 시 어떤 프로퍼티가 추가·삭제·수정되었는지 기록
/* include/linux/mod_devicetable.h — struct of_device_id */
struct of_device_id {
char name[32]; /* 매칭 대상 노드 이름 (현재 거의 사용 안 함, 보통 빈 문자열) */
char type[32]; /* 매칭 대상 device_type (현재 deprecated, 빈 문자열 권장) */
char compatible[128]; /* 핵심 매칭 필드: "vendor,model" 형식 (e.g. "arm,pl011") */
const void *data; /* 매칭 엔트리별 드라이버 private 데이터 포인터 */
};
코드 설명
- 3행 name과거 node-type 기반 매칭용이나 현재는 사용 권장하지 않음. 빈 문자열로 두면 이름 조건 무시.
of_match_node()에서 name·type·compatible 순서로 AND 조건 평가 - 5행 compatibleDTS의
compatible = "arm,pl011", "arm,primecell"처럼 다중 문자열 리스트의 각 항목과 순서대로 비교. 첫 번째 매칭 엔트리의data가of_device_get_match_data()로 반환 - 6행 data드라이버가 칩 변형(variant)별로 다른 설정 구조체를 여기 저장. probe()에서
of_device_get_match_data(dev)로 획득하여 IP 블록의 레지스터 오프셋이나 기능 플래그를 구분
of_find_compatible_node() — compatible 기반 노드 검색
of_find_compatible_node()는 드라이버 또는 초기화 코드에서 특정 compatible 값을 가진 DT 노드를 직접 찾을 때 사용합니다. 전체 DT 트리를 깊이 우선으로 순회하며 첫 번째 매칭 노드를 반환합니다.
/* drivers/of/base.c */
struct device_node *of_find_compatible_node(
struct device_node *from,
const char *type, /* device_type 필터 (NULL이면 무시) */
const char *compatible) /* 검색할 compatible 문자열 */
{
struct device_node *np;
struct device_node *from_node = from;
/* of_raw_node_start_from_next: from 다음 노드부터 순회 시작
* from == NULL이면 루트(of_root)부터 탐색 */
of_node_get(from); /* 참조 카운트 증가 (순회 중 해제 방지) */
raw_spin_lock(&devtree_lock);
np = __of_find_node_by_full_name(from ? from->sibling : of_allnodes,
compatible);
while (np) {
/* compatible 리스트의 각 항목과 비교 */
if (__of_device_is_compatible(np, compatible,
type, NULL)) {
of_node_get(np); /* 반환 전 참조 카운트 증가 */
break;
}
np = np->allnext; /* 전역 allnodes 연결 리스트로 다음 노드 이동 */
}
raw_spin_unlock(&devtree_lock);
of_node_put(from); /* 순회 시작점 참조 카운트 감소 */
return np; /* 호출자가 of_node_put()으로 해제 책임 */
}
코드 설명
- 2행 from이전 검색 결과 노드를 전달하면 그 다음 노드부터 탐색 재개.
NULL전달 시 루트부터 시작.for_each_compatible_node(dn, type, compat)매크로가 이 패턴을 래핑 - 11행
of_node_get(from): 순회 시작 전 참조 카운트를 증가시켜 다른 스레드의of_node_put()으로 인한 해제를 방지. raw_spin_lock으로 보호되는 구간 외에서도 안전하게 접근 가능 - 18행
__of_device_is_compatible():np->properties에서"compatible"프로퍼티를 찾아 null-separated 문자열 리스트를 순회하며compatible인자와 비교. 다중 compatible 중 하나라도 일치하면 참 - 22행
allnext:device_node의 숨겨진 포인터로 DT 트리의 모든 노드를 생성 순서대로 연결하는 전역 단방향 리스트. 트리 DFS 탐색 없이 선형 순회 가능 (현재 커널 버전에선 제거되고 xarray로 교체) - 27행반환된
np의 참조 카운트는 1 증가된 상태. 호출자는 사용 완료 후 반드시of_node_put(np)호출 필요. 누락 시kobj참조 누수 발생
of_property_read_u32_array() — 배열 프로퍼티 읽기
드라이버 probe()에서 가장 자주 호출되는 OF API 중 하나입니다. DTS의 <1 2 3> 형식 big-endian 값을 CPU 엔디안의 u32 배열로 변환하여 반환합니다.
/* drivers/of/base.c — of_property_read_u32_array */
int of_property_read_u32_array(const struct device_node *np,
const char *propname,
u32 *out_values, /* 결과 저장 배열 (호출자 제공) */
size_t sz) /* 읽을 u32 원소 개수 */
{
const __be32 *val;
/* 프로퍼티 존재 여부 + 크기 검증: sz * sizeof(u32) 이상인지 확인 */
val = of_find_property_value_of_size(np, propname,
(u32)(sz * sizeof(*out_values)),
0, /* 최대 크기 제한 없음 */
NULL);
if (IS_ERR(val))
return PTR_ERR(val); /* -EINVAL(없음), -ENODATA(빈 값), -EOVERFLOW(크기 부족) */
/* big-endian → CPU 엔디안 변환하며 배열 복사 */
while (sz--)
*out_values++ = be32_to_cpup(val++); /* FDT big-endian u32 읽기 */
return 0;
}
EXPORT_SYMBOL_GPL(of_property_read_u32_array);
코드 설명
- 9행
of_find_property_value_of_size(): 이름으로 프로퍼티 검색 후 크기 유효성 검사까지 수행하는 내부 헬퍼. 최소 크기(sz*4) 미달 시ERR_PTR(-EOVERFLOW)반환 - 14행에러 코드 의미:
-EINVAL은 프로퍼티 미존재,-ENODATA는 프로퍼티는 있으나 값이 없는 경우(불리언 flag),-EOVERFLOW는 프로퍼티 크기가 요청 크기보다 작은 경우 - 18행
be32_to_cpup(): big-endian 포인터에서 32비트 값을 읽어 CPU 네이티브 엔디안으로 변환. x86/ARM64에서는 바이트 스왑 수행, big-endian 아키텍처에서는 NOP. FDT 명세상 모든 셀 값은 BE이므로 필수 변환 - 22행
EXPORT_SYMBOL_GPL: GPL 라이선스 드라이버만 이 함수를 사용 가능. 커널 모듈로 작성된 디바이스 드라이버는 GPL 또는 GPL-compatible 라이선스여야 DT API 사용 가능
of_platform_device_create_pdata() — device_node → platform_device 변환
of_platform_bus_create()가 각 DT 노드에 대해 호출하는 핵심 함수입니다. device_node의 reg, interrupts 프로퍼티를 struct resource 배열로 변환하고, device_add()를 통해 버스에 등록합니다.
/* drivers/of/platform.c — of_platform_device_create_pdata 상세 */
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np, /* 변환할 DT 노드 */
const char *bus_id, /* 디바이스 이름 (NULL이면 DT full_name 사용) */
void *platform_data, /* 드라이버에 전달할 private 데이터 */
struct device *parent) /* sysfs 부모 디바이스 */
{
struct platform_device *dev;
/* 조건 1: status = "disabled"면 건너뜀 (available 노드만 처리) */
if (!of_device_is_available(np) ||
/* 조건 2: OF_POPULATED 플래그 이미 설정 → 중복 생성 방지 */
of_node_test_and_set_flag(np, OF_POPULATED))
return NULL;
/* of_device_alloc():
* - platform_device + resource 배열 할당
* - np→reg 프로퍼티 → IORESOURCE_MEM resource 변환
* - np→interrupts → IORESOURCE_IRQ resource 변환 (irq_domain 통해 hwirq→virq)
* - dev.of_node = np (device_node 포인터 연결)
*/
dev = of_device_alloc(np, bus_id, parent);
if (!dev)
goto err_clear_flag;
/* DMA 마스크 기본값 설정 (DT의 dma-ranges 고려 전 기본값) */
dev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
if (!dev->dev.dma_mask)
dev->dev.dma_mask = &dev->dev.coherent_dma_mask;
dev->dev.bus = &platform_bus_type; /* 플랫폼 버스에 등록 */
dev->dev.platform_data = platform_data;
/* MSI 도메인 설정: msi-parent 프로퍼티로 MSI 컨트롤러 연결 */
of_msi_configure(&dev->dev, dev->dev.of_node);
/* device_add() → bus_probe_device() → platform_match()
* → of_driver_match_device() → compatible 비교
* → 매칭 드라이버 found → driver_probe_device() → driver.probe()
* 매칭 실패 시 디바이스는 등록되지만 probe 없이 대기
* (드라이버 모듈 로드 시 재시도) */
if (of_device_add(dev) != 0) {
platform_device_put(dev);
goto err_clear_flag;
}
return dev;
err_clear_flag:
of_node_clear_flag(np, OF_POPULATED); /* 실패 시 플래그 원복 (재시도 허용) */
return NULL;
}
코드 설명
- 10행
of_device_is_available():status프로퍼티가 없거나"okay"/"ok"이면 true."disabled","fail","fail-sss"이면 false. Overlay로 나중에"okay"로 변경 시of_platform_device_create()를 명시적으로 호출해야 함 - 12행
of_node_test_and_set_flag(np, OF_POPULATED): 원자적 test-and-set. 이미 플래그가 설정된 경우 true 반환하여 중복platform_device생성을 방지. SMP 환경에서 레이스 컨디션 없이 안전 - 15행
of_device_alloc():platform_device_alloc()+of_address_to_resource()+of_irq_to_resource_table()조합. DT의reg와interrupts를 드라이버가platform_get_resource()로 읽을 수 있는resource배열로 변환 - 25행DMA 마스크 32비트 기본값.
dma-ranges프로퍼티가 있거나 IOMMU가 붙은 경우 이후of_dma_configure()에서 실제 마스크로 갱신됨 - 36행
of_device_add()는 내부적으로device_add()를 호출. 이 시점에platform_match()가 이미 등록된 드라이버와 compatible을 비교하여 즉시 probe 가능 여부를 결정. 드라이버 미등록 시deferred_probe_pending리스트에 삽입 - 44행
err_clear_flag에서OF_POPULATED를 클리어하여 재시도 기회를 유지.of_device_alloc()실패 또는device_add()실패 시 이 경로로 진입
참고자료
공식 규격 및 표준
- Devicetree Specification — devicetree.org에서 관리하는 공식 Device Tree 규격입니다
- dt-schema (GitHub) — Device Tree 바인딩 검증을 위한 JSON Schema 기반 도구입니다
- Devicetree Specification Source (GitHub) — Device Tree 규격 문서의 소스 저장소입니다
커널 문서
- Open Firmware and Devicetree — Kernel Documentation — 커널 공식 Device Tree 문서입니다
- Linux and the Devicetree — Usage Model — 리눅스 커널의 Device Tree 사용 모델을 설명합니다
- Device Tree Unit Tests — Device Tree 유닛 테스트 프레임워크를 설명합니다
- Writing Devicetree Bindings in DT Schema — dt-schema 형식으로 바인딩을 작성하는 가이드입니다
- Devicetree Overlay Notes — Device Tree Overlay의 커널 지원 방식을 설명합니다
LWN 기사
- Device tree overlays — Device Tree Overlay의 설계와 구현을 다룹니다 (2013)
- Device tree validation — DT 바인딩 검증 도구와 dt-schema 도입 배경을 설명합니다 (2014)
- Device tree, pair of boots, and a mainline kernel — DT 기반 하드웨어 기술의 실무 활용 사례를 다룹니다
- ACPI, PCI, and ... Device Tree? — ACPI와 Device Tree의 관계를 비교합니다 (2013)
- Device tree troubles — Device Tree 관련 ABI 안정성 논의를 다룹니다 (2013)
커널 소스 코드
- drivers/of/ — Open Firmware(Device Tree) 핵심 서브시스템 소스 코드입니다
- drivers/of/fdt.c — Flattened Device Tree(FDT) 파싱 코드입니다
- drivers/of/platform.c — DT 노드를 platform_device로 변환하는 코드입니다
- drivers/of/overlay.c — Device Tree Overlay 적용 및 제거 코드입니다
- scripts/dtc/ — Device Tree Compiler(DTC) 소스 코드입니다
컨퍼런스 발표 및 기술 자료
- eLinux — Device Tree Usage — Device Tree 사용법을 단계별로 설명하는 튜토리얼입니다
- eLinux — Device Tree Reference — Device Tree 관련 자료의 종합 모음입니다
관련 문서
Device Tree와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.