리눅스 ARM64에서 기본적으로 커널의 어떤 옵션을 활성화할지는 DTB 정의하고 커널 로드시에 DTB 참조해서 동적으로 옵션을 활성화 한다.

디바이스 트리는 보통 디바이스 트리 스크립트언어로 작성하고 이를 컴파일해서 바이너리 파일인 DTB(FDT) 만들어서 사용하게 된다.

과정을 정리하면 Device Tree script 컴파일해서 DTB(FDT)라는 바이너리 파일로만들고 커널에서는 해당 바이너리 파일을 파싱해서 slab cache 변환 필요한 옵션들을 트리 format으로 가져와서 사용한다.

 

1. Makefile

기본적으로 device tree 작성하는 .dts파일은 Boot 시에 Makefile 추가되어 참조된다. 때문에 board 맞는 DTB 작성하고 해당 파일을 Make file 연결해주어야 한다. Makefile 경로는 Raspi3(broadcom vendor chipset) 기준으로 다음과 같다. <arch/arm64/boot/dts/broadcom/Makefile>

# SPDX-License-Identifier: GPL-2.0
dtb-$(CONFIG_ARCH_BCM2835) += bcm2711-rpi-400.dtb \
                             
bcm2711-rpi-4-b.dtb \
                             
bcm2837-rpi-3-a-plus.dtb \
                             
bcm2837-rpi-3-b.dtb \
                             
bcm2837-rpi-3-b-plus.dtb \
                             
bcm2837-rpi-cm3-io3.dtb

subdir-y        += bcm4908
subdir-y        += northstar2
subdir-y        += stingray

 

ARM64 부터는 chipset 제조사별로 코드를 개별구현하여 무분별한 코드가 생성되는 것을 막기위해 디바이스트리를 통해 모든 제조사를 공통 인터페이스로만들고 공통 인터페이스에서 벗어나 제조사별로 유니크한 구현이 필요한 경우 arch/arm64/Kconfig.platfomrs파일을 통해 구분할 있도록 정의되어 있다.

 

config ARCH_BCM2835
        
bool "Broadcom BCM2835 family"
        
select TIMER_OF
        
select GPIOLIB
        
select MFD_CORE
        
select PINCTRL
        
select PINCTRL_BCM2835
        
select ARM_AMBA
        
select ARM_GIC
        
select ARM_TIMER_SP804
        
select BRCMSTB_L2_IRQ
        
help
          This enables support for the Broadcom BCM2837 and BCM2711 SoC.
          These SoCs are used in the Raspberry Pi 3 and 4 devices.

위는 broadcom 2835 raspi 3 적용하는 uniqe config값이다. 사용하는 config값들은 bool값으로 broadcom2835 맞춰 사용하고자하는 config값을 셋업 하는것 같다.(이후에 해당 값이 set되었는지로 추가 작업이 진행되고 있는게 맞는지는 확인해볼 필요가 있을 같다)

 

2. DTB 구조

Device Tree Script 컴파일해서 생성한 DTB 바이너리 데이터로 크게 4가지 block으로 구성된다.

FDT_Header, memory_reservaction, structure, string block

추가로 구조내 데이터는 ARM64 경우 대부분 Little Endian으로 구성된다.

1) fdt_header

DTB 파일을 hex editor 열면 가장먼저 FDT Header값이 나온다

FDT Header 값은 구조체로 이루어져 있으며 실제적으로 DTB의시작을 알리는 magic number이후 total size, structe block 시작 offset, stirng blobk 시작 offset, mem_reservation_block 시작 offset FDT 구조체의 다른 멤버들의 시작 주소와 사이즈 등의 정보를 담고 있다.

2) memory_reservation_block

struct fdt_reserve_entry {
   
uint64_t address;
   
uint64_t size;
};

memory reservation 부트로더에서 사용하는, 커널 로드에 사용하는 주소 값등을 user 접근하지 못하도록 하기 위함이다. 중요한 config 설정된 부분을 reservation block으로 지정하여 user 접근을 막는다.

3) structure block

structure block 5개의 토큰으로 구성되어있으며 실제적으로 Tree구조 뼈대로 노드들을 나타내는 block이다.

FDT_BEGIN_NODE, FDT_PROP, FDT_NOP, FDT_END_NODE, FDT_END

FDT_BEGIN_NODE 모든 노드의 시작을 알리는 토큰이다. 노드의 앞에 오게되며 FDT_BEGIN_NODE 뒤에 바로 Node 이름이 오게된다. 여기서 가장 처음에 오는 노드는 root 노드로 이름이 NULL값인 00 00 00 00 이다.

 

FDT_PROP 해당 노드의 속성값을 나타낸다. 속성 값은 string block 이용해서 나타내기 때문에

struct {
   
uint32_t len;
   
uint32_t nameoff;
}

FDT_PROP에서는 위와같이 속성 값을 나타내는 string block offset 길이 값을 가지고 있다.

 

FDT_END_NODE Node 부분을 알린다. FDT_NOP NULL값으로 파싱 무시한다.

마지막으로 FDT_END struct block 끝임을 알린다.

 

결과적으로 정리를하면 항상 반드시 루트 노드가 존재하고 아래에 device장치에 따른 노드들이 하위로 존재하는 tree구조를 가지기 때문에 아래와 같은 구조를 가지게 된다.

 

