Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions docs/user/AutoMemories.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,135 @@ the left edge, alternating horizontal M4 `VDD`/`VSS` straps across the
macro (which the platform's PDN macro grid connects to M5), and an `OBS`
covering M1 to M4.

## Register files

A register file, many read and write ports over a few words, is no
SRAM: FakeRAM's single-port model does not describe it, and synthesised
to flops its mux trees are placed by a placer that does not know they
are an array. `AUTO_MEMORIES_REGFILES` lists spec files, one per RTL
module, and OpenROAD's `generate_ram` builds each as an array of placed
standard cells: a flop per bit, its write logic beside it, the read
trees in columns. The spec names the module, its words and bits, its
read and write ports by the module's own port names, and the cells:

```
module RegFile
mode netlist
words 16
bits 8
clock clock
read io_r0_addr io_r0_data
write io_w0_addr io_w0_data io_w0_en
cell flop DFFHQNx1_ASAP7_75t_R
...
```

`memories/regfile_ram.py` translates the spec to `generate_ram_netlist`
options (ports, banks, folds, the zero word, reset, read latency, write
style and priority, cells) and checks it against the module's ports,
from synthesis: a port in one and not the other, or a data port of
another width, stops the build rather than miswire the parent. The RAM
is a block `<m>_ram`, and a wrapper takes the module's place,
connecting the module's ports to the RAM's (an active-high reset
inverted for an active-low flop, an address narrowed or widened). What
happens next depends on `AUTO_MEMORIES_MACRO_PLACE`, the register files
to place as macros:

- **Not listed (the default): inlined.** Synthesis reads the wrapper
and the RAM's netlist in the module's place; from there on their cells
are placed, sized and buffered with the rest of the design. No macro,
no abstract, no dissolve. The better shape for a small design, where a
macro's outline and channels cost more core than the array saves.
- **Listed: a macro.** `<m>_ram` is a macro to synthesis and macro
placement: its abstract `<m>_ram.lef` and a liberty of its area and
pins land beside the FakeRAM views and it joins `blackboxes.txt`. In
`mode netlist` (the only one) `generate_ram` places only the array's
core and leaves its periphery (decode, select and input buffers) to
the parent, and the macro dissolves into its cells at the end of macro
placement (`regfile_dissolve.tcl`): the core lands FIRM where the
macro was placed, flipped as the macro was, on the parent's rows; the
periphery is left to global placement and the resizer, and dead logic
is eliminated as at synthesis. A large design whose floorplan plans
its register files lists them.
- **A macro of its own.** A register file wanted as a macro to the end
is a design of its own: run its module through the flow (in
bazel-orfs, its own `orfs_flow`, its spec listed and macro-placed so
it dissolves there) and give the parent its abstract as a macro. Its
clock tree is then built and its timing model is written from it.
`mode macro`, which wrote the model from `generate_ram`'s netlist
before any clock tree, stops with that advice.

A word holds its value between writes either through a hold mux per
bit (`write_style mux`, the default here) or through one integrated
clock gate per word, enabled by its write selects (`write_style
clock_gate`, `generate_ram`'s own default). Where a row holds more than
one word (`bank_columns`), each word's bits sit side by side, so its
selects and write enables are no longer than its bits; and a word whose
bits would run more than 150 um is folded into more rows. `tap_columns`
and `service_sites` leave empty columns in the array for the buffers a
parent puts on the nets that cross it.

Listed register files appear in `memories.json` with kind `regfile`;
the rest have kind `fakeram` or `flops`.

Moving the dissolve into OpenROAD is left for later: it would make it
faster, testable at unit level and maintained with the database it
edits.

### Designs to measure it on

What a register file of placed cells buys is a shorter minimum clock
period (the read mux trees are columns, not a cloud) and a shorter
build (fewer cells for synthesis, placement and the resizer to work
on). The ORFS designs whose register file is flip-flops today and its
own module, the seam a spec needs, measured as flops against
`AUTO_MEMORIES_REGFILES`:

| Design | Module | Today | Shape | Ready? |
|---|---|---|---|---|
| asap7/riscv32i | `regfile` | flip-flops: `reg [31:0] rf[31:0]`, mapped to flops (no AUTO_MEMORIES) | 32 x 32, 2R1W, x0 reads 0 | Yes: the ports are the spec's (`zero_word 0`). Also exercises `OPENROAD_HIERARCHICAL=1` beside two FakeRAM macros. |
| asap7/ibex | `ibex_register_file_ff` | flip-flops: `RegFile = RegFileFF`, the default | 32 x 32, 2R1W, x0 reads 0 | Yes: `unused` names `test_en_i` and `dummy_instr_id_i`, ports the array does not use, and `async_reset rst_ni low` resets the flops to 0 as ibex does; simulated against ibex's RTL. |
| asap7/cva6 | `ariane_regfile` (`ariane_regfile_ff.sv`) | flip-flops: cv32a65x has `FpgaEn` 0 | 32 x 32, 2R1W | Not yet: the ports are packed arrays (`raddr_i[1:0][4:0]`), one port per slice, which a spec cannot yet address; asynchronous reset as ibex. |
| asap7/swerv_wrapper | `dec_gpr_ctl_*` | flip-flops | 32 x 32, 4R3W | Not yet: read enables, bank ids and scan ports around the array need a seam in the RTL. |

coralnpu's `Regfile` (9R6W) and `FRegfile` (4R2W) carry their
scoreboard and bypass logic in the same module; asap7/coralnpu gives
their words modules of their own with `SYNTH_VERILOG_SURGERY` and
generates both. picorv32 and tinyRocket keep their register file inside
the CPU module: those need a seam in their RTL first.

### Status: at parity on riscv32i, on a smaller core

riscv32i with its register file generated and inlined (a spec in
`AUTO_MEMORIES_REGFILES`, `zero_word 0`), at riscv32i's own
utilisation, against `designs/asap7/riscv32i`. To global route, 950 ps
clock, minimum period from placement parasitics on the global-route
database:

| | flops | register file |
|---|---|---|
| minimum period | 950 ps | 943 ps |
| core area | 4 012 um2 | 3 503 um2 |

Both are limited by the same single-cycle path: the instruction into
the register file's read, the ALU, the data memory and back into the
register file's write. The register file's share of it, decode and read
tree, is about 260 ps in either.

What did not pay, measured on the same design:

- the register file as a macro dissolved after macro placement: it does
not fit riscv32i's 62 % beside the four FakeRAMs, and at 55 % it is
959 ps on a 4 644 um2 core;
- a hold mux per bit instead of the clock gate: 994 ps inlined, every
array endpoint carrying the mux;
- predecoded read selects: 960 ps;
- a tree of 2:1 muxes steered by the address bits instead of a decoded
one-hot read: 959 ps, the address bits' fanout (about 1 000 inputs a
port) needs as many buffer levels as the decode it replaces;
- four-input gates in the read tree, stronger cells: 950 to 954 ps,
within what any change to the placement moves.

## Platform support

**asap7 only.** `gen_memories.py` rejects any other platform. The
Expand Down Expand Up @@ -183,3 +312,5 @@ bazel-orfs implements this.

- [AUTO_MEMORIES](FlowVariables.md#AUTO_MEMORIES)
- [ADDITIONAL_MEMORIES](FlowVariables.md#ADDITIONAL_MEMORIES)
- [AUTO_MEMORIES_MACRO_PLACE](FlowVariables.md#AUTO_MEMORIES_MACRO_PLACE)
- [AUTO_MEMORIES_REGFILES](FlowVariables.md#AUTO_MEMORIES_REGFILES)
4 changes: 4 additions & 0 deletions docs/user/FlowVariables.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ configuration file.
| <a name="ADDITIONAL_SITES"></a>ADDITIONAL_SITES| Passed as -additional_sites to initialize_floorplan.| |
| <a name="ASAP7_USE_VT"></a>ASAP7_USE_VT| A space separated list of VT options to use with the ASAP7 standard cell library: RVT, LVT, SLVT.| RVT|
| <a name="AUTO_MEMORIES"></a>AUTO_MEMORIES| Experimental. When set to 1, memories that yosys infers in the RTL (`$mem_v2` cells, module boundary only) are detected pre-synthesis, inventoried in `$(RESULTS_DIR)/memories.json`, and — when idiomatic as an SRAM macro — given generated .lib/.lef views under `$(RESULTS_DIR)/memories/`. Synthesis blackboxes those modules and all later stages read the generated views, so an existing design gets macro-based results without a memory compiler. Supported for the asap7 platform only. See docs/user/AutoMemories.md.| 0|
| <a name="AUTO_MEMORIES_MACRO_PLACE"></a>AUTO_MEMORIES_MACRO_PLACE| Experimental. Space-separated list of module names of AUTO_MEMORIES_REGFILES register files to place as macros, blackboxed in synthesis and placed by the macro placer (and, in `mode netlist`, dissolved after macro placement). A register file not listed is inlined: its generated netlist of standard cells replaces the module's body in synthesis, and its cells are placed, sized and buffered with the rest of the design -- the better shape for a small design, where a macro's outline and channels cost more core than the array saves. A name no spec builds stops the build. Requires AUTO_MEMORIES=1.| |
| <a name="AUTO_MEMORIES_REGFILES"></a>AUTO_MEMORIES_REGFILES| Experimental. Space-separated list of register-file spec files, one per RTL module to build as a register file of placed standard cells rather than synthesise to flops. Each spec names its module, words, bits, read and write ports by the RTL's own port names, and the cells. memories/regfile_ram.py checks the spec against the module's ports and has OpenROAD's generate_ram build it as `<module>_ram`, under `$(RESULTS_DIR)/memories/`, with a wrapper in the module's place. A module named in AUTO_MEMORIES_MACRO_PLACE is blackboxed like an AUTO_MEMORIES macro and placed as one; any other is inlined as its generated netlist. A placed spec in `mode netlist` then dissolves into its cells after macro placement: its core FIRM where the macro was placed, in the macro's orientation, its periphery left to placement. Every register file appears in memories.json with kind `regfile`. Requires AUTO_MEMORIES=1.| |
| <a name="BALANCE_ROWS"></a>BALANCE_ROWS| Balance rows during placement.| 0|
| <a name="BLOCKS"></a>BLOCKS| Blocks used as hard macros in a hierarchical flow. Do note that you have to specify block-specific inputs file in the directory mentioned by Makefile.| |
| <a name="BUFFER_PORTS_ARGS"></a>BUFFER_PORTS_ARGS| Specify arguments to the buffer_ports call during placement. Only used if DONT_BUFFER_PORTS=0.| |
Expand Down Expand Up @@ -347,6 +349,8 @@ configuration file.
- [ABC_SCRIPT](#ABC_SCRIPT)
- [ADDER_MAP_FILE](#ADDER_MAP_FILE)
- [ADDITIONAL_MEMORIES](#ADDITIONAL_MEMORIES)
- [AUTO_MEMORIES_MACRO_PLACE](#AUTO_MEMORIES_MACRO_PLACE)
- [AUTO_MEMORIES_REGFILES](#AUTO_MEMORIES_REGFILES)
- [CACHED_REPORTS](#CACHED_REPORTS)
- [CLKGATE_MAP_FILE](#CLKGATE_MAP_FILE)
- [DFF_LIB_FILE](#DFF_LIB_FILE)
Expand Down
8 changes: 6 additions & 2 deletions flow/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,14 @@ do-auto-memories: $(RESULTS_DIR)/memories_inferred.json
--yosys-json $(RESULTS_DIR)/memories_inferred.json \
--out-dir $(RESULTS_DIR)/memories \
--json $(RESULTS_DIR)/memories.json \
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file))
$(foreach file,$(ADDITIONAL_MEMORIES),--memories $(file)) \
$(foreach file,$(AUTO_MEMORIES_REGFILES),--regfile-spec $(file)) \
$(foreach m,$(AUTO_MEMORIES_MACRO_PLACE),--macro-place $(m)) \
$(if $(AUTO_MEMORIES_REGFILES),--openroad $(OPENROAD_EXE)) \
$(foreach file,$(TECH_LEF) $(SC_LEF),--lef $(file))

$(RESULTS_DIR)/memories.json: $(RESULTS_DIR)/memories_inferred.json $(ADDITIONAL_MEMORIES) \
$(wildcard $(SCRIPTS_DIR)/memories/*.py)
$(AUTO_MEMORIES_REGFILES) $(wildcard $(SCRIPTS_DIR)/memories/*.py)
$(UNSET_AND_MAKE) do-auto-memories

yosys-dependencies: $(RESULTS_DIR)/memories.json
Expand Down
19 changes: 19 additions & 0 deletions flow/designs/asap7/coralnpu/FRegfile_rf.regfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# coralnpu's floating-point register file, FRegfile_rf, the module
# surgery.py moves FRegfile's words into: 32 x 32 (sign, exponent,
# mantissa from the top), four combinational reads (FRegfile's three
# read ports and its bus port), two writes of which the later
# (FRegfile's write port 0) wins a word both write, every word reset to
# zero while reset is high, a clock gate per word.
module FRegfile_rf
words 32
bits 32
clock clock
async_reset reset high
read raddr0 rdata0
read raddr1 rdata1
read raddr2 rdata2
read raddr3 rdata3
write waddr0 wdata0 we0
write waddr1 wdata1 we1
write_priority last
write_style clock_gate
78 changes: 78 additions & 0 deletions flow/designs/asap7/coralnpu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# coralnpu on asap7

[CoralNPU](https://github.com/google-coral/coralnpu) is Google's open
RISC-V core for edge machine learning: an in-order scalar core that
dispatches up to four instructions a cycle, with a float unit, an ITCM
and a DTCM, and AXI ports. The top is `CoreMiniAxi`.

Its memories are generated: the five TCM banks as SRAM macros with
their byte write masks (`AUTO_MEMORIES`), and the integer and
floating-point register files as standard cells by OpenROAD's
`generate_ram` (`AUTO_MEMORIES_REGFILES`), every word with its
asynchronous reset. What is left as flip-flops is a handful of queues
of two or three entries.

## Results

The whole flow at a 1,900 ps clock, in about 90 minutes:

| stage | worst setup slack | achieved period |
|---|---|---|
| CTS | +4.6 ps | 1,895 ps |
| global route | +6.2 ps | 1,894 ps |
| final, extracted parasitics | -108 ps | 2,008 ps |

Standard cells take 19,411 µm² and the five SRAM macros 13,965 µm².
Detailed route ends with no DRC errors. CTS takes 2.7 minutes and
global route 9.1; detailed route takes an hour.

The worst path at the end runs from the debug module's request queue
through the CSRs, the float core and the load/store unit to the
retirement buffer, in and out of the kept units.
The generated register files' write enables close with 36 ps to spare.

## Where the RTL comes from

`../../src/coralnpu/CoreMiniAxi.sv` is generated from coralnpu's source
at the commit pinned in `../../src/coralnpu/generate/MODULE.bazel`, by
Bazel from that directory:

cd flow/designs/src/coralnpu/generate
bazelisk run //:update

`generate/CoreMiniAxiGenerator.scala` elaborates coralnpu's `CoreAxi` with the
parameters of coralnpu's own `CoreMiniAxi` build (128-bit fetch and
load/store, float with Zfbfmin), in a wrapper that ties the test enable
`te` to 0, so synthesis removes the test-mode reset bypasses and clock
gate overrides. firtool runs without the verification layers and
without register randomization. To move to a newer coralnpu, change the
commit and its `sha256` in `MODULE.bazel` and run `//:update`.

## Memories

- The ITCM (one bank) and DTCM (four banks) are coralnpu's `Sram`,
512 x 128, one read-write port and a 16-lane byte mask.
`sram.memories` names its pins (`ADDITIONAL_MEMORIES`), and
`AUTO_MEMORIES` replaces it with a generated macro.
- `Regfile` and `FRegfile` keep their words as separate registers, with
no array for yosys to infer. `surgery.py` (`SYNTH_VERILOG_SURGERY`)
moves them into modules of their own, `Regfile_rf` and `FRegfile_rf`,
and `generate_ram` builds those from `Regfile_rf.regfile` and
`FRegfile_rf.regfile`. The integer register file has nine
combinational reads (eight read ports and the debug port) and six
writes; the floating-point one four reads and two writes.
- The largest array left is a ROM, the divider's SRT bound table
(128 x 83), which yosys maps to logic. `SYNTH_MEMORY_MAX_BITS` counts
ROMs today and stops synthesis on it; the design builds once a ROM
is no longer counted, a separate change.

## Surgery

`surgery.py` recognises each register file token for token, comments
and whitespace aside, and anything else stops the build. It moves
storage and changes no function: each register file before and after
the surgery was simulated side by side on the same random inputs,
every output compared every cycle (200,000 cycles, resets included,
read and write addresses biased to collide), and a deliberate error in
the moved storage, a write port's mask dropped or the write priority
swapped, is caught within 20 cycles.
28 changes: 28 additions & 0 deletions flow/designs/asap7/coralnpu/Regfile_rf.regfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# coralnpu's integer register file as standard cells, inlined in its
# place: Regfile_rf, the module surgery.py moves Regfile's storage into.
# 32 x 32 with x0 reading zero, nine combinational reads (the eight read
# ports and the debug port), six writes ORed when two write one word
# (Regfile's own data_N), a clock gate per word, every word reset to
# zero while reset is high.
module Regfile_rf
words 32
bits 32
clock clock
async_reset reset high
read raddr0 rdata0
read raddr1 rdata1
read raddr2 rdata2
read raddr3 rdata3
read raddr4 rdata4
read raddr5 rdata5
read raddr6 rdata6
read raddr7 rdata7
read raddr8 rdata8
write waddr0 wdata0 we0
write waddr1 wdata1 we1
write waddr2 wdata2 we2
write waddr3 wdata3 we3
write waddr4 wdata4 we4
write waddr5 wdata5 we5
zero_word 0
write_style clock_gate
33 changes: 23 additions & 10 deletions flow/designs/asap7/coralnpu/config.mk
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,39 @@ export PLATFORM = asap7
export DESIGN_NICKNAME = coralnpu
export DESIGN_NAME = CoreMiniAxi

export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NICKNAME)/$(DESIGN_NAME).sv \
$(PLATFORM_DIR)/verilog/fakeram_512x128.sv \
$(PLATFORM_DIR)/verilog/fakeram_2048x128.sv
export VERILOG_FILES = $(DESIGN_HOME)/src/$(DESIGN_NICKNAME)/$(DESIGN_NAME).sv

export VERILOG_DEFINES += -D USE_ASAP7
# The integer and floating-point register files' words moved into
# modules of their own, Regfile_rf and FRegfile_rf.
export SYNTH_VERILOG_SURGERY = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/surgery.py

export ADDITIONAL_LEFS = $(PLATFORM_DIR)/lef/fakeram_512x128.lef \
$(PLATFORM_DIR)/lef/fakeram_2048x128.lef
# The five TCM banks (ITCM one, DTCM four) are coralnpu's Sram, 512 x
# 128, an array with one read-write port and a 16-lane byte mask;
# sram.memories names its pins, and AUTO_MEMORIES replaces it with a
# generated macro, mask and all.
export AUTO_MEMORIES = 1
export ADDITIONAL_MEMORIES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/sram.memories

export ADDITIONAL_LIBS += $(PLATFORM_DIR)/lib/NLDM/fakeram_512x128.lib \
$(PLATFORM_DIR)/lib/NLDM/fakeram_2048x128.lib
# The integer and floating-point register files, moved by surgery.py
# into Regfile_rf and FRegfile_rf, built by generate_ram as standard
# cells inlined in their place.
export AUTO_MEMORIES_REGFILES = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/Regfile_rf.regfile \
$(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/FRegfile_rf.regfile

export SDC_FILE = $(DESIGN_HOME)/$(PLATFORM)/$(DESIGN_NICKNAME)/constraint.sdc

export SYNTH_HDL_FRONTEND ?= slang

export CORE_UTILIZATION = 65

# coralnpu's units, kept by name: the core, its fetch, dispatch,
# register files, load/store unit, CSRs and FPU, and the two AXI
# bridges. The ALUs, branch units, multiplier and divider are small and
# belong with the dispatch and writeback around them. The size
# threshold is out of reach so that the list alone decides.
export SYNTH_HIERARCHICAL = 1
export SYNTH_MINIMUM_KEEP_SIZE = 40000
export SYNTH_KEEP_MODULES = SCore UncachedFetch DispatchV2 Regfile FRegfile LsuV3 Csr FloatCore AxiSlave DBus2AxiV2
export SYNTH_MINIMUM_KEEP_SIZE = 1000000000
export OPENROAD_HIERARCHICAL = 1

export RTLMP_MIN_CHANNEL_SIZE = 4 4

17 changes: 14 additions & 3 deletions flow/designs/asap7/coralnpu/constraint.sdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@ current_design CoreMiniAxi

set clk_name io_aclk
set clk_port_name io_aclk
set clk_period 10.00
# The period the flow closes at global route, with timing repair.
set clk_period 1900

set clk_port [get_ports $clk_port_name]
# IO budgets as optimization targets, the platform's set_max_delay
# model: 0.8 of the period for a path with a register at one end, 0.6
# for a combinational path straight through.
set in2reg_max [expr { $clk_period * 0.8 }]
set reg2out_max [expr { $clk_period * 0.8 }]
set in2out_max [expr { $clk_period * 0.6 }]

create_clock -period $clk_period -name $clk_name $clk_port
# io_aresetn is asynchronous and reaches the core through RstSync's
# synchroniser; its recovery and removal are not what sets the core's
# frequency.
set_false_path -from [get_ports io_aresetn]

source $::env(PLATFORM_DIR)/constraints.sdc
Loading
Loading