Device Tree 심화

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

전제 조건: 커널 아키텍처디바이스 드라이버 문서를 먼저 읽으세요. 하드웨어 기술 정보는 커널 초기화와 드라이버 바인딩의 입력이므로, 기술 데이터와 런타임 탐색 경로를 함께 이해해야 합니다.
일상 비유: 이 주제는 설비 배선도와 장비 목록 대조와 비슷합니다. 도면과 실제 배치가 맞아야 정상 동작하듯이, ACPI/DT 정보와 드라이버 기대값이 일치해야 안정적으로 부팅됩니다.

핵심 요약

  • 단계 분리 — 펌웨어, 부트로더, 커널 초기화 경계를 구분합니다.
  • 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
  • 신뢰 체인 — Secure Boot 등 검증 체인을 흐름으로 이해합니다.
  • 실패 지점 — 부팅 로그에서 단계별 실패 단서를 빠르게 찾습니다.
  • 호환성 관점 — 플랫폼 차이에 따른 초기화 분기를 함께 점검합니다.

단계별 이해

  1. 부팅 단계 식별
    현재 이슈가 어느 단계에서 발생하는지 먼저 고정합니다.
  2. 입력 데이터 확인
    펌웨어/테이블/이미지 메타데이터를 점검합니다.
  3. 전환 경계 검증
    단계 간 인자 전달과 상태 인계를 추적합니다.
  4. 플랫폼별 재검증
    다른 하드웨어 조건에서도 동일하게 동작하는지 확인합니다.
관련 표준: Devicetree Specification v0.4 (devicetree.org) — DT 문법, FDT 바이너리 포맷, 주소 변환 규칙 등 근본 규격. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

Device Tree는 하드웨어 구성을 기술하는 데이터 구조로, PCI/USB처럼 자동 열거(enumeration)가 불가능한 SoC 내장 디바이스의 정보를 커널에 전달합니다. ARM, RISC-V, PowerPC 등 임베디드 플랫폼에서 필수적이며, Open Firmware(IEEE 1275) 표준에서 유래했습니다.

DTS 처리 흐름: .dts (소스) → dtc (컴파일러) → .dtb (바이너리 블롭) → 부트로더가 메모리에 로드 → 커널이 파싱하여 struct device_node 트리 구축 → 드라이버가 of_* API로 프로퍼티 조회

Device Tree 아키텍처

빌드 시점 (Build Time) .dts / .dtsi Device Tree Source dtc DT Compiler .dtb FDT Binary Blob .dtbo (Overlay) 런타임 수정 가능 부팅 시점 (Boot Time) 부트로더 (U-Boot / UEFI) DTB 메모리 로드 + 아키텍처별 전달 Overlay 병합 (선택) 커널 (Kernel Space) unflatten_device_tree() struct device_node 트리 /proc/device-tree/ of_platform_populate() platform_driver i2c_driver spi_driver of_*() API fwnode API /sys/firmware/devicetree/base/ compatible 매칭 → probe() 호출
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;
                    };
                };
            };
        };
    };
};

표준 프로퍼티 레퍼런스

프로퍼티타입설명예시
compatiblestring-list드라이버 매칭 키. 구체적→일반적 순서"vendor,exact", "vendor,fallback"
regprop-encoded주소/크기 쌍. 해석은 부모의 #address-cells/#size-cells에 의존<0x10000 0x1000>
interruptsprop-encoded인터럽트 지정자. 해석은 인터럽트 컨트롤러의 #interrupt-cells에 의존<GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>
interrupt-parentphandle인터럽트 컨트롤러 참조 (생략 시 부모 노드에서 상속)<&gic>
clocksphandle+args클럭 소스 참조<&ccu CLK_UART0>
clock-namesstring-list클럭 이름 (clocks와 순서 대응)"apb", "mod"
resetsphandle+args리셋 컨트롤러 참조<&ccu RST_UART0>
statusstring"okay"=활성, "disabled"=비활성"okay"
#address-cellsu32자식 reg의 주소 u32 개수<2>
#size-cellsu32자식 reg의 크기 u32 개수 (0이면 크기 없음)<1>
rangesprop-encoded자식→부모 주소 변환. 빈 값이면 1:1 매핑<0x0 0x0 0x10000000 0x1000000>
dma-rangesprop-encodedDMA 주소 변환 (CPU 주소 ≠ DMA 주소일 때)<0x0 0x0 0x80000000 0x80000000>
pinctrl-0phandle-list핀 설정 참조 (상태 0=default)<&uart0_pins>
pinctrl-namesstring-list핀 설정 상태 이름"default", "sleep"
*-gpiosphandle+argsGPIO 참조 (접두사가 이름)reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>
*-supplyphandle전원 레귤레이터 참조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 직접 비교 (폴백)
 */
fwnode API (v4.13+): Device Tree와 ACPI 양쪽을 지원하는 드라이버는 of_* 대신 device_property_read_*(fwnode) API를 사용하면 DT/ACPI 코드를 통일할 수 있습니다. 예: device_property_read_u32(dev, "fifo-depth", &val)

특수 노드와 고급 패턴

/* ===== 주요 특수 노드들 ===== */

/* 1. reserved-memory — 커널이 사용하지 않을 메모리 영역 */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    /* CMA (Contiguous Memory Allocator) 영역 */
    linux,cma {
        compatible = "shared-dma-pool";
        reusable;
        size = <0x0 0x10000000>;   /* 256 MiB */
        linux,cma-default;
    };

    /* 펌웨어 전용 영역 */
    fw_reserved: framebuffer@be000000 {
        reg = <0x0 0xbe000000 0x0 0x2000000>;
        no-map;                  /* 커널이 매핑하지 않음 */
    };
};

/* 2. GPIO hog — 부팅 시 GPIO를 고정 상태로 설정 */
&gpio1 {
    led-hog {
        gpio-hog;
        gpios = <10 GPIO_ACTIVE_HIGH>;
        output-high;
        line-name = "status-led";
    };
};

/* 3. 클럭/레귤레이터 고정 정의 (물리 클럭을 DT에서 선언) */
osc24m: oscillator-24m {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;     /* 24 MHz */
    clock-output-names = "osc24m";
};

reg_3v3: regulator-3v3 {
    compatible = "regulator-fixed";
    regulator-name = "vcc-3v3";
    regulator-min-microvolt = <3300000>;
    regulator-max-microvolt = <3300000>;
    regulator-always-on;
};

/* 4. OPP (Operating Performance Points) 테이블 */
cpu_opp_table: opp-table {
    compatible = "operating-points-v2";

    opp-600000000 {
        opp-hz = /bits/ 64 <600000000>;
        opp-microvolt = <900000>;
    };
    opp-1200000000 {
        opp-hz = /bits/ 64 <1200000000>;
        opp-microvolt = <1100000>;
    };
    opp-1800000000 {
        opp-hz = /bits/ 64 <1800000000>;
        opp-microvolt = <1300000>;
        opp-suspend;                 /* suspend 시 이 OPP 사용 */
    };
};

/* 5. 인터럽트 매핑 (interrupt-map) — PCI 등 */
pcie@10000000 {
    interrupt-map-mask = <0x1800 0 0 7>;
    interrupt-map =
        <0x0000 0 0 1 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
        <0x0000 0 0 2 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
};

Device Tree 디버깅

# ===== 실행 중인 시스템에서 DT 확인 =====

