0️⃣ 들어가며

이전 글에 이어 드라이버 제작 기초 3번째 편이다.
이번에는 디바이스 트리에 대해 다룰 예정!
디바이스 트리는 리눅스 부팅 과정에서 잠깐 다루었던 적이 있는데
이 친구의 역할과 상세한 내용에 대해 배운 건 처음이었다.
세상 모든 디바이스가 연결하자마자 짜잔 하고 작동이 된다면 디바이스 트리는 필요가 없겠지만,
현실은 마법 같지 않으니까 우리는 열심히 공부하도록 합시다.


1️⃣ 학습 내용

5. 리눅스 디바이스 트리

5-1. 디바이스 트리의 개요

✅ 디바이스 트리의 개요

  • 부팅 과정에서의 디바이스 트리

    1. 전원 On / Bootloader 실행

      전원이 켜지면 부트로더가 실행되고 Clock(PLL), DRAM 컨트롤러 등 시스템 기본 설정을 수행

      커널 이미지(kernel8.img)와 디바이스 트리 바이너리(.dtb)를 메인 메모리(DRAM)에 적재

      부트로더는 DTB가 저장된 메모리 주소를 레지스터로 넘겨줌(Handover)

    2. 커널 실행

      커널 압축 해제 및 초기화로 이미지 얻기(vmlinux)

      전달받은 DTB를 바탕으로 하드웨어 연결 확인 및 셋업

  • 디바이스 트리의 목적

    하나의 커널 이미지로 하드웨어 구성이 다른 여러 보드를 지원할 수 있게 됨

    커널을 다시 컴파일할 필요 없이 DTB만 교체하는 방식

✅ 디바이스 트리의 구성

  • 디바이스 트리의 구성

    확장자설명역할
    .dtsDevice Tree Source하드웨어 정보를 기술한 텍스트 파일 (소스 코드)
    .dtsiDevice Tree Source Include여러 보드에서 공통으로 쓰이는 SoC 레벨의 정의 파일 (헤더 파일 역할)
    .dtbDevice Tree BlobDTS를 컴파일하여 생성된 바이너리 파일. 부트로더가 커널에 전달하는 실체 (= FDT: Flattened Device Tree)
    .dtboDevice Tree Blob Overlay런타임 중에 동적으로 로드하여 하드웨어 구성을 변경하기 위한 바이너리 조각 (라즈베리파이 HAT 등에서 사용)

    컴파일러 : DTC(Device Tree Compiler)로, 소스(.dts)를 바이너리(.dtb)로 변환

  • U-Boot 부팅 예시

    # bootm <kernel_addr> <ramdisk_addr> <dtb_addr>
    
    # 램디스크가 없는 경우 ('-' 사용)
    bootm 0x40080000 - 0x40000000

    U-Boot가 커널을 메인 메모리에 로드할 때 DTB의 주소를 넘겨주는 방식

    # bootm kernel_addr - dtb_addr

5-2. 디바이스 트리의 구조와 문법

✅ 디바이스 트리의 기본 구조

  • 디바이스 트리의 기본 형식

    루트 노드(/)에서 시작하여 다른 노드로 뻗어나가는 트리 구조

    노드(Node) : 특정 장치나 버스를 나타내는 블록으로, 속성과 자식 노드들로 구성

    속성(Property) : 노드의 특성을 설명하는 역할, 키-값으로 구성된 쌍

    자식 노드(Child Node) : 버스 아래에 연결된 장치들

  • 기본 구조와 형태

    중괄호 블록 구조로 작성 (/ { };)

