Device Tree

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

전제 조건: 커널 아키텍처디바이스 드라이버 문서를 먼저 읽으세요. Device Tree는 드라이버가 하드웨어를 어떻게 찾는지의 입력이므로, 드라이버 구조를 먼저 이해하면 바인딩 규칙이 훨씬 명확해집니다.
일상 비유: Device Tree는 건물 전기 배선도와 같습니다. 전기 기사(드라이버)가 어떤 콘센트가 어디에 있고 몇 볼트짜리인지 알기 위해 배선도(DT)를 읽듯이, 커널 드라이버는 DT에서 하드웨어 주소·인터럽트·클록 정보를 읽어 초기화합니다.

핵심 요약

  • 노드(Node) — 하드웨어 컴포넌트 하나를 표현하는 중괄호 블록. uart0: uart@1c28000 { … }
  • 프로퍼티(Property) — 노드의 속성. reg(주소), interrupts, clocks 등 이름=값 쌍.
  • compatible — 드라이버 매칭 키. "allwinner,sun8i-h3-uart"처럼 "vendor,device" 형식.
  • phandle — 다른 노드를 가리키는 포인터. &ccu처럼 레이블로 참조하면 컴파일 시 정수로 변환됩니다.
  • status — 노드 활성화 여부. "okay"면 활성화, "disabled"면 드라이버 바인딩이 생략됩니다.

단계별 이해

  1. 루트 노드 파악
    / 루트 아래 cpus, memory, soc 등 최상위 구조를 먼저 확인합니다.
  2. 타깃 노드 찾기
    관심 있는 디바이스를 compatible 값으로 검색하거나, 주소(@1c28000)로 찾습니다.
  3. 프로퍼티 해석
    reg(메모리 맵(Memory Map) 주소), interrupts, clocks, pinctrl-0 프로퍼티를 차례로 읽습니다.
  4. phandle 참조 추적
    &ccu, &pio 같은 phandle 참조가 나오면 해당 레이블의 노드로 이동해 전체 연결을 파악합니다.
관련 표준: 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 (바이너리 블롭) → 부트로더(Bootloader)가 메모리에 로드 → 커널이 파싱하여 struct device_node 트리 구축 → 드라이버가 of_* API로 프로퍼티 조회

왜 Device Tree가 필요한가

PCI나 USB 버스는 연결된 장치를 런타임에 자동으로 열거(enumerate)할 수 있습니다. 그러나 SoC에 내장된 UART, I²C, SPI, GPIO 컨트롤러 같은 주변 장치들은 표준 열거 방법이 없으므로, 커널이 그 존재와 위치를 별도로 알아야 합니다.

board file 폭발 문제

Device Tree 도입 이전(2011년 이전), ARM 아키텍처는 각 보드마다 arch/arm/mach-xxx/board-yyy.c 파일을 따로 두고 하드웨어 정보를 C 코드로 하드코딩했습니다. 보드 종류가 수백 개에 달하자 커널 소스에 유사한 C 파일이 폭증했고, Linus Torvalds는 2011년 ARM 보드 파일에 강한 불만을 표명했습니다. 이를 계기로 ARM 메인테이너들이 Device Tree를 채택해 하드웨어 기술을 커널 코드에서 완전히 분리했습니다.

Before (board file 시대) After (Device Tree) Linux Kernel Source arch/arm/mach-*/board-*.c (수백 개) board-a.c board-b.c board-c.c … 하드웨어 변경 → 커널 재컴파일 필수 보드 추가 → C 파일 추가 Linux Kernel (플랫폼 독립 드라이버) drivers/… (of_* API로 DT 조회) board-a.dts .dtb board-b.dts .dtb board-c.dts … .dtb 동일 커널 이미지 하드웨어 변경 → DTB만 교체 보드 추가 → .dts 파일 추가
board file 방식(Before) vs. Device Tree 방식(After) — 커널 코드와 하드웨어 기술의 분리
항목board file 방식Device Tree 방식
하드웨어 기술 위치C 소스 (board-*.c)텍스트 파일 (.dts)
새 보드 추가C 파일 작성 + 커널 재컴파일.dts 작성 + dtc로 컴파일
커널 이미지보드별로 다름플랫폼 전체 공유 가능
하드웨어 변경C 패치(Patch) + 커널 빌드DTB만 교체
유지보수보드 수 × 파일 수 증가.dtsi 공통 파일 공유

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 처리 흐름 — 빌드, 부팅, 커널 파싱, 드라이버 매칭까지

