본문 바로가기
Project/Embedded

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

by FastBench 2024. 6. 12.

이전글

https://microelectronics.tistory.com/68

https://microelectronics.tistory.com/69

https://microelectronics.tistory.com/70

https://microelectronics.tistory.com/71

 

원본 게시글

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

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

 

OpenSBI 소스 코드 구조

현재 OpenSBI의 소스 코드 구조는 비교적 간단하고 명확하다. 주요 디렉토리와 그 역할은 다음과 같다:

  • sbi 디렉토리: OpenSBI의 핵심 파일들이 위치한 곳이다. 여기에는 시스템 초기화, 메모리 관리, 인터럽트 처리 등 핵심 기능을 구현하는 코드가 포함되어 있다.
  • utils 디렉토리: 주로 디바이스 트리 파싱을 위한 libfdt 및 기타 유틸리티 파일들이 위치한 곳이다.
    • 디바이스 트리: 디바이스 트리는 Linux 커널에서 널리 사용되는 하드웨어 기술 파일로, 드라이버 코드를 간소화하고 재사용성과 이식성을 높이는 데 도움을 준다. 디바이스 트리를 통해 하드웨어 구성을 기술하고, 이를 기반으로 드라이버가 하드웨어를 초기화하고 제어할 수 있다. 디바이스 트리는 점점 더 많은 임베디드 플랫폼 코드 프로젝트에서 사용되고 있다.
  • platform 디렉토리: 특정 보드에 특화된 코드를 정의하는 곳이다. 각 보드에 필요한 초기화 코드와 하드웨어 설정 코드가 포함되어 있다.

OpenSBI는 RISC-V 시스템에서 부팅 시 필요한 초기화와 환경 설정을 담당하며, 부트로더와 운영 체제 간의 인터페이스를 제공한다. OpenSBI는 RISC-V 아키텍처의 표준화를 지원하며, 다양한 운영 체제들이 일관된 방식으로 하드웨어에 접근하고 제어할 수 있게 한다.

 

목표 부트 로드 디자인

부트 로드 프로세스 설계

  1. BL0 (BootLoader 0):
    • 시스템의 초기화 첫 단계로, 기본적인 하드웨어 초기화와 최소한의 설정을 수행한다.
  2. BL1 (BootLoader 1):
    • pflash의 첫 주소에서 시작하여 실행된다.
    • DDR 메모리를 초기화한다.
    • pflash에서 OpenSBI(BL2) 프로그램과 필요한 디바이스 트리 파일을 로드하여 DDR로 이동시킨다.
  3. BL2 (OpenSBI):
    • 시스템 초기화와 환경 설정을 마무리하고, 운영 체제를 실행할 준비를 한다.

OpenSBI Firmware

먼저 OpenSBI에서 생성되는 펌웨어는 세 가지 종류가 있다. 여기서는 fw_jump만 사용하므로 이 과정만 분석한다.

원 게시글에는 opensbi-0.9 버전으로 진행을 하는데, 현재는 opensbi-1.2 로 update 가 된 상황이다.

코드 흐름의 일부가 변경되어서, 일단은 1.2 버전으로 게시글을 작성하기보다 원글에 나온 0.9버전에 맞는 설명을 따르겠다.  추후 코드 및 fw 아키텍처 분석은 별도의 게시글에서 진행해보도록 하자.

 

opensbi-0.9/firmware/fw_base.ld.S 파일은 fw_jump 펌웨어의 링크 스크립트를 정의한다.

이 링크 스크립트는 C 전처리 매크로가 확장된 후 사용되지만, 읽는 데는 지장이 없다.

FW_TEXT_START는 펌웨어 실행의 시작 주소를 정의하고, 그 다음으로 text 섹션, ro 섹션, data 섹션, bss 섹션이 차례로 배치된다. bss 섹션 뒤는 스택 공간으로 사용된다.

 

이 링크 스크립트는 매우 간단하여, 임베디드 베어메탈 개발에 익숙한 사람이라면 쉽게 이해할 수 있다.

여기서는 자세히 설명하지 않겠다. 주의할 점은 data 섹션에 LMA가 없다는 것이다.

즉, OpenSBI는 DDR 내에서 실행되며, 플래시 내에서 XIP(Execute In Place) 모드로 실행될 수 없다. 물론 링커 스크립트를 수정하면 이를 구현할 수 있다.

 

