I built an OLIMEX rvpc kit - this is a small single board computer with a 48MHz 32-bit RISC-V processor on it, 2K of RAM and 16K of flash. The ch32v003 processor used on the board costs less than $0.10, and the entire kit costs 1 euro. Shipping is can be quite a bit, depending on where you buy it, so you may want to buy more than one.
The rvpc bit-bangs VGA video out using GPIO pins on the processor, and has a PS/2 keyboard interface. So you’ll need a VGA monitor, a keyboard with a PS/2 connector, and a 5V power supply to run your board. It also has a power LED and a small buzzer for sound. Programming is done with a 2-pin header, but my programming device hasn’t arrived yet, so I decided to initially write code using the machine code monitor which comes pre-loaded in the flash memory on the processor.
The design is very reminiscent of the Sinclair ZX-80
Although I haven’t soldered in quite a while, I was able to put together the kit in a couple hours of careful work. The trickiest part for me was soldering the surface mount processor on the board, but thankfully it has a coarse pitch and only 8 pins.
When you power up your assembled kit, the speaker chirps, the power LED comes on, and the following screen is displayed:
As you can see, you are booted into the machine code monitor, RVMON. I have the programming dongle on order and it will get here in a couple weeks, but I didn’t want to wait for it to arrive to write code, so I will write machine code with the monitor.
From the listed addresses, it appears that the flash is mapped to 0x00000000
and the RAM to 0x20000000
, so a simplified memory map would look like this:
0x00000000 - 0x00003FFF - 16K of user application flash
0x20000000 - 0x20000800 - 2K of RAM
Actual memory map here (page 2), confirms this.
RVMON reports the following routine addresses:
reset 0x0000061c
buzz_ok 0x00000496
buzz_err 0x000009b8
sandbox 0x200000cc
using the 'g' command we can call these routines:
00000496G
-> results in the buzzer playing the ok sound.
The name sandbox implies we can put things there. The first 204 bytes (0xCC
) must be used by the monitor. That leaves us 1844 bytes of RAM for us. The monitor does not offer options for viewing registers and such, so we will store values in memory where they can be examined later. Let's write a simple program to store '4' in a memory location to start. Since the G
command returns from the monitor routines, I assume doing a ret
(pseudo op for jr ra
) at the end of our code will return us to the monitor. We will need to use the RISC-V instruction set supported by the ch32v003: RV32EC_Zicsr
Here’s my source file, ch32v003.s:
.section .bss
result:
.word 0x00
.section .text
.globl _start
_start:
la a0, result
li a1, 4
sw a1, 0(a0)
ret
As is typical in assembly language, many of these are pseudo-operations (la, li, ret
) which expand into different underlying machine code depending upon their arguments. We are going to need to get to the machine code level and punch in the actual hexadecimal bytes for our program in the monitor. We will then jump to our code, and when it returns, we should be able to dump memory we stored the value in and see our results. Let's store our data at address 0x20000100
(above the storage used by the monitor) and our code at address 0x20000200
. To do this, we will use a linker script to map our ELF section names such as bss
and text
into physical addresses.
Linker script, ch32v003.ld:
SECTIONS
{
. = 0x20000200;
.text : { *(.text) }
. = 0x20000100;
.data : { *(.data) }
.bss : { *(.bss) }
}
As I haven't memorized RISC-V opcodes and bitfields for arguments, let's use clang
to generate the machine code from the assembly code. Note the arguments to specify the instruction set and ABI for the ch32v003 processor:
clang -c --target=riscv32 -march=rv32ec_zicsr -mabi=ilp32e ch32v003.s -o ch32v003.o
Now, we need to link our code and map it to the specific addresses we want:
ld.lld ch32v003.o -T ch32v003.ld -o ch32v003.x
Finally, let's look at the results of our work with llvm-objdump
:
llvm-objdump -s -M no-aliases -d ch32v003.x
ch32v003.x: file format elf32-littleriscv
Contents of section .text:
20000200 17050000 130505f0 91450cc1 8280 .........E....
Contents of section .bss:
<skipping contents of bss section at [20000100, 20000104)>
Contents of section .riscv.attributes:
0000 41270000 00726973 63760001 1d000000 A'...riscv......
0010 05727633 32653270 305f6332 70305f7a .rv32e2p0_c2p0_z
0020 69637372 32703000 icsr2p0.
Contents of section .comment:
0000 4c696e6b 65723a20 5562756e 7475204c Linker: Ubuntu L
0010 4c442031 382e312e 3300 LD 18.1.3.
Contents of section .symtab:
0000 00000000 00000000 00000000 00000000 ................
0010 01000000 00010020 00000000 00000200 ....... ........
0020 08000000 00010020 00000000 00000200 ....... ........
0030 0d000000 00020020 00000000 00000100 ....... ........
0040 19000000 00020020 00000000 00000100 ....... ........
0050 1e000000 00000000 00000000 0000f1ff ................
0060 23000000 00020020 00000000 10000100 #...... ........
Contents of section .shstrtab:
0000 002e7465 7874002e 62737300 2e726973 ..text..bss..ris
0010 63762e61 74747269 62757465 73002e63 cv.attributes..c
0020 6f6d6d65 6e74002e 73796d74 6162002e omment..symtab..
0030 73687374 72746162 002e7374 72746162 shstrtab..strtab
0040 00 .
Contents of section .strtab:
0000 00726573 756c7400 24642e30 002e4c70 .result.$d.0..Lp
0010 6372656c 5f686930 0024782e 31002464 crel_hi0.$x.1.$d
0020 2e32005f 73746172 7400 .2._start.
Disassembly of section .text:
20000200 <_start>:
20000200: 17 05 00 00 auipc a0, 0x0
20000204: 13 05 05 f0 addi a0, a0, -0x100
20000208: 91 45 c.li a1, 0x4
2000020a: 0c c1 c.sw a1, 0x0(a0)
2000020c: 82 80 c.jr ra
We can see the bss section is 4 bytes, at the addresses we expect - 0x20000100
, 0x20000104
and we can see the symbol _start
is at the address we expect: 0x20000200
, and the PC-relative load of the address of result
is -0x100
bytes from the start of the text segment:
20000204: 13 05 05 f0 addi a0, a0, -0x100
Everything looks good! We need to use RVMON to load the following machine code (the contents of the text
section) into memory starting at 0x20000200
:
17050000 130505f0 91450cc1 8280
Let's use the following commands to load our code into RAM in RVMON:
20000200:17 05 00 00
20000204:13 05 05 f0
20000208:91 45 0c c1
2000020C:82 80
Next, check to make sure that 0x20000100
contains 0 as we expect:
20000100+
and then run our code with:
20000200G
and finally check our results:
20000100+
As you can see, location 0x20000100
now contains 0x4
, whereas before, it contained 0x0
. Our program worked!
OK, now let’s do something more complicated. Here is a routine to divide a 32-bit unsigned integer by 3 on the RV32E architecture, which lacks hardware for multiplication and division. The approach taken here is to multiply by the reciprocal (1/3 decimal = 0.01010101…. binary - see figure 10-8 of Hacker’s Delight and my How Many More Times? substack post). Since we don’t have a multiply instruction, we will use the shift-and-add approach. The input number will be stored as a 4-byte little endian number at the address input, and the result will be placed at the address output as a 4-byte little endian number:
.section .bss
input: .word 0x00
output: .word 0x00
.section .text
.globl _start
_start:
la a0, input
lw a1, 0(a0) # a1: n
srli a2, a1, 2 # a2: q = n >> 2
srli a3, a1, 4 # a3: n >> 4
add a2, a3, a2 # a2: q = (n >> 2) + (n >> 4)
srli a3, a2, 4 # a3: q >> 4
add a2, a3, a2 # a2: q = q + (q >> 4)
srli a3, a2, 8 # a3: q >> 8
add a2, a3, a2 # a2: q = q + (q >> 8)
srli a3, a2, 16 # a3: q >> 16
add a2, a3, a2 # a2: q = q + (q >> 16)
slli a4, a2, 1 # a4: q * 2
add a4, a4, a2 # a4: q * 3 (q * 2 + q)
sub a4, a1, a4 # a4: r = n - q * 3
addi a3, a4, 5 # a3: r + 5
slli a4, a4, 2 # a4: r << 2
add a3, a4, a3 # a3: (r + 5) + (r << 2)
srli a3, a3, 4 # a3: ((r + 5) + (r << 2)) >> 4
add a3, a3, a2 # a3: q + (((r + 5) + (r << 2)) >> 4)
sw a3, 4(a0) # store to output
ret
Note: I’ve optimized this to 16 instructions with 3 registers used as the div3 routine in my rvint package.
Let’s assemble and link the code, using the same linker script as last time:
clang -c --target=riscv32 -march=rv32ec_zicsr -mabi=ilp32e div3.s -o div3.o
ld.lld div3.o -T ch32v003.ld -o div3.x
And dump the object file to get the contents of the text
section:
llvm-objdump -s -M no-aliases -d div3.x
div3.x: file format elf32-littleriscv
Contents of section .text:
20000200 17050000 130505f0 0c4113d6 250093d6 .........A..%...
20000210 45003696 93564600 36969356 86003696 E.6..VF.6..V..6.
20000220 93560601 36961317 16003297 3387e540 .V..6.....2.3..@
20000230 93065700 0a07ba96 9182b296 54c18280 ..W.........T...
Contents of section .bss:
<skipping contents of bss section at [20000100, 20000108)>
Contents of section .riscv.attributes:
0000 41270000 00726973 63760001 1d000000 A'...riscv......
0010 05727633 32653270 305f6332 70305f7a .rv32e2p0_c2p0_z
0020 69637372 32703000 icsr2p0.
Contents of section .comment:
0000 4c696e6b 65723a20 5562756e 7475204c Linker: Ubuntu L
0010 4c442031 382e312e 3300 LD 18.1.3.
Contents of section .symtab:
0000 00000000 00000000 00000000 00000000 ................
0010 01000000 00010020 00000000 00000200 ....... ........
0020 07000000 00010020 00000000 00000200 ....... ........
0030 0c000000 04010020 00000000 00000200 ....... ........
0040 13000000 00020020 00000000 00000100 ....... ........
0050 1f000000 00020020 00000000 00000100 ....... ........
0060 24000000 00000000 00000000 0000f1ff $...............
0070 29000000 00020020 00000000 10000100 )...... ........
Contents of section .shstrtab:
0000 002e7465 7874002e 62737300 2e726973 ..text..bss..ris
0010 63762e61 74747269 62757465 73002e63 cv.attributes..c
0020 6f6d6d65 6e74002e 73796d74 6162002e omment..symtab..
0030 73687374 72746162 002e7374 72746162 shstrtab..strtab
0040 00 .
Contents of section .strtab:
0000 00696e70 75740024 642e3000 6f757470 .input.$d.0.outp
0010 7574002e 4c706372 656c5f68 69300024 ut..Lpcrel_hi0.$
0020 782e3100 24642e32 005f7374 61727400 x.1.$d.2._start.
Disassembly of section .text:
20000200 <_start>:
20000200: 17 05 00 00 auipc a0, 0x0
20000204: 13 05 05 f0 addi a0, a0, -0x100
20000208: 0c 41 c.lw a1, 0x0(a0)
2000020a: 13 d6 25 00 srli a2, a1, 0x2
2000020e: 93 d6 45 00 srli a3, a1, 0x4
20000212: 36 96 c.add a2, a3
20000214: 93 56 46 00 srli a3, a2, 0x4
20000218: 36 96 c.add a2, a3
2000021a: 93 56 86 00 srli a3, a2, 0x8
2000021e: 36 96 c.add a2, a3
20000220: 93 56 06 01 srli a3, a2, 0x10
20000224: 36 96 c.add a2, a3
20000226: 13 17 16 00 slli a4, a2, 0x1
2000022a: 32 97 c.add a4, a2
2000022c: 33 87 e5 40 sub a4, a1, a4
20000230: 93 06 57 00 addi a3, a4, 0x5
20000234: 0a 07 c.slli a4, 0x2
20000236: ba 96 c.add a3, a4
20000238: 91 82 c.srli a3, 0x4
2000023a: b2 96 c.add a3, a2
2000023c: 54 c1 c.sw a3, 0x4(a0)
2000023e: 82 80 c.jr ra
Use the :
command in RVMON to enter the contents of the text section, 4 bytes at a time starting at 0x20000200
:
17050000 130505f0 0c4113d6 250093d6
45003696 93564600 36969356 86003696
93560601 36961317 16003297 3387e540
93065700 0a07ba96 9182b296 54c18280
It’s a lot of typing, but remember people used to toggle in programs with front panel switches, we have it easy. You may want to use the +
command to check your work.
Then use the :
command to enter a number to divide, and place it in the input memory location at 0x20000100
, as a 32-bit little endian number. In this case, I’m going to divide 35234 decimal (0x000089a2 hex), and then call our division routine and print out the results:
20000100:A2 89 00 00
20000200G
20000100+
You should see a result of 0x00002DE0 hexadecimal, which is 11744. (Integer division of 35234/3=11744).
Enjoy hacking machine code on your rvpc!