Skip to content

RISC V Co Pro Notes

David Banks edited this page Jan 1, 2025 · 16 revisions

Introduction

The indigo release of PiTubeDirect (under development) includes an experimental RISC-V Co Processor.

This includes:

  • the RISC-V Co Processor itself (Co Pro number 23, based on the Mini RV32ima emulator)
  • the RISC-V Tube ROM, written in assembler, that provides a MOS API
  • a low level RISC-V debugger (break points, single step disassembler, etc. accessed via the Pi serial port)
  • a standard RISC-V GCC toolchain that allows C/assembler programs to be compiled and linked with newlib-nano
  • a sample application (written in C) that calculates Pi
  • a port of Richard Russell's BBC Basic that supports 32/64 bit integers and 64-bit floats

This page is an initial attempt at documentation for the above.

RISC-V Co Processor

The RISC-V Co Processor has been assigned Co Pro number 23, so can be selected using:

*FX 151,230,23
<control break>

This RISC-V Co Processor uses the Mini RV32ima emulator, which emulates the rv32ima flavour of RISC-V. This includes the 32-bit integer core, plus the multiple/divide and atomic operation extensions.

There is currently no support for floating point at the instruction level, but there are two ways that floating point can be added:

  • using a undefined instruction handler and a software emulation
  • using a library, such as Newlib-nano that is included with the RISC-V GNU C toolchain

The rational for excluding floating point from the core is to allow the PiTubeDirect RISC-V Co Processor to be compatible with a future hardware RISC-V Co Processor implemented in the FPGA-based Matchbox Co Processor platform. This hardware is very resource limited, and hardware floating point is definitely out of scope. This decision could be revisited in the future.

At the moment, there is 16MB of RAM available to the RISC-V Co Processor. This is fixed, but could be increased if necessary.

The current memory map is:

&00000000-&00F7FFFF : User RAM
&00F80000-&00FBFFFF : Spare, can used for a language (256KB)
&00FC0000-&00FFFFDF : Tube ROM (256KB, 5K used)
&00FFFFE0-&00FFFFFF : Tube Registers

OSBYTE &83 returns the start of user RAM as &00000000.

OSBYTE &84 returns the end of user RAM as &00F80000.

The default stack is starts just below &00F80000.

RISC-V Tube ROM

The RISC-V Tube ROM provides:

  • a full set of MOS APIs invoked using the RISC-V ecall instruction
  • a RISC-V interrupt handler (handling the tube interrupt)
  • a RISC-V exception handler (handling the dispatch of ecall instructions)
  • a small number of built-in *commands
  • tube data transfer routines, supporting all transfer modes

Much of this is derived from Jonathan Harston's various Tube ROMs, most notably the PDP11 Tube ROM.

The following commands are built in to the RISC-V Tube ROM

*HELP
*GO <address>
*TEST <hex>
*PI <decimal>

RISC-V MOS API

The MOS API makes use of the RISC-V ecall (environment call) instruction, with the following conventions:

  • a0..a6 contain the API parameters
  • a7 contains the API function number
  • on exit, the a0 register typically contains the result

Here is an example of calling OSWRCH:

    li      a7, 0xAC0004
    li      a0, 'X'
    ecall

The API function numbers are as follows:

    OS_QUIT        0xAC0000
    OS_CLI         0xAC0001
    OS_BYTE        0xAC0002
    OS_WORD        0xAC0003
    OS_WRCH        0xAC0004
    OS_NEWL        0xAC0005
    OS_RDCH        0xAC0006
    OS_FILE        0xAC0007
    OS_ARGS        0xAC0008
    OS_BGET        0xAC0009
    OS_BPUT        0xAC000A
    OS_GBPB        0xAC000B
    OS_FIND        0xAC000C
    OS_SYS_CTRL    0xAC000D
    OS_HANDLERS    0xAC000E
    OS_ERROR       0xAC000F

The API parameters and result registers are specific to the function. The API is based on JGH's PDP11 MOS API.