노드·프로퍼티·phandle — 기초 개념

Device Tree는 노드(node)가 계층적으로 중첩된 트리 구조입니다. 루트 노드 / 아래에 cpus, memory, soc 같은 자식 노드들이 있고, soc 아래에는 실제 컨트롤러 노드들이 위치합니다.

/ 루트 노드 cpus CPU 클러스터 memory 물리 메모리 soc SoC 버스 영역 ccu@1c20000 클록 컨트롤러 uart0@1c28000 UART 컨트롤러 i2c0@1c2ac00 I²C 컨트롤러 sensor@48 I²C 디바이스 (온도센서) clocks = &ccu (phandle)
Device Tree 계층 구조 — 루트(/)부터 SoC 내부 컨트롤러까지, phandle 참조(점선)로 노드 간 연결

노드와 프로퍼티

노드 이름은 device-type@unit-address 형식입니다. @ 뒤의 단위 주소는 reg 프로퍼티의 첫 번째 값과 일치해야 합니다. 아래는 실제 Allwinner H3 SoC의 UART 노드를 단순화한 예시입니다.

/* 루트 노드 */
/ {
    #address-cells = <1>;   /* reg 주소 필드 개수 */
    #size-cells    = <1>;   /* reg 크기 필드 개수 */

    /* 클록 컨트롤러 노드 (레이블: ccu) */
    ccu: clock@1c20000 {
        compatible = "allwinner,sun8i-h3-ccu";
        reg = <0x01c20000 0x400>;   /* 주소, 크기 */
        #clock-cells = <1>;         /* clocks = <&ccu CLK_BUS_UART0> 형식 허용 */
    };

    soc {
        #address-cells = <1>;
        #size-cells    = <1>;

        /* UART0 노드 (레이블: uart0) */
        uart0: serial@1c28000 {
            compatible = "allwinner,sun8i-h3-uart",
                         "snps,dw-apb-uart";   /* 매칭 우선순위: 구체적 → 일반 */
            reg       = <0x01c28000 0x400>;
            interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
            clocks    = <&ccu CLK_BUS_UART0>;  /* phandle: &ccu 참조 */
            status    = "disabled";             /* 보드 파일에서 "okay"로 오버라이드 */
        };
    };
};

/* 보드 파일 (.dts) — 공통 SoC .dtsi를 include 후 오버라이드 */
&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pa_pins>;
    status = "okay";   /* 이 보드에서 UART0 활성화 */
};

phandle — 노드 간 참조

phandle은 한 노드가 다른 노드를 참조할 때 사용하는 포인터입니다. DTS에서는 &label 문법으로 참조하며, dtc가 컴파일 시 각 노드에 고유 정수를 할당해 phandle 프로퍼티로 저장합니다. 커널은 이 정수 값으로 노드를 역참조(Dereference)합니다.

DTS (소스) clocks = <&ccu CLK_BUS_UART0>; 레이블로 참조 dtc 컴파일 phandle = <3> 할당 DTB (바이너리) clocks = <0x3 CLK_BUS_UART0>; 정수 phandle로 저장 커널 of_parse_phandle() 정수 3 → struct device_node *ccu 포인터 반환
phandle 동작 원리 — DTS의 &label이 컴파일 시 정수로 변환되고, 커널 API가 해당 노드로 역참조
of_parse_phandle() 사용 예: 드라이버에서 of_parse_phandle(np, "clocks", 0)을 호출하면, clocks 프로퍼티의 첫 번째 phandle 값으로 참조된 struct device_node를 반환합니다. 이를 통해 UART 드라이버가 클록 컨트롤러 노드를 찾아 clk_get()을 호출할 수 있습니다.

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 = &eth0;
        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의 핀 다중화(Multiplexing)(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 등 펌웨어(Firmware)가 전원 제어 수행
인터럽트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)에서 해당 주소가 레지스터(Register)(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 바이너리 포인터 (물리 주소(Physical Address)에서 fixmap으로 매핑된 가상 주소(Virtual Address)), 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 연결 리스트(Linked List)에 추가

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) 조회 가능 (해시 테이블(Hash Table) 캐시(Cache))
  • 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 재적용 시 복원 가능한 체크포인트(Checkpoint) 역할
  • 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 네이티브 엔디안(Endianness)으로 변환
  • 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): 순회 시작 전 참조 카운트(Reference Count)를 증가시켜 다른 스레드(Thread)의 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에서는 바이트 스왑(Swap) 수행, big-endian 아키텍처에서는 NOP. FDT 명세상 모든 셀 값은 BE이므로 필수 변환
  • 22행EXPORT_SYMBOL_GPL: GPL 라이선스 드라이버만 이 함수를 사용 가능. 커널 모듈(Kernel Module)로 작성된 디바이스 드라이버는 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): 원자적(Atomic) 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() 실패 시 이 경로로 진입

