RISC-V & Rust 1: Introduction and blinking LED

Table of contents

1. Introduction

RISC-V is a relatively new kid on the block of RISC-based ISAs, but also quite bright one. The project began in 2010 and the first specification was published in 2011, but first widely available implementations started to emerge only around 2020.

RISC-V absorbed a lot of good ideas from pre existing RISC ISAs and at the same time managed to keep things simple and orthogonal, cleaning up a lot of warts of previous RISC generations. But probably the coolest aspect of RISC-V is that it is an open standard, which means that you can start building RISC-V CPUs in your garage without paying any royalties to big corporations like you would have to in case of ARM.

This article series is going to be focused on practical tinkering with RISC-V and Rust, rather than providing a deep dive into the ISA. If you are not already familiar with it, here's a good compilation of RISC-V learning materials: github.com/riscv/learn.

I am not entirely sure what will come out of this project, but in the long term, I am thinking of building a tiny microkernel OS capable of running on MangoPI MQ-Pro board (and maybe in QEMU), complete with a basic networking stack and some primitive graphics capabilities. But that is if I ever find enough time for this.

2. Target hardware

We will be experimenting with a MangoPi MQ-Pro SBC. It is a small $30 board in the Raspberry PI Zero W form factor based on a single core Allwinner D1 CPU. Despite its size, it has enough features to be interesting, namely:

MangoPi MQ-Pro MangoPi MQ-Pro front view

The board is based on the Allwinner D1-H SoC, which was among the first widely available budget RISC-V CPUs on the market.

Here is the SoC description from the user manual:

D1-H is an advanced application processor designed for RISC-V Multi-Media decoding platform. It integrates a 64-bit XuanTie C906 RISC-V CPU and a HiFi4 DSP to provide the high-efficient computing power. D1-H supports full format decoding such as H.265, H.264, MPEG-1/2/4, JPEG, VC1, and so on. The independent encoder can encode in JPEG or MJPEG. Integrated multi ADCs/DACs and I2S/PCM/DMIC/OWA audio interfaces can work seamlessly with the CPU to accelerate multimedia algorithms and improve the user experience. D1-H supports RGB/LVDS/MIPI DSI/HDMI/CVBS OUT display output interfaces to meet the requirements of the different screen display. D1-H comes with extensive connectivity and interfaces, such as USB, SDIO, EMAC, TWI, UART, SPI, PWM, GPADC, LRADC, TPADC, IR TX&RX, and so on. Besides, D1-H can connect with other different peripherals like WiFi and BT via SDIO and UART.

Allwinner D1 SoC block diagram Allwinner D1-H SoC block diagram

The SoC integrates a single 64-bit XuanTie C906 RISC-V core, which is an open source RISC-V core design developed by Alibaba Group.

Core features from user manual:

XuanTie C906 core structure XuanTie C906 core structure

RV64IMAFDC instruction set means that we have a 64 bit (XLEN=64) RISC-V core with the following standard extensions:

Besides standard extensions, the core also implements custom instruction set extension called T-Head, which contains many additional instructions ranging from cache manipulation to half-precision floating point operations.

All in all, we will need the following reference documents to keep close by:

Software projects we will be using as a reference:

3. Blinking LED

Now that we have a basic grasp of the hardware, let's get to practice and write some code.

The board has a single user-controlled blue LED (DS2 in the schematics) connected to PD18 GPIO. Driving GPIO is trivial, so let's get straight to code.

_payload:
.equ GPIO_BASE, 0x02000000
.equ GPIO_PD_CFG2, 0x98
.equ GPIO_PD_DAT, 0xA0

/* Configure PD18 as output: GPIO_PD_CFG2[11:8] = 0b0001 */
li a0, GPIO_BASE + GPIO_PD_CFG2
lw a1, 0(a0)
li a2, 0xfffff0ff
and a1, a1, a2
ori a1, a1, 0x100
sw a1, 0(a0)

/* Invert PD18 state: ~GPIO_PD_DAT[18] */
li a0, GPIO_BASE + GPIO_PD_DAT
lw a1, 0(a0)
li a2, 1<<18

_blink:
xor a1, a1, a2
sw a1, 0(a0)

/* Sleep for 500ms and jump back */
li a3, 0
li a4,  0x40d9000
_delay:
addi a3, a3, 1
beq a3, a4, _blink
j _delay

_hang:
j _hang

Some notes on the code:

4. Building and running

Now, its not enough to just compile the code above and flash it to the SD card, as CPU BROM (boot ROM code) requires that specific header is present before the actual executable payload.

The only description of this header I found is in the comments of the original bootloader mentioned above. Here it is:

.text

.global _start

j _payload        /* jump over the metadata below to the actual payload */
.ascii "eGON.BT0" /* header marker (magic) */
.word 0x5f0a6c39  /* checksum initial value */
.word 0x00000000  /* payload size */
.word _payload - _start /* header size */
.word 0 /* public header size (we don't need one) */
.word 0 /* public header version */
.word 0 /* return address (dont care about this one) */
.word 0x20000 /* run address of the payload (SRAM A1) */
.word 0 /* boot cpu / eGON version (don't care) */
.dword 0 /* platform information (don't care) */

_payload:
/* ... our blinking LED program goes here ... */

The meaning of most fields should be clear from the comments, but some require further elaboration:

Finally, we are ready to compile the code above and flash it to the SD card. As for compiling, we will only need as and objcopy from the GNU RISC-V toolchain for now, as we're not compiling any Rust code yet. On Archlinux and Majaro, the toolchain can be easily installed with pacman -S riscv64-elf-binutils riscv64-elf-gcc.

For a program this simple, we also won't even need a linker script, so the only piece that's left is a convenience Makefile listed below.

FLASH_DEV?=/dev/sda

boot.img: boot.elf
	riscv64-elf-objcopy -O binary boot.elf boot.bin
	scripts/gencksum boot.bin boot.img
	rm -f boot.bin

boot.elf: boot.S
	riscv64-elf-as boot.S -o boot.elf

clean:
	rm -f boot.elf boot.img boot.img.S boot.elf.S

flash: boot.img
	[ -b $(FLASH_DEV) ]
	sudo dd if=boot.img of=$(FLASH_DEV) bs=8192 seek=1
	sync

boot.img.S: boot.img
	riscv64-elf-objdump -m riscv:rv64 -b binary -D boot.img > boot.img.S

boot.elf.S: boot.elf
	riscv64-elf-objdump -d boot.elf > boot.img.S

.PHONY: clean

A couple of notes on the makefile:

All we have to do now is to build and flash the image with make flash FLASH_DEV=/dev/sdcard command, insert the SD card into the SBC and behold the results of our hard work.

5. What's next?

That's all for now! Next time we will be initializing the UART and finally running some Rust code.

Source code for the article is available on GitHub at: github.com/alexeyden/os5/tree/p1-blinking-led

Top