디바이스 트리란 무엇인가

August 7, 2026 · 방문자

임베디드 보드에서 리눅스를 올려본 적이 있다면 언젠가는 .dts로 끝나는 파일을 만나게 된다. 보통은 최악의 순간에 그렇다. 보드가 부팅되지 않고, 화면에는 아무것도 나오지 않고, 누군가 "디바이스 트리를 봐라"라고 말한다. 이 글은 그 파일이 무엇이고, 왜 존재하며, 그 안에서 실제로 무엇을 찾고 있는지에 대한 것이다.

어떤 문제를 푸는가

데스크톱에서는 커널이 하드웨어를 거의 스스로 찾아낸다. PCI와 USB는 공식적으로 규격화된, 열거 가능한 버스이기 때문이다 (1). 장치는 제조사 ID와 제품 ID로 자신을 알리고, 드라이버는 지원하는 ID를 표에 나열하며, 커널은 표의 항목과 일치하는 장치마다 그 드라이버의 probe() 함수를 호출한다 (2). USB 쪽도 자체 ID 표로 같은 방식이 돌아간다 (3). 누구도 미리 누구에게 무언가를 알려줄 필요가 없다.

임베디드 보드에는 그런 호사가 없다. 커널 문서는 이 경계를 분명하게 긋는다. SoC에 통합된 주변장치는 최소한의 인프라만 갖춘 버스에 매달려 있으며, "PCI나 USB처럼 규모가 크고 공식적으로 규격화된 것들과는 대조적으로" 이 장치들의 공통점은 CPU 버스에서 직접 주소가 지정된다는 점이다 (1). I²C 센서나 SPI에 달린 디스플레이 컨트롤러는 스스로를 알리지 않는다. 그저 거기, 그 주소에, 그 인터럽트 라인에 연결된 채로 있을 뿐이다. 커널이 그것을 알 수 있는 유일한 방법은 누군가가 알려주는 것이다.

오랫동안 그 "누군가"는 코드 자체였다. 보드마다 커널 트리 안에 어떤 장치가 어디에 연결되어 있는지를 C로 기술한 파일이 있었다 — 커널은 지금도 이 경로를 문서화한다. 보드별 초기화 코드가 장치를 하나씩 등록하는 방식이다 (1). ARM에서는 이런 파일이 너무 불어나서, 2011년 리누스 토르발스가 메일에서 이 상황을 날카롭게 해부하며 ARM 코드가 지속 불가능한 방향으로 가고 있다고 말했다. 채택된 출구가 디바이스 트리였다.

발상

디바이스 트리는 하드웨어가 어떻게 연결되어 있는지를 기술한 데이터 파일이다. 코드가 아니다 — 실행되지도 않고, 커널 안으로 컴파일되어 들어가지도 않는다. 기술일 뿐이다. 이 주소에 컨트롤러가 있고, 이 인터럽트 라인을 쓰며, 이 클록에서 공급받고, 그 아래에 이런 장치들이 매달려 있다는 식이다.

포맷 자체는 공식 규격에 근거한다 (4). 트리라고 부르는 이유는 정말로 트리이기 때문이다. 루트 노드에서 시작해 버스가 갈라져 나오고, 그 버스에 붙은 장치들이 다시 갈라져 나온다 — 하드웨어의 물리적 연결을 그대로 비춘다.

지어낸 예제 대신 진짜를 보자. 아래 노드는 TI의 J722S/AM67A 계열을 위한 U-Boot 트리의 k3-am62p-main.dtsi에서 가져온 것으로, AM67A 보드가 부팅할 때 실제로 읽는 바로 그 기술이다.

main_i2c0: i2c@20000000 {
    compatible = "ti,am64-i2c", "ti,omap4-i2c";
    reg = <0x00 0x20000000 0x00 0x100>;
    interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
    clocks = <&k3_clks 102 2>;
    clock-names = "fck";
    status = "disabled";
};

여기 한 줄 한 줄에 의미가 있다. reg는 메모리 맵에서 컨트롤러의 주소를 준다 — 0x20000000, 길이는 0x100. 주소가 숫자 두 개씩 짝지어 적힌 이유는 주소 공간이 64비트이기 때문이고, 앞의 숫자가 상위 32비트를 담는다. interrupts는 어떤 인터럽트 라인을 쓰는지 알려준다. GIC_SPIIRQ_TYPE_LEVEL_HIGH 같은 이름은 포함된 헤더 파일에서 온다 — 즉 디바이스 트리 소스는 순수한 데이터이면서도 여전히 C 전처리기를 거친다.

