# mini-riscv-os筆記 *** ## RISC-V 是甚麼 ? 我個人簡單來說就是,cpu的架構裡擁有一個指令集架構,也就是專屬於電腦的機器語言,也能說是電腦看得懂的語言,RISC就是屬於cpu指令集架構的架構之一,而RISC-V就是RISC架構的指令集,以下為個人繪製概念圖 ![](https://github.com/ali1234-56/co111a/blob/main/cpu%E6%8C%87%E4%BB%A4.png) \ cpu的指令集架構分為 **複雜指令集 CISC 架構** 與 **精簡指令集 RISC 架構** ![](https://i.imgur.com/MRoAUvr.png) ## 為甚麼使用 RISC-V ? 我看完老師給的內容,以及尋閱大量關於 RISC-V 的內容,我發現了以下這些優點 * **開放原始碼硬體的授權** > 1. RISC-V基金會所託管、維護的完全開放的指令集,任何人都可以任意使用 > 2. 對於大學老師授課的《計算機結構、系統程式、作業系統、嵌入式系統》教材而言,不會受到商用處理器架構不斷修修改改,導致教材不斷過時的問題 * **指令集精簡** > 1. 現在的趨勢來看,我們希望 CPU 算得愈快愈好,因此指令集要愈精簡愈好,而 RISC-V 就是設計更為精簡的 RISC 指令集。因為現在 CPU 已經算得很快了,要讓它更快的成本要比以前更多,為了讓同一代 CPU 能用更久,RISC-V是一個很好的選擇 * **資源豐富** > 1. 開發的網站提供完整的開發支援,包含架構文件、編譯器、軟體開發工具 >>RISC-V 官網 -- https://riscv.org/ \ >>軟體 -- https://riscv.org/exchange/software/ \ >>開發板 -- https://riscv.org/exchange/ \ >>處理器原始碼 -- https://riscv.org/exchange/cores-socs/ \ >>技術手冊 -- https://riscv.org/technical/specifications/ * **低成本製造** > 1. 前面寫到因為原始碼硬體的免費授權,所以在設計、製造和銷售RISC-V芯片和軟件上都無需購買架構許可證 > 2. 模塊化的方式,可以透過組裝,客製化成專用於某項應用的處理器 * **低功耗和低面積** > 1. 電路複雜度減低,具有優勢 > 2. 大部分的芯片在cpu的耗能非常高,導致整個續航能力下降,而 RISC-V 適合高效設計實現,加上全部 RISC-V指令不超過50個,因此內核面積更小,所以相對功耗較低,藉而實現低耗能實現高效的AI元件運行 * **靈活彈性的擴展功能** > 1. RISC-V能以模塊化的方式將不同部位串在一起,透過統一的架構滿足各種不同的應用 > 2. 預留了客製指令的擴充空間(custom instructions),可以根據不同需求做出相對應設計。這點在 x86 和 Arm 處理 器中幾乎做不到,如果需要加強某些特定的功能,價格也會隨之上升。 * **安全性保證** > 1. 採用封閉式架構平台,所以開發者看不到原程式碼,所以無法了解電路設計細節 > 2. RISC-V的用戶可以查看所有細節,可以全面檢查每一行程式碼以確定系統的安全,甚至根據要求定制化自己的安全 模塊 *** ## RISC-V 的暫存器 **暫存器分有兩大塊,且基本暫存器也還有區分** > **基本暫存器** >>* t1-t6 (temporaries) 都是臨時變數暫存器 >>* a0-a7 為參數暫存器 (arguments) >>* s0-s11 則是 Saved Registers >>* PC (Program Counter),不過這個暫存器特別的是,它屬於獨立的暫存器,一般指令無法存取,想要讀取 PC 得用 auipc 這樣的特殊指令 > **控制暫存器** ### 基本暫存器 RISC-V 處理器內含 32 個整數暫存器,x0到x31,如果加上浮點數的話則再加上 32 個浮點暫存器\ ![](https://camo.githubusercontent.com/783a702635967dc0e54c4e20be1a910548a64c222c75096b0fcef75d0f35da33/68747470733a2f2f692e696d6775722e636f6d2f66436551386e412e706e67) * Return Address : 存放函式執行結束時需要返回的記憶體地址 (被呼叫函式 (Callee) 回到呼叫函式 (Caller) ),當一個函式執行結束並且回傳時,便會回到此暫存器所存放的記憶體地址,而這個暫存器又會被稱為 Caller Saver。 * Thread pointer : 在 xv6 中會包含 Thread ID。 * Global pointer : 由編譯器使用,裡面的值設置之後基本上就不再進行更動了,用來存取一些全域或是區域變數,用來增進效能。 * Callee-Saved : 函式呼叫時不可以修改此暫存器的值,如果需要使用到這一些暫存器,需要先將其內容的值儲存起來才可以使用,並且在函式結束前,需要復原原先的值,反之則為 Caller-Saved。 ### 控制暫存器 叫做 CSR 暫存器 , 可以透過讀取 time 得到目前的時間值\ \ ![](https://scontent.xx.fbcdn.net/v/t1.15752-9/321778242_550887263734325_3741757779417087536_n.png?stp=dst-png_p206x206&_nc_cat=100&ccb=1-7&_nc_sid=aee45a&_nc_ohc=4vERy-90XWgAX_ERUde&_nc_oc=AQkW2m6-QJhf4XQf6zvH-MhWiTTprH5ZUGBazpmpsc4tATxwa69pOIxDqJn02xKm2mGKFEP9MEodog9gxEQflRNh&_nc_ad=z-m&_nc_cid=0&_nc_ht=scontent.xx&oh=03_AdR42cXGabb5Tgsb1pMApOCqgj3907vzf6feLHyXrdg0wA&oe=63D68BB3) ## RISC-V 指令集 * **RV32I:基础整数指令集 (固定不變了)** * **RV32M:乘法和除法** * **RV32F:單精度浮点操作(和 RV32D:雙精度浮點操作)** * **RV32A:原子操作** * **RV32C:可选的压缩扩展 (對應 32 位元的 RV32G)** * **RV32B:基本擴展。** * **RV32V:向量扩展(SIMD)指令** * **RV64G:RISC-V 的 64 位地址版本。** * \ ![](https://github.com/ali1234-56/co111a/blob/main/%E6%8C%87%E4%BB%A4%E9%9B%86.PNG) ### RV32I 為最基本的指令集 ,指令表如下表 ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/rv32i1.png) ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/rv32i2.png) ### RV32IM 代表 32 位元含乘除法的指令集 (RV32IM = RV32I + RV32M) ![](https://pic2.zhimg.com/80/v2-261c5f7626df0189f780baf59f2e7815_1440w.webp) ### RISC-V 全部指令目前狀態 ![](https://github.com/ali1234-56/co111a/blob/main/%E6%8C%87%E4%BB%A4%E7%8B%80%E6%85%8B.PNG) ## RISC-V 指令格式 指令格式是使用二進制編碼的表示結構,正常一條指令區分成操作碼和地址碼\ \ ![](https://img-blog.csdnimg.cn/161792b5208c405ea73adcacc77ed9db.png#pic_center)\ ### 1. 操作碼 操作指令的屬性功能和執行指令類型,操作數對應的二進制位數決定了計算機能實現的最大指令數\ \ 例如,操作碼是 7 位的二進制碼,計算機能夠實現的最大指令數目是 128(2^7)。操作碼根據二進制位數是否具有可變性將其細分為固定長度和可變長度兩種。 ### 2. 地址碼 * **零地址碼**\ ![](https://img-blog.csdnimg.cn/169e9df7e8e343bb8efc6bfd04a3cea1.png#pic_center)\ 零地址碼只有一個操作碼而沒有地址碼,所以無須先設置對應的操作碼,像是空操作、停機等指令 * **一地址碼**\ ![](https://img-blog.csdnimg.cn/08eb7972d7cd406aafd9bb2937ae115d.png#pic_center)\ 一地址碼擁有一個存儲操作數的地址像rs1,也是最終處理結果的保存地址,也被稱為單操作數指令 * **二地址碼**\ ![](https://img-blog.csdnimg.cn/bff432f4e41742ce8682af216170fa44.png#pic_center)\ 二地址碼也被稱作雙操作數指令,屬於當前計算機系統應用最廣泛的\ rs1 表示源操作數地址和最終處理結果的存儲目的地址,rs2 表示另外一個源操作數地址。 * **三地址碼**\ ![](https://img-blog.csdnimg.cn/a9232f157a8c4b44a2983ec46530e16a.png#pic_center)\ 三地址碼擁有三個地址存放操作數\ rs1 代表源操作數地址,rs2 代表另外一個源操作數地址,rd 代表目的操作數地址即存放處理結果的地址 ## * 三地址碼與前幾碼不同,操作數地址即存放處理結果的地方不是rs1而是rd ### **RISC-V 指令格式是一個典型的三操作數,7 位操作碼的指令格式。 RISC-V 指令集具有六種基本指令格式** ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/riscv_format32_2.png) * 其中 opcode 表示 7 位指令操作碼,其作用是區分不同的指令 * funct3 表示 3 位 的功能碼,funct7 表示 7 位的功能碼,它們可以輔助區分不同種類的指令 * rs1 和 rs2 表示兩個 5 位的源寄存器 * rd 是 5 位的目的寄存器,指令運算的結果就存儲 rd 中 * imm 代表不同長度的立即數,可直接作為操作數使用 ## RISC-V的壓縮指令集 雖然risc-v並不是太注重在商業競爭力上,不過在這方面還是有支援的壓縮模式 RV32C ,其中就有很多指令可以被壓縮表達為16位元模式 ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/riscv_format.png) ### * 尾部兩位不是 aa 的話 屬於 16bit 指令 ### * 根據 16bit 指令 aa的前三位不是 bbb 的話 變為 32bit 指令 ### * 此外,為了壓縮至 16bit,除了縮減立即值 imm 的長度之外,也將 rd/rs1 用同一個欄位表示,這代表 rd/rs1 必 須是同一個暫存器。 ## RISC-V的浮點指令集 **浮點指令集分有兩大塊** > **單精度 float (32 位元浮點數)* ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/rv32f.png) > **雙精度 double (64 位元浮點數)* ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/rv32d.png) ## RISC-V的原子指令集 原子指令集也就是 RV32A,圖中可以看到前兩列為一組稱作 **LR/SC 指令** ,剩下的都是 **AMO 指令** ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/rv32a.png) ### LR/SC 指令 * LR 指令是 Load Reserved 的縮寫,屬於**讀取保留** > * LR 指令是從內存地址 rs1 中加載內容到 rd 寄存器。然後在 rs1 對應地址上設置保留標記 * SC 指令是 Store Conditional 的縮寫,屬於**條件存儲** > * sc 指令在把 rs2 值寫到 rs1 地址之前,會先判斷 rs1 內存地址是否有設置保留標記,如果設置了,則把 rs2 值正常寫入到 rs1 內存地址裡,並把 rd 寄存器設置成 0 >* 如果 rs1 內存地址沒有設置保留標記,則不保存,並把 rd 寄存器設置成 1 表示保存失敗。不管成功還是失敗,sc 指令都會把當前 hart 保留的所有保留標記全部清除 ### AMO 指令 * AMO 是 Atomic Memory Operation 的縮寫,和 LR/SC 指令類似,所有的 AMO 指令都要求 rs1 寄存器的地址是按寬帶對齊的,否則會觸發異常。 \ ![](https://pic4.zhimg.com/80/v2-e045fc946081eb6ff1768805b9dbe53b_720w.webp) ## RISC-V的假指令 \ 雖然指令集內容的精簡是RISC-V的優點,不過相對的,其他處理器所具備的指令讓RISC-V不見得能相呼對應\ \ 所以創造出 **假指令** 讓 RISC-V也能寫出這樣的指令 (**我感覺 RISC-V 和 假指令的關係就和機械語言和程式語言一樣**) ![](https://raw.githubusercontent.com/wiki/riscv2os/riscv2os/img/pseudo1.png) # 本章心得結語 ## 一開始就對 RISC-V 感到疑惑,心想著從來都沒有聽說過這個語言,深入了解後才知道cpu還有不同架構的設計語言,還有一開始也把 RISC 架構 跟 RISC-V 混再一起 ,弄清楚後也更明白整個架構,也畫了張圖 ## 再來就是了解 RISC-V 的優點後,對我們這些學生而言也能簡易的設計出芯片 ## 最後就是雖然只能大概了解 RISC-V 的許多 暫存器或指令的功能 ,但以後如果能有多的時間,我也想了解更多並親自實作看看 ## 參考資料 * 老師寫的https://github.com/riscv2os/riscv2os/wiki/riscvOverview * https://zh.wikipedia.org/wiki/RISC-V * https://blog.csdn.net/qq_39507748/article/details/120150936 * https://blog.csdn.net/weixin_43914889/article/details/106108377 * https://zhuanlan.zhihu.com/p/467648512 * https://zhuanlan.zhihu.com/p/489679551 * https://weikaiwei.com/riscv/riscv-1/ # 印出 Hello OS! ### 要執行檔案需要有 **啟動程式 start.s ,連結檔案 os.ld,以及建置檔 Makefile** UART 映射區 * 0x10000000 THR (Transmitter Holding Register) 同時也是 RHR (Receive Holding Register) * 0x10000001 IER (Interrupt Enable Register) * 0x10000002 ISR (Interrupt Status Register) * 0x10000003 LCR (Line Control Register) * 0x10000004 MCR (Modem Control Register) * 0x10000005 LSR (Line Status Register) * 0x10000006 MSR (Modem Status Register) * 0x10000007 SPR (Scratch Pad Register) ### 執行檔 ``` #include <stdint.h> #define UART 0x10000000 #define UART_THR (uint8_t*)(UART+0x00) // THR:transmitter holding register #define UART_LSR (uint8_t*)(UART+0x05) // LSR:line status register #define UART_LSR_EMPTY_MASK 0x40 // LSR Bit 6: Transmitter empty; both the THR and LSR are empty int lib_putc(char ch) { while ((*UART_LSR & UART_LSR_EMPTY_MASK) == 0); return *UART_THR = ch; } void lib_puts(char *s) { while (*s) lib_putc(*s++); } int os_main(void) { lib_puts("Hello OS!\n"); while (1) {} return 0; } ``` 下列函數來送一個字元給 UART 以便印出到宿主機 (host) 上 ``` int lib_putc(char ch) { while ((*UART_LSR & UART_LSR_EMPTY_MASK) == 0); return *UART_THR = ch; } ``` 能印出一個字之後,就能用以下的 lib_puts(s) 印出一大串字 ``` void lib_puts(char *s) { while (*s) lib_putc(*s++); } ``` 於是我們的主程式就呼叫 lib_puts 印出了 Hello OS! ``` int os_main(void) { lib_puts("Hello OS!\n"); while (1) {} return 0; } ``` ### 建置檔 Makefile ``` CC = riscv64-unknown-elf-gcc CFLAGS = -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 QEMU = qemu-system-riscv32 QFLAGS = -nographic -smp 4 -machine virt -bios none OBJDUMP = riscv64-unknown-elf-objdump all: os.elf os.elf: start.s os.c $(CC) $(CFLAGS) -T os.ld -o os.elf $^ qemu: $(TARGET) @qemu-system-riscv32 -M ? | grep virt >/dev/null || exit @echo "Press Ctrl-A and then X to exit QEMU" $(QEMU) $(QFLAGS) -kernel os.elf clean: rm -f *.elf ``` Makefile 文法符號 ``` $@ : 該規則的目標文件 (Target file) $* : 代表 targets 所指定的檔案,但不包含副檔名 $< : 依賴文件列表中的第一個依賴文件 (Dependencies file) $^ : 依賴文件列表中的所有依賴文件 $? : 依賴文件列表中新於目標文件的文件列表 $* : 代表 targets 所指定的檔案,但不包含副檔名 ?= 語法 : 若變數未定義,則替它指定新的值。 := 語法 : make 會將整個 Makefile 展開後,再決定變數的值。 ``` 在 Makefile 中我們使用 riscv64-unknown-elf-gcc 去編譯,然後用 qemu-system-riscv32 去執行 make clean 清除上次的編譯產出,然後用 make 呼叫 riscv64-unknown-elf-gcc 編譯專案 ![](https://scontent.ftpe8-2.fna.fbcdn.net/v/t1.15752-9/352912958_969803534471804_745506177852962344_n.jpg?_nc_cat=101&ccb=1-7&_nc_sid=ae9488&_nc_ohc=pr2YtGv6kzMAX9BcJ6P&_nc_ht=scontent.ftpe8-2.fna&oh=03_AdRJKaqsmyJIA1Ka3arnLhqHT20Cw3IjHnoZP7FxmAeHeg&oe=64AEA744) ### Link Script 連結檔 ``` OUTPUT_ARCH( "riscv" ) ENTRY( _start ) MEMORY { ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = 128M } PHDRS { text PT_LOAD; data PT_LOAD; bss PT_LOAD; } SECTIONS { .text : { PROVIDE(_text_start = .); *(.text.init) *(.text .text.*) PROVIDE(_text_end = .); } >ram AT>ram :text .rodata : { PROVIDE(_rodata_start = .); *(.rodata .rodata.*) PROVIDE(_rodata_end = .); } >ram AT>ram :text .data : { . = ALIGN(4096); PROVIDE(_data_start = .); *(.sdata .sdata.*) *(.data .data.*) PROVIDE(_data_end = .); } >ram AT>ram :data .bss :{ PROVIDE(_bss_start = .); *(.sbss .sbss.*) *(.bss .bss.*) PROVIDE(_bss_end = .); } >ram AT>ram :bss PROVIDE(_memory_start = ORIGIN(ram)); PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram)); } ``` ### 啟動程式 ``` .equ STACK_SIZE, 8192 .global _starthttps://hackmd.io/bRzRX_SVRRehLG9crJtyqw#%E5%8D%B0%E5%87%BA-Hello-OS _start: # setup stacks per hart csrr t0, mhartid # read current hart id slli t0, t0, 10 # shift left the hart id by 1024 la sp, stacks + STACK_SIZE # set the initial stack pointer # to the end of the stack space add sp, sp, t0 # move the current hart stack pointer # to its place in the stack space # park harts with id != 0 csrr a0, mhartid # read current hart id bnez a0, park # if we're not on the hart 0 # we park the hart j os_main # hart 0 jump to c park: wfi j park stacks: .skip STACK_SIZE * 4 # allocate space for the harts stacks ``` # ContextSwitch RISC V 的內文切換 ### 執行結果 ![](https://scontent.ftpe8-4.fna.fbcdn.net/v/t1.15752-9/354035334_960455895107121_2731521805743609979_n.jpg?_nc_cat=102&ccb=1-7&_nc_sid=ae9488&_nc_ohc=878he4rZN7kAX8IfG1C&_nc_ht=scontent.ftpe8-4.fna&oh=03_AdQ4WnKU57uiWvWU9ckBuuBrzTnL9TEKlZ9m-0cQaNZZiw&oe=64AEB526) ``` #include "os.h" #define STACK_SIZE 1024 uint8_t task0_stack[STACK_SIZE]; struct context ctx_os; struct context ctx_task; extern void sys_switch(); void user_task0(void) { lib_puts("Task0: Context Switch Success !\n"); while (1) {} // stop here. } int os_main(void) { lib_puts("OS start\n"); ctx_task.ra = (reg_t) user_task0; ctx_task.sp = (reg_t) &task0_stack[STACK_SIZE-1]; sys_switch(&ctx_os, &ctx_task); return 0; } ``` * task0_stack[STACK_SIZE] = stack * ctx_os 作業系統 * user_task0 是第一個stack * lib_puts("OS start\n");啟動作業系統 * 呼叫 sys_switch(&ctx_os, &ctx_task); os控制權切換到task * 主要是印出 os start 後印出 task0 * 主要換掉整個暫存器 *** 但是每個任務都必須有堆疊空間,才能在 C 語言環境中進行函數呼叫。所以我們分配了 task0 的堆疊空間,並用 ctx_task.sp 指向堆疊開頭。 ``` .globl sys_switch .align 4 sys_switch: ctx_save a0 # a0 => struct context *old ctx_load a1 # a1 => struct context *new ret # pc=ra; swtch to new task (new->ra) ``` * ctx_save a0 # a0 => struct context *old 存取舊的暫存器 * ctx_load a1 # a1 => struct context *new 在載入a1** * ret # pc=ra; swtch to new task (new->ra) return後進行pc=ra的動作 呼叫 save 和 load 後 把整個暫存器存起來,在 RISC-V 中,參數主要放在 a0, a1, ..., a7 這些暫存器當中,當參數超過八個時,才會放在堆疊裏傳遞。 ``` 上述程式的 a0 對應 old (舊任務的 context),a1 對應 new (新任務的context),整個 sys_switch 的功能是儲存舊任務的 context ,然後載入新任務 context 開始執行。 最後的一個 ret 指令非常重要,因為當新任務的 context 載入時會把 ra 暫存器也載進來,於是當 ret 執行時,就會設定 pc=ra,然後跳到新任務 (例如 void user_task0(void) 去執行了 # ============ MACRO ================== base屬於a0 .macro ctx_save base sw ra, 0(\base) sw sp, 4(\base) sw s0, 8(\base) sw s1, 12(\base) sw s2, 16(\base) sw s3, 20(\base) sw s4, 24(\base) sw s5, 28(\base) sw s6, 32(\base) sw s7, 36(\base) sw s8, 40(\base) sw s9, 44(\base) sw s10, 48(\base) sw s11, 52(\base) .endm .macro ctx_load base lw ra, 0(\base) lw sp, 4(\base) lw s0, 8(\base) lw s1, 12(\base) lw s2, 16(\base) lw s3, 20(\base) lw s4, 24(\base) lw s5, 28(\base) lw s6, 32(\base) lw s7, 36(\base) lw s8, 40(\base) lw s9, 44(\base) lw s10, 48(\base) lw s11, 52(\base) .endm # ============ Macro END ================== ``` # MultiTasking RISC V 的協同式多工 ### 執行結果 ![](https://scontent.ftpe8-4.fna.fbcdn.net/v/t1.15752-9/352853618_3445774729009969_4080734980292283404_n.jpg?_nc_cat=110&ccb=1-7&_nc_sid=ae9488&_nc_ohc=ybcRCNxU8MsAX8sQT3C&_nc_ht=scontent.ftpe8-4.fna&oh=03_AdS2ywSkYu8D8ZJgILVZpbxEIKc8uvM8Hk6sH0-iGF_Sig&oe=64AECE27) ![](https://scontent.ftpe8-4.fna.fbcdn.net/v/t1.15752-9/352843110_838075447652127_2908147115454152811_n.jpg?_nc_cat=102&ccb=1-7&_nc_sid=ae9488&_nc_ohc=ud1TIC0uwN4AX8B_fiH&_nc_ht=scontent.ftpe8-4.fna&oh=03_AdQxJcNUCJ3qkoyBjmjtSOxCoTgNSjypJpNnGl-aWhjwPA&oe=64AEA8F5) 現代的作業系統,都有透過時間中斷強制中止行程的《搶先》(Preemptive) 功能,這樣就能在某行程霸佔 CPU 太久時,強制將其中斷,切換給別的行程執行。 但是在沒有時間中斷機制的系統中,作業系統《無法中斷惡霸行程》,因此必須依賴各個行程主動交回控制權給作業系統,才能讓所有行程都有機會執行。 這種仰賴自動交還機制的多行程系統,稱為《協同式多工》(Coorperative Multitasking) 系統。 RISC-V協同式多工 ### 協同式多工的優點 * 簡單易於實現:相較於抢占式多工,協同式多工不需要複雜的排程算法和硬體支持。 * 低開銷:由於不需要額外的硬體支持,協同式多工可以在資源受限的系統中實現。 * 可預測性:協同式多工允許任務按照預定的順序運行,提供可預測性和確定性。 ### RISC-V協同式多工的實現方式 * 狀態保存:每個任務需要保存其狀態,例如暫存器內容、堆疊指針等。 * 任務切換:任務切換發生在任務自願釋放CPU控制權的時候,通常是在任務完成或遇到I/O等待的時候。 * 上下文切換:當任務切換發生時,系統需要保存當前任務的狀態,並還原下一個任務的狀態。 * 中斷處理:中斷處理與協同式多工密切相關,當中斷發生時,系統需要處理中斷並可能進行任務切換。 ### 協同式多工的限制和挑戰 * 可靠性:如果一個任務進入無限迴圈或發生錯誤,可能會影響整個系統的運行。 * 響應時間:由於任務切換是由任務自主釋放CPU控制權觸發的,某些任務可能占用較長的時間,導致其他任務的響應時間延遲。 * 資源管理:協同式多工需要良好的資源管理,以確保每個任務獲得足夠的資源並避免任 ### start.s檔 ``` .equ STACK_SIZE, 8192 .global _start _start: csrr a0, mhartid # 讀取核心代號 bnez a0, park # 若不是 0 號核心,跳到 park 停止 la sp, stacks + STACK_SIZE # 0 號核心設定堆疊 j os_main # 0 號核心跳到主程式 os_main park: wfi j park stacks: .skip STACK_SIZE # 分配堆疊空間 ``` ``` #include "os.h" void os_kernel() { task_os(); } void os_start() { lib_puts("OS start\n"); user_init(); } int os_main(void) { os_start(); int current_task = 0; while (1) { lib_puts("OS: Activate next task\n"); task_go(current_task); lib_puts("OS: Back to OS\n"); current_task = (current_task + 1) % taskTop; // Round Robin Scheduling lib_puts("\n"); } return 0; } 上述排程方法原則上和 [Round Robin Scheduling](https://en.wikipedia.org/wiki/Round-robin_scheduling) 一致,但是 Round Robin Scheduling 原則上必須搭配時間中斷機制,但本章的程式碼沒有時間中斷,所以只能說是協同式多工版本的 Round Robin Scheduling。 協同式多工必須依賴各個 task 主動交回控制權,像是在 user_task0 裏,每當呼叫 os_kernel() 函數時,就會呼叫內文切換機制,將控制權交回給作業系統 [os.c](https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/os.c) 。 ``` void user_task0(void) { lib_puts("Task0: Created!\n"); lib_puts("Task0: Now, return to kernel mode\n"); os_kernel(); while (1) { lib_puts("Task0: Running...\n"); lib_delay(1000); os_kernel(); } } ``` ``` void os_kernel() { task_os(); } ``` 而 task_os() 則會呼叫組合語言 [sys.s](https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/sys.s) 裏的 sys_switch 去切換回作業系統中。 ``` // switch back to os void task_os() { struct context *ctx = ctx_now; ctx_now = &ctx_os; sys_switch(ctx, &ctx_os); } ``` user_init()設定使用者形成 ``` int os_main(void) { os_start(); int current_task = 0; while (1) { lib_puts("OS: Activate next task\n"); task_go(current_task); lib_puts("OS: Back to OS\n"); current_task = (current_task + 1) % taskTop; // Round Robin Scheduling lib_puts("\n"); } return 0; } ``` ``` void user_task0(void) { lib_puts("Task0: Created!\n"); lib_puts("Task0: Now, return to kernel mode\n"); os_kernel(); while (1) { lib_puts("Task0: Running...\n"); lib_delay(1000); os_kernel(); } } ``` # TimerInterrupt RISC V 的時間中斷 ### 時間中斷介紹 * 時間中斷是由硬體計時器觸發的中斷,用於在指定的時間間隔內執行特定的操作。 * 在RISC-V架構中,時間中斷通常由計時器中的計數器到達指定值時觸發。 ### 計時器硬體 * RISC-V架構通常具有一個計時器硬體,用於跟蹤時間和觸發時間中斷。 * 計時器通常包括一個計數器和一組控制寄存器,用於設置計數器的初始值和控制計時器的運作。 ``` .section .text .globl _start # 定義計時器中斷處理程序 .align 2 .globl timer_interrupt_handler timer_interrupt_handler: # 在此處執行時間中斷處理邏輯 # 例如更新計時器,執行任務切換等 # ... # 完成中斷處理後,從中斷返回 mret # 配置計時器中斷 .align 2 .globl configure_timer_interrupt configure_timer_interrupt: # 將時間間隔設置為指定值 li t0, <計時器間隔值> csrw <計時器計數器地址>, t0 # 設置中斷處理程序 la t1, timer_interrupt_handler csrw mtvec, t1 # 啟用計時器中斷 li t2, <計時器中斷位> csrs mie, t2 # 啟用全域中斷 csrs mstatus, 8 ret # 程式入口點 _start: # 配置時間中斷 call configure_timer_interrupt # 啟用計時器 # ... # 無窮迴圈 loop: j loop ```