# Live Device Tree (procfs)
$ ls /proc/device-tree/
#address-cells  cpus     memory@80000000  soc
#size-cells     chosen   model            compatible

# 특정 노드의 프로퍼티 읽기
$ cat /proc/device-tree/model
MyVendor MyBoard Rev.A
$ hexdump -C /proc/device-tree/soc/serial@1c28000/reg
00000000  01 c2 80 00 00 00 04 00

# sysfs를 통한 접근 (동일한 데이터)
$ ls /sys/firmware/devicetree/base/
$ cat /sys/firmware/devicetree/base/compatible

# ===== 커널 로그에서 DT 관련 메시지 =====
$ dmesg | grep -iE 'device.?tree|of_|dts|dtb|compatible'
OF: fdt: Machine model: MyVendor MyBoard Rev.A
OF: fdt: Ignoring memory range 0x0 - 0x80000000

# ===== probe 실패 디버깅 =====

# 매칭되지 않은(드라이버 없는) 디바이스 확인
$ ls /sys/bus/platform/devices/
# 1c28000.serial  1c2ac00.i2c  ...

# 특정 디바이스의 드라이버 바인딩 상태
$ ls -la /sys/bus/platform/devices/1c28000.serial/driver
# symlink → 해당 드라이버 (없으면 매칭 실패)

# deferred probe 목록 (의존성 대기 중)
$ cat /sys/kernel/debug/devices_deferred
# 1c2ac00.i2c  ← 클럭/레귤레이터 등 의존성 미충족

# 드라이버 강제 바인드/언바인드
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/bind
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/unbind

# ===== Overlay 상태 확인 =====
$ ls /sys/kernel/config/device-tree/overlays/
my-hat/
$ cat /sys/kernel/config/device-tree/overlays/my-hat/status
applied

# ===== ftrace로 DT 매칭 추적 =====
$ echo 1 > /sys/kernel/tracing/events/bus/bus_add_device/enable
$ echo 1 > /sys/kernel/tracing/events/bus/driver_bound/enable
$ cat /sys/kernel/tracing/trace_pipe
# bus_add_device: device 1c28000.serial
# driver_bound: device 1c28000.serial driver my-device

# ===== DT Validation (빌드 시) =====
$ make ARCH=arm64 dt_binding_check    # YAML 스키마 검증
$ make ARCH=arm64 dtbs_check          # DTB vs 바인딩 검증
$ make ARCH=arm64 W=1 dtbs            # 경고 활성화 빌드
DT 작성 시 주의사항:
  • 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 트리를 구축합니다.

DTB (Flattened Device Tree) 메모리 레이아웃 struct fdt_header (40 bytes) magic: 0xD00DFEED totalsize | off_dt_struct | off_dt_strings off_mem_rsvmap | version(17) | boot_cpuid_phys off_dt_struct → Structure Block 시작 off_dt_strings → Strings Block 시작 off_mem_rsvmap → Reserved Map 시작 totalsize → DTB 전체 크기 Memory Reservation Block { address(u64), size(u64) } 쌍의 배열 — {0,0}으로 종료 0x28 Structure Block FDT_BEGIN_NODE (0x01) + name + padding FDT_PROP (0x03) + len + nameoff + data + padding FDT_BEGIN_NODE (자식) ... FDT_END_NODE FDT_END_NODE (0x02) FDT_END (0x09) FDT_PROP 구조: nameoff → Strings Block의 프로퍼티 이름 오프셋 Strings Block "compatible\0reg\0interrupts\0status\0..." (NUL 종료 문자열) Free Space (Overlay 확장 여유) totalsize
DTB 바이너리 포맷 — fdt_header가 각 블록의 오프셋을 지정
/* ===== FDT 헤더 구조체 (include/linux/libfdt_env.h → scripts/dtc/libfdt/) ===== */

struct fdt_header {
    fdt32_t magic;             /* 0xD00DFEED (big-endian) */
    fdt32_t totalsize;          /* DTB 전체 크기 (bytes) */
    fdt32_t off_dt_struct;      /* Structure Block 시작 오프셋 */
    fdt32_t off_dt_strings;     /* Strings Block 시작 오프셋 */
    fdt32_t off_mem_rsvmap;     /* Memory Reservation Block 오프셋 */
    fdt32_t version;            /* 포맷 버전 (현재 17) */
    fdt32_t last_comp_version;  /* 호환 가능한 최소 버전 (16) */
    fdt32_t boot_cpuid_phys;   /* 부팅 CPU의 physical ID */
    fdt32_t size_dt_strings;   /* Strings Block 크기 */
    fdt32_t size_dt_struct;    /* Structure Block 크기 */
};

/* FDT는 모두 big-endian으로 저장됨 — cpu_to_fdt32() / fdt32_to_cpu() 로 변환 */

/* ===== Structure Block 토큰 ===== */
#define FDT_BEGIN_NODE  0x00000001  /* 노드 시작 + 이름(NUL종료, 4-byte 정렬) */
#define FDT_END_NODE    0x00000002  /* 노드 종료 */
#define FDT_PROP        0x00000003  /* 프로퍼티: len(u32) + nameoff(u32) + data */
#define FDT_NOP         0x00000004  /* 무시 (편집 시 패딩용) */
#define FDT_END         0x00000009  /* Structure Block 종료 */

/* ===== Memory Reservation Block =====
 * 커널이 사용하면 안 되는 물리 메모리 영역 (예: DTB 자체, 펌웨어 영역)
 * { uint64_t address; uint64_t size; } 쌍의 배열
 * address=0, size=0 엔트리로 종료
 *
 * 참고: reserved-memory DT 노드와 다름!
 * - Memory Reservation Block: FDT 바이너리 레벨, early boot에서 처리
 * - reserved-memory 노드: DT 노드 레벨, memblock 서브시스템에서 처리
 */

/* ===== FDT 프로퍼티 인코딩 예시 =====
 *
 * DTS: compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
 *
 * Structure Block에 저장되는 바이너리:
 * [FDT_PROP]                       ← 0x00000003
 * [len = 39]                       ← 두 문자열 + NUL 포함 길이
 * [nameoff = 0]                    ← Strings Block에서 "compatible" 오프셋
 * "myvendor,my-soc-uart\0snps,dw-apb-uart\0"  ← 실제 데이터
 * [padding]                        ← 4-byte 정렬 맞춤
 *
 * DTS: reg = <0x1c28000 0x400>;
 *
 * [FDT_PROP]
 * [len = 8]                        ← u32 × 2 = 8 bytes
 * [nameoff = 11]                   ← Strings Block에서 "reg" 오프셋
 * [0x01C28000] [0x00000400]        ← big-endian u32 값들
 */

/* ===== 커널에서 FDT 직접 접근 (early boot) ===== */
#include <linux/of_fdt.h>

/* early_init_dt_scan(): 부팅 초기에 FDT에서 핵심 정보 추출 */
void __init early_init_dt_scan_nodes(void)
{
    /* chosen 노드에서 bootargs, initrd 위치 추출 */
    early_init_dt_scan_chosen(boot_command_line);

    /* /memory 노드에서 물리 메모리 범위 추출 → memblock에 등록 */
    early_init_dt_scan_memory();

    /* root 노드에서 #address-cells, #size-cells 가져오기 */
    early_init_dt_scan_root();
}

