Generic Tile
The generic tile runs an RV32 core that has no OpenPiton adapter of its own. The core sits in a per-core wrapper that presents one simple memory bus. A shared transducer connects that bus to the tile’s L1.5. A short YAML core descriptor tells the build where the core’s RTL is and what it supports. MACE selects the tile with core="generic" and a descriptor, which adds -generic, the descriptor’s -rv32_march/-rv32_mabi, and -rv32_image_base to the sims command line. MACE can also write a new core’s wrapper itself, as the last section of this page describes.
core RTL -- wrapper -- bridge -- simple bus -- generic_l15_transducer -- tile L1.5 wires
(per core) (shared) (shared)
scripts/patch_openpiton.sh fixes 14 to 17 install the tile into a checkout; fix 18 lets a program load at the descriptor’s RAM base, and fix 19 builds C programs. See Environment Patches.
Status
Core |
Wired |
Meshes |
Result |
|---|---|---|---|
PicoRV32 |
|
1x1, 2x2, 4x2, 4x4 |
PicoRV32’s gate tests and the generic tile’s programs pass on every mesh. At 1x1, all nine AMO tests pass. |
Ibex ( |
|
1x1, 2x2, 4x4 |
At 1x1 and 2x2, the 44 RV32I/M tests other than |
Ibex with its RAM at |
|
2x2 |
The ISA, ID, and shared-memory programs and both C programs pass, with every tile starting at |
PicoRV32 on Wishbone ( |
|
2x2 |
The ISA, ID, and shared-memory programs and both C programs pass. |
SERV (bit-serial, OBI) |
|
1x1, 2x2 |
With Vertex Gemini 2.5 Flash, MACE wrote a wrapper that passed |
VexRiscv ( |
|
none yet |
Held out: no wrapper was written by hand, so MACE’s runs are the first. |
scripts/local_generic_gate.py runs the PicoRV32 gate through the adapter MACE uses. At 2x2, 12 of 12 verdicts matched: 10 passes, the expected fail, and the expected timeout.
On every mesh, generic_expect_fail.S fails and generic_expect_timeout.S times out, as they should.
Some of PicoRV32’s tests don’t fit a multicore run, and they fail on native PicoRV32 too:
simple.Shas nopasslabel, sosimsfinds no good trap and the run times out, on any mesh.The AMO tests run one read-modify-write sequence on one shared word from every tile, so they fail from 2x2 up.
generic_amo_counter.Stests atomics across tiles instead.The store tests (
sb.S,sh.S,sw.S) write different values to the same shared words in different test cases, so one tile’s store can land between another tile’s store and load. They fail at 4x4.
The simple bus
PicoRV32’s memory interface, made explicit:
The master raises
validwithaddr,wstrb,wdata, andamo_op, and holds them untilready.readyis high for one cycle, withrdatafor loads and atomics. The master may raisevalidagain on the next cycle.wstrbof zero is a word load. A store’swstrbis one of0001,0010,0100,1000,0011,1100, or1111, with its data in the byte lanes it selects.A request with a non-zero
wstrband anamo_opother thanL15_AMO_OP_NONEis a 4-byte atomic.amo_opis ignored for loads: PicoRV32 leaves it set on the fetches after an atomic.
One request is outstanding at a time, so the transducer sends a core’s requests to the L1.5 in the order the core issues them.
The transducer
generic_l15_transducer.v carries PicoRV32’s request and return logic with a request state machine in place of its valid-edge detector. It stays in reset until the L1.5 leaves reset (spc_grst_l), several cycles after the tile’s rst_n: the L1.5 drops a request it takes earlier, and a core such as SERV fetches in its first cycle. The core’s request waits on the bus until then. Every generic build sets CONFIG_DISABLE_BIST_CLEAR, since a core that boots before the SRAM model’s power-on clear ends loses its first writes (see PicoRV32). It swaps byte order in both directions, since the L1.5 keeps data big-endian. An address with bit 31 set, or an atomic, goes non-cacheable. A store with any other wstrb sets mon_error, and the monitor reports the run as failed.
It answers three addresses itself, without an L1.5 request:
Address |
Access |
Value |
|---|---|---|
|
read |
This tile’s flat ID |
|
read |
The number of tiles |
|
write |
Finish: 1 means pass, any other non-zero value means fail. The first non-zero write is kept. |
Every core gets its hart ID and its finish signal the same way, so test programs need no per-core code. In pc_cmp.v.pyv, the RTL_GENERIC<n> branch reports a set finish register as the program’s good-trap or bad-trap address, and the existing compare, finish mask, and timeout decide the run. A program that never writes the register times out.
The transducer also prints each tile’s first request address (generic tile <n>: first request at <addr>). boot_problems() in chia_openpiton/core_descriptor.py compares it with the descriptor’s reset vector. A test pass does not show the vector is right: with Ibex’s descriptor set to 0x40000000, Ibex still starts at 0x40000080, which skips only the test’s 32 register-clearing instructions, and addi.S passes. boot_problems() reports the mismatch, and in a MACE run a task with a boot problem fails even when its programs pass.
Bridges and wrappers
Bridge |
For |
|---|---|
none |
Cores whose memory interface is the simple bus, such as PicoRV32 |
|
Separate instruction and data ports in OBI style (req/gnt, then rvalid), such as Ibex. Data goes before instruction, one request at a time. |
|
One Wishbone classic master ( |
|
Separate instruction and data Wishbone classic masters ( |
No bridge carries atomics. Both Wishbone bridges take a request when cyc and stb are both high and raise ack for one cycle, with the simple bus’s ready. Their adr ports are byte addresses: a core with a word address, such as VexRiscv’s 30-bit ADR, drives {adr, 2'b00}. A read is a word read. sel gives the byte lanes of a write. The bridges never raise err or rty.
Each core has a module named generic_core_wrapper with a fixed interface: clk, rst_n, hart_id, the simple bus as master, and wake (the L1.5’s wake-up interrupt). The wrapper instantiates the core and its bridge and ties off the inputs the tile doesn’t drive. examples/cores/ibex/generic_core_wrapper.sv is Ibex’s, and examples/cores/picorv32_wb/generic_core_wrapper.v is the one-port Wishbone example.
scripts/generic_ut/run_bridge_wishbone_ut.sh runs a unit testbench for both Wishbone bridges against a stub simple bus, and scripts/generic_ut/mutate_wishbone_ut.py runs it against 24 mutated copies of the bridges, each of which it must fail.
The core descriptor
core: ibex
top_module: ibex_top
wrapper: ibex/generic_core_wrapper.sv
include_dirs: [ibex/vendor/lowrisc_ip/ip/prim/rtl]
sources: [ibex/rtl/ibex_pkg.sv, ibex/rtl/ibex_top.sv]
isa: {march: rv32imc, mabi: ilp32}
bus: obi # simple | obi | wishbone | wishbone_split
reset_vector: 0x40000080
atomics: none # none | bus
private_cache: none # none | disabled | write_through | write_back
hart_id: port # port | register
Paths are relative to piton/design/chip/tile/. ram_base (default 0x40000000), ram_size (default 0x100000, 1 MB), harts (default 1), own_interconnect (default false), and notes are optional. chia_openpiton/core_descriptor.py reads it and writes two files into the checkout: tile/generic/rtl/Flist.core, which lists the core’s sources and wrapper, and the test environment’s link.ld. The descriptor’s digest is part of the build ID, so two cores never share a model.
The RAM is ram_base up to ram_base + ram_size. The build passes -rv32_image_base=<ram_base> to sims, so rv32_as loads the program image at ram_base (patch fix 18). The link script starts there, puts .text.init at the reset vector, then .text, .rodata, .data, and .bss, and defines __bss_start, __bss_end, _end, and __stack_top (the end of RAM). The link fails if the program does not fit. A core that resets elsewhere than 0x40000000 needs a wrapper that sets its boot address to match: examples/cores/ibex_moved.yaml is Ibex with its RAM at 0x20000000.
support_problems() checks the descriptor before any build. It rejects:
an RV64 core;
more than one hart;
a write-back or write-through private cache, since the tile has no invalidation path to keep it coherent;
a core with its own L2, L3, or NoC;
RAM outside
0x40-0x7fffffff, the addresses a 32-bit tile reaches DRAM for in the simulation (an address with bit 31 set is I/O), or aram_baseorram_sizethat is not a multiple of 64, the image loader’s line size;a reset vector that is not word-aligned or not inside RAM;
atomics: buson a bus other thansimple;an A extension in
isa.marchwithatomics: none.
A MACE run with an unsupported core ends with status unsupported_core and its reasons, before any LLM call or build. The retry agent makes the same check.
python -m chia_openpiton.core_descriptor check examples/cores/ibex.yaml /path/to/openpiton
Verification ladder
Each step runs only when the one before it passes.
Step |
Check |
Tool |
|---|---|---|
0 |
Descriptor and support check |
|
1 |
Verilator lint of the wrapper, core, and bridges |
|
2 |
Every tile’s first request is at the reset vector |
|
3 |
RV32I/M tests at 1x1 |
|
4 |
The ISA tests whose stores stay private, at 2x2 and 4x4 |
|
5 |
|
|
6 |
|
|
7 |
C programs: |
|
Tests run in piton/verif/diag/assembly/include/riscv/generic/. Its riscv_test.h stores to the finish register and assembles tests without compressed instructions, since the RV32I tests assume 4-byte instructions. The generic tile’s own programs are in piton/verif/diag/assembly/riscv/generic/, beside generic_expect_fail.S and generic_expect_timeout.S, which must end in a failure and a timeout.
scripts/generic_ut/run_generic_ut.sh runs the transducer’s unit testbench against a stub L1.5.
C programs
A C program runs on the generic tile with the runtime in piton/verif/diag/assembly/include/riscv/generic/: crt.S, syscalls.c, util.h, and the link script. Fix 19 teaches rv32_as to build a diag.c with them. It passes the descriptor’s -rv32_march and -rv32_mabi, -nostdlib -nostartfiles, and libgcc. The toolchain has no newlib, so it adds picolibc’s headers when the toolchain has them. An assembly test is built as before. mem.image starts at RV32_IMAGE_BASE, 16 hex digits with a default of 0000000040000000.
libgcc comes from the nearest multilib the compiler has: rv32ima links the rv32ia library and rv32imc the rv32im one. rv32i and rv32im have their own.
The link script defines __bss_start, __bss_end, and __stack_top. crt.S starts every tile:
It clears the registers and reads the tile ID and the number of tiles from the transducer’s registers.
It gives each tile a 4 KB stack, below
__stack_topby tile ID.Tile 0 clears
.bssbetween__bss_startand__bss_end, then sets a flag in.data. The other tiles wait for it.It calls
main(1, argv).argv[0][0]is the tile ID andargv[0][1]the number of tiles, as in Ariane’s runtime.It writes
main’s return value to the finish register: 0 becomes 1 (pass), and any other valuevbecomes(v << 1) | 1(fail).
The pass and fail labels at the two stores give rv32_as its good-trap and bad-trap symbols. There is no trap handler, so a trap ends in a timeout.
syscalls.c has exit, putchar, printstr, a small printf (%d %i %u %x %c %s %%, a 0 flag, and a width), and the string functions gcc may call on its own. Output goes to the fake UART at 0xF0C2C000, one character per store, and lands in fake_uart.log in the run directory. util.h has ATOMIC_OP and ATOMIC_FETCH_OP for cores with the A extension, and read_csr.
Program |
Where |
Checks |
|---|---|---|
|
|
Every hart prints its ID, one after another, and returns 0. |
|
|
Hart 0 writes sixteen words, then a flag. The other harts wait for the flag and check the words. No atomics. |
|
|
Returns 3. A run of it must fail. |
|
|
MACE’s gate workloads. They build unchanged and need |
On a 2x2 mesh, PicoRV32 passes all five programs it can run and fails generic_c_fail.c. Ibex passes generic_c_hello.c and generic_c_flags.c, with every tile starting at its reset vector, and fails generic_c_fail.c.
Adding a core
Copy the core’s RTL under
piton/design/chip/tile/<core>/and write itsgeneric_core_wrapper.examples/cores/ibex/install.shdoes both for Ibex.Write the descriptor and run
core_descriptor check, thencore_descriptor apply.Climb the ladder, starting with
lint_core.shand a 1x1 build with-genericand the descriptor’s ISA flags.
Integrating a core with MACE
MACE can write the wrapper itself. Copy only the core’s RTL and write its descriptor. For SERV and VexRiscv the descriptors are in examples/cores/, and examples/cores/serv/install.sh <serv clone> <piton root> and examples/cores/vexriscv/install.sh <pythondata-cpu-vexriscv clone> <piton root> copy the RTL. When the wrapper the descriptor names does not exist, core_descriptor apply writes a skeleton in its place: the module with its fixed ports and a block marked wrapper body where the core and its bridge go. No core runs while that block is there.
MAKEFLAGS=-j1 python examples/generic_integrate.py --piton-root ~/openpiton \
--descriptor examples/cores/serv.yaml --method mace --mesh 2x2 \
--workloads addi.S generic_tile_id.S generic_shared_flag.S generic_c_hello.c
Pick gate programs the core can run. generic_amo_counter.S and MACE’s atomic C workloads need atomics: bus, which only a core on the simple bus has, so a core such as SERV or VexRiscv takes addi.S, generic_tile_id.S, generic_shared_flag.S, and generic_c_hello.c or generic_c_flags.c. The descriptor check does not look at the workloads.
The run turns RTL edits on and goes through the loop like any other:
Before its first LLM call it checks the descriptor and the core’s files, and ends with status
unsupported_coreif the tile cannot run the core.Before its first build it writes the descriptor’s
Flist.coreandlink.ldinto the checkout, and the skeleton if the wrapper file is missing.The planner’s inputs name the core, its bus and reset vector, and the wrapper file, and say that an
rtltask must write thewrapper bodyblock first. While the skeleton is there, the plan check sends back a plan with nortltask, and anyconfigorworkloadtask that does not depend on one.The
rtltask’s agent is told which bridge to instantiate, that the wrapper’s ports stay as they are, which reset vector to set, and which inputs to tie off. PicoRV32’s wrapper is named as a finished example.A task passes when every gate program passes on every tile and every tile’s first request is at the reset vector.
After a failed iteration, the edits of the last task that built carry into the next one (see Fixed Checks).
Like every RTL edit, the wrapper is applied for the run’s builds and reverted afterwards, so the checkout keeps the skeleton. The wrapper is not lost: the rtl_edits column of the run’s tasks table holds each task’s diff, and the diff of the task that passed is the finished wrapper. Apply it to the skeleton with patch -p1 from the checkout’s root to keep it.
Option |
Default |
Meaning |
|---|---|---|
|
|
|
|
|
The target mesh |
|
|
The gate programs |
|
5, 20, 10800 |
The run’s budget |
|
none |
With |
|
0, empty |
Recorded in the run’s labels; the tag is appended to the task label |
|
|
The run database |
|
|
The LLM; Vertex needs |
The script exits 0 when the run passes, and prints each iteration’s tasks, their verdicts, and any boot problems.