Category: 101

Writing and Understanding RISC-V Assembly Code

For quite a while I have followed the RISC-V ISA with growing intereset. Now that RISC-V is becoming more and more popular and catching a lot of public attention, it is time to get my hands dirty with some low level RISC-V assembly coding.

An instruction set architecture (ISA) is a specification of the commands/instructions a specific processor or processor architecture family does understand and that it can execute. For this tutorial we will be looking at the RISC-V ISA.
Assembly code (often abbreviated asm) is textual low-level program code, which can be directly translated to machine code. We will be working with the RISC-V assembly language.
The connection between an ISA and assembly code is such that assembly language/code is used to write a program for a corresponding ISA. In theory there could be more than one assembly language for the same ISA (like there are multiple high level programming language for the same machine architecture), but this is seldom the case (having different “flavors” of the same assembly language is more common).

I think there is no need to reproduce any details about the RISC-V ISA at this point. There are plenty of good sources online, the official specification of the RISCV user-level ISA of course, especially the chapters “Instruction Set Listings” and “RISC-V Assembly Programmer’s Handbook”, or one of the RISCV reference cards.

Another good tutorial for beginners, which is a little slow paced imho, is the Western Digital RISC-V ASM tutorial on Youtube (the tutorial really gets started with part 6). For beginners it is nice to see how the base addresses for peripherals can be found using the datasheet and how to “talk to” those peripheral blocks using assembly code.

Note that all examples presented in the following are created/run on Ubuntu 18.04 (WSL) and platformio is used for convenience (no need to set up the toolchains manually). On Ubuntu platformio can be obtained with apt:

$> sudo apt install platformio

Now lets start by creating a new folder for our platformio project and initializing it.

$> mkdir riscv-asm-examples
$> cd riscv-asm-examples 
$> platformio init

I use the following platformio.ini file to define the project parameters, since I have a HiFive1 board lying around somewhere. Feel free to use any other supported RISC-V board or framework instead. If you do not own a RISC-V board you can still run the assembler and have a look at the machine code. But I really recommend to get a board, the Longan Nano costs only 5$.
The platformio.ini file I am using is very simple:

$> cat ./platformio.ini
platform = riscv
board = freedom-e300-hifive1
framework = freedom-e-sdk

To check that platformio is installed and the platformio.ini file is correct run a sanity check using an empty main function:

$> echo "int main(void) {return 13;}" > ./src/main.c
$> platformio run

Running platformio for the first time can take a while because all the required packages and toolchains will be downloaded, so do not get impatient if its takes a little while.
If everything went fine a [SUCCESS] message should show up after a while.

Okay, the main function has been compiled. To see the assembler code which was produced we can used objdump -d. The -d option stands for disassemble, which means that the given object file will be “translated” to human readable assembler code.
The object file of interest is hidden away by platformio in .pio/build/freedom-e300-hifive1/src/ (folder may vary if you used a different build framework).
To disassemble the almost empty main function from before do like so:

$> objdump -d .pio/build/freedom-e300-hifive1/src/main.o
.pio/build/freedom-e300-hifive1/src/main.o:     file format elf32-little
 objdump: can't disassemble for architecture UNKNOWN!

Uh oh! Why isn’t it working? The tutorial said that this should work. Liar!
Here is why:

$> ls -lha $(which objdump)
lrwxrwxrwx 1 root root 24 May  8  2019 /usr/bin/objdump -> x86_64-linux-gnu-objdump*

The objdump we called is for the x86 ISA, not for RISC-V. We need to call the correct objdump executable from our RISC-V toolchain. The toolchain is hidden away by platformio inside the user’s home folder under ~/.platformio.

~/.platformio/packages/toolchain-riscv/riscv64-unknown-elf/bin/objdump -d .pio/build/freedom-e300-hifive1/src/main.o
Disassembly of section .text.startup:
    0:   4535                    li      a0,13
    2:   8082                    ret

Nice. The general structure of the assembly code is:

<memory_address>: <opcode>    <instruction> [<parameter(s)>]

So the assembly code above tells us that an immediate value 13 is loaded into register a0, then we return. Register a0 is the register which holds the function return value according to the RISC-V calling convention. The value 13 was given as return value in the main function. So this is really the assembly code corresponding to our main function. Hurrah!

To make things easier I recommend to set a symbolic link to the correct objdump executable, e.g. inside the platformio project folder.

$> ln -s ~/.platformio/packages/toolchain-riscv/riscv64-unknown-elf/bin/objdump objdump
$> ./objdump --version
GNU objdump (SiFive Binutils 2.32.0-2019.08.0) 2.32

OK, time to start with some assembly coding examples. Most examples are very minimalistic and use the main function as entry point from which the assembly code is invoced. The main goal, to show that assembly code can achieve the same things as C code, is proven nontheless.

Oh and to upload an example to your board (e.g. HiFive1) run the following command inside the project root directory (folder where platformio.ini is located):

