본문 바로가기
Project/Embedded

[QEMU] 임베디드 리눅스 시스템 구축 프로젝트 - 가상 보드 등록

by FastBench 2024. 6. 8.

github 를 뒤지다가 rom 코드 부터 커널을 step by step 으로 올리는 친절한 예제가 있어, 따라해보며 블로그에 포스팅을 해보려 한다.

 

원본 게시글

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

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

 

간략히 보니, QEMU 빌드부터 진행하는데, 타겟 SoC 의 구성을 셋팅하는 것 부터 시작해서 아키텍쳐 적으로 이해하는데 많은 도움이 될 것 같다.

 

문서가 중국어로 되어있기 때문에, ai 의 도움을 받아 한국어로 번역을 하면서 첨언할 부분을 추가할 예정이다.

 

의존성 패키지 설치

sudo apt install ninja-build pkg-config libglib2.0-dev libpixman-1-dev libgtk-3-dev libcap-ng-dev libattr1-dev libsdl2-dev device-tree-compiler bison flex gperf intltool mtd-utils libslirp-dev

 

QEMU 환경 구축

본 예제에서는 8.0.0 버전을 사용한다. 레포지토리에 빌드에 필요한 소스코드가 포함되어있으므로 별도로 진행할 필요가 없다. 친절하게 build.sh 스크립트에 해당 내용이 명시되어있으므로 그대로 따라한다.

./build.sh qemu

스크립트 내용을 살펴보면 알겠지만, 실행되는 세부 내용은 아래와 같다.

 

  • --prefix=$SHELL_FOLDER/output/qemu:
    • QEMU가 설치될 디렉토리를 지정한다. 
  • --target-list=riscv64-softmmu:
    • QEMU가 지원할 타겟 아키텍처를 지정한다. 여기서는 riscv64 아키텍처를 대상으로 하며, softmmu는 소프트웨어 MMU(메모리 관리 유닛)을 사용함을 의미한다. 이는 RISC-V 64비트 하드웨어를 시뮬레이션하는 데 사용된다.
  • --enable-gtk:
    • GTK+ 그래픽 프론트엔드를 활성화한다. 이를 통해 QEMU의 그래픽 인터페이스를 사용할 수 있다.
  • --enable-virtfs:
    • VirtFS를 활성화한다. VirtFS는 호스트와 게스트 간의 파일 시스템 공유를 가능하게 하는 기능이다.
  • --disable-gio:
    • GIO(GNOME Input/Output)를 비활성화한다. GIO는 GLib의 입출력, 파일 시스템, 네트워킹 등의 기능을 제공하는 모듈이다.
  • --enable-plugins:
    • QEMU 플러그인을 활성화한다. 이를 통해 QEMU에 다양한 기능을 추가할 수 있다.
  • --audio-drv-list=pa,alsa,sdl,oss:
    • 사용할 오디오 드라이버를 지정한다. 여기서는 pa(PulseAudio), alsa(Advanced Linux Sound Architecture), sdl(Simple DirectMedia Layer), oss(Open Sound System) 드라이버를 사용하도록 설정한다.

 

설치가 완료되었는지 제대로 확인한다.

$ output/qemu/bin/qemu-system-riscv64 -version
QEMU emulator version 8.0.0
Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers

 

QEMU 에 가상의 보드 추가하기

프로젝트에 진행할 가상의 Quard-star board에 관한 소스코드는 이미 레포지토리에 포함되어있다.

따라하는 입장에서 별도로 진행할 내용은 없고, 그냥 기작성된 코드를 이해해보자.

/qemu-8.0.0/hw/riscv/quard_star.c

보드 등록 및 정의

static const TypeInfo quard_star_machine_typeinfo = {
    .name       = MACHINE_TYPE_NAME("quard-star"),
    .parent     = TYPE_MACHINE,
    .class_init = quard_star_machine_class_init,
    .instance_init = quard_star_machine_instance_init,
    .instance_size = sizeof(RISCVVirtState),
};

static void quard_star_machine_init_register_types(void)
{
    type_register_static(&quard_star_machine_typeinfo);
}

type_init(quard_star_machine_init_register_types)

 

  • quard_star_machine_typeinfo: QEMU 시스템에 보드를 등록하기 위한 구조체이다. 보드의 이름, 부모 타입, 클래스 초기화 함수, 인스턴스 초기화 함수, 인스턴스 크기를 정의한다.
  • quard_star_machine_init_register_types: quard_star_machine_typeinfo 구조체를 QEMU 타입 시스템에 등록하는 함수이다.
  • type_init: QEMU 초기화 시 quard_star_machine_init_register_types 함수를 호출하여 보드를 등록한다.