✅ 디바이스 트리 기본 작성법

  • 속성 (Property)

    속성은 Key = Value 형태로 정의되며 값의 타입에 따라 표기법이 다름

    값의 해석은 디바이스 트리를 사용하는 OS(드라이버)의 책임

    String : 큰따옴표, Cells(정수 배열) : <> 괄호, Binary Data : [] 대괄호, Empty Property : 값 없음

    compatible = "arm,cortex-a9";
    reg = <0x101f1000 0x1000>; // 주소, 크기
    interrupts = <1 2 3>;
    mac-address = [00 11 22 33 44 55];
    gpio-controller; // 존재 자체로 의미가 있음
  • 노드 이름 규칙

    노드는 name@unit-address 형태로 표현됨

    1. name : 장치 종류

      장치의 기능을 나타내는 일반적인 이름으로 표현하고, 제조사나 모델명을 쓰지 않음

      최대 31자로 길이가 제한되어 있음

      (예 : 3com의 이더넷 어댑터는 3com509@... 가 아닌 ethernet@... )

    2. unit-address : 구분자

      같은 종류의 장치를 구분하려는 목적으로 사용하며, 큰 의미는 없음

      보통 해당 장치의 reg 속성에 정의된 첫 번째 레지스터 주소를 사용

      동일한 이름(name)을 가진 다른 노드가 없다면 생략 가능

      (예 : cpu@0 , serial@101f1000 )

    • 노드 이름의 종류(표준 바인딩)

      종류권장 노드 이름 (Name)예시
      Processorcpu, cache-controllercpu@0
      Bus/Bridgepci, pcie, i2c, spi, usbi2c@400
      Systemtimer, rtc, watchdogrtc@1
      Input/Outputgpio, serial, ethernet, displayethernet@1000
      Controllerinterrupt-controller, dma-controllerinterrupt-controller@100
  • 바인딩(Binding)과 Compatible

    디바이스 트리와 커널 드라이버를 연결하는 식별자

    커널 소스의 Documentation/devicetree/bindings/ 내에 장치별 필수 속성과 작성법(바인딩) 정의되어 있음

    디바이스를 표현하는 모든 노드는 compatible 속성을 가짐

    여러 개의 문자열이 나열된 리스트로 표현되어, 디바이스의 정보를 찾는 키로 사용

    • Compatible 속성

      // Most Specific -> Least Specific 순서
      // 1순위로 deviceAA, 없으면 2순위로 deviceA 드라이버를 찾아서 사용
      compatible = "company,deviceAA", "company,deviceA";

      compatible 은 네임스페이스 충돌 방지를 위해 사용하는 Magic String

      “제조사,모델” 형태로 사용하고, 내부 문자열은 띄어쓰기하지 않고 붙여서 씀

      드라이버 소스 코드(of_match_table)의 문자열과 정확히 일치해야 함

      여러 개의 모델을 쓸 수 있고, 구체적인 모델명부터 범용적인 모델 순서로 나열

      (예 : compatible = "fsl,imx28-evk", "fsl,imx28";)

    • Top Level Compatible (루트 노드)

      루트 노드인 /compatible 속성은 보드와 SoC를 식별함

      커널이 부팅될 때 이 문자열을 보고 적절한 머신 초기화 코드(DT_MACHINE)를 선택

      DT_MACHINE 구조체의 dt_compatible 필드에 사용하게 됨

      // Kernel Side (Machine definition)
      static const char *mxs_dt_compat[] __initdata = {
          "fsl,imx28-evk",
          "fsl,imx28",
          NULL,
      };

✅ 디바이스 트리에 장치 추가

  • CPU 노드 추가

    모든 CPU는 cpus 라는 부모 노드 아래에 위치해야 함

    #address-cells : 주소를 몇 개의 숫자로 표시할지 정의

    #size-cells : CPU는 크기를 가지지 않으므로 0으로 정의

    각 CPU는 자식 노드인 cpu@n 으로 추가

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
    
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a7";
            reg = <0x0>; // 하드웨어 ID 0
        };
    
        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a7";
            reg = <0x1>; // 하드웨어 ID 1
        };
    };
  • 메모리와 버스, 컨트롤러 추가

    메모리 : 시스템 RAM의 시작 주소와 크기를 기술

    버스 : APB 등의 SoC 내부 버스는 자식 노드를 가지는 컨테이너 역할로 주로 사용

    디바이스 : serial , gpio , interrupt-controller 등은 버스 아래에 reg 속성과 함께 정의

5-3. 데이터 표현과 환경 설정