$> platformio run -t upload

On my Windows 10 machine this only worked with VisualStudioCode (with the platformio plugin intalled). WSL did not work (USB driver stuff). Using a Linux VM should not pose any problem though.
Also the HiFive1 requires a push of the red reset button to reset the board and load the new program after it was uploaded.

Example 1: Superblink

This example just reproduces the WesternDigital RISC-V assembler tutorial from Youtube. Some GPIO pins are initialized and LEDs are toggled between on and off.
Check out the code on github.

Cycling through the RGB LEDs with superblink.

Example 2: Superfade

This example is an extension to superblink and uses PWM signals to control the intensity of LED’s. Three PWM channels are used to control the intensity of the 3 RGB color LEDs. One-by-one the LEDs are ramped up to full intensity and then reverted back to 0 intensity, creating a (incomplete) rainbow pattern.
Check out the code on github.

Fading the RGB LEDs with PWM signals.

Example 3: Simple UART Echo

This example waits to receive data in the UART RX FIFO and subsequently sends the same data out to the UART TX FIFO, thus echoing back any characters that are received.
Check out the code on github.

uartecho in action.

That’s all for the moment. See you next time.



Anlogic TANG PriMER dev board

Recently I purchased a Sipeed TANG PriMER development board featuring an Anlogic EG4S20 FPGA (codenamed Eagle S20). The only reason I bought the board was to see what Anlogic FPGAs are capable of, since I had never heard of that FPGA vendor before. No need to think twice when the board costs less than 20$.

The TANG PriMER board is officially marketed as a RISC-V development board and comes with a Hummingbird E200 RISC-V softcore design preloaded into the onboard configuration flash. The Hummingbird is basically a slightly modified variation of the SiFive E2 core.
Setting up the tool chain was a bit of a hustle until I found this site which hosts both the TD IDE and the required license files. There are also some datasheets and schematics. Most of the official documentation is only available in Chinese, therefor I strongly recommend the inofficial english translation.

I have not done a lot with this board yet but verify the tool chain with a simple blink LED example. The design included a 32 bit counter which resulted in an estimated maximum frequency of 252 MHz. Not too bad. The TD IDE also comes with an IP wizard to generate IP cores, but it seems to just generate a wrapper for some primitive instantiations. It’s worth mentioning that the EG4S20 has an on-chip oscillator (250 or 266 MHz, documentation and IDE do not agree), on-chip SDRAM (64Mbit) and an 8-channel ADC (1MHz sample rate). Still need to figure out how to configure the board and which programmer can be used.

Since Anlogic FPGAs are not listed on or other distributor websites, it seems unlikely they will become widely available outside of China anytime soon.

Process Control in a Linux Shell

Working on the command line in a Linux shell can be tricky at times. A frequent source of frustration is that tasks which are trivial with a graphical desktop environment can be seemingly much more difficult to achieve on the command line. For example switching between multiple running applications only requires one or two mouse clicks on a graphical desktop, but demands more arcane knowledge to do it on the command line.

The jobs command lists the jobs (processes) running in the current shell session.

The keyboard shortcut [Ctrl+Z] does stop the current job and sends it to the background.

The fg command brings a job back to the foreground and resumes it.

The bg command resumes a job in the background.

The kill command sends a signal to a process. By default this is the SIGTERM signal to terminate the process in an orderly way.

The disown command detaches a job from the shell session. This way a command can continue running even after the shell session terminates.

Electronics 101: Standard Resistor Values

When we learn the theory behind electronic circuits we often calculate the exact value a certain resistor, capacitor or inductance should have.

However, in reality there is no 23.94 Ohms resistor available as standard off-the-shelf (SOTS) electronic part. Go and check it if you don’t believe me.

In reality only a selected range of resistor values is available. The specific resistor values are standardized in the EIA “E range” of standards, with EIA E12 and EIA E24 being the most common. This way manufacturers only need to produce and store a limited variety of different resistors and consumers can rely on the fact that resistors of different manufacturers are interchangeable.

The following graphic shows the values and color codings for the E24 series. The E12-series is obtained by skipping even row numbers. Knowing the common resistor values can help a lot when trying to decode the colored stripes on a resistor 🙂


As an example, for the E12 set of resistor values every decade of resistance values is divided into 12 equal parts. This is done in a way that each part is equally spaced on a logarithmic scale, i.e. R_i = round(10 * 10^(i/12)) for i = 1..12. So for the range from 10 Ω to 100 Ω the resistor values become 10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 100 Ω.

It works analogous for the other E-series. The higher the number of the E-series the more divisions per decade exist and thus the precision of the resistors values increases. The typical tolerances for the E12 series resistor values are 10%, for the E24 series it’s 5% and for the E96 series tolerances are down to 1%.

That’s the basic idea behind standard resistor series. In practice some deviations may occur, e.g. it is not uncommon to find resistors of the E24 series offered at 1% tolerance.