Building Your Own SoC#
This tutorial will walk you through the process of building an ASIC containing one PicoRV32 RISC-V CPU core and 2 kilobytes of SRAM, on an open-source 130nm Skywater process node, with SiliconCompiler’s remote workflow:
We will walk through the process of downloading the design files and writing a build script, but for your reference, you can find complete example designs which reflect the contents of this tutorial in the public SiliconCompiler repository. The first part of the tutorial will cover building the CPU core without RAM, and the second part will describe how to add an SRAM block.
See the Installation section for information on how to install SiliconCompiler, and the Remote Processing section for instructions on setting up the remote workflow.
Download PicoRV32 Verilog Code#
The heart of any digital design is its HDL code, typically written in a language such as Verilog or VHDL. High-level synthesis languages are gaining in popularity, but most of them still output their final design sources in a traditional HDL such as Verilog.
PicoRV32 is an open-source implementation of a small RISC-V CPU core, the sort you might find in a low-power microcontroller. Its source code, license, and various tooling can be found in its GitHub repository.
Build the PicoRV32 Core using SiliconCompiler#
Before we add the complexity of a RAM macro block, let’s build the core design using the open-source Skywater 130 PDK.
Copy the following build script into the same directory which you copied picorv32.v
into:
#!/usr/bin/env python3
import siliconcompiler
from siliconcompiler.targets import skywater130_demo
def rtl2gds(target=skywater130_demo):
'''RTL2GDS flow'''
# CREATE OBJECT
chip = siliconcompiler.Chip('picorv32')
# SETUP
chip.use(target)
chip.register_source(name='picorv32',
path='git+https://github.com/YosysHQ/picorv32.git',
ref='c0acaebf0d50afc6e4d15ea9973b60f5f4d03c42')
chip.input('picorv32.v', package='picorv32')
chip.set('option', 'quiet', True)
chip.set('option', 'remote', False)
chip.clock('clk', period=25)
# RUN
chip.run()
# ANALYZE
chip.summary()
return chip
if __name__ == '__main__':
rtl2gds()
Note in the code snippet above that remote is set to False
. If this is set to True
, this means it is set up for Remote processing, and if you run this example as a Python script, it should take approximately 20 minutes to run if the servers are not too busy.
We have not added a RAM macro yet, but this script will build the CPU core with I/O signals placed pseudo-randomly around the edges of the die area.
Once the job finishes, you should receive a screenshot of your final design, and a report containing metrics related to the build in build/picorv32/job0/report.html
. SiliconCompiler will try to open the file after the job completes, but it may not be able to do so if you are running in a headless environment.
For the full GDS-II results and intermediate build artifacts, you can run the build locally. See the Local Run section for more information.
Adding an SRAM block#
A CPU core is not very useful without any memory. Indeed, a real system-on-chip would need quite a few supporting IP blocks to be useful in the real world. At the very least, you would want a SPI interface for communicating with external non-volatile memory, a UART to get data in and out of the core, a debugging interface, and a small on-die cache.
In this tutorial, we’ll take the first step by adding a small (2 kilobyte) SRAM block and wiring it to the CPU’s memory interface. This will teach you how to import and place a hard IP block in your design.
The open-source Skywater130 PDK does not currently include foundry-published memory macros. Instead, they have a set of OpenRAM configurations which are blessed by the maintainers. You can use those configurations to generate RAM macros from scratch if you are willing to install the OpenRAM utility, or you can download pre-built files.
We will use the sky130_sram_2kbyte_1rw1r_32x512_8 block in this example.
Create a Python script called sky130_sram_2k.py
to describe the RAM macro in a format which can be imported by SiliconCompiler:
import os
import siliconcompiler
def setup(stackup=None):
# Core values.
design = 'sky130_sram_2k'
if stackup is None:
raise RuntimeError('stackup cannot be None')
# Create library Chip object.
lib = siliconcompiler.Library(design)
lib.register_source('vlsida',
'git+https://github.com/VLSIDA/sky130_sram_macros',
'c2333394e0b0b9d9d71185678a8d8087715d5e3b')
lib.set('output', stackup, 'gds',
'sky130_sram_2kbyte_1rw1r_32x512_8/sky130_sram_2kbyte_1rw1r_32x512_8.gds',
package='vlsida')
lib.set('output', stackup, 'lef',
'sky130_sram_2kbyte_1rw1r_32x512_8/sky130_sram_2kbyte_1rw1r_32x512_8.lef',
package='vlsida')
rootdir = os.path.dirname(__file__)
lib.set('output', 'blackbox', 'verilog', os.path.join(rootdir, "sky130_sram_2k.bb.v"))
# Ensure this file gets uploaded to remote
lib.set('output', 'blackbox', 'verilog', True, field='copy')
return lib
You will also need a “blackbox” Verilog file to assure the synthesis tools that the RAM module exists: you can call this file sky130_sram_2k.bb.v
. You don’t need a full hardware description of the RAM block to generate an ASIC design, but the open-source workflow needs some basic information about the module:
(* blackbox *)
module sky130_sram_2kbyte_1rw1r_32x512_8 (
`ifdef USE_POWER_PINS
vccd1,
vssd1,
`endif
// Port 0: RW
input clk0,
input csb0,
input web0,
input [3:0] wmask0,
input [8:0] addr0,
input [31:0] din0,
output reg [31:0] dout0,
// Port 1: R
input clk1,
input csb1,
input [8:0] addr1,
output reg [31:0] dout1
);
endmodule
Next, you need to create a top-level Verilog module containing one picorv32
CPU core, one sky130_sram_2k
memory, and signal wiring to connect their I/O ports together.
Note that for the sake of brevity, this module does not include some optional parameters and signals:
`timescale 1 ns / 1 ps
module picorv32_top #(
parameter [0:0] ENABLE_COUNTERS = 1,
parameter [0:0] ENABLE_COUNTERS64 = 1,
parameter [0:0] ENABLE_REGS_16_31 = 1,
parameter [0:0] ENABLE_REGS_DUALPORT = 1,
parameter [0:0] LATCHED_MEM_RDATA = 0,
parameter [0:0] TWO_STAGE_SHIFT = 1,
parameter [0:0] BARREL_SHIFTER = 0,
parameter [0:0] TWO_CYCLE_COMPARE = 0,
parameter [0:0] TWO_CYCLE_ALU = 0,
parameter [0:0] COMPRESSED_ISA = 0,
parameter [0:0] CATCH_MISALIGN = 1,
parameter [0:0] CATCH_ILLINSN = 1,
parameter [0:0] ENABLE_PCPI = 0,
parameter [0:0] ENABLE_MUL = 0,
parameter [0:0] ENABLE_FAST_MUL = 0,
parameter [0:0] ENABLE_DIV = 0,
parameter [0:0] ENABLE_IRQ = 0,
parameter [0:0] ENABLE_IRQ_QREGS = 1,
parameter [0:0] ENABLE_IRQ_TIMER = 1,
parameter [0:0] ENABLE_TRACE = 0,
parameter [0:0] REGS_INIT_ZERO = 0,
parameter [31:0] MASKED_IRQ = 32'h0000_0000,
parameter [31:0] LATCHED_IRQ = 32'hffff_ffff,
parameter [31:0] PROGADDR_RESET = 32'h0000_0000,
parameter [31:0] PROGADDR_IRQ = 32'h0000_0010,
parameter [31:0] STACKADDR = 32'hffff_ffff
) (
input clk,
resetn,
output reg trap,
// Look-Ahead Interface
output mem_la_read,
output mem_la_write,
output [31:0] mem_la_addr,
output reg [31:0] mem_la_wdata,
output reg [ 3:0] mem_la_wstrb,
// Pico Co-Processor Interface (PCPI)
output reg pcpi_valid,
output reg [31:0] pcpi_insn,
output [31:0] pcpi_rs1,
output [31:0] pcpi_rs2,
input pcpi_wr,
input [31:0] pcpi_rd,
input pcpi_wait,
input pcpi_ready,
// IRQ Interface
input [31:0] irq,
output reg [31:0] eoi,
`ifdef RISCV_FORMAL
output reg rvfi_valid,
output reg [63:0] rvfi_order,
output reg [31:0] rvfi_insn,
output reg rvfi_trap,
output reg rvfi_halt,
output reg rvfi_intr,
output reg [ 1:0] rvfi_mode,
output reg [ 1:0] rvfi_ixl,
output reg [ 4:0] rvfi_rs1_addr,
output reg [ 4:0] rvfi_rs2_addr,
output reg [31:0] rvfi_rs1_rdata,
output reg [31:0] rvfi_rs2_rdata,
output reg [ 4:0] rvfi_rd_addr,
output reg [31:0] rvfi_rd_wdata,
output reg [31:0] rvfi_pc_rdata,
output reg [31:0] rvfi_pc_wdata,
output reg [31:0] rvfi_mem_addr,
output reg [ 3:0] rvfi_mem_rmask,
output reg [ 3:0] rvfi_mem_wmask,
output reg [31:0] rvfi_mem_rdata,
output reg [31:0] rvfi_mem_wdata,
output reg [63:0] rvfi_csr_mcycle_rmask,
output reg [63:0] rvfi_csr_mcycle_wmask,
output reg [63:0] rvfi_csr_mcycle_rdata,
output reg [63:0] rvfi_csr_mcycle_wdata,
output reg [63:0] rvfi_csr_minstret_rmask,
output reg [63:0] rvfi_csr_minstret_wmask,
output reg [63:0] rvfi_csr_minstret_rdata,
output reg [63:0] rvfi_csr_minstret_wdata,
`endif
// Trace Interface
output reg trace_valid,
output reg [35:0] trace_data
);
// Memory signals.
reg mem_valid, mem_instr, mem_ready;
reg [31:0] mem_addr;
reg [31:0] mem_wdata;
reg [ 3:0] mem_wstrb;
reg [31:0] mem_rdata;
// No 'ready' signal in sky130 SRAM macro; presumably it is single-cycle?
always @(posedge clk) mem_ready <= mem_valid;
// (Signals have the same name as the picorv32 module: use '.*')
picorv32 rv32_soc (.*);
// SRAM with always-active chip select and write control bits.
sky130_sram_2kbyte_1rw1r_32x512_8 sram (
.clk0 (clk),
.csb0 ('b0),
.web0 (!(mem_wstrb != 0)),
.wmask0(mem_wstrb),
.addr0 (mem_addr),
.din0 (mem_wdata),
.dout0 (mem_rdata),
.clk1 (clk),
.csb1 ('b1),
.addr1 ('b0),
.dout1 ()
);
endmodule
Finally, your core build script will need to be updated to import the new SRAM Library, and specify some extra parameters such as die size and macro placement:
#!/usr/bin/env python3
import os
import siliconcompiler
from siliconcompiler.targets import skywater130_demo
try:
from . import sky130_sram_2k
except: # noqa E722
import sky130_sram_2k
def build_top():
# Core settings.
design = 'picorv32_top'
target = skywater130_demo
die_w = 1000
die_h = 1000
# Create Chip object.
chip = siliconcompiler.Chip(design)
# Set default Skywater130 PDK / standard cell lib / flow.
chip.use(target)
# Set design source files.
chip.register_source(name='picorv32',
path='git+https://github.com/YosysHQ/picorv32.git',
ref='c0acaebf0d50afc6e4d15ea9973b60f5f4d03c42')
chip.input(os.path.join(os.path.dirname(__file__), f"{design}.v"))
chip.input("picorv32.v", package='picorv32')
# Optional: silence each task's output in the terminal.
chip.set('option', 'quiet', True)
# Set die outline and core area.
margin = 10
chip.set('constraint', 'outline', [(0, 0), (die_w, die_h)])
chip.set('constraint', 'corearea', [(margin, margin),
(die_w - margin, die_h - margin)])
# Setup SRAM macro library.
chip.use(sky130_sram_2k, stackup=chip.get('option', 'stackup'))
chip.add('asic', 'macrolib', 'sky130_sram_2k')
# SRAM pins are inside the macro boundary; no routing blockage padding is needed.
chip.set('tool', 'openroad', 'task', 'route', 'var', 'grt_macro_extension', '0')
# Disable CDL file generation until we can find a CDL file for the SRAM block.
chip.set('tool', 'openroad', 'task', 'export', 'var', 'write_cdl', 'false')
# Reduce placement density a bit to ease routing congestion and to speed up the route step.
chip.set('tool', 'openroad', 'task', 'place', 'var', 'place_density', '0.5')
# Place macro instance.
chip.set('constraint', 'component', 'sram', 'placement', (150, 150))
chip.set('constraint', 'component', 'sram', 'rotation', 'R180')
# Set clock period, so that we won't need to provide an SDC constraints file.
chip.clock('clk', period=25)
# Run the build.
chip.set('option', 'remote', False)
chip.set('option', 'quiet', False)
chip.run()
# Print results.
chip.summary()
return chip
if __name__ == '__main__':
build_top()
With all of that done, your project directory tree should look something like this:
<rundir>
├── sky130_sram_2k.bb.v
├── sky130_sram_2k.py
├── picorv32.py
├── picorv32_ram.py
└── picorv32_top.v
Your picorv32_ram.py
build script should take about 20 minutes to run on the cloud servers if they are not too busy, with most of that time spent in the routing task.
As with the previous designs, you should see updates on its progress printed every 30 seconds, and you should receive a screenshot once the job is complete and a report in the build directory:
Extending your design#
Now that you have a basic understanding of how to assemble modular designs using SiliconCompiler, why not try building a design of your own creation, or adding a custom accelerator to your new CPU core?