# BUILD_SOC_CM.md — Sapphire SoC + Church Machine Combined Bitstream

## Quick build (one command)

```bash
# Full pipeline: firmware → patch → synth → P&R → pgm → copy to bitstreams/
make bitstream

# Same, plus automatic flash and UART smoke-test at the end:
make bitstream-flash
```

See `scripts/build_ti60_bitstream.sh` for details.  The resulting hex lands in
`bitstreams/church_ti60_f225.hex` and is immediately served by the IDE at
`/dl/ti60-hex`.

---

## What this builds

A combined Efinix Ti60F225 bitstream that places the Sapphire RISC-V SoC and the
Church Machine RTL side-by-side in a single Efinity project.

On power-on:
- The Sapphire SoC boots its firmware and sends `CHURCH Ti60 SoC+CM v2.0\r\n`
  over **ttyUSB2** (57600 baud, GPIOL_02 → FT4232H interface 2).
- The Church Machine streams NIA traces, fault codes, and call-home packets
  over **ttyUSB3** (115200 baud, GPIOL_P_03 → FT4232H interface 3).

**The Ti60F225 devkit has exactly 3 user LEDs (not 4):**
- LED0 (GPIOR_P_07) — ON when SoC is out of reset
- LED1 (GPIOR_P_08) — ON within ~1 ms when CM boot ROM completes (sticky)
- LED2 (GPIOR_P_09) — Turns ON ~3 s after power-on when CM banner is sent; also ON on fault

The SoC firmware controls the CM via an APB3 register bridge.
See **APB3 register map** below.

---

## ⚠️  Critical: how firmware gets into the bitstream on Ti60

**EFX_MAP on Efinix Titanium completely ignores `$readmemb`.**
It is treated as simulation-only regardless of where the `.bin` files are placed.
Copying symbol files to `work_syn/` does NOT help.

The correct flow:

1. Build firmware → four byte-lane `.bin` symbol files.
2. **Run `scripts/patch_sapphire_init.py`** — replaces `$readmemb` (or existing
   inline assignments if already patched) with explicit `initial` block assignments.
3. Set `optimize-zero-init-rom = 0` in `church_soc_cm.xml`.
4. Synthesise → EFX_MAP creates 64 EFX_RAM10 instances for system_ramA **with
   `INIT_` parameters already populated** from the inline assignments.
5. Place & route.
6. **Run `efx_pgm church_soc_cm.xml`** (from the `SoC/` directory) — generates the SPI flash hex.
7. Flash with `openFPGALoader`.

**There is no separate `patch_mapv_init.py` step.** The inline initial block
approach means EFX_MAP propagates firmware bytes to the EFX_RAM10 `INIT_` params
during synthesis.  Confirm with:
```bash
grep "INIT_0" outflow/church_soc_cm.map.v | grep "ram_symbol" | head -4
# All four lanes must show non-zero hex values
```

**`patch_sapphire_init.py` must be re-run every time the firmware changes**
before re-synthesising.  The script handles both the virgin case (`$readmemb`
present) and the already-patched case (inline assignments already there).

---

## Prerequisites

| Item | Notes |
|---|---|
| Efinity 2026.1 | Installed at `~/efinity/2026.1` (scripts default to this; set `EFINITY_HOME` to override) |
| Efinity RISC-V IDE 2025.2 | Toolchain at `~/efinity/efinity-riscv-ide-2025.2/toolchain/bin` |
| Sapphire SoC IP | Ships with Efinity — see Step 1 |
| Python 3 + Amaranth | Required to generate CM RTL in Step 2 |
| `pyserial` | `pip install pyserial` — for the smoke test |
| `openFPGALoader` | `~/oss-cad-suite/bin/openFPGALoader` — for flashing |
| `libnss3` | Required by Efinity 2026.1 Interface Designer on Chromebook/headless Linux: `sudo apt-get install -y libnss3` |

---

## Steps

### Step 1 — Copy the Sapphire SoC IP files

```bash
cp ~/efinity/2025.2/ipm/ip/efx_tsemac/fpga/Ti60F225_devkit/ip/sapphire/sapphire.v \
   hardware/soc_combined/

cp ~/efinity/2025.2/ipm/ip/efx_tsemac/fpga/Ti60F225_devkit/ip/sapphire/sapphire_define.vh \
   hardware/soc_combined/
```

