diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2024-09-03 11:53:15 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2024-12-05 16:02:49 +0100 |
| commit | 4f69fb67ee229c7b59ced5db64142b527d74ce7b (patch) | |
| tree | d9aab519f0caa6086368c297d9849bbf7e4589b0 /arch | |
| parent | e94cc18273810ce9cc44d80608042c16eae4e6e4 (diff) | |
| download | farga-4f69fb67ee229c7b59ced5db64142b527d74ce7b.tar.gz farga-4f69fb67ee229c7b59ced5db64142b527d74ce7b.zip | |
Provide an improved example on early boot stage
This also implied a renaming arch/riscv/bios -> arch/riscv/machine.
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'arch')
| -rw-r--r-- | arch/riscv/bios/Makefile | 25 | ||||
| -rw-r--r-- | arch/riscv/bios/config.ld | 9 | ||||
| -rw-r--r-- | arch/riscv/bios/hello.s | 23 | ||||
| -rw-r--r-- | arch/riscv/machine/Makefile | 33 | ||||
| -rw-r--r-- | arch/riscv/machine/README.md | 117 | ||||
| -rw-r--r-- | arch/riscv/machine/config.ld | 10 | ||||
| -rw-r--r-- | arch/riscv/machine/gdb.txt | 3 | ||||
| -rw-r--r-- | arch/riscv/machine/machine.S | 125 | ||||
| -rw-r--r-- | arch/riscv/usr/basics.c | 14 |
9 files changed, 295 insertions, 64 deletions
diff --git a/arch/riscv/bios/Makefile b/arch/riscv/bios/Makefile deleted file mode 100644 index 60e3cc4..0000000 --- a/arch/riscv/bios/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -NAME = hello - -CROSS_AS ?= riscv64-suse-linux-as -CROSS_LD ?= riscv64-suse-linux-ld -CROSS_DUMP ?= riscv64-suse-linux-objdump -ASFLAGS = -march=rv64g -mabi=lp64 - -EMU ?= qemu-system-riscv64 - -## -# Targets - -.PHONY: run -run: clean build - $(CROSS_DUMP) -D $(NAME) - $(EMU) -machine virt -bios hello - -.PHONY: build -build: - $(CROSS_AS) $(ASFLAGS) -o $(NAME).o -c $(NAME).s - $(CROSS_LD) -T config.ld $(NAME).o -o $(NAME) - -.PHONY: clean -clean: - @rm -f $(NAME).o $(NAME) diff --git a/arch/riscv/bios/config.ld b/arch/riscv/bios/config.ld deleted file mode 100644 index 085caa3..0000000 --- a/arch/riscv/bios/config.ld +++ /dev/null @@ -1,9 +0,0 @@ -MEMORY { - dram_space (rwx) : ORIGIN = 0x80000000, LENGTH = 128 -} - -SECTIONS { - .text : { - hello.o(.text.bios) - } > dram_space -} diff --git a/arch/riscv/bios/hello.s b/arch/riscv/bios/hello.s deleted file mode 100644 index f3aa20a..0000000 --- a/arch/riscv/bios/hello.s +++ /dev/null @@ -1,23 +0,0 @@ -# From: https://github.com/qemu/qemu/blob/23fa74974d8c96bc95cbecc0d4e2d90f984939f6/hw/riscv/virt.c#L82. -.set QEMU_UART0_ADDRESS, 0x10000000 - -.global _start -.section .text.bios - -_start: li a0, 0x68 - li a1, QEMU_UART0_ADDRESS - sb a0, (a1) # 'h' - - li a0, 0x65 - sb a0, (a1) # 'e' - - li a0, 0x6C - sb a0, (a1) # 'l' - - li a0, 0x6C - sb a0, (a1) # 'l' - - li a0, 0x6F - sb a0, (a1) # 'o' - -loop: j loop diff --git a/arch/riscv/machine/Makefile b/arch/riscv/machine/Makefile new file mode 100644 index 0000000..c6fe845 --- /dev/null +++ b/arch/riscv/machine/Makefile @@ -0,0 +1,33 @@ +NAME = machine + +CROSS_COMPILE ?= riscv64-suse-linux- +AS = $(CROSS_COMPILE)as +LD = $(CROSS_COMPILE)ld +ASFLAGS = -g -march=rv64g -mabi=lp64 +QEMU ?= qemu-system-riscv64 +QEMU_FLAGS ?= + +DEBUG = +ifneq ($(strip $(DEBUG)),) + QEMU_FLAGS += -s -S +endif + +## +# Targets + +.PHONY: run +run: clean build + @$(QEMU) $(QEMU_FLAGS) -machine virt -bios $(NAME) -nographic + +.PHONY: build +build: + @$(AS) $(ASFLAGS) -o $(NAME).o -c $(NAME).S + @$(LD) -T config.ld $(NAME).o -o $(NAME) + +.PHONY: clean +clean: + @rm -f $(NAME).o $(NAME) + +.PHONY: gdb +gdb: + $(Q) gdb --command gdb.txt diff --git a/arch/riscv/machine/README.md b/arch/riscv/machine/README.md new file mode 100644 index 0000000..e873b71 --- /dev/null +++ b/arch/riscv/machine/README.md @@ -0,0 +1,117 @@ +# TL;DR + +This example is an extension from [this blog post from Uros +Popovic](https://popovicu.com/posts/bare-metal-programming-risc-v/). Here we +take full advantage that the early boot process will pass down the +[FDT](https://devicetree-specification.readthedocs.io/en/stable/flattened-format.html) +to the `a1` register. This way we don't have to hardcode the value for the UART +address. + +# Description + +When booting up a RISC-V system it will start in M ("machine") privilege mode. +This mode allows code to access any piece of the hardware and it will be able to +interact with it without any intermediaries. This is where projects like +[openSBI](https://github.com/riscv-software-src/opensbi) come into place. That +is, this is software that is meant to be run at M privilege mode and provide an +API wrapper (see the [RISC-V +SBI](https://github.com/riscv-non-isa/riscv-sbi-doc) documentation) so code +above it (in S privilege mode) can talk to it without having to deal with the +myriad of hardware configurations. + +That being said this example scratches that and is meant to be run at this very +early stage of boot time. So, how does it work? Well, when you hit the power +button the system will start by running the ZSBL (Zero-Stage Boot Loader). You +can check this boot loader from QEMU take place when running this example in +debug mode. So, on one terminal run: + +``` +$ make DEBUG=1 +``` + +And then on another terminal: + +``` +$ make gdb +``` + +If you step on the code you will get the following: + +``` +0x0000000000001000 in ?? () +=> 0x0000000000001000: 00000297 auipc t0,0x0 +(gdb) si +0x0000000000001004 in ?? () +=> 0x0000000000001004: 02828613 addi a2,t0,40 +(gdb) si +0x0000000000001008 in ?? () +=> 0x0000000000001008: f1402573 csrr a0,mhartid +(gdb) si +0x000000000000100c in ?? () +=> 0x000000000000100c: 0202b583 ld a1,32(t0) +(gdb) si +0x0000000000001010 in ?? () +=> 0x0000000000001010: 0182b283 ld t0,24(t0) +(gdb) si +0x0000000000001014 in ?? () +=> 0x0000000000001014: 00028067 jr t0 +``` + +That is, only six instructions before it jumps to our code (how it knows where +to jump is another story which I won't dwelve right now). From this, `a0` will +contain the Hart ID and `a1` will contain a pointer to the +[FDT](https://devicetree-specification.readthedocs.io/en/stable/flattened-format.html) +structure. This can be further validated by looking at the [QEMU +code](https://github.com/qemu/qemu/blob/039003995047b2f7911142c7c5cfb845fda044fd/hw/riscv/boot.c#L397-L409) +that handles the reset vector. As for the FDT structure, you can check the +default one from QEMU with the following commands: + +``` +$ qemu-system-riscv64 -machine virt -machine dumpdtb=qemu.dtb +$ dtc -I dtb -O dts -o - qemu.dtb +``` + +The important information here is that the `serial` device is mapped to a +specific physical address. This address could have also been validated, again, +by [checking at QEMU's +code](https://github.com/qemu/qemu/blob/23fa74974d8c96bc95cbecc0d4e2d90f984939f6/hw/riscv/virt.c#L82). +Either way, if you send bytes to this physical address then these bytes are +guaranteed to be passed into the `serial` port. This is what the code from +[@popovicu](https://popovicu.com/posts/bare-metal-programming-risc-v/) used to +do: + +1. Hardcode the value for the UART device. +2. Send one byte at a time there to print "hello". + +As a shorter example, the following code will send "h" to the serial port: + +``` assembly +addi a0, x0, 0x68 # `a0` now contains the ASCII value for "h" +li a1, 0x10000000 # the hardcoded UART address +sb a0, (a1) # send the byte to the UART address +``` + +This is fine and all, but I wanted to take a step further. Now the code does the +following: + +1. Specifically requires `a1` to contain a pointer to the FDT blob. +2. Calls the newly created `get_serial_address` function to parse the FDT blob + in search for the address of the `serial` device. + +After calling `get_serial_address` we will have the physical address stored in +`a0`. From then on we could have just: + +``` assembly +addi a1, x0, 0x68 # `a1` now contains the ASCII value for 'h' +sb a1, (a0) # send the byte to the UART address already stored in `a0` +``` + +But, as a cherry on top, I have written a simple `printm` function, which allows +the caller to print a whole message. + +# Special thanks to + +Thanks to [@popovicu](https://github.com/popovicu) and his blog. More +specifically, [this blog post from +him](https://popovicu.com/posts/bare-metal-programming-risc-v/) was specially +useful as an entry point to this whole topic. diff --git a/arch/riscv/machine/config.ld b/arch/riscv/machine/config.ld new file mode 100644 index 0000000..7d9bbf5 --- /dev/null +++ b/arch/riscv/machine/config.ld @@ -0,0 +1,10 @@ +MEMORY { + dram_space (rwx) : ORIGIN = 0x80000000, LENGTH = 1024 +} + +SECTIONS { + .text : { + *(.text.bios) + *(.text) + } > dram_space +} diff --git a/arch/riscv/machine/gdb.txt b/arch/riscv/machine/gdb.txt new file mode 100644 index 0000000..786e6fe --- /dev/null +++ b/arch/riscv/machine/gdb.txt @@ -0,0 +1,3 @@ +set architecture riscv:rv64 +set disassemble-next-line on +target remote :1234 diff --git a/arch/riscv/machine/machine.S b/arch/riscv/machine/machine.S new file mode 100644 index 0000000..8a7ce8d --- /dev/null +++ b/arch/riscv/machine/machine.S @@ -0,0 +1,125 @@ +.section .text + +# Returns the physical address that was identified to be for the serial port. If +# any error was found then it just returns 0. This function accepts one argument +# which is a pointer to the FDT structure as given by the firmware. You can +# consider this function to have this equivalent C signature: +# +# uint64_t get_serial_address(void *fdt); +# +.type get_serial_address, @function +get_serial_address: + # Leave early if the caller passed a NULL pointer. + beqz a0, .parse_error + + # First loop. This one simply iterates over the FDT structure in search for + # 'serial@', which marks the beginning of the address definition. +.search_serial_loop_reset: + la t1, .serial +.search_serial_loop: + lbu t2, (t1) + beqz t2, .parse_hex + + lbu t3, (a0) + addi a0, a0, 1 + bne t2, t3, .search_serial_loop_reset + addi t1, t1, 1 + j .search_serial_loop + + # Second loop. On each digit shift left one nibble and add the character + # converted to an integer. If the character cannot be converted, then we go + # to `parse_error` which sets the return value to 0. +.parse_hex: + mv t1, zero + li t2, 10 + li t3, 0x30 # Character '0' + +.parse_hex_loop: + # The FDT specification guarantees that strings are NULL-terminated. Thus, + # whenever we find the NULL character, then we are done parsing. + lbu t0, (a0) + beqz t0, .get_serial_address_end + + # Trying to parse a numeric character. Note that this parser is grossly + # uncapable of handling Aa-Ff hexadecimal values, and only knows numeric + # digits. This can be improved but I did not have an example to work + # on this case. + sub t0, t0, t3 + bltz t0, .parse_error + blt t2, t0, .parse_error + + # Shift one nibble on the accumulator and add the computed value to it. + sll t1, t1, 4 + add t1, t1, t0 + + # Next loop. + addi a0, a0, 1 + j .parse_hex_loop + +.parse_error: + mv t1, zero +.get_serial_address_end: + mv a0, t1 + ret + +# Send the `message` to the given `address` so it's printed there. You can +# consider this function to have this equivalent C signature: +# +# void printm(uint64_t address, char *message); +# +.type printm, @function +printm: + mv t1, a1 + +.printm_loop: + lbu t0, (t1) + beqz t0, .printm_end + + sb t0, (a0) + addi t1, t1, 1 + j .printm_loop + +.printm_end: + ret + +.section .text.bios + +# The entry point: the firmware will blindly jump here. We expect the firmware +# to pass up two arguments, the first one being the hart ID, and the other being +# a pointer to the FDT describing this machine. You can consider this function +# to have this equivalent C signature: +# +# void _start(uint64_t hart_id, void *fdt) __attribute__((noreturn)); +# +.global _start +.type _start, @function +_start: + # We expect the configuration to be given as a devicetree blob pointed by + # the second argument (a1). Hence, the configuration pointer from RISC-V + # should be set to NULL indicating that. If that's not the case, then it's + # not a supported scenario and we jump to the infinite loop gracefully. + csrr t0, mconfigptr + bnez t0, .end + + # We don't care about the hard ID, but we need the FDT pointer as an + # argument for the `get_serial_address` function. + mv a0, a1 + call get_serial_address + + # If the return value of `get_serial_address` is a NULL value, then + # something went wrong there and we just have to end it here. + beqz a0, .end + + # And now we can print our message. + la a1, .msg1 + call printm + + # Infinite loop so not to crash :) +.end: + j .end + +.section .rodata +.msg1: + .string "Hello, world!\n" +.serial: + .string "serial@" diff --git a/arch/riscv/usr/basics.c b/arch/riscv/usr/basics.c index 4fa7df3..9194565 100644 --- a/arch/riscv/usr/basics.c +++ b/arch/riscv/usr/basics.c @@ -28,12 +28,12 @@ void test_factorial(void) * * Implemented in string.S */ -char * reverse_string(char *str); +char *reverse_string(char *str); void test_reverse_string(void) { assert(reverse_string(NULL) == NULL); - assert(reverse_string("") == ""); + assert(strlen(reverse_string("")) == 0); char s1[] = "This is a string."; assert(strcmp(reverse_string(s1), ".gnirts a si sihT") == 0); @@ -75,13 +75,13 @@ bool greater_than_ten(double a, uint64_t b); void test_greater_than_ten(void) { assert(!greater_than_ten(2.3, 1)); // 4.3 - assert(greater_than_ten(2.3, 2)); // 10.6 + assert(greater_than_ten(2.3, 2)); // 10.6 printf("greater_than_ten:\tOK\n"); } /* - * Atomically add the integer pointed by `a` with the given `value`. Although + * Atomically add the integer pointed by `a` with the given `value`. * this is a function, I've checked that the assembly produced by GCC on a * decent optimization level actually inlines this. */ @@ -111,11 +111,11 @@ static inline void atomic_add(uint64_t *a, uint64_t value) * * See: https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html. */ - asm volatile(".Latomic_add_retry:\n\t" + asm volatile(".Latomic_add_retry:\n\t" "amoadd.d t0, %1, %0\n\t" "beqz t0, .Latomic_add_retry" - : "+A" (*a) - : "r" (value) + : "+A"(*a) + : "r"(value) : "memory"); } |