Here is a current API documentation extracted from the RISC-V Tube ROM source code:

# --------------------------------------------------------------
# MOS interface
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  0 - OSQUIT - Exit current program
# On entry:
#     no parameters
# On exit:
#     this call does not return
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  1 - OSCLI - Send command line to host
# --------------------------------------------------------------
# On entry:
#     a0: pointer to command string, terminated by 0D
# On exit (for a command that runs on the host):
#     t0-t3: undefined, all other registers preserved
# On exit (for a program that runs on the parasite):
#     a0: program result
#     t0-t3: undefined, all other registers as set by the program
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  2 - OSBYTE - Call MOS OSBYTE function
# --------------------------------------------------------------
# On entry:
#     a0: OSBYTE A parameter (see AUG)
#     a1: OSBYTE X parameter (see AUG)
#     a2: OSBYTE Y parameter (see AUG)
# On exit:
#     a0: preserved
#     a1: OSBYTE X result
#     a2: OSBYTE Y result (if a0 >= 0x80, otherwise preserved)
#     a3: OSBYTE C result (if a0 >= 0x80, otherwise preserved)
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  3 - OSWORD - Call MOS OSWORD function
# --------------------------------------------------------------
# On entry:
#     a0: OSWORD number (see AUG)
#     a1: pointer to OSWORD block (see AUG)
# On exit:
#     a0: preserved
#     a1: preserved, OSWORD block updated with response data
#     t0-t3: undefined, all other registers preserved
#
# In addition, for OSWORD 0:
#     a2: response length, or -1 if input terminated by escape
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  4 - OSWRCH - Write character to output stream
# --------------------------------------------------------------
# On entry:
#     a0: character to output
# On exit:
#     a0: preserved
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  5 - OSNEWL - Write <NL><CR> to output stream
# --------------------------------------------------------------
# On entry:
#     no parameters
# On exit:
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  6 - OSRDCH - Wait for character from input stream
# --------------------------------------------------------------
# On entry:
#     no parameters
# On exit:
#     a0: character read from input, or -1 if escape pressed
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  7 - OSFILE - Read/write a whole files or its attributes
# --------------------------------------------------------------
# On entry:
#     a0: function (see AUG)
#     a1: pointer to filename, terminated by zero
#     a2: load  address
#     a3: exec  address
#     a4: start address
#     a5: end   address
# On exit:
#     a0: result
#     a1: preserved
#     a2: updated with response data
#     a3: updated with response data
#     a4: updated with response data
#     a5: updated with response data
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  8 - OSARGS - Read/write an open files's arguments
# --------------------------------------------------------------
# On entry:
#     a0: function (see AUG)
#     a1: file handle
#     a2: 32-bit value to read/write (Note: NOT a block pointer)
# On exit:
#     a0: preserved (except for a0=0 a1=0, where its the FS number)
#     a1: preserved
#     a2: 32-bit value to read/write (updated for a read operation)
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall  9 - OSBGET - Get a byte from open file
# --------------------------------------------------------------
# On entry:
#     a1: file handle
# On exit:
#     a0: byte read (0..255), or -1 if EOF reached
#     a1: preserved
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 10 - OSBPUT - Put a byte to an open file
# --------------------------------------------------------------
# On entry:
#     a0: byte to write
#     a1: file handle
# On exit:
#     a0: preserved
#     a1: preserved
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 11 - OSGBPB - Read or write multiple bytes
# --------------------------------------------------------------
# On entry:
#     a0: function (see AUG)
#     a1: file handle (8 bits)
#     a2: start address of data (32 bits)
#     a3: number of bytes to transfer (32 bits)
#     a4: sequential file pointer (32 bits)
#
# On exit:
#     a0: bits 0-7 are the 8-bit result, bit 31 indicates EOF
#     a1: preserved
#     a2: updated with response data
#     a3: updated with response data
#     a4: updated with response data
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 12 - OSFIND - Open or Close a file
# --------------------------------------------------------------
# On entry:
#     a0: function (see AUG)
#     a1: file handle                          (if close file), or
#         pointer to filename terminated by 0D (if open file)
# On exit:
#     a0=zero or handle
#     t0-t3: undefined, all other registers preserved
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 13 - OS SYSTEM CONTROL - Miscellaneous
# --------------------------------------------------------------
# On entry:
#     a0: function
# On exit:
#     a0: result
#     t0-t3: undefined, all other registers preserved
# Only one function is currently defined:
#     a0 = &01: setup new program enviroment. This must be called by
#     code that wants to become the current program instead of being
#     transient code. The current program is re-entered at Soft Break.
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 14 - OS HANDLERS - Reads/writes environment handlers
#                          or EMT dispatch table entries.
# --------------------------------------------------------------
#
# a0 >= 0: Reads or writes ECALL dispatch address
#
# On entry:
#     a0: ECALL number (0..15)
#     a1: address of ECALL routine, or zero to read
# On exit:
#     a0: preserved
#     a1: previous ECALL dispatch address
#
# a0 < 0: Reads or writes environment handler:
#
# On entry:
#     a0: Environment handler number
#     a1: address of environment handler or zero to read
#     a2: address of environment data block or zero to read
# On exit:
#     a0: preserved
#     a1: previous environment handler address
#     a2: previous environment data address
#
# Environment handler numbers are:
#     a0     a1 = handler         a2 = data
#     -1     Exit                 version
#     -2     Escape               Escape flag (one byte)
#     -3     Error                Error buffer (256 bytes)
#     -4     Event                unused
#     -5     Unknown IRQ          (used during data transfer)
#     -6     Unknown ECALL        Ecall dispatch table (64 bytes)
#     -7     Uncaught EXCEPTION   unused
#
# The Exit handler is entered with a0=return value.
#
# The Escape handler is entered with a0=new escape state in
# b6, must preserve all registers other than a0 and return
# with RET.
#
# The Error handler is entered with a0=>error block. Note that
# this may not be the address of the error buffer, the error
# buffer is used for dynamically generated error messages.
#
# The Event handler is entered with a0,a1,a2 holding the event
# parameters, must preserve all registers, and return with RET.
#
# The Unknown IRQ handler must preserve all registers, and
# return with RET.
#
# The Unknown CALL handler is entered a7=unknown ecall number
# and a0..a7 with the call parameters. It should return with RET.
#
# The Uncaught Exception handler must preserve all registers, and
# return with RET.
# --------------------------------------------------------------

