This deep dive into the upstream RISC-V boot chain was presented as a poster at the RISC-V Summit Europe 2026 in Bologna, Italy.
While porting Freedesktop SDK to the EBC7700 for my FOSDEM'26 talk, I encountered some UEFI boot issues, which motivated me to dig deeper and uncover all the mysteries about the RISC-V boot chain. We start by introducing a few important concepts/tools.
RISC-V Toolchain
First, you may need a toolchain to actually be able to compile any of the binaries involved in the boot chain. And if you are, just like me, still waiting for your long pre-ordered RISC-V laptop/workstation, and therefore still using an x86_64 machine as your daily driver, the easiest is to download a pre-built cross-toolchain. Throughout this tutorial, I used the nightly 2026.05.19 releases:
- riscv64-glibc-ubuntu-24.04-gcc.tar.xz
- riscv64-glibc-ubuntu-24.04-llvm.tar.xz
The setup for gcc:
1 2 3 4 5 | |
Or alternatively for llvm, which is my preferred option nowadays:
1 2 3 4 5 6 7 8 | |
Note: The -@ flag instructs the device tree compiler to generate symbol tables, which are mainly used should you ever play with device tree overlays.
Privilege Levels
Second, I want to briefly mention an important low-level RISC-V hardware concept called privilege levels, which define the processor's access rights to memory, I/O devices, and instructions. Understanding the basic concept of privilege levels is important for the overall understanding of this article. RISC-V knows 3 privilege levels:
- M (machine) mode:
- highest privilege
- low-level firmware (e.g. OpenSBI), system boot, and security (e.g. secure execution environments)
- handles critical system traps
- return to lower-privileged levels using mret instruction
- S (supervisor) mode:
- intermediate privilege
- used by operating system kernels to manage memory and hardware resources
- restrict direct access to machine-level configuration registers and physical memory protection (PMP)
- uses Memory Management Unit (MMU) and virtual memory
- ecall instruction to request kernel services
- return to U-mode using sret instruction
- U (user) mode:
- lowest privilege
- cannot execute privileged instructions, access physical memory directly without virtual memory translations, or directly alter core control registers
- user space executing standard applications
- prevents incorrect or malicious application code from interfering with other running programs or the operating system itself
QEMU
Third, I want to introduce the Quick Emulator (QEMU), which is a free and open source machine emulator and virtualiser. The QEMU RISC-V emulation with its (riscv64) virt machine is the perfect playground for us to gain a deeper understanding of the RISC-V boot flow. It uses a hard-coded reset vector, and by default, loads the built-in OpenSBI binary into system memory space, unless a custom firmware binary is specified using the -bios argument. OpenSBI executes hardware-specific initialisation (in M-mode) and passes control (in S-mode) to the -kernel or bootloader payload at 0x80000000. The virt machine automatically generates a device tree blob (DTB) which it passes to the guest, if there is no -dtb option specified.
For QEMU I relied on the latest version shipped with my distro, rather than also compiling it from scratch:
1 2 | |
Boot Flow Overview
With those three things introduced, let us now look at the RISC-V boot flow:

