RISC-V Bare Bones: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
m (references and links)
m (update source tags to syntaxhighlight)
 
(14 intermediate revisions by 9 users not shown)
Line 11: Line 11:
The thing we will make here is a simple serial port output/reader.
The thing we will make here is a simple serial port output/reader.


<source lang="c">
<syntaxhighlight lang="c">
#include <stdint.h>
#include <stdint.h>
#include <stddef.h>
#include <stddef.h>

volatile uint8_t * uart = (volatile uint8_t *)0x10000000;
unsigned char * uart = (unsigned char *)0x10000000;

void putchar(char c) {
void putchar(char c) {
*uart = c;
*uart = c;
return;
return;
}
}

void print(const char * str) {
void print(const char * str) {
while(*str != '\0') {
while(*str != '\0') {
putchar(*str);
putchar(*str);
str++;
}
}
return;
return;
}
}

void kmain(void) {
void kmain(void) {
print("Hello world!\r\n");
print("Hello world!\r\n");
Line 35: Line 35:
putchar(*uart);
putchar(*uart);
}
}
return;
__builtin_unreachable();
}
}
</syntaxhighlight>
</source>


This will print a simple "Hello world!" and a newline following, after that it will echo any input given to the serial port.
This will print a simple "Hello world!" and a newline following, after that it will echo any input given to the serial port.


== boot.S ==
== entry.S ==
Be wary we are just setting up stack and jumping into kernel. It's the kernel's job to set up IRQs and other system stuff
Be wary we are just setting up stack and jumping into kernel. It's the kernel's job to set up IRQs and other system stuff


<source lang="asm">
<syntaxhighlight lang="asm">
.section .init
.section .init


Line 56: Line 56:
.option push
.option push
.option norelax
.option norelax
la gp, _global_pointer
la gp, global_pointer
.option pop
.option pop
Line 71: Line 71:
sd zero, (t5)
sd zero, (t5)
addi t5, t5, 8
addi t5, t5, 8
bgeu t5, t6, bss_clear
bltu t5, t6, bss_clear
la t0, kmain
la t0, kmain
Line 78: Line 78:
/* Jump to kernel! */
/* Jump to kernel! */
tail kmain
tail kmain
/* If the kernel returns (should NOT) then we hang forever, this is equivalent to hlt */
infinite:
wfi
j infinite
.cfi_endproc
.cfi_endproc

.end
.end

</source>
</syntaxhighlight>


Take in account that you need to implement a type of locking if you want to support SMP, and implement a trampoline or a jail for the CPUs. Be wary that we haven't enabled interrupts just yet and any return from kmain will result in invalid opcodes being executed.
Take in account that you need to implement a type of locking if you want to support SMP, and implement a trampoline or a jail for the CPUs. Be wary that we haven't enabled interrupts just yet and any return from kmain will result in invalid opcodes being executed.
Line 94: Line 91:
We need to specify where our kernel will be loaded at. For this example we will use the start of the RAM as our load address.
We need to specify where our kernel will be loaded at. For this example we will use the start of the RAM as our load address.


<source lang="c">
<syntaxhighlight lang="c">
ENTRY(start);
ENTRY(start);

. = 0x80000000;
. = 0x80000000;

SECTIONS {
SECTIONS {
/* Include entry point at start of binary */
/* Include entry point at start of binary */
.text BLOCK(16K) : ALIGN(4K) {
.text : ALIGN(4K) {
*(.init);
*(.text);
*(.text);
}
}
.bss : ALIGN(4K) {
PROVIDE(_global_pointer = .);
.bss BLOCK(8K) : ALIGN(4K) {
PROVIDE(bss_start = .);
PROVIDE(bss_start = .);
*(COMMON);
*(.bss);
*(.bss);
. += 4096;
. += 4096;
PROVIDE(stack_top = .);
PROVIDE(stack_top = .);
. += 4096;
PROVIDE(global_pointer = .);
PROVIDE(bss_end = .);
PROVIDE(bss_end = .);
}
}
.rodata BLOCK(8K) : ALIGN(4K) {
.rodata : ALIGN(4K) {
*(.rodata);
*(.rodata);
}
}
.data BLOCK(8K) : ALIGN(4K) {
.data : ALIGN(4K) {
*(.data);
*(.data);
}
}
}
}
</syntaxhighlight>
</source>


