RISC-V Assembly Code Examples
By: Erik Engheim
Tags: Assembly, Beginners Guide, Programming, Risc V

RISC-V Assembly Code Examples

A lot of RISC-V example code jumps straight into fairly complex code examples. I intend to start slow do some very simple examples.

For a proper introduction to things such as registers, conditional branching and assembly code format, I advice reading: RISC-V Assembly for Beginners.

That is an overview story, but does not contain that many examples. As with the first article we will use the online Cornell University RISC-V interpreter.

When working through the code examples, you could print out this RISC-V reference sheet to help you: James Zhu’s RISC-V Reference.

The most important instructions are probably:

ADDI rd, zero, immediate , to load a value ( immediate ) into register rd .

, to load a value ( ) into register . LW rd, offset(rs1) to load content at memory location offset + rs1 into register rd .

to load content at memory location into register . SW rd, offset(rs1) store content of rd register at memory location offset + rs1 .

store content of register at memory location . ADD rd, rs1, rs2 to content of two registers and store value in destination register rd .

to content of two registers and store value in destination register . The branch instructions BEQ , BNE , BLT . Read previous story for more details.

Input Doubler

Let us begin with a really simple example. You are writing a function which doubles the its input. If you read my previous intro: RISC-V Assembly for Beginners.

You should know that on RISC-V functions get their inputs in registers a0 , a1 , up to a7 . These are just aliases for registers starting at x10 .

Jot down a solution before looking at my solution below:

doubler:

ADD a0, a0, a0

To test this put an initial value in register a0 before running the code in the simulator. If it works, then a0 should be doubled. Functions should return their result in a0 so this is correct way of doing it.

If this was real function which got called from somewhere else, we would need this line at the end to return from function:

JALR zero, ra, 0

Usually RISC-V assembler will have a pseudo instruction RET for this.

How does this instruction work? In a real program our doubler function called with 42 as an argument would have to be written like this:

ADDI a0, zero, 42

JAL ra, doubler

SUB t3, t4, t2

That means the return address gets stored in the ra ( x1 ) register, so when doubler returns it begins execution of the SUB t3, t4, t2 instruction. What does that mean? Just an arbitrary instruction I put there.

Times Eight

Ok here is a simple follow up in case the previous one was not obvious. This time I want you to multiple the input of input by 8. Here is the challenge. There is no multiply instruction is the base RISC-V ISA, and Interpreter we are using don’t have any of the RISC-V M extension which support division and multiplication.

See if you can figure out how to do this anyway before looking at two of my solutions.

Keep in mind if you want to change input to a0 you have to stop program. After you made change to initial value, you need to hit the blue Reset button before you run the code.

Using Addition

Conceptually multiplication is just repeated additions, so this may be the most obvious solution:

eight_times:

ADD a0, a0, a0

ADD a0, a0, a0

ADD a0, a0, a0

HLT # Stop execution

Using Logical Shifts

In a binary number system shifting all the digits one position to the left is the same as multiplying by two. Shifting two positions is like multiplying by four.

010b = 2

010b << 1 = 100b = 4

001b << 2 = 100b

In RISC-V assembly we perform left shifts with SLLI and SLL . The I suffix indicates we are using an immediate value instead of a register to specify how many positions to shift.

eight_times:

SLLI a0, a0, 3

JALR zero, ra, 0

HTL # Stop excution. Normally you put RET

Find Max Value

This is an implementation of the common function c = max(a, b) which assigns to c the value of a or b which is larger.

To solve this problem you need to use the jump and branch instructions. A tip on how to move values from one register to another. You can use the ADD or ADDI instructions for this purpose.

Normal RISC-V assembly code has a pseudo instruction called MV , which we don't have access to here. These two lines would be equivalent:

MV a4, a3

ADDI a4, a3, 0

Okay try to solve this before looking at solution below:

max:

BLT a0, a1, second # if a0 < a1 then a1 is larger

JAL zero, done

second:

ADD a0, zero, a1 # make a1 the return value

done:

HLT # normally a RET would be here

If you didn’t get this right. You could have a go at implementing a min function.

Simple Multiplier

