본문 바로가기
Project/Embedded

[QEMU] 임베디드 리눅스 시스템 구축 프로젝트 - device tree 와 OpenSBI

by FastBench 2024. 6. 13.

이전글

https://microelectronics.tistory.com/68

https://microelectronics.tistory.com/69

https://microelectronics.tistory.com/70

https://microelectronics.tistory.com/71

https://microelectronics.tistory.com/72

 

원본 게시글

docs : https://quard-star-tutorial.readthedocs.io/zh-cn/latest/

github : https://github.com/QQxiaoming/quard_star_tutorial

 

Device Tree File 이란?

Flattened Device Tree (FDT)는 Linux 커널의 PowerPC 아키텍처에서 기인한 드라이버 추상화 기술이다.

초창기 Linux 커널에는 많은 mach-XXX 드라이버 코드가 있었고, 이는 당시 커널 유지 관리 팀에게 큰 부담이 되었다.

첨언을 하자면, SoC 제조사 또는 FW 엔지니어가 IC의 디바이스 구성을 명시하는
device tree source (dts) 를 작성한다.
이를 device tree compiler (dtc) 를 통해 컴파일하여 바이너리로 만들면
device tree binary (dtb) 가 생성된다.

dtb 는 바이너리로 '평평' 하므로 flattend device tree 라고도 불린다.

 

FDT의 도입으로 드라이버 작성 구조가 명확해졌으며, 많은 사람들의 호응을 받게 되었다.

이후 FDT는 모든 아키텍처에 도입되었고, 최근 몇 년 동안 U-Boot, OpenSBI 등 다른 오픈 소스 프로젝트에서도 FDT를 직접 드라이버 프레임워크로 사용하여 하드웨어의 구체적인 설정을 분리하고 있다.

 

FDT를 사용하는 방법은 매우 간단하다.

먼저 확장자가 dts/dtsi인 디바이스 트리 소스 파일을 작성한 다음, dtc 도구를 사용하여 이를 dtb 파일로 컴파일한다.

그런 다음 커널이 부팅될 때 이를 로드하여 파싱하면 된다.

 

디바이스 트리 소스 파일(dts)의 내용은 노드 형태로 장치에 존재하는 하드웨어를 설명하며, 하드웨어 토폴로지 구조에 대한 평면적 설명을 제공한다.

그 규칙은 매우 간단하며, 각 노드는 일반적으로 compatible 속성 문자열을 포함하고 있어 드라이버 초기화 시 장치 탐색과 매칭에 사용된다.

드라이버와 디바이스 트리의 노드가 일치하면, 노드 정보에 따라 드라이버가 초기화되고 커널에 등록된다.

 

quard-start board 의 dts

여기서는 우리가 사용하는 디바이스 트리를 중심으로 설명하겠다.

디바이스 트리에 관심 있는 분들은 다른 저자가 작성한 튜토리얼을 참고할 수 있다.

예를 들어, 이 링크에 매우 상세한 설명이 있다: 디바이스 트리 튜토리얼

/dts-v1/;