/* unflatten: FDT 바이너리 → struct device_node 트리 변환 */
void __init unflatten_device_tree(void)
{
    /* 1차 패스: 필요한 메모리 크기 계산 */
    /* 2차 패스: device_node + property 구조체 할당 및 연결 */
    __unflatten_device_tree(initial_boot_params, NULL,
                           &of_root, early_init_dt_alloc_memory_arch, false);

    /* of_root: 전역 루트 device_node 포인터 */
    /* /proc/device-tree/와 /sys/firmware/devicetree/base/로 노출 */
}

struct device_node / struct property 내부 구조

unflatten_device_tree() 완료 후 커널 메모리에 존재하는 자료구조입니다. 모든 of_* API는 이 구조체를 통해 DT 정보에 접근합니다.

/* include/linux/of.h */

struct device_node {
    const char *name;            /* 노드 이름 (@ 앞 부분) */
    phandle phandle;              /* 고유 식별자 (phandle 프로퍼티 값) */
    const char *full_name;        /* 전체 경로명 또는 name[@unit-address] */
    struct fwnode_handle fwnode;   /* 펌웨어 노드 추상화 (DT/ACPI 통합) */

    struct property *properties;  /* 프로퍼티 연결 리스트 헤드 */
    struct property *deadprops;   /* 제거된 프로퍼티 (overlay undo용) */

    /* 트리 탐색 포인터 */
    struct device_node *parent;   /* 부모 노드 */
    struct device_node *child;    /* 첫 번째 자식 */
    struct device_node *sibling;  /* 다음 형제 */

#if defined(CONFIG_OF_KOBJ)
    struct kobject kobj;           /* sysfs 표현 (/sys/firmware/devicetree/) */
#endif
    unsigned long _flags;          /* OF_POPULATED, OF_DETACHED 등 */
    void *data;                    /* 드라이버 private 데이터 */
};

/* 플래그 상수 */
#define OF_DYNAMIC       1  /* overlay로 동적 생성된 노드 */
#define OF_DETACHED      2  /* 트리에서 분리된 노드 */
#define OF_POPULATED     3  /* platform_device가 이미 생성됨 */
#define OF_POPULATED_BUS 4  /* 자식 디바이스들도 생성됨 */

struct property {
    char *name;                   /* 프로퍼티 이름 ("compatible", "reg" 등) */
    int length;                    /* 값의 바이트 길이 */
    void *value;                   /* 프로퍼티 값 (raw 바이트) */
    struct property *next;        /* 같은 노드의 다음 프로퍼티 */
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
    unsigned long _flags;
#endif
#if defined(CONFIG_OF_KOBJ)
    struct bin_attribute attr;    /* sysfs 바이너리 속성 */
#endif
};

/* ===== device_node 트리 순회 매크로 ===== */

/* 모든 자식 노드 순회 */
for_each_child_of_node(parent, child) { ... }

/* available(status != "disabled") 자식만 순회 */
for_each_available_child_of_node(parent, child) { ... }

/* 특정 compatible을 가진 노드만 순회 */
for_each_compatible_node(dn, type, compatible) { ... }

/* 특정 프로퍼티를 가진 노드 순회 */
for_each_node_with_property(dn, prop_name) { ... }

/* of_node 참조 카운팅 */
struct device_node *np = of_node_get(node);  /* refcount++ */
of_node_put(np);                                /* refcount-- */
/* for_each_* 매크로는 루프 내에서 자동으로 get/put 처리
 * 주의: break로 루프를 탈출하면 of_node_put()을 수동 호출해야 함! */

/* ===== 노드 → platform_device 변환 흐름 =====
 *
 * 1. unflatten_device_tree() → device_node 트리 구축
 * 2. of_platform_default_populate()
 *    → 루트의 direct children 중 compatible 있는 노드를 platform_device로 생성
 *    → "simple-bus", "simple-mfd", "isa", "arm,amba-bus" compatible의 노드는
 *       재귀적으로 자식도 platform_device로 생성
 * 3. 각 platform_device의 compatible과 등록된 platform_driver의 of_match_table 비교
 * 4. 매칭 성공 → driver->probe() 호출
 * 5. probe 시 의존성(clk, regulator 등) 미충족이면 -EPROBE_DEFER 반환
 *    → 나중에 재시도 (deferred probe)
 */

주소 변환 (Address Translation) 상세

Device Tree에서 각 버스 레벨마다 독립적인 주소 공간을 가집니다. ranges 프로퍼티가 자식 주소 공간 → 부모 주소 공간으로의 변환 규칙을 정의합니다.

DT 주소 변환: ranges 프로퍼티 동작 / (루트): #address-cells=<2>, #size-cells=<2> CPU 물리 주소 공간 (64-bit) soc: #address-cells=<1>, #size-cells=<1> ranges = <0x0 0x0 0x0 0x40000000>; child_addr(1 cell) → parent_addr(2 cells): 0x0 → 0x0_0000_0000, size=1GiB serial@1c28000: reg=<0x1c28000 0x400> 로컬 주소 0x01C28000 → CPU 주소 0x0_01C28000 pcie@10000000: #address-cells=<3> PCI 주소 공간 → CPU 주소 공간 변환 ranges = <0x02000000 0x0 0x20000000 0x0 0x20000000 0x0 0x10000000>; PCI MEM 0x20000000 → CPU 0x20000000 (256MiB)
주소 변환 체인 — 각 bus 레벨의 ranges가 자식→부모 주소를 변환
/* ===== ranges 프로퍼티 해석 규칙 =====
 *
 * ranges = < child_addr  parent_addr  length >;
 *
 * - child_addr의 셀 수 = 현재 노드의 #address-cells
 * - parent_addr의 셀 수 = 부모 노드의 #address-cells
 * - length의 셀 수 = 현재 노드의 #size-cells
 * - 빈 ranges (ranges;) → 1:1 매핑 (주소 동일)
 * - ranges 없음 → 자식 주소를 부모 주소로 변환 불가 (독립 주소 공간)
 */

/* 예시 1: 단순 SoC 버스 — 오프셋 변환 */
/ {
    #address-cells = <2>;   /* 루트: 64-bit 주소 */
    #size-cells = <2>;

    soc {
        compatible = "simple-bus";
        #address-cells = <1>; /* SoC: 32-bit 주소 */
        #size-cells = <1>;
        /* child(1 cell)  parent(2 cells)  size(1 cell)
         * 0x0          → 0x0_0000_0000    1 GiB 범위 */
        ranges = <0x0  0x0 0x0  0x40000000>;

        /* serial의 reg 0x1c28000은:
         * child_addr = 0x01c28000
         * ranges 적용: 0x01c28000 + 0x0 = 0x0_01c28000 (CPU 물리 주소) */
        serial@1c28000 {
            reg = <0x1c28000 0x400>;
        };
    };
};

/* 예시 2: 다중 ranges — 여러 주소 윈도우 */
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0x00000000  0x0 0x00000000  0x20000000>,  /* 0~512M: 1:1 */
             <0x40000000  0x0 0x40000000  0x20000000>;  /* 1G~1.5G */
};