Instead of multiplying with a fixed number let us multiply two arbitrary numbers. In this case you can use just additions and branches. It does not need to be efficient. The function takes two arguments in a0 and a1 to be multiplied and returns result in a0 as usual.

multiply:

ADD t0, zero, zero

ADDI a1, a1, -1

accumulate:

ADD t0, t0, a0

ADDI a1, a1, -1

BGE a1, zero, accumulate

ADD a0, zero, t0

HLT

This is slightly longer an less readable than it needs to be because our interpreter lacks a lot of RISC-V instructions. If I was not running this in the simulator and could use all instructions, including the pseudo instructions, I would have written this as:

multiply:

LI t0, 0 # set t0 to 0

accumulate:

ADD t0, t0, a0

ADDI a1, a1, -1 # decrement a1

BGT a1, zero, accumulate

MV a0, t0 # copy t0 value to a0

RET # return to calling function

Fast Multiplication

When multiplication was implemented in software the method we used above was not used. It would have been far too slow. Instead a combination of additions and shifts was used.

This is your next challenge attempt to make a more efficient multiplication algorithm. It should be possible to perform 42 × 20 with only two addition, rather than twenty. You can use left shift SLL or SLLI as well as right shifts SRA and SRAI .

fast_multiply:

ADD t0, zero, zero # to keep track of result



next_digit:

ANDI t1, a1, 1 # is rightmost bit 1?

SRAI a1, a1, 1



BEQ t1, zero, skip # if right most bit 0, don't add

ADD t0, t0, a0

skip:

SLLI a0, a0, 1 # double first argument

BNE a1, zero, next_digit

ADD a0, zero, t0 # move accum result to a0

HLT

This solution while it works can be hard to follow, so let us first write a version if all instructions are available to us:

fast_multiply:

LI t0, 0



next_digit:

ANDI t1, a1, 1 # is rightmost bit 1?

SRAI a1, a1, 1



BEQ t1, zero, skip # if right most bit 0, don't add

ADD t0, t0, a0

skip:

SLLI a0, a0, 1 # double first argument

BNE a1, zero, next_digit

MV a0, t0

RET

Although this Julia code which almost reads like pseudo code may clarify to some degree what is happening.

function multiply(input, counter)

accumulator = 0

while counter > 0

if counter & 1 == 1

accumulator += input

end

counter >>= 1

input <<= 1

end

return accumulator

end

It can be hard to wrap your head around immediately because thinking binary is alien to us. But if you think of this in decimal terms it could be easier. Say you multiply 32 with 432. You deal with each digit in 432 separately. First you multiply 32 with 2. Next you multiply 32 with 30. Finally you multiply 32 with 400.

But we can change the logic of this slightly. The line counter >>= 1 is chopping off one digit at a time at the right most end. That means getting first 2, then 3 and finally 4. Since we get single digits, we fix the multiplication by always making the input 32 ten times bigger. So first we multiply 32 with 2. Next we multiply 320 with 3, and finally we multiply 3200 with 4.

Making the input bigger on each iteration is what input <<= 1 does. Except we are not making it 10x bigger but 2x bigger because we are dealing with binary numbers.

Create List of Square Numbers

Time to actually use our functions as real functions. We are going to use what we learned earlier to call multiply repeatedly to create a list of square numbers such as 4, 9, 16, 25 in memory. We will need a loop to repeat this.

Until I have a more complete description you could try to fix the program below to make it work as intended and write square numbers at consecutive memory addresses.

ADD s1, zero, a0

ADDI s0, zero, 1

loop:

ADDI s0, zero, 1

ADD a0, zero, s0

ADD a1, zero, a0

JAL ra, fast_multiply

SLLI t0, s0, 1

SW a0, 0(t0)

BLT s0, s1, loop

HLT fast_multiply:

ADD t0, zero, zero



next_digit:

ANDI t1, a1, 1

SRAI a1, a1, 1



BEQ t1, zero, skip

ADD t0, t0, a0

skip:

SLLI a0, a0, 1

BNE a1, zero, next_digit

ADD a0, zero, t0

JALR zero, ra, 0

Final Remarks

This will be a work in progress. I will keep adding small programs and challenges here as I come up with new ideas. Many of these programming challenges are adapted from my Calcutron-33 examples: How Does a Modern Microprocessor Work?