Implementing an IO pad ring#

Every build in the other tutorials stops at a rectangle of standard cells. A part you can put on a board needs more than that: it needs pads to bond to, supplies that reach them, and a ring of cells around the edge tying the two together.

This tutorial builds one. The design is examples/padring – a RISC-V processor with an SRAM inside a complete sky130 pad ring – and it runs to GDSII:

cd examples/padring
./padring.py

Note

This needs a local toolchain (Yosys, OpenROAD, OpenSTA and KLayout) and takes around ten minutes. The pad ring is generated by lambdalib, which installs alongside lambdapdk, so there is nothing extra to fetch.

What a ring is made of#

Four things have to line up, and they live in different places:

Piece

Where it lives

The pad cells

RTL. A dependency on lambdalib’s generator, plus a CELLMAP saying which cell sits where on each side.

The interface

RTL. A top level whose ports are the pads.

Placement

A TCL script, attached to the floorplanning task.

Power

Two more TCL scripts: the grid, and the global connections that tie the core’s supplies to the ring’s.

The interface#

The top level is the module that becomes the die, and its ports are the pads:

The die’s pins#
module picorv32_top (
    // Supplies. Core and IO are separate domains.
    inout vdd,
    inout vss,
    inout vddio,
    inout vssio,

    // Clock and reset in.
    input clk,
    input resetn,

    // Processor status out.
    output trap,

    // Memory bus, brought out so the processor can be watched on a bench.
    output        mem_valid,
    output        mem_la_read,
    output        mem_la_write,
    output [ 3:0] mem_la_wstrb,
    output [29:0] mem_addr
);

Two conventions are worth copying. The ports are named for what they carry, not for where they sit in the ring – nobody debugging a board wants to work out which bit of no_pad is the clock. And each one declares the direction it is really used in: the cells in the ring are all bidirectional, but a pin on the finished part is not, and only the supplies are genuinely inout.

Underneath, the logic never touches a pad cell. It drives four buses per side – dout out, din back in, oen and ie for direction, and a technology-specific configuration bus – which is what lets the same processor be wrapped in another technology’s pads without editing it.

Describing the ring#

CELLMAP lists one side, cell by cell, in the order they abut. Each entry is 80 bits packing the cell type, the pin it carries, and its power section:

