chastdin calculator for RISC-V Assembly

I really did it this time. I translated all of my Intel Assembly functions into RISC-V Assembly and rebuilt my calculator. It works flawlessly running under the rars simulator.

To run this example, you need the RARS java archive and a java runtime environment installed on your machine.

You can get RARS here:

https://github.com/rarsm/rars

However, once you do, you can run my program with a command like the following.

java -jar ~/rars.jar main.s
# chastelib test suite for RISC-V Assembly in RARS simulator

# this program tests the stdin extension of chastelib

# The same library of functions I commonly use in my Intel Assembly code
# have now been translated to RISC-V.
# All assembly code seen here is for the RARS simulator written in Java.

.data

##################################################################
# chastelib core specific variables                              #
#                                                                #
# These variables are used by the intstr function to convert an  #
# integer to a string and what radix and widthshould be used     #
# width means how many minimum digits including 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 

##################################################################
# chastdin specific variables                                    #
#                                                                #
# these variables are used as the default controllers            #
# for the getstr and getline functions                           #
# buf stores keyboard input during those functions               #
# count stores how many bytes were read during system read calls #
# last_char stores the last character read                       #
# usually this will be a space, tab, or newline                  #
##################################################################

buf: .space 0x100
count: .word 0
last_char: .byte 0

# program specific variables
# These variables are for outputting specific messages
# or to simulate user input as integers in the strint function

string0: .ascii "calculator for RISC-V Assembly\n"
string1: .asciz "chastdin (Chastity's STanDard INput) extension\n\n"

string_add: .asciz "add"
string_sub: .asciz "sub"
string_mul: .asciz "mul"
string_div: .asciz "div"
string_rem: .asciz "rem"
string_setradix: .asciz "setradix"

string_help: .asciz "help"
string_exit: .asciz "exit"
string_putstack: .asciz "?"
string_clear: .asciz "clear"

string_prompt: .asciz "->"

string_err: .asciz "Error: invalid number or command: "
string_err1: .asciz "Error: need one number on stack for command: "
string_err2: .asciz "Error: need two numbers on stack for command: "

chastdin_help: .ascii "chastdin is a stack based interactive calculator\n"
              .ascii "that reads stdin for numbers and commands.\n"
              .ascii "Numbers are pushed on the stack for all math.\n"
              .ascii "Each line can contain multiple numbers or commands.\n\n"
              .ascii "Arithmetic commands are add,sub,mul,div,rem\n"
              .ascii "The exit command ends the program\n"
              .ascii "The ? command prints the entire stack\n"
              .asciz "The setradix command changes the radix for input and output\n"

.align 2  # Aligns the next item to a 4-byte (2^2) word boundary
chastack: .space 0x400 #reserve space for RPN calculator stack

.text

la s0, string0
jal putstr

# change radix for this program
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

la s11, chastack #s11 will be used as the virtual stack pointer for this program

#print the help message at the beginning of the program
la s0, chastdin_help
jal putstr

#print the initial arrow prompt
la s0, string_prompt
jal putstr

main_loop:

la t1, last_char #load address of last_char
lb t0, 0(t1)     #get the last character

#show the arrow indicating we wait for the user to enter something
#but only show it when the last character is a newline
#otherwise it will print too many if multiple commands were entered on the same line
li t1, 0xA
bne t0, t1, skip_prompt
la s0, string_prompt
jal putstr
skip_prompt:

jal getstr  # read the string from standard input

#load the length of string just entered from (count)
la t1, count            #load address of count into t1
lw t0, 0(t1)            #load number of chars read at (count) address
beq t0, zero, main_loop #restart main_loop on empty string

#jal putline # print extra line for readability
#jal putstr # echo it to standard output
#jal putline

#s0 already contains string that was input
#s1 will be loaded with address of exit string
la s1, string_exit
jal strcmp
# end program if the string entered is equal to string_exit
beq t0, zero, exit

la s1, string_putstack
jal strcmp
beq t0, zero, command_putstack

