Architecture and contribution
Simulation architecture
Icestudio design (.ice)
│
├── ice-build/<design>/main.v ──► Verilog interface + board profile
└── ice-build/<design>/main.pcf or main.xdc
│ │
└── HDL nets ↔ FPGA pins ↔ board endpoints
│
▼
Verilator incremental build cache
│
generated C++ wrapper + native capture
│
▼
per-design library (.so / .dll / .dylib)
│
ctypes binding
│
▼
SimulationWorker (Qt thread)
│ compact SimulationFrame updates
┌──────────────────┴──────────────────┐
▼ ▼
Board view and controls Peripheral workbench
(SVG + layout JSON) (Lab + catalog manifests)
│
GPIO / temporal / VGA / edge streams
Lab connection: peripheral terminal → board endpoint → FPGA pin → HDL net
The native wrapper batches virtual FPGA cycles and publishes SimulationFrame objects. GPIO inputs, sampled outputs, temporal observations, and streaming sinks such as VGA remain separate data paths.
The generated C++ wrapper exposes input setters, output getters, clock stepping,
batched execution, temporal measurements, and streaming hooks through a native
ABI. Python binds the per-design shared library (.so, .dll, or .dylib)
with ctypes; SimulationWorker runs it off the GUI thread and delivers compact
frames to the board and workbench. Qt does not refresh at the FPGA clock rate.
Incremental builds are kept in a managed user cache, outside Icestudio's
ice-build directory.
GPIO inputs, sampled outputs, temporal observations for LEDs and displays, cycle-accurate VGA streaming, and edge streams/timed input for the development UART/SPI API are distinct paths within that runtime flow.
Project constraints map generated HDL names to physical board endpoints, so LEDs and controls work even when the HDL net is not named after its visual label. A peripheral may stay physically connected in a Lab when the current HDL does not use that board pin.
Main modules
app.py: application orchestration and build lifecycle.compiler.pyandbuild_cache.py: Verilator generation and incremental native builds.simulation.pyandsimulation_worker.py: native binding and background execution.virtual_lab.py: board/workbench composition.peripherals/catalog.pyandperipherals/manifest.py: declarative catalog.workbench/view.pyandworkbench/item.py: canvas interaction and rendered instances.wiring.py: Lab terminal-to-board-to-HDL resolution.
Board assets live together in fpga_lab/assets/boards/<board-id>/. Each board
directory keeps its definition, pin constraints, profile, layout, and SVG in
one place. The Alhambra II directory is the reference layout for future boards.
| File | Purpose |
|---|---|
board.json |
Board identity, physical endpoints, clock, and integrated controls. |
pinout.pcf or pinout.xdc |
Reference pin constraints; include exactly one. |
profile.json |
Native-model input and output port profile. |
layout.json |
Interactive controls, placement, and SVG reference. |
board.svg |
Scalable artwork. |
FPGALab consumes the project-specific main.pcf or main.xdc in the Icestudio
project's ice-build directory; a board package provides exactly one
pinout.pcf or pinout.xdc as its pinout reference. For XDC, FPGALab reads
literal set_property PACKAGE_PIN <pin> [get_ports {<port>}] assignments,
including bus bits and the -dict form. Other XDC commands are not evaluated.
Unsupported PACKAGE_PIN expressions produce an error rather than an empty map.
BoardCatalog discovers these directories and validates their required files,
definition, layout, profile, and pin constraints. Invalid packages are skipped with a
diagnostic. Its board_id is the directory name (for example, alhambra_ii);
the id in board.json remains the board's public identifier.
Labs store the public identifier in metadata.board_id. The GUI resolves it to
the package directory ID when opening a Lab; changing the board selector updates
that Lab metadata. Labs without this field use the default Alhambra II board.
An optional positive metadata.virtual_clock_hz overrides the board's clock_hz
for that Lab. Changing the board clears the override. Interface refresh and
temporal sampling are user-wide settings; --clock-hz is a session override.
In layout.json, a board LED may declare "role": "power" and a board button
may declare "role": "reset". These optional roles control the simulation
indicator and reset action without depending on the elements' signal names.
Select the element in Edit layout to assign its role; each role can belong to
only one element.
The controls.leds order in board.json determines the order of LED samples
published by the worker. Each endpoint name selects the corresponding visual
LED in layout.json; a board may declare any number of LEDs, including none.
External peripherals
The external peripheral API v1 documents the manifest fields, reusable renderers, package metadata, examples, validation, and installation. Use it when creating a shareable package. FPGALab does not load Python code from user peripheral folders.
There are two different locations:
# Bundled with FPGALab (core repository; may use an internal renderer)
fpga_lab/peripherals/<peripheral-id>/
├── manifest.json
└── icon.svg
fpga_lab/peripherals/renderers/<renderer>.py
# External/installable package (declarative; no Python code)
examples/peripherals/<peripheral-id>/
├── manifest.json
├── icon.svg
└── artwork SVG files referenced by the manifest
Add a bundled peripheral only when its behavior belongs in the core catalog.
For a shareable component, start in examples/peripherals/, validate the
folder, and install it from the catalog or package it as a ZIP. External
packages must use one of the renderers already provided by FPGALab.
Add a bundled peripheral
Create fpga_lab/peripherals/<id>/manifest.json and its licensed SVG resources. Define terminal direction, required connections, properties, visual renderer, size, and simulation mode. Reuse a generic renderer when possible; add a renderer class under fpga_lab/peripherals/renderers/ only when the behavior cannot be described by existing primitives.
See the API reference for the available renderers and their manifest fields.
Run the tests before opening a pull request:
pip install -e ".[dev]"
QT_QPA_PLATFORM=offscreen pytest -q
Keep source code, identifiers, and developer comments in English. User-visible text must pass through the translation layer.