Device Tree

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

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

핵심 요약

  • 단계 분리 — 펌웨어(Firmware), 부트로더(Bootloader), 커널 초기화 경계를 구분합니다.
  • 하드웨어 기술 — ACPI/DT 등 기술 정보가 어디서 소비되는지 확인합니다.
  • 신뢰 체인(Chain of Trust) — 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 Controller)의 #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 디버깅(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            # 경고 활성화 빌드
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가 각 블록의 오프셋(Offset)을 지정
/* ===== 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 프로퍼티가 자식 주소 공간 → 부모 주소 공간으로의 변환 규칙을 정의합니다.

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의 인터럽트 계층은 디바이스 트리(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
전원 관리(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 리소스 패킹
커널 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 + 흐름 제어(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/에 디바이스가 바인딩되지 않음

체크리스트:

  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: 디바이스에 접근하면 커널 패닉(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)을 모두 수행해야 합니다.

  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 원문 확인

FDT 부팅 로드 흐름

펌웨어/부트로더가 DTB를 메모리에 로드하고, 커널이 이를 파싱하여 디바이스 트리를 구축하는 전체 과정을 살펴봅니다. 이 흐름을 이해하면 부팅 실패 시 어느 단계에서 문제가 발생했는지 빠르게 진단할 수 있습니다.

핵심 포인트: DTB는 물리 메모리(Physical Memory)의 특정 주소에 로드되며, 커널 진입점(Entry Point)에서 해당 주소가 레지스터(ARM64: x0, ARM32: r2)를 통해 전달됩니다. 커널은 이 주소를 기반으로 FDT를 검증하고, 2-pass 알고리즘으로 struct device_node 트리를 구축합니다.
FDT 부팅 로드 흐름 — 펌웨어에서 커널 디바이스 생성까지 Phase 1: 펌웨어 / 부트로더 U-Boot / TF-A DTB를 스토리지에서 로드 fdt_open_into() DTB 검증 + overlay 병합 bootz / booti 커널 + DTB 주소 전달 ARM64: x0=dtb ARM32: r2=dtb Phase 2: 커널 초기화 (early boot) setup_arch() fixmap으로 FDT 매핑 fdt_check_header() 검증 early_init_dt_scan() chosen → bootargs memory → memblock 등록 unflatten_device_tree() Pass 1: 메모리 크기 계산 Pass 2: device_node 할당 of_root 전역 device_node 루트 /proc/device-tree/ Phase 3: 디바이스 생성 (board_init) of_platform_default_populate() 루트 직속 자식 순회 simple-bus 재귀 탐색 of_device_alloc() platform_device 생성 reg → resource 변환 device_add() 버스에 디바이스 등록 compatible 매칭 시도 driver.probe() 매칭된 드라이버 호출 of_* API로 DT 접근 시간 순서: start_kernel() → setup_arch() → early_init_dt_scan() → unflatten_device_tree() → of_platform_default_populate() → driver probe() ※ I2C/SPI 버스 하위 디바이스는 해당 버스 드라이버의 probe()에서 별도로 of_register_child_devices() 호출
FDT 부팅 로드 흐름 — 펌웨어 DTB 로드부터 드라이버 probe까지 3단계
FDT 메모리 레이아웃과 부트로더 전달 부팅 시 물리 메모리 배치 물리 메모리 (DRAM) 커널 이미지 (Image/zImage) TEXT_OFFSET부터 로드 DTB (FDT Blob) 8-byte 정렬 필수 initrd / initramfs chosen 노드에 위치 기록 DTB 내부 구조 (부트로더가 로드한 상태) fdt_header (40 bytes) — magic=0xD00DFEED, totalsize, offsets Memory Reservation Block — 커널이 건드리면 안 되는 영역 Structure Block — FDT_BEGIN_NODE / FDT_PROP / FDT_END_NODE 토큰 스트림 노드 이름 inline, 프로퍼티 데이터 inline, 이름은 nameoff로 참조 Strings Block — 프로퍼티 이름 NUL 종료 문자열 테이블 Free Space (overlay 병합 시 확장 여유 — fdt_open_into() 할당) ← 부트로더가 로드 ← x0/r2 레지스터로 주소 전달
부팅 시 물리 메모리 배치 — 커널, DTB, initrd의 메모리 위치와 DTB 내부 구조
/* ===== 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)의 바인딩을 정확히 기술해야 드라이버가 올바른 클록을 획득하고 제어할 수 있습니다.

SoC 클록 트리 계층 구조 SoC 클록 트리 계층 구조 OSC 24MHz (외부 크리스탈) PLL_CPU (1.2GHz) PLL_PERIPH (600MHz) PLL_DDR (800MHz) PLL_VIDEO (297MHz) AHB DIV (/4) APB1 DIV (/2) UART MUX SPI MUX HDMI DIV GATE_EMAC GATE_USB GATE_UART0 GATE_UART1 GATE_SPI0 GATE_HDMI EMAC USB UART0 UART1 SPI0 HDMI OSC: 외부 발진기 PLL: Phase-Locked Loop (체배기) DIV: 분주기 MUX: 클록 소스 선택 GATE: 클록 온/오프 제어 — 전력 절감의 핵심
SoC 클록 트리 — OSC → PLL → DIV/MUX → GATE → 주변장치
/* ===== 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에서 주변장치까지 전원 레일 계층 구조 배터리 / DC 입력 (5V) PMIC (예: AXP803 / MAX77620 / TPS65910) DCDC1 3.3V (고전류) always-on DCDC2 0.9~1.1V (DVFS) CPU 코어 전원 LDO1 1.8V (저노이즈) 아날로그 회로 LDO2 2.8V 카메라 모듈 eMMC / SD vcc-supply = DCDC1 CPU Core cpu-supply = DCDC2 WiFi 모듈 vcc-supply = LDO1 카메라 센서 AVDD-supply = LDO2 USB PHY vcc-supply = DCDC1 DCDC: 벅 컨버터 (고효율, 고전류, 리플 있음) LDO: 리니어 레귤레이터 (저노이즈, 저전류, 낮은 효율)
전원 레일 계층 — PMIC DCDC/LDO에서 주변장치 전원 공급까지
/* ===== 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 컨트롤러와 인터럽트 연결 구조 GPIO/IRQ 컨트롤러 연결 구조 GIC (인터럽트 컨트롤러) #interrupt-cells = <3> GPIO 컨트롤러 A (pio) gpio-controller + interrupt-controller #gpio-cells = <2> (pin, flags) #interrupt-cells = <2> (pin, type) I2C GPIO Expander gpio-controller + interrupt-controller #gpio-cells = <2> interrupt-parent = <&pio> PCI 브리지 interrupt-map 사용 INTx → GIC SPI 매핑 interrupt-map-mask 필수 SPI 직접 연결 GPIO 경유 LED gpios = <&pio 7 0> 버튼 interrupts = <5 2> 터치스크린 gpios = <&expander 3 0> 리셋 제어 reset-gpios (active-low) PCIe 디바이스 interrupt-map 변환 GPIO_ACTIVE_HIGH=0, GPIO_ACTIVE_LOW=1 IRQ_TYPE: EDGE_RISING=1, EDGE_FALLING=2, EDGE_BOTH=3, LEVEL_HIGH=4 interrupt-map: 자식 인터럽트 → 부모 인터럽트 도메인 변환 테이블
GPIO/IRQ 컨트롤러 연결 — GPIO 컨트롤러가 인터럽트 컨트롤러를 겸하는 일반적 구조
/* ===== 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 컨트롤러 채널 매핑 DMA 컨트롤러 채널 매핑 DRAM (메모리) 소스 또는 목적지 DMA 컨트롤러 #dma-cells = <1> (request line ID) CH0: UART0_TX (req=6) CH1: UART0_RX (req=7) CH2: SPI0_TX (req=22) CH3: SPI0_RX (req=23) CH4: I2S_TX (req=15) ... 최대 N 채널 (SoC마다 다름) UART0 dmas: TX(6), RX(7) SPI0 dmas: TX(22), RX(23) I2S dmas: TX(15) request line: SoC 하드웨어에서 고정된 DMA 요청 신호 번호 (데이터시트 참조)
DMA 채널 매핑 — 메모리 ↔ 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 생명주기 — 로드에서 제거까지 Device Tree Overlay 생명주기 1. 로드 .dtbo 파일 읽기 request_firmware() 2. Resolve __fixups__ 처리 phandle 심볼 해석 3. Apply device_node 트리에 병합 platform_device 생성 4. Remove 디바이스 제거 changeset 롤백 __fixups__ / __symbols__ 메커니즘 기본 DTB: dtc -@ → __symbols__ 노드 생성 Overlay: __fixups__ → 기호 이름으로 phandle 참조 __local_fixups__ → overlay 내부 phandle 상대 오프셋 Resolve: __symbols__[name] → phandle 값 → __fixups__ 패치 적용 방법 비교 U-Boot: fdt apply — 부팅 전 병합 (가장 안전) configfs: /sys/kernel/config/device-tree/overlays/ 커널 API: of_overlay_fdt_apply() — 모듈에서 호출 Raspberry Pi: config.txt dtoverlay= 지시어 주의사항 런타임 제거(remove)는 모든 드라이버가 안전하게 분리 가능해야 함 — 일부 드라이버는 hot-remove를 지원하지 않아 시스템 불안정 유발 가능 LIFO 순서: 마지막에 적용한 overlay를 먼저 제거해야 함 (changeset 스택 구조)
Overlay 생명주기 — fixup/resolve 과정과 적용 방법 비교
/* ===== 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 바인딩을 제출할 때 이 검증을 통과해야 합니다.

dt-schema 검증 워크플로 dt-schema 검증 워크플로 바인딩 YAML Documentation/dt-bindings/ DTS 소스 arch/arm64/boot/dts/ dt_binding_check YAML 문법 + 스키마 자체 검증 dtbs_check DTB를 YAML 스키마에 대해 검증 PASS 바인딩 규격 준수 FAIL 누락/잘못된 프로퍼티 보고 검증 도구 체인 yamllint dt-doc-validate dt-validate dtc -Wno-* YAML 문법 검사 바인딩 스키마 구조 DTB ↔ 스키마 대조 DTS 컴파일 경고
dt-schema 검증 — YAML 바인딩 작성부터 DTB 검증까지의 도구 체인
# ===== 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.cof_find_*, of_property_read_*, of_node_get/put 핵심 API
drivers/of/fdt.cFDT 파싱, unflatten, early_init_dt_* 함수
drivers/of/platform.cof_platform_populate, platform_device 생성
drivers/of/address.c주소 변환 (of_translate_address, of_address_to_resource)
drivers/of/irq.c인터럽트 파싱, 도메인 해석
drivers/of/overlay.cOverlay 적용/제거, 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_device_tree() → platform_device 콜 체인 drivers/of/fdt.c drivers/of/platform.c unflatten_device_tree() 초기 DTB → device_node 트리 구축 (2-pass) pass 1: size unflatten_dt_nodes() FDT 노드 순회 · populate_node() 반복 호출 pass 2: alloc populate_node() device_node 할당 · full_name 복사 populate_properties() 로 property 연결 of_root 전역 포인터로 트리 완성 arch_initcall of_platform_default_populate() 루트 자식 노드 순회 시작 of_platform_bus_create() of_platform_device_create_pdata() 호출 of_root (전역 device_node 트리) /proc/device-tree/ · sysfs 노출 ① 2-pass: Pass1=메모리 크기 계산, Pass2=실제 device_node 할당 및 연결 ② compatible="simple-bus" 노드는 of_platform_bus_create에서 자식도 재귀 처리
unflatten_device_tree()에서 platform_device 생성까지 콜 체인 — fdt.c와 platform.c 경계

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행 _flagsOF_POPULATED(bit 3): 이 노드로부터 platform_device가 이미 생성됨을 표시. of_platform_device_create_pdata()에서 중복 생성 방지에 사용
  • 18행 datafull_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"처럼 다중 문자열 리스트의 각 항목과 순서대로 비교. 첫 번째 매칭 엔트리의 dataof_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_property_read_u32_array() 내부 흐름 of_find_property_value_of_size() np→properties 선형 탐색 이름 일치 + 크기 검증 IS_ERR(val)? -EINVAL / -ENODATA -EOVERFLOW be32_to_cpup() 루프 BE → CPU 엔디안 변환 out_values[] 배열 채움 FDT 바이너리 내 프로퍼티 값 레이아웃 (big-endian) property.value (raw bytes) 00 00 00 01 00 00 10 00 00 00 00 10 ... be32_to_cpup() out_values[] (CPU 엔디안 u32) 0x00000001 0x00001000 0x00000010 드라이버 사용 예: of_property_read_u32_array(np, "reg", vals, 2) → vals[0]=base_addr, vals[1]=size 단일 값: of_property_read_u32(np, "clock-frequency", &freq) — 배열 버전의 래퍼 (sz=1)
of_property_read_u32_array() 내부 흐름 — 프로퍼티 탐색, 크기 검증, big-endian 변환 3단계

of_platform_device_create_pdata() — device_node → platform_device 변환

of_platform_bus_create()가 각 DT 노드에 대해 호출하는 핵심 함수입니다. device_nodereg, 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의 reginterrupts를 드라이버가 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() 실패 시 이 경로로 진입

참고자료

공식 규격 및 표준

커널 문서

LWN 기사

커널 소스 코드

컨퍼런스 발표 및 기술 자료

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