/* 예시 3: PCI 주소 공간 (#address-cells = <3>) */
pcie@10000000 {
    compatible = "pci-host-ecam-generic";
    /* PCI는 #address-cells = 3: (phys.hi  phys.mid  phys.lo)
     * phys.hi 비트 구조:
     *   [31]    = relocatable
     *   [30:29] = 프리페치 (01=I/O, 10=32-bit MEM, 11=64-bit MEM)
     *   [24]    = prefetchable
     *   [23:16] = bus number
     *   [15:11] = device number
     *   [10:8]  = function number
     *   [7:0]   = register number */
    #address-cells = <3>;
    #size-cells = <2>;

    /* PCI 주소(3 cells) → CPU 주소(2 cells), 크기(2 cells) */
    ranges =
        /* I/O 공간: PCI I/O 0x0 → CPU 0x1000_0000, 64KiB */
        <0x01000000 0x0 0x00000000  0x0 0x10000000  0x0 0x00010000>,
        /* 32-bit MEM: PCI MEM 0x2000_0000 → CPU 0x2000_0000, 256MiB */
        <0x02000000 0x0 0x20000000  0x0 0x20000000  0x0 0x10000000>,
        /* 64-bit MEM (prefetchable): PCI 0x8_0000_0000 → CPU 0x8_0000_0000, 4GiB */
        <0x43000000 0x8 0x00000000  0x8 0x00000000  0x1 0x00000000>;
};

/* ===== 커널의 주소 변환 API ===== */
#include <linux/of_address.h>

/* of_translate_address(): DT 주소 → CPU 물리 주소 변환
 * ranges 체인을 루트까지 재귀적으로 따라가며 변환 */
u64 cpu_addr = of_translate_address(np, addr_prop);

/* of_address_to_resource(): reg → struct resource 변환
 * 내부적으로 of_translate_address() + 크기 정보 포함 */
struct resource res;
of_address_to_resource(np, 0, &res);  /* 첫 번째 reg 엔트리 */
/* res.start = 변환된 CPU 물리 주소
 * res.end   = start + size - 1
 * res.flags = IORESOURCE_MEM 또는 IORESOURCE_IO */

/* of_translate_dma_address(): DMA 주소 변환 (dma-ranges 사용) */
u64 dma_addr = of_translate_dma_address(np, addr_prop);

/* dma-ranges: DMA 엔진이 보는 주소 ≠ CPU 물리 주소일 때
 * 예: GPU나 DMA 컨트롤러가 IOMMU 없이 다른 주소로 메모리 접근 */
soc {
    /* DMA 주소 0x0 → CPU 물리 주소 0x8000_0000 */
    dma-ranges = <0x0  0x0 0x80000000  0x80000000>;
};

인터럽트 도메인과 Nexus 노드 심화

Device Tree의 인터럽트 계층은 디바이스 트리 구조(부모-자식)와 독립적입니다. interrupt-parent가 인터럽트 도메인 트리를 형성하고, interrupt-map이 도메인 간 인터럽트 번호 변환을 수행합니다.

/* ===== 인터럽트 처리 핵심 개념 =====
 *
 * 1. interrupt-controller: 이 노드가 인터럽트 컨트롤러임을 선언 (빈 프로퍼티)
 * 2. #interrupt-cells: 자식이 interrupts에 넣는 셀 수 (GIC=3, GPIO=2 등)
 * 3. interrupt-parent: 인터럽트를 수신할 컨트롤러 (생략 시 DT 부모에서 상속)
 * 4. interrupts: 인터럽트 지정자 (해석은 컨트롤러의 #interrupt-cells에 의존)
 * 5. interrupt-map: 인터럽트 도메인 간 변환 (nexus 노드에서 사용)
 */

/* ===== GIC (ARM Generic Interrupt Controller) ===== */
gic: interrupt-controller@1c81000 {
    compatible = "arm,gic-400";
    interrupt-controller;
    #interrupt-cells = <3>;
    /* 셀 해석:
     * [0] type: 0=SPI(Shared), 1=PPI(Private Per-Processor)
     * [1] irq number: SPI=0~987, PPI=0~15 (GIC HW IRQ = SPI+32, PPI+16)
     * [2] flags: 1=rising edge, 2=falling edge, 4=level high, 8=level low */
    reg = <0x1c81000 0x1000>,
          <0x1c82000 0x2000>;
};

/* ===== GPIO 인터럽트 컨트롤러 (계층적) ===== */
gpio0: gpio@1c20800 {
    compatible = "myvendor,my-soc-gpio";
    reg = <0x1c20800 0x40>;
    /* GPIO 컨트롤러이면서 인터럽트 컨트롤러 */
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    /* 셀 해석: [0] GPIO 핀 번호, [1] 트리거 타입 (IRQ_TYPE_*) */

    /* 이 GPIO 컨트롤러의 인터럽트가 GIC로 전달됨 */
    interrupt-parent = <&gic>;
    interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
};

/* GPIO 핀을 인터럽트로 사용하는 디바이스 */
button@0 {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;     /* GIC가 아닌 GPIO 컨트롤러로! */
    interrupts = <7 IRQ_TYPE_EDGE_FALLING>;  /* GPIO 핀 7, 하강 에지 */
};

/* ===== interrupt-map (Nexus 노드) =====
 *
 * PCI, USB 등의 버스에서 디바이스 인터럽트를 부모 컨트롤러로 변환합니다.
 * nexus 노드: interrupt-controller는 아니지만 interrupt-map으로 변환 수행
 */
pcie@10000000 {
    /* PCI 인터럽트: INTA=1, INTB=2, INTC=3, INTD=4 */
    #interrupt-cells = <1>;

    /* interrupt-map-mask: 매칭에 사용할 비트 마스크
     * PCI 주소(3 cells) + 인터럽트(1 cell) 총 4 cells
     * device 번호(bit 15:11)와 인터럽트 번호만 매칭 */
    interrupt-map-mask = <0xf800 0 0 7>;

    /* interrupt-map: (child_unit_addr  child_irq  parent  parent_irq)
     * child_unit_addr: #address-cells 만큼의 셀 (AND mask 적용 후 비교)
     * child_irq: #interrupt-cells 만큼의 셀
     * parent: phandle → 부모 인터럽트 컨트롤러
     * parent_irq: 부모의 #interrupt-cells 만큼의 셀 */
    interrupt-map =
        /* Device 0, INTA → GIC SPI 100 */
        <0x0000 0 0 1  &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTB → GIC SPI 101 */
        <0x0000 0 0 2  &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTC → GIC SPI 102 */
        <0x0000 0 0 3  &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTD → GIC SPI 103 */
        <0x0000 0 0 4  &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 1, INTA → GIC SPI 104 (rotation: INTB부터 시작) */
        <0x0800 0 0 1  &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 2  &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 3  &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 4  &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;

    /* PCI 인터럽트 회전(swizzle):
     * IRQ = (device_slot + interrupt_pin - 1) % 4 + 1
     * 이를 통해 여러 디바이스의 인터럽트가 4개 GIC IRQ에 분산 */
};

/* ===== interrupts-extended: 여러 컨트롤러의 인터럽트를 한 노드에서 사용 ===== */
my-device {
    /* interrupt-parent + interrupts는 하나의 컨트롤러만 가능.
     * interrupts-extended는 여러 컨트롤러의 인터럽트를 지정 가능 */
    interrupts-extended =
        <&gic GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>,    /* GIC에서 오는 인터럽트 */
        <&gpio0 7 IRQ_TYPE_EDGE_FALLING>;           /* GPIO에서 오는 인터럽트 */
    interrupt-names = "data-irq", "wakeup-irq";
};

