系列文章 : [gem5] 從零開始的 gem5 學習筆記
這一篇文章,來嘗試從頭開始自己組裝一個 RISC-V 的 virtual platform,並且在這個 virtual plaform 裡面,運行一個簡單的 firmware。
這個 virtual platform 希望是越簡單越好 ( 甚至沒有 PLIC ),希望可以藉由這個 platform 來跑跑看其他簡單的實驗,或是拿來 trace 其他 model 的程式碼。
git clone git@github.com:TommyWu-fdgkhdkgh/simple-riscv-vp.git
cd simple-riscv-vp
# install riscv tool chain
make install/riscv64-unknown-elf/gcc
# 安裝一些 gem5 需要的東西
make gem5/apt-install
# 把 gem5 這個專案給 clone 下來
make gem5/clone
# 安裝 gem5 需要的 python 套件
make gem5/venv
# 編譯 gem5.opt
make gem5/build-opt
# 編譯 gem5.debug
make gem5/build-debug
# 運行 gem5 的 hello world,看一下編譯有沒有成功
make gem5/run-opt/hello-world
# 編譯 firmware
make firmware/build
# 運行 firmware
gem5/run-opt/simple-riscv-vp/firmware
# 用 minor CPU model 來運行 firmware
gem5/run-opt/simple-riscv-vp/firmware CPU_TYPE=minor
# 在另外一個 terminal,連進正在運行的 gem5
make gem5/term/run
假如有運行成功的話,預期 terminal 會輸出下面的訊息。
可以藉由輸入 m 來觸發一個 timer interrupt。
==== m5 terminal: Terminal 0 ====
start simple firmware!
============ menu ============
a : calculate a big number and insert mtime interrupt
m : set mtime cmp
==============================
m : set mtime cmp !
handle_interrupt !
timer interrupt !
{ https://github.com/TommyWu-fdgkhdkgh/simple-riscv-vp/blob/main/simple-riscv-vp.py }
class SimpleRiscvPlatform(SimplePlatform):
def __init__(self):
super(SimpleRiscvPlatform, self).__init__()
SimplePlatform
基本上就是什麼也不做的 Platform。
可以看一下其他人寫的 platform, 例如 HiFive
simple-riscv-vp.py 去描述我所有想放的 IPs,於是只好自己開一個空的 platform。Q: 在 platform 裡面, postConsoleInt, clearConsoleInt 有什麼用處 ?
在 gem5 裡面,有一些 model 不是透過 IntSourcePin 去發 interrupt,而是直接透過 platform 指標去發 interrupt。
Q: 為什麼要自己額外弄一個 SimplePlatform
希望有一個什麼也不做的 platform,原本是想直接用 class Platform,可惜它是 abstract 的,不能直接被使用,只能被當作 base class。
# RTCCLK (Set to 100MHz for faster simulation)
self.rtc = RiscvRTC(frequency=Frequency("100MHz"))
定期 ( 頻率可以設定 ) 發訊號出來。
self.clint = Clint(pio_addr=0x2000000, num_threads=1)
self.clint.int_pin = self.rtc.int_pin
接受來自 rtc 的訊號,並以該頻率去遞增 mtime CSR。
Q: mtime CSR 是怎麼被讀取的 ?
大致上的順序
RiscvSystem
RiscvSystem 再去找 Clint
Q: Clint 是怎麼接上 CPU 的 ?
Clint 的 base class 是 BasicPioDevice
BasicPioDevice 的 base class 是 PioDevice
PioDevice 有個 parameter 是 system = Param.System(Parent.any
Parent.XXX 會去父母節點找 XXX
Param.System(Parant.any... 會去所有祖先節點找找看,有沒有 Type 是 System 的節點。Clint 可以擁有指向 RiscvSystem 的指標Q: 為什麼不用 port,而是用這樣的方式進行連接呢 ?
不知道 QQ。
情感上,我比較喜歡 SystemC 那樣,用 pin / port 接在一起的感覺。
self.terminal = Terminal()
self.uart = SimpleUart(pio_addr=0x10000000)
SerialDevice
Platform 以及 SerialDevice 的指標{ https://github.com/TommyWu-fdgkhdkgh/gem5/blob/stable/src/cpu/BaseCPU.py#L233 }
create hardware thread, 絕大部分的情況下,thread 的數量為 1
{ https://github.com/TommyWu-fdgkhdkgh/gem5/blob/stable/src/cpu/BaseCPU.py#L176 }
建立符合 thread 數量的 ArchInterrupts。在 RISC-V,會是 RiscvInterrupts
# note :
# cpu.ArchISA is a class
# cpu.isa is an object array
# cpu.isa[0] is the isa for the first thread
# ( we almost only use one thread for a CPU model )
system.cpu.isa[0].riscv_type = "RV32"
設定 riscv_type,可以是 RV32 或是 RV64。
system.workload = RiscvBareMetal(wait_for_remote_gdb=False)
system.workload.bootloader = args.firmware_path
RiscvBareMetal 的 base class 是 Workload
System 會拿到 Workload 的指標System 會讓 Workload 也拿到自己的指標ThreadContext 拿到 System 指標System 拿到 Workload 指標Workload 拿到 entry point 的位址Workload : 解析 ELF,拿到 ELF 的相關資訊 ( e.g. entry point )
{ https://github.com/TommyWu-fdgkhdkgh/simple-riscv-vp/blob/main/firmware/boot.S }
.global _start
_start:
csrr t0, mhartid
lui t1, 0
beq t0, t1, 2f
1: wfi
j 1b
2:
# initialize global pointer
la gp, _gp
# initialize stack pointer
la sp, stack_top
call start
初始化 stack pointer 後,就跳到 C function : start。
// trap entry
.globl trap_entry
.align 4
trap_entry:
# make room to save registers.
addi sp, sp, -256
# save the registers.
sw ra, 0(sp)
sw sp, 8(sp)
sw gp, 16(sp)
sw tp, 24(sp)
sw t0, 32(sp)
sw t1, 40(sp)
…
sw s10, 200(sp)
sw s11, 208(sp)
sw t3, 216(sp)
sw t4, 224(sp)
sw t5, 232(sp)
sw t6, 240(sp)
call handle_trap
# restore registers.
lw ra, 0(sp)
lw sp, 8(sp)
lw gp, 16(sp)
# not this, in case we moved CPUs: ld tp, 24(sp)
lw t0, 32(sp)
lw t1, 40(sp)
lw t2, 48(sp)
lw s0, 56(sp)
…
lw t3, 216(sp)
lw t4, 224(sp)
lw t5, 232(sp)
lw t6, 240(sp)
addi sp, sp, 256
mret
當 trap 發生的時候,把 register 存進 stack 裡面,等到 handle_trap 處理完後,再把狀態恢復回來。
{ https://github.com/TommyWu-fdgkhdkgh/simple-riscv-vp/blob/main/firmware/main.c }
// set up mtvec
w_mtvec((uint32_t)trap_entry);
把 mtvec 設為 trap_entry,這樣當 trap ( interrupt, exception, ecall ) 發生的時候,就會跳來這裡。
// init mstatus.MPP to M-mode
tmp = r_mstatus();
tmp |= MSTATUS_MPP_M;
w_mstatus(tmp);
把 MPP 設為 M-mode,這樣當 mret 的時候,特權級就還是 M-mode
// init mstatus.MPIE
tmp = r_mstatus();
tmp |= MSTATUS_MPIE;
w_mstatus(tmp);
把 MPIE 打開,這樣當 mret 之後,就會啟用 interrupt。
// mepc
w_mepc((uint32_t)main);
把 mepc 設為 main,這樣當 mret 之後,會跳到 main function。
// mret to main
asm volatile("mret");
利用 mret 跳到 main function。
{ https://github.com/TommyWu-fdgkhdkgh/simple-riscv-vp/blob/main/firmware/uart.c }
void uart_putchar(char c) {
*(volatile uint32_t *)SIMPLE_UART_ADDR = (uint32_t) c;
}
char uart_getchar(void) {
return *(volatile uint32_t *)SIMPLE_UART_ADDR;
}
因為我這邊是用 gem5 的 SimpleUart model,這個 model 不會去發 interrupt,而是要我們自己去 polling。
當我們想要從 uart 拿資料時,就是瘋狂去讀取他的 register 就是了。