Code Flow

opensbi-0.9/firmware/fw_base.S 어셈블리 파일이 바로 OpenSBI의 시작 지점이다.

.entry 섹션의 _start 심볼은 링크 스크립트의 첫 번째 코드 섹션이며, 상위 로더 프로그램이 로드 완료된 후 이 주소로 점프하여 실행된다. 먼저 start 코드는 부트 코어가 아닌 경우 _wait_for_boot_hart로 점프하여 대기한다.

 

부트 코어는 먼저 한 번의 _relocate 코드를 실행한다.

OpenSBI가 자신의 링크 주소 내에서 실행되지 않는다면, 자체 코드를 타겟 RAM으로 복사하여 실행한다.

따라서 SPL처럼 플래시에서 시작할 수 있다.

그러나 우리는 자체 작성한 로더 프로그램을 사용하므로 이 _relocate 단계는 실행되지 않는다.

 

이후의 과정은 .bss 섹션을 초기화하고 SP 포인터를 설정한다.

다음으로 fw_platform_init 함수를 호출하는데, 이때 매개변수로 a0는 hart ID, a1는 fdt 주소, a2, a3, a4는 상위 로더 프로그램의 매개변수가 전달된다.

 

이 함수는 플랫폼에서 구현하며, 사용되지 않으면 약한 정의의 빈 함수로 대체된다.

플랫폼 함수의 구체적인 내용은 나중에 구현할 때 살펴보겠다. 여기서는 건너뛰겠다.

 

다음은 _scratch_init 함수로, scratch는 또 다른 SP 포인터와 유사한 것으로, 데이터를 저장하기 위해 메모리 영역을 정의한다. 마치 스택처럼 후입선출 방식으로 동작한다. _scratch_init는 순서대로 SBI의 다음 단계 프로그램의 주소와 매개변수 정보를 기록한다.

 

이 정보는 프로젝트 내의 사전 정의된 매크로에 의해 지정된다. 여기서 우리의 설계에 큰 영향을 미치지 않는다.

우리는 디바이스 트리 파일을 사용하여 OpenSBI에 다음 단계 부팅 주소 등의 정보를 제공한다.

그 다음은 _fdt_reloc로, 코드 reloc과 유사하게 fdt를 처리한다. 우리의 설계에서는 이 단계가 실행되지 않는다.

 

마지막으로 _start_warm에 도달한다.

이 시점에서 부트 코어는 플래그를 해제하고, _wait_for_boot_hart에서 대기 중이던 다른 코어들도 _start_warm으로 점프한다. _start_warm은 각 코어의 리셋 레지스터에 대해 자신만의 스택 공간을 설정하고, 트랩 예외 등을 구성한 후 sbi_init을 호출하여 어셈블리 코드에서 벗어난다.

 

이후 코드는 이해하기 쉬운 C 언어로 작성된다. 여기까지가 startup 코드 분의 끝이다. 간단한 흐름도를 제시한다.

https://quard-star-tutorial.readthedocs.io/zh-cn/latest/ch5-2.html

 

init 함수

opensbi-0.9/lib/sbi/sbi_init.c는 SBI 핵심 코드의 시작점이다. sbi_init 함수는 주로 cold boot과 warm boot으로 나뉜다.

어느 방식이든, 주요 기능은 디바이스 트리를 파싱하고 관련 하드웨어 장치를 초기화하는 것이다.

 

중요하게 초기화되는 하드웨어 장치들로는 IRQ, TLB, IPI, ECALL, domain, PMP, 타이머(timer), 콘솔(console) 등이 있다. 요약하면, 시스템 호출을 설정하고, 하드웨어 권한을 구성하며, 시스템 MMIO 및 메모리 영역을 도메인으로 나누는 작업이 SBI가 수행하는 주요 작업이다. 이러한 작업이 완료되면 모든 도메인은 부트 코어에서 시작하여 하위 프로그램으로 이어지는 SBI의 작업을 완료하게 된다.

 

여기서는 구체적인 설명을 생략하지만, 관심이 있는 부분이 있다면 직접 코드를 확인해 보자. 

 

사용자 입장에서 해야 할 일은 사용 중인 하드웨어에 필요한 디바이스 트리 FDT 파일을 작성하고, 도메인 노드 관련 설명을 작성하는 것이다.

