% Copyright (c) 2026 Rana Umar Nadeem, Samrah Mumtaz, Muhammad Imran # 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. ```text 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](../04_chia_openpiton/environment_patches.md). ## Status | Core | Wired | Meshes | Result | |---|---|---|---| | PicoRV32 | `examples/cores/picorv32.yaml`, the regression core | 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 (`ibex_top`, RV32IMC) | `examples/cores/ibex.yaml`, by hand | 1x1, 2x2, 4x4 | At 1x1 and 2x2, the 44 RV32I/M tests other than `simple.S` and the AMO tests pass, with the ID and shared-memory programs. At 4x4, `addi`, `beq`, `jal`, `lw`, `mul`, `div`, and those two programs pass. | | Ibex with its RAM at `0x20000000` | `examples/cores/ibex_moved.yaml`, by hand | 2x2 | The ISA, ID, and shared-memory programs and both C programs pass, with every tile starting at `0x20000080`. | | PicoRV32 on Wishbone (`picorv32_wb`) | `examples/cores/picorv32_wb.yaml`, by hand | 2x2 | The ISA, ID, and shared-memory programs and both C programs pass. | | SERV (bit-serial, OBI) | `examples/cores/serv.yaml`, wrapper written by MACE | 1x1, 2x2 | With Vertex Gemini 2.5 Flash, MACE wrote a wrapper that passed `addi.S` and `generic_tile_id.S` in 5 of 5 runs at 1x1, and those two with `generic_shared_flag.S` in 4 of 5 runs at 2x2 (commit `8520a28`). With one of those wrappers, the ISA, ID, and shared-memory programs and both C programs pass at 2x2. | | VexRiscv (`VexRiscv_Min`, two Wishbone ports) | `examples/cores/vexriscv.yaml`, no wrapper | none yet | Held out: no wrapper was written by hand, so MACE's runs are the first. `examples/cores/vexriscv/install.sh` installs the core, and `core_descriptor apply`, which it runs, writes a skeleton wrapper. | `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.S` has no `pass` label, so `sims` finds 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.S` tests 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 `valid` with `addr`, `wstrb`, `wdata`, and `amo_op`, and holds them until `ready`. - `ready` is high for one cycle, with `rdata` for loads and atomics. The master may raise `valid` again on the next cycle. - `wstrb` of zero is a word load. A store's `wstrb` is one of `0001`, `0010`, `0100`, `1000`, `0011`, `1100`, or `1111`, with its data in the byte lanes it selects. - A request with a non-zero `wstrb` and an `amo_op` other than `L15_AMO_OP_NONE` is a 4-byte atomic. `amo_op` is 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](pico.md)). 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 | |---|---|---| | `0xFFFF_FF00` | read | This tile's flat ID | | `0xFFFF_FF04` | read | The number of tiles | | `0xFFFF_FF08` | 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` 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 : first request at `). `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 | | `generic_bridge_obi.v` | Separate instruction and data ports in OBI style (req/gnt, then rvalid), such as Ibex. Data goes before instruction, one request at a time. | | `generic_bridge_wishbone.v` | One Wishbone classic master (`bus: wishbone`), such as PicoRV32's Wishbone top, `picorv32_wb` | | `generic_bridge_wishbone2.v` | Separate instruction and data Wishbone classic masters (`bus: wishbone_split`), such as VexRiscv. Data goes before instruction, one request at a time. | 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 ```yaml 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=` 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 a `ram_base` or `ram_size` that 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: bus` on a bus other than `simple`; - an A extension in `isa.march` with `atomics: 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. ```bash 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 | `core_descriptor check` | | 1 | Verilator lint of the wrapper, core, and bridges | `scripts/generic_ut/lint_core.sh` | | 2 | Every tile's first request is at the reset vector | `boot_problems()` on `sim.log` | | 3 | RV32I/M tests at 1x1 | `ladder_tests()["isa"]` | | 4 | The ISA tests whose stores stay private, at 2x2 and 4x4 | `ladder_tests()["mesh"]` | | 5 | `generic_tile_id.S` and `generic_shared_flag.S`: ID registers, and shared memory without atomics | `ladder_tests()["shared"]` | | 6 | `generic_amo_counter.S`, for `atomics: bus` only | `ladder_tests()["atomics"]` | | 7 | C programs: `generic_c_hello.c` and `generic_c_flags.c`, plus MACE's atomic workloads for `atomics: bus` | `ladder_tests()["c"]` | 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_top` by tile ID. - Tile 0 clears `.bss` between `__bss_start` and `__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 and `argv[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 value `v` becomes `(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 | |---|---|---| | `generic_c_hello.c` | `riscv/generic/` | Every hart prints its ID, one after another, and returns 0. | | `generic_c_flags.c` | `riscv/generic/` | Hart 0 writes sixteen words, then a flag. The other harts wait for the flag and check the words. No atomics. | | `generic_c_fail.c` | `riscv/generic/` | Returns 3. A run of it must fail. | | `barrier_atomic.c`, `producer_consumer.c`, `scatter_gather.c` | `mace/workloads/` | MACE's gate workloads. They build unchanged and need `atomics: bus`. A run finds them through `asm_diag_root`. | 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 1. Copy the core's RTL under `piton/design/chip/tile//` and write its `generic_core_wrapper`. `examples/cores/ibex/install.sh` does both for Ibex. 2. Write the descriptor and run `core_descriptor check`, then `core_descriptor apply`. 3. Climb the ladder, starting with `lint_core.sh` and a 1x1 build with `-generic` and 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 ` and `examples/cores/vexriscv/install.sh ` 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. ```bash 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_core` if the tile cannot run the core. - Before its first build it writes the descriptor's `Flist.core` and `link.ld` into 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 `rtl` task must write the `wrapper body` block first. While the skeleton is there, the plan check sends back a plan with no `rtl` task, and any `config` or `workload` task that does not depend on one. - The `rtl` task'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](../03_mace_design/fixed_checks.md)). 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 | |---|---|---| | `--method` | `mace` | `mace`, or `retry_agent_rtl` for the retry agent with RTL designs; both get the same spec | | `--mesh` | `1x1` | The target mesh | | `--workloads` | `addi.S generic_tile_id.S` | The gate programs | | `--max-iterations`, `--max-usd`, `--max-wall-s` | 5, 20, 10800 | The run's budget | | `--off PART` | none | With `--method mace` only, turns one loop part off for an ablation (one part per run): `plan_check`, `raw_evidence`, `narrowing`, `fast_path`, `gate_pass_ends_run`, or `carry_edits`. The run is recorded as method `no_`. | | `--repeat`, `--tag` | 0, empty | Recorded in the run's labels; the tag is appended to the task label | | `--db-path` | `runs/generic_integrate.db` | The run database | | `--backend`, `--model` | `vertex`, the backend's default | The LLM; Vertex needs `GOOGLE_CLOUD_PROJECT` | The script exits 0 when the run passes, and prints each iteration's tasks, their verdicts, and any boot problems.