/* 정의 루트 노드 */
/ {
	#address-cells = <0x2>;  /* 노드의 reg 주소 폭은 64비트 */
	#size-cells = <0x2>;     /* 노드의 reg 크기 폭은 64비트 */
	compatible = "riscv-quard-star"; /* 노드의 이름 */
	model = "riscv-quard-star,qemu"; /* 장치 제조사와 모델 지정 */

    /* chosen 서브 노드 정의, 입력 파라미터 전달용 */   
	chosen {
		stdout-path = "/soc/uart0@10000000";  /* 시스템 표준 출력 stdout에 사용될 노드 /soc/uart0@10000000 정의 */
	};

    /* memory 서브 노드 정의, 시스템 주 메모리 정의용 */   
	memory@80000000 {
		device_type = "memory";    /* 장치 타입 */
		reg = <0x0 0x80000000 0x0 0x40000000>;  /* 주소와 크기 정의, 64비트임에 주의 */
	};

    /* cpus 서브 노드 정의, 시스템 코어 정보 정의용 */   
	cpus {
		#address-cells = <0x1>; /* 노드의 reg 주소 폭은 32비트 */
		#size-cells = <0x0>;    /* 노드의 reg는 크기 없음 */
		timebase-frequency = <0x989680>; /* 사용자 정의 속성 */

		cpu0: cpu@0 {             /* cpu0 서브 노드 정의 */   
			phandle = <0xf>;      /* 기타 구체적인 속성은 드라이버 코드에서 사용 */
			device_type = "cpu";
			reg = <0x0>;
			status = "okay";      /* status 속성은 장치 활성화를 나타냄 */   
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller { /* 인터럽트 컨트롤러 서브 노드 정의 */   
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x10>;
			};
		};

		cpu1: cpu@1 {
			phandle = <0xd>;
			device_type = "cpu";
			reg = <0x1>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0xe>;
			};
		};

		cpu2: cpu@2 {
			phandle = <0xb>;
			device_type = "cpu";
			reg = <0x2>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0xc>;
			};
		};

		cpu3: cpu@3 {
			phandle = <0x9>;
			device_type = "cpu";
			reg = <0x3>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0xa>;
			};
		};

		cpu4: cpu@4 {
			phandle = <0x7>;
			device_type = "cpu";
			reg = <0x4>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x8>;
			};
		};

		cpu5: cpu@5 {
			phandle = <0x5>;
			device_type = "cpu";
			reg = <0x5>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x6>;
			};
		};

		cpu6: cpu@6 {
			phandle = <0x3>;
			device_type = "cpu";
			reg = <0x6>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x4>;
			};
		};

		cpu7: cpu@7 {
			phandle = <0x1>;
			device_type = "cpu";
			reg = <0x7>;
			status = "okay";
			compatible = "riscv";
			riscv,isa = "rv64imafdcsu";
			mmu-type = "riscv,sv48";

			interrupt-controller {
				#interrupt-cells = <0x1>;
				interrupt-controller;
				compatible = "riscv,cpu-intc";
				phandle = <0x2>;
			};
		};

		cpu-map {
			cluster0 {
				core0 {
					cpu = <0xf>;
				};
				core1 {
					cpu = <0xd>;
				};
				core2 {
					cpu = <0xb>;
				};
				core3 {
					cpu = <0x9>;
				};
				core4 {
					cpu = <0x7>;
				};
				core5 {
					cpu = <0x5>;
				};
				core6 {
					cpu = <0x3>;
				};
				core7 {
					cpu = <0x1>;
				};
			};
		};
	};

    /* soc 서브 노드 정의, 시스템 주변 장치 정보 정의용 */   
	soc {
		#address-cells = <0x2>; /* 노드의 reg 주소 폭은 64비트 */
		#size-cells = <0x2>;    /* 노드의 reg 크기 폭은 64비트 */
		compatible = "simple-bus";
		ranges;                 /* 서브 노드 주소 공간과 부모 주소 공간의 매핑 방식, 여기서는 직접 매핑 */

        /* 그 다음으로 uart 정보와 인터럽트 컨트롤러 정보가 나옴, 현재 soc에는 이 두 가지 주변 장치만 있음, 이는 장치 드라이버의 구체적인 속성으로, 드라이버 소스 코드와 함께 사용법을 확인해야 함 */   
		uart0: uart0@10000000 {
			interrupts = <0xa>;
			interrupt-parent = <0x11>;
			clock-frequency = <0x384000>;
			reg = <0x0 0x10000000 0x0 0x100>;
			compatible = "ns16550a";
		};

		uart1: uart1@10001000 {
			interrupts = <0xa>;
			interrupt-parent = <0x11>;
			clock-frequency = <0x384000>;
			reg = <0x0 0x10001000 0x0 0x100>;
			compatible = "ns16550a";
		};

        uart2: uart2@10002000 {
			interrupts = <0xa>;
			interrupt-parent = <0x11>;
			clock-frequency = <0x384000>;
			reg = <0x0 0x10002000 0x0 0x100>;
			compatible = "ns16550a";
		};

		plic@c000000 {
			phandle = <0x11>;
			riscv,ndev = <0x35>;
			reg = <0x0 0xc000000 0x0 0x210000>;
			interrupts-extended = <0x10 0xb 0x10 0x9 0xe 0xb 0xe 0x9 0xc 0xb 0xc 0x9 0xa 0xb 0xa 0x9 0x8 0xb 0x8 0x9 0x6 0xb 0x6 0x9 0x4 0xb 0x4 0x9 0x2 0xb 0x2 0x9>;
			interrupt-controller;
			compatible = "riscv,plic0";
			#interrupt-cells = <0x1>;
			#address-cells = <0x0>;
		};

		clint@2000000 {
			interrupts-extended = <0x10 0x3 0x10 0x7 0xe 0x3 0xe 0x7 0xc 0x3 0xc 0x7 0xa 0x3 0xa 0x7 0x8 0x3 0x8 0x7 0x6 0x3 0x6 0x7 0x4 0x3 0x4 0x7 0x2 0x3 0x2 0x7>;
			reg = <0x0 0x2000000 0x0 0x10000>;
			compatible = "riscv,clint0";
		};
	};
};