> If the path does not exist: `find ~/efinity -name "sapphire.v" 2>/dev/null`

---

### Step 2 — Generate the Church Machine RTL

```bash
python hardware/gen_verilog.py --ti60
cp build/church_ti60_f225.v hardware/soc_combined/
```

---

### Step 3 — Build the SoC firmware

```bash
make -C hardware/soc_combined/firmware
```

Produces `firmware/firmware.bin` and four byte-lane symbol files in
`hardware/soc_combined/`:
```
EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol{0,1,2,3}.bin
```

---

### Step 4 — Patch sapphire.v (MUST re-run on every firmware change)

```bash
cd ~/church-machine   # repo root
python3 scripts/patch_sapphire_init.py \
  hardware/soc_combined/sapphire.v \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol0.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol1.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol2.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol3.bin
```

Expected output:
```
symbol0: 131072 entries, NNN non-zero, [0]=0xXX
  -> replaced $readmemb          ← first run
  OR
  -> updated N inline block(s)   ← subsequent runs
...
sapphire.v OK (+N chars, M total)
```

If you see `ERROR: no pattern found for ram_symbol0`, the sapphire.v is a
different version of the Efinix IP — grep for `ram_symbol` to inspect it.

---

### Step 5 — Ensure optimize-zero-init-rom is off

```bash
grep "optimize-zero-init-rom" hardware/soc_combined/church_soc_cm.xml
# Must show value="0"
```

If it shows `value="1"`:
```bash
sed -i 's/optimize-zero-init-rom" value="1"/optimize-zero-init-rom" value="0"/' \
  hardware/soc_combined/church_soc_cm.xml
```

---

### Step 5b — Copy symbol files to work_syn/ (optional but safe)

```bash
bash hardware/soc_combined/scripts/prep_syn.sh
```

This copies the four BRAM symbol `.bin` files from the project root into `work_syn/`.
It aborts with a clear error if any file is missing or empty — catching a failed
firmware build before synthesis wastes time on a 4-minute synth run.

> **Note:** On Efinix Titanium, EFX_MAP ignores `$readmemb` regardless of where
> the `.bin` files are placed. The real firmware embedding is done by
> `patch_sapphire_init.py` (Step 4). `prep_syn.sh` is a safety net and is also
> required if a future Efinity version restores `$readmemb` support.

---

### Step 6 — Synthesise

```bash
bash hardware/soc_combined/run_efx_map.sh 2>&1 | tail -5
```

Verify all 4 BRAM lanes have non-zero INIT_0 (firmware confirmed embedded):
```bash
for sym in 0 1 2 3; do
  LINENUM=$(grep -n "EFX_RAM10" hardware/soc_combined/outflow/church_soc_cm.map.v \
    | grep "ram_symbol${sym}__D\\\$g1" | head -1 | cut -d: -f1)
  echo "symbol${sym}: $(sed -n "${LINENUM},$((LINENUM+3))p" \
    hardware/soc_combined/outflow/church_soc_cm.map.v | grep INIT_0)"
done
# All four must show non-zero hex strings
```

---

### Step 7 — Place & Route

**Option A — Command-line (preferred):**
```bash
bash hardware/soc_combined/run_efx_pnr.sh 2>&1 | tail -5
```

**Option B — GUI (if command-line crashes):**
Some Efinity 2025.2 installations on Chromebook/Linux crash with `efx_pnr` command-line. If you see:
```
ERROR: [Internal] Error: Unsupported value for family=
```
Use the GUI instead:
1. Open Efinity GUI
2. File → Open Project → select `church_soc_cm.xml`
3. Project → Place & Route
4. Project → Generate Bitstream

Both paths produce the same `outflow/church_soc_cm.bit` and `outflow/church_soc_cm.hex`.

> **Troubleshooting:** If the GUI also cannot open the project, the Efinity device database may be missing. Verify:
> ```bash
> ls ~/efinity/2025.2/share/efinity/devices/
> ```
> This directory should contain `.json` files for each family (Titanium, Trion, etc.). If it's missing, reinstall Efinity with the full device database.

