This time I really went wild with RISC-V Assembly. I discovered a new simulator written in Python that is much more strict about what it allows. Many of the same pseudo instructions that worked in RARS don’t work in riscemu. I took this as a challenge to adapt my code for compatibility with both riscemu and RARS. Other than the different exit call numbers, the functions work the same in both emulators. This gives me a viable foundation for learning and teaching about RISC-V Assembly and how it is different from Intel. I love them both but RISC-V is the new way that many people are excited about.
main.asm
# chastelib test suite for RISC-V Assembly in riscemu simulator
# The same library of functions I commonly use in my Intel Assembly code
# have now been translated to RISC-V.
.data
# These variables are used by the intstr function to convert an integer to a string
# and what radix should be used as well as the width (how many leading zeros)
int_string: .space 32 #reserve space for 32 bytes for up to 32 bits if printed in binary
int_end: .byte 0 #the terminating zero of the integer string
radix: .byte 2 #the radix the number will be shown in
int_width: .byte 1 #by default
# These variables are for outputting special strings
# such as a newline, space, or a single character based on s0
space: .byte 0x20, 0
line: .byte 0x0A, 0
char: .byte 0, 0
# These variables are for outputting specific messages
# or to simulate user input as integers in the strint function
string0: .asciz "chastelib test suite for RISC-V Assembly in riscemu simulator\n"
input_int_0: .asciz "0"
input_int_1: .asciz "100"
.text
la s0, string0
jal putstr
# at the beginning of a program, it is usually good to get user input
# this program doesn't use real user input but simulates it with global strings we will interpret
# as if they are hexadecimal integers
# change radix to decimal
li t0, 16 #load t0 register with the new radix
la t1, radix #load t1 register with the address the radix will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
# load s0 with address of first integer string, convert it with strint, and save in another register
la s0, input_int_0
jal strint
mv s2, s0
# load s0 with address of second integer string, convert it with strint, and save in another register
la s0, input_int_1
jal strint
mv s3, s0
# this is how we would load the loop controller variables directly
# these are commented out for this example
# li s0, 0
# li s1, 0x100
mv s0, s2
mv s1, s3
loop:
# change radix to binary
li t0, 2 #load t0 register with the new radix
la t1, radix #load t1 register with the address the radix will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
# change width to 8 to represent an 8 bit binary value
li t0, 8 #load t0 register with the new width
la t1, int_width #load t1 register with the address the width will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
jal putint
jal putspace
# change radix to hexadecimal
li t0, 16 #load t0 register with the new radix
la t1, radix #load t1 register with the address the radix will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
# change width to 2 to represent an 8 bit binary value as a two digit hex value
li t0, 2 #load t0 register with the new width
la t1, int_width #load t1 register with the address the width will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
jal putint
jal putspace
# change radix to decimal
li t0, 10 #load t0 register with the new radix
la t1, radix #load t1 register with the address the radix will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
# change width to 3 to represent an 8 bit binary value decimal value of up to 3 digits
li t0, 3 #load t0 register with the new width
la t1, int_width #load t1 register with the address the width will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
jal putint
li t1, 0x20
blt s0, t1, not_char
li t1, 0x7E
blt t1, s0, not_char
jal putspace
jal putchar
not_char: # jump here if character is outside range to print
jal putline
addi s0, s0, 1
blt s0, s1, loop
la s0, string0
jal putstr
addi a0, zero, 0 # a0=0 (exit code for OS)
addi a7, zero, 93 # a7=93 (exit system call)
ecall # (environment call)
#################################################################################
# The following functions are independent of a specific RISC-V Operating System #
# #
# intstr = convert integer into a string ready for printing #
# putint = prints integer using intstr and the OS specific putstr function #
# strint = convert string into an integer #
# #
# The s0 register is used for pass data in or out of these functions #
# See comments above those specific functions for full details #
#################################################################################
# The intstr function does several things at once and is the foundation for all integer output.
# It uses the global radix variable to know which radix or number base to use when turning the integer to a string
# It also uses the global int_width variable to determine how many leading zeros should be used for the string
# The purpose of this is to make numbers look good when lined up when they are printed in a list.
# radices 2 to 36 are supported. Digits higher than 9 will be capital letters
intstr:
la t1, radix # load address of radix into t1
lb t2, 0(t1) # load value of radix into t2
la t1, int_width # load address of width into t1
lb t4, 0(t1) # load value of int_width into t4
li t3, 1 # load current number of digits, always 1
la t1, int_end # t1=address of terminating zero in string
addi t1, t1, -1 # t1-- to go to lowest digit
digits_start:
remu t0, s0, t2 # t0=remainder of the previous division
divu s0, s0, t2 # s0=s0/t2 (divide s0 by the radix value in t2)
li t5, 10 # load t5 with 10 because RISC-V does not allow constants for branches
blt t0, t5, decimal_digit
bge t0, t5, hexadecimal_digit
decimal_digit: # we go here if it is only a digit 0 to 9
addi t0, t0, 0x30
j save_digit
hexadecimal_digit:
addi t0, t0, -10
addi t0, t0, 0x41
save_digit:
sb t0, 0(t1) # store byte from t0 at address t1
beq s0, zero, intstr_end
addi t1, t1, -1
addi t3, t3, 1
j digits_start
intstr_end:
li t0, 0x30
prefix_zeros:
bge t3, t4, end_zeros
addi t1, t1, -1
sb t0, 0(t1) # store byte from t0 at address t1
addi t3, t3, 1
j prefix_zeros
end_zeros:
mv s0, t1
ret
# this function calls intstr to convert the s0 register into a string
# then it uses the system specific putstr call to print the string
# it also uses the stack to save the value of s0 and ra (return address)
# this way, s0 is restored to the value it had before this function
# restoring ra is required because it is modified during calls to other functions
putint:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
jal intstr
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
# RISC-V does not allow constants for branches
# Because of this fact, the RISC-V version of strint
# requires a lot more code than the MIPS version
# Whatever value I wanted to compare in the branch statement
# was placed in the t5 register on the line before the conditional branch
# Even though it is completely stupid, it has proven to work
strint:
la t1, radix # load address of radix into t1
lb t2, 0(t1) # load value of radix into t2
mv t1, s0 # copy string address from s0 to t1
li s0, 0
read_strint:
lb t0, 0(t1)
addi t1, t1, 1
beq t0, zero, strint_end
# if char is below '0' or above '9', it is outside the range of these and is not a digit
li t5, 0x30
blt t0, t5, not_digit
li t5, 0x39
blt t5, t0, not_digit
# but if it is a digit, then correct and process the character
is_digit:
andi t0, t0, 0xF
j process_char
not_digit:
# it isn't a digit, but it could be perhaps and alphabet character
# which is a digit in a higher base
# if char is below 'A' or above 'Z', it is outside the range of these and is not capital letter
li t5, 0x41
blt t0, t5, not_upper
li t5, 0x5A
blt t5, t0, not_upper
is_upper:
li t5, 0x41
sub t0, t0, t5
addi t0, t0, 10
j process_char
not_upper:
# if char is below 'a' or above 'z', it is outside the range of these and is not lowercase letter
li t5, 0x61
blt t0, t5, not_lower
li t5, 0x7A
blt t5, t0, not_lower
is_lower:
li t5, 0x61
sub t0, t0, t5
addi t0, t0, 10
j process_char
not_lower:
# if we have reached this point, result invalid and end function
# this is only reached if the byte was not a valid digit or alphabet character
j strint_end
process_char:
blt t2, t0 strint_end #;if this value is above or equal to radix, it is too high despite being a valid digit/alpha
mul s0, s0, t2 # multiply s0 by the radix
add s0, s0, t0 # add the correct value of this digit
j read_strint # jump back and continue the loop if nothing has exited it
strint_end:
ret
###############################################################################
# This putstr function is the most portable function for RISC-V simulators #
# It calculates the length of a zero terminated string before printing it #
# This is the same way used in my Intel Assembly programs for DOS and Linux #
# This function was written to operate the same in both RARS and riscemu #
###############################################################################
putstr:
mv t1, s0 # t1 will be used as an index register
putstr_strlen_start:
lb t0, 0(t1) # load byte into t0 from address of t1
beq t0, zero, putstr_strlen_end # if t0==0, then we jump to the end of the loop.
addi t1, t1, 1 # go to next byte
j putstr_strlen_start # jump to start of the loop
putstr_strlen_end:
addi a0, zero, 1 # a0=1 (STDOUT file number)
addi a1, s0, 0 # a1=s0 (address of string )
sub a2, t1, s0 # a2=t1-s0 (length of string )
addi a7, zero, 64 # a7=64 (write system call )
ecall # (environment call )
ret
#############################################################################
# The next four 3 functions print things to standard output #
# All of them use the putstr function above to achieve the output #
# They use the stack to preserve the values of the s0 and t1 registers used #
# They also use global variables in the data section #
#############################################################################
#the putchar function, which is named after the C language function of the same name
#prints the lowest byte of the s0 register as a byte or character to standard output
putchar:
addi sp, sp, -12
sw ra, 0(sp)
sw s0, 4(sp)
sw t1, 8(sp)
la t1, char
sb s0, 0(t1)
la s0, char
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
lw t1, 8(sp)
addi sp, sp, 12
ret
# the putspace function prints a space to standard output
putspace:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
la s0, space
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
# the putline function prints a newline to standard output
putline:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
la s0, line
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
Leave a comment