위 dts 를 test 폴더에 copy 후 dtb 로 변환한다. 그리고 fw.bin 의 512 위치에 write 한다.

dtc -I dts -O dtb -o quard-star-sbi.dtb quard-star-sbi.dts
dd of=fw.bin bs=1k conv=notrunc seek=512 if=quard-star-sbi.dtb

 

lowlevelfw 작성

앞서 작업한 결과로 우리의 펌웨어는 세 가지 내용을 포함하게 되었다. 먼저 0x20000000의 test_fw, 그 다음 0x20080000 주소의 dtb 파일, 그리고 0x20200000 주소의 OpenSBI 프로그램이 있다. 이제 test_fw 코드를 다시 수정하여 OpenSBI를 DDR 주소 0x80000000로 로드하고, dtb를 DDR 주소 0x82200000로 로드한 후, 0x80000000로 점프하여 실행하도록 한다. 코드는 매우 쉽게 작성할 수 있으며, 아래와 같다(이전 소절에서 이미 설명한 어셈블리 코드는 주석을 생략한다)

    .macro loop,cunt          /* 간단한 루프 매크로 정의, cunt는 루프 매개변수 */
    li      t1, 0xffff        /* 즉시 값을 t1에 로드 */
    li      t2, \cunt         /* 즉시 값을 t2에 로드 */
1:
    nop                      /* NOP 명령어 */
    addi    t1, t1, -1      /* t1을 1 감소 */
    bne     t1, x0, 1b       /* t1이 0이 아니면 이전 1 위치로 점프 */
    li      t1, 0xffff        /* 즉시 값을 t1에 로드 */
    addi    t2, t2, -1      /* t2를 1 감소 */
    bne     t2, x0, 1b       /* t2가 0이 아니면 이전 1 위치로 점프 */
    .endm                   /* 매크로 종료 */

    .macro load_data,_src_start,_dst_start,_dst_end /* 간단한 데이터 로드 매크로 정의 (여기서는 워드 단위로 데이터를 복사하지만, 실제로는 64비트에서 더블 워드 단위로 복사하면 효율이 더 높다), _src_start는 소스 주소, _dst_start는 대상 주소, _dst_end는 대상 종료 주소 */
    bgeu    \_dst_start, \_dst_end, 2f /* 대상 종료 주소가 시작 주소보다 크면 유효성 검사 */
1:
    lw      t0, (\_src_start)         /* 소스 주소의 데이터를 t0에 로드 */
    sw      t0, (\_dst_start)        /* t0의 데이터를 대상 주소에 저장 */
    addi    \_src_start, \_src_start, 4 /* 소스 주소를 4 증가 */
    addi    \_dst_start, \_dst_start, 4 /* 대상 주소를 4 증가 */
    bltu    \_dst_start, \_dst_end, 1b /* 대상 주소가 종료 주소보다 작으면 이전 1 위치로 점프 */
