tinysys part one: Overview
Software-first approach
Since I’m a software developer, tinysys was designed to be something that I can simply plug in and upload an ELF file onto, and not care about having it crash in a way that’s unrecoverable. Now that I think about it, I probably wanted a retro computer from the 80s but with a RISC-V processor and a multitasking OS, and with plenty of RAM.
To that end, I chose to build a very simple task system and a command line interpreter, together with some simple hardware interfaces and put it onto the ROM. The total executable size for the OS turned out to be around 56 kbytes, which sits nicely on a corner of the FPGA, in the boot ROM area. Added bonus: instant boot.
Once those decisions and code considerations were out of the way, rest was easy.
Choice of peripherals
Of course, the system had to have video output (since I’m a graphics programmer by trade so I want pixel access when possible) and audio wouldn’t hurt either, so I threw a couple chips for that purpose in my design.
You could sit at your PC with tinysys connected to it, and still do something useful over UART, though I really wanted some kind of USB keyboard support to be able to work untethered, therefore I added yet another chip for that purpose.
And finally, since I wanted to be able to interface with USB serial / Wi-Fi / Bluetooth in a hassle-free way, I chose to incorporate an ESP32-C6-WROOM module as my communications processor. That also helped me to be able to reboot the device (which mostly lives on the FPGA) while the serial connection is still alive, which comes in handy when you’re developing remotely.
Damnit Jim, I’m a software developer!
Well, that is all fine but at one point I had to sit down and start KiCad and learn about board design, search for chips that are simplest to interface and have good tolerances to varying conditions. I went with a two-layer board design, but I limited all components to one side of the board for ease of assembly. This comes in handy especially if you use a hot plate to put the board together and not have to care about the ‘other side’
I think I’ve gone through 14 or 15 iterations of the board until I arrived at the one you see above. Previous iterations had a MAX3420 chip for USB serial and I had to write a lot of driver code to work with it, which taught me a lot but also took up precious ROM space, so I did away with that and switched to the ESP32-C6-WROOM module instead (seen on the lower left-hand side in the above image).
The greatest number of components on the board go to the audio circuitry, as you can see in the following image:
I’ve probably done this part by hand a few dozen times and got surprisingly good results but still, hot plates save a lot of time from your day. The values of the resistors and capacitors were taken from a reference design for the CS4344-CZZR DAC chip used here. The audio is capped to 16 bit stereo input and is sent across to the audio DAC using an I2S bus, which then emits the stereo signal over the audio jack.
And the board needs an enclosure
Of course, I could not leave all those chips exposed to the elements, my fingers, or cat paws, so I decided to dust off my mechanical engineer hat and design an enclosure for the whole thing.
KiCad can export a .step file of the whole design, which comes in handy for building enclosures. If you can import this to a CAD software, you can then take measurements or follow the outlines of connectors or the board itself.
For that purpose, I used SOLIDWORKS Connected which is very reasonably priced for hobbyists. Once you import the files here, the design flow is a series of sketches, extruded or beveled to create the shell.
After this was done, I simply had to do a few thickness checks, make sure I had two separate and properly matching parts (top and bottom) and send it over to a manufacturer for a test 3D print. The result was good and very sturdy, so I included those files with the project files.
And now, the OS
The device at this point has a rudimentary OS, but it’s getting better. I’ve been adding a few quality-of-life improvements such as CTRL+C stopping tasks across all cores, which helps clean up after executables nicely, or being able to swap the OS to one loaded from sdcard instead of from ROM.
The OS currently has a task system that depends on a shared, non-cached memory region to talk across CPUs. It places all the task data there for each CPU’s task manager to find, and the OS core can control task execution by manipulating this memory area. That allowed me to do this in a lightweight fashion without having to care about cache coherency between the cores.
For the task manager itself, I’ve chosen to keep a simple list per core, which holds the states, maximum allowed execution lengths (more on this later) and addresses of each task. Each core gets a timer interrupt service installed at boot time, which will then switch to the next task, if that task is still marked for execution (i.e. not crashed or shutting down) The registers and PC for the previous task are saved to the same task list area and the register set that belongs to the next task are read here. For the ISR itself, all the registers are stashed into the user CSR area, so they don’t hit main (cached) memory. Overall, switching tasks on tinysys does not pollute or touch data caches apart from anything else that might need to be accessed from DRAM at that point.
Add to this support for ecall (that is, syscall in RISC-V land) which lets the ISR to handle system requests such as file access or text output, and you’ve got yourself an operating system that can boot, run, and switch between tasks.
Next time
If you haven’t seen it yet, go to this repo and take a look. It includes the OS code, SDK and samples, the System Verilog code, board or the enclosure files (oh and there’s a playable port of Doom to tinysys there as well)
I’m going to go into more detail about the OS internals in the next part, and be warned, there shall be some code which may not be pretty to look at. Until then, I yield the CPU to you.