이렇게 하면 SBI 초기화가 이러한 하드웨어 작업을 직접 처리할 수 있게 된다.

물론, 사용 중인 장치에 관련 드라이버가 없는 경우, 기존 SBI 드라이버를 참고하여 디바이스 트리 파싱 및 구성을 위한 코드를 작성해야 한다.

OpenSBI 에서 도메인?  해당 부분이 이해가 안되서 따로 정리를 해보았다.

OpenSBI에서 "도메인(domain)"이라는 용어는 시스템 내에서 서로 다른 권한과 역할을 가진 하드웨어 자원과 소프트웨어 실행 환경을 구분하는 논리적 구역을 의미한다.

OpenSBI에서의 도메인

OpenSBI는 이러한 도메인 기능을 지원하여, RISC-V 시스템에서의 보안과 자원 관리를 강화한다. 이를 위해 다음과 같은 작업을 수행한다:
  1. PMP(Physical Memory Protection) 설정:
    • 각 도메인에 대해 물리적 메모리 보호 영역을 설정하여, 도메인 간의 메모리 접근을 제한한다.
  2. 시스템 호출 환경 설정:
    • 각 도메인에 대해 적절한 시스템 호출 인터페이스를 제공하여, 도메인 간의 권한을 관리한다.
  3. 하드웨어 자원 할당:
    • 각 도메인에 필요한 하드웨어 자원(I/O 장치, 인터럽트 등)을 할당하고, 이를 격리하여 사용할 수 있도록 한다.

 

Platform 추가

SBI는 디바이스 트리를 사용하여 플랫폼 부분 코드의 개발을 크게 줄였다.

대부분의 작업은 몇 가지 후크(hook) 함수를 제공하여, 다양한 보드의 특수한 설정 코드를 SBI 초기화 과정에 삽입하는 것이다. 아래에서는 관련 데이터 구조와 후크 함수 집합을 구성하는 방법을 명확히 볼 수 있다.

우리가 사용하는 하드웨어 IP 드라이버는 이미 SBI 소스 코드에 포함되어 있으므로, 대부분의 함수는 FDT(디바이스 트리)를 파싱하여 필요한 설정을 완료할 수 있다. 따라서 독립적인 설정 코드를 작성할 필요가 없다.

 

platform/quard_star/platform.c

const struct sbi_platform_operations platform_ops = {
	.early_init		= quard_star_early_init,
	.final_init		= quard_star_final_init,
	.early_exit		= quard_star_early_exit,
	.final_exit		= quard_star_final_exit,
	.domains_init		= quard_star_domains_init,
	.console_init		= fdt_serial_init,
	.irqchip_init		= fdt_irqchip_init,
	.irqchip_exit		= fdt_irqchip_exit,
	.ipi_init		= fdt_ipi_init,
	.ipi_exit		= fdt_ipi_exit,
	.pmu_init		= generic_pmu_init,
	.pmu_xlate_to_mhpmevent = generic_pmu_xlate_to_mhpmevent,
	.get_tlbr_flush_limit	= quard_star_tlbr_flush_limit,
	.timer_init		= fdt_timer_init,
	.timer_exit		= fdt_timer_exit,
};

struct sbi_platform platform = {
	.opensbi_version	= OPENSBI_VERSION,
	.platform_version	= SBI_PLATFORM_VERSION(0x0, 0x01),
	.name			= "Quard-Star",
	.features		= SBI_PLATFORM_DEFAULT_FEATURES,
	.hart_count		= SBI_HARTMASK_MAX_BITS,
	.hart_index2id		= quard_star_hart_index2id,
	.hart_stack_size	= SBI_PLATFORM_DEFAULT_HART_STACK_SIZE,
	.platform_ops_addr	= (unsigned long)&platform_ops
};

강조하고자 하는 두개의 함수가 있다.

fw_platform_init 함수는 주로 FDT를 통해 일부 설정을 플랫폼 구조체에 읽어들이며, 이는 디바이스 트리를 통해 하드웨어 정보를 유연하게 전달하기 위한 것이다. 이는 플랫폼 코드가 다양한 벤더의 여러 보드에 맞게 적응할 수 있도록 한다.