la s1, string_clear
jal strcmp
beq t0, zero, command_clear

la s1, string_help
jal strcmp
beq t0, zero, command_help

#next we begin checking for actual math commands of arithmetic

la s1, string_add
jal strcmp
beq t0, zero, command_add

la s1, string_sub
jal strcmp
beq t0, zero, command_sub

la s1, string_mul
jal strcmp
beq t0, zero, command_mul

la s1, string_div
jal strcmp
beq t0, zero, command_div

la s1, string_rem
jal strcmp
beq t0, zero, command_rem

la s1, string_setradix
jal strcmp
beq t0, zero, command_setradix


#if the last string entered was not exit or a math command then
#The default command is to turn the argument into a number and push to stack
command_num:

mv s1, s0              #back up this string address to s1 register
jal strint             #try to get a number from the string pointed to by s0 register
beq a0, zero, num_push #branch to number push if zero errors in integer string

la s0, string_err    #load error message
jal putstr           #print error message
mv s0, s1            #load original command string
jal putstr           #print which command failed
jal putline
j num_push_end       #skip the push because this can't be used

num_push:            #push the number to the fake stack
addi s11, s11, 4     #increment the pointer by the size of the native int for this mode
sw s0, 0(s11)        #store the value we converted from the string with strint to this stack space
num_push_end:
j main_loop          #once value is pushed, continue the program

exit:
li a0, 0  #status
li a7, 93 #exit
ecall     #environment call

#################################################################################
# The following functions are used in the calculator program                    #
# The all jump back to the main_loop after they are done                        #
#                                                                               #
#################################################################################

#check if the stack has enough space for the last command
#this will print an error if less than two numbers were on the stack
#when using one of the math commands above

memory_check:

la s10, chastack        #load s10 with chastack address for branch comparison
blt s10, s11, memory_ok # if s10 is less than s11, no errors

print_stack_error:   #otherwise we print error message
la s0, string_err2   #get error message for less than 2 numbers on stack
jal putstr           #print error message
mv s0, s1            #get name of the command used
jal putstr           #print which command failed
jal putline
addi s11, s11, 4     #increment the pointer to what it was before the failed command
j main_loop          #now go back to main loop after error was printed

memory_ok:
sw zero, 4(sp)       #if no error, erase the old top of stack by storing zero
j main_loop          #and continue main_loop as normal


command_putstack: #print all numbers on the stack
la s9, chastack #load s9 with address of chastack
mv s10, s11     #copy value of s11 to s10
command_putstack_loop:

#is s10 equal to the address of stack start?
#if so, end the putstack loop
beq s9, s10 command_putstack_end
lw s0, 0(s10) #load the word at s10 into s0 for printing integer 
addi s10, s10, -4 #subtract the word size from this temp stack index
jal putint
jal putline
j command_putstack_loop
command_putstack_end:
j main_loop




command_clear: #erase all numbers on the stack
la s9, chastack #load s9 with address of chastack
command_clear_loop:

#is s11 equal to the address of stack start?
#if so, end the clear loop
beq s9, s11 command_clear_end
sw zero, 0(s11) #store zero into the word at 0(s11) to erase it
addi s11, s11, -4 #subtract the word size from this temp stack index
j command_clear_loop
command_clear_end:
j main_loop


command_help:
la s0, chastdin_help
jal putstr
j main_loop

#add number on top of stack to the one below it
command_add:
lw t1, 0(s11)     #load the word at this chastack address
addi s11, s11, -4 #subtract the word size from s11
lw t0, 0(s11)     #load the word at this chastack address
add t0, t0, t1    #t0 = t0 + t1
sw t0, 0(s11)     #save the word at this chastack address
j memory_check    #check stack for errors after this command

#add number on top of stack to the one below it
command_sub:
lw t1, 0(s11)     #load the word at this chastack address
addi s11, s11, -4 #subtract the word size from s11
lw t0, 0(s11)     #load the word at this chastack address
sub t0, t0, t1    #t0 = t0 - t1
sw t0, 0(s11)     #save the word at this chastack address
j memory_check    #check stack for errors after this command