Four supplies then ten signal pads, per side#
    localparam [80*NCELLS-1:0] CELLMAP = {
        {16'h0000, 16'h0000, LA_VSS, 16'h0000, PIN_NONE},
        {16'h0000, 16'h0000, LA_VDD, 16'h0000, PIN_NONE},
        {16'h0000, 16'h0000, LA_VDDIO, 16'h0000, PIN_NONE},
        {16'h0000, 16'h0000, LA_VSSIO, 16'h0000, PIN_NONE},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd0},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd1},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd2},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd3},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd4},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd5},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd6},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd7},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd8},
        {16'h0000, 16'h0000, LA_BIDIR, 16'h0000, 16'd9}
    };

All four sides reuse this map here. A real part rarely has such tidy symmetry – different sides carry different pin counts, and each gets its own map.

Placing it#

The RTL settled which cells exist. A TCL script settles where they go, and the order of operations matters because each step depends on the last: make the IO rows, place the pads into them, place the corners, fill the gaps, connect the ring by abutment, then add the bond pads.

Pads go into rows in CELLMAP order#
# North and south run left to right; east and west are built bottom to top,
# so their declaration order is reversed relative to the row.
place_pads -row IO_NORTH {*}[padring_side_insts north]
place_pads -row IO_SOUTH {*}[padring_side_insts south]
place_pads -row IO_WEST {*}[lreverse [padring_side_insts west]]
place_pads -row IO_EAST {*}[lreverse [padring_side_insts east]]

connect_by_abutment is the step people miss. The supply rails and the configuration signals pass between neighbouring cells through touching pins rather than through routed wire, so it has to come after the fill has closed the gaps – otherwise the ring is a ring of islands.

See also

Every command in that script belongs to OpenROAD, not to SiliconCompiler, and each takes more arguments than this example uses. OpenROAD’s pad module documents make_io_sites, place_pads, place_corners, place_io_fill, connect_by_abutment and place_bondpad; its pdn module documents the power grid commands in the next section.

Wiring it to the flow#

None of those scripts do anything until a task is told to read them, and that happens in two steps. First the design carries each script in a fileset of its own – "padring.sky130" and the two beside it are names this design invents, not names SiliconCompiler looks for:

Three filesets, one per script#
        # Physical construction, kept in separate filesets so that a
        # different technology can supply its own without touching the RTL.
        with self.active_dataroot("padring"), self.active_fileset("padring.sky130"):
            self.add_file("openroad/padring.tcl")

        with self.active_dataroot("padring"), self.active_fileset("pdn.sky130"):
            self.add_file("openroad/pdn.tcl")

        with self.active_dataroot("padring"), self.active_fileset("globalconns.sky130"):
            self.add_file("openroad/global_connect.tcl")

Then the project hands those filesets to the tasks that read them. These are task variables rather than schema keys, because each configures one step:

Attaching the physical filesets#
    InitFloorplanTask.find_task(project).add_openroad_padringfileset("padring.sky130")

    PowerGridTask.find_task(project).add_openroad_powergridfileset("padring", "pdn.sky130")

    for task in APRTask.find_task(project):
        task.add_openroad_globalconnectfileset("padring", "globalconns.sky130")

Important

The second block is the end of the setup, not the whole of it. Each line names a fileset that the first block created; on a design without them the three hooks point at nothing, and the tasks run as if no ring had been asked for.

Note

The pad ring hook takes only a fileset name, while the power grid and global connect hooks also take the library that owns the fileset. That is because a pad ring is always read from the design being built, while a power grid or a set of global connections often comes from a library instead: when neither is set explicitly, both tasks collect them from the ASIC libraries in the build. Set one here, as this design does, and that fallback no longer runs – so a library’s grid has to be named alongside the design’s rather than inherited.

The power grid then needs one addition an ordinary design does not: the core’s rings are tied out to the supply pads with -connect_to_pads, so the core is fed through the ring rather than through pins on the die edge.

How big does the die have to be?#

The ring sets a floor: fourteen cells plus two corners need about 1520 µm a side. A die sized by that is pad limited – set by how many pins must fit around the edge rather than by the logic inside.

It also has to be set explicitly. init_floorplan refuses to size a die with a ring in it from the usual density and aspect ratio, because the ring is placed at absolute coordinates and a die chosen after the fact would not line up with it:

The die the ring is drawn against#
    project.constraint.area.set_dieoutline(1900, 1900, coremargin=250)

The memory fits inside that. sky130’s SRAM macro is 683 µm wide and needs a placement channel around it, which it has once the 250 µm margin holds the core off the pads, so this die lands at 1900 µm.

What to look at#

Open the result with project.show(), or the GDS directly, and check the ring before anything else:

../../_images/padring_layout.png

The finished die. Ten signal pads and four supply pads on each side, corners turning the ring, IO fill closing the gaps between cells, and bond pads over the top. Inside, the SRAM macro is the wide block across the lower core and the processor is the dense region above it.#

  • Four populated sides. The floorplan log reports what it placed – Placed 14 pads in IO_NORTH and so on. A ring that is missing pads on one side usually means the selection in the placement script stopped matching the instance names the RTL generates.

  • Continuous supplies. The fill cells should leave no gaps, and the rails should run unbroken through the corners.

  • Bond pads landing on the plate each pad cell carries, not merely somewhere over the cell – a bond pad off the plate sits on the cell’s keep-out and the power grid check reports it as a short.

Warning

This example stops short of a tapeout in one respect worth naming: the supply pads carry ESD clamp pins (DRN_HVC, SRC_BDY_HVC) which are left unconnected, and the power grid step says so. Where those clamps belong depends on the chip’s ESD strategy and on foundry guidance, so the example does not guess. A real part has to resolve them.

Next#