RISC-V processors, like ARM processors, are complex beasts, making them challenging to teach with. When teaching with them it’s important to have appropriate development tools to reflect the immediate pedagogical goals but also to provide students with a pathway for further learning and application. When I teach courses on computer architecture, like EECS 2021 at YorkU, I want to provide students with tools that let them explore the way computers work. These tools should include:
- Quality texts
- IDEs with a simulator, support for assembler and C or C++ (e.g. Segger Embedded Studio)
- Command-line compiler tools and simulators (e.g. gcc or clang build tools)
- Command-line simulators (e.g. Spike and Kite)
The first two points were easy to figure out. The last two have been more challenging.
Fortunately, Ilya Sotnikov’s riscv-asm-spike repository brings together solutions for points 3 and 4 that work together as I wrote about in an earlier blog post. It’s not perfect, however. Two major issues remain that I will have to tackle at a later date:
- Spike is feature poor
- No C standard library support
Spike simulates a variety of RISC-V devices but it doesn’t have a lot of important features for diving in and evaluating the simulation in detail. That’s why people use additional tools like pk and GDB to provide features. I was hoping to do likewise but have run into a few roadblocks that have forced me to stick to plain Spike for the time being.
The second issue, the lack of standard library support, means that certain features that developers are used to, like printf(), are not available. Where this really impacts things is that it doesn’t allow for tools like the Unity unit testing library to be used. As with the roadblocks forcing me to use Spike without pk or GDB, I’ll need to investigate the inclusion of a standard library for RISC-V at a later date.
Setting up the tools and simulator
I want to use command-line simulators for automatically graded exercises on Moodle using Virtual Programming Lab. That’s because those are the learning management system tools that are applicable for the EECS 2021 course here at York University. Effectively, I want be able to use RISC-V build tools and a RISC-V simulator in a similar manner to what I had discussed earlier for ATMEGA processors and the simAVR simulator.
Before proceeding, you’ll need to
- Install the RISC-V build tools
- Download and build Spike
- Download the riscv-asm-spike repository
Next, modify the riscv-asm-spike Makefile to suit your build environment:
.PHONY: all clean run
TOOLCHAIN_PREFIX := riscv64-elf
AS := $(TOOLCHAIN_PREFIX)-as
CC := $(TOOLCHAIN_PREFIX)-gcc
LD := $(TOOLCHAIN_PREFIX)-ld
SRC_DIR := src
TARGET_DIR := target
SPIKE_DIR := /Users/jamessmith/riscv_spike/install/install/bin
SRC := $(wildcard $(SRC_DIR)/*.s $(SRC_DIR)/*.c)
OBJ := $(filter %.o, $(SRC:$(SRC_DIR)/%.s=$(TARGET_DIR)/%.o) \
$(SRC:$(SRC_DIR)/%.c=$(TARGET_DIR)/%.o))
TARGET_NAME := main.elf
LINKER_SCRIPT := riscv.ld
RV_ARCH := rv64gc
AS_FLAGS := -march=$(RV_ARCH)
LD_FLAGS := -Map=$(TARGET_DIR)/$(TARGET_NAME).map -T $(SRC_DIR)/$(LINKER_SCRIPT)
C_FLAGS := -Wall -Wextra -std=c11 -pedantic -g -Og -ffreestanding -nostdlib \
-mcmodel=medany -march=$(RV_ARCH) -c
all: $(TARGET_DIR)/$(TARGET_NAME)
$(TARGET_DIR)/$(TARGET_NAME): $(OBJ)
$(LD) $(LD_FLAGS) -o $@ $^
$(TARGET_DIR)/%.o: $(SRC_DIR)/%.s
mkdir -p $(TARGET_DIR)
$(AS) $(AS_FLAGS) -I $(SRC_DIR) -o $@ $<
$(TARGET_DIR)/%.o: $(SRC_DIR)/%.c
mkdir -p $(TARGET_DIR)
$(CC) $(C_FLAGS) -o $@ $<
clean:
rm -rf $(TARGET_DIR)
run: all
$(SPIKE_DIR)/spike --isa=$(RV_ARCH) $(TARGET_DIR)/$(TARGET_NAME)
As highlighted above, it’s important that you specify
- the toolchain we’re using (e.g. riscv64-elf here, but it could also be something like riscv-unknown-elf)
- the kind of RISC-V processor that we’re going to use (rv64gc)
- the name of the output file (main.elf)
- the use of debug symbols and limited optimization (-g -Og)
- the explicit naming of the simulator directory if it’s not in your path (/Users/jamessmith/riscv_spike/install/install/bin)
Also important to not is the inclusion of a linker script. This linker script will specify where the main.elf file will start in the RISC-V memory space. When RISC-V processors run without an operating system (i.e. in a “bare-metal” configuration) they are supposed to start, apparently, at memory location 0x8000000 (eight million, in hexadecimal). The script also specifies reserved memory space (“to host”) for the simulated RISC-V processor and the Spike simulator to deal with each other.
If all of your build tools are set up correctly then you should be able to run the Make file as follows:
make clean; make all
This will take all the source code, including assembler and C files, located in the src sub-directory, compile and link them according to the linker script, placing the resulting object (.o) and executable (.elf) files in the target sub-directory.
If you have set up your Spike simulator correctly you can also attempt to run the ELF file (main.elf) in it, either directly from the command line:
/Users/jamessmith/riscv_spike/install/install/bin/spike --isa=rv64gc target/main.elf
Which assumes that the Spike simulator is located in /Users/jamessmith/riscv_spike/install/install/bin/.
You can also invoke the simulator from the Makefile:
make run
If you haven’t modified any of the C or Assembler files from the original repository then you should get some text in your terminal showing the the simulation is running:
Modifying RISCV-ASM-SPIKE for C vs ASM unit testing
Now that we know that the tools are working, it’s time to update the code to allow for unit testing of student code. What we want is to be able to ask the student to write a stand-alone assembler file and validate it against a model file based on how it performs. For example, we can ask the student to write an assembler function that adds two numbers together and return the result. We’ll then call that function and see if it behaves correctly.
We’re going to do this by comparing the student’s Assembler function with an equivalent function, written by the teacher, in C.
To this end, we need to modify the original repository code to allow for:
- Calling a student function, written in Assembler, with two input parameters and one returned value
- Calling a model teacher function, written in C, with two input parameters and one returned value
- Conversion of integer values into character arrays because we don’t have stdlib.h or strings.h libraries available
Here is a list of files that we will modify or create:
- main.s
- testStudentCode.c
- studentAsmFunction.s
- teacherFunction.c
- teacherFunction.h
- iota.c
- iota.h
First, we modify the main.s file:
.section .text
.globl main
main:
# Save to stack.
addi sp, sp, -8
sd ra, 0(sp)
# Jump to a C function.
la a0, msg_c_fn
jal print_str_ln
jal c_function
la a0, msg_sep
jal print_str_ln
# Jump to another C function
jal testStudentCode
# remove from stack.
ld ra, 0(sp)
addi sp, sp, 8
# Exit the program
li a0, 0
j tohost_exit
# Interrupts / Exceptions loop. Don't use.
1:
wfi
j 1b
.section .data
msg_mtime: .asciz "mtime:"
msg_c_fn: .asciz "calling a C function from asm..."
msg_wfi: .asciz "waiting for interrupts..."
msg_exc: .asciz "manually invoking an exception..."
.globl msg_sep
msg_sep: .asciz "-------------------------"
Next, we write a function called studentAsmFunction.s :
.section ".text"
.globl studentAsmFunction
studentAsmFunction:
# Function to add two integers
add a0, a0, a1 # a0 = a0 + a1
ret # Return from the function
# Example usage in C:
# int studentAsmFunction(int a, int b);
We’re going to assume that the student wrote that file. In practice we would probably write it as a template like this:
# Template version of studentAsmFunction.
# Function to add two integers
.section ".text"
.globl studentAsmFunction
studentAsmFunction:
# Dear student: write your code here
ret # Return from the function
Next, we write the C file, teacherFunction.c, containing the model function that the student’s code will be evaluated against:
// Model function from the Teacher.
int teacherFunction(int a, int b){
return a + b;
}
and the associated header file, teacherFunction.h:
int teacherFunction(int a, int b);
Next we have the file that does the testing and returns the result to Moodle. You can modify the minimum grade, maximum grade and values that are tested. As it is currently written the minimum grade is 0 and the maximum grade is 1. I also set it up to run the tests twenty times, with different values of input pairs to ensure that students don’t hard code a short cut answer. At the end of this file there will be some strings that print out comments and a grade. These are formatted in a manner that will allow VPL to send the results to Moodle. Note: I used Bing to develop some of the code in here, mostly to get around the limitations imposed by not having access to stdlib.h. In the future, I hope that be able to incorporate an appropriate standard library.
Here is the file, testStudentCode.c:
#include <stdint.h>
//#include "studentFunction.h"
#include "teacherFunction.h"
#include "itoa.h"
// Set min and max grades. I tend to give either 1 (success) or 0 (failed).
#define MAX_GRADE (1) // This is the maximum numeric grade. I make it 1.
#define MIN_GRADE (0) // This is the minimum numeric grade. I make it 0.
#define INPUT_ARRAY_SIZE (20) // number of inputs used
// Functions defined elsewhere
extern int print_hex_ln(uint64_t n);
extern int print_str_ln(const char *s);
extern int studentAsmFunction(int a, int b);
// Prototypes
void testStudentCode(void);
void printMoodleGrade(int);
void testStudentCode(void) {
/* start the integer to character array (string) example. */
char buffer[20];
int i = 0; // counter for loop.
volatile int studentValue = 0;
volatile int teacherValue = -1;
int correctValues = 0;
/* Generate two sets of ten manually-selected unsigned integers
* (I can't use stdlib so can't use random number generators)
*/
uint32_t input_set1[INPUT_ARRAY_SIZE] = {0, 1, 16, 256, 123456, 789012, 345678, 20, 567890, 234567, 890123, 456789, 123789, 789456, 456123, 789789, 123123, 456456, 2, 4};
uint32_t input_set2[INPUT_ARRAY_SIZE] = {0, 1, 16, 256, 654321, 987654, 321098, 765432, 109876, 32, 876543, 210987, 654987, 321654, 987321, 654123, 321987, 987654, 2, 4};
for (i = 0; i < INPUT_ARRAY_SIZE; i++)
{
// ---------------------------------------------------------
print_str_ln("-------------------------------------");
print_str_ln("Test set number: ");
itoa(i, buffer, 10);
print_str_ln(buffer);
print_str_ln("-------------------------------------");
print_str_ln("Testing against two inputs:");
print_str_ln("First input:");
itoa(input_set1[i], buffer, 10);
print_str_ln(buffer);
print_str_ln("Second input:");
itoa(input_set1[i], buffer, 10);
print_str_ln(buffer);
// ---------------------------------------------------------
print_str_ln("Run the student's function, written in Assembler");
studentValue = studentAsmFunction(input_set1[i],input_set2[i]);
studentValue = studentValue + 0;
itoa(studentValue, buffer, 10);
print_str_ln("Result from the call to the student function:");
print_str_ln(buffer);
// ---------------------------------------------------------
print_str_ln("Run the teacher's reference function (written in C)");
teacherValue = teacherFunction(input_set1[i],input_set2[i]);
teacherValue = teacherValue + 0;
itoa(teacherValue, buffer, 10);
print_str_ln("Result from the call to the teacher's function:");
print_str_ln(buffer);
print_str_ln("");
// If the student and teacher values match, increment the counter.
if(teacherValue != studentValue){
correctValues = correctValues + 0;
}
else{
correctValues = correctValues + 1;
}
}
print_str_ln("Number of tests run:");
itoa(i, buffer, 10);
print_str_ln(buffer);
print_str_ln("Number of correct tests:");
itoa(correctValues, buffer, 10);
print_str_ln(buffer);
// Give a full grade only if all values match.
if((correctValues) != INPUT_ARRAY_SIZE){
printMoodleGrade(MIN_GRADE);
}
else{
printMoodleGrade(MAX_GRADE);
}
}
// Print the grade with prefix that Moodle will accept.
// Based on Jim Burton's code: https://github.com/jimburton/vpl-unit-test/blob/master/src/Main.java
// Based on printing a character string from an integer. No using stdlib.h.
// Had help with Bing on this.
void printMoodleGrade(int numeric_grade){
int grade = numeric_grade;
char output[50]; // Adjust size as necessary
int i = 0;
// Manually construct the character array
char prefix[] = "Grade :=>> ";
while (prefix[i] != '\0') {
output[i] = prefix[i];
i++;
}
// Convert numeric grade to character array and append
int temp = grade;
char gradeCharArray[10];
int j = 0;
if (temp == 0) {
gradeCharArray[j++] = '0';
} else {
while (temp > 0) {
gradeCharArray[j++] = (temp % 10) + '0';
temp /= 10;
}
// Reverse the gradeCharArray array
for (int k = 0; k < j / 2; k++) {
char tempChar = gradeCharArray[k];
gradeCharArray[k] = gradeCharArray[j - k - 1];
gradeCharArray[j - k - 1] = tempChar;
}
}
gradeCharArray[j] = '\0';
// Append gradeCharArray to output
int k = 0;
while (gradeCharArray[k] != '\0') {
output[i++] = gradeCharArray[k++];
}
output[i] = '\0';
// Print the grade character array.
print_str_ln(output);
// Print a comment
if(numeric_grade == 0){
print_str_ln("Comment:=>> Unit Test failed. Student function does not match Teacher function.");
}
else{
print_str_ln("Comment:=>> Unit Test passed. Congratulations.");
}
}
The integer to character array code, iota.c, is as follows:
/* --------------------------------------------------------------
* Integer to String / Character array conversion.
* Used because I don't have access to stdlib.h.
*
* I used Bing / ChatGPT to create this quickly.
*
* Initial test shows it works.
*
* Sure! Here’s a simple implementation of the itoa function in C without using stdlib.h or stdio.h. This function converts an integer to a string representation:
* -------------------------------------------------------------- */
// Function definitions
void reverse(char str[], int length) {
int start = 0;
int end = length - 1;
while (start < end) {
char temp = str[start];
str[start] = str[end];
str[end] = temp;
start++;
end--;
}
}
char* itoa(int num, char* str, int base) {
int i = 0;
int isNegative = 0;
// Handle 0 explicitly, otherwise empty string is printed for 0
if (num == 0) {
str[i++] = '0';
str[i] = '\0';
return str;
}
// In standard itoa(), negative numbers are handled only with base 10
if (num < 0 && base == 10) {
isNegative = 1;
num = -num;
}
// Process individual digits
while (num != 0) {
int rem = num % base;
str[i++] = (rem > 9) ? (rem - 10) + 'a' : rem + '0';
num = num / base;
}
// If number is negative, append '-'
if (isNegative) {
str[i++] = '-';
}
str[i] = '\0'; // Append string terminator
// Reverse the string
reverse(str, i);
return str;
}
/* In this example, the `itoa` function is used to convert integers to strings in different bases. The `main` function demonstrates converting an integer to a string in base 10 (decimal) and base 16 (hexadecimal). The `printf` statements then print the results.
Feel free to run this code and see how it works! If you have any more questions or need further assistance, just let me know.
Source: Conversation with Copilot, 2024-07-30
(1) github.com. https://github.com/aWildOtto/awesome-shell/tree/6a28a9b968f750e751b4e8896f0ec5be177c826c/shell_sol.c.
(2) github.com. https://github.com/DSC-RIT/Bug-Bounty-Editorial/tree/2b9332d15eef87513c02d805c27cea82b8424b22/Medium%2FQ6%2Fprogram.c.
(3) github.com. https://github.com/Dant86/assembler/tree/2c83a96c3dd9fe197a09d542369b8e4cf6bd2207/parser.c.
(4) github.com. https://github.com/saitor/Usaco-solution/tree/f55d6d32d1b858e53b3d21c5cfa90c26bc506a97/dualpal.c.
(5) github.com. https://github.com/Qwerasdzxc/xv6-Users-and-groups/tree/40458fbb94406c98d97dda8a439a10cf7f3bd10d/user%2Fulib.c.
(6) github.com. https://github.com/gopro2027/ParadiseWAW/tree/81b6086418a543e71e7b3dd5471c516d0ce192a2/ParadiseCompatabilityTester%2Fprx.cpp.
(7) github.com. https://github.com/Sdcross/MPX-OS/tree/9af3d8ed8c604d699c211434891de25ad4c57c5d/lib%2Fstring.c.
*/
// alternative: https://stackoverflow.com/questions/71626521/writing-my-own-itoa-function-from-scratch-with-no-stdlib
and the header file, iota.h:
void reverse(char str[], int length);
char* itoa(int num, char* str, int base);
I have tested this out, stand-alone, on my development computer and am satisfied that it basically works. Next, I will have to try it out on an actual VPL system to see if the communication with Moodle is valid. Again, I’m hoping to eventually get stdlib.h or some embedded variant included so that I can use the Unity unit testing framework. Then I’ll remove the usage of the Bing-generated code. But that’s a job for another time.
James Andrew Smith is a Professional Engineer and Associate Professor in the Electrical Engineering and Computer Science Department of York University’s Lassonde School, with degrees in Electrical and Mechanical Engineering from the University of Alberta and McGill University. Previously a program director in biomedical engineering, his research background spans robotics, locomotion, human birth and engineering education. While on sabbatical in 2018-19 with his wife and kids he lived in Strasbourg, France and he taught at the INSA Strasbourg and Hochschule Karlsruhe and wrote about his personal and professional perspectives. James is a proponent of using social media to advocate for justice, equity, diversity and inclusion as well as evidence-based applications of research in the public sphere. You can find him on Twitter. Originally from Québec City, he now lives in Toronto, Canada.