#mul number on top of stack to the one below it
command_mul:
lw t1, 0(s11)     #load the word at this chastack address
addi s11, s11, -4 #subtract the word size from s11
lw t0, 0(s11)     #load the word at this chastack address
mul t0, t0, t1    #t0 = t0 * t1
sw t0, 0(s11)     #save the word at this chastack address
j memory_check    #check stack for errors after this command

#divide and store quotient on stack
command_div:
lw t1, 0(s11)     #load the word at this chastack address
addi s11, s11, -4 #subtract the word size from s11
lw t0, 0(s11)     #load the word at this chastack address
divu t0, t0, t1    #t0 = t0 / t1
sw t0, 0(s11)     #save the word at this chastack address
j memory_check    #check stack for errors after this command

#divide and store remainder on stack
command_rem:
lw t1, 0(s11)     #load the word at this chastack address
addi s11, s11, -4 #subtract the word size from s11
lw t0, 0(s11)     #load the word at this chastack address
remu t0, t0, t1   #t0 = t0 % t1
sw t0, 0(s11)     #save the word at this chastack address
j memory_check    #check stack for errors after this command



#pop top of stack and set the current radix to it
#it has error checking and leaves the radix as is
#unless at least one number is on the stack
command_setradix:


la s10, chastack     #load s10 with chastack address for branch comparison
ble s11, s10, change_radix_no # if s11 is less than or equal to chastack address, branch to radix error
change_radix_yes:
lw t0, 0(s11)        #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
sw zero, 0(s11)      #erase the old top of stack by storing zero
addi s11, s11, -4
j main_loop          #and continue main_loop as normal
change_radix_no:
la s0,string_err1    #get error message for less than 1 numbers on stack
jal putstr           #print error message
mv s0, s1            #get name of the command used
jal putstr           #print which command failed
jal putline

addi s11, s11, 4     #increment the pointer to what it was before the failed command
j main_loop          #now go back to main loop after error was printed

#################################################################################
# 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

# strint takes the string at address pointed to by s0 register
# and then loads the s0 register with an integer equivalent value
# the a0 register is returned with the number of errors that happened
# programs can use this to find if a user entered a valid number
# number is intepreted according to the current radix

strint:

li a0, 0         #load zero into register for error counting

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 an alphabet character
#which counts as 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_error

process_char:

blt t2, t0 strint_end_error #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_error:  #we jump here if there was an error with one of the chars
addi a0, a0, 1 #add 1 to the a0 register indicating an error occurred

strint_end: #we jump here when no errors happened
ret

###############################################################################
# This putstr function is my 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:              

li a0, 1                        # STDOUT file number
mv a1, s0                       # address of string 
sub a2, t1, s0                  # length of string
li a7, 64                       # write call number
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

##########################################################################
# chastdin extension functions                                           #
#                                                                        #
# all functions that deal with getting strings and characters from stdin #
##########################################################################

# the getstr function will read a string into a buffer from stdin
# and return it in the s0 register for printing with the putstr function
# the (count) variable will also return the number of characters

getstr:

li t0, 0                        # use t0 register to track chars read
la a1, buf                      # load address of buffer for read string
li a2, 1                        # read only 1 byte for each env call

getstr_chars:

li a0, 0                        # STDIN file number
li a7, 63                       # read call number
ecall                           # environment call

# Branch to label getstr_end if a0 is less than a2
# a0 is the return value of this environment read call
# as will be -1 on error or 1 if successful
# because we read 1 character at a time

blt a0, a2, getstr_end

# if no error, test range of the last byte

lb t1, 0(a1)      #load byte at address (a1) into t1 register

# if t1 is less than 0x21
# or t1 is more than 0x7E
# branch to function end because it is outside of print range

li t2, 0x21
blt t1, t2, getstr_end
li t2, 0x7E
blt t2, t1, getstr_end