Boot ROM aka ZSBL (Zero Stage Bootloader)
The ZSBL is what a RISC-V chip, or its emulated derivative, runs first at power-up out of reset. It initialises a few registers and jumps directly to some hard-coded address (in case of QEMU's riscv64 virt machine, that is 0x80000000), where the first user-provided code runs (by default, QEMU will load OpenSBI there).
Debugging
Let us use QEMU's inbuilt gdb server (debugger) to have a look at what exactly ZSBL does in the QEMU case.
In a first terminal, we launch QEMU with the -S argument, which instructs it to just wait for a gdb debugger session to connect to its gdb server and further specify what to do next:
1 | |
And in a second terminal, we launch gdb-multiarch (again, just using the distro-provided one), instruct it to disassemble the next line and connect to the above-launched QEMU. Note that it will automatically detect the riscv:rv64 architecture. We can use si a few times to step through ZSBL's instructions one by one, until we observe the final jump to OpenSBI at 0x80000000:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
So QEMU's ZSBL execution starts at address 0x1000, and there are only 6 instructions executed, but what exactly are they doing? Turns out in the case of QEMU's virt machine, this is the riscv_setup_rom_reset_vec() function in hw/riscv/boot.c which is populated as part of its own bring up before booting the guest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
So the 6 instructions are literally hard-coded right there in QEMU's source code!
QEMU populates a struct fw_dynamic_info with the information required to boot the next stage and passes its address in the a2 register of the RISC-V CPU. The address must be aligned to 8 bytes on RV64 and 4 bytes on RV32. The struct fw_dynamic_info is defined in include/hw/riscv/boot_opensbi.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
The next_mode is set to S to select a CPU that supports S-mode.
QEMU also passes a unique per-hart ID in the a0 register. We skipped the discussion of harts here as it is not relevant for the discussion of the boot chain.
The fdt_load_addr, pointing to the flattened device tree, is passed via the a1 register.
Then it jumps to address 0x80000000 to execute the next element within the boot chain.
OpenSBI
SBI stands for Supervisor Binary Interface, which is the system call style calling convention between a Supervisor (e.g. S-mode OS) and Supervisor Execution Environment (SEE, e.g. the M-mode runtime firmware). SBI calls help to reduce duplicate platform code across OSes, provide common drivers for an OS which can be shared by multiple platforms and an interface for direct access to hardware resources (M-mode only resources). The specification is maintained by the Platform Runtime Services (PRS) Task Group (TG), which ratified the SBI specification v1.0 in 2022.
OpenSBI (Open Source Supervisor Binary Interface) is an open source implementation of that RISC-V SBI specification.
In OpenSBI, the FW_DYNAMIC approach dynamically obtains the runtime configuration and information about the next boot stage (e.g. U-Boot proper) from any previous boot stage via passing a pointer to a struct fw_dynamic_info structure in a CPU register, rather than using hard-coded compile-time values.
The assembly bootstrapping phase does early reset and sanity checks by validating its environment and setting up basic mtvec trap vectors. Subsequently, the dynamic information from fw_dynamic_info gets parsed, stack allocated, scratch space initialised (mscratch), and the C-runtime jumped into.
The core initialisation and platform setup phase (sbi_init) does early console setup, configuring the primitive serial UART block for debug log output. The platform hardware init (platform_init) invokes platform-specific functions, initialising critical hardware blocks. The physical memory protection (PMP) registers are provisioned, shielding M-mode memory from unauthorised S-mode accesses. The interrupt controllers, like core timers, inter-processor interrupts (IPIs), and peripheral controllers like the PLIC/APLIC get initialised.
The payload transition phase (sbi_expected_trap to S-mode) extracts the jump target from struct fw_dynamic_info, modifies the mstatus/misa registers, configuring the previous privilege level to S-mode, programs the target next_addr value directly into the machine exception program counter (mepc), restores the rest of the original dynamic information (e.g. FDT address) passed via CPU registers, and executes the mret instruction, which drops the processor privilege down to S-mode and jumps directly to the address staged inside mepc, passing system execution to U-Boot proper.
Rather than using QEMU's built-in OpenSBI, we can build our own for later debugging:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Note: The latest master branch did not build for me with llvm, which is why I picked released v1.8.1 instead, which seems to work fine.
The resulting binary may be launched by QEMU (remember, without the -bios argument, it would just launch the built-in/default OpenSBI):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | |
Debugging
We can launch QEMU in the first terminal, but this time, in addition to instructing it with the -bios argument to load our self-built OpenSBI, again with the -S argument, which instructs it to just wait for the gdb debugger to connect to it and further specify what to do next:
1 2 | |
In the second terminal, we start gdb, giving it the OpenSBI symbol file and instructing it to break at sbi_boot_print_banner. Then we can let it print the banner line by line:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
U-Boot
Das U-Boot (Universal Boot Loader) is the de facto open source bootloader used in embedded devices. Its main job is to initialise hardware, load the operating system (usually Linux) into memory, and boot. It supports multi-stage booting, often split into an SPL (Secondary Program Loader) that sets up basic RAM, and full U-Boot proper that takes over to load the next stage. It features an interactive command line interface (CLI) usually accessible via serial port UART used to interrupt the boot sequence, poke/test hardware, and configure various aspects of the system boot. As part of its hardware management, it allows loading files from file systems on various (boot) devices like NAND/NOR flashes, (e)MMC/SD cards/chips, NVMe drives, SD cards, UFS, or USB mass storage. But it also features a built-in network stack for downloading images over Ethernet using e.g. TFTP, which is very convenient for debugging/development. Last but not least, it does the OS hand-off, including preparing a device tree and loading the Linux kernel image into RAM before transferring execution. Alternatively, as we will see later on, it may also hand off to a next-stage UEFI boot manager.
The first phase of U-Boot's SPL consists of M-mode assembly entry. The hardware reset vector gets directly set to the entry point _start in arch/riscv/cpu/start.S. The hardware setup includes configuring fundamental control and status registers (CSRs), like disabling interrupts (mstatus), clearing delegation registers (medeleg/mideleg), and setting the trap vector base address (mtvec) to catch early faults. The hart ID is extracted by querying via the csrr a0, mhartid instruction. The primary executing core (boot "hart") continues initialisation, while auxiliary harts are placed in a secondary wait loop if symmetric multiprocessing (CONFIG_SPL_SMP) is enabled, as is the case on QEMU. An early runtime C stack is prepared in internal SRAM or cache-as-RAM (L2 cache). The gp (Global Pointer) register is set up to reserve space for the early global data structure pointer (gd).
The second phase consists of the pre-RAM initialisation (board_init_f), which now runs as a C runtime within arch/riscv/lib/spl.c. The spl_early_init() sets up a basic driver model (DM) tree structure to find essential SoC components configured via device tree, either passed by QEMU or baked into U-Boot SPL. The riscv_cpu_setup() issues early CPU core tweaks and configures the physical memory protection (PMP) boundaries. The preloader_console_init() prepares the system console (typically a standard 16550 UART or similar device), allowing debug log prints. And spl_board_init_f() is the board/SoC-specific implementation called to power up, configure clock phase-locked loops (PLLs), and explicitly initialise the external RAM.
The third phase consists of the post-RAM initialisation (board_init_r), which now runs within generic U-Boot SPL common/spl/spl.c. The operational stack and active global data structures get transferred out of early internal SRAM into RAM if configured to do so (CONFIG_SPL_STACK_R), however, on QEMU this is not the case. Storage media drivers and devices, such as MMC/SD cards or NAND/NOR flashes, get initialised as preparation for loading the next stage payloads, the exact source of which may be dynamically discovered.
The fourth and final phase of the SPL consists of image loading and executing the jump into OpenSBI, which does the transition from M-mode, where the SPL runs, to S-mode for U-Boot proper to accommodate an operating system like Linux. The FIT image gets parsed and may consist of U-Boot proper, OpenSBI and optionally a device tree blob (DTB). However, QEMU usually passes in its own DTB via FW_DYNAMIC mechanism. U-Boot proper gets loaded into its target execution RAM address, while OpenSBI is loaded into its respective baseline location. The instruction pipeline is flushed in invalidate_icache_all(), clearing caches. Last but not least, the jump gets initiated by loading the target execution entry address to point directly to OpenSBI's entry point. The information gets passed via regular RISC-V registers: a0 receives the boot hart ID (gd->arch.boot_hart), a1 the memory pointer referencing the DTB, and a2 the memory pointer referencing the struct fw_dynamic_info.
Once OpenSBI completes its M-mode initialisation, it transitions to S-mode, and ultimately branches to the pre-loaded U-Boot proper entry address in system RAM to continue the full boot sequence.
The first phase of U-Boot proper consists of S-mode assembly entry (start.S), where the incoming information from OpenSBI is parsed, the S-mode trap vector base address register (stvec) is set up to catch early faults, the S-mode interrupt enable flags (sie) are cleared to ensure sequential, deterministic initialisation, and the global pointer registers, thread pointer (tp) and global pointer (gp), get loaded.
The second phase consists of pre-relocation C initialisation (board_init_f), where a sequential array of early setup functions get executed, the driver model (DM) gets activated in dm_init_f from device tree information for early core devices (clocks, reset controllers, and pin multiplexers), the memory layout is calculated reserving space at the top of memory to store the relocated U-Boot binary, the master page tables, the permanent stack, and the new heap.
The third phase consists of the binary relocation relocate.S, where U-Boot migrates itself out of the way towards the top of memory leaving low memory addresses available for further boot stages, the .rela.dyn relocation tables are iterated updating all address offsets to point to the newly relocated ones, the RISC-V fence instruction fence.i gets called to flush any stale instructions out of the CPU pipeline execution cache.
The fourth phase consists of the post-relocation C initialisation (board_init_r), where the full suite of drivers as declared in the device tree gets bound to, the environment gets loaded from non-volatile storage with configuration scripts and parameters (e.g. bootargs, bootcmd), and further subsystems like Ethernet, PCIe/NVMe, and USB get dynamically probed.
The fifth and final phase consists of the main loop and OS handover, where user interaction may be established or the default boot initiated after a certain countdown time. The interactive shell features command parsing in a cli_loop, where user debugging commands are evaluated. The OS handover gets initiated by a booting call like booti, bootm or bootefi, where the system gets prepared to transfer control to the next S-mode stage, first disabling all active hardware peripheral timers, flushing caches, and triggering the next entry point. Just like in previous stages, information gets passed using the FW_DYNAMIC mechanism via CPU registers.
We get the U-Boot source code:
1 2 | |
However, if we do like the OpenSBI banner to be printed, we need to change its configuration as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Guess how/where I found out about this OpenSBI scratch-options handling? Yes, the attentive reader noticed it during our previous debugging session (the terminal box just above the U-Boot section) at line 32, source code line 51!
Only now we configure/build U-Boot for SPL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
This actually builds the SPL binary spl/u-boot-spl.bin, as well as the U-Boot proper binary u-boot.bin packaged together with the OpenSBI binary fw_dynamic.bin, into a single flattened image tree (FIT) itb.fit.fit blob. Its former name was u-boot.itb, where ITB meant image tree blob. As I am more familiar with that former name, I will stick to u-boot.itb throughout this article. Such .fit or .itb files may be inspected using U-Boot's mkimage tool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
The interested reader may find much more information in the Flattened Image Tree Specification.
But, now, let us actually launch this by QEMU, which is slightly more complex, as not only a -bios argument, but also a loader device with the additional parts needs to be specified:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
Debugging
Now, it starts to get interesting. We can launch QEMU again in the first terminal like before, but again with the -S argument, which instructs it to just wait for the gdb debugger to connect to it and further specify what to do next:
1 2 | |
In the second terminal, we start gdb, giving it not only the OpenSBI symbol file as before, but also U-Boot's SPL and U-Boot proper symbol files as well. We instruct it to break at board_init_f and spl_invoke_opensbi in the U-Boot SPL, sbi_boot_print_banner and sbi_hart_switch_mode in OpenSBI and display_options in U-Boot proper. Then we can let it print the various banners line by line:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
The preloader_console_init in U-Boot SPL is what prints its banner and spl_invoke_opensbi is where it hands over execution to OpenSBI. The sbi_boot_print_banner as we learned already, prints OpenSBI's banner and sbi_hart_switch_mode does the mret instruction handing over execution to U-Boot proper. The display_options in U-Boot proper finally prints its banner.
The observant reader may have noticed that we instructed gdb to load U-Boot proper's symbols twice. This has to do with the fact that U-Boot proper actually re-locates itself during execution towards the end of RAM leaving as much RAM as possible free to load further artifacts to be booted. To find the relocation address one can use U-Boot's bdinfo command:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
UEFI Boot Manager
Systemd-boot (sd-boot for short, previously known as gummiboot) is a simple, easy-to-configure UEFI (Unified Extensible Firmware Interface) boot manager. It provides a textual menu to select the entry to boot and an editor for the kernel command line. Its source code is located primarily within src/boot/ (previously src/boot/efi/) within systemd.
The first phase consists of the entry point handover, where control gets transferred to the primary UEFI payload with efi_main() in src/boot/efi/boot.c being the initial code hook. The UEFI firmware, in our case U-Boot proper, passes standard arguments providing access to core console interfaces, system tables, and basic file system drivers.
The second phase consists of execution environment sanity checks, where the configuration is validated, a watchdog is initialised to safely reset the platform if a hard freeze occurs during image parsing, and optional secure boot checks are invoked, including random seed provisioning. Note that secure boot is outside the scope of this article.
The third phase consists of driver and file system probing, where file-access routines get initialised to locate available OS targets. The EFI system partition (ESP) where the binary was run from gets probed, and supplementary storage or file system drivers are loaded by iterating over /EFI/systemd/drivers/. It dynamically handles and loads any such compiled specifically with the architecture suffix riscv64.efi.
The fourth phase consists of config processing and menu presentation, where /loader/loader.conf gets parsed to handle general preferences, /loader/entries/*.conf gets scanned for target detection, searching inside /EFI/Linux/ looking for Unified Kernel Images (UKIs), and drawing the interactive boot menu, usually automatically booting the default target after a timeout occurred.
The fifth and final phase consists of executing the Kernel via EFI stub, where systemd-boot hands execution directly off to a kernel image after selection. It calls the EFI_BOOT_SERVICES.StartImage() execution standard, as the Linux kernel acts like a second UEFI application. Hardware information gets passed through via flattened device tree (FDT) or ACPI tables. Note that ACPI is outside the scope of this article.
To build systemd to include systemd-boot, make sure to have python3-pyelftools available and proceed as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Note: The latest main branch did not work for me, which is why I picked released v260.2 instead, which seems to work fine.
Testing/debugging sd-boot is slightly more involved, as one can't just easily deploy it on its own, plus what would be the point, as we want it to subsequently boot a full Linux system. So we need to build one for RISC-V first. We can do this using Freedesktop SDK:
1 2 3 4 5 6 | |
We can now replace sd-boot inside the EFI partition of that disk.img with our previously built one:
1 2 3 4 5 | |
With that in place, we can now launch it in QEMU:
1 2 3 4 | |
Note: Without setting the -m 256M argument to QEMU, it will fail to load with a Unhandled exception: Load access fault.
Once we are on the U-Boot command line shell, we can poke it some more to find out about what it loads next:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
And once we are fully booted, we can look at the partition layout and where sd-boot's binary and configuration got deployed to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Linux Kernel
The Linux kernel needs to be built as an EFI executable (CONFIG_EFI_STUB) for the EFI boot manager to be able to execute it:
1 2 3 4 5 6 7 | |
Note that the Linux kernel EFI stub exits the boot services by executing ExitBootServices(), terminating systemd-boot's operations and shifting control into the Linux S-mode kernel environment.
Conclusion
The RISC-V boot process is well understood, and we are ready to tackle applying our knowledge to any RISC-V system out there. Looking forward to getting our hands on the first RVA23 boards, making sure they have their upstream boot chains ready.
Other Content
- Porting an Automotive Operating System to RISC-V
- Understanding Codethink's IEC 61508 Mapping for the Eclipse Trustable Software Framework
- Resisting Hyrum's Law with Private Constructors in Python
- FOSDEM 2026
- Building on STPA: How TSF and RAFIA can uncover misbehaviours in complex software integration
- Adding big‑endian support to CVA6 RISC‑V FPGA processor
- Bringing up a new distro for the CVA6 RISC‑V FPGA processor
- Externally verifying Linux deadline scheduling with reproducible embedded Rust
- Engineering Trust: Formulating Continuous Compliance for Open Source
- Why Renting Software Is a Dangerous Game
- Linux vs. QNX in Safety-Critical Systems: A Pragmatic View
- Is Rust ready for safety related applications?
- The open projects rethinking safety culture
- RISC-V Summit Europe 2025: What to Expect from Codethink
- Cyber Resilience Act (CRA): What You Need to Know
- Podcast: Embedded Insiders with John Ellis
- To boldly big-endian where no one has big-endianded before
- How Continuous Testing Helps OEMs Navigate UNECE R155/156
- Codethink’s Insights and Highlights from FOSDEM 2025
- CES 2025 Roundup: Codethink's Highlights from Las Vegas
- FOSDEM 2025: What to Expect from Codethink
- Codethink/Arm White Paper: Arm STLs at Runtime on Linux
- Speed Up Embedded Software Testing with QEMU
- Open Source Summit Europe (OSSEU) 2024
- Watch: Real-time Scheduling Fault Simulation
- Improving systemd’s integration testing infrastructure (part 2)
- Meet the Team: Laurence Urhegyi
- A new way to develop on Linux - Part II
- Shaping the future of GNOME: GUADEC 2024
- Developing a cryptographically secure bootloader for RISC-V in Rust
- Meet the Team: Philip Martin
- Improving systemd’s integration testing infrastructure (part 1)
- A new way to develop on Linux
- RISC-V Summit Europe 2024
- Safety Frontier: A Retrospective on ELISA
- Codethink sponsors Outreachy
- The Linux kernel is a CNA - so what?
- GNOME OS + systemd-sysupdate
- Codethink has achieved ISO 9001:2015 accreditation
- Outreachy internship: Improving end-to-end testing for GNOME
- Lessons learnt from building a distributed system in Rust
- FOSDEM 2024
- QAnvas and QAD: Streamlining UI Testing for Embedded Systems
- Outreachy: Supporting the open source community through mentorship programmes
- Using Git LFS and fast-import together
- Testing in a Box: Streamlining Embedded Systems Testing
- SDV Europe: What Codethink has planned
- How do Hardware Security Modules impact the automotive sector? The final blog in a three part discussion
- How do Hardware Security Modules impact the automotive sector? Part two of a three part discussion
- How do Hardware Security Modules impact the automotive sector? Part one of a three part discussion
- Automated Kernel Testing on RISC-V Hardware
- Automated end-to-end testing for Android Automotive on Hardware
- GUADEC 2023
- Embedded Open Source Summit 2023
- Full archive