✅ 주소 지정 (Address Binding)

  • 주소 지정

    부모 노드에서 주소의 체계를 정의하면 자식 노드들에게 적용됨

    자식 노드는 부모 노드가 지정한 규칙에 따라 자신의 주소 reg를 작성

    #address-cells : 주소를 표현하는 데 필요한 32비트 셀의 개수

    #size-cells : 크기를 표현하는 데 필요한 셀의 개수

    parent {
    		#address-cells = <2>; // 셀 2개, 주소에 64비트 필요
    		#size-cells = <1>;    // 셀 1개, 크기는 32비트 필요
    		
    		child {
    				// [주소 상위32] [주소 하위32] [크기] 순으로 표기
    				reg = <0x00000000 0xc0000000 0x1000>;
    		};
    };
  • reg 속성

    reg = <address1 length1 [address2 length2] ...>;

    일반적으로 주소-길이의 쌍으로 구성되어 하드웨어 장치의 위치와 크기를 나타내는 속성

    할당되는 주소는 연속적일 수도 있고 불연속적일 수도 있음

    불연속적인 경우 여러 쌍을 나열할 수 있음

✅ 인터럽트 연결 (Interrupt Binding)

  • 인터럽트 트리와 구조

    인터럽트 트리 구조는 기존의 하드웨어 연결 구조와 다름

    별도의 속성으로 연결 관계를 정의할 필요가 있음

    인터럽트 컨트롤러가 여러 장치를 관리하고, 각 디바이스는 인터럽트 컨트롤러에게 신호 전송

    • Controller 측 속성

      interrupt-controller : 해당 노드가 인터럽트 컨트롤러임을 명시

      #interrupt-cells : 인터럽트를 표현하는 데 사용되는 셀의 개수

    • Device 측 속성

      interrupt-parent : 인터럽트를 받는 컨트롤러의 phandle

      interrupts : 실제로 전달할 인터럽트 정보로, <번호, 트리거> 로 구성

      // 인터럽트 컨트롤러
      intc: interrupt-controller@10140000 {
      		compatible = "...";
      		reg = <...>;
      		interrupt-controller;       // 인터럽트 컨트롤러임을 선언
      		#interrupt-cells = <2>;     // 셀 크기 지정
      };
      
      //인터럽트 요청 디바이스
      spi@10115000 {
      		compatible = "...";
      		reg = <...>;
      		interrupt-parent = <&intc>; // 컨트롤러 지정
      		interrupts = < 4 0 >;       // <번호, 트리거>
      };

✅ 핀 설정

  • 핀 컨트롤 (Pinctrl Subsystem)

    핀의 기능(Muxing)과 전기적 특성(Pull-up/down)을 설정하는 것

    장치가 활성화될 때 필요한 핀 설정(Pin Mux)를 디바이스 트리에서 관리

    (예 : gpio의 INPUT / OUTPUT 모드 설정)

    Muxing(Multiplexing)이란 하나의 물리적 핀에 여러 기능 중 어떤 것을 할당할지 설정하는 것

    Pin Controller는 핀의 용도를 결정하는 하드웨어, Client Device는 핀을 사용하는 장치

  • 핀 컨트롤의 Properties

    1. pinctrl-names

      장치가 가질 수 있는 핀 상태의 이름을 리스트로 나열한 것

      (예 : pinctrl-names = "default", "sleep"; )

    2. pinctrl-N

      pinctrl-names 에서 나열한 순서에 맞게 연결할 핀 설정

      (예 : pinctrl-0 → 첫 번째 이름에 매칭되는 설정, pinctrl-1 : 두 번째 이름에 매칭되는 설정)

5-4. 빌드와 매칭 시스템

