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.