개요
PC에서는 BIOS가 있어서 하드웨어 장치가 변경되면 자동적으로 검출하여 동작하도록 해주는 작업이 쉽다.
ARM 프로세서는 BIOS가 없어서 리눅스 커널이 모든 역할을 다해주고 있어서 리눅스 커널을 믿는 수밖에 없다.
그러므로, 리눅스 커널에 도입된 해결책이 디바이스 트리(Device Tree)이며, Open Firmware(Abbreviated OF) 혹은 Flattened Device Tree(FDT)라고 언급되기도 한다.

Device Tree는 정해진 API가 특정 데이터에 접근하도록 표준화된 Tree 구조체를 사용하도록 한다. 주변장치들의 버스, 주소, 인터럽트, 변수들은 정해진 관례를 지켜서 Device Tree에 표현한다. 디바이스 트리는 커널의 일부분으로서 하드웨어 정보를 추가, 제거, 운반하는 장소 역할을 한다.
리눅스 소스 코드에서 arch/arm/boot/dts 경로에는 .dts파일과 .dtsi 파일이 board마다 존재하는 것을 확인할 수 있다.

|
.dtsi : Soc 레벨에서 정의한 include 파일
|
|
.dts : Board 레벨에서 정의한 스크립트 파일
|
두 파일 모두 XML처럼 데이터의 구성을 문법적으로 기술한 것이다.
dtb 파일은 부팅할 때 부트로더에 의해서 적재되고 커널이 parsing 한다.
커널에는 kerner_entry() 함수에 dtb_addr을 다음과 같이 적재한다.
|
kernel_entry(0, mach_id, dtb_addr)
|
커널을 부팅한 이후에는 /proc/device-tree 경로에서 다음과 같이 장치 설정 정보들을 확인할 수 있다.
DTS 기본 문법
/{
node1 {
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property = [0x01 0x023 0x34 0x56];
child-node1 {
first-child-property;
second-child-property = <1>;
a-string-property = "Hello World";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
};
};
};
위 예제에서,
/는 root node를 위미한다.
node는 {}로 범위를 규정한다.
Tree 구조로 표현하면 아래와 같이 표현된다.

