aboutsummaryrefslogtreecommitdiff
path: root/arch/riscv
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2024-09-03 11:53:15 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2024-12-05 16:02:49 +0100
commit4f69fb67ee229c7b59ced5db64142b527d74ce7b (patch)
treed9aab519f0caa6086368c297d9849bbf7e4589b0 /arch/riscv
parente94cc18273810ce9cc44d80608042c16eae4e6e4 (diff)
downloadfarga-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/riscv')
-rw-r--r--arch/riscv/bios/Makefile25
-rw-r--r--arch/riscv/bios/config.ld9
-rw-r--r--arch/riscv/bios/hello.s23
-rw-r--r--arch/riscv/machine/Makefile33
-rw-r--r--arch/riscv/machine/README.md117
-rw-r--r--arch/riscv/machine/config.ld10
-rw-r--r--arch/riscv/machine/gdb.txt3
-rw-r--r--arch/riscv/machine/machine.S125
-rw-r--r--arch/riscv/usr/basics.c14
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");
}