# --------------------------------------------------------------
# ECall 15 - OS ERROR - invoke the current error handler
# --------------------------------------------------------------
# On entry:
#     the error block should follow the ecall instruction
# On exit:
#     this call does not return
# --------------------------------------------------------------

RISC-V Debugger

The RISC-V Debugger works the same as the other PiTubeDirect debuggers.

To use it, edit the config.txt file to select the debug kernel, and connect a Pi Serial cable to the serial port on the PiTubeDirect level shifter.

The Help command lists the available commands:

>> help
PiTubeDirect debugger
    cpu = RISCV
   base = 16
  width =  8 bits (1 byte)

Commands:
    info 
    help [ <command> ]
continue 
    step [ <num instructions> ]
    next 
    regs [ <name> [ <value> ]]
   traps 
     dis <start> [ <end> ]
    fill <start> <end> <data>
     crc <start> <end>
     mem <start> [ <end> ]
      rd <address>
      wr <address> <data>
   trace <interval> 
   clear <address> | <number>
    list 
  breakx <address> [ <mask> ]
  watchx <address> [ <mask> ]
  breakr <address> [ <mask> ]
  watchr <address> [ <mask> ]
  breakw <address> [ <mask> ]
  watchw <address> [ <mask> ]
    base 8 | 16
   width 8 | 16 | 32

RISC-V GNU Toolchain

