試實踐)
簡介面向RISC-V學習者的MySBI與BenOS實驗代碼包配套《RISC-V體系結構編程與實踐》第二章適合深入理解系統(tǒng)級編程、操作系統(tǒng)內(nèi)核設計及RTOS實現(xiàn)路徑的開發(fā)者。壓縮包共17個文件大小僅7KB包含4個頭文件、4個鏈接腳本、3個匯編文件、3個C源文件以及sbi_payload、riscv64-benos_defconfig和Makefile。其中C源碼與頭文件覆蓋內(nèi)核初始化、串口驅動和SBI主要邏輯鏈接腳本與匯編實現(xiàn)啟動引導config與Makefile用于定制構建整體目錄緊湊便于逐文件研讀。從boot.S可觀察處理器啟動早期的寄存器初始化sbi_payload.S演示SBI如何裝載并跳轉到內(nèi)核而kernel.c與uart.c串起異常入口、中斷分發(fā)和串口輸出等典型路徑形成完整可讀閉環(huán)。已有300余人學習瀏覽。通過閱讀和實驗可掌握RISC-V異常與中斷處理、MySBI接口的調(diào)用方式、BenOS任務調(diào)度與內(nèi)存管理并能在模擬器上完成配置編譯與調(diào)試對嵌入式及RTOS開發(fā)者也具有直接借鑒價值。 其實關于“怎么寫一個最簡單的操作系統(tǒng)”大部分人上來就會碰壁教材說QEMU的RISC-V機器加電后從0x80000000開始執(zhí)行但你照著寫好裸機程序打印一個字符都要折騰好幾天。問題通常不在內(nèi)核本身而在沒人講清楚SBI這一層。MySBI與BenOS實驗代碼這套東西就是把這塊短板補上——MySBI是一個極簡的SBI實現(xiàn)跑在RISC-V的機器態(tài)M態(tài)給BenOS這個教學用操作系統(tǒng)內(nèi)核提供底層服務BenOS跑在監(jiān)管者態(tài)S態(tài)通過ecall向MySBI要控制臺輸出、定時器和復位功能。整套代碼加起來不過幾百行但能讓你親手把“加電→M態(tài)固件→S態(tài)內(nèi)核→定時器中斷→串口打印”這條鏈路完整跑通。這篇文章我按照自己調(diào)試這組實驗的真實順序把代碼分層、啟動流程、環(huán)境搭建和幾個經(jīng)典坑都梳理一遍適合想從零搞懂RISC-V特權級和操作系統(tǒng)底層的同學參考。1. 為什么必須自己寫SBI先看清OpenSBI替你扛了什么1.1 SBI不是“引導程序”而是S態(tài)內(nèi)核的底層服務邊界很多人把SBI理解成Bootloader這是最大的誤解。真正的引導程序負責把內(nèi)核鏡像從磁盤搬到內(nèi)存而SBISupervisor Binary Interface監(jiān)管者二進制接口是M態(tài)固件與S態(tài)內(nèi)核之間的一套約定接口。S態(tài)內(nèi)核受特權級限制不能直接訪問定時器、部分CSR寄存器、串口等硬件資源只能通過ecall指令陷入M態(tài)讓固件代勞。你可以把它理解成“操作系統(tǒng)的系統(tǒng)調(diào)用”內(nèi)核態(tài)向用戶態(tài)提供服務靠系統(tǒng)調(diào)用S態(tài)內(nèi)核向M態(tài)固件請求服務就靠SBI。OpenSBI是完整的企業(yè)級實現(xiàn)支持眾多平臺、多核喚醒協(xié)議、PMP配置、固件加載模式代碼量大而全。但對學習而言這些恰恰是噪音。你自己寫MySBI時只需要面對四個問題復位后棧和BSS怎么辦、S態(tài)發(fā)來的ecall怎么分發(fā)、定時器中斷怎么轉發(fā)、串口字符怎么輸出。把這些問題弄清楚整個RISC-V特權級模型的地基就打好了。1.2 把OpenSBI換掉反而能更快看懂特權級切換直接用OpenSBI啟動BenOS第一行內(nèi)核代碼就在0x80200000你永遠看不到它到來之前發(fā)生了什么。mstatus.MPP怎么被修改、mtvec和stvec如何切換、ecall穿過特權級時mepc和mcause怎么變化這些對你都是黑盒。一旦自己寫過MySBI你會親眼看到M態(tài)設置好mstatus.MPP等于S態(tài)執(zhí)行mret指令的瞬間CPU就降級到S態(tài)從0x80200000取下一條指令。這種“親眼看見特權級切換”的體驗是讀十遍RISC-V規(guī)范都換不來的。2. MySBI四件事復位、異常、定時器、串口2.1 復位匯編棧、BSS、mhartid分流一個都不能少MySBI的入口在0x80000000這一段的匯編代碼雖然只有幾十行但每行都是剛需。第一步設置棧指針SBI運行時必須有獨立的棧第二步清BSS段從__bss_start到__bss_end逐字清零因為編譯器生成的C啟動代碼在裸機環(huán)境下不可用不清零的話tick計數(shù)這類全局變量初值就是內(nèi)存里的垃圾數(shù)據(jù)第三步讀取mhartid如果是非0號核直接wfi自旋避免多核啟動時幾個核共用一份棧互相踩踏第四步把mtvec設置成異常入口地址最后跳轉到C語言主函數(shù)。我在實驗里最初把清BSS放在了設置棧之前導致清零循環(huán)本身就要用棧第一個restore就把地址搞亂了。后來才意識到裸機啟動的代碼順序必須完全符合硬件現(xiàn)狀先SP、再BSS、最后才敢開CSR和調(diào)函數(shù)。_start: la sp, _stack_top la t0, _bss_start la t1, _bss_end 1: bgeu t0, t1, 2f sd zero, 0(t0) addi t0, t0, 8 j 1b 2: csrr t0, mhartid beqz t0, 3f wfi j 2b 3: la t0, trap_entry csrw mtvec, t0 call main2.2 異常入口與ecall分派mepc 4 是“退路”MySBI的異常入口負責判斷這次陷入M態(tài)到底發(fā)生了什么。mcause寄存器的bit63為1表示異步中斷為0表示同步異常。實驗里最關鍵的兩類事件是異常碼9來自S態(tài)的ecall和中斷碼7M態(tài)定時器中斷。入口匯編先把通用寄存器保存到棧上然后讀出mepc、mcause傳給C函數(shù)處理。這中間的“靈魂細節(jié)”是mepc寄存器。RISC-V規(guī)定同步異常發(fā)生時mepc保存的是觸發(fā)異常的那條指令地址。也就是說S態(tài)執(zhí)行ecall陷入M態(tài)后mepc指向的是ecall指令本身而不是它的下一條。如果MySBI處理完服務后直接mretCPU會回到ecall那里再執(zhí)行一次再次陷入M態(tài)形成死循環(huán)。所以必須把mepc加4再返回跳過這條ecall指令。這個“加4”的邏輯是整套實驗里最容易忽略、也最致命的地方我后來在GDB里看到mepc原地踏步時才反應過來。至于ecall參數(shù)MySBI實驗代碼做了簡化a7存放調(diào)用號a0、a1存放參數(shù)處理完通過修改棧幀里的a0位置設置返回值。標準OpenSBI用的是擴展ID加功能ID的兩級方案原理一樣這里先不展開。2.3 定時器鏈路mtimecmp比較是一次性的RISC-V的定時器基于CLINT外設核心是mtime和mtimecmp兩個寄存器。mtime是一個64位遞增計數(shù)器mtimecmp是一個比較值只要mtime大于等于mtimecmp就產(chǎn)生M態(tài)定時器中斷。在QEMU的virt機器上這兩個寄存器的MMIO地址分別是0x0200BFF8和0x02004000時鐘頻率是10MHz1秒等于10000000個tick。寫MySBI時最反直覺的一點mtimecmp不是周期性觸發(fā)的。中斷到來后如果你想在下一個周期繼續(xù)觸發(fā)必須在中斷處理函數(shù)里重新計算并寫入新的mtimecmp值。我第一次實現(xiàn)時只寫了一次比較值結果時鐘中斷只來一次tick數(shù)永遠停在1。正確做法是每次進中斷處理函數(shù)立刻讀當前mtime加上固定間隔比如10ms就是100000個tick寫回mtimecmp。為了讓BenOS能收到時鐘中斷MySBI要做的是初始化時通過mideleg把S態(tài)定時器中斷STIP委托給S態(tài)收到M態(tài)定時器中斷后更新mtimecmp再設置mip的STIP位mret返回S態(tài)。這樣BenOS的stvec入口會看到scause等于5的S態(tài)定時器中斷自己完成業(yè)務邏輯后在軟件里清除sip的STIP位。2.4 UART本文還有配套的精品資源點擊獲取