FDT_BEGIN_NODE [null(root 노드이름)] FDT_PROP FDT_BEGIN_NODE [node이름(test)] FDT_PROP FDT_END_NODE FDT_END_NODE FDT_END

struct node 아래와 같은 트리 구조를 가진다.(FDT_END_NODE없이 FDT_BEGIN_NODE 것은 노드가 앞서 노드의 하위 노드임을 의미한다)
-null(root)
-- test
 

4) string block

string block string 값으로 prop값들을 정의하며 마지막에 null값을 줌으로써 노드에 따른 string 값의끝을 알린다.

 

3. DTB early 처리

DTB 바이너리파일로 위에서 언급한바와 같이 slab cache 변환해서 커널에서 참조 사용한다고 했다 하지만 여기서 일부 옵션들은 slab cache 생성하기 전에 먼저 필요한 값들이 존재한다. (boot_commaond_line, initrd_start ) 이러한 값들을 elary 항목이라고 하며 early 항목에 한해서는 slab cache 변환과정 없이 직접 DTB 바이너리 파싱을 통해 직접적으로 값을 가져와서 활용한다.

 

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
        
int size;
        
void *dt_virt = fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL);
        
const char *name;

if (dt_virt)
                
memblock_reserve(dt_phys, size);

if (!dt_virt || !early_init_dt_scan(dt_virt)) {
                
pr_crit("\n"
                        
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
                        
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
                        
"\nPlease check your bootloader.",
                        
&dt_phys, dt_virt);

while (true)
                        
cpu_relax();
        
}

/* Early fixups are done, map the FDT as read-only now */
        
fixmap_remap_fdt(dt_phys, &size, PAGE_KERNEL_RO);

name = of_flat_dt_get_machine_name();
        
if (!name)
                
return;

pr_info("Machine model: %s\n", name);
        
dump_stack_set_arch_desc("%s (DT)", name);
}

function arch/arm64/kernel/setup.c 존재하는 FDT early처리를 위한 functino이다.

먼저 dt_virt DTB virtual address 할당을 진행한다(head.S에서 레지스터에 FDT 주소 값을 저장해두었기 때문에 해당 값을 이용)

후에 memory_reservation 값을 가져와서 reservation동작을 수행한다.

이후 early_init_dt_scan function 통해 early값들을 설정한다.

/* Retrieve various information from the /chosen node */
        rc
= of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

DTB 구조의 chosen node에는 bootargs에대한 값이 들어있기 때문에 해당 node scan해서 boot_command_line 무엇인지 확보할 있다.

과정을 통해서 부트 파리미터, initrd위치와 크기, memblock 물리 메모리 시작 주소 크기 정보 저장 등의 과정을 진행한다

 

  • memblock

리눅스 커널에서는 다양한 메모리 기법들이 존재한다. 중에서 mm_init() 통한 정규 메모리 기법들의 사용 준비가 되지 않은 상태에서 사용하는 리눅스 커널 부팅 초반에 사용하는 메모리 기법이 존재하는데 이게 바로 memblock이다..

memblock 경우 위와 같은 중첩 구조체 형식으로 되어있다. 먼저 사용가능한 메모리 영역인 memory 멤버변수 구조체, 예약된 사용불가능한 reserved 존재하고 각각의 구조체 객체는 regions 라는 memblock_region 구조체 객체를 통해 offset(base) size 정의함으로써 메모리를 관리한다.

위에서 물리 메모리 시작 주소 크기 정보 저장등을 memblock 진행하는 것은 사용가능한 메모리영역의 offset size등을 설정하는 과정이라고 있다.

 

early과정들은 drivers/of/fdt.c 파일 내에 각각의 function들로 정의되어있다.

early_linit_dt_scan_chosen // chosen early처리

of_scan_flat_dt // root node찾기

early_init_dt_check_for_initrd() // initrd early 처리

early_init_dt_scan_root // size_cells, address_cells 속성을 읽어 저장

early_init_dt_scan_memory : memory 노드를 읽어 memblock 초기화

early_init_dt_add_memory_arch : memblock 필요한 memory 추가

 

4. Device Tree expanded format

Device Tree expanded format DTB 바이너리 파일에서 of_ 시작하는 API 통해 구조체형식으로 각각의 노드를 정리한 데이터 포맷을 얘기한다

각각의 노드는 구조체 형식으로 정리되고 노드 안에 속성 값도 속성 값을 나타내는 구조체로 정리가 된다. 이렇게 구조체 형식으로 Tree 만들어

바이너리 -> Tree 구조체로 (expanded format)으로 drivers/of/fdt.c에서 변환된다.

 

__unflatten_device_tree 실제적으로 구조체를 alloc으로 할당하고 노드를 변환하는 과정을 나타낸다.

그리고 alias_scan of_alias /aliases 노드를 가리키도록, of_chosen chosen 노드를 가리키도록 설정한다.

위처럼 기본적인 root, 특수한 노드를 먼저 변환하고 이후에는 unflatten_dt_node function 통해 DTB 일반적인 노드들을 파싱하여 변환한다.

 

 

  • ref

https://devicetree-specification.readthedocs.io/en/v0.2/flattened-format.html

https://elixir.bootlin.com/linux/v5.14.16/source

https://kernel.bz/boardPost/118682/2

 

+ Recent posts