# otherwise, proceed to read more characters
add t0, t0, a0    # add to read counter
addi a1, a1, 1    # add 1 to buffer pointer register a1
j getstr_chars # unconditional jump to getstr_chars

getstr_end:

la t2, count       #load address of count into t2
sw t0, 0(t2)       #store number of chars read at (count) address
la t2, last_char   #load address of last_char into t2
sb t1, 0(t2)       #store last byte at (last_char) address
sb zero, 0(a1)     #store byte zero to terminate string
la s0, buf         #return address of buf in s0 register

ret




# the getline function will read a string into a buffer from stdin
# and return it in the s0 register for printing with the putstr function
# the (count) variable will also return the number of characters
# this function will get the whole line including spaces

getline:

li t0, 0                        # use t0 register to track chars read
la a1, buf                      # load address of buffer for read string
li a2, 1                        # read only 1 byte for each env call

getline_chars:

li a0, 0                        # STDIN file number
li a7, 63                       # read call number
ecall                           # environment call

# Branch to label getline_end if a0 is less than a2
# a0 is the return value of this environment read call
# as will be -1 on error or 1 if successful
# because we read 1 character at a time

blt a0, a2, getline_end

# if no error, test range of the last byte

lb t1, 0(a1)      #load byte at address (a1) into t1 register

# if t1 is less than 0x20
# or t1 is more than 0x7E
# branch to function end because it is outside of print range

li t2, 0x20
blt t1, t2, getline_end
li t2, 0x7E
blt t2, t1, getline_end

# otherwise, proceed to read more characters
add t0, t0, a0    # add to read counter
addi a1, a1, 1    # add 1 to buffer pointer register a1
j getline_chars # unconditional jump to getline_chars

getline_end:

la t2, count       #load address of count into t2
sw t0, 0(t2)       #store number of chars read at (count) address
la t2, last_char   #load address of last_char into t2
sb t1, 0(t2)       #store last byte at (last_char) address
sb zero, 0(a1)     #store byte zero to terminate string
la s0, buf         #return address of buf in s0 register

ret



# Short Description of strlen:
# The strlen function gets the length of string in s0 and returns it in s0
# This is the same algorithm used in my putstr function but is independent of an operating system.

strlen:

mv t1, s0                       # t1 will be used as an index register

strlen_start:
lb t0, 0(t1)                    # load byte into t0 from address of t1
beq t0, zero, strlen_end        # if t0==0, then we jump to the end of the loop.
addi t1, t1, 1                  # go to next byte
j strlen_start                  # jump to start of the loop
strlen_end:              

sub s0, t1, s0                  # return length of string in s0

ret


# Short Description of strcmp:
# strcmp compares the string at s0 to the one at s1
# t0 returns 0 if the strings are the same and non zero if different
# the algorithm is simple but I will explain it for those who are confused

# Long Description of strcmp:
# each byte from each string is loaded into the t0 and t1 registers
# the bytes are compared. if they are different, then we jump to the end
# However, if they are the same, then we check if one of them is zero
# if it is zero, this also jumps to the end of the function
# If neither jump took place, then we jump to the start of the loop
# but when the function finally ends t1 will be subtracted from t0
# this ensures that the t0 register returns zero if the final characters are the same
# a zero result in t0 also guarantees that both strings are equal

strcmp:

mv a0, s0 # move pointer s0 to a0
mv a1, s1 # move pointer s1 to a1

strcmp_start:

#read a byte from each string
lb t0, 0(a0) 
lb t1, 0(a1) 
#if the two bytes are not equal end comparison
bne t0, t1, strcmp_end

#but if they are equal, test for zero
#if one of them is zero, also end the loop
beq t0, zero, strcmp_end

addi a0, a0, 1                  # go to next byte
addi a1, a1, 1                  # go to next byte

j strcmp_start

strcmp_end:

#subtract t1 from t0
#if t0 is still zero after the function returns
#it means that the strings are equal
sub t0, t0, t1

ret

Comments

Leave a comment