보드 리소스 초기화 함수 및 코어 수 등록

static void quard_star_machine_class_init(ObjectClass *oc, void *data)
{
    MachineClass *mc = MACHINE_CLASS(oc);

    mc->desc = "RISC-V Quard Star board";
    mc->init = quard_star_machine_init;
    mc->max_cpus = QUARD_STAR_MANAGEMENT_CPU_COUNT +
                   QUARD_STAR_COMPUTE_CPU_COUNT;
	...
}

 

  • mc->init: 보드 초기화 함수로 quard_star_machine_init을 설정한다.
  • mc->max_cpus: 보드가 지원하는 최대 SMP(대칭 멀티프로세싱) 코어 수를 설정한다.

qemu 소스 디렉토리의 include/hw/riscv/quard-star.h 를 참고하면

management cpu 는 1개, compute cpu 는 7개로, 총 옥타코어이다.

가상 CPU 구성

static void quard_star_cpu_create(MachineState *machine)
{
    QuardStarState *s = RISCV_VIRT_MACHINE(machine);

    object_initialize_child(OBJECT(machine), "c-cluster",
                                    &s->c_cluster, TYPE_CPU_CLUSTER);
    qdev_prop_set_uint32(DEVICE(&s->c_cluster), "cluster-id", 0);
    object_initialize_child(OBJECT(&s->c_cluster), "c-cpus",
                                &s->c_cpus, TYPE_RISCV_HART_ARRAY);
    object_property_set_str(OBJECT(&s->c_cpus), "cpu-type",
                            machine->cpu_type, &error_abort);
    object_property_set_int(OBJECT(&s->c_cpus), "hartid-base",
                            0, &error_abort);
    object_property_set_int(OBJECT(&s->c_cpus), "num-harts",
                            QUARD_STAR_COMPUTE_CPU_COUNT, &error_abort);
    object_property_set_int(OBJECT(&s->c_cpus), "resetvec", 
                            quard_star_memmap[QUARD_STAR_MROM].base, 
                            &error_abort);
    sysbus_realize(SYS_BUS_DEVICE(&s->c_cpus), &error_fatal);
    qdev_realize(DEVICE(&s->c_cluster), NULL, &error_abort);
    // similar as above. Set r-cluster
    ...
    ...
}

보드 내부에 cpu 가 클러스터로 구성되어있다. (c-cluster, r-cluster) 

c-cluster 는 compute 전용, r-cluster 는 management 전용 cpu 로 보인다.

 

virt machine 을 보면, num-harts 관련 함수의 인자로 hart_count 변수를 입력 받는데, 해당 예제의 경우 상수값이 들어간다.

따라서 이후의 -smp 옵션도 따로 필요없을것으로 보인다. (디폴트가 7+1 코어 구성)

메모리 관련 리소스 정의

여기서는 CPU 내부에 마스크롬(Mask ROM), SRAM, DDR 메모리를 정의한다.

  • 마스크롬(Mask ROM): CPU가 부팅될 때 내부의 고정된 코드를 실행하는 데 사용된다.
  • SRAM: 초기 부팅 코드의 데이터 저장 공간으로 사용된다.
  • DDR: 일반적으로 실제 보드에서는 컨트롤러 초기화 후에 사용할 수 있지만, QEMU 시뮬레이션에서는 바로 사용할 수 있는 메모리로 설정된다. 그러나 현실성을 추구하기 위해 초기 부팅 시에는 이 메모리 공간을 사용하지 않는다.
