A Gowin FPGA, modelled tile by tile, built into a Xilinx Artix-7 on a Digilent Arty A7-100T. After the Arty is programmed, the emulated Gowin chip is still blank. A Gowin bitstream, made with the open Gowin toolchain (yosys, nextpnr, apicula), is shifted into it over the Arty's USB-UART. That bitstream holds SERV, a bit-serial RISC-V, cut down to fit, which then prints
Hi, I'm Servant!
from inside the emulated Gowin chip, from inside the Artix-7.
Download: l8servant-dist.tar.gz: the Vivado
project (vivado/top.v,
vivado/gw_servtiny_arty.v, the constraints and the flow),
the prebuilt bitstream bit/top.bit, the shipped Gowin
configuration, the host tools, the toolchain script and its patches, and
the sources.
tar xzf l8servant-dist.tar.gz && cd l8servant
make run # program the Arty, shift the Gowin configuration in, print what SERV sends
Three layers. The Artix-7 is real silicon. The Gowin is a model, built out of Artix-7 LUTs and flip-flops. SERV is a Gowin bitstream: it exists only as the values of the Gowin's configuration bits.
┌───────────────────────────────────────────────────────────────────────────────┐
│ Arty A7-100T: Xilinx XC7A100T (bit/top.bit) │
│ │
│ USB-UART rx ──>┌─────────────────┐ 52 120 bits ┌─────────────────────────┐ │
│ 1 Mbaud │ uart_loader ├──────────────>│ GW1NZ-1-trim10-s2 │ │
│ │ shift chain + │ 2 x 512 rows │ the emulated Gowin │ │
│ │ block RAM rows ├──────────────>│ 9 x 10 tiles │ │
│ └────────┬────────┘ │ │ │
│ │ done: GSR off │ ┌───────────────────┐ │ │
│ └───────────────────────>│ │ servtiny (SERV) │ │ │
│ 100 MHz ──> 8 MHz ──> / 16 ──> pin 47 (clock) ──>│ │ a Gowin bitstream │ │ │
│ │ └───────────────────┘ │ │
│ USB-UART tx <── uart_retime <── pin 6 (tx) ──────┤ │ │
│ 115200 baud (~1 792 baud) │ │ │
│ LED 0 <────────────────── pin 8 (led) ─────┤ │ │
│ └─────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
The model is never a netlist of the loaded design. Every routing multiplexer, every LUT, flip-flop, carry chain and block RAM of the Gowin tile is built, whether the bitstream uses it or not, and only the configuration decides what they do. Any bitstream for this chip that the toolchain can place runs, without rebuilding the Arty side.
SERV bit-bangs its UART, so its baud rate is the fabric clock divided
by its loop (~1 792 baud here). The Arty side re-times it
(uart_retime: an 8N1 receiver at the design's bit time, a
one-byte buffer, a transmitter at 115200), so the host hears a standard
rate with any terminal.
What you need: an Arty A7-100T on USB, Linux, python3
with pyserial, and permission to use the serial port (the
dialout group). The archive carries its own
openFPGALoader; if it does not run on your host, it is
built from the source in tools/src/ (or install
openfpgaloader from your distribution).
There are two FPGAs to program, one inside the other:
your PC ── USB (JTAG) ──> step 1: the Arty's FPGA (Xilinx Artix-7) <- bit/top.bit: the emulated Gowin FPGA
── USB (UART) ──> step 2: the Gowin FPGA emulated inside it <- cfg/<design>: a Gowin bitstream
Step 1, make prog-arty (=
make prog): openFPGALoader writes bit/top.bit
into the Artix-7 over JTAG. The Arty now holds an emulated Gowin
GW1NZ-1-trim10-s2, still blank (LED 1 off). This is needed once after
power-up; the Artix-7 forgets it when the power goes.
Step 2, make prog-gowin:
tools/send_fs.py sends a Gowin bitstream over the Arty's
USB-UART at 1 Mbaud into the emulated Gowin's configuration port: the
SYNC word, the 52 120 configuration bits, the block RAM rows (11 635
bytes, 0.12 s). The Gowin design starts at once (LED 1 on). Step 2 can
be repeated as often as you like, with the same or another design: a new
SYNC resets the emulated Gowin and loads the new bitstream, no step 1
needed.
make run # the menu: step 1, then choose a Gowin bitstream for step 2 (and another, ...)
make prog-arty # step 1 only
make prog-gowin # step 2: "Hi, I'm Servant!" (DESIGN=servtiny, the default), then listen
make prog-gowin DESIGN=blinky # step 2: the LED blinker (LD4 blinks about once a second)
make prog-gowin FS=top.fs PNR=pnr.json # step 2: your own Gowin design (see "Your own designs")
make reset-gowin # reset the emulated Gowin to a blank chip (an empty configuration)
make listen # only listen at 115200
make bit # rebuild bit/top.bit with Vivado from vivado/ (about 40 minutes)
SERIAL=<FTDI serial> picks one board of several,
TTY=/dev/ttyUSBn the port, SECS= the listening
time.
On the board:
| LED 0 | the Gowin design's LED pin (pin 8) |
| LED 1 | the fabric is configured |
| LED 2 | the fabric clock divided by 2^16: blinks while the Gowin runs |
| LED 3 | block RAM rows have arrived |
| BTN 0 | held: SERV's reset (pin 5 low) |
| BTN 1 | the loader starts over (the same as a new SYNC from
make prog-gowin), the fabric back to its global
set/reset |
The loader holds SERV in reset for about half a second after the last
byte, so the host has time to switch its port from 1 Mbaud to 115200
before the first character. BTN 0 restarts SERV: it prints
again. Any terminal at 115200 8N1 works for listening
(picocom -b 115200 /dev/ttyUSB1).
On the Arty A7-100T (2026-10-27): make run programs the
board, shifts the 11 635 bytes of configuration in (0.12 s at 1 Mbaud)
and listens at 115200:
cfg/servtiny/servtiny.hex: 52120 configuration bits (6515 bytes) + 1024 block RAM rows = 11635 bytes
sent in 0.12 s
Hi, I'm Servant!
[17 bytes received at 115200 baud]
BTN 0 resets SERV: it prints again.
The base is the Gowin GW1NZ-1 (1 152 LUT4, 864
flip-flops, 4 block RAMs, 11 x 20 tiles), the chip of the Sipeed Tang
Nano 1K. It does not fit an Artix-7 100T as a whole: built tile by tile
it needs about 176 000 LUT6. So the emulated chip is a
virtual Gowin, made from GW1NZ-1 by
chipdb/variant.py, which rewrites apicula's chip
database:
col 0 col 1 col 2 col 3 col 4 col 5 col 6 col 7 col 8 col 9
┌───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┬───────┐
row 0 │ corner│ │ │ │ │ │ IO AB │ IO AB │ IO AB │ IO AB │
├───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
row 1 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
row 2 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
row 3 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
├───────┼───────┴───────┴───────┼───────┴───────┴───────┼───────┼───────┼───────┤
row 4 │ left │ BSRAM 0 (3 tiles) │ BSRAM 1 (3 tiles) │ - │ - │ - │
├───────┼───────┬───────┬───────┼───────┬───────┬───────┼───────┼───────┼───────┤
row 5 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
row 6 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
row 7 │ left │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │ CFU │
├───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┼───────┤
row 8 │ corner│ bottom│ bottom│ bottom│ bottom│ bottom│ bottom│ bottom│ bottom│ corner│
└───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┴───────┘
| GW1NZ-1 | GW1NZ-1-trim10-s2 | |
|---|---|---|
| tiles | 11 x 20 = 220 | 9 x 10 = 90, 21 tile types |
| CFU (logic tiles) | 144 | 54 |
| LUT4 / flip-flops | 1 152 / 864 | 432 / 324 |
| block RAMs (18 Kbit) | 4 | 2 |
| package pins | QFN48 | 8 of its QFN48 pins (IO pairs at the top) |
| routing mux sources | up to 28 | up to 10 |
| nets | 28 868 | 14 688 |
| configuration bits in the model | ~582 000 fuses | 52 120 cells |
| IDCODE | 0x0100681B | 0x0100F81B (own) |
The model keeps the Gowin's own structure: the same tiles, the same
wire names, the same fuses. Its bitstreams are ordinary Gowin
.fs files, written by gowin_pack for the
virtual device.
The Artix-7 side (vivado/, 8 MHz host clock): 54 228 of
63 400 (85.53 %) LUTs, 48 497 of 126 800 (38.25 %) flip-flops, no block
RAM tiles (the Gowin block RAMs are LUT-RAM columns); worst setup slack
0.446 ns (the fabric's own paths are false paths: the emulation samples
them once a fabric half period, 16 host clocks).
The fabric is a GENERATOR, not one hand-written chip.
chipdb/extract.py reads the routing graph from nextpnr's
own Gowin generator and the fuses from apicula;
chipdb/gen_l8.py writes one component per tile type and the
grid that wires them. The tiles are built from reusable components
(src/model/components/):
| component | what it is |
|---|---|
cfgbit |
a configuration word: a flip-flop per bit here; a latch or an SRAM cell in a real chip |
luts/lut4 |
LUT4: 16 configuration bits, a 4-input mux |
alu/alu |
the Gowin ALU mode of a LUT pair: LUT4 + LUT2 + hard carry, the ten vendor modes |
ff/dff |
DFF / DFFE / DFFS / DFFR / DFFP / DFFC / DFFN...: one component, the mode in fuses |
widemux/mux2_lut |
MUX2_LUT5..8, the wide multiplexers between LUTs |
router/rmux |
a routing multiplexer as the silicon has it: each source selected by its own fuse pattern |
router/smux |
the compact form used here: the fuse pattern stored as an index, one indexed mux |
bsram/bsram |
the 18 Kbit block RAM: SP / SDP / DP, widths 1..36, read modes, byte enables, a load port |
iob/iob |
an IO pair: input, output, output enable |
A CFU tile = 8 LUT4 (4 slices, ALU-capable, wide muxes) + 6 flip-flops + its routing multiplexers (about 127 of them, each with its own fuses). The emulated flip-flops sample their ROUTED clock with the host clock: the fabric clock is a net like any other, it arrives through the Gowin's clock spines, and its edge is detected. That is why the fabric clock (500 kHz) is a fraction of the host clock (8 MHz): a routed clock must reach every flip-flop within one host clock, through up to dozens of emulated routing multiplexers.
A Gowin .fs is turned into what the model stores by
chipdb/fs_to_fuses.py: one bit per configuration cell (52
120, in the order of gen/.../fuses.txt), and the block
RAMs' initial contents as 512 rows of 36 bits each
(bsram_init.py is the inverse of apicula's init map). The
routing fuses of each multiplexer are decoded to the index of the
pattern they select (exact match, as the silicon does).
On the Arty, uart_loader is the configuration port, like
a real FPGA's, in small:
host (tools/send_fs.py) Arty: uart_loader fabric
┌───────────────────────┐ 1 Mbaud ┌────────────────────────────┐
│ 6 515 bytes: ├───────────>│ 8N1 receiver │
│ the configuration │ │ phase 0: each byte shifts │ cells ┌───────────┐
│ bits, LSB first │ │ the chain by 8 cells ├──────>│ tiles │
│ │ │ phase 1: 5 bytes = a row, │ rows │ block RAM │
│ 1 024 rows x 5 bytes │ │ written by the load port ├──────>│ load port │
└───────────────────────┘ │ phase 2: GSR released │ └───────────┘
└────────────────────────────┘
11 635 bytes in all, about 0.12 s at 1 Mbaud. The host then switches
its port to 115200 and listens. BTN 1 starts over: a new
bitstream can be shifted in without reprogramming the Arty.
A Gowin routing multiplexer has a default connection: with none of its fuses set, a LUT's D input, for example, is wired to the F output of a neighbouring LUT in the same CLS. nextpnr leaves unused LUT inputs alone, so in servtiny's bitstream hundreds of them carry such a default. Through a carry chain a few of them close a ring (F2 → D1 → COUT1 → CIN2 → F2): harmless in silicon, where the LUT ignores a don't-care input. In the emulation the truth table is a multiplexer tree over configuration flip-flops, so a toggling don't-care input can make it glitch, and the ring can oscillate. That was the last bug on the way to the first "Hi": the fabric simulated correctly, but on the Artix-7 the UART pin sat at 0.
chipdb/loops.py finds such rings in a configuration
(strongly connected components of the switched-on routing and the LUT
inputs the truth tables depend on).
chipdb/fs_to_fuses.py --pnr pnr.json opens the default
connections into LUT data inputs that nextpnr did not route (1 826 in
servtiny), which leaves 0 loops. Clock enables and set/resets keep their
defaults: nextpnr relies on them.
The program is SERV's own hello_uart.S, built from
source with clang/lld for rv32i: a bit-banged UART on one GPIO bit
(start bit, 8 data bits, stop bit, two nops a bit), 216
bytes.
#define GPIO_ADDR 0x40000000
#define HALT_ADDR 0x90000000
/*
a0 = GPIO address
a1 = String address
t0 = Character to write
*/
.globl _start
_start:
/* Load gpio address to a0 */
li a0, GPIO_ADDR
/* Set GPIO high initially */
addi t0, zero, 1
sb t0, 0(a0)
/* Load string address to a1 */
la a1, str
next_char:
/* Read char from string */
lbu t0, 0(a1)
/* If zero, we reached end of string and will exit the simulation */
beqz t0, halt
/* Bitbanged UART loop */
ori t0, t0, 0x100
slli t0, t0, 1
1: sb t0, 0(a0)
srli t0, t0, 1
/*
* Adding delay nops to achieve an approximate
* baud rate of 57600 at 16MHz
*/
nop
nop
bnez t0, 1b
/* Increase address to next char in string */
addi a1, a1, 1
j next_char
/* Writing anything to HALT_ADDR will exit simulation */
halt: li t0, HALT_ADDR
sw zero, 0(t0)
/*
* Loop to prevent PC from keep reading memory in case
* we run on real HW and not in simulation
*/
j halt
str:
.section .data
.string "Hi, I'm Servant!\n"
One bit is 279 fabric clocks (SERV takes 32+ clocks per instruction):
at the fabric's 500 kHz about 1 792 baud, which the Arty side re-times
to 115200. The same bitstream on the same emulated fabric, simulated
with Verilator (every tile, every routing mux, no netlist of the
design), sends H and i within the first 6 000
fabric clocks.
SERV's reference SoC, servant, does not fit 432 LUT4. The steps, each placed and routed by nextpnr:
| step | what changed | LUT4 | ALU | DFF | BSRAM |
|---|---|---|---|---|---|
| servant | upstream SoC: SERV + CSRs, 4 KB RAM, timer, GPIO, register file inferred as DP x2 | 513 | 98 | 260 | 3 |
| servant lean | no CSRs (with_csr 0), 1 KB RAM; register file moved to
ONE SDPB (serv_rf_ram_sdpb.v: GW1NZ-1 has no dual port at
width 2) |
361 | 64 | 209 | 2 |
| servmin | servile (SERV + its bus arbiter) + RAM + a 1-bit GPIO; no timer, no peripheral mux | 347 | 30 | 176 | 2 |
| servtiny | serv_top with our own glue: SERV never fetches and
loads at once, so instruction and data share ONE RAM port (address
muxed, acked a clock later); GPIO on address bit 30; the GPIO bit stored
inverted, so the UART line idles high without a set-type flip-flop |
298 | 30 | 175 | 2 |
servtiny on GW1NZ-1-trim10-s2: 298 of 432 LUT4 (68 %), 175 of 324
DFF, both block RAMs, routed at 52.3 MHz (nextpnr seed 2; the 10-source
muxes route it only with some seeds). Its RTL prints the same text with
the same UART edges as servant under iverilog
(designs/servant/sim).
Any design that fits 432 LUT4 / 324 DFF / 2 block RAMs and uses the 8 pins can be loaded. The toolchain is built from source, with two small patches that teach it the virtual chips:
chipdb/patches/nextpnr-gw1nz0.patch: nextpnr's Gowin
device list gains GW1NZ-0, GW1NZ-1-trim8/12/16, -trim12-s, -trim10-s2,
-trim12-s2; its chip generator reflects U-turn wires more than once (a
cropped grid is only 9 rows high) and adds wires a trimmed mux no longer
reaches; the cropped IO rows are "simple IO" rowschipdb/patches/apicula-0.35-gw1nz0.patch:
gowin_pack treats the virtual chips as GW1NZ-1 class, keeps
banks whose corner tiles were cropped away (no bank fuses), and
bslib knows the virtual IDCODE (0x0100F81B, 480-bit
frames)chipdb/variant.py: writes the virtual chips' databases
into apicula (crop + trim); nextpnr's chip databases are generated from
them, so it runs before nextpnr is builttoolchain/build.sh does all of it: it clones yosys,
nextpnr and iverilog at the commits used here, fetches the apycula 0.35
source and Boost 1.86 / Eigen 3.4 sources, builds numpy, msgspec and
fastcrc from source in a venv, applies the patches, writes the virtual
chips and builds nextpnr for them. Nothing prebuilt is executed.
make toolchain # = sh toolchain/build.sh ~/git/gowin-tools ~/opt/gowin-tools (~30 min)
make servtiny # SERV cloned at its pinned commit, servtiny.fs built again (same bytes)
make load-fs FS=out/servtiny/servant_gw1nz.fs PNR=out/servtiny/pnr.json
For a design of your own:
P=~/opt/gowin-tools
$P/bin/yosys -p "read_verilog top.v; synth_gowin -nolutram -top top -json top.json"
LD_LIBRARY_PATH=$P/lib $P/bin/nextpnr-himbaechel --json top.json --write pnr.json \
--device GW1NZ-LV1QN48C6/I5 --vopt family=GW1NZ-1-trim10-s2 --vopt cst=top.cst
$P/venv/bin/gowin_pack -d GW1NZ-1-trim10-s2 -o top.fs pnr.json
make load-fs FS=top.fs PNR=pnr.json # fs_to_fuses.py --pnr -> cfg/top/, then shifted in over the USB-UART
The pins that exist (designs/servant/servant_crop.cst):
QFN48 pin 47 is the clock (500 kHz from the Arty), pin 5 the reset
button (BTN 0, active low), pin 6 goes to the USB-UART's
tx, pin 8 to LED 0. Use -nolutram: the LUT-RAM mode
(RAM16SDP4) is not modelled yet.
Implemented, and exercised by servtiny's bitstream:
Not modelled yet (they read 0 or are ignored): the LUT-RAM mode (RAM16SDP4), the PLL and the internal oscillator (OSCZ), DCS / DQCE clock selection and the clock dividers, the IO logic (IOLOGIC: DDR, serdes), IO standards and drive strengths, the user flash, JTAG and the vendor configuration commands.
What could be emulated with the same generator:
variant.py presets), new mixes of tiles; the toolchain
targets them after one rebuild of nextpnrThe fabric is written as plain synthesizable RTL for a REAL chip, not only for emulation: the same components, emitted to Verilog, lint clean with Verilator and synthesize with yosys' generic flow. The steps from here to silicon:
cfgbit is a
flip-flop here. In an ASIC it becomes a latch or a 6T SRAM cell, written
by a frame register and row enables (the Gowin's own SRAM configuration:
frames of 480 or 608 bits), not a shift chain. 52 120 cells x 6T is
small next to the logic.smux (indexed) is for
the emulation; the silicon form is rmux, one pass-gate or
transmission-gate per fused source, sized for the wire it drives. A
custom cell per tile type, laid out once, abutted (the tiles are
regular: 21 types for 90 tiles).bsram becomes an SRAM macro
(18 Kbit, two ports) from the PDK's memory compiler, with the model's
mode logic around it.iob..fs that runs here runs on the netlist of the ASIC in
simulation, and on the real GW1NZ-1 for the designs that fit both.The result would be an FPGA whose bitstream format and toolchain already exist: a Gowin-compatible open FPGA.
| path | what |
|---|---|
bit/top.bit |
the Arty bitstream: the emulated GW1NZ-1-trim10-s2 + the configuration port |
cfg/servtiny/ |
the shipped configuration: servtiny.hex (52 120 bits),
bsram_init.hex (1 024 rows) |
vivado/ |
the Vivado project: top.v,
gw_servtiny_arty.v (the fabric, emitted),
top.xdc, top.tcl |
tools/send_fs.py |
the host side of the configuration port, then the listener at 115200 |
tools/openFPGALoader |
the programmer (+ its source in tools/src/) |
chipdb/ |
fs_to_fuses.py (.fs to configuration),
loops.py (rings a configuration closes),
bsram_*.py, variant.py (the virtual chips),
patches/ |
toolchain/build.sh |
the open Gowin toolchain from source, patched |
gen/gw1nz_1_trim10_s2_c/ |
the device's tables: fuse order, mux patterns, pads |
designs/servant/ |
servtiny: the top, the register file on one SDPB, the pins,
hello_uart.S, the RTL golden |
src/model/ |
the model's sources: components, tiles, the generated device, the loader, the re-timer, the board top |
SERV is by Olof Kindgren (ISC licence,
designs/servant/SERV.LICENSE). apicula, yosys and nextpnr
are their authors' (MIT / ISC); the patches here are small and under the
same licences.