/* ===== 커널 인터럽트 도메인 API (drivers/irqchip/) ===== */
#include <linux/irqdomain.h>

/* irq_domain: HW IRQ 번호 → Linux virq(가상 IRQ) 번호 매핑
 * 각 인터럽트 컨트롤러가 자신의 도메인을 등록
 * DT의 interrupts 값이 HW IRQ로, irq_domain을 통해 Linux IRQ로 변환 */

struct irq_domain *domain;
domain = irq_domain_add_linear(np, nr_irqs, &my_domain_ops, priv);
/* linear: HW IRQ → virq 직접 테이블 매핑 (소규모)
 * hierarchy: 계층적 도메인 (GIC→GPIO 등 cascaded 구조) */

domain = irq_domain_create_hierarchy(parent_domain, 0, nr_irqs,
    of_fwnode_handle(np), &my_domain_ops, priv);
/* hierarchy 도메인: 인터럽트 처리가 여러 컨트롤러를 거침
 * button → GPIO IRQ 7 → GIC SPI 11 → CPU
 * 각 단계의 도메인이 HW IRQ를 변환 */

Pinctrl 서브시스템과 DT 연동

SoC의 핀 다중화(muxing)와 전기적 설정을 DT에서 선언합니다. 드라이버의 probe() 시 자동으로 pinctrl-0이 적용됩니다.

/* ===== 핀 컨트롤러 노드 (SoC .dtsi) ===== */
pio: pinctrl@1c20800 {
    compatible = "myvendor,my-soc-pinctrl";
    reg = <0x1c20800 0x400>;
    clocks = <&ccu CLK_APB1>;

    /* UART0 핀 그룹 정의 */
    uart0_pins: uart0-pins {
        pins = "PA4", "PA5";       /* TX, RX */
        function = "uart0";         /* 핀 기능 선택 (mux) */
        drive-strength = <10>;     /* mA 단위 출력 세기 */
        bias-pull-up;                /* 풀업 활성화 */
    };

    uart0_sleep_pins: uart0-sleep-pins {
        pins = "PA4", "PA5";
        function = "gpio_in";       /* sleep 시 GPIO 입력으로 */
        bias-disable;
    };

    /* I2C0 핀 그룹 */
    i2c0_pins: i2c0-pins {
        pins = "PA11", "PA12";    /* SDA, SCL */
        function = "i2c0";
        drive-strength = <10>;
        bias-pull-up;
    };

    /* SPI0 핀 그룹 + CS */
    spi0_pins: spi0-pins {
        pins = "PC0", "PC1", "PC2", "PC3"; /* CLK, MOSI, MISO, CS */
        function = "spi0";
        drive-strength = <10>;
    };

    /* GPIO 키 (외부 풀업, 내부 바이어스 없음) */
    key_pins: key-pins {
        pins = "PG7";
        function = "gpio_in";
        bias-disable;
    };
};

/* ===== 디바이스에서 pinctrl 참조 ===== */
&uart0 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&uart0_pins>;        /* "default" 상태 (probe 시 적용) */
    pinctrl-1 = <&uart0_sleep_pins>;   /* "sleep" 상태 (suspend 시 적용) */
    status = "okay";
};

/* pinctrl-names와 pinctrl-N은 순서 대응:
 * pinctrl-names[0] = "default" → pinctrl-0
 * pinctrl-names[1] = "sleep"   → pinctrl-1
 *
 * 커널 PM 시스템이 suspend/resume 시 자동으로 상태 전환:
 * probe → "default", suspend → "sleep", resume → "default"
 *
 * "init" 상태: probe 중에만 사용, probe 완료 후 "default"로 전환 */

/* ===== 핀 설정 바인딩 주요 프로퍼티 (vendor-independent) ===== */
/*
 * pins:            핀 이름 목록
 * groups:          핀 그룹 이름 (대체)
 * function:        핀 기능 (mux 선택)
 * bias-disable:    바이어스 없음
 * bias-pull-up:    내부 풀업 활성화
 * bias-pull-down:  내부 풀다운 활성화
 * drive-strength:  출력 드라이브 세기 (mA)
 * input-enable:    입력 활성화
 * output-high:     출력 High로 설정
 * output-low:      출력 Low로 설정
 * slew-rate:       슬루율 (0=slow, 1=fast)
 */

IOMMU와 DMA 관련 DT 프로퍼티

/* ===== IOMMU (I/O Memory Management Unit) DT 바인딩 ===== */

/* IOMMU 컨트롤러 노드 */
smmu: iommu@12c00000 {
    compatible = "arm,smmu-v2";
    reg = <0x12c00000 0x10000>;
    #iommu-cells = <1>;              /* 자식이 참조 시 stream ID 1개 */
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
};

/* DMA를 수행하는 디바이스에서 IOMMU 참조 */
gpu@12000000 {
    compatible = "vendor,my-gpu";
    reg = <0x12000000 0x10000>;
    iommus = <&smmu 0x100>;          /* SMMU stream ID = 0x100 */
    /* 커널이 자동으로 IOMMU 도메인을 설정하여 DMA 주소 변환 수행 */
};

ethernet@1c30000 {
    compatible = "vendor,my-eth";
    reg = <0x1c30000 0x10000>;
    iommus = <&smmu 0x200>;          /* 다른 stream ID */
};

/* ===== DMA 관련 프로퍼티 ===== */

my-device@1000 {
    compatible = "vendor,my-dev";

    /* dma-coherent: 하드웨어가 캐시 코히어런시 보장
     * → 커널이 수동 캐시 flush/invalidate 생략 (성능 향상) */
    dma-coherent;

    /* dma-ranges가 부모에 있으면 DMA 주소 ≠ CPU 주소 */

    /* DMA 컨트롤러 참조 (slave DMA 사용 시) */
    dmas = <&dma_controller 5>, <&dma_controller 6>;
    dma-names = "tx", "rx";
};

/* DMA 컨트롤러 노드 */
dma_controller: dma-controller@1c02000 {
    compatible = "myvendor,my-soc-dma";
    reg = <0x1c02000 0x1000>;
    interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>;
    #dma-cells = <1>;              /* 자식 참조 시 채널 번호 1개 */
    clocks = <&ccu CLK_DMA>;
    resets = <&ccu RST_DMA>;
};

/* ===== 커널에서 DMA 채널 가져오기 ===== */
#include <linux/dmaengine.h>

struct dma_chan *tx_chan, *rx_chan;
tx_chan = dma_request_chan(dev, "tx");  /* dma-names의 "tx"에 대응하는 채널 */
rx_chan = dma_request_chan(dev, "rx");  /* dma-names의 "rx"에 대응하는 채널 */

/* restricted-dma-pool: 특정 디바이스용 DMA 메모리 제한 */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    gpu_dma_pool: dma-pool@90000000 {
        compatible = "restricted-dma-pool";
        reg = <0x0 0x90000000 0x0 0x10000000>;  /* 256MiB */
    };
};

gpu@12000000 {
    memory-region = <&gpu_dma_pool>;  /* 이 디바이스의 DMA는 이 영역만 사용 */
};

Thermal-zones DT 바인딩

/* ===== SoC 온도 센서와 쿨링 제어를 DT에서 정의 ===== */