To compile C programs to run on the RISC-V Coprocessor, you need to build the RISC-V GNU toolchain with some options specific to the RISC-V architecture used by PiTubeDirect:

Create a directory for the RISC-V GCC binaries:

sudo mkdir /opt/riscv
sudo chown $USER:$USER /opt/riscv

Clone the main RISC-V GNU repository:

git clone https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain

Install the prerequisites needed by the build:

sudo apt-get install autoconf automake autotools-dev curl python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build git cmake libglib2.0-dev

(these are the dependencies for Ubuntu, see the README for other operating systems)

Configure the target toolchain to match PiTubeDirect's Co Processor:

./configure --target=riscv32-unknown-elf --prefix=/opt/riscv --with-arch=rv32ima --with-abi=ilp32 

And run the build:

make

The build can take a very long time (hours on an older machine)

When it's complete, you should have the following binaries available:

$ ls -l /opt/riscv/bin/
total 493664
-rwxr-xr-x 1 dmb dmb   5677944 Jul 29 12:16 riscv32-unknown-elf-addr2line
-rwxr-xr-x 2 dmb dmb   5918048 Jul 29 12:16 riscv32-unknown-elf-ar
-rwxr-xr-x 2 dmb dmb   8469368 Jul 29 12:16 riscv32-unknown-elf-as
-rwxr-xr-x 2 dmb dmb   7014016 Jul 29 13:17 riscv32-unknown-elf-c++
-rwxr-xr-x 1 dmb dmb   5624176 Jul 29 12:16 riscv32-unknown-elf-c++filt
-rwxr-xr-x 1 dmb dmb   7007296 Jul 29 13:17 riscv32-unknown-elf-cpp
-rwxr-xr-x 1 dmb dmb    282992 Jul 29 12:16 riscv32-unknown-elf-elfedit
-rwxr-xr-x 2 dmb dmb   7014016 Jul 29 13:17 riscv32-unknown-elf-g++
-rwxr-xr-x 2 dmb dmb   6991176 Jul 29 13:17 riscv32-unknown-elf-gcc
-rwxr-xr-x 2 dmb dmb   6991176 Jul 29 13:17 riscv32-unknown-elf-gcc-12.2.0
-rwxr-xr-x 1 dmb dmb    167384 Jul 29 13:17 riscv32-unknown-elf-gcc-ar
-rwxr-xr-x 1 dmb dmb    167272 Jul 29 13:17 riscv32-unknown-elf-gcc-nm
-rwxr-xr-x 1 dmb dmb    167288 Jul 29 13:17 riscv32-unknown-elf-gcc-ranlib
-rwxr-xr-x 1 dmb dmb   4810480 Jul 29 13:17 riscv32-unknown-elf-gcov
-rwxr-xr-x 1 dmb dmb   3459136 Jul 29 13:17 riscv32-unknown-elf-gcov-dump
-rwxr-xr-x 1 dmb dmb   3727552 Jul 29 13:17 riscv32-unknown-elf-gcov-tool
-rwxr-xr-x 1 dmb dmb 151378264 Jul 29 13:39 riscv32-unknown-elf-gdb
-rwxr-xr-x 1 dmb dmb      4627 Jul 29 13:39 riscv32-unknown-elf-gdb-add-index
-rwxr-xr-x 1 dmb dmb   6373608 Jul 29 12:16 riscv32-unknown-elf-gprof
-rwxr-xr-x 4 dmb dmb   9062016 Jul 29 12:16 riscv32-unknown-elf-ld
-rwxr-xr-x 4 dmb dmb   9062016 Jul 29 12:16 riscv32-unknown-elf-ld.bfd
-rwxr-xr-x 1 dmb dmb 196518032 Jul 29 13:17 riscv32-unknown-elf-lto-dump
-rwxr-xr-x 2 dmb dmb   5746992 Jul 29 12:16 riscv32-unknown-elf-nm
-rwxr-xr-x 2 dmb dmb   6573960 Jul 29 12:16 riscv32-unknown-elf-objcopy
-rwxr-xr-x 2 dmb dmb   9496320 Jul 29 12:16 riscv32-unknown-elf-objdump
-rwxr-xr-x 2 dmb dmb   5918072 Jul 29 12:16 riscv32-unknown-elf-ranlib
-rwxr-xr-x 2 dmb dmb   4591992 Jul 29 12:16 riscv32-unknown-elf-readelf
-rwxr-xr-x 1 dmb dmb   9295384 Jul 29 13:39 riscv32-unknown-elf-run
-rwxr-xr-x 1 dmb dmb   5667760 Jul 29 12:16 riscv32-unknown-elf-size
-rwxr-xr-x 1 dmb dmb   5685432 Jul 29 12:16 riscv32-unknown-elf-strings
-rwxr-xr-x 2 dmb dmb   6573960 Jul 29 12:16 riscv32-unknown-elf-strip