최신 동향 — Overlay · dt-schema · 혼합 아키텍처 (2025-2026)

Device Tree 서브시스템은 2025~2026년 기간에 dt-schema 기반 검증 강화, configfs Overlay 안정화, RISC-V+ARM64 혼합 SoC의 DT 재구성으로 성숙 단계에 접어들었습니다.

영역변경비고
dt-schema / dtbs_check YAML 바인딩 비율 지속 상승, 신규 바인딩은 YAML 필수 make dtbs_check가 CI의 표준. dt-validate는 YAML만 지원
Overlay 안정화 /sys/kernel/config/device-tree/overlays/ configfs 기반 런타임 오버레이가 메인라인 안정 CONFIG_OF_OVERLAY + CONFIG_OF_CONFIGFS. Raspberry Pi, BeagleBone, Xilinx FPGA에서 실사용
v6.16 SG200X (RISC-V+ARM64) SG2042/cv18xx 계열 DTS 리팩터링, RISC-V+ARM64 혼합 SoC를 공통 DT로 정리 하나의 SoC 상위 DTS에서 두 아키텍처 노드 공존 — 향후 혼합 플랫폼 확대 대비
v6.19 ARM64 MPAM DT 바인딩 MPAM 드라이버와 함께 DT에서 PARTID 맵, MSC(Memory System Component) 기술이 표준화 서버·네트워크 QoS를 DT로 기술. x86 resctrl과 사용자 경로는 통일
v6.17 Snapdragon X 노트북 Dell XPS 13, HP Omnibook X14, Asus Zenbook A14의 DTS가 메인라인 병합 — 상용 ARM64 노트북 생태계 확대 Qualcomm SA8775P 자동차용 SoC(OPP 테이블·DSI·비디오 코덱)도 함께 추가
v6.17 Samsung Galaxy S22+ Exynos 2200(gs201) DTS 추가 — 플래그십 안드로이드 폰의 메인라인 DT 지원 확장 Exynos 990 watchdog/USB, Exynos 850 Ethernet 바인딩도 포함
v6.18 산업용 SoC (RZ/T2H·RZ/N2H) Renesas RZ/T2H(r9a09g077)·RZ/N2H(r9a09g087) Cortex-A55 산업용 SoC DT 추가 — 실시간(Real-time) 제어 플랫폼 지원 Xilinx Kria K24/KR260/KD240 보드도 동시 추가. 6.18은 LTS 커널
v6.18 ARM CPU C1 바인딩 예약 DT CPU 문자열에 arm,c1-nano·arm,c1-premium·arm,c1-pro·arm,c1-ultra 추가 ARM C1(차세대 CPU) 대응 DT 바인딩 사전 정의 — 신규 SoC 병합 준비
v7.0 Rust 드라이버 연동 Rust 커널 드라이버가 of_match_table을 안전 추상화로 사용 가능 Tyr(ARM Mali CSF GPU)가 대표 사례
v7.0 DT 바인딩 검증 정리 구형 ARM DT 파일의 바인딩 검증 오류 일괄 수정, Rockchip RK3576 PCIe·HDMI·UFS DT 추가 Amlogic·Qualcomm·RISC-V(Sophgo) 플랫폼도 DT 포맷 최적화 적용

configfs Overlay 권장 사용법