2:
    .endm

    .section .text
    .globl _start
    .type _start,@function

_start:
    // OpenSBI 로드
    // [0x20200000:0x20400000] --> [0x80000000:0x80200000]
    li      a0, 0x202
    slli    a0, a0, 20      // a0 = 0x20200000
    li      a1, 0x800
    slli    a1, a1, 20      // a1 = 0x80000000
    li      a2, 0x802
    slli    a2, a2, 20      // a2 = 0x80200000
    load_data a0,a1,a2    /* 0x20200000에서 0x80000000로 복사 */

    // dtb 로드
    // [0x20080000:0x20100000] --> [0x82200000:0x82280000]
    li      a0, 0x2008
    slli    a0, a0, 16       // a0 = 0x20080000
    li      a1, 0x822
    slli    a1, a1, 20       // a1 = 0x82200000
    li      a2, 0x8228
    slli    a2, a2, 16       // a2 = 0x82280000
    load_data a0,a1,a2  /* 0x20080000에서 0x82200000로 복사 */

    csrr    a0, mhartid
    li      t0, 0x0     
    beq     a0, t0, _no_wait /* core0이 아니면 1000 루프 후 시작, core0은 OpenSBI의 콜드 부팅 코어로 설정 */
    loop    0x1000
_no_wait:
    li      a1, 0x822
    slli    a1, a1, 20       // a1 = 0x82200000
    li      t0, 0x800
    slli    t0, t0, 20       // t0 = 0x80000000
    jr      t0                /* 이 시점에 a0에는 core의 hart ID가, a1에는 디바이스 트리 dtb의 시작 주소가, t0에는 OpenSBI 프로그램의 DDR 주소가 있음. jr 명령어로 OpenSBI 프로그램으로 점프 */

    .end

 

어셈블리코드가 좀 아쉽긴하다. hartid 를 먼저 확인한 후 core 0 이 주도해서 코드를 flash 에서 ddr 로 로드하는게 맞는 것 같은데.. 실제로라면 모든 hart 가 동시에 flash 에 접근해서 메모리를 복사해대니, race 가 엄청나게 발생할 것 같다.

아무튼 qemu functional emulator 에서는 잘 동작하는 것이 확인 된다.

 

나름대로 수정한 어셈블리코드는 아래와 같다. _start 를 하자마자 hartid 를 검사하고 hart0 만 메모리를 복사한다.

    .macro load_data,_src_start,_dst_start,_dst_end /* 간단한 데이터 로드 매크로 정의 (여기서는 워드 단위로 데이터를 복사하지만, 실제로는 64비트에서 더블 워드 단위로 복사하면 효율이 더 높다), _src_start는 소스 주소, _dst_start는 대상 주소, _dst_end는 대상 종료 주소 */
    bgeu    \_dst_start, \_dst_end, 2f /* 대상 종료 주소가 시작 주소보다 크면 유효성 검사 */
1:
    lw      t0, (\_src_start)         /* 소스 주소의 데이터를 t0에 로드 */
    sw      t0, (\_dst_start)        /* t0의 데이터를 대상 주소에 저장 */
    addi    \_src_start, \_src_start, 4 /* 소스 주소를 4 증가 */
    addi    \_dst_start, \_dst_start, 4 /* 대상 주소를 4 증가 */
    bltu    \_dst_start, \_dst_end, 1b /* 대상 주소가 종료 주소보다 작으면 이전 1 위치로 점프 */
2:
    .endm

    .section .text
    .globl _start
    .type _start,@function

