Easily build your own SoC

This tutorial takes you through how to easily build your own SoC. We use the hardware IP library provided by OpenTitan because of its high maturity and because I am familiar with the codebase. Our initial SoC takes lowRISC's Ibex core and connects it up to a TileLink bus. This bus connects to an RAM and a UART device. The RAM contains code and data, while the UART is for serial communication.

Block diagram showing the Ibex CPU connected to the RAM and the UART.

Create a git repository

In this tutorial we use Git for our repository management. I'm expecting you to start with an empty repository, but you can adapt the instructions for an existing repository as well. Instead of manually writing the .gitignore file from scratch we can pull it from the SoC easy build repository that I prepared for this tutorial:

mkdir mysoc
cd mysoc
git init
echo "# My SoC" > README.md
wget https://codeberg.org/marno/soc-easy-build/raw/branch/main/.gitignore
git add README.md .gitignore
git commit -m "My first commit"

Animation showing the git comands being executed.

After each step, feel free to add your files using git add and committing them. I won't repeat those instructions through this tutorial. You can also push to a remote if you would like to back your repository up, for example:

git remote add origin [email protected]:marno/soc-easy-build.git
git push

Vendoring

There are many ways to include external sources in your project. For example, you can just copy the files or you can use Git submodules. In this tutorial we use vendoring, which is a way of copying files into your repository while still keeping track of where they came from. This flow allows you to easily pull in changes from upstream and also has a useful feature of being able to automatically patch changes that you might need to make. It's also the same flow that OpenTitan uses to pull in external sources, and it's nice to align with that. Let's copy the vendoring utility to your new repository and make it executable:

wget -P util https://github.com/lowRISC/opentitan/raw/205497fb5112fd99105be008913bb1cc71652a6e/util/vendor.py
chmod +x util/vendor.py

To run this script, we need to set up our Python environment:

wget https://codeberg.org/marno/soc-easy-build/raw/branch/main/python-requirements.txt
python -m venv .venv
source .venv/bin/activate
pip install -r python-requirements.txt

Animation showing setting up your Python environment.

Getting the IP

First let's vendor in our CPU, which is Ibex. To do this we need to tell our vendor script where to find it which I provide for you. Have a look in lowrisc_ibex.vendor.hjson to see what repository it points to.

wget -P vendor https://codeberg.org/marno/soc-easy-build/raw/branch/main/vendor/lowrisc_ibex.vendor.hjson
cat vendor/lowrisc_ibex.vendor.hjson
git add --all
git commit -m "Commit before vendoring Ibex"
util/vendor.py --commit --update vendor/lowrisc_ibex.vendor.hjson

Animation showing importing the Ibex CPU.

Then let's get the rest of our IP, this includes the TileLink bus, UART and UART DPI. We need a few patch files because our SoC is much simpler than that of OpenTitan. Luckily we can gather these patch files from other projects.

wget -P vendor https://codeberg.org/marno/soc-easy-build/raw/branch/main/vendor/lowrisc_ip.vendor.hjson
cat vendor/lowrisc_ip.vendor.hjson
wget -P vendor/patches/lowrisc_ip/reggen https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/reggen/0001-Remove-Integrity-From-Reg-Top.patch
wget -P vendor/patches/lowrisc_ip/tlgen https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/tlgen/0001-Crossbar-Core-Portability.patch
wget -P vendor/patches/lowrisc_ip/tlul https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/tlul/0001-Remove-LC-Control-As-TLUL-Dependency.patch
wget -P vendor/patches/lowrisc_ip/tlul https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/tlul/0002-Tighten-TLUL-Host-Adapter-Prim-Dependency.patch
wget -P vendor/patches/lowrisc_ip/tlul https://codeberg.org/marno/soc-easy-build/raw/branch/main/vendor/patches/lowrisc_ip/tlul/0003-Remove-Integrity.patch
wget -P vendor/patches/lowrisc_ip/uart https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/uart/0001-Remove-Alerts-And-Integrity.patch
wget -P vendor/patches/lowrisc_ip/uartdpi https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/uartdpi/0001-Ignore-Write-Errors.patch
wget -P vendor/patches/lowrisc_ip/uartdpi https://github.com/lowRISC/sonata-system/raw/6e54069435f1279dce83768ae23f00e05568eabb/vendor/patches/lowrisc_ip/uartdpi/0002-Simulator-Exit-Condition.patch
wget -P vendor/patches/lowrisc_ip/prim https://codeberg.org/marno/soc-easy-build/raw/branch/main/vendor/patches/lowrisc_ip/prim/0001-Alerts-Removed.patch
git add --all
git commit -m "Commit before vendoring IP"
util/vendor.py --commit --update vendor/lowrisc_ip.vendor.hjson