런타임 Overlay를 적용할 때 dtbo 파일을 configfs 서브디렉터리에 복사하는 방식은 대부분 메인라인에서 사용 가능합니다. 장치 트리(Device Tree) 변화는 즉시 of_platform_populate()가 반영하여 드라이버 probe를 트리거합니다.

# 1) configfs 마운트 (보통 자동)
mount -t configfs none /sys/kernel/config

# 2) 오버레이 디렉터리 생성 후 dtbo 로드
mkdir /sys/kernel/config/device-tree/overlays/my_sensor
cat my_sensor.dtbo > /sys/kernel/config/device-tree/overlays/my_sensor/dtbo

# 3) 적용 상태 확인
cat /sys/kernel/config/device-tree/overlays/my_sensor/status
# applied

# 4) 해제
rmdir /sys/kernel/config/device-tree/overlays/my_sensor
ACPI 병행 트렌드: RISC-V 서버(SG2042 등)는 v6.19부터 ACPI 부팅 경로가 본격화되어, 데이터센터 환경은 ACPI, 임베디드는 Device Tree로 역할이 분화되는 추세입니다. Device Tree는 여전히 SoC 임베디드의 사실상 표준이며, Rust 드라이버 확산과 혼합 아키텍처 SoC 등장으로 중요성이 유지됩니다.

신규 보드·플랫폼 DT 추가 (v6.14~v6.15)

v6.14(2025년 3월)와 v6.15(2025년 5월)는 특히 ARM64·RISC-V 영역에서 신규 보드 DTS가 대거 메인라인에 편입되었습니다. 주요 사례를 정리합니다.

커널보드·플랫폼SoC / 특이사항
v6.14 Qualcomm Snapdragon 8 Elite (SM8750) 프리미엄 모바일 SoC의 DT 초기 지원. Snapdragon AR2 (SAR2130P) 혼합 현실 플랫폼도 병합
v6.14 Raspberry Pi 5 디스플레이 파이프라인(Pipeline) BCM2712 기반 RPi 5의 디스플레이 파이프라인 DT 노드가 추가되어 GPU 기반 디스플레이 출력 가능
v6.14 Banana Pi BPI-F3 SpacemiT K1 RISC-V SoC 탑재 보드. RISC-V 보드의 메인라인 DT 편입 확산을 보여줌
v6.15 Google Pixel 6 Pro (gs101) Samsung Exynos Modem을 포함한 Google Tensor GS101 기반 플래그십 폰 DT 추가
v6.15 Orange Pi 5 Ultra Rockchip RK3588 기반 고성능 SBC. MNT Reform 2 노트북도 RK3588로 메인라인 지원
v6.15 Milk-V Jupiter ITX Sophgo SG2042 RISC-V 프로세서 기반 ITX 보드. RISC-V 데스크톱/서버 생태계 확장
v6.15 Morello CHERI (ARM64 실험 플랫폼) ARM64 기반 CHERI 능력 아키텍처(Capability Architecture) 연구 플랫폼이 노멀 모드 DT를 획득. 메모리 안전성 연구에 활용
v6.15 Allwinner A523 / T527 워치독·클럭 드라이버와 함께 DTS가 추가되어 임베디드 미디어 플랫폼 지원 확대

커널 v6.14~v6.15부터: 신규 바인딩은 YAML dt-schema 형식만 허용되며, 레거시 텍스트 바인딩 신규 추가는 거부됩니다. 위 보드들의 DTS도 모두 YAML 바인딩 검증을 통과해야 메인라인에 병합됩니다.

신규 보드·플랫폼 DT 추가 (v6.16~v6.18)

v6.16(2025년 7월)~v6.18 LTS(2025년 12월) 기간에 ARM64·RISC-V·산업용 SoC 영역에서 신규 DTS가 대거 메인라인에 편입되었습니다. 특히 Snapdragon X 기반 상용 노트북과 Samsung Galaxy 시리즈, Renesas 산업용 SoC가 눈에 띕니다.