_start:
    csrr    a0, mhartid
    li      t0, 0
    bne     a0, t0, wait_for_core0

    // OpenSBI 로드
    // [0x20200000:0x20400000] --> [0x80000000:0x80200000]
    li      a0, 0x202
    slli    a0, a0, 20      // a0 = 0x20200000
    li      a1, 0x800
    slli    a1, a1, 20      // a1 = 0x80000000
    li      a2, 0x802
    slli    a2, a2, 20      // a2 = 0x80200000
    load_data a0,a1,a2    /* 0x20200000에서 0x80000000로 복사 */

    // dtb 로드
    // [0x20080000:0x20100000] --> [0x82200000:0x82280000]
    li      a0, 0x2008
    slli    a0, a0, 16       // a0 = 0x20080000
    li      a1, 0x822
    slli    a1, a1, 20       // a1 = 0x82200000
    li      a2, 0x8228
    slli    a2, a2, 16       // a2 = 0x82280000
    load_data a0,a1,a2  /* 0x20080000에서 0x82200000로 복사 */

    la      t0, ready_flag
    li      t1, 1
    sw      t1, 0(t0)
    j      _no_wait


wait_for_core0:
    la      t0, ready_flag
    lw      t1, 0(t0)
    beqz    t1, wait_for_core0

_no_wait:
    li      a1, 0x822
    slli    a1, a1, 20       // a1 = 0x82200000
    li      t0, 0x800
    slli    t0, t0, 20       // t0 = 0x80000000
    jr      t0                /* 이 시점에 a0에는 core의 hart ID가, a1에는 디바이스 트리 dtb의 시작 주소가, t0에는 OpenSBI 프로그램의 DDR 주소가 있음. jr 명령어로 OpenSBI 프로그램으로 점프 */


.section    .bss
    .align 4
ready_flag:
    .word   0

    .end

 

여기서 주목해야할 점은, OpenSBI 로 점프하기전에 a1 레지스터에 dtb 가 저장되어있는 주소를 저장해야한다.

OpenSBI 는 이를 통해 DTB의 위치를 알 수 있다.

 

 

빌드, flash, qemu run 이 귀찮으면 아래 스크립트를 참고하자.

#!/bin/sh

# Check if the input argument is provided
if [ -z "$1" ]; then
  echo "Usage: $0 <filename>"
  exit 1
fi

if [ $1 = "clean" ]; then
    rm -rf *.o
    rm -rf test_fw.elf
    rm -rf test_fw.map
    rm -rf test_fw.dump
    rm -rf test_fw.bin
    rm -rf fw.bin
    exit 1
fi



filename=$(basename "$1" .S)
objectname="${filename}.o"

rm -rf *.o
rm -rf test_fw.elf
rm -rf test_fw.map
rm -rf test_fw.dump
rm -rf test_fw.bin

riscv64-unknown-elf-gcc -c $1 -o ${objectname}
riscv64-unknown-elf-gcc -nostartfiles -T ./boot.lds -Wl,-Map=./test_fw.map -Wl,--gc-sections ${objectname} -o test_fw.elf
riscv64-unknown-elf-objcopy -O binary -S test_fw.elf test_fw.bin
riscv64-unknown-elf-objdump --source --demangle --disassemble --reloc --wide test_fw.elf > test_fw.dump

이걸로 test.fw 를 빌드하고

 

#!/bin/bash

rm -rf fw.bin

#dtc -I dts -O dtb -o quard-star-sbi.dtb quard-star-sbi.dts


dd of=fw.bin bs=1k count=32k if=/dev/zero
dd of=fw.bin conv=notrunc seek=0 if=test_fw.bin
dd of=fw.bin bs=1k conv=notrunc seek=512 if=quard-star-sbi.dtb
dd of=fw.bin bs=1k conv=notrunc seek=2k if=../opensbi-1.2/build/platform/quard_star/firmware/fw_jump.bin

이걸로 dd 이미지를 만들고

 

#!/bin/sh

../output/qemu/bin/qemu-system-riscv64 -M quard-star -m 1G -drive if=pflash,format=raw,file=./fw.bin -nographic

이걸로 qemu run 을 하면 된다.

댓글