Try   HackMD

Minimal OS Kernel for RISC-V

吳中玄

Mission

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

1.Bootstrap

_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.

2.Print Hello World!

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.

Baud Rate=UART Clock Frequency16×Divisor

Divisor=184320016×38400=3
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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

3.Mem_Management

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

linker script

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.

4.Context_Switch

#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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

5.Trap

Exception

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).
image

Interrupt

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');
}

image
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.

6.Preemptive

image

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--);
}

image
image