Skip to content

Commit 96d4b97

Browse files
authored
Merge pull request #23 from lcapossio/feature/gowin-support
Gowin (BRS-100-GW1NR9) support: EIO reference design, GUI/CLI over OpenOCD, and a hardware test suite
2 parents 92efc27 + f0431c1 commit 96d4b97

23 files changed

Lines changed: 864 additions & 125 deletions

docs/06_eio_core.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,18 @@ EIO wrapper with a non-default `CHAIN` parameter (see
161161
[chapter 04](04_rtl_integration.md)). The host's `chain` argument
162162
must match the RTL `CHAIN` parameter.
163163

164+
**Shared-chain EIO.** When EIO is address-muxed onto another core's chain
165+
instead of having its own, pass `base_addr` (the mux offset) so every register
166+
access is routed to the EIO window. The common case is Gowin `EIO_EN=1`, which
167+
muxes EIO onto the ELA chain at offset `0x8000`:
168+
169+
```python
170+
eio = EioController(transport, chain=1, base_addr=0x8000)
171+
```
172+
173+
The CLI equivalent is `--base-addr 0x8000`, and the desktop GUI discovers this
174+
location automatically on connect.
175+
164176
After `connect()`, the controller has cached `IN_W` and `OUT_W` from
165177
the bitstream's identity registers, so subsequent reads/writes know
166178
exactly how many words to scan.

docs/10_cli_reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,17 @@ wrote 0x55
359359
All three EIO subcommands take `--chain N` (default 3) to override
360360
the BSCANE2 USER chain. For mixed-manager designs where EIO shares USER1,
361361
also pass `--instance N` to select the EIO slot before each register access.
362+
For a **shared-chain** EIO that is address-muxed onto another core's chain
363+
(e.g. Gowin `EIO_EN=1` at offset `0x8000`), pass `--base-addr` instead:
362364

363365
```bash
364366
fcapz --backend hw_server --tap xc7a100t eio-probe --chain 1 --instance 2
365367
fcapz --backend hw_server --tap xc7a100t eio-write --chain 1 --instance 2 0x11
366368
fcapz --backend hw_server --tap xc7a100t eio-read --chain 1 --instance 2
369+
370+
# Gowin shared-chain EIO (chain 1, mux offset 0x8000):
371+
fcapz --backend openocd --tap GW1NR-9C.tap eio-read --chain 1 --base-addr 0x8000
372+
fcapz --backend openocd --tap GW1NR-9C.tap eio-write --chain 1 --base-addr 0x8000 0x15
367373
```
368374

369375
## AXI subcommands

docs/12_gui.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,18 @@ The leftmost (or topmost, depending on layout) panel. Holds:
8181
| **Backend** | Dropdown: `hw_server` (default) or `openocd` |
8282
| **Host** | TCP host of the transport, default `127.0.0.1` |
8383
| **Port** | TCP port, default `3121` for hw_server / `6666` for openocd |
84-
| **FPGA target** | hw_server target name (e.g. `xc7a100t`) or openocd TAP name |
84+
| **FPGA target / TAP** | hw_server target name (e.g. `xc7a100t`) or OpenOCD TAP name. For OpenOCD you can enter `auto` to use the first tap OpenOCD reports |
85+
| **[Scan]** | hw_server: lists XSDB JTAG targets. OpenOCD: lists tap names via `jtag names`. Click to fill the **TAP** field instead of typing it |
8586
| **Bitfile** | Optional path to a `.bit` file; if set, the GUI runs `fpga -file <bitfile>` and waits for the readiness probe before declaring "connected" |
86-
| **IR table** | Dropdown: `Xilinx 7-series` (default) or `Xilinx UltraScale / UltraScale+`. Maps to the `IR_TABLE_*` presets in [chapter 14](14_transports.md) |
87+
| **IR table** | Dropdown: `Xilinx 7-series` (default), `Xilinx UltraScale / UltraScale+`, or `Gowin (OpenOCD)`. Maps to the `IR_TABLE_*` presets in [chapter 14](14_transports.md) |
8788
| **[Connect] / [Disconnect]** | Open or close the underlying transport |
8889

90+
> **Gowin boards** connect over the **OpenOCD** backend (the FTDI/Gowin cable
91+
> driven by an already-running `openocd`): set Backend = `openocd`, Port `6666`,
92+
> TAP = your tap name or `auto`, IR table = **Gowin (OpenOCD)**. The ELA tab
93+
> works immediately, and **EIO is auto-discovered and attached** on connect (see
94+
> the EIO panel below) — no USER chain or mux offset to enter.
95+
8996
When you click **Connect**:
9097

