Build a Boneless SOC
A little cpu and the things that you need on a FPGA
System On a Chip builder, available here
The systems is based on nmigen, a python suite to build RTL logic from python. It uses the Boneless-CPU as the primary CPU.
FPGA Board
Currently a TinyBX with an FTDI and some leds, should be portable to any nmigen-board.
SOC
The System On a Chip includes the processor, peripherals and interconnect.
Memory Layout
The working memory layout is as follows, it’s fairly standard. As always you have to make sure that the heap and the stack does not collide
After reading copious amounts of compiler literature on the net and waiting for the knowledge to boil down. Endevoring to not make C compiler was important, So wrapping python classes around known constructs and formulating larger programs was the way. So, strictly it is a compiler , but everything is written in Boneless-v3 assembly lists wrapped in python classes. The manual is here.
Classes are broken down as follows.
Firmware
Firmware is a wrapper for a complete program. It adds base features and variables to the following classes and has some metaclasses with flag testing to only add the strings, subroutines and variables that are actually used.
An example firmware is built like this:
it needs setup , prelude and instr that returns a list of ASM.
class MyThing(Firmware):
def setup(self):
# request some named registers in the starting window
self.w.req(["counter","stuff","test","narg"])
def prelude(self):
# this code is in the main after init and before the main loop
return []
def instr(self):
return ["SOME","ASSEMBLY","CODE","IN","A","LOOP"]
Simple Example firmware: Dump all readable chars and blink
Window
The Window in the Boneless-CPU is a weird beasty, it is a CPU register that points to a set of 8 linear word registers. You can put it anywhere in RAM, just make sure that you align it MOD 8. In the runtime presented here, the register window has a standard format. Two of the registers are reserved for top level references and the remaining SIX are used for storage and calling.
You can do ALU functions on any of the registers in the window. Having a set of 8 registers anywhere make it cool and weird at the same time.
SubR
The SubR is a class for making subroutines. It embraces the window as part of the calling structure and attempts to be overly clever with python metaclasses. The upshot is that you create an instace of the subroutine and when you call it is marked as used and gets added to the firmware.
And example of a SubR
class Wait(SubR):
def setup(self):
self.params = ["value"]
self.locals = ["counter"]
def instr(self):
w = self.w
ll = LocalLabels()
return [
MOV(w.counter,w.value),
ll("wait"),
SUBI(w.counter, w.counter, 1),
CMPI(w.counter, 0),
BNE(ll.wait),
]
To use this subroutine you have create an instance of the class and then call it in your code
waitr = Wait()
# then by calling it the asm is returned
instr = waitr(R0)
The trick that happens here is that if it gets called the SubR is marked and included in the firmware.
External Registers
Attached to the cpu is a PeripheralCollection, this adds CSR peripherals to the cpu and creates a register map that can be used within python as named attributes. Most of this code has been stolen from lamda-soc with the Wishbone ripped out. It is a little bloaty with the muxing on the inside but you can add a peripheral and it will autoname and attach to the CSR bus.
From fwtest
# Make the peripheral
serial = AsyncSerialPeripheral(pins=uart, divisor=uart_divisor)
# Attach it to the CPU
cpu.add_peripheral(serial)
# A countdown timer with interrupt
timer = TimerPeripheral(32)
cpu.add_peripheral(timer)
This will create a heirachy of names that can be referenced in code.
UART
- reg.serial.divisor 0
- reg.serial.rx.data 1
- reg.serial.rx.rdy 2
- reg.serial.rx.err 3
- reg.serial.tx.data 4
- reg.serial.tx.rdy 5
- reg.serial.ev.status 8
- reg.serial.ev.pending 9
- reg.serial.ev.enable 10
Timer
- reg.timer.reload_0 16
- reg.timer.reload_1 17
- reg.timer.en 18
- reg.timer.ctr_0 19
- reg.timer.ctr_1 20
- reg.timer.ev.status 24
- reg.timer.ev.pending 25
- reg.timer.ev.enable 26
The numbering is dynamic, so by using the name it will reference the correct CSR. This python object is attched to firmwares , subroutines , inlines and data objects, so you can refer to peripheral addresses even if they change. The interface needs some work so relative addressing works as well.
Globals
Global variables are another one of the shared types that gets attached when you build the firmware , they are named registers that get a fixed allocation when the Firmware is built.
Stringer
The stringer is another construct that is attached Firmware,SubR,Inline and data objects , they are byte encoded (16BIT!!!) and the references are absolute memory. you can declare other stringers they just get another id.
Strings are all built but only included in the firmware if they are called.
Some Scaffolding
It’s all a bit messy
So far I have built some infrastructure around WQ’s most Awesome processor, most of the code has been stolen, liberated or cutpasta. I have managed to get a fantasy processor running on an FPGA. To get it up and running there was some infrastructre that was needed.
Hexloader
The hexloader is a program burnt into the FPGA image that allows new programs to be uploaded. It waits for chars from the serial port in the form of 0000-FFFF , takes length,data,checksum and boots into the new program.
If the is any error in the data it prints a “!” and “F” for a checksum fail, otherwise it just boots into the loaded program
Library
There is some functions already defined as SubR it’s a bit haphazard but there are a number of usefull constructs.
This is set of functions to read and write to the serial port, it creates instances of all the subroutines and then if they are used in code() then they get included.
- readword = ReadWord()
- writeword = WriteWord()
- read = Read()
- write = Write()
- writestring = WriteString()
- writeHex = WriteHex()
- readHex = ReadHex()
- readWait = ReadWait()
- cr = CR()
- sp = SP()
- colon = COLON()
- core = CoreDump()