커널보드·플랫폼SoC / 특이사항
v6.16 Radxa Cubie A5E, Avaota-A1, YuzukiHD Chameleon Allwinner 기반 SBC 다수 추가. Rockchip RK3562 레퍼런스 보드·산업용 RK3399 eval도 편입
v6.16 Qualcomm Snapdragon X Plus, IPQ5332 모바일·네트워크 플랫폼 지원 확대. SM8750 업데이트도 포함
v6.16 Raspberry Pi 5 PCIe RPi 5(BCM2712)의 PCIe 루트 컴플렉스 DT 노드 활성화 — NVMe 등 PCIe 장치 공식 지원 기반 마련
v6.17 Qualcomm SA8775P (자동차) 자동차용 SoC의 CPU OPP 테이블, L3 인터커넥트, DSI, 비디오 코덱 DT 노드 추가 — 커넥티드카 플랫폼 지원
v6.17 Dell XPS 13 · HP Omnibook X14 · Asus Zenbook A14 Snapdragon X 기반 상용 ARM64 노트북 3종 DTS 병합 — 상업용 ARM 노트북의 메인라인 지원 확산
v6.17 Samsung Galaxy S22+ (Exynos 2200, gs201) Exynos 2200 플래그십 SoC DTS 추가. Exynos 990 watchdog·USB, Exynos 850 Ethernet 바인딩도 포함
v6.17 NanoPi M5, PinePhone Pro 카메라 Rockchip 기반 SBC·스마트폰 추가. Firefly ROC-RK3588S-PC, Luckfox Omni3576도 포함
v6.18 Renesas RZ/T2H · RZ/N2H (산업용) Cortex-A55 기반 산업 제어 SoC(r9a09g077, r9a09g087) DT 추가 — 실시간 산업 자동화 플랫폼 지원
v6.18 Xilinx Kria K24 · KR260 · KD240 FPGA+ARM64 SoM(System-on-Module) 보드 DTS 추가. Xilinx 산업 FPGA 플랫폼의 메인라인 확장
v6.18 Samsung Galaxy S20 시리즈 Exynos990 기반 Galaxy S20 계열 DTS 추가 — 안드로이드 플래그십 폰 지원 확대
v6.18 RISC-V: OrangePi RV2, Milk-V Mars CM Lite SpacemiT·StarFive 기반 RISC-V 보드 메인라인 편입. eswin eic7700 SoC도 추가

커널 v6.18(LTS)부터: ARM CPU 차세대 C1 시리즈를 위한 DT 문자열(arm,c1-nano·arm,c1-premium·arm,c1-pro·arm,c1-ultra)이 예약 정의되었습니다. 차세대 SoC 출시 전 바인딩을 선행 등록하여 빠른 메인라인 진입을 준비합니다.

v6.19 — ARM MPAM DT 바인딩 상세

커널 6.19에서 ARM MPAM(Memory System Resource Partitioning and Monitoring) 드라이버가 메인라인에 병합되면서 관련 DT 바인딩이 표준화되었습니다. MPAM은 ARMv8.4부터 지원되는 기능으로, 캐시 및 메모리 대역폭(Bandwidth)을 PARTID(Partition ID) 단위로 분리하여 데이터센터·클라우드 환경의 QoS를 하드웨어 수준에서 제어합니다.

데이터센터 적용: MPAM은 컨테이너(Container)·VM별로 LLC(Last Level Cache) 점유율과 메모리 대역폭을 격리(Isolation)하여 "noisy neighbor" 문제를 완화합니다. 공식 문서 기준, 6.19 시점에서 기본 PARTID 할당과 MBWU 통계는 동작하지만 고급 제어(PMG, Error Injection 등)는 개발이 진행 중입니다.

v7.0 — Rust 드라이버의 DT 안전 추상화

커널 7.0에서 Rust 커널 드라이버가 Device Tree를 다루는 안전 추상화 레이어가 안정화되었습니다. 기존 C 드라이버는 of_match_table을 직접 조작했지만, Rust 드라이버는 타입 시스템으로 검증된 구조체를 통해 동일한 기능을 제공합니다.

커널 v7.0부터: 새로운 Rust 드라이버는 DT 바인딩 검색에 of::IdTable 추상화를 사용하도록 권장됩니다. C 언어의 OF_DEVICE_ID_TABLE() 매크로와 동등한 기능을 제공하면서 Rust의 수명·경계 검사가 보장됩니다.

참고자료

공식 규격 및 표준

커널 문서

LWN 기사

커널 소스 코드

컨퍼런스 발표 및 기술 자료

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