Learn Multi platform Risc-V Assembly Programming...
For Open Source CPUs!
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
|
ChibiAkumas Tutorials
Absolute Beginner Series
A warm up for those who aren't ready to start programming, covers concepts
and terminology
Risc-V tutorials
Hello World Series
Simple Samples
Risc-V Registers
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 |
Risc-V Addressing Modes
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 |
Functions we'd like to have but don't
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 |
Lesson
1 - Getting Started with the Risc-V
Lets start learning about the Risc-V... Lets learn how to do simple
maths operations, and how to transfer data to and from memory.
There's a video of this lesson, just click the icon to
the right to watch it -> |
|
|
|
|
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
|
A template program
To allow us to get started programming quickly and see the
results, we'll be using a 'template program'...
This consists of 3 parts:
A Generic Header - this includes some
parameters in the data segment for our program
The Program - this is the body of our
program where we do our work.
A Generic Footer - this will return control to
the system
The code needs to be in the .text section...
the data needs to be in the .data section
|
|
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!
|
|
Commands, Labels and jumps
Lets take a look at a simple program!...
There will be times we need to jump around the code... the
simplest way to do this is the command
'J'... this will jump to another position in the code ...
notice, commands like this are indented by a tab.
Notice the line which is not indented and ends with a colon :
- that makes it a label called 'shutdown'
... labels tell the assembler to 'name' this position in the
program - the assembler will convert the label to a byte number in
the executable... thanks to the assembler we don't need to worry
what number that ends up being...
you'll also notice text in green starting with a hash # - this is a comment (REMark) - they
have no effect on the code (Semicolon has no effect!) |
|
Loading Immediate values
We have 31 registers in total - but we'll be using a0-a7 for
testing... the t0-5 and s0-11 registers all work the same.
To load a register we use the function LI (Load
Immediate)... This sets a register to a fixed value in the code..
the destination register is on the left of
the comma... the source value is on the
right.
We can use decimal, Hexadecimal (by starting the value with 0x) or
Ascii (by putting a character in quotes '')
'Immediate values are values on the same source code line - rather
than being taken from a register or memory address. |
|
The Registers will be loaded as specified. |
|
If we want to give a number a label we can use a Symbol... these
are defined with .eqv - a name
and value are specified
The symbol can then be used in the source
code, the assembler will convert the symbols back to their
numeric values in the bytecode. |
|
Here's the result! |
|
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.
|
|
Moving between registers
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 |
|
Add and Subtract
We can add or subtract immediate values... the AddI
command will add a value to a register,
and store the result in the leftmost register.
In this example we add 1 to A0, and save the result to A1 - A0 is
unchanged by this function.
Strangely, we don't have a SubI command!... but we can 'add' a
negative immediate command. |
|
here is the result |
|
As well as adding an immediate value, we can add or subtract a
register.... the commands are ADD and SUB
The leftmost register is the destination, the
middle one is the first value, the right hand
register is the second value to be added or
subtracted |
|
Here is the result... |
|
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!
J - Jump
J is a simple jump to label command... we specify the label to jump
to, and the code execution will continue there... there's not RETurn
command like a call - if we want the code to continue after the
jump, we probably want another jump back. |
|
JR - Jump to Register
Rather than specifying a label as an 'immediate' value, we can use a
register value
we would just load the address into the register, then specify that
address |
|
JAL - Jump and Link ... This is the equivalent
of a Call / Gosub
Unlike other CPU's this function does not use the stack... the
return address is loaded into RA (register x1)... to return we use
the command RET (a psudeo op for jr ra)
If we want to nest calls with JAL - we should push RA onto the stack
at the start, and pop it at the end.
|
|
JAL can also use an alternative return register, rather than RA we
can specify a different register to store the return address
|
|
JALR - Jump and Link to Register
This function also uses a return address... however it jumps to the
address in a register...
This function also allows an alternative return register (normally
RA) - it also allows an 'offset' to the label in the register to be
specified for the jump that occurs |
|
|
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
The Stack pointer uses register SP (Register x2) to point to the
top of the stack...
We actually have no 'proper' Stack commands!... to 'push'
an item onto the stack, we subtract 4 from SP - then load the
register we want to push to the SP address
To 'pop' an item off the stack we do the
reverse - loading the register from the address in SP - and then add
4 to the SP register.
We can define these as macros to make our lives easier. |
|
Here we've loaded a value into A0... pushed it onto the stack,
loaded a different value onto the stack and then performed a call...
Finally we pop the old value off the stack.
We dump the state of the system at each stage |
|
The changes to the stack can be seen here...
Each push to the stack can be seen in memory. |
|
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
Because Hexadecimal signed numbers have their top bit as 1 we have
to use different compare commands for signed and unsigned numbers...
there is a U at the end of unsigned comparisons.
We have 4 options:
BLTU - Branch if Less Than Unsigned
BGTU - Branch if Greater Than Unsigned
BLEU - Branch if Less Than or Equals Unsigned
BGEU - Branch if Greater Than or Equals
Unsigned |
|
The results are shown here |
|
Less - Greater - Signed- LT / LE / GE / GT
If we're working with Signed numbers, we have alternate versions -
these do not have U at the end
We have 4 options:
BLT - Branch if Less Than signed
BGT - Branch if Greater Than signed
BLE - Branch if Less Than or Equals signed
BGE - Branch if Greater Than or Equals signed |
|
The results are shown here |
|
Comparing with Zero
The Risc-V's x0 register always equals zero.. we can take
advantage of this for quick comparisons to zero
x0's alias is ZERO - and we'll use this in the compare functions
we've already seen
there are also Psuedo operations for these:BEQZ, BNEZ, BLEZ, BGEZ,
BLTZ, BGTZ |
|
Here is the result |
|
Lesson
3 - Bit ops and more maths!
We've looked at basic maths, addressing modes and branches... but
we've not covered all the maths functions of the Risc-V yet...
Lets take a look at the other options |
|
|
|
|
Logical ops - AND, OR, XOR
We have OR, AND and XOR functions...
As always they have a Desitnation on the left, and two parameters
for the logical operation...
All three commands have an Immediate version, where the second
parameter is a fixed number - these are ORI ANDI and XORI |
|
The results are shown here. |
|
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)
As well as the ones mentioned above we have NOT and NEG!
NOT flips all the bits in a register
NEG flips all the bits in a register and adds
one, effectively converting a positive number into a negative one. |
|
Here are the results of these operations |
|
Bit Shifts... SLL(I), SRL(I), SRA(I)
The Risc-V offers three kinds of bitshifts, each of which supports
a register shift amount, or an immediate one
SLL is Shift Left Logical Left - all bits move
to the left - any bits pushed out the register are lost
SRL is Shift Right Logical - all bits move to
the right - any bits pushed out the register are lost
SRA is Shift Right Ari thematic - all bits move
to the right - any bits pushed out the register are lost - any new
bits on the left contain the previous leftmost bit - maintaining the
sign. |
|
We've performed 4 shifts of A0 with each of the 3 options... here |
|
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)
The Risc-V has Four special commands for returning True (1) or
False(0)... if the condition is true, the leftmost register will be
set to 1 - else it will be set to zero
SLTU (Set Less Than Unsigned)
SGTU (Set Greater Than Unsigned)
SLT (Set Less Than Unsigned)
SGT (Set Greater Than Unsigned)
All these also have versions that take an immediate value. |
|
The results are shown here. |
|
|
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!
|
| |
Buy my Assembly programming book on Amazon in Print or Kindle!
Available worldwide! Search 'ChibiAkumas' on your local Amazon website!
Click here for more info!
|