吳中玄
I will develop an OS Kernel based on RISC-V and use QEMU as the simulation tool. QEMU can emulate a virtual RISC-V platform, including the CPU, memory, and peripheral devices, enabling me to proceed with the development
I will accomplish the following functions.
1.Bootstrap
2.Print Hello World!
3.Mem_Management
4.Context_Switch
5.Trap
_start:
# park harts with id != 0
csrr t0, mhartid # read current hart id
mv tp, t0 # keep CPU's hartid in its tp for later usage.
bnez t0, park # if not hart 0
park:
wfi
j park
The system is configured to have 8 harts by default, but in this project, I am only using a single hart. Therefore, I check whether the current hart is the only active one (hart ID 0) and treat hart 0 as the primary core. All other harts are set to idle. The wfi instruction will halt the current hart and put it into an idle state.
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 first stack space
add sp, sp, t0 # move the current hart stack pointer
# to its place in the stack space
j start_kernel # hart 0 jump to c
Hart 0 serves as the primary core responsible for executing tasks.
Since the RISC-V simulation using QEMU does not have a display, I need to use UART to connect the QEMU-simulated RISC-V machine with my host machine, allowing the simulation results to be displayed on my host.
uart_write_reg(IER, 0x00);
uint8_t lcr = uart_read_reg(LCR);
uart_write_reg(LCR, lcr | (1 << 7));
uart_write_reg(DLL, 0x03);
uart_write_reg(DLM, 0x00);
When initializing the UART, the interrupts are first disabled (by setting IER to 0) to prevent interruptions during the initialization process.
The UART baud rate is configured using the baud rate divisor, and accessing the DLL and DLM registers requires setting the 7th bit of the LCR register (DLAB) to 1.
The UART clock frequency is 1.8432 MHz, and my target baud rate is 38400 bps, so the divisor must be set to 3 to achieve this.
Thus,DLL and DLM are set to 3.
int uart_putc(char ch)
{
while ((uart_read_reg(LSR) & LSR_TX_IDLE) == 0);
return uart_write_reg(THR, ch);
}
uart_puts("Hello, World!\n");
Check the 5th bit of the LSR register to confirm whether the transmit buffer is idle. When the buffer is idle, write the character to the THR, and the UART begins transmission
OUTPUT_ARCH("riscv")
ENTRY(_start)
MEMORY
{
ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = LENGTH_RAM
}
SECTIONS
{
.text : {
PROVIDE(_text_start = .);
*(.text .text.*)
PROVIDE(_text_end = .);
} >ram
.rodata : {
PROVIDE(_rodata_start = .);
*(.rodata .rodata.*)
PROVIDE(_rodata_end = .);
} >ram
.data : {
. = ALIGN(4096);
PROVIDE(_data_start = .);
*(.sdata .sdata.*)
*(.data .data.*)
PROVIDE(_data_end = .);
} >ram
.bss :{
PROVIDE(_bss_start = .);
*(.sbss .sbss.*)
*(.bss .bss.*)
*(COMMON)
PROVIDE(_bss_end = .);
} >ram
PROVIDE(_memory_start = ORIGIN(ram));
PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram));
PROVIDE(_heap_start = _bss_end);
PROVIDE(_heap_size = _memory_end - _heap_start);
Define the output machine architecture as RISC-V to ensure the executable file is generated for the RISC-V platform.
Define the available memory regions in the system, specifying their attributes, starting addresses, and sizes.
Define the setup for sections (.text, .rodata, .data, .bss).
Finally, calculate the remaining memory size to determine how to configure the heap.
#a0:pointer to the context of the next task
switch_to:
csrrw t6, mscratch, t6 # swap t6 and mscratch
beqz t6, 1f # Note: the first time switch_to() is
# called, mscratch is initialized as zero
# (in sched_init()), which makes t6 zero,
# and that's the special case we have to
# handle with t6
reg_save t6 # save context of prev task
# Save the actual t6 register, which we swapped into
# mscratch
mv t5, t6 # t5 points to the context of current task
csrr t6, mscratch # read t6 back from mscratch
STORE t6, 30*SIZE_REG(t5) # save t6 with t5 as base
1:
# switch mscratch to point to the context of the next task
csrw mscratch, a0
# Restore all GP registers
# Use t6 to point to the context of the new task
mv t6, a0
reg_restore t6
# Do actual context switching.
ret
trap_vector:
# save context(registers).
csrrw t6, mscratch, t6 # swap t6 and mscratch
reg_save t6
# Save the actual t6 register, which we swapped into
# mscratch
mv t5, t6 # t5 points to the context of current task
csrr t6, mscratch # read t6 back from mscratch
STORE t6, 30*SIZE_REG(t5) # save t6 with t5 as base
# Restore the context pointer into mscratch
csrw mscratch, t5
# call the C trap handler in trap.c
csrr a0, mepc
csrr a1, mcause
call trap_handler
# trap_handler will return the return address via a0.
csrw mepc, a0
# restore context(registers).
csrr t6, mscratch
reg_restore t6
# return to whatever we were doing before trap.
mret
Save the context of the current control flow, then jump to trap_handler to handle the interrupt or exception.
Finally, restore the context and execute mret to return to the state before the trap.
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");
break;
case 7:
uart_puts("timer interruption!\n");
break;
case 11:
uart_puts("external interruption!\n");
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
return_pc += 4;
}
return return_pc;
}
void trap_test()
{
*(int *)0x00000000 = 100;
uart_puts("Leave the trap state!\n");
}
The if-else statement determines whether this trap is an interrupt or an exception. If it is 1, it is an interrupt; if it is 0, it is an exception.
For interrupts, the switch-case statement is used to identify the reason for the interrupt.
For exceptions, the program currently skips the exception and executes the next instruction (directly exiting the exception).
int plic_claim(void)
{
int hart = r_tp(); // Retrieve the hardware thread ID (hart ID) of the current processor
int irq = *(uint32_t*)PLIC_MCLAIM(hart); // Read the interrupt number from the interrupt request register of the PLIC
return irq; // Return the interrupt number
}
The plic_claim function is used to determine which external device has triggered the current interrupt.
void plic_complete(int irq)
{
int hart = r_tp(); // Retrieve the hardware thread ID (hart ID) of the current processor
*(uint32_t*)PLIC_MCOMPLETE(hart) = irq; // Write the interrupt number back to the completion register to notify the PLIC that the interrupt processing is complete
}
The plic_complete function is used to notify that the interrupt processing is complete.
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");
break;
case 7:
uart_puts("timer interruption!\n");
break;
case 11:
uart_puts("external interruption!\n");
external_interrupt_handler();
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
return_pc += 4;
}
return return_pc;
}
void external_interrupt_handler()
{
int irq = plic_claim();
if (irq == UART0_IRQ){
printf("irq = %d\n", irq);
uart_isr();
} else if (irq) {
printf("unexpected interrupt irq = %d\n", irq);
}
if (irq) {
plic_complete(irq);
}
}
int uart_getc(void)
{
while (0 == (uart_read_reg(LSR) & LSR_RX_READY))
;
return uart_read_reg(RHR);
}
/*
* handle a uart interrupt, raised because input has arrived, called from trap.c.
*/
void uart_isr(void)
{
char received_char = uart_getc();
uart_putc(received_char);
uart_putc('\n');
}
First, determine the reason for the trap. In this example, it is an external interrupt, so it enters case 11 and proceeds to external_interrupt_handler(). Then, plic_claim is used to identify which external device triggered the interrupt. In this example, it simulates inputting a character, so the source is UART (with an IRQ of 10 in the PLIC). Next, uart_isr is called to handle the interrupt. Finally, plic_complete is used to notify that the interrupt processing is complete, and the program returns to the next instruction of the original program to continue execution.
reg_t trap_handler(reg_t epc, reg_t cause)
{
reg_t return_pc = epc;
reg_t cause_code = cause & MCAUSE_MASK_ECODE;
if (cause & MCAUSE_MASK_INTERRUPT) {
/* Asynchronous trap - interrupt */
switch (cause_code) {
case 3:
uart_puts("software interruption!\n");
/*
* acknowledge the software interrupt by clearing
* the MSIP bit in mip.
*/
int id = r_mhartid();
*(uint32_t*)CLINT_MSIP(id) = 0;
schedule();
break;
case 7:
uart_puts("timer interruption!\n");
timer_handler();
break;
case 11:
uart_puts("external interruption!\n");
external_interrupt_handler();
break;
default:
printf("Unknown async exception! Code = %ld\n", cause_code);
break;
}
} else {
/* Synchronous trap - exception */
printf("Sync exceptions! Code = %ld\n", cause_code);
panic("OOPS! What can I do!");
//return_pc += 4;
}
return return_pc;
}
void timer_load(int interval)
{
/* each CPU has a separate source of timer interrupts. */
int id = r_mhartid();
*(uint64_t*)CLINT_MTIMECMP(id) = *(uint64_t*)CLINT_MTIME + interval;
}
void timer_init()
{
/*
* On reset, mtime is cleared to zero, but the mtimecmp registers
* are not reset. So we have to init the mtimecmp manually.
*/
timer_load(TIMER_INTERVAL);
/* enable machine-mode timer interrupts. */
w_mie(r_mie() | MIE_MTIE);
}
void timer_handler()
{
_tick++;
printf("tick: %d\n", _tick);
timer_load(TIMER_INTERVAL);
schedule();
}
void user_task0(void)
{
uart_puts("Task 0: Created!\n");
while (1) {
uart_puts("Task 0: Running...\n");
task_delay(DELAY);
}
}
void user_task1(void)
{
uart_puts("Task 1: Created!\n");
while (1) {
uart_puts("Task 1: Running...\n");
task_delay(DELAY);
}
}
void task_delay(volatile int count)
{
count *= 50000;
while (count--);
}