static const MemMapEntry quard_star_memmap[] = {
    [QUARD_STAR_MROM]        = { 0x00000000,   0x20000 },
    [QUARD_STAR_SRAM]        = { 0x00020000,   0xe0000 },
    ...
    [QUARD_STAR_DRAM]        = { 0x80000000,       0x0 },
};
static void quard_star_memory_create(MachineState *machine)
{
    MemoryRegion *system_memory = get_system_memory();
    QuardStarState *s = RISCV_VIRT_MACHINE(machine);
    MemoryRegion *main_mem = g_new(MemoryRegion, 1);
    MemoryRegion *sram_mem = g_new(MemoryRegion, 1);
    MemoryRegion *mask_rom = g_new(MemoryRegion, 1);

    memory_region_init_ram(main_mem, NULL, "riscv_quard_star_board.dram",
                           machine->ram_size, &error_fatal);
    memory_region_add_subregion(system_memory, 
                                quard_star_memmap[QUARD_STAR_DRAM].base, main_mem);

    memory_region_init_ram(sram_mem, NULL, "riscv_quard_star_board.sram",
                           quard_star_memmap[QUARD_STAR_SRAM].size, &error_fatal);
    memory_region_add_subregion(system_memory, 
                                quard_star_memmap[QUARD_STAR_SRAM].base, sram_mem);

    memory_region_init_rom(mask_rom, NULL, "riscv_quard_star_board.mrom",
                           quard_star_memmap[QUARD_STAR_MROM].size, &error_fatal);
    memory_region_add_subregion(system_memory, 
                                quard_star_memmap[QUARD_STAR_MROM].base, mask_rom);

    quard_star_setup_rom_reset_vec(machine, &s->r_cpus, 
                              quard_star_memmap[QUARD_STAR_FLASH].base,
                              quard_star_memmap[QUARD_STAR_MROM].base,
                              quard_star_memmap[QUARD_STAR_MROM].size,
                              0x0, 0x0);
}

 

 

memory_region_init_ram 으로 특정 타입의 메모리 영역을 설정하고

memory_region_add_subregion 을 통해 시스템 메모리에 추가한다.

 

quard_star_setup_rom_reset_vec 함수 호출을 통해 rom code 와 관련된 설정을 한다.

아래에서 살펴보자

 

Mask ROM 코드를 MROM 영역에 로드

quard_star_setup_rom_reset_vec  을 통해 아래를 인자로 전달받는다

  • start_addr = FLASH 의 base address
  • rom_base = rom 의 base address
  • rom_size = rom 저장공간의 size
static void quard_star_setup_rom_reset_vec(MachineState *machine, 
                                RISCVHartArrayState *harts, hwaddr start_addr,
                                hwaddr rom_base, hwaddr rom_size,
                                uint64_t kernel_entry, uint32_t fdt_load_addr)
{
    QuardStarState *s = RISCV_VIRT_MACHINE(machine);
    uint32_t start_addr_hi32 = 0x00000000;

    if (!riscv_is_32bit(harts)) {
        start_addr_hi32 = start_addr >> 32;
    }
    /* reset vector */
    uint32_t reset_vec[10] = {
        0x00000297,                  /* 1:  auipc  t0, %pcrel_hi(fw_dyn) */
        0x02828613,                  /*     addi   a2, t0, %pcrel_lo(1b) */
        0xf1402573,                  /*     csrr   a0, mhartid  */
        0,
        0,
        0x00028067,                  /*     jr     t0 */
        start_addr,                  /* start: .dword */
        start_addr_hi32,
        fdt_load_addr,               /* fdt_laddr: .dword */
        0x00000000,
                                     /* fw_dyn: */
    };
    if (riscv_is_32bit(harts)) {
        reset_vec[3] = 0x0202a583;   /*     lw     a1, 32(t0) */
        reset_vec[4] = 0x0182a283;   /*     lw     t0, 24(t0) */
    } else {
        reset_vec[3] = 0x0202b583;   /*     ld     a1, 32(t0) */
        reset_vec[4] = 0x0182b283;   /*     ld     t0, 24(t0) */
    }

    /* copy in the reset vector in little_endian byte order */
    for (int i = 0; i < ARRAY_SIZE(reset_vec); i++) {
        reset_vec[i] = cpu_to_le32(reset_vec[i]);
    }

    ...
    
    rom_add_blob_fixed_as("mrom.reset", reset_vec, sizeof(reset_vec),
                          rom_base, &address_space_memory);
}

롬코드를 해석해보면, 아래와 같다.\.

  1. 0x0000_0000 _00 을 t0 에 load
    1. %pcrel_hi(fw_dyn) : 컴파일러 지시어로, 'fw_dyn 의 주소(0x28) - 현 pc값(0x00)'  의 상위 20비트
    2. 즉 t0 에 0x0 을 load 
  2. a2 에 0x00 을 저장 (이유는 모르겠음..)
  3. a0 에 hartid 를 load
  4. a1 에 t0 (0x0) + 32byte offset 에 위치한 값을  load => fdt_load_addr
  5. t0 에 t0 (0x0) + 24byte offset 에 위치한 값을 load => 매개변수로 전달받은 flash 메모리의 시작 위치
  6. t0 (flash 메모리의 시작 주소) 으로 점프 (risc-v ABI 규격에 따르면 a0 은 매개변수0, a1 은 매개변수 1 로 사용된다.)

댓글