== Final ==
== Final ==


The process is pretty straightforward:
The process is pretty straightforward:
<source lang="bash">
<syntaxhighlight lang="bash">
riscv64-elf-gcc -Wall -Wextra -c -mcmodel=medany kernel.c -o kernel.o
riscv64-elf-gcc -Wall -Wextra -c -mcmodel=medany kernel.c -o kernel.o -ffreestanding
riscv64-elf-as -c entry.S -o entry.o
riscv64-elf-as -c entry.S -o entry.o
riscv64-elf-ld -T linker.ld -lgcc -nostdlib -mcmodel=medany kernel.o entry.o -o kernel.elf
riscv64-elf-ld -T linker.ld -lgcc -nostdlib kernel.o entry.o -o kernel.elf
</syntaxhighlight>
</source>


Once this is done, the kernel can be run with the following:
Once this is done, the kernel can be run with the following:
<source lang="bash">
<syntaxhighlight lang="bash">
qemu-system-riscv64 -machine virt -kernel kernel.elf -bios none
qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf -serial mon:stdio
</syntaxhighlight>
</source>


You will promptly see a terminal with the "Hello world!" and you can "write stuff" to it. A serial terminal is very "boring", but you still have a lot at your disposal, you have a PLIC, a CLIC and a bunch of opportunities to learn PCI internals.
You will promptly see a terminal with the "Hello world!" and you can "write stuff" to it. A serial terminal is very "boring", but you still have a lot at your disposal, you have a PLIC, a CLIC and a bunch of opportunities to learn PCI internals.
Line 140: Line 138:
== Going further ==
== Going further ==
* Store input somewhere for later processing (i.e: "echo" command)
* Store input somewhere for later processing (i.e: "echo" command)
* Implement SMP support, for now our basic bootloader will crash on any multicore system
* Implement SMP support, for now our basic kernel will crash on any multicore system
* QEMU will give FDT in register a1. Use libfdt to parse the tree to avoid nightrames
* QEMU will give FDT in register a1. Use libfdt to parse the tree to avoid nightrames
* Implement a heap
* Implement a heap
Line 151: Line 149:
* [[PCI Express]]
* [[PCI Express]]
* [[HiFive-1 Bare Bones]]
* [[HiFive-1 Bare Bones]]
* [[RISC-V_Meaty_Skeleton_with_QEMU_virt_board|RISC-V Meaty Skeleton with QEMU virt board]]
* [https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html RISC-V instruction set]
* [https://msyksphinz-self.github.io/riscv-isadoc/html/rvi.html RISC-V instruction set]
* [https://osblog.stephenmarz.com/ Small Bare Metal RISCV OS in asm and Rust]
* [https://marz.utk.edu/my-courses/cosc562/riscv/ Page of above with asm code setting up different kernel stack for each hart]
* [https://github.com/cksystemsteaching/selfie Selfie: a minimalist self hosting compiler, riscv64 os, and simulator]
* [https://github.com/cksystemsteaching/selfie/tree/main/theses Selfie page includes 2 well written bare-metal theses]
* [https://github.com/phoenix-rtos Complete open source Microkernel for riscv64 (also arm7, ia32) plus support code]
* [https://github.com/mit-pdos/xv6-riscv XV6: a reimplementation of original Unix for teaching riscv64 OS development]
* [https://github.com/michaelengel/xv6-vf2 Port of XV6 to execute on VisionFive 2 SoC]
* [https://github.com/lupyuen/nuttx-star64 Wonderfully clear and detailed base level port how2: NuttX RTOS to Star64 SoC]

[[Category:Bare bones tutorials|RISC-V bare bones]]
[[Category:RISC-V]]

Latest revision as of 01:23, 9 June 2024

This page is a stub.
You can help the wiki by accurately adding more contents to it.

RISC-V is a instruction set architecture, fully opensource. The ISA has a bunch of extensions, in this tutorial we will assume that imad are available.

At our disposal we will have a generic board we will program for: virt. This board is available for QEMU.

You can obtain a riscv64 gcc toolchain on kernel.org; Designing an operating system for this architecture is not complicated and is pretty straight forward:

kernel.c

The thing we will make here is a simple serial port output/reader.

#include <stdint.h>
#include <stddef.h>
 
unsigned char * uart = (unsigned char *)0x10000000; 
void putchar(char c) {
	*uart = c;
	return;
}
 
void print(const char * str) {
	while(*str != '\0') {
		putchar(*str);
		str++;
	}
	return;
}
 
void kmain(void) {
	print("Hello world!\r\n");
	while(1) {
		// Read input from the UART
		putchar(*uart);
	}
	return;
}

This will print a simple "Hello world!" and a newline following, after that it will echo any input given to the serial port.

entry.S

Be wary we are just setting up stack and jumping into kernel. It's the kernel's job to set up IRQs and other system stuff

.section .init

.option norvc

.type start, @function
.global start
start:
	.cfi_startproc
	
.option push
.option norelax
	la gp, global_pointer
.option pop
	
	/* Reset satp */
	csrw satp, zero
	
	/* Setup stack */
	la sp, stack_top
	
	/* Clear the BSS section */
	la t5, bss_start
	la t6, bss_end
bss_clear:
	sd zero, (t5)
	addi t5, t5, 8
	bltu t5, t6, bss_clear
	
	la t0, kmain
	csrw mepc, t0
	
	/* Jump to kernel! */
	tail kmain
	
	.cfi_endproc

.end

Take in account that you need to implement a type of locking if you want to support SMP, and implement a trampoline or a jail for the CPUs. Be wary that we haven't enabled interrupts just yet and any return from kmain will result in invalid opcodes being executed.

linker.ld

We need to specify where our kernel will be loaded at. For this example we will use the start of the RAM as our load address.

ENTRY(start);
 
. = 0x80000000;
 
SECTIONS {
	/* Include entry point at start of binary */
	.text : ALIGN(4K) {
		*(.init);
		*(.text);
	}
	.bss : ALIGN(4K) {
		PROVIDE(bss_start = .);
		*(.bss);
		. += 4096;
		PROVIDE(stack_top = .);
		. += 4096;
		PROVIDE(global_pointer = .);
		PROVIDE(bss_end = .);
	}
	.rodata : ALIGN(4K) {
		*(.rodata);
	}
	.data : ALIGN(4K) {
		*(.data);
	}
}

Final

The process is pretty straightforward:

riscv64-elf-gcc -Wall -Wextra -c -mcmodel=medany kernel.c -o kernel.o -ffreestanding
riscv64-elf-as -c entry.S -o entry.o
riscv64-elf-ld -T linker.ld -lgcc -nostdlib kernel.o entry.o -o kernel.elf

Once this is done, the kernel can be run with the following:

qemu-system-riscv64 -machine virt -bios none -kernel kernel.elf -serial mon:stdio

You will promptly see a terminal with the "Hello world!" and you can "write stuff" to it. A serial terminal is very "boring", but you still have a lot at your disposal, you have a PLIC, a CLIC and a bunch of opportunities to learn PCI internals.

Going further

  • Store input somewhere for later processing (i.e: "echo" command)
  • Implement SMP support, for now our basic kernel will crash on any multicore system
  • QEMU will give FDT in register a1. Use libfdt to parse the tree to avoid nightrames
  • Implement a heap
  • Enable Sv39 paging
  • SMP support
  • Manage IRQs
  • You have a PIO and a MMIO window at your disposal, use them wisely, you will also have to face the PCIe ECAM, for your sanity: Do not implement bridges

See also