Have a look in lowrisc_ip.vendor.hjson to see that it points to the OpenTitan repository. Also, look in the vendor/lowrisc_ip/ip directory and you'll find folders for the TileLink bus and the UART.

Animation showing importing the OpenTitan IP.

Generating the crossbar

Since the memory map is usually different per SoC, we'll need to specify our address map and generate the RTL for it. We're specifying RAM to be located at 0x0010_0000 and UART at 0x8000_0000.

wget -P util https://codeberg.org/marno/soc-easy-build/raw/branch/main/util/xbar_main.hjson
mkdir -p rtl/autogen
vendor/lowrisc_ip/util/tlgen.py -t util/xbar_main.hjson -o rtl/autogen
rm -r rtl/autogen/dv
mv rtl/autogen/rtl/autogen/* rtl/autogen

Opening up rtl/autogen/tl_main_pkg.sv you can check that the UART and RAM are defined as devices and the Ibex is defined as a host.

Animation showing the generation of our crossbar.

Connecting everything up

Now we create a module that instantiates our Ibex core, the generated crossbar and a UART. Most of this file is about connecting everything up. Have a look inside the file and find the following blocks: ibex_top_tracing, uart, prim_ram_2p and xbar_main.

wget -P rtl https://codeberg.org/marno/soc-easy-build/raw/branch/main/rtl/soc_mod.sv

Making our simulator top-level

We would really like to simulate our design and we can do that by creating a wrapper that Verilator can understand. The SystemVerilog file instantiates our SoC module and the UART DPI. The C++ file tells Verilator how to run our design including where the memory is to load our ELF file in. Don't feel like you need to understand all of these files.

wget -P dv https://codeberg.org/marno/soc-easy-build/raw/branch/main/dv/top_verilator.sv
wget -P dv https://codeberg.org/marno/soc-easy-build/raw/branch/main/dv/top_verilator.cc

Building our SoC using FuseSoC

FuseSoC is an amazing build system for open-source RTL. To specify what FuseSoC should do you need a core file. To build the simulator, you'll need to install Verilator. Try the following commands:

wget https://codeberg.org/marno/soc-easy-build/raw/branch/main/soc.core
fusesoc --cores-root=. run --target=sim --tool=verilator --setup --build marno:soc:main

This should end by saying "Building simulation model" and then returning without an error.

Animation showing the getting of our top module, the simulator and building it all.

Running some code

Now let's test our SoC by writing hello world from our Ibex over the bus to the UART. First of all get the RISC-V toolchain from here. Then compile and run the example hello world using the following commands:

wget -P sw https://codeberg.org/marno/soc-easy-build/raw/branch/main/sw/boot.S
wget -P sw https://codeberg.org/marno/soc-easy-build/raw/branch/main/sw/hello_world.c
wget -P sw https://codeberg.org/marno/soc-easy-build/raw/branch/main/sw/link.ld
wget -P sw https://codeberg.org/marno/soc-easy-build/raw/branch/main/sw/build.sh
cat sw/build.sh
chmod +x sw/build.sh
sw/build.sh
build/marno_soc_main_0/sim-verilator/Vtop_verilator -t -E hello_world.elf

After a little while press ctrl + c to exit the simulator. Then let's inspect our UART output:

cat uart0.log

If everything is successful, your log should say "Hello World!" I've created an example repository so you can double check that everything is still working correctly.

Animation showing getting our software and seeing that it outputs hello world over the UART.

Now what?

Congratulations you can now build your own SoC! You can use it as it is and write software that outputs over the UART. For more inspiration: OpenTitan is a full chip design that contains even more IP, including security blocks AES, HMAC and an asymmetric crypto accelerator. There are many more blocks that you can make use of!

Block diagram showing the OpenTitan Earl Grey chip.

An example of a system you can build using this technique is Sonata. This has been the inspiration for this tutorial. Sonata system is an FPGA-based system that integrates many peripherals such as SPI, I2C, GPIO, ADC, USB device, etc.

Block diagram showing the Sonata system.

I hope you've found this tutorial useful and thanks for trying it out!