fpga-led-probe-allocation · git:20260613.86cacb4 · 2026-06-13 · sha256 8c0d65b1e9bb4ee8

fpga-led-probe-allocation git:20260613.86cacb4A

Immutable. This exact content is served forever at /api/v1/blob/8c0d65b1e9bb4ee8.

---
name: fpga-led-probe-allocation
description: Codify FPGA LED probe allocation patterns (instantaneous / pulse-stretched / sticky / byte-display) and produce an FPGA-top template with comment-table mapping (LED → signal → expected behaviour per test stage). Use when a fresh agent has to verify a chip on a DE10-Lite-class board with no scope and only the on-board LEDs as visibility, and you want to allocate the LEDs systematically rather than guessing.
---

# FPGA LED probe allocation

## When to use

- The host machine has no oscilloscope / SignalTap on the rig.
- The user has a DE10-Lite, DE10-Nano, or similar board with a row of
  LEDs as the only on-board observability.
- A USB webcam (or phone) will be used to capture LED states for
  before/after diff via `device_camera_capture` /
  `device_camera_led_diff` (mcp-eda v0.99+).

## The four probe modes

Picking the right mode for each signal is the difference between an
LED that flashes too fast for a 30 fps webcam to catch and one that
holds long enough for diff-capture but doesn't lie about the system
state.

| Mode | Visible duration | Use when | Pattern |
|------|------------------|----------|---------|
| **instantaneous** | 1 cycle | signal is steady-state (FSM in IDLE, register holds value) | `assign LED[N] = signal;` |
| **pulse-stretched** | ≥ 5 ms | signal is a 1-cycle pulse you want to *see* | `pulse_stretch #(50000) u_st(.clk(clk_50m), .pulse_in(signal), .led_out(LED[N]));` |
| **sticky** | latched until reset / next event | signal is a "did this event ever happen?" flag | `always_ff @(posedge clk_50m) if (rst_n & signal) seen_q <= 1; assign LED[N] = seen_q;` |
| **byte-display** | a multi-LED column showing 8 bits of a byte | byte register snapshot | `assign LED[7:0] = byte_q;` (consumes 8 LEDs) |

## How to allocate

For a 10-LED board (DE10-Lite has LEDR[9:0]):

| LED | Mode | Signal | What it tells you |
|-----|------|--------|-------------------|
| LEDR[9] | sticky | `tx_done_q` | "RTL ever finished a TX packet" |
| LEDR[8] | sticky | `cmd_decoded_q` | "RTL ever decoded a cmd opcode" |
| LEDR[7:0] | byte-display | `last_response_byte` | most recent response byte the device sent |

That layout consumes all 10 LEDs and gives you (a) a "did anything
happen?" signal at LEDR[9:8], (b) the exact byte at LEDR[7:0]. For a
host-driven test you can sequence:

  1. Hold reset, capture (everything LOW = baseline).
  2. Release reset, send cmd 0x70.
  3. Capture again; LEDR[9:8] tell you the FSM at least executed a TX
     and decoded a cmd; LEDR[7:0] should show the response byte.

## Top-template (DE10-Lite)

The FPGA top should explicitly comment the LED table so a reviewer
glancing at any photo of the board can decode the state.

```verilog
//-----------------------------------------------------------------
// LED PROBE TABLE  (kept in sync with host capture script)
//
// LEDR[9]    sticky      tx_done_q          packet TX completed at least once
// LEDR[8]    sticky      cmd_decoded_q      RTL ever decoded a CMD
// LEDR[7:0]  byte-disp   last_response_byte most recent response byte
//-----------------------------------------------------------------
module fpga_top(
    input  CLK_50M,
    input  KEY_n_reset,
    inout  ID_BUS_PIN,
    output [9:0] LEDR
);
    // ... 2-FF synchronisers (see fpga_async_input_synchronizer_check) ...
    // ... DUT instantiation ...
    wire tx_done_pulse, cmd_decoded_pulse;
    wire [7:0] last_response_byte;

    reg tx_done_q, cmd_decoded_q;
    always @(posedge CLK_50M or negedge KEY_n_reset) begin
        if (!KEY_n_reset) {tx_done_q, cmd_decoded_q} <= 2'b00;
        else begin
            if (tx_done_pulse)     tx_done_q     <= 1'b1;
            if (cmd_decoded_pulse) cmd_decoded_q <= 1'b1;
        end
    end

    assign LEDR[9]   = tx_done_q;
    assign LEDR[8]   = cmd_decoded_q;
    assign LEDR[7:0] = last_response_byte;
endmodule
```

