Risc-V is a relative newcomer -
essentially competing with ARM. When ARM is incredibly cheap, widely available, and impressively powerful, why would we need a competitor? Well the answer is simple... you need an ARM license to make the chip... and that license can be revoked at any time If you want a truly open platform that you have total control over, ARM cannot currently provide that... which is where Risc-V comes in! In this tutorial we'll be using RARS... a Risc-V simulator with macro and include support. |
If you want to learn Risc-V get the Cheatsheet! it has all the RISC-V commands, it covers the commands and how those commands compile to bytecode |
These
tutorials assume you have a basic understanding of concepts like
HEX and Registers... As Risc-V is really an 'Upcoming' CPU If you've never programmed before, you're better off looking at a more mainstream CPU like the Z80, 6502 or 68000 |
Lesson1 : For absolute beginners! |
RISC-V Lesson 2 - The stack and conditions |
Risc-V Assembly Lesson 3 - Bit ops and more maths! |
Hello world with RARS! - RiscV - Lesson H1 |
RiscV Lesson S1 - Reading and Showing a Register |
Bits | Int Reg | Name | Detail | |
00000 | x0 | zero | Hard-wired zero | |
00001 | x1 | ra | Return address Caller | May be changed by sub Used for function return with jalr x0, x1, 0 |
00010 | x2 | sp | Stack pointer Callee | if changed by Sub must be backed up |
00011 | x3 | gp | Global pointer | |
00100 | x4 | tp | Thread pointer | |
00101 | x5 | t0 | Temporary/alternate link register Caller | May be changed by sub |
00110 | x6 | t1 | Temporaries Caller | May be changed by sub |
00111 | x7 | t2 | Temporaries Caller | May be changed by sub |
01000 | x8 | s0 / fp | Saved register/frame pointer Callee | if changed by Sub must be backed up |
01001 | x9 | s1 | Saved register Callee | if changed by Sub must be backed up |
01010 | x10 | a0 | Function arguments/return values Caller | Use to pass to functions - May be changed by sub |
01011 | x11 | a1 | Function arguments/return values Caller | Use to pass to functions - May be changed by sub |
01100 | x12 | a2 | Function arguments Caller | Use to pass to functions - May be changed by sub |
01101 | x13 | a3 | Function arguments Caller | Use to pass to functions - May be changed by sub |
01110 | x14 | a4 | Function arguments Caller | Use to pass to functions - May be changed by sub |
01111 | x15 | a5 | Function arguments Caller | Use to pass to functions - May be changed by sub |
10000 | x16 | a6 | Function arguments Caller | Use to pass to functions - May be changed by sub |
10001 | x17 | a7 | Function arguments Caller | Use to pass to functions - May be changed by sub |
10010 | x18 | s2 | Saved registers Callee | if
changed by Sub must be backed up |
10011 | x19 | s3 | Saved registers Callee | if changed by Sub must be backed up |
10100 | x20 | s4 | Saved registers Callee | if changed by Sub must be backed up |
10101 | x21 | s5 | Saved registers Callee | if changed by Sub must be backed up |
10110 | x22 | s6 | Saved registers Callee | if changed by Sub must be backed up |
10111 | x23 | s7 | Saved registers Callee | if changed by Sub must be backed up |
11000 | x24 | s8 | Saved registers Callee | if changed by Sub must be backed up |
11001 | x25 | s9 | Saved registers Callee | if changed by Sub must be backed up |
11010 | x26 | s10 | Saved registers Callee | if changed by Sub must be backed up |
11011 | x27 | s11 | Saved registers Callee | if changed by Sub must be backed up |
11100 | x28 | t3 | Temporaries Caller | May
be changed by sub |
11101 | x29 | t4 | Temporaries Caller | May be changed by sub |
11110 | x30 | t5 | Temporaries Caller | May be changed by sub |
11111 | x31 | t6 | Temporaries Caller | May be changed by sub |
All the registers function the same... but there
are 'Official Rules!' A calling function should use the Ax registers to send data to a subroutine... if the calling function needs the Tx registers to stay the same it should back them up - the Ax registers may also change... The Sx registers can be changed by the subroutine - but it's the subroutines job to back them up if it changes them... not the calling function! |
Float Reg | Name | Detail |
f0 | ft0 | FP temporaries Caller |
f1 | ft1 | FP temporaries Caller |
f2 | ft2 | FP temporaries Caller |
f3 | ft3 | FP temporaries Caller |
f4 | ft4 | FP temporaries Caller |
f5 | ft5 | FP temporaries Caller |
f6 | ft6 | FP temporaries Caller |
f7 | ft7 | FP temporaries Caller |
f8 | fs0 | FP saved registers Callee |
f9 | fs1 | FP saved registers Callee |
f10 | fa0 | FP arguments/return values Caller |
f11 | fa1 | FP arguments/return values Caller |
f12 | fa2 | FP arguments Caller |
f13 | fa3 | FP arguments Caller |
f14 | fa4 | FP arguments Caller |
f15 | fa5 | FP arguments Caller |
f16 | fa6 | FP arguments Caller |
f17 | fa7 | FP arguments Caller |
f18 | fs2 | FP saved registers Callee |
f19 | fs3 | FP saved registers Callee |
f20 | fs4 | FP saved registers Callee |
f21 | fs5 | FP saved registers Callee |
f22 | fs6 | FP saved registers Callee |
f23 | fs7 | FP saved registers Callee |
f24 | fs8 | FP saved registers Callee |
f25 | fs9 | FP saved registers Callee |
f26 | fs10 | FP saved registers Callee |
f27 | fs11 | FP saved registers Callee |
f28 | ft8 | FP temporaries Caller |
f29 | ft9 | FP temporaries Caller |
f30 | ft10 | FP temporaries Caller |
f31 | ft11 | FP temporaries Caller |
The RISC-V is a 'Load and Store' architecture processor, meaning that many of the commands only work between registers.
Mode | Notes | Format | Example |
Immediate Addressing |
A fixed number is the parameter for an operation. | n | LI a7,12 ADDI a0,a0,-7 LI a2,255 |
Register Addressing |
A register itself will be used as a source or destination of an operation. | Rn | OR a1,a1,A0 MV a3,a1 |
Register Indirect with Offset Addressing |
This Addressing mode uses the value from the address in a register, offset by a fixed numeric value. | n(Rm) | LW a1,4(a0) LW a1,0(a0) |
Program Counter Relative with Offset Addressing |
used by the AUIPC command (Add Upper Immediate, Program Counter), and for relative jump and branch operations. | BEQ a0,a1,TestLabel AUIPC a0,0xFF |
Push | .macro push(%reg) addi sp,sp,-4 sw %reg,0(sp) .end_macro |
Pop | .macro pop(%reg) lw %reg,0(sp) addi sp,sp,4 .end_macro |
Push multiple | addi sp,sp,-8 sw x1,4(sp) sw x2,8(sp) |
Pop multiple | lw x1,4(sp) lw x2,8(sp) addi sp,sp,8 |
PrintChar (on RARS simulator) | li a7, '!' #; Char to print li a7, 11 ecall |
Exit (on RARS simulator) | li a7, 10 ecall |
Our simulator
We're going to be using RARS as a
simulator, it's a free open source Risc-V simulator.... RARS uses
java. My Devtools provide a batch file which will build the programs for you, but if you don't want to use them, the format of the build script is shown below: %BuildFile%... this would be the sourcefile you want to compile... Eg: Lesson1.asm |
Warning! ECALL is
not a real Risc-V command - it's a special command used by our
simulator to perform tasks like printing characters to the
screen, making sounds, and returning to the OS! We'll need it a lot (Via PrintChar) to output things to the screen during our tests! |
Some of these commands are
'Pseudo-ops'... this is where the assembler compiles one command
into multiple in the final binary... It makes things easier for us to let the assembler to do as much of the work as possible, so we won't differentiate between Psuedo and 'Real' commands. |
We can transfer values
between registers with MV (move) The destination register is on the left, the source is on the right. |
|
The value is copied from A0 to A1 and A2 |
Reading and Writing to and from Ram
The
Risc-V is a 32 bit CPU - so WORDS are 32 bits (not 16 like on
8/16 bit systems)... A Half-Word is 16 bits!... but relax, a byte is still 8 bits! so at least everything hasn't changed! |
To be able to read data, it needs to be in the
.data section. It cannot be in the code section |
|
To read from an address we need to load it into a
register with LA (LoadAddress) -
any register can be used for this - not just A0-7... we need to
specify the source address in brackets () - the destination register
is on the left of the comma as always We then need to load in from the address... We have 3 size options: LW for Load Word (32 bit) LH for Load Half (16 bit) LB for Load Byte (8 bit) These commands are 'sign extended' - meaning when we load a Byte or Half, the top unfilled bits take the same value of the top loaded bit (to maintain the sign)... if we don't want this we can use the Unsigned versions... There is no LWU - as there is no uloaded bits to sign extend. LHU for Load Half (16 bit) LBU for Load Byte (8 bit) |
|
Because the top bit of each memory byte was 1, The empty bytes of
the LH,LB values are FFF... This is because of the way negative numbers work in Hexadecimal. if you don't understand Hexadecimal, please watch this video |
|
We can store back to ram with SW (Store
Word), SH (Store Half) and SB
(Store Byte)... There is no need for signed and unsigned versions As well as using (A2) as an address... we can use an Offset... just put a number before the (a2) part... this will be a byte offset from the address - this works with Load statements too, and can be positive or negative! |
|
The results can be seen here |
Jumps: J JR JAL JALR... and RET!
Things
tend to work a little different on RISC compared to the 6502 or
z80 that you're used to! but don't worry... it's not so bad! You can create macro's to do things you're used to, and things will soon be much easier... In fact, the PUSH and POP commands above are macros - we'll learn how to use the stack soon! |
Lesson
2 - The stack and conditions We used the stack a bit in the last lesson with JAL - but we didn't really cover it... it's time to fix that now - it's also time to look at conditions. |
The Stack
Comparisons
Unlike other CPU's the Risc-V does not have a flag register as such... when we want to do a conditional branch, we use a branch command with two registers to compare, and a label to jump to if the condition is true...
Lets look at all the options!...
The examples
shown here are all available for download!... there are various
possible values and conditions remmed out with # - You should try enabling different conditions, and providing different input values and see how things change! |
Equals - Not Equals - EQ / NE
if we want to perform actions if the two registers are the same -
or different - we can do this with BEQ and BNE BEQ will Branch if Equal BNE will Branch if Not Equal |
|
The results can be seen here |
Less - Greater - Unsigned - LTU / LEU / GEU / GTU
Less - Greater - Signed- LT / LE / GE / GT
Comparing with Zero
Logical ops - AND, OR, XOR
LUI - Load Upper Immediate
LUI will load an immediate value into the top half of a register. | |
Here is the result! |
strictly speaking, a LI command is a pseudo op
which uses the command LUI - which loads the top half of the
register - the rest is ORed in. Fortunately, this is all handled by our assembler... phew! |
NOT (bitflip) and NEG (flip sign)
Bit Shifts... SLL(I), SRL(I), SRA(I)
There is no SLA
command (shift arithmetic left) because there is no need for one..
SLL will do the job you need! More problematic is the lack of rotation commands... ROR/ROL don't exist!... you'll have to use AND/OR and the shift commands that do exist to simulate them. |
True/False returning conditions SLT(I), SGT(I), SLTU(I),
SGTU(I)
Phew! We've covered the basics! Risc-V has extensions for 64 bit and floating point, but they are beyond what this tutorial was intended to cover... you should at least have enough now to get started! |