node 속성들은 아래와 같은 종류가 있다.
|
문자열 : ""을 사용하여 표현한다
ex>> string-property = "a string";
|
|
cells : <>를 사용하여 부호 없는 32비트 정수로 표현한다
ex>> cell-property = <0xbeef 123 0xabcd1234>;
|
|
이진 데이터 : []를 사용하여 표현한다
ex>> binary-property = [0x01 0x23 0x45 0x67];
|
|
혼합된 데이터 : ,를 사용하여 표현한다
ex>> mixed-property = "a string", [0x1 0x23 0x45 0x67], <0x12345678>;
|
|
문자열 리스트 : ,을 사용하여 표현한다
ex>> string-list = "red fish", "blue fish";
|
root 구조체
/{
compatible = "company, product name";
};
compatible 속성은 시스템의 이름을 "제조사, 제품명" 형태로 기술한다.
커널은 machine(하드웨어)을 어떻게 실행할 가에 대한 판단을 해당 값을 통해서 한다.
CPU
/{
compatible = "company, product name";
cpus {
cpu@0 {
compatible = "arm, cortex-a53";
};
cpu@1 {
compatible = "arm, cortex-a53";
};
};
};
가장 간단한 cpu 표현은 위와 같다. 위는 Dual Core를 표현한 것이다.
실제로는 clock, device-type 등의 정보가 더 필요하다.
node 이름
모든 node에는 name[@unit-address] 형태의 명칭이 있다.
name은 ascii 형태의 문자열로 표현하며 31개 문자 길이까지 가능하다.
name은 어떤 종류의 장치인지를 일반적으로 나타낸다.
unit-address는 장치의 주소를 의미한다. 일반적으로, 장치 주소는 장치에 접근하고자 할 때 사용되는 것으로 reg 속성에 나열된다.
compatible 속성
장치를 표현하는 각각의 node는 compatible 속성을 필요로 한다.
compatible 속성은 커널이 어떤 device driver를 해당 장치와 연결할 것인가를 판단하는데 사용한다.
일반적으로, 첫 번째 문자열은 "제조사, 모델명" 형태로 장치를 표현한다.
두 번째 문자열은 이 장치와 호환되는 다른 장치를 표현한다.
주소
device의 주소를 Device Tree에 표현하기 위해서는 아래의 3가지 속성을 사용한다.
#address-cell
#size-cells
reg
#address-cells 속성과 #size-cells 속성은 reg 속성의 데이터에 대한 개수 규칙을 지정한다.
#address-cells 속성과 #size-cells 속성은 부모 node에서 지정하고, reg 속성은 자식 node에서 지정한다.
일반적으로는 아래의 형태를 가진다.
reg = <주소1 길이1 [주소2 길이2] [주소3 길이3] ...>;
주소와 길이가 하나의 묶음이 된다.
구체적으로 예를 들어보면,
parent {
#address-cells = <2>;
#size-cells = <1>;
child1 {
reg = <0xC0000000 0x0000 1024>;
};
child2 {
reg = <0xD0000000 0x0000 1024 0xE0000000 0x0000 2048>;
};
};
위의 예제는 다음과 같이 해석된다.
"child1이라는 device는 0xC00000000000에 시작해서 1024의 크기를 통해서 접근하는 디바이스"
"child2이라는 device는 두 개의 주소 공간을 점유하고 있다. 각각은 0xD00000000000부터 1024만큼, 0xE00000000000부터 2048만큼"
위의 예제처럼 node들은 하부에 자식 node들이 포함될 수 있고 해당 자식 node 역시 reg 속성을 가질 수 있다. 이때 #address-cells와 #size-cells는 특별히 아래에서 선언하지 않으면 자식 node에게 그대로 상속되어 적용된다.
CPU 주소
/{
compatible = "company, product name";
cpus {
cpu@0 {
compatible = "arm, cortex-a53";
reg = <0>;
};
cpu@1 {
compatible = "arm, cortex-a53";
reg = <1>;
};
};
};
cpu를 표현할 때, reg 속성은 주소가 아닌 구분자를 의미한다.
규정에는 노드의 reg 속성값으로 사용된 첫 번째 주솟값을 node 이름의 장치 주소에 사용하도록 한다.
非 메모리 mapping 장치
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
위의 예제를 보면 I2C와 같은 장치들은 각각의 장치가 주소를 가지고 있지만, 길이나 범위로는 표현되지 않는다.
그러므로 reg의 3개 숫자 모두 주소를 나타내며 각각의 상하관계를 가지고 있는 숫자들이다.
range 속성
root(/) node는 항상 CPU 관점에서 주소 공간을 기술한다.
루트의 직계 자식이 아닌 손자 node들은 CPU 주소 영역을 사용하지 못한다. 그러므로 주소를 변환하는 방식이 Device Tree에 도입이 필요하고 range 속성을 통해 구현한다.
메모리 mapping되어 있는 I2C 구조를 생각하고 아래의 예제를 살펴보자.
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
...
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
I2C 같은 경우에는 chip select를 통해서 접근할 수도 있지만, 메모리에 바로 mapping되어 있을 수 있다.
그렇다면, device들은 각 device node의 주소 도메인과 CPU의 주소 도메인 간 변환이 가능하도록 관련된 정보를 제공해야 한다. 이런 정보를 제공하는 것이 range 속성이다.
range 속성은 아래와 같이 표현된다.
range = < 자식주소1 부모주소1 자식주소크기1
자식주소2 부모주소2 자식주소크기2
자식주소3 부모주소3 자식주소크기3 >;
이때, 자식 주소와 부모 주소의 크기와 시작 지점을 따로 지정할 수 있는데. 부모 node에서 지정한 #address-cells 속성은 부모 주소의 셀 수를 지정하고 자식 node에서 지정한 #address-cells 속성은 자식 주소의 셀 수를 지정한다. 자식 주소 크기를 나타내는 셀 수는 자식 노드에 선언된 #size-cells 속성에 지정된 크기를 셀 수로 지정한다.
그렇다면 위의 예제는 다음과 같이 해석된다.
- 칩 셀렉트 0번의 오프셋 0은 0x10100000부터 0x10000만큼 매핑된다.
- 칩 셀렉트 1번의 오프셋 0은 0x10160000부터 0x10000만큼 매핑된다.
- 칩 셀렉트 2번의 오프셋 0은 0x30000000부터 0x1000000만큼 매핑된다.
만약 부모와 자식 주소 공간이 동일하다면, 비어있는 range 속성을 사용하면 된다. 즉, 1:1 매핑이 되어 있다는 의미이다.
비어있는 range 속성이 아니라 range 속성이 없는 경우에는, 부모 외에는 어떤 디바이스도 접근을 할 수 없음을 나타낸다.
Interrupt
Device Tree에 구조적으로 표현된 주소 접근 장치와는 다르게, 인터럽트 신호는 Device Tree와는 독립적인 node들에 연결되는 방식으로 표현된다.
인터럽트는 컨트롤러가 해당 신호를 수신하는 방식이므로, 디바이스 node 간에 링크 구조로 표현된다.
인터럽트를 표현하는 속성은 아래의 4개가 있다.
|
interrupt-controller :
값이 없는 빈속성으로 해당 node의 디바이스가 인터럽트 신호를 수신하는 인터럽트 컨트롤러 디바이스 임을 표현한다.
|
|
interrupt-parent :
해당 속성을 통해서 라벨 참조 형태로 지정하는 디바이스를 디바이스 트리의 계층 구조에서 가장 상위의 인터럽트 컨트롤러로 정의하게 된다. interrupt-parent 속성을 갖지 않은 node들은 그들의 부모 node 중 interrupt-parent 속성이 선언된 node의 interrupt-parent 속성을 상속받는다.
|
|
#interrupt-cells :
해당 속성에 지정된 값은 interrupt 속성에서 인터럽트에 대한 정보를 속성값으로 표현할 때 사용될 cell 개수를 나타낸다. interrupt-controller 속성이 선언된 node에 같이 선언한다.
|
|
interrupts :
디바이스가 발생하는 인터럽트 출력 신호에 대한 정보의 리스트를 값으로 표현한다.
|
인터럽트에 대해서는 아래의 예제를 살펴보자.
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
interrupts = < 1 0 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = < 3 0 >;
};
intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller;
#interrupt-cells = <2>;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = < 4 0 >;
};
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ethernet@0,0 {
compatible = "smc,smc91c111";
reg = <0 0 0x1000>;
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
위의 예제에서는 node 이름이 interrupt-controller@10140000로 지정된 한 개의 인터럽트 컨트롤러를 가지고 있다. 여기서 특이하게 'intc:'라고 하는 label이 인터럽트 컨트롤러 노드에 추가된 것을 볼 수 있다.
이 label은 루트 node에서 아래와 같이 interrupt-parent 속성으로 사용되고 있다.
interrupt-parent = <&intc>;
해당 interrupt-parent 속성 값은 시스템의 디폴트 인터럽트 컨트롤러를 지정한다.
그리고 #interrupt-cells의 값은 2인데 첫 번째 값은 라인 번호를 나타내고, 두 번째는 액티브 시키기 위해서 레벨이 low인지 high인지 edge 인지를 나타낸다.
aliases 노드
aliases {
ethernet0 = @eth0;
serial0 = @serial0;
};
Device Tree는 aliases라는 node를 제공하여 쉽게 긴 이름을 짧은 이름으로 치환하게 하거나 일반적으로 알 수 있는 용어로 바꾸어 사용할 수 있도록 해줄 수 있다.
즉. "별칭 = &라벨;" 같이 사용이 가능하다.
chosen 노드
chosen node는 실제 device를 Device Tree에 표현하는 것은 아니다. 단지, boot argument와 같이 Firmware에서 운영 체제에 전달된 데이터를 parsing 하기 위한 방법으로 제공된다.
chosen {
bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200";
};
'Kernel > 理解' 카테고리의 다른 글
| Block IO 분석 (0) | 2024.03.24 |
|---|---|
| Device Tree 분석 (Kernel Source) (0) | 2024.02.02 |
| ZRAM 분석 (0) | 2024.02.02 |
| ELF 실행 & execve (0) | 2024.02.02 |
| Bootloader 개요 (0) | 2024.02.02 |