9198
1. The GUI builds a `XilinxHwServerTransport` (or `OpenOcdTransport`)
@@ -266,15 +273,22 @@ adjacent identical values render as a flat line, not a sawtooth.
266273

267274
### EIO panel
268275

276+
On connect the GUI **auto-discovers and attaches** the EIO core: it probes the
277+
known locations — core-manager slots, then each USER chain at register offsets
278+
`0x0000` and `0x8000` — and attaches the first that reports the EIO identity
279+
(`'IO'`). You normally do not touch the controls below; they are a manual
280+
override.
281+
269282
For managed designs, the **Core** selector lists detected EIO cores as
270283
`core 2`, `core 3`, and so on, with the selected USER chain and manager
271284
slot shown beside it. Choosing a core attaches it immediately and shows
272285
the EIO identity / bus widths. The Arty multi-core reference uses chain
273286
**1**, slots **2** and **3**.
274287

275-
For legacy standalone EIO designs with no detected manager slot, the panel
276-
falls back to manual **JTAG chain**, optional **Managed slot**, and
277-
**Attach EIO** controls.
288+
For standalone or shared-chain EIO designs, the panel exposes manual
289+
**JTAG chain**, optional **Managed slot**, and a **Base** offset. A
290+
**shared-chain** EIO — e.g. Gowin `EIO_EN=1`, which muxes EIO onto the ELA
291+
chain — lives at chain **1**, Base **0x8000** (discovery fills that in for you).
278292

279293
For each input bit, **read-only checkboxes** update from
280294
`EioController.read_inputs()` while **Poll inputs** is checked.

docs/14_transports.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,24 @@ OpenOCD does **not** program the FPGA from this transport — you do
182182
that separately with `pld load`, an `init`-time script, or your own
183183
`openocd -c "...; init; pld load 0 my.bit; exit"`.
184184

185+
#### Tap auto-detect
186+
187+
The tap name must match a tap defined in the running OpenOCD config. Pass
188+
`tap="auto"` (or leave it empty) and `connect()` resolves it to the first name
189+
OpenOCD reports from `jtag names` — handy for single-FPGA chains where you don't
190+
want to hard-code the name. The helper `fcapz.transport.list_openocd_taps()`
191+
returns that list; the GUI's **Scan** button uses it to populate the TAP field
192+
for the OpenOCD backend.
193+
194+
#### Gowin over OpenOCD
195+
196+
Gowin boards use this transport with `ir_table=OpenOcdTransport.IR_TABLE_GOWIN`
197+
(ER1/ER2 → chains 1/2). The CLI auto-selects it for `--tap GW...`; in code,
198+
pass it explicitly. The ELA sits on chain 1; a shared-chain EIO (`EIO_EN=1`) is
199+
reached on chain 1 at base offset `0x8000`
200+
(`EioController(t, chain=1, base_addr=0x8000)`). See the
201+
[BRS-100-GW1NR9 example](../examples/brs_100_gw1nr9/README.md).
202+
185203
## IR table presets
186204

187205
Different Xilinx families use different IR opcodes for the BSCANE2

examples/brs_100_gw1nr9/README.md

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ BRS-100-GW1NR9 board. It targets the `GW1NR-LV9QN88PC7/I6` device and
55
instantiates the fpgacapZero Gowin ELA wrapper through the `GW_JTAG`
66
primitive.
77

8-
The design is deliberately small: it builds one ELA, drives the board LEDs
9-
from simple local status signals, and captures internal counters, buttons, and
10-
header I/O through a six-channel probe mux.
8+
The design is deliberately small but exercises two cores over the single
9+
`GW_JTAG` primitive: an **ELA** that captures internal counters, the user
10+
buttons, and header I/O through a six-channel probe mux, and an **EIO** that
11+
exposes the two user buttons (host-readable) and drives the six board LEDs
12+
(host-controlled over JTAG). The EIO shares the ELA's JTAG chain through the
13+
register-bus mux at offset `0x8000`.
1114

1215
## Files
1316

@@ -29,7 +32,8 @@ header I/O through a six-channel probe mux.
2932
- ELA sample width: 8 bits
3033
- ELA depth: 64 samples
3134
- ELA channels: 6
32-
- EIO: disabled in this example
35+
- EIO: enabled — 2 inputs (user buttons), 6 outputs (board LEDs)
36+
- EIO location: shares the ELA chain (chain 1) at register-bus mux offset `0x8000`
3337