---

### Step 8 — Generate the SPI flash hex (Interface Designer + efx_pgm)

This is the **verified 2026-06-15 headless flow** (CALLHOME telemetry confirmed):

```bash
cd ~/church_project/SoC
export EFINITY_HOME=~/efinity/2026.1
source $EFINITY_HOME/bin/setup.sh 2>/dev/null

# Step 8a: Interface Designer — generates the LPF that efx_pgm requires
# --timing_model C3 is required; without it the run silently produces a wrong LPF
python3 $EFINITY_HOME/scripts/efx_run_pt_unified.py \
    --project church_soc_cm \
    --flow interface \
    --family Titanium \
    --device Ti60F225 \
    --timing_model C3 2>&1

# Step 8b: Bitstream generation
$EFINITY_HOME/bin/efx_run church_soc_cm --prj \
    --flow pgm --family Titanium --device Ti60F225 2>&1

ls -lh outflow/church_soc_cm.hex
```

> **Note (Efinity 2026.1):** `efx_pgm` alone gives `Missing Interface Designer LPF constraint file`.
> The two-step approach above generates the LPF from `peri.xml` (via Interface Designer), then
> runs the bitstream generator.  `--timing_model C3` is required for the Ti60F225 speed grade —
> omitting it produces a degenerate LPF that causes silent flash failures.
> Or use the companion script: `bash hardware/soc_combined/run_efx_pgm.sh`

---

### Step 9 — Flash and test

```bash
cd ~/church-machine
sudo ~/oss-cad-suite/bin/openFPGALoader \
  -b titanium_ti60_f225_jtag \
  -f hardware/soc_combined/outflow/church_soc_cm.hex

sleep 5 && python3 scripts/test_ti60_uart.py \
  --port=/dev/ttyUSB2 --timeout=30 --verbose
```

---

## Rebuild-from-firmware-change checklist

When only the firmware changes (no RTL changes), you can skip Steps 1–2.

> **After any firmware change:** the four `.bin` symbol files in `hardware/soc_combined/`
> must be regenerated (by `make -C hardware/soc_combined/firmware`), then both
> `prep_syn.sh` and `patch_sapphire_init.py` must be re-run before synthesis.
> Skipping either step leaves the BRAM initialised with stale or zeroed firmware.

```bash
cd ~/church-machine
make -C hardware/soc_combined/firmware                    # Step 3 — regenerates .bin files
bash hardware/soc_combined/scripts/prep_syn.sh            # Step 5b — copies .bin to work_syn/
python3 scripts/patch_sapphire_init.py \                  # Step 4 — MUST NOT SKIP
  hardware/soc_combined/sapphire.v \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol0.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol1.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol2.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol3.bin
bash hardware/soc_combined/run_efx_map.sh                  # Step 6  (~4 min)
bash hardware/soc_combined/run_efx_pnr.sh                  # Step 7  (~5 min)
bash hardware/soc_combined/run_efx_pgm.sh                  # Step 8  (~30 s)
sudo ~/oss-cad-suite/bin/openFPGALoader \
    -b titanium_ti60_f225_jtag \
    -f ~/church_project/SoC/outflow/church_soc_cm.hex      # Step 9a
python3 scripts/test_ti60_uart.py \
  --port=/dev/ttyUSB2 --timeout=30 --verbose               # Step 9b (57600 baud)
```

---

## FT4232H port layout