To makes these commands available, add /opt/riscv/bin to your path:

export PATH=/opt/riscv/bin:$PATH

Building Sample Application

TODO:

RISC-V BBC Basic

An experimental port of Richard Russell's BBC Basic is available here: https://github.com/hoglet67/BBCSDL/releases

Running RVBASIC

This disc image is bootable, and will load RVBASIC to correct address, then *GO to the start address. It also contains a few example programs: SPHERE, CLOCKSP and TSTEST. The latter is Tom Seddon's file system test suite, which needs to be run from ADFS.

The !BOOT script executes the following commands to start RVBASIC:

*LOAD RVBASIC F80000
*GO F80000

It's not possible to use *RUN with DFS-based filesystems, because they only provide 18 bits of addressing, and 24 bits are needed to load at F80000.

RVBASIC program format

Richard Russell's BBC BASICs use a different line structure when storing BASIC programs in memory compared to Acorn's 6502 BBC BASICs.

The line structure with Richard Russell's BBC BASICs is:

<Line Length><Line Number LSB><Line Number MSB><0D>
...
<Line Length><Line Number LSB><Line Number MSB><0D>
<00> <FF> <FF>

The line structure with Acorn's BBC BASIC is:

<0D><Line Number MSB><Line Number LSB><Line Length>
...
<0D><Line Number MSB><Line Number LSB><Line Length>
<0D><FF?

See: https://beebwiki.mdfs.net/Program_format for more details.

In practice, this means that attempting to LOAD an Acorn 6502 BASIC program into RISC-V BASIC will result in a Bad Program error.

To work around this, you must first write the 6502 BASIC program out as a plain text file, using *SPOOL. On the 6502 BASIC:

LOAD "MYPROG"
*SPOOL MYPROGR
LIST
*SPOOL

Then on the RISCV BASIC:

*EXEC MYPROGR

In fact, you will find that LOAD also works with plain text files:

LOAD "MYPROGR"

RVBASIC Memory Map

The memory map when BBC Basic is running is:

&00000000-&0000FFFF : Basic String Buffer (64KB)
&00010000-&00010BFF : Basic Miscellaneous Buffers 3KB)
&00010C00           : default PAGE
&00780000           : default HIMEM (about half of available memory)
&00F00000           : maximum HIMEM
&00F00000-&00F3FFFF : C language heap (256KB, grows upwards)
&00F40000-&00F7FFFF : RISC-V stack (256KB, grows downwards)
&00F80000-&00FBFFFF : BBC Basic (256KB, 140KB used)
&00FC0000-&00FFFFDF : Tube ROM (256KB, 5K used)
&00FFFFE0-&00FFFFFF : Tube Registers

Here's the same info slightly more graphically (thanks to Charles, see #177):

RVBASIC Assembler

RISC-V BBC Basic includes a build-in assembker.

For details on the the RISC-V instruction set, see the RISC-V Reference by James Zhu

The following instructions are available:

  • all RV32I Base Integer instructions
  • all RV32M Multiply Extension instructions
  • all pseudo instructions