3438
The ELA probe bus is split into six 8-bit channels:
3539

@@ -44,20 +48,13 @@ The ELA probe bus is split into six 8-bit channels:
4448

4549
## Board I/O
4650

47-
The LEDs are active-low at the pads and are driven from these internal status
48-
signals:
51+
The six LEDs are **driven by the EIO output register** — the host controls them
52+
over JTAG (`eio.write_outputs`). They are active-low at the pads; EIO
53+
`probe_out[5:0]` maps to LED[5:0].
4954

50-
| LED | Meaning |
51-
| --- | --- |
52-
| 0 | 1 Hz heartbeat |
53-
| 1 | JTAG activity indicator |
54-
| 2 | User button 0 |
55-
| 3 | User button 1 |
56-
| 4 | `pad_io[1]` |
57-
| 5 | `pad_io[2]` |
58-
59-
The two user buttons are active-low on the board and are sampled into the
60-
60 MHz domain before being exposed through ELA channel 1.
55+
The two user buttons are active-low on the board, sampled into the 60 MHz
56+
domain, and exposed two ways: through **EIO `probe_in[1:0]`** (host-readable via
57+
`eio.read_inputs`) and through **ELA channel 1** (captured into a waveform).
6158

6259
## Build
6360

@@ -133,13 +130,67 @@ fcapz --backend openocd --host 127.0.0.1 --port 6666 \
133130
For VCD output, change `--format json` to `--format vcd` and use a `.vcd`
134131
output path.
135132

133+
## Embedded I/O (EIO)
134+
135+
The EIO core shares the ELA's JTAG chain (chain 1) at register-bus mux offset
136+
`0x8000`. Read the buttons via `probe_in`, drive the LEDs via `probe_out`.
137+
138+
**Desktop GUI** (easiest): connect with backend **OpenOCD**, tap `GW1NR-9C.tap`
139+
(or `auto`), IR table **Gowin** — the GUI **auto-discovers and attaches** the
140+
EIO, so there is no chain or offset to enter. Toggle the six output bits to
141+
drive the LEDs, and enable *Poll inputs* to watch the buttons.
142+
143+
**CLI** — pass the shared-chain location explicitly (`--chain 1 --base-addr
144+
0x8000`):
145+
146+
```sh
147+
# Read the two buttons (probe_in)
148+
fcapz --backend openocd --host 127.0.0.1 --port 6666 \
149+
--tap GW1NR-9C.tap eio-read --chain 1 --base-addr 0x8000
150+
151+
# Drive the six LEDs (probe_out): light LED0/2/4
152+
fcapz --backend openocd --host 127.0.0.1 --port 6666 \
153+
--tap GW1NR-9C.tap eio-write --chain 1 --base-addr 0x8000 0x15
154+
```
155+
156+
**Python API:**
157+
158+
```python
159+
from fcapz.transport import OpenOcdTransport
160+
from fcapz.eio import EioController
161+
162+
t = OpenOcdTransport(port=6666, tap="GW1NR-9C.tap",
163+
ir_table=OpenOcdTransport.IR_TABLE_GOWIN)
164+
t.connect()
165+
eio = EioController(t, chain=1, base_addr=0x8000)
166+
eio.attach()
167+
print(eio.read_inputs()) # buttons (probe_in[1:0])
168+
eio.write_outputs(0x3F) # all six LEDs on
169+
```
170+
171+
## Hardware tests
172+
173+
`test_hw_integration.py` is an opt-in regression that drives the board over
174+
OpenOCD. Build + load the bitstream, start OpenOCD, then:
175+
176+
```sh
177+
openocd -f examples/brs_100_gw1nr9/brs_100_gw1nr9.cfg &
178+
python -m pytest examples/brs_100_gw1nr9/test_hw_integration.py -v
179+
```
180+
181+
It covers the ELA (identity, a counter capture with `+1` per-sample checks,
182+
trigger match, register roundtrip, JSON/VCD export) and the shared-chain EIO
183+
(identity/widths, auto-discovery, LED output roundtrip, button reads). Without a
184+
board the tests **skip** (OpenOCD unreachable or no fcapz design loaded); set
185+
`FPGACAP_SKIP_HW=1` to skip unconditionally. Override the port/tap with
186+
`FPGACAP_OPENOCD_PORT` / `FPGACAP_OPENOCD_TAP`.
187+
136188
## Notes
137189

