Turning a CPU into a SoC

In my previous blog I wrote about a one page CPU in SpinalHDL. But just a CPU is a bit boring, time to turn it into a full System on Chip by adding memory, peripherals and even a UART bootloader. One of the great things of Spinal is the existing libraries, making adding some peripherals a lot less work compared to traditional hardware design languages.

So, first things first, adding some instruction and data memory. Spinal supports generating memory with just a single line of code. So to generate 1Kword of RAM and 1K of instruction memory, the needed code is just this

val RAM = Mem(Bits(8 bits),wordCount = 1024)
val ROM = Mem(Bits(16 bits),wordCount = 1024)

Of course, reading and writing to memory is also required, luckily it’s fairly easy to do so as well. Loading a file in the ROM can easily be done, as the Hextools library support reading in an Intel hex file. The code to read and write from memory is straightforward as well, and some more info can be found the related Spinal website.

//default program
HexTools.initRam(ROM, onChipRamHexFile, 0x0)

RAM.write(
	enable  = areaCPU.CPU.io.we_ram,
	address = areaCPU.CPU.io.addr_ram.asUInt,
	data    = areaCPU.CPU.io.dout_ram
)

ROM.write(
	enable  = progDataValid,
	address = progCounter.resized,
	data    = progData.asBits
)

val readValid = !areaCPU.CPU.io.we_ram
CPU.io.din_ram := RAM.readSync(
	enable = readValid,
	address = CPU.io.addr_ram.asUInt
)

CPU.io.din_instr := ROM.readSync(
	address = CPU.io.addr_instr.asUInt.resized
)

All peripherals are memory mapped, for now, all memory above address 896 is reserved for peripherals. The UART peripheral is made using the existing UART library in Spinal and looks like this:

val uart_readValid, uart_read, uart_writeValid, uart_write, uart_writeDone = Reg(Bits(8 bits)) init("00000000")
val uartCtrlConfig = UartCtrlGenerics(
  dataWidthMax = 8,
  clockDividerWidth = 16,
  preSamplingSize = 1,
  samplingSize = 5,
  postSamplingSize = 2)
val uartCtrl = new UartCtrl(uartCtrlConfig)
uartCtrl.io.config.clockDivider := 156  //12Mhz clock, 9600BAUD
uartCtrl.io.config.frame.dataLength := 7  //8 bits
uartCtrl.io.config.frame.parity := UartParityType.NONE
uartCtrl.io.config.frame.stop := UartStopType.ONE

uartCtrl.io.write.valid := uart_writeValid.asBool
uartCtrl.io.write.payload := uart_write

uartCtrl.io.uart <> io.uart

uart_readValid := uartCtrl.io.read.valid.asBits.resized
uart_read := uartCtrl.io.read.payload

io.din_ram := 0

when(uartCtrl.io.write.ready){
  uart_writeDone := 1
}

when(io.we_ram){
  when(io.addr_ram === "d907"){
    uart_writeDone := io.dout_ram
  }.elsewhen(io.addr_ram === "d908"){
    uart_write := io.dout_ram
  }.elsewhen(io.addr_ram === "d909"){
    uart_writeValid := io.dout_ram
  }
}.otherwise{          
  when(io.addr_ram === "d905") {
    io.din_ram := uart_readValid
  }.elsewhen(io.addr_ram === "d906"){
    io.din_ram := uart_read
  }.elsewhen(io.addr_ram === "d907"){
    io.din_ram := uart_writeDone
  }

Most of it is setting up the UART library and decoding reads/writes from memory to the UART registers. The other 2 peripherals, a Timer and GPIO are setup in a similar way. In the end, 2 pages of code, 132 lines, are needed for a GPIO, Timer and a UART peripheral, a lot more compact then a traditional HDL. The full code for the SoC can be found on my github.


So, what do you think ?