ECAD and Architecture Practical Classes

Assembler programming guide

Some tips on assembly language programming for RISC-V...


Rather than local variables, we have registers to store temporary values and pass data to and from functions. The RISC-V calling convention dictates whether each register must be preserved by the caller or the callee, and which have special purposes. In particular, it specifies which registers are used for function arguments and return values.

To preserve data we can use the stack. This just involves decrementing the stack pointer `sp` appropriately and then storing data to the location that `sp` points to. Since you will only be writing simple assembly functions this may not be needed.


The RISC-V Green Card has a full listing (note that our architecture is RV32I). Here are the instructions you will likely find most useful:

    beq  rs1, rs2, label    # branch if rs1 == rs2
                            # similarly: bne (!=), blt (<), bge (>=)

    add  rd, rs1, rs2       # rd := rs1 + rs2
    sub  rd, rs1, rs2       # rd := rs1 - rs2
                            # similarly: xor, or, and

    sll  rd, rs1, rs2       # rd := rs1 << rs2
    srl  rd, rs1, rs2       # rd := rs1 >> rs2
    sra  rd, rs1, rs2       # rd := rs1 >>> rs2 (arithmetic shift sign-extends the top bits of rs1)

    slt  rd, rs1, rs2       # if rs1 < rs2 then rd := 1 else rd := 0

    addi rd, rs, immediate  # rd := rs1 + immediate. The immediate must be at most 12-bits.
                            # similarly xori, ori, andi, slli, srli, srai, slti

    sw   rs2, offset(rs1)   # *(rs1 + offset) := rs2. Store the value in rs2 to the address (rs1 + offset).
    lw   rd, offset(rs1)    # rd := *(rs1 + offset). Load the value at address (rs1 + offset) into rd.

The assembler also provides a variety of pseudo-instructions, which correspond to one or two more complicated instructions but make life easier for the pogrammer. You may find the following useful:

    li   rd, immediate      # rd := immediate
    mv   rd, rs             # rd := rs

    bgt  rs1, rs2, label    # branch to label if rs1 > rs2
    ble  rs1, rs2, label    # branch to label if rs1 <= rs2
    beqz rs, label          # branch to label if rs1 == 0
                            # similarly: bnez, blez, bgez, bltz, bgtz

    j    label              # unconditional jump to label
    call label              # call subroutine (jump to label and store the return address into register ra)
    ret                     # return from subroutine (jump to the address in register ra)

We have also provided some special functionality for debugging:

    ecall                   # stops the simulator when testing in ModelSim
    csrw  0x7B2, rs         # output a debug message in the trace containing the value of rs

Control flow

In high level languages `goto` is to be avoided, but in assembler that is the only control flow mechanism. This means we must translate for-loops and if-else statements into linear versions using branches instead.

For instance, a conditional statement in C such as:

    if (i > 42) {
        // true path
    } else {
        // false path

Might become in assembly:

        li   t1, 42         # initialize state for conditiom
        ble  t0, t1, else   # if condition not met, go to false path
        ...                 # true path
        j    end
        ...                 # false path

And a simple for loop in C:

    for (int i = 0; i < 10; i++) {
        // something terribly clever

Becomes in assembly:

        li   t0, 0          # initialize the loop variable
        li   t1, 10         # initialize the condition variable
        bge  t0, t1, end    # if the loop condition is not met, exit
        ...                 # something terribly clever
        addi t1, t1, 1      # increment the loop variable
        j    loop           # loop

In many cases the number of branches required can be reduced by updating the loop variable at the start of the loop or checking the condition at the end. Extra care must be taken when there are `break` statements within the loop to ensure that the loop variable is always updated.

Do and while loops are implemented similarly.