diff --git a/04-TimerInterrupt/README.md b/04-TimerInterrupt/README.md index cdc756b..245df9e 100644 --- a/04-TimerInterrupt/README.md +++ b/04-TimerInterrupt/README.md @@ -3,11 +3,19 @@ ## Build & Run ```sh +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-TimerInterrupt (master) +$ make clean +rm -f *.elf + +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-TimerInterrupt (master) $ make -riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c timer.c os.c +riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima +-mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c timer.c os.c + +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-TimerInterrupt (master) $ make qemu Press Ctrl-A and then X to exit QEMU -qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf +qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf OS start timer_handler: 1 timer_handler: 2 diff --git a/04-TimerInterrupt/os.c b/04-TimerInterrupt/os.c index a8c6884..00a8b8b 100644 --- a/04-TimerInterrupt/os.c +++ b/04-TimerInterrupt/os.c @@ -4,7 +4,8 @@ int os_main(void) { lib_puts("OS start\n"); timer_init(); // start timer interrupt ... - while (1) {} // os : do nothing, just loop! + + while (1) {} // stop here ! return 0; } diff --git a/04-TimerInterrupt/os.h b/04-TimerInterrupt/os.h index 2e8389e..4058445 100644 --- a/04-TimerInterrupt/os.h +++ b/04-TimerInterrupt/os.h @@ -5,7 +5,8 @@ #include "lib.h" #include "timer.h" -extern void os_loop(void); +extern void user_init(); +extern void os_kernel(); extern int os_main(void); #endif diff --git a/04-TimerInterrupt/sys.s b/04-TimerInterrupt/sys.s index a4d46b1..afcfaef 100644 --- a/04-TimerInterrupt/sys.s +++ b/04-TimerInterrupt/sys.s @@ -1,15 +1,163 @@ # This Code derived from xv6-riscv (64bit) # -- https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/swtch.S +# ============ MACRO ================== +.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 reg_save base + # save the registers. + sw ra, 0(\base) + sw sp, 4(\base) + sw gp, 8(\base) + sw tp, 12(\base) + sw t0, 16(\base) + sw t1, 20(\base) + sw t2, 24(\base) + sw s0, 28(\base) + sw s1, 32(\base) + sw a0, 36(\base) + sw a1, 40(\base) + sw a2, 44(\base) + sw a3, 48(\base) + sw a4, 52(\base) + sw a5, 56(\base) + sw a6, 60(\base) + sw a7, 64(\base) + sw s2, 68(\base) + sw s3, 72(\base) + sw s4, 76(\base) + sw s5, 80(\base) + sw s6, 84(\base) + sw s7, 88(\base) + sw s8, 92(\base) + sw s9, 96(\base) + sw s10, 100(\base) + sw s11, 104(\base) + sw t3, 108(\base) + sw t4, 112(\base) + sw t5, 116(\base) + sw t6, 120(\base) +.endm + +.macro reg_load base + # restore registers. + lw ra, 0(\base) + lw sp, 4(\base) + lw gp, 8(\base) + # not this, in case we moved CPUs: lw tp, 12(\base) + lw t0, 16(\base) + lw t1, 20(\base) + lw t2, 24(\base) + lw s0, 28(\base) + lw s1, 32(\base) + lw a0, 36(\base) + lw a1, 40(\base) + lw a2, 44(\base) + lw a3, 48(\base) + lw a4, 52(\base) + lw a5, 56(\base) + lw a6, 60(\base) + lw a7, 64(\base) + lw s2, 68(\base) + lw s3, 72(\base) + lw s4, 76(\base) + lw s5, 80(\base) + lw s6, 84(\base) + lw s7, 88(\base) + lw s8, 92(\base) + lw s9, 96(\base) + lw s10, 100(\base) + lw s11, 104(\base) + lw t3, 108(\base) + lw t4, 112(\base) + lw t5, 116(\base) + lw t6, 120(\base) +.endm +# ============ Macro END ================== + +# Context switch +# +# void sys_switch(struct context *old, struct context *new); +# +# Save current registers in old. Load from new. + +.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) + +.globl sys_kernel +.align 4 +sys_kernel: + addi sp, sp, -128 # alloc stack space + reg_save sp # save all registers + call timer_handler # call timer.c:timer_handler + reg_load sp # restore all registers + addi sp, sp, 128 # restore stack pointer + jr a7 # jump to a7=mepc , return to timer break point + .globl sys_timer .align 4 sys_timer: - # call the C timer_handler(reg_t epc, reg_t cause) - csrr a0, mepc - csrr a1, mcause - call timer_handler + # timer_init() has set up the memory that mscratch points to: + # scratch[0,4,8] : register save area. + # scratch[12] : address of CLINT's MTIMECMP register. + # scratch[16] : desired interval between interrupts. - # timer_handler will return the return address via a0. - csrw mepc, a0 + csrrw a0, mscratch, a0 # exchange(mscratch,a0) + sw a1, 0(a0) + sw a2, 4(a0) + sw a3, 8(a0) - mret # back to interrupt location (pc=mepc) + # schedule the next timer interrupt + # by adding interval to mtimecmp. + lw a1, 12(a0) # CLINT_MTIMECMP(hart) + lw a2, 16(a0) # interval + lw a3, 0(a1) # a3 = CLINT_MTIMECMP(hart) + add a3, a3, a2 # a3 += interval + sw a3, 0(a1) # CLINT_MTIMECMP(hart) = a3 + + csrr a7, mepc # a7 = mepc, for sys_kernel jump back to interrupted point + la a1, sys_kernel # mepc = sys_kernel + csrw mepc, a1 # mret : will jump to sys_kernel + + lw a3, 8(a0) + lw a2, 4(a0) + lw a1, 0(a0) + csrrw a0, mscratch, a0 # exchange(mscratch,a0) + + mret # jump to mepc (=sys_kernel) diff --git a/04-TimerInterrupt/timer.c b/04-TimerInterrupt/timer.c index e188cd3..ed34982 100644 --- a/04-TimerInterrupt/timer.c +++ b/04-TimerInterrupt/timer.c @@ -1,6 +1,9 @@ #include "timer.h" -#define interval 10000000 // cycles; about 1 second in qemu. +extern void os_kernel(); + +// a scratch area per CPU for machine-mode timer interrupts. +reg_t timer_scratch[NCPU][5]; void timer_init() { @@ -8,8 +11,18 @@ void timer_init() int id = r_mhartid(); // ask the CLINT for a timer interrupt. + int interval = 10000000; // cycles; about 1 second in qemu. *(reg_t*)CLINT_MTIMECMP(id) = *(reg_t*)CLINT_MTIME + interval; + // prepare information in scratch[] for timervec. + // scratch[0..2] : space for timervec to save registers. + // scratch[3] : address of CLINT MTIMECMP register. + // scratch[4] : desired interval (in cycles) between timer interrupts. + reg_t *scratch = &timer_scratch[id][0]; + scratch[3] = CLINT_MTIMECMP(id); + scratch[4] = interval; + w_mscratch((reg_t)scratch); + // set the machine-mode trap handler. w_mtvec((reg_t)sys_timer); @@ -22,15 +35,7 @@ void timer_init() static int timer_count = 0; -reg_t timer_handler(reg_t epc, reg_t cause) -{ - reg_t return_pc = epc; - // disable machine-mode timer interrupts. - w_mie(~((~r_mie()) | (1 << 7))); +void timer_handler() { lib_printf("timer_handler: %d\n", ++timer_count); - int id = r_mhartid(); - *(reg_t *)CLINT_MTIMECMP(id) = *(reg_t *)CLINT_MTIME + interval; - // enable machine-mode timer interrupts. - w_mie(r_mie() | MIE_MTIE); - return return_pc; + // os_kernel(); } diff --git a/04-TimerInterrupt/timer.h b/04-TimerInterrupt/timer.h index 2c15d29..73b9f3a 100644 --- a/04-TimerInterrupt/timer.h +++ b/04-TimerInterrupt/timer.h @@ -5,7 +5,7 @@ #include "sys.h" #include "lib.h" -extern reg_t timer_handler(reg_t epc, reg_t cause); +extern void timer_handler(); extern void timer_init(); #endif diff --git a/04-TimerInterrupt2.zip b/04-TimerInterrupt2.zip deleted file mode 100644 index 5b8697c..0000000 Binary files a/04-TimerInterrupt2.zip and /dev/null differ diff --git a/04-TimerInterrupt3.zip b/04-TimerInterrupt3.zip deleted file mode 100644 index 9f946db..0000000 Binary files a/04-TimerInterrupt3.zip and /dev/null differ diff --git a/doc/tw/04-TimerInterrupt.md b/doc/tw/04-TimerInterrupt.md index 598cf61..f8bd376 100644 --- a/doc/tw/04-TimerInterrupt.md +++ b/doc/tw/04-TimerInterrupt.md @@ -33,7 +33,6 @@ RISC-V 架構有規定,系統平台必須要有一個計時器。並且,該 ``` 在了解如何產生 Timer interrupt 後,等等在下面的介紹我們就會看到有一段程式碼描述每個中斷觸發的時間間隔 (Interval)。 - 不只如此,我們還需要在系統初始化的時候開啟 Timer interrupt ,具體的作法是: 開啟將 mie register 負責管理 Timer interrupt 的域寫成 1 。 ### 什麼是中斷向量表 @@ -99,7 +98,7 @@ csrw mepc, a0 ``` - csrrw[i] - 將 csr 的值寫入 rd ,同時將 rs1 的值寫入 csr 。 + 將 csr 的值寫入 rd 後,且將 rs1 的值寫入 csr 。 ```assembly= csrrw rd, csr, rs1/imm @@ -115,11 +114,10 @@ csrrw t6, mscratch, t6 ## 系統執行 -首先讓我們展示一下系統的執行狀況,當你用 make clean, make 等指令建置好 04-TimerInterrupt 下的專案後,就可以用 make qemu 開始執行,結果如下: +首先讓我們展示一下系統的執行狀況,當你用 make clean, make 等指令建置好 +04-TimerInterrupt 下的專案後,就可以用 make qemu 開始執行,結果如下: ``` -$ make -riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c timer.c os.c $ make qemu Press Ctrl-A and then X to exit QEMU qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf @@ -129,24 +127,7 @@ timer_handler: 2 timer_handler: 3 timer_handler: 4 timer_handler: 5 -timer_handler: 6 -$ make clean -rm -f *.elf -$ make -riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c timer.c os.c -$ make qemu -Press Ctrl-A and then X to exit QEMU -qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf -OS start -timer_handler: 1 -timer_handler: 2 -timer_handler: 3 -timer_handler: 4 -timer_handler: 5 -timer_handler: 6 -timer_handler: 7 -timer_handler: 8 -timer_handler: 9 +QEMU: Terminated ``` 系統會很穩定的以大約每秒一次的方式,印出 `timer_handler: i` 這樣的訊息,這代表時間中斷機制啟動成功,而且定時進行中斷。 @@ -162,12 +143,13 @@ int os_main(void) { lib_puts("OS start\n"); timer_init(); // start timer interrupt ... - while (1) {} // os : do nothing, just loop! + + while (1) {} // stop here ! return 0; } ``` -基本上這個程式印出了 `OS start` 之後,啟動了時間中斷,然後就進入 os_loop() 無窮迴圈函數卡住了。 +基本上這個程式印出了 `OS start` 之後,啟動了時間中斷,然後就進入無窮迴圈卡住了。 但是為何該系統之後還會印出 `timer_handler: i` 這樣的訊息呢? @@ -184,7 +166,10 @@ timer_handler: 3 ```cpp #include "timer.h" -#define interval 10000000 // cycles; about 1 second in qemu. +extern void os_kernel(); + +// a scratch area per CPU for machine-mode timer interrupts. +reg_t timer_scratch[NCPU][5]; void timer_init() { @@ -192,8 +177,18 @@ void timer_init() int id = r_mhartid(); // ask the CLINT for a timer interrupt. + int interval = 10000000; // cycles; about 1 second in qemu. *(reg_t*)CLINT_MTIMECMP(id) = *(reg_t*)CLINT_MTIME + interval; + // prepare information in scratch[] for timervec. + // scratch[0..2] : space for timervec to save registers. + // scratch[3] : address of CLINT MTIMECMP register. + // scratch[4] : desired interval (in cycles) between timer interrupts. + reg_t *scratch = &timer_scratch[id][0]; + scratch[3] = CLINT_MTIMECMP(id); + scratch[4] = interval; + w_mscratch((reg_t)scratch); + // set the machine-mode trap handler. w_mtvec((reg_t)sys_timer); @@ -203,35 +198,86 @@ void timer_init() // enable machine-mode timer interrupts. w_mie(r_mie() | MIE_MTIE); } + +static int timer_count = 0; + +void timer_handler() { + lib_printf("timer_handler: %d\n", ++timer_count); + // os_kernel(); +} ``` -而 [sys.s] 裏的 sys_timer 這個函數,會用 csrr 這個特權指令將 mepc 特權暫存器 (儲存中斷點的位址) 暫存入 a0 當中保存,等到 timer_handler() 執行完後,才能透過 mret 回到中斷點。 +而 [sys.s] 裏的 sys_timer 這個函數,會用 csrr 這個特權指令將 mepc 特權暫存器暫存入 a7 當中,然後設定 mepc = sys_kernel 。 ```s sys_timer: - # call the C timer_handler(reg_t epc, reg_t cause) - csrr a0, mepc - csrr a1, mcause - call timer_handler - - # timer_handler will return the return address via a0. - csrw mepc, a0 - - mret # back to interrupt location (pc=mepc) + # .... + csrr a7, mepc # a7 = mepc, for sys_kernel jump back to interrupted point + la a1, sys_kernel # mepc = sys_kernel + csrw mepc, a1 # mret : will jump to sys_kernel + # .... + mret ``` 在這裡讀者必須先理解 RISC-V 的中斷機制,RISC-V 基本上有三種執行模式,那就是《機器模式 machine mode, 超級模式 super mode 與 使用者模式 user mode》。 -本書中 mini-riscv-os 的所有範例,都是在機器模式 (machine mode) 下執行的,沒有使用到 super mode 或 user mode。 +本書中 mini-riscv-os 的所有範例,都是在機器模式下執行的,沒有使用到 super mode 或 user mode。 而 mepc 就是機器模式下中斷發生時,硬體會自動執行 mepc=pc 的動作。 當 sys_timer 在執行 mret 後,硬體會執行 pc=mepc 的動作,然後就跳回原本的中斷點繼續執行了。(就好像沒發生過甚麼事一樣) -以上我們已經將 RISC-V 的中斷機制原理粗略的講解完了,但是更細緻的過程,還得進一步理解 RISC-V 處理器的機器模式 (machine mode) 相關特權暫存器,像是 mhartid (處理器核心代號),mstatus (狀態暫存器),mie (中斷暫存器) 等等。 +但是我們想要偷樑換柱,讓 sys_timer 在執行到 mret 時,跳到另一個 sys_kernel 函數去執行。 + +```s +sys_kernel: + addi sp, sp, -128 # alloc stack space + reg_save sp # save all registers + call timer_handler # call timer_handler in timer.c + reg_load sp # restore all registers + addi sp, sp, 128 # restore stack pointer + jr a7 # jump to a7=mepc , return to timer break point +``` + +因此我們在 sys_timer 裏,偷偷的把 mepc 換為 sys_kernel ,並且將原本的 mepc 保存在 a7 暫存器當中。(a7 暫存器預設是函數呼叫時的第七個暫存器,因為參數很少到達 7 個以上,所以很少被用到,我們拿來偷存 mepc,希望不會因此影響到系統行為) + +```s +sys_timer: + # .... + csrr a7, mepc # a7 = mepc, for sys_kernel jump back to interrupted point + la a1, sys_kernel # mepc = sys_kernel + csrw mepc, a1 # mret : will jump to sys_kernel + # .... + mret +``` + +這樣當 mret 執行時,就會跳到 sys_kernel 去,而不是跳回被打斷那個點。 + +然後我們在 sys_kernel 裏才去真正呼叫中斷時要做的 C 語言函數 timer_handler。 + +當然,在呼叫 C 語言函數前必須先把目前暫存器全都透過 reg_save 這個巨集保存起來,呼叫完 timer_handler 之後將暫存器的內容恢復,才能像沒事一樣繼續回到原本的中斷點繼續執行。(否則暫存器被改掉的話,回到中斷點也無法正確地繼續執行了) + +```s +sys_kernel: + addi sp, sp, -128 # alloc stack space + reg_save sp # save all registers + call timer_handler # call timer_handler in timer.c + reg_load sp # restore all registers + addi sp, sp, 128 # restore stack pointer + jr a7 # jump to a7=mepc , return to timer break point +``` + +當 timer_handler 呼叫完成,且恢復了暫存器之後,就可以透過 jr a7 這個指令,跳回到當初保存的返回點 (a7=mepc),於是又回到原本的中斷點繼續執行,好像沒發生過甚麼事一樣了。 + +以上我們已經將 RISC-V 的中斷機制原理粗略的講解完了,但是更細緻的過程,還得進一步理解 RISC-V 處理器的機器模式 (machine mode) 相關特權暫存器,像是 mhartid (處理器核心代號), mscratch (臨時暫存區起始點), mstatus (狀態暫存器),mie (中斷暫存器) 等等。 ```cpp -#define interval 10000000 // cycles; about 1 second in qemu. +#include "timer.h" + +extern void os_kernel(); + +// a scratch area per CPU for machine-mode timer interrupts. +reg_t timer_scratch[NCPU][5]; void timer_init() { @@ -239,8 +285,18 @@ void timer_init() int id = r_mhartid(); // ask the CLINT for a timer interrupt. + int interval = 10000000; // cycles; about 1 second in qemu. *(reg_t*)CLINT_MTIMECMP(id) = *(reg_t*)CLINT_MTIME + interval; + // prepare information in scratch[] for timervec. + // scratch[0..2] : space for timervec to save registers. + // scratch[3] : address of CLINT MTIMECMP register. + // scratch[4] : desired interval (in cycles) between timer interrupts. + reg_t *scratch = &timer_scratch[id][0]; + scratch[3] = CLINT_MTIMECMP(id); + scratch[4] = interval; + w_mscratch((reg_t)scratch); + // set the machine-mode trap handler. w_mtvec((reg_t)sys_timer); @@ -259,28 +315,57 @@ RISC-V 的時間中斷機制是比較 CLINT_MTIME 與 CLINT_MTIMECMP 兩個數 因此 timer_init() 函數才會有下列指令 ```cpp - *(reg_t*)CLINT_MTIMECMP(id) = *(reg_t*)CLINT_MTIME + interval; + *(reg_t*)CLINT_MTIMECMP(id) = *(reg_t*)CLINT_MTIME + interval; ``` -該指令就是為了設定第一次的中斷時間。 +就是為了設定第一次的中斷時間。 -同樣的,在 [timer.c] 的 timer_handler 裏,也要去設定下一次的中斷時間: +同樣的,在 [sys.c] 的 sys_timer 裏,也要去設定下一次的中斷時間: -```cpp -reg_t timer_handler(reg_t epc, reg_t cause) -{ - reg_t return_pc = epc; - // disable machine-mode timer interrupts. - w_mie(~((~r_mie()) | (1 << 7))); - lib_printf("timer_handler: %d\n", ++timer_count); - int id = r_mhartid(); - *(reg_t *)CLINT_MTIMECMP(id) = *(reg_t *)CLINT_MTIME + interval; - // enable machine-mode timer interrupts. - w_mie(r_mie() | MIE_MTIE); - return return_pc; -} +```s +sys_timer: + # timer_init() has set up the memory that mscratch points to: + # scratch[0,4,8] : register save area. + # scratch[12] : address of CLINT's MTIMECMP register. + # scratch[16] : desired interval between interrupts. + + csrrw a0, mscratch, a0 # exchange(mscratch,a0) + sw a1, 0(a0) + sw a2, 4(a0) + sw a3, 8(a0) + + # schedule the next timer interrupt + # by adding interval to mtimecmp. + lw a1, 12(a0) # CLINT_MTIMECMP(hart) + lw a2, 16(a0) # interval + lw a3, 0(a1) # a3 = CLINT_MTIMECMP(hart) + add a3, a3, a2 # a3 += interval + sw a3, 0(a1) # CLINT_MTIMECMP(hart) = a3 + + csrr a7, mepc # a7 = mepc, for sys_kernel jump back to interrupted point + la a1, sys_kernel # mepc = sys_kernel + csrw mepc, a1 # mret : will jump to sys_kernel + + lw a3, 8(a0) + lw a2, 4(a0) + lw a1, 0(a0) + csrrw a0, mscratch, a0 # exchange(mscratch,a0) + + mret # jump to mepc (=sys_kernel) +``` + +請特別注意其中的這段程式碼,其目的是將 CLINT_MTIMECMP 加上 interval,也就是 CLINT_MTIMECMP += interval 的意思。 + +```s + # schedule the next timer interrupt + # by adding interval to mtimecmp. + lw a1, 12(a0) # CLINT_MTIMECMP(hart) + lw a2, 16(a0) # interval + lw a3, 0(a1) # a3 = CLINT_MTIMECMP(hart) + add a3, a3, a2 # a3 += interval + sw a3, 0(a1) # CLINT_MTIMECMP(hart) = a3 ``` 這樣下一次 CLINT_MTIMECMP 時間到的時候,CLINT_MTIME 就會大於 CLINT_MTIMECMP,於是中斷就再次發生了。 -以上就是 RISC-V 處理器在 virt 這個虛擬機器上的時間中斷機制原理! +以上就是 RISC-V 處理器在 virt 這個虛擬機器上的中斷機制原理!