/* 온도 센서 노드 */
tsensor: thermal-sensor@1c25000 {
    compatible = "myvendor,my-soc-thermal";
    reg = <0x1c25000 0x400>;
    #thermal-sensor-cells = <1>;   /* 센서 인덱스 1개 (다중 존) */
    clocks = <&ccu CLK_THS>;
    resets = <&ccu RST_THS>;
};

/* 쿨링 디바이스: CPU freq 스로틀링 */
/* cpu 노드에 #cooling-cells = <2>; 추가 필요 */
&cpu0 {
    #cooling-cells = <2>;  /* min_state, max_state */
};

/* thermal-zones 노드 */
thermal-zones {
    /* 각 zone = 센서 + 트립 포인트 + 쿨링 맵 */
    cpu-thermal {
        polling-delay-passive = <250>;  /* 트립 후 폴링 주기 (ms) */
        polling-delay = <1000>;          /* 평상시 폴링 주기 (ms) */

        thermal-sensors = <&tsensor 0>;  /* 센서 0번 (CPU zone) */

        trips {
            /* 패시브 쿨링: CPU freq 스로틀 시작 */
            cpu_alert: cpu-alert {
                temperature = <75000>;  /* 75°C (밀리도) */
                hysteresis = <2000>;    /* 73°C에서 해제 */
                type = "passive";
            };
            /* 크리티컬: 시스템 셧다운 */
            cpu_crit: cpu-critical {
                temperature = <100000>; /* 100°C */
                hysteresis = <0>;
                type = "critical";
            };
            /* 핫: 능동 쿨링(팬) 시작 */
            cpu_hot: cpu-hot {
                temperature = <85000>;  /* 85°C */
                hysteresis = <5000>;
                type = "hot";
            };
        };

        cooling-maps {
            /* 75°C 이상: CPU freq 스로틀 (state 0~최대) */
            cpu-throttle {
                trip = <&cpu_alert>;
                cooling-device = <&cpu0
                    THERMAL_NO_LIMIT       /* min state */
                    THERMAL_NO_LIMIT>;    /* max state */
            };
            /* 85°C 이상: 팬 활성화 */
            fan-cooling {
                trip = <&cpu_hot>;
                cooling-device = <&fan0 0 3>;  /* 팬 레벨 0~3 */
            };
        };
    };

    gpu-thermal {
        polling-delay-passive = <250>;
        polling-delay = <1000>;
        thermal-sensors = <&tsensor 1>;  /* 센서 1번 (GPU zone) */

        trips {
            gpu_alert: gpu-alert {
                temperature = <80000>;
                hysteresis = <2000>;
                type = "passive";
            };
        };
    };
};

/* 팬 제어용 PWM 쿨링 디바이스 */
fan0: pwm-fan {
    compatible = "pwm-fan";
    pwms = <&pwm0 0 25000>;            /* PWM 채널 0, 25kHz */
    #cooling-cells = <2>;
    cooling-levels = <0 64 128 255>;  /* state 0~3의 PWM duty */
};

/* sysfs 확인 */
/* /sys/class/thermal/thermal_zone0/temp     → 현재 온도 */
/* /sys/class/thermal/thermal_zone0/type     → "cpu-thermal" */
/* /sys/class/thermal/thermal_zone0/trip_point_0_temp → 75000 */
/* /sys/class/thermal/cooling_device0/cur_state → 현재 쿨링 레벨 */

전력 도메인 (Power Domain) DT

/* ===== 전력 도메인: SoC 내 독립적으로 전원을 제어할 수 있는 영역 =====
 *
 * SoC 설계에서 GPU, DSP, ISP 등은 별도 전력 도메인에 배치되어
 * 사용하지 않을 때 완전히 전원을 차단(power gating)할 수 있습니다.
 * DT에서 이 관계를 선언하면 커널 PM 시스템이 자동 관리합니다.
 */

/* 전력 도메인 컨트롤러 (PMU, Power Management Unit) */
pmu: power-controller@1c20000 {
    compatible = "myvendor,my-soc-power";
    reg = <0x1c20000 0x100>;
    #power-domain-cells = <1>;    /* 도메인 인덱스 1개 */

    /* 서브노드 형태도 가능 (일부 SoC) */
    pd_gpu: power-domain@0 {
        reg = <0>;
        #power-domain-cells = <0>;
        clocks = <&ccu CLK_GPU>;
        resets = <&ccu RST_GPU>;
    };
    pd_dsp: power-domain@1 {
        reg = <1>;
        #power-domain-cells = <0>;
    };
};

/* 디바이스에서 전력 도메인 참조 */
gpu@12000000 {
    compatible = "vendor,my-gpu";
    reg = <0x12000000 0x10000>;

    /* 방법 1: 인덱스 기반 (#power-domain-cells = <1>) */
    power-domains = <&pmu 0>;       /* 도메인 0 = GPU */

    /* 방법 2: 서브노드 phandle (#power-domain-cells = <0>) */
    /* power-domains = <&pd_gpu>; */

    power-domain-names = "gpu";
};

/* 여러 전력 도메인에 걸친 디바이스 */
isp@14000000 {
    compatible = "vendor,my-isp";
    reg = <0x14000000 0x10000>;
    power-domains = <&pmu 2>, <&pmu 3>;
    power-domain-names = "isp-core", "isp-io";
};

/* ===== 커널에서 전력 도메인 관리 =====
 *
 * Runtime PM과 연동:
 * - pm_runtime_get_sync() → 전력 도메인 ON (참조 카운트 기반)
 * - pm_runtime_put()      → 전력 도메인 OFF (모든 사용자가 put하면)
 *
 * 커널 내부 흐름:
 * 1. DT 파싱 → genpd(Generic Power Domain) 구조체 생성
 * 2. pm_genpd_add_device() → 디바이스를 도메인에 연결
 * 3. dev_pm_domain_attach() → probe 시 자동 호출
 * 4. Runtime PM 콜백에서 genpd_power_on/off() 자동 호출
 *
 * 디버깅:
 * $ cat /sys/kernel/debug/pm_genpd/pm_genpd_summary
 *   domain                status  /device          runtime status
 *   gpu_pd                on      /12000000.gpu    active
 *   dsp_pd                off
 */

Device Tree vs ACPI 비교

항목Device Tree (DT)ACPI
기원Open Firmware (IEEE 1275), PowerPC/SPARCIntel, x86 서버/데스크톱
주요 플랫폼ARM, RISC-V, PowerPC, MIPSx86, ARM 서버 (SBSA)
데이터 형식DTS(텍스트) → DTB(바이너리), 정적 데이터ASL(텍스트) → AML(바이코드), 실행 가능 메서드 포함
하드웨어 기술선언적 (데이터만)선언적 + 절차적 (AML 메서드 실행 가능)
런타임 수정Overlay (.dtbo, configfs)동적 테이블 로드 (SSDT), hotplug
전원 관리DT 프로퍼티 + 커널 드라이버에서 직접 구현_PS0/_PS3 메서드, _PR0 등 펌웨어가 전원 제어 수행
인터럽트interrupts, interrupt-map_CRS(Current Resource Settings) 내 IRQ 디스크립터
열 관리thermal-zones DT 노드_TMP, _PSV, _CRT, _ACx 메서드
디바이스 식별compatible 문자열_HID (Hardware ID), _CID (Compatible ID)
리소스 기술reg, interrupts, clocks 등 개별 프로퍼티_CRS 버퍼에 Memory32/IRQ/DMA 리소스 패킹
커널 APIof_*() (DT 전용)acpi_*() (ACPI 전용)
통합 APIdevice_property_*() / fwnode_*() — DT/ACPI 양쪽 지원
바인딩 문서Documentation/devicetree/bindings/ (YAML)ACPI Spec + DSDT/SSDT (벤더 구현)
검증 도구dt_binding_check, dtbs_checkiasl (Intel ASL Compiler), acpidump
fwnode API — DT/ACPI 통합 드라이버: DT와 ACPI 양쪽을 지원하는 드라이버를 작성할 때는 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>;
    };
};
DTS 읽기 연습 팁: 커널 소스의 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";
이유: 커널은 compatible 리스트를 앞에서부터 순회하며 매칭을 시도합니다. 가장 구체적인 모델을 먼저 나열해야 해당 모델에 특화된 드라이버가 바인딩됩니다.