The Ti60 F225 USB port assignments (ttyUSB0–3, baud rates, and the ChromeOS ttyUSB3 console trap) are documented in the canonical table at **[docs/HARDWARE.md § 2. USB Port Map](../../docs/HARDWARE.md#2-usb-port-map)**. In brief: ttyUSB2 = Sapphire SoC UART (57,600 baud, smoke-test target); ttyUSB3 = CM debug UART (115,200 baud).

---

## Firmware addresses

| Symbol | Value | Used by |
|---|---|---|
| `UART_BASE` | `0xF8010000` | `firmware/main.c` |
| `UART_DATA` | `0xF8010000` | write = TX, read = RX |
| `UART_STATUS` | `0xF8010004` | bits[23:16] = TX avail (Sapphire UART) |
| `UART_CLOCKDIV` | `0xF8010008` | 25 MHz / (8 × (div+1)) = baud rate |
| APB slave 0 (CM bridge) | `0xF8100000` | `firmware/main.c` `CM_APB_BASE` |
| Boot ROM base | `0xF9000000` | CPU reset vector, `link.ld` |

UART baud rate: firmware writes `CLOCKDIV = 53` → 25 000 000 / (8 × 54) = 57,870 ≈ 57,600 baud.
The Sapphire SoC UART resets `CLOCKDIV` to **0x00** on power-up (not 53); firmware **must** write it
explicitly before the first `uart_puts` call, or the UART runs at 25 MHz / 8 = 3.125 Mbaud and
produces silence on any standard terminal.

**uart_putc design:** Uses unconditional write + 3000-NOP inter-character delay
(~120 µs @ 25 MHz) rather than polling STATUS. This avoids infinite spins if
the STATUS register bit layout differs between Sapphire IP versions.

---

## APB3 register map

The full APB3 bridge register table (CTRL, STATUS, NIA, FAULT, UID_LO/HI, FAULT_GT, FAULT_INSTR, FAULT_CR14, FAULT_STAGE) is documented canonically at **[docs/HARDWARE.md § 4. APB3 Register Map](../../docs/HARDWARE.md#4-apb3-register-map)**. The SoC accesses the bridge at `0xF8100000` (`IO_APB_SLAVE_0_INPUT`, `CM_APB_BASE` in `firmware/main.c`).

---

## Troubleshooting

### UART silent — 0 bytes on ttyUSB2

Check in order:

1. **Was the hex regenerated after the latest synthesis?**
   ```bash
   ls -lh hardware/soc_combined/outflow/church_soc_cm.hex
   # Timestamp must be AFTER the most recent P&R run
   # If stale: cd hardware/soc_combined && ~/efinity/2025.2/bin/efx_pgm --project-xml church_soc_cm.xml
   ```

2. **Did patch_sapphire_init.py run before synthesis?**
   ```bash
   grep -c "ram_symbol0\[" hardware/soc_combined/sapphire.v
   # Must be >> 2 (hundreds of assignments)
   ```

3. **Is optimize-zero-init-rom = 0?**
   ```bash
   grep "optimize-zero-init-rom" hardware/soc_combined/church_soc_cm.xml
   ```

4. **Do all 4 BRAM lanes have non-zero INIT_0?**
   ```bash
   for sym in 0 1 2 3; do
     LINENUM=$(grep -n "EFX_RAM10" hardware/soc_combined/outflow/church_soc_cm.map.v \
       | grep "ram_symbol${sym}__D\\\$g1" | head -1 | cut -d: -f1)
     echo "sym${sym}: $(sed -n "${LINENUM},$((LINENUM+3))p" \
       hardware/soc_combined/outflow/church_soc_cm.map.v | grep INIT_0)"
   done
   # All four must be non-zero
   ```

5. **Is ttyUSB2 vs ttyUSB3 correct?**  SoC UART → ttyUSB2.  CM UART → ttyUSB3.
   Both ports should show 0x00 glitch bytes on open; ttyUSB2 should have actual
   ASCII text once the SoC firmware boots.

### Interface Designer fails: `ERROR encountered running Interface Designer`

The peri.xml is in the wrong format. Efinity 2026.1 Interface Designer requires:
- All 12 IO banks declared (`1A`–`4B` plus `BL`/`BR`/`TL`/`TR`) — missing banks → silent failure
- `version="2025.2.288.4.15"` and `db_version="20252999"` (not the old `20241999` format)

The Replit `hardware/soc_combined/church_soc_cm.peri.xml` is already corrected. If the file on the Penguin is stale, re-download it:

```bash
curl -s "https://lab.cloomc.org/dl/peri-xml" \
  -o ~/church_project/SoC/church_soc_cm.peri.xml
```

Key changes in the corrected peri.xml (2026.1 vs old 2025.2 format):
- Clock GPIO resource: `GPIOT_P_07_CLK4_P` → **`GPIOL_P_18`** (pin B2, LEFT bank, 1.8 V LVCMOS)
- Clock mode: `mode="input_single"` → `mode="input"`
- Clock conn_type: `conn_type="clkin"` → `conn_type="normal"` (avoids PLL assertion)
- Clock clkmux_buf_name: `"CLKMUX_T"` → `""` (avoids "duplicate pin name" error)
- CLKMUX sections: all `name="clk"` on ROUTE0 pins removed (all must be `name=""`)

---

### Efinity 2026.1 headless: up to 5 one-time patches (now automated)

The Interface Designer (PT Unified) refuses to generate the LPF on a headless
build machine because it validates HSIO GPIO clock rules that the Ti60F225
devkit clock pin (GPIOL_P_18 as `comp_gpio`) fails. `run_efx_pnr.sh` now
applies these patches automatically, idempotently, at the start of every run
(Step -1, before Interface Designer) by calling:

```bash
python3 scripts/apply_efinity_headless_patches.py --apply
```

This resolves the Efinity install root the same way every other OBBS script
does — `$EFINITY_HOME` if set, else `~/efinity/2026.1` — so it works
regardless of which user/home-directory the build runs under (an earlier
version of this doc hardcoded `/home/sipantichijk/...`, which silently did
nothing on any other machine, including root-owned build hosts).

To check patch status manually without applying anything:

```bash
python3 scripts/apply_efinity_headless_patches.py --check --root "$EFINITY_HOME"
```

The script patches call sites P1–P5: `clkmux_rule_adv.py` pll_reg=None
crash, `clock_rule_adv.py` osc_reg=None crash, `efx_run_pt_unified.py`
check_design() try/except wrap, and `design.py`'s `generate()` — corrected
to actually call `check_design()` and swallow its exception rather than the
disproven `if True:` bypass, see `.agents/memory/ti60-headless-lpf.md`.
Plus a best-effort optional P6 that neutralizes `efx_run_pt_unified.py`'s
`return PTFlowRunnerStatusCode.ERROR` after a design-check failure, applied
only when there is exactly one unambiguous match.

**P2, P3, and P4-5 are required.** P1 is best-effort/optional (like P6):
confirmed on a real Efinity 2026.1 install (2026-07) that some sub-builds'
`clkmux_rule_adv.py` already null-guard the PLL registry natively — no
unguarded `pll_reg.get_all_pll()` loop exists there at all — so a missing
anchor for P1 is reported as `SKIP`, not treated as a failure.

Each patch embeds a `church-headless-patch-v1` sentinel comment so re-running
`--apply` is a true no-op once patched, and each is verified with
`py_compile` (rolled back automatically if verification fails). If a
*required* patch's anchor text isn't found, the script fails loudly and
names the file — that means the installed Efinity version has drifted and
the anchor in `scripts/apply_efinity_headless_patches.py` needs updating,
rather than the old silent "Fail to generate outputs" you'd get from the
doc's manual snippets when they no-op on a path that doesn't exist. For the
optional P1/P6, a missing anchor is expected on some sub-builds and does not
fail the run.

Regression coverage: `scripts/test_build_guard.sh` Section G exercises this
patcher against a synthetic fixture tree (virgin apply, idempotent re-apply,
simulated version drift on a required patch, simulated missing anchor on
optional P1, `--check` mode, mixed pre-patched state) — no real Efinity
installation required.

---

### Interface Designer silently no-ops: empty `interface.log`, exit 0, no `.interface.csv`

Distinct from the crash-based failures above (which raise a visible Python
traceback). This one is quieter and more confusing: `efx_run --flow interface`
exits **0**, produces an `interface.log` with **no content**, and never
writes `<circuit>.interface.csv` — so `run_efx_pnr.sh`'s later step fails
with a generic "did not produce .interface.csv" error that gives no hint
why.

**Root cause:** `efx_run_pt_unified.py` (in `$EFINITY_HOME/scripts/`) does
`from device.service import DeviceService`, but the `device` package
actually lives under `$EFINITY_HOME/pt/bin/device/`, not next to
`efx_run_pt_unified.py` itself. Normally Efinity's own `bin/setup.sh` would
put the right paths on `PYTHONPATH`, but this OBBS deliberately does not
source `setup.sh` (it calls `exit` in non-interactive shells and would kill
the build script — see the Step -1 comment in `run_efx_pnr.sh`). Without
that, the import fails with `ModuleNotFoundError: No module named 'device'`
— and `efx_run.py`'s own code catches this with a bare
`except ImportError`, writes a one-line warning to its *log file only* (not
stdout/stderr), and returns as if nothing happened. Confirmed by reproducing
the import directly:

```bash
cd $EFINITY_HOME/scripts
$EFINITY_HOME/bin/python3 -c "import efx_run_pt_unified"
# Traceback ... ModuleNotFoundError: No module named 'device'
```

**Fix:** `run_efx_pnr.sh` now exports
`PYTHONPATH="$EFINITY/pt/bin${PYTHONPATH:+:$PYTHONPATH}"` before Step 0
(Interface Designer), alongside the other headless env vars
(`EFINITY_USER_DIR_INI`, `EFXPT_HOME`). No Efinity source patching needed —
this is a plain missing search path, not a broken/incomplete install.

Regression coverage: `scripts/test_build_guard.sh` Section H statically
asserts `run_efx_pnr.sh` exports this `PYTHONPATH` and that the export
appears before the Interface Designer invocation.

---

### Interface Designer fails: `ERROR, unusupported device Ti60F225 in Interface Designer`

(The misspelling is Efinity's own — not a typo in this repo.) This appears
*after* Interface Designer starts running (i.e. after fixing the PYTHONPATH
gap above), for a device name that is used consistently everywhere else in
the project — the project XML, `peri.xml`'s `device_def="Ti60F225"`, and
every other build script, including MAP synthesis, which succeeds with the
same device string.

**Root cause:** the device check calls
`DeviceService.is_device_exists()` → `get_device_map_file()`, which reads
`os.environ["EFXPT_HOME"]` and builds `"$EFXPT_HOME/db/devicemap.csv"`. The
real file lives at `$EFINITY_HOME/pt/db/devicemap.csv` — note the `/pt`.
Several OBBS scripts exported `EFXPT_HOME` as the *plain* Efinity root
(`EFXPT_HOME="$EFINITY"`, no `/pt`), so the CSV path never resolves,
`get_device_map_file()` silently returns `""`, and `is_device_exists()`
always returns `False` — regardless of whether the device name is valid.
Efinity's own `bin/setup.sh` defines `export EFXPT_HOME=$EFINITY_HOME/pt`;
that is the value every OBBS script must use.

**Fix:** every script that exports `EFXPT_HOME` (`run_efx_pnr.sh`,
`run_efx_map.sh`, `run_full_build.sh`, `run_efx_pgm.sh`, `build_b4.sh`,
`build_ti60.sh`, `build_and_flash.sh`) now sets it to `$EFINITY_HOME/pt`
(or the script's equivalent Efinity-root variable + `/pt`). No Efinity
source patching required.

Regression coverage: `scripts/test_build_guard.sh` Section I statically
asserts every OBBS script's `EFXPT_HOME` export ends in `/pt`.

---

### `efx_pgm` fails with `ERROR: Unknown device family ""`

**Efinity 2026.1 does not read family from the project XML or `--device` alone.**  
Always pass `--family Titanium` explicitly:

```bash
cd ~/church_project/SoC
export EFINITY_HOME=~/efinity/2026.1
$EFINITY_HOME/bin/efx_pgm \
    --source work_pnr/church_soc_cm.lbf \
    --family Titanium --device Ti60F225 \
    --mode active --width 1 --enable_roms smart \
    --spi_low_power_mode on --io_weak_pullup on \
    --oscillator_clock_divider DIV8 --bitstream_compression on
```

Or simply: `bash hardware/soc_combined/run_efx_pgm.sh`

### patch_sapphire_init.py prints `ERROR: no pattern found`

The sapphire.v does not contain either `$readmemb` or the expected inline
assignments for `ram_symbol0`. This means the file is a different version of the
Efinix Sapphire IP. Inspect it:
```bash
grep -n "ram_symbol\|readmemb" hardware/soc_combined/sapphire.v | head -20
```

### Boot loop — NIA=0x00000000 repeating in the IDE stream panel

**Symptom** — The 📡 Live UART Stream panel shows this pattern repeating:
```
← CHURCH Ti60 SoC+CM v1.1
NIA → 0x00000000
── REBOOT ──
← CHURCH Ti60 SoC+CM v1.1
NIA → 0x00000000
── REBOOT ──
```

**What it means** — The Church Machine starts, attempts its very first instruction at
address `0x00000000` (the start of the boot LUMP), faults immediately, and the firmware
reboots. The boot LUMP baked into the BRAM during synthesis is corrupted, stale, or
was compiled from an incompatible firmware version.

**Reading the fault data** — The IDE Connect tab CALLHOME line shows the fault detail:
```
CALLHOME valid: board=Ti60F225 fw=1.0 nia=0x00000000
```
The `nia=` field is the **fault NIA** from the previous boot stored in the CALLHOME
packet (`fault_nia` bytes 19–22). The IDE fault popup (visible in the simulator) maps
fault codes to mnemonics:

| HW Code | Mnemonic | Meaning at boot |
|---|---|---|
| `0x07` | `NULL_CAP` | Boot LUMP capability slot is zeroed — BRAM not patched |
| `0x08` | `BOUNDS` | Boot LUMP size field is wrong — firmware/LUMP mismatch |
| `0x09` | `VERSION` | Golden Token version field invalid — stale BRAM init |
| `0x0A` | `SEAL` | CRC seal on boot capability is broken — BRAM corrupted |
| `0x0B` | `INVALID_OP` | Opcode at address 0 is not a valid CM instruction |
| `0x0D` | `DOMAIN_PURITY` | Code/capability mixed in boot LUMP — layout error |

**Capturing full fault detail** — in the IDE Connect log, the CALLHOME line includes
`fault_code` (byte 18 of the packet). You can also read it directly via the APB3 bridge
while the board is halted:

```
APB offset 0x04 (STATUS) bit[1] = fault_valid
APB offset 0x0C (FAULT)  bits[4:0] = fault code  ← matches HW Code table above
APB offset 0x08 (NIA)              = faulting instruction address
```

**Fix** — the BRAM init is wrong. Re-run Steps 3–9 (firmware-only rebuild):

```bash
cd ~/church-machine
make -C hardware/soc_combined/firmware                    # Step 3
python3 scripts/patch_sapphire_init.py \                  # Step 4 — MUST NOT SKIP
  hardware/soc_combined/sapphire.v \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol0.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol1.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol2.bin \
  hardware/soc_combined/EfxSapphireSoc.v_toplevel_system_ramA_logic_ram_symbol3.bin
bash hardware/soc_combined/run_efx_map.sh                  # Step 6
bash hardware/soc_combined/run_efx_pnr.sh                  # Step 7
bash hardware/soc_combined/run_efx_pgm.sh                  # Step 8
sudo ~/oss-cad-suite/bin/openFPGALoader \
    -b titanium_ti60_f225_jtag \
    -f ~/church_project/SoC/outflow/church_soc_cm.hex      # Step 9
```

After a successful flash the stream panel should show:
```
← CHURCH Ti60 SoC+CM v1.1
NIA → 0x00000002
NIA → 0x00000005
...
```
(incrementing NIA addresses, no REBOOT)

Save the working hex as a good build:
```bash
cp hardware/soc_combined/outflow/church_soc_cm.hex \
   hardware/soc_combined/good-builds/church_soc_cm-$(date +%Y-%m-%d).hex
git add hardware/soc_combined/good-builds/
git commit -m "good-build: Ti60 $(date +%Y-%m-%d) — boot loop fixed"
```

---

### LED0 never lights after flash

The SoC is stuck in reset. Check that `io_asyncReset` in `top.v` is tied to
`1'b0` (not floating) and that the 50 MHz clock is reaching `CLKMUX_T ROUTE0`.

---

## Resource utilisation

The Ti60F225 has ~60 K logic elements and ~220 KB BRAM (176 EFX_RAM10 blocks).
The combined SoC+CM design uses ~180 EFX_RAM10 blocks. Check after synthesis:

```bash
python scripts/check_ti60_utilisation.py --missing-ok
```
