SPORK!
Boneless SOC builder for nmigen

Mar 2021

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.

uartIO

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()