❌ 실수 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"

성능 저하 원인:

런타임 메모리 사용 최적화

# struct device_node 메모리 사용량 추정
# (노드 개수 × sizeof(device_node) + 프로퍼티 메모리)
cat /proc/meminfo | grep DeviceTree
# DeviceTree:        512 kB

# /proc/device-tree/ procfs 오버헤드 비활성화 (선택)
# CONFIG_PROC_DEVICETREE=n 설정 시 메모리 절약 (디버깅 불편)

of_find_node 캐싱 패턴

/* ❌ 비효율적: 반복 탐색 */
static int my_function(void) {
    struct device_node *np;

    for (int i = 0; i < 100; i++) {
        np = of_find_node_by_path("/soc/i2c@1000");
        /* 매번 트리 탐색 발생 */
        of_node_put(np);
    }
}

/* ✅ 효율적: 한 번만 탐색 후 캐싱 */
struct my_driver_data {
    struct device_node *i2c_node;
};

static int my_probe(struct platform_device *pdev) {
    struct my_driver_data *data = dev_get_drvdata(&pdev->dev);

    data->i2c_node = of_find_node_by_path("/soc/i2c@1000");
    /* probe 시 한 번만 탐색 */
}

static int my_remove(struct platform_device *pdev) {
    struct my_driver_data *data = dev_get_drvdata(&pdev->dev);
    of_node_put(data->i2c_node);  /* 참조 카운트 해제 */
}

실전 케이스 스터디

실제 하드웨어를 위한 DTS 작성부터 드라이버 연동까지 단계별로 따라해봅니다.

케이스 1: I2C 온도 센서 추가 (TMP102)

시나리오: TMP102 I2C 온도 센서를 I2C 버스 1, 주소 0x48에 연결한 경우

1단계: 하드웨어 정보 수집

# 데이터시트에서 확인할 정보:
# - I2C 주소: 0x48 (ADD0=GND)
# - 인터럽트 핀: ALERT (옵션)
# - 전원: VCC 1.4V-3.6V

2단계: 커널 드라이버 확인

# drivers/hwmon/lm75.c가 TMP102 지원 (compatible 확인)
git grep -n "ti,tmp102" drivers/hwmon/
# drivers/hwmon/lm75.c:123:  { .compatible = "ti,tmp102" },

# 바인딩 문서 확인
cat Documentation/devicetree/bindings/hwmon/lm75.txt

3단계: DTS 작성

/* myboard.dts */
&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  /* 100kHz */

    tmp102: temperature-sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
        /* 옵션: 인터럽트 사용 시 */
        interrupt-parent = <&gpio1>;
        interrupts = <10 IRQ_TYPE_EDGE_FALLING>;
        /* 옵션: 전원 레귤레이터 연결 */
        vcc-supply = <®_3v3>;
    };
};

4단계: 컴파일 및 검증

# DTS 컴파일
make dtbs

# 보드에 DTB 배포 후 부팅
# dmesg에서 드라이버 로딩 확인
dmesg | grep lm75
# [    2.345678] lm75 1-0048: hwmon0: sensor 'tmp102'

# sysfs에서 온도 읽기
cat /sys/class/hwmon/hwmon0/temp1_input
# 25000 (섭씨 25도)

# device tree 노드 확인
ls -l /sys/firmware/devicetree/base/soc/i2c@*/temperature-sensor@48/

케이스 2: GPIO LED 추가

시나리오: GPIO5 핀에 연결된 LED (Active Low)

/* myboard.dts */
/ {
    leds {
        compatible = "gpio-leds";

        status_led: led-status {
            label = "status";
            gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
            default-state = "on";
            linux,default-trigger = "heartbeat";
        };

        disk_led: led-disk {
            label = "disk";
            gpios = <&gpio0 6 GPIO_ACTIVE_HIGH>;
            linux,default-trigger = "disk-activity";
        };
    };
};

/* 검증 */
# LED 수동 제어
echo 0 > /sys/class/leds/status/brightness   # OFF
echo 255 > /sys/class/leds/status/brightness # ON (최대)
echo timer > /sys/class/leds/status/trigger
echo 500 > /sys/class/leds/status/delay_on    # 500ms ON
echo 500 > /sys/class/leds/status/delay_off   # 500ms OFF

케이스 3: SPI LCD 디스플레이 (ILI9341)

시나리오: SPI0에 연결된 2.4" TFT LCD (320x240, ILI9341 컨트롤러)

/* myboard.dts */
&spi0 {
    status = "okay";

    display: display@0 {
        compatible = "ilitek,ili9341";
        reg = <0>;  /* CS0 */
        spi-max-frequency = <32000000>;  /* 32MHz */

        /* DC (Data/Command), Reset, LED backlight GPIO */
        dc-gpios = <&gpio0 24 GPIO_ACTIVE_HIGH>;
        reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;
        led-gpios = <&gpio0 18 GPIO_ACTIVE_HIGH>;

        rotation = <90>;  /* 화면 회전 */
        bgr;           /* BGR 픽셀 순서 (RGB 아님) */

        /* 디스플레이 해상도 */
        width = <320>;
        height = <240>;
        buswidth = <8>;
        fps = <30>;
    };
};

/* 검증 */
# fbdev 드라이버 로딩 확인
dmesg | grep fb
# [    3.456789] fb0: ili9341 frame buffer, 320x240, 150 KiB video memory

# 프레임버퍼 테스트 (흰색 화면)
cat /dev/zero > /dev/fb0

케이스 4: Pinctrl 설정 (UART + 흐름 제어)

시나리오: UART0를 RTS/CTS 하드웨어 흐름 제어와 함께 사용

/* my-soc.dtsi — 핀 컨트롤러 정의 */
pinctrl: pinctrl@1000 {
    compatible = "myvendor,pinctrl";
    reg = <0x1000 0x100>;

    uart0_default: uart0-default-state {
        tx-rx {
            pins = "gpio14", "gpio15";
            function = "uart0";
            bias-disable;
        };
    };

    uart0_rts_cts: uart0-rts-cts-state {
        tx-rx {
            pins = "gpio14", "gpio15";
            function = "uart0";
        };
        rts-cts {
            pins = "gpio16", "gpio17";
            function = "uart0";
            bias-pull-up;
        };
    };
};

/* myboard.dts — 보드별 활성화 */
&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_rts_cts>;  /* RTS/CTS 사용 */
    uart-has-rtscts;
    status = "okay";
};