Here's a small example program that prints the characters 32 to 126 using OSWRCH:

>LIST
   10 DIM code% 256
   20 FOR I%=0 TO 2 STEP 2
   30 P%=code%
   40 [OPT I%
   50 align
   60 .test
   70 li a0, 32
   80 .loop
   90 li a7, &AC0004
  100 ecall
  110 addi a0, a0, 1
  120 li t0, 127
  130 bne a0, t0, loop
  140 ret
  150 ]
  160 NEXT
  170 CALL test
  180 PRINT
>RUN
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~

Rebuilding RVBASIC from source

To rebuild BBC Basic from source, there are two dependencies:

  • the RISC-V GNU toolchain (see above)
  • Stephen Harris's mmbutils (to build the .ssd disk image)

These tools need to be available on your PATH.

To build the disk image from source:

git clone https://github.com/hoglet67/BBCSDL.git
cd BBCSDL/console/riscv
make

This should result in:

riscv32-unknown-elf-as -I ../../include -march=rv32im_zicsr -mabi=ilp32 crt0.s -o crt0.o
riscv32-unknown-elf-gcc --specs=nano.specs -u _printf_float -Wall -I ../../include -march=rv32im_zicsr -mabi=ilp32 -c -Os ../../src/riscv_wrapper.c -o riscv_wrapper.o
riscv32-unknown-elf-gcc --specs=nano.specs -u _printf_float -Wall -I ../../include -march=rv32im_zicsr -mabi=ilp32 -c -Os ../../src/bbmain.c -o bbmain.o
riscv32-unknown-elf-gcc --specs=nano.specs -u _printf_float -Wall -I ../../include -march=rv32im_zicsr -mabi=ilp32 -c -Os ../../src/bbexec.c -o bbexec.o
riscv32-unknown-elf-gcc -Wno-array-bounds --specs=nano.specs -u _printf_float -Wall -I ../../include -march=rv32im_zicsr -mabi=ilp32 -c -Os ../../src/bbeval.c -o bbeval.o
riscv32-unknown-elf-as -I ../../include -march=rv32im_zicsr -mabi=ilp32 ../../../BBCSDL/src/bbdata_riscv.s -o bbdata.o
riscv32-unknown-elf-gcc --specs=nano.specs -u _printf_float -Wall -I ../../include -march=rv32im_zicsr -mabi=ilp32 -c -Os ../../src/bbasmb_riscv.c -o bbasmb.o
riscv32-unknown-elf-gcc crt0.o riscv_wrapper.o bbmain.o bbexec.o bbeval.o bbdata.o bbasmb.o --specs=nano.specs -u _printf_float -lm -nostartfiles -Tbbcbasic.ld  -Os -o bbcbasic
/opt/riscv/lib/gcc/riscv32-unknown-elf/12.2.0/../../../../riscv32-unknown-elf/bin/ld: warning: bbcbasic has a LOAD segment with RWX permissions
riscv32-unknown-elf-objcopy -O binary bbcbasic RVBASIC
rm -f rvbasic.ssd
beeb blank_ssd rvbasic.ssd
Blank rvbasic.ssd created
beeb title rvbasic.ssd "RISC-V BASIC"
rvbasic.ssd updated
beeb putfile rvbasic.ssd RVBASIC
beeb putfile rvbasic.ssd examples/*
beeb opt4 rvbasic.ssd 3
beeb info rvbasic.ssd
Disk title: RISC-V BASIC (1)  Disk size: &320 - 200K
Boot Option: 3 (EXEC)   File count: 5

Filename:  Lck Lo.add Ex.add Length Sct
$.TSTEST       000000 000000 0029D8 246
$.SPHERE       000000 000000 00014F 244
$.CLOCKSP      000000 000000 000BE0 238
$.!BOOT        000000 000000 000020 237
$.RVBASIC      000000 000000 0234CC 002

The result is a bootable single sided disk image (rvbasic.ssd)

Clone this wiki locally