## Companion mcp-eda tools

| Tool | Use |
|------|-----|
| `mcp__eda-tools__device_camera_capture` | Snapshot LEDs to JPG with auto-exposure |
| `mcp__eda-tools__device_camera_led_diff` | Compare two captures, output per-LED state diff |

Capture once at reset (baseline) and again after the test stimulus;
diff to confirm the expected LEDs lit up.

## Anti-patterns (run the lint — do NOT eyeball these)

> **Doctrine (user, 2026-05-29):** 把修法寫進工具,而非寫進 prompt.
>
> The four deterministic structural anti-patterns below are now enforced
> by **`programs/fpga_led_probe_lint.py`** (17 pytest cases pin each
> rule + every no-false-alert guard). Run the program on your emitted
> top BEFORE claiming the LED allocation is sound — the prose below is
> the rationale, not the rule applicator.

```bash
python3 plugins/vibe-ic/programs/fpga_led_probe_lint.py \
    <your_fpga_top.v> \
    [--qsf <board.qsf>] \
    [--json fpga_led_probe_lint.json]
```

Exit codes: `0` = PASS / SKIP (nothing to lint); `1` = anti-pattern found;
`2` = usage error. The JSON `findings[]` carries `rule` / `file` / `line`
/ `detail` / `fix_hint` for each hit.

The four rules it flags (and ONLY these — no false alerts):

- **`instantaneous-on-pulse`** — Using `instantaneous` mode
  (`assign LED[N] = sig;`) for a 1-cycle pulse → the camera will never
  catch it. The lint recognises a pulse by a pulse-token name (deny-listed
  against held levels like `*_en` / `*_busy` / `*_state`) **or** the
  structural set-1/set-0 pulse shape; a pulse fed through `pulse_stretch`
  is NOT flagged.
- **`mode-mix-without-table`** — Mixing ≥ 2 of {pulse, sticky, byte}
  modes without a commented **`LED PROBE TABLE`** → reviewer cannot decode
  the photo. A single-mode top needs no table (not flagged).
- **`sticky-without-reset-clear`** — A sticky LED latch set to `1'b1` and
  driving an LED but never cleared on reset will be ON forever even if the
  test never stimulated the signal. The lint accepts both `if (!rst_n)
  reg <= 1'b0;` and group-clear `{a, b} <= 2'b00;`.
- **`shared-pin-vs-QSF`** — An LED bit driven in RTL with no matching
  `set_location_assignment ... -to LEDR[N]` in the supplied `.qsf` (some
  boards reuse LED pins for USB-Blaster / configuration). **Skipped
  entirely when no `--qsf` is supplied** — absence is never a false FAIL.

**AI judgment still required:** the lint enforces the *structural*
anti-patterns; YOU still choose the right mode per signal (is this signal
truly steady-state, a 1-cycle pulse, or an event flag?) and author a
PROBE TABLE whose human-readable "expected behaviour per test stage"
column actually matches the host capture sequence. The lint cannot judge
whether your mode *choice* matches the signal's real timing — only that
the code is internally consistent.

## Compliance

This skill emits a *template*. Validate with:

- `fpga_async_input_synchronizer_check` — ensure inputs are properly
  synchronised before driving FSMs that LEDs probe.
- `fpga_pullup_lint` — ensure tri-state pins have the correct pull-up
  declarations.

When camera capture is part of the verification flow, the LED probe
allocation should also be cited in `RESULTS.md` so a reviewer can map
the captured JPGs back to the design.

## Compliance gate (mandatory)

After producing your output, save it to a file and run:

```bash
python3 plugins/vibe-ic/_shared/skill_compliance_check.py \
    --requirements plugins/vibe-ic/skills/fpga-led-probe-allocation/compliance.yaml \
    <your_output_file>
```

Exit 0 = PASS, exit 1 = FAIL with specific missing elements listed.
`compliance.yaml` in the corresponding skill directory enumerates
every required element of your output: section headers, metadata fields,
handoff lines, tool invocations.

**Your task is not complete until the audit returns PASS.** Missing
elements are the single largest source of skill-execution non-determinism
across different agents.