unsigned long fw_platform_init(unsigned long arg0, unsigned long arg1,
				unsigned long arg2, unsigned long arg3,
				unsigned long arg4)
{
	const char *model;
	void *fdt = (void *)arg1;
	u32 hartid, hart_count = 0;
	int rc, root_offset, cpus_offset, cpu_offset, len;

	root_offset = fdt_path_offset(fdt, "/");
	if (root_offset < 0)
		goto fail;

	model = fdt_getprop(fdt, root_offset, "model", &len);
	if (model)
		sbi_strncpy(platform.name, model, sizeof(platform.name) - 1);

	cpus_offset = fdt_path_offset(fdt, "/cpus");
	if (cpus_offset < 0)
		goto fail;

	fdt_for_each_subnode(cpu_offset, fdt, cpus_offset) {
		rc = fdt_parse_hart_id(fdt, cpu_offset, &hartid);
		if (rc)
			continue;

		if (SBI_HARTMASK_MAX_BITS <= hartid)
			continue;

		quard_star_hart_index2id[hart_count++] = hartid;
	}

	platform.hart_count = hart_count;

	/* Return original FDT pointer */
	return arg1;

fail:
	while (1)
		wfi();
}

final_init 함수는 FDT 라이브러리의 몇 가지 함수들을 호출하지만, 매우 중요하다.

이 함수는 플랫폼 초기화가 완료된 후 디바이스 트리를 수정하여, 초기화가 완료된 후의 상태 정보를 반영한다.

이는 입력된 FDT와 플랫폼 코드가 공동으로 작용한 후의 실제 하드웨어 구성 정보를 반영한다.

static int quard_star_final_init(bool cold_boot)
{
	void *fdt;

	if (cold_boot)
		fdt_reset_init();

	if (!cold_boot)
		return 0;

	fdt = sbi_scratch_thishart_arg1_ptr();

	fdt_cpu_fixup(fdt);
	fdt_fixups(fdt);
	fdt_domain_fixup(fdt);

	return 0;
}
플랫폼 코드도 어렵지 않다.
파일은 하나뿐이고, objects.mk에 platform-objs-y += platform.o를 추가하면 컴파일에 포함된다.
config.mk에서는 우리가 필요한 FW_JUMP와 FW_TEXT_START를 DDR의 시작 주소로 설정한다.
 
FW_JUMP_ADDR는 사용하지 않지만, 컴파일 요구사항 때문에 정의해서 0으로 설정하면 된다.
 
* 게시글에는 OpenSBI-0.9 설명 그대로 번역을 했지만, 실제 테스트는 OpenSBI-1.2로 진행했다.
의문인 점은 최신 레포지토리 (opensbi-1.2) quard_star_tutorial/opensbi-1.2/platform/quard_star/objects.mk 를 보면 아래와 같이 FW_TEXT_STAR 의 주소가 BFF80000 으로 되어있다.

단순 opensbi 의 시작 주소에 해당하므로 크게 신경쓸 필요는 없다. 이후에 진행될 챕터에서 bff80000 로 바꾸기 때문에 주소가 저렇게 셋팅 되어있다. 80000000 으로 바꾸어도 openSBI 를 띄우는데는 문재가 없다.
 
펌웨어 패키징

Make를 사용하여 PLATFORM=quard_star를 설정하면 컴파일이 완료되고, 생성된 fw_jump.bin을 dd 명령어를 사용하여 pflash 펌웨어의 2K 오프셋 영역에 추가하면, 펌웨어와 lowlevel_fw가 함께 패키징된다.

cd opensbi-1.2
make CROSS_COMPILE=riscv64-unknown-linux-gnu- PLATFORM=quard_star

cd ../test
rm -rf fw.bin
dd of=fw.bin bs=1k count=32k if=/dev/zero
dd of=fw.bin bs=1k conv=notrunc seek=0 if=test_fw.bin
dd of=fw.bin bs=1k conv=notrunc seek=2k if=../opensbi-1.2/build/platform/quard_star/firmware/fw_jump.bin

 

아직 opensbi fw 를 ddr 로 load 하는 test_fw.bin 파일이 update(OpenSBI FW 를 DDR 로 점프시키는 역할) 되지 않았으므로 qemu 에서 실행을 해도 동작하지 않는다.
다음 게시글에서 dts 를 학습하고 작성하여, 실제로 OpenSBI 로 진입하는 부분을 다룬다.

댓글