문제 해결 FAQ

Device Tree 관련 자주 발생하는 문제와 해결 방법입니다.

Q1: 드라이버가 probe되지 않습니다

증상: dmesg에 드라이버 로딩 메시지가 없고, /sys/bus/platform/drivers/에 디바이스가 바인딩되지 않음

체크리스트:

  1. compatible 문자열 확인
    # 드라이버의 of_device_id 확인
    git grep -A3 "of_device_id.*my_device" drivers/
    # DTS의 compatible과 정확히 일치해야 함
  2. status 프로퍼티 확인
    cat /proc/device-tree/soc/mydevice@*/status
    # "okay" 또는 "ok"여야 함 (disabled면 probe 안 됨)
  3. 드라이버 모듈 로딩 확인
    lsmod | grep my_driver
    modprobe my_driver  # 수동 로딩 시도
  4. 디바이스 등록 확인
    ls /sys/bus/platform/devices/ | grep mydevice
    # 노드가 platform_device로 등록되었는지 확인
  5. probe 실패 로그 확인
    dmesg | grep -i "mydevice\|probe\|fail"
    # EPROBE_DEFER, 리소스 부족, 의존성 문제 등 확인

Q2: EPROBE_DEFER가 계속 발생합니다

증상: dmesg | grep defer에서 같은 디바이스가 반복적으로 defer됨

[    5.123456] my_device 10000.mydev: probe deferred
[    6.234567] my_device 10000.mydev: probe deferred
# ... 계속 반복

원인: 의존하는 리소스(클럭, 레귤레이터, GPIO 등)의 드라이버가 로딩되지 않음

해결:

# 1. 의존성 확인 (DTS에서 phandle 참조 추적)
cat /proc/device-tree/soc/mydevice@*/clocks  # 바이너리 출력
hexdump -C /proc/device-tree/soc/mydevice@*/clocks

# 2. 클럭/레귤레이터 드라이버 로딩 확인
ls /sys/class/clk/
ls /sys/class/regulator/

# 3. 드라이버 로딩 순서 조정 (Makefile의 obj-y 순서 또는 initcall 우선순위)
# 또는 의존 드라이버를 built-in으로 변경 (=y)

Q3: 인터럽트가 동작하지 않습니다

디버깅 단계:

# 1. 인터럽트 등록 확인
cat /proc/interrupts | grep mydevice
# 출력 없으면 request_irq() 실패

# 2. 인터럽트 번호 확인
# DTS의 interrupts 프로퍼티와 드라이버에서 받은 IRQ 번호 비교
dmesg | grep "IRQ.*mydevice"

# 3. 인터럽트 컨트롤러 확인
cat /proc/device-tree/soc/mydevice@*/interrupt-parent
# phandle 값 확인 후, 해당 노드 찾기

# 4. #interrupt-cells 확인
cat /proc/device-tree/interrupt-controller@*/\#interrupt-cells
# DTS의 interrupts 지정자 개수와 일치해야 함

# 5. 하드웨어 트리거 타입 확인 (실제 HW 동작과 일치해야 함)
# IRQ_TYPE_EDGE_RISING/FALLING/LEVEL_HIGH/LEVEL_LOW

Q4: 디바이스에 접근하면 커널 패닉이 발생합니다

증상: Unable to handle kernel paging request at virtual address ...

원인: 잘못된 reg 주소 또는 주소 변환 오류

# 1. ioremap된 주소 확인
dmesg | grep ioremap
cat /proc/iomem | grep mydevice

# 2. DTS의 reg 주소가 데이터시트와 일치하는지 확인
dtc -I dtb -O dts /boot/myboard.dtb | grep -A2 "mydevice@"

# 3. ranges 프로퍼티 검증 (부모 버스의 주소 변환)
# reg 주소가 CPU 물리 주소인지, 버스 주소인지 확인

# 4. 클럭/전원 활성화 확인
# 클럭이 꺼져있으면 레지스터 접근 시 버스 에러 발생 가능

Q5: Device Tree Overlay가 적용되지 않습니다

검증:

# ConfigFS를 통한 overlay 적용
mount -t configfs none /sys/kernel/config
mkdir /sys/kernel/config/device-tree/overlays/my_overlay
cat my_overlay.dtbo > /sys/kernel/config/device-tree/overlays/my_overlay/dtbo

# 적용 상태 확인
cat /sys/kernel/config/device-tree/overlays/my_overlay/status
# "applied" 출력되어야 함

# 에러 발생 시
dmesg | tail -20
# OF: overlay: apply failed 'xxx'
# → fragment target 노드가 존재하지 않거나 phandle 불일치

# overlay fragment target 확인
dtc -I dtb -O dts my_overlay.dtbo | grep "target ="

Q6: "clock not found" 에러가 발생합니다

# 증상
dmesg | grep "clock"
# [    2.345678] mydevice: failed to get clock 'apb': -2 (ENOENT)

# 해결 1: clock-names 확인
# DTS의 clock-names와 드라이버의 clk_get() 이름이 일치해야 함

# DTS:
clocks = <&ccu CLK_APB>, <&ccu CLK_MOD>;
clock-names = "apb", "mod";  /* 순서 일치 필수 */

# 드라이버:
clk = devm_clk_get(&pdev->dev, "apb");  /* "apb" 일치 */

# 해결 2: 클럭 프로바이더 드라이버 로딩 확인
ls /sys/kernel/debug/clk/  # (CONFIG_DEBUG_FS 필요)
추가 디버깅 리소스:
  • /sys/firmware/devicetree/base/ — 런타임 DT 트리 탐색
  • /sys/devices/platform/ — platform_device 목록
  • /proc/device-tree/ — 심볼릭 링크 (레거시, /sys/firmware/devicetree/base/ 권장)
  • dtc -I fs /proc/device-tree/ — 런타임 DT를 DTS로 재구성
  • scripts/dtc/dt-validate — YAML 스키마 검증 도구

Device Tree 검증 플레이북

Device Tree 문제는 "문법은 맞는데 런타임에서 바인딩 실패"하는 경우가 많습니다. 따라서 정적 검증(dtbs_check)과 런타임 검증(/sys/firmware/devicetree/base)을 모두 수행해야 합니다.

  1. 문법 검증: dtc 경고/오류 제거
  2. 스키마 검증: 바인딩 YAML과 속성 일치 확인
  3. 런타임 트리 확인: 실제 로드된 노드/프로퍼티 확인
  4. 드라이버 바인딩 확인: compatible 매칭과 probe 로그 확인
# 정적 검증
make ARCH=arm64 dtbs
make ARCH=arm64 dtbs_check

# DTB 디컴파일로 결과 확인
dtc -I dtb -O dts -o out.dts arch/arm64/boot/dts/vendor/board.dtb

# 런타임 트리 확인
ls /sys/firmware/devicetree/base
grep -R "my,device" /sys/firmware/devicetree/base 2>/dev/null

# 드라이버 바인딩 확인
dmesg | grep -Ei "of:|probe|mydevice"
증상 원인 후보 대응
probe가 호출되지 않음 compatible 문자열 불일치 드라이버 of_match_table와 DTS 비교
irq/clock not found phandle, 이름, 순서 불일치 interrupts, clocks, *-names 동시 확인
overlay 적용 실패 fragment target 누락 target 경로/phandle 재검증, dmesg 원문 확인

Device Tree와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.