RISC V Assembler - tong-ece-cmu/Unnamed-Simulator GitHub Wiki

The Assembler is written in Python. It's made for generating binary instruction for memory file, which is then used for hardware simulation.

Table of Contents

Assembly Syntax

The Assembler accepts the Assembly code syntax shown below. unimplemented means the Assembler can't handle this instruction. I will implement them as the need arrives.

For the implemented instructions, there is a note after the hyphen '-' that will hint on what this instruction does. For the detailed information on the instruction, see RISC-V ISA documentation. There is a copy of the ISA specification in the Github repository.

Some implemented instructions has - untested in the end, it just means I haven't manually check the result of the assembler yet.

Also, this assembler support comments. All comments must start with semicolon. They must on their own line, meaning don't put comment after instruction.

Assembly Syntax
LUI     unimplemented
AUIPC   unimplemented
JAL     rd, imm         - jump to pc+imm, store old pc in rd
JALR    unimplemented
BEQ     rs1, rs2, imm   - rs1 == rs2 ? jump to pc+immediate : next
BNE     unimplemented
BLT     unimplemented
BGE     unimplemented
BLTU    unimplemented
BGEU    unimplemented
LB      unimplemented
LH      unimplemented
LW      rd, rs1, imm     - dest, base address, offset
LBU     unimplemented
LHU     unimplemented
SB      unimplemented
SH      unimplemented
SW      rs1, rs2, imm   - base address, src, offset
ADDI    rd, rs1, imm    - dest, src, immediate
SLTI    rd, rs1, imm    - dest, src, immediate - untested
SLTIU   rd, rs1, imm    - dest, src, immediate - untested
XORI    rd, rs1, imm    - dest, src, immediate - untested
ORI     rd, rs1, imm    - dest, src, immediate - untested
ANDI    rd, rs1, imm    - dest, src, immediate - untested
SLLI    rd, rs1, imm    - dest, src, immediate - untested
SRLI    rd, rs1, imm    - dest, src, immediate - untested
SRAI    rd, rs1, imm    - dest, src, immediate - untested
ADD     unimplemented
SUB     unimplemented
SLL     unimplemented
SLT     unimplemented
SLTU    unimplemented
XOR     unimplemented
SRL     unimplemented
SRA     unimplemented
OR      unimplemented
AND     unimplemented
FENCE   unimplemented   - multi-core synchronization related
ECALL   unimplemented   - Operating System Privilege related
EBREAK  unimplemented   - Operating System Privilege related

Assembly alias
NOP     ADDI x0, x0, 0

Assembly Code Example

Example 1

ADDI x1, x0, 4
SW x0, x1, 0
LW x2, x0, 0

As we know, the register zero always stores the value 0, and writing to it has no effect.

The first instruction adds register zero content with value 4, then put the result into register one.

The second instruction stores the register one content into memory. The memory address is the sum of register zero and value 0.

The third instruction loads a 32-bit word from memory and puts it into register two. The memory address is the sum of register zero and value 0.

In the end, the memory will store 4 at location 0, and the register one and two will also store 4.

Example 2

ADDI x1, x0, -4
;comment
XORI X2, x0, 2

The first instruction adds register zero content with value -4, then put the result into register one.

The second line is a comment. All comment must start with semicolon. They must on their own line, meaning don't put comment after some instructions.

The third instruction do a bitwise XOR on register zero content and value 2, then put the result into register two.

In the end, register one will store 0xFFFFFFFC, two's complement number. Register two will store value 2.

Example 3

ADDI x1, x0, 1
BEQ x1, x1, 12
ADDI x1, x1, 2
JAL x0, 8
ADDI x1, x1, 4
ADDI x1, x1, 1
NOP

This is equivalent of an if-statement in C.

If BEQ evaluates to true, jump to the fifth instruction, evaluate it and the rest. If evaluates to false, keep executing until JAL. Then jump to the sixth instruction, evaluate it and the rest.

The first instruction adds register zero content with value 1, then put the result into register one.

The second line check whether register one content is equal to register one content. If equal, we are going to add 12 in Program Counter(PC), else go to the next instruction. In this case, we are going to add 12 in PC. Each instruction is 4 bytes, this is just how our architecture is. So when we are executing BEQ, the PC is 4. Add 12 to PC give us 16, meaning we are at the fifth instruction and executing it.

The third instruction will execute if BEQ evaluate to be false. It adds the register one content with value 2 and write the result back to register one.

The fourth instruction will add 8 to PC, and store the current PC into register zero. Writing to register zero has no effect, so we are basically discarding it. PC of JAL is 12, add 8, gives us 20, which is the sixth instruction.

The fifth instruction will add 4 to register one. This is the code for BEQ is true.

The sixth instruction will add 1 to register one. This will always be executed independent of BEQ result.

The seventh instruction is NOP, basically stalling the processor. It's an alias for ADDI x0, x0, 0.

In the end, register one will store 6.

Assembler Structure

The assembler makes extensive use of the Python built-in RE package. The following code block is the heart and soul of the assembler.

import re
asem = '''some assembly code'''
for s in asem.splitlines():
    tokens = re.split(', +| |,', s)
    if tokens[0][0] == ';':
        continue
    oprd = getOperands(tokens)
    mc = getMachineCode(tokens, oprd)
    printHex(mc)

So the splitlines method from string converts each line of assembly code into list of strings.

Next, for each line of assembly code (the s variable), we use split from RE package to extract words. If we observe the example assembly code below, we can see that there are four piece of info: Opcode, destination register, source register, and immediate value. The split method will create a list of those important info, by separating the line on comma, space, or comma followed by a lot of space.

ADDI x1, x1, 1

Then if the first character of the first token is a semicolon, skip this line, as this is a comment.

Next, the getOperands() method I wrote will convert the register and immediate from string to integer.

Then the getMachineCode() method will create the machine code for this line of instruction. The returned result is a 32-bit integer.

Then the printHex() method will print the machine code into proper format into console, and I will copy and paste them into the simulator test case file for testing.

Let's look at one example case in the getMachineCode() method:

if (token[0].lower() == 'lw'):
    opcode = 0x03
    funct3 = 0x2
    rd = oprd[0]
    rs1 = oprd[1]
    imm = oprd[2]
    mc = imm << 20 | rs1 << 15 | funct3 << 12 | rd << 7 | opcode

Here, we see it's a LW instruction. From the spec, we know its opcode and funct3 value. Then we do some bitwise shift and or to form our binary code.