눈여겨볼 점은 compatible이 값을 두 개 담고 있다는 것이다. "ti,am64-i2c""ti,omap4-i2c". 이 목록은 구체적인 것에서 일반적인 것 순으로 읽힌다. 커널은 먼저 am64 전용 드라이버를 찾고, 없으면 더 오래되고 일반적인 omap4 드라이버로 물러난다 (5). 덕분에 새 칩도 누군가 전용 드라이버를 쓰지 않아도 동작할 수 있다.

clocksclock-names는 장치가 클록을 어디서 받는지 알려준다. &k3_clks의 102번 장치의 2번 출력이고, 드라이버는 "fck"라는 이름으로 그것을 찾는다. power-domains도 같은 논리로 전원 도메인을 연결한다. 이것들은 주파수 설정이 아니다 — 배선의 기술, 즉 어떤 클록이 어디로 가는지에 대한 것이다.

마지막 줄이 중요하다. status = "disabled". 칩 제조사는 주변장치를 기술하되 꺼진 채로 둔다. 그 I²C 버스가 당신의 보드에서 쓰이는지 알 도리가 없기 때문이다. 켜는 것은 보드 자신의 .dts가 할 일이다. J722S EVM의 보드 파일에서는 이렇게 보인다.

&main_i2c0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&main_i2c0_pins_default>;
    clock-frequency = <400000>;

    exp1: gpio@23 {
        compatible = "ti,tca6424";
        reg = <0x23>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

앞머리의 &main_i2c0는 위 노드에 대한 참조다 — 새 노드를 만드는 것이 아니라 기존 노드 위에 덮어쓴다. status가 켜지고, 핀이 배정되고, 속도가 400 kHz로 정해지고, 그 아래에 실제 장치가 추가된다. I²C 버스의 0x23 주소에 앉아 있는 TCA6424 GPIO 확장기다. 컨트롤러가 #address-cells = <1>을 선언했기 때문에 이 주소는 숫자 하나로 적힌다. 컨트롤러 자신의 reg와는 다른 점이다.

이것이 실제로 디바이스 트리가 작동하는 방식이다. 제조사가 SoC를 한 번 기술하고, 당신은 자기 보드에 무엇이 연결되어 있는지를 말한다.

정말로 중요한 속성은 compatible이다. 커널이 드라이버와 하드웨어를 짝지으려고 들여다보는 곳이 여기다. 드라이버가 "나는 ti,tca6424와 호환된다"고 말하고, 디바이스 트리의 노드가 같은 문자열을 지니고 있으면 커널이 둘을 이어준다. 그래서 오타는 조용히 실패한다. 문자열이 맞지 않으면 오류가 나지 않는다 — 장치가 그냥 영영 나타나지 않을 뿐이다.

염두에 둘 것 하나. 디바이스 트리는 하드웨어를 기술하지, 드라이버의 설정을 기술하지 않는다. 커널의 바인딩 작성 지침은 이 경계를 특히 강조한다 (6). 실무에서는 이 선이 끊임없이 흐려지고, 커널 메일링 리스트에서 가장 많이 논쟁되는 주제이기도 하다 — "이게 정말 하드웨어의 속성인가, 아니면 당신의 선호인가?"

파일들

확장자를 세 가지 보게 되는데, 헷갈리기 쉽다 (11).

.dts는 당신이 쓰는 소스, 사람이 읽을 수 있는 형태다. .dtsi는 다른 파일이 포함할 수 있는 조각으로, "i"는 include에서 왔다. 칩 제조사는 보통 SoC 전체를 .dtsi에 기술한다. 당신은 자기 보드용으로 짧은 .dts를 쓰고, 그것을 include한 뒤 자기 보드에만 해당하는 것만 덧붙인다.

.dtb는 컴파일된 형태 — 디바이스 트리 블롭이다. 커널이 실제로 읽는 것이 이것이다. 컴파일러는 dtc다 (7).

dtc -I dts -O dtb -o my-board.dtb my-board.dts

반대 방향으로도 동작하는데, 문제를 쫓을 때 이것이 정말 요긴하다. 동작하는 .dtb가 있다면 열어서 안을 들여다볼 수 있다.

dtc -I dtb -O dts -o decoded.dts my-board.dtb

돌아가는 시스템에서는 파일 시스템에서 디바이스 트리를 바로 읽을 수도 있다. 커널은 실제로 쓰고 있는 트리를 여기에 펼쳐 놓는다.

ls /proc/device-tree/

"내가 편집한 파일이 정말 로드된 파일인가?"라는 물음에 대한 가장 빠른 답이다. 놀라울 만큼 자주, 답은 "아니오"다.

부팅할 때 무슨 일이 일어나는가

순서는 이렇다. 부트로더 — 보통 U-Boot (8) — 가 .dtb를 메모리에 올리고, 커널도 올린 다음, 커널을 시작할 때 블롭의 메모리 주소를 레지스터로 넘긴다. ARM에서는 그 레지스터와 부트로더가 지켜야 할 조건이 커널의 부팅 문서에 정해져 있다 (9). 커널은 드라이버를 하나라도 올리기 전에 이 트리를 읽고, 거기서 장치를 뽑아내어 compatible 문자열에 따라 드라이버를 짝짓는다.

실질적인 결과는 커널과 디바이스 트리를 따로 갱신할 수 있다는 점이다. 센서 하나 추가한다고 커널을 다시 빌드해야 하는 일은 드물고, 보통은 .dtb만 갱신하면 된다. 그런데 이건 양날이다 — 서로 독립적이기에 어긋날 수 있다. 새 커널을 낡은 .dtb로 부팅하면 보드는 조용히 불완전하게 올라오고, 그 이유를 알아내는 데 한참이 걸린다.

오버레이라는 개념도 있다. 실행 중에 기존 트리 위에 얹히는 조각이다 (10). 라즈베리 파이에 HAT을 꽂을 때 일어나는 일이 이것으로, 주 트리는 그대로 있고 작은 조각이 위에 덮인다. 베이스 보드는 늘 같고 꽂는 모듈만 바뀔 때 유용하다.

무언가 동작하지 않을 때

디바이스 트리 결함에는 특유의 성가심이 있다. 대개 오류를 내지 않기 때문이다. 장치가 그냥 없다. 이 순서로 짚어가면 시간이 절약된다.

먼저 장치가 아예 만들어지기는 했는지 확인한다. /proc/device-tree 아래에 그 노드가 있는가? 없다면 문제는 당신이 편집한 파일에 있지 않다 — 십중팔구 엉뚱한 .dtb가 로드되고 있거나, 노드가 한 단계 위에서 status = "disabled"로 꺼져 있다. 후자는 늘 일어난다. 제조사의 .dtsi 파일에서 주변장치는 보통 비활성으로 오고, 자기 보드의 .dts에서 status = "okay"로 하나씩 켜는 것이 기대된다.

노드는 있는데 드라이버가 붙지 않았다면 compatible 문자열에 눈을 둔다. 드라이버 소스에서 of_device_id 표를 찾아 문자 단위로 대조한다. 쉼표 하나, 하이픈 하나면 충분하다.

하나 더. dtc 경고는 대부분의 프로젝트에서 꺼진 채로 오지만, 정말 유용한 것들을 말해준다. 주소와 reg의 불일치, 빠진 #address-cells — 바로 부팅 시 조용한 고장으로 바뀌는 종류의 결함이다. 빌드할 때 경고를 켜 두는 편이 좋다.

맺으며

디바이스 트리를 파악하기 어렵게 만드는 것은, 그것이 실제로 무엇인지 아무도 처음에 말해주지 않는다는 점이다. 설정 파일처럼 보이지만 아니고, 드라이버처럼 보이지만 그것도 아니다. 가장 쓸모 있는 틀은 하드웨어와 소프트웨어 사이의 계약으로 여기는 것이다. 보드가 어떻게 배선되어 있는지를 커널이 이해하는 언어로 적어 둔 것.

그 틀에 자리 잡고 나면 나머지는 따라온다. 왜 compatible이 그토록 결정적인지, 왜 .dtb가 커널과 따로 존재하는지, 왜 무언가 빠졌을 때 가장 먼저 볼 곳이 /proc/device-tree인지 — 모두 같은 발상에서 나온다.

관련 글

참고 문헌

  1. Platform Devices and Drivers — SoC 주변장치가 PCI, USB처럼 열거되지 않는 이유와 보드별 코드가 이를 등록하는 방식.
  2. How To Write Linux PCI Driverspci_device_id 표와 일치하는 장치에 대한 probe() 호출.
  3. USB Device Drivers — USB 쪽의 ID 기반 드라이버 짝짓기.
  4. Devicetree Specification — 포맷의 공식 정의.
  5. Linux and the Devicetree — 커널이 트리를 읽고 드라이버를 짝짓는 방식.
  6. Writing Devicetree Bindings — 하드웨어 기술과 드라이버 설정 사이의 경계.
  7. dtc — Device Tree Compiler — 컴파일러의 저장소와 사용법.
  8. U-Boot: Devicetree Control — U-Boot이 디바이스 트리를 다루는 방식.
  9. Booting ARM Linux — 부트로더가 어느 레지스터로 블롭을 커널에 넘기는지.
  10. Devicetree Overlay Notes — 오버레이가 실행 중에 적용되는 방식.
  11. Device Tree Reference (eLinux) — 예제가 정리된 커뮤니티 참고 자료.

댓글