138190
- This example uses Gowin register-path ELA readback, not Xilinx-style burst
139191
readback.
140192
- OpenOCD must already be running; the fpgacapZero OpenOCD transport does not
141193
program the FPGA.
142-
- The example does not include hardware regression tests yet.
143194

144195
## More Detail
145196

examples/brs_100_gw1nr9/brs_100_gw1nr9_top.v

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ localparam int DEPTH = 64;
2828
// Internal signals
2929
// ----------------------------------------------
3030

31-
reg [5:0] i_leds;
31+
wire [5:0] i_eio_leds; // host-driven LEDs (EIO out)
3232
reg [1:0] i_buttons;
3333
reg [32-1:0] i_pad;
3434

@@ -85,7 +85,12 @@ localparam int DEPTH = 64;
8585
.SAMPLE_W (SAMPLE_W),
8686
.DEPTH (DEPTH),
8787
.NUM_CHANNELS (CHANNELS),
88-
.EIO_EN (0)
88+
.EIO_EN (1),
89+
// NOTE: EIO shares the single GW_JTAG primitive with the ELA
90+
// (mux offset 0x8000). Host: EioController(t, chain=1,
91+
// base_addr=0x8000).
92+
.EIO_IN_W (2), // read the 2 user buttons
93+
.EIO_OUT_W (6) // drive the 6 LEDs
8994
) u_ela (
9095
.clk (i_jtagclk),
9196
// NOTE: this is a separate clock
@@ -98,10 +103,8 @@ localparam int DEPTH = 64;
98103
.sample_rst (i_sysclk_reset),
99104
.probe_in (i_probe),
100105

101-
.eio_probe_in (0),
102-
.eio_probe_out (),
103-
// NOTE: external trigger ports
104-
// tie off if not used
106+
.eio_probe_in (i_buttons), // host reads button state
107+
.eio_probe_out (i_eio_leds), // host drives LED state
105108

106109
.tms_pad_i (tms_pad_i),
107110
.tck_pad_i (tck_pad_i),
@@ -219,37 +222,12 @@ localparam int DEPTH = 64;
219222

220223
// NOTE: Leds
221224
// ------------
225+
//
226+
// EIO demo: all 6 LEDs are driven by the EIO output register, so the
227+
// host controls them directly over JTAG (eio.write_outputs). LEDs are
228+
// active-low on this board.
222229

223-
always @(posedge i_sysclk) begin
224-
if (i_second_tick == 1'b1) begin
225-
i_leds[0] <= ~i_leds[0];
226-
end
227-
228-
if (i_jtag_activity_jtagclk == 1'b1) begin
229-
// NOTE: this isn't proper CDC...
230-
// fit for demonstration only.
231-
232-
i_leds[1] <= 1'b1;
233-
end
234-
if (|i_millisecond_counter[6:0] == 1'b0) begin
235-
i_leds[1] <= 0;
236-
end
237-
238-
i_leds[3:2] <= i_buttons[1:0];
239-
i_leds[4] <= i_pad[0];
240-
i_leds[5] <= i_pad[1];
241-
242-
if (i_sysclk_resetn == 1'b0) begin
243-
i_leds <= 0;
244-
end
245-
end
246-
assign pad_leds_n = ~i_leds;
247-
// NOTE:
248-
// led[5]: i_pad[1]
249-
// led[4]: i_pad[0]
250-
// led[3]: i_buttons[1]
251-
// led[2]: i_buttons[0]
252-
// led[1]: JTAG Activity
253-
// led[0]: Heartbeat
230+
assign pad_leds_n = ~i_eio_leds;
231+
// NOTE: led[5:0] = EIO probe_out[5:0] (host-driven)
254232

255233
endmodule

examples/brs_100_gw1nr9/build_brs_100_gw1nr9.tcl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,6 @@ run pnr
8585

8686
puts "Deploying Bitstream"
8787
file mkdir $output_dir
88-
file copy -force ${project_dir}/impl/pnr/${project_name}.fs ${output_dir}/fcapz_brs_100_gw1nr9.fs
88+
# Gowin create_project nests the project under ${project_dir}/${project_name},
89+
# so PnR writes the bitstream to ${project_dir}/${project_name}/impl/pnr/.
90+
file copy -force ${project_dir}/${project_name}/impl/pnr/${project_name}.fs ${output_dir}/fcapz_brs_100_gw1nr9.fs

0 commit comments

Comments
 (0)