✅ 컴파일과 빌드, 오버레이(Overlay)

  • 소스 파일의 계층 구조

    1. .dtsi : SoC Level

      칩셋에서 사용하는 공통 정의 부분, 주로 여러 개의 파일로 구성

      헤더 파일처럼 #include 하여 사용됨

    2. .dts : Board Level

      특정 보드 전용 설정이 포함된 부분, 주로 하나의 최종 파일로 구현됨

      .dtsi 를 포함하고 필요한 부분만 덮어쓰기(Overlay)하여 최종 구성을 완성

  • .dtb 컴파일 및 빌드

    커널을 빌드할 때는 make dtbs 명령으로 arch/arm/boot/dts/Makefile 에 정의된 목록을 컴파일

    이때 .dtsi.dts 에 같은 내용이 들어 있다면, 오버레이에 의해 .dts 의 내용이 최종 컴파일됨

    빌드가 완료되면 .dtb 도 자동으로 생성됨

  • 오버레이(Overlay)

    .dtsi.dts 를 합칠 때는 .dts 의 내용이 오버레이로 사용됨

    결과물인 .dtb 에도 .dtbo 라는 오버레이 파일을 씌울 수 있음

    운영체제가 돌아가는 중에 새로운 장치를 연결해서 디바이스 트리에 변경이 생겼을 때 사용

    .dtbo (Device Tree Blob Overlay)로, 이미 빌드된 시스템에 새로운 하드웨어 동적 추가 가능

✅ 매칭 (Matching)

  • 디바이스 인식 방식

    1. Discoverable Devices (자동 인식 장치)

      Plug-and-Play 방식으로, USB나 PCI 장치 등이 해당됨

      연결 시 시그널 발생 → 어댑터가 인터럽트 발생 → 장치에게 Enumeration 요청 → Device Descriptor가 정보 제공

    2. Non-discoverable Devices (수동 명세 장치)

      시스템이 부팅될 때 수동으로 알려줘야 하는 장치

      장치가 어느 주소(reg)에 있고 어떤 드라이버(compatible)를 사용하는지 명세가 필요함

      디바이스 드라이버에 해당 정보를 담아 두게 됨

  • 매칭 메커니즘

    플랫폼 드라이버와 디바이스가 연결되는 방식

    4가지 방법이 있지만 OF Style Match가 가장 널리 쓰이는 방법

  • 매칭 방법의 종류

    1. OF Style Match (Open Firmware Style)

      Device Tree에서 가장 많이 사용되는 방식

    2. ACPI Style Match

      PC에서 주로 쓰이고, 임베디드에서는 잘 안 쓰임

    3. ID Table Match

      Old Fashioned

    4. Driver Name Match

  • OF Style Match 방법

    Device tree의 각 디바이스 노드는 문자열 형태로 표현된 compatible property를 가짐

    디바이스 트리의 compatible 문자열과 드라이버의 of_match_table 이 일치하면 probe 함수를 호출

  • 리눅스 커널의 OF APIs

    구분함수명설명 및 역할
    노드 찾기of_find_node_by_name노드의 이름(name)으로 검색하여 해당 device_node 포인터 반환
    of_find_compatible_nodecompatible 속성 문자열이 일치하는 노드를 검색
    of_find_node_with_property특정 속성(Property)을 가진 노드를 검색
    속성 읽기of_property_read_string노드에서 문자열(String) 타입의 속성 값을 읽음
    of_property_read_u32노드에서 32비트 정수(Cell) 값을 읽음
    of_property_read_bool값이 없는 플래그성 속성(Bool)의 존재 여부를 확인
    of_property_read_u32_array정수 배열(Array) 형태의 값을 읽음
    리소스 매핑platform_get_resourcereg(메모리)나 interrupts(IRQ) 정보를 커널 표준 리소스(struct resource) 형태로 변환하여 가져옴
    platform_get_irq디바이스 트리에서 인터럽트 번호를 파싱하여 리눅스 가상 IRQ 번호로 반환

2️⃣ 느낀 점

디바이스 트리 코드를 직접 작성할 일이 많으려나 싶었는데
실제로 드라이버 코드를 실행하기 위해서는 .dtbo 파일을 만들어서 디바이스 트리 오버레이를 하는 과정이 필요했다.
우리가 CPU 노드부터 추가할 일은 당장 없지만
개별 장치의 디바이스 트리를 만드는 방법 정도는 꼭 알고 있어야 할 것!
작성이 많이 어려운 편은 아니니 그때그때 예시 코드를 참고하면 충분히 만들 수 있을 것이다.

0개의 댓글