The probe FPGA contains an Axon Controller, which routes packets between probe ASICs and the headstage CPU. The gateware component of the Axon Peripheral SDK supports development of interface modules between the ASIC and the Axon Controller over two AXI-Stream Interfaces.
This page covers the module's architecture, the peripheral.yaml file which describes the module's configuration, the main state machine, as well as the provided tools to simulate and build the gateware.
The SDK supports several different board targets. For complete specifications for your project, including the FPGA part number, available clocks, and resource constraints, see Board Profiles.
Module architecture
By default, the module consists of five ports, and uses two AXI-Stream interfaces with a framed message format to communicate with the host.
Module ports
By default, the module exposes 5 ports:
| Port | Direction | Width | Notes |
|---|---|---|---|
clk | input | 1 | Main clock. 40 MHz on via-devkit. |
rst | input | 1 | Synchronous, active-high reset — not rstn. Clear all state while it is high. |
periph_addr | input | 32 | This peripheral's address. The transport already routes frames to and from your module, so most peripherals — including the example — leave it unused. |
rx_axis | axi4_stream_interface.secondary | — | Frames in. Your module consumes this stream. |
tx_axis | axi4_stream_interface.main | — | Frames out. Your module produces this stream. |
module axon_test_source_peripheral_top (
input logic clk,
input logic rst,
input logic [31:0] periph_addr,
axi4_stream_interface.secondary rx_axis,
axi4_stream_interface.main tx_axis
);More ports can be added using the peripheral.yaml configuration file in order to establish physical communication with an external ASIC. This process is described in the Project Configuration section below.
The AXI-Stream interfaces
Both rx_axis and tx_axis are instances of axi4_stream_interface with the following signals and widths:
| Signal | Width | Meaning |
|---|---|---|
tdata | 32 | One 32-bit word per beat. |
tkeep | 4 | Valid-byte mask for tdata. Payload is 32-bit aligned, so this is 4'hF on full words. |
tvalid | 1 | The producer asserts it when tdata is valid. |
tready | 1 | The consumer asserts it when it can accept a beat. A beat transfers when tvalid && tready. |
tlast | 1 | Asserted on the final beat of a frame. |
tid | 8 | Stream ID, carried through framing. |
tdest | 1 | Destination routing bit. |
tuser | 1 | Sideband user bit. |
The modport fixes the direction of every signal for you:
- On
rx_axis(.secondary), your module readstdata,tkeep,tvalid,tlast,tid,tdest,tuserand drivestready. Asserttreadywhen you can accept a beat. - On
tx_axis(.main), your module drives all signals and readstready. Holdtvalidwith validtdatauntil you seetreadyhigh — that is when the beat is taken.
A beat moves on any cycle where both tvalid and tready are high. Hold tvalid and tdata steady until the beat has been accepted.
Frame format
A frame is a header beat followed by zero or more payload beats.
31 16 15 0
+------------------+------------------+
beat 0: | msg_type | len (bytes) | <- header
+------------------+------------------+
beat 1: | payload[0] |
+-------------------------------------+
beat 2: | payload[1] |
+-------------------------------------+
| ... |
+-------------------------------------+
beat N: | payload[N-1] | tlast = 1
+-------------------------------------+- Beat 0 is the header. The high 16 bits are
msg_type, the low 16 bits arelen, the payload length in bytes. - Beats 1..N are the payload, packed into 32-bit words.
N = len / 4— the payload is always 32-bit aligned. - The payload is little-endian: the first payload byte is bits
[7:0]of beat 1, the second is bits[15:8], and so on. tlastis asserted on the final payload beat. A header-only frame (len == 0) assertstlaston beat 0.
Note: The payload is capped at 1 KB — 256 32-bit words (len ≤ 1024 bytes). This is the largest packet the driver software will send or receive in a single frame; split anything larger across multiple frames.
msg_type is your peripheral's command or event opcode. You define the table of opcodes and declare them in peripheral.yaml under api.msg_types. Your module reads the header word, decides what the frame is, and acts on the payload that follows.
When you frame data back out on tx_axis, you build the same structure: emit a header beat with your msg_type and byte length, then the payload words, asserting tlast on the last one. The SDK handles everything upstream and downstream of your module — you only see and produce these frames.
periph_addr
periph_addr is the 32-bit address assigned to your peripheral instance. The SDK transport (the encap/decap logic in via_top) already routes inbound frames to the right peripheral and stamps the source address on outbound frames, so your module does not need periph_addr for normal framing — the example leaves it unused. It is exposed for advanced cases where your RTL needs to know its own address.
Project configuration
peripheral.yaml is the configuration schema for the project, defining ports, message types, and other configuration options. This file is used to autogenerate the top module's scaffolding, including port definitions and a default state machine. The top-level definition includes the schema format, the board target (see Board Profiles), a project description, and a list of all included peripherals.
Project
schema_version: 1
target_profile: via-devkit # see Board Reference page
project:
name: gateware # required
description: "" # optional
build:
radiant_version: "2024.2" # required
peripherals:| Field | Type | Required | Notes |
|---|---|---|---|
schema_version | int | yes | Must be 1. |
target_profile | string | yes | The board profile. |
project | object | yes | See below. |
build | object | yes | See below. |
peripherals | list | yes | One or more peripheral entries. |
peripherals[]
Each entry in the peripheral list describes one peripheral:
- name: axon_test_source
type: record
fpga:
module: axon_test_source_peripheral_top
sources:
- src/axon_test_source_peripheral.sv
constraints:
sdc: []
pdc: []
io: []
api:
msg_types:
# Set stream parameters: payload word0 = channel_count, word1 = sample_period (clk ticks).
- name: CONFIGURE
value: 0x0052
dir: host_to_fpga
payload_bytes: 8
# Begin streaming DATA_FRAME packets.
- name: START_STREAM
value: 0x0054
dir: host_to_fpga
payload_bytes: 0
# Stop streaming.
- name: STOP_STREAM
value: 0x0055
dir: host_to_fpga
payload_bytes: 0
# One frame of channel_count dummy words (free-running counter; low 16 bits are the sample).
- name: DATA_FRAME
value: 0x0056
dir: fpga_to_host
payload_bytes: variable
verification:
cocotb_tests:
- test/tb/test_axon_test_source.py
sv_tests: []
dev_peripheral_id: 0xf001| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | A valid identifier (^[A-Za-z][A-Za-z0-9_]*$). Names files and the SV module. |
dev_peripheral_id | int | no | Optional — omit it and generate allocates the lowest free ID and writes it back. If you set it, it must lie in 0xF001..0xFFFE. |
type | enum | yes | record or stim. |
description | string | no | Free text. |
vendor | string | no | Free text. |
fpga | object | yes | The gateware block. See below. |
api | object | no | Declares msg_types. Defaults to empty. |
verification | object | no | Lists tests. Defaults to empty. |
The name flows through to the SystemVerilog module, the source file names, and the driver registration — the example's axon_test_source is what you change to make the peripheral your own, and its dev_peripheral_id must match the ID the driver registers. To add a second peripheral, add another entry here and run synapsectl peripherals gateware generate.
fpga
The fpga block names your top module and its sources, and declares the pins it drives.
| Field | Type | Required | Notes |
|---|---|---|---|
module | string | yes | The SV top module name (a valid identifier). Must match your module declaration exactly. |
sources | list of strings | no | Paths to your SystemVerilog sources, relative to the project root. |
constraints | object | no | sdc and pdc lists of constraint file paths. |
io | list | no | Pin declarations. See below. |
fpga.io[]
Each entry adds a port to your module. Pin placement for these ports is something you add yourself, in the user-append region of src/scir_sdk.pdc (see Constraints):
fpga:
module: axon_test_source_peripheral_top
sources:
- src/axon_test_source_peripheral.sv
io:
- name: spi_sclk
dir: output
pin: A7
iostd: LVCMOS18
- name: spi_miso
dir: input
pin: B7| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Port name (a valid identifier). |
dir | enum | yes | input, output, or inout. |
pin | string | yes | The package pin. |
iostd | string | no | I/O standard, e.g. LVCMOS18. The via-devkit banks run at 1.8 V (bank 3 at 1.2 V), so use a compatible standard — LVCMOS18, LVCMOS18H, or LVDS. |
drive, slew, pull | string | no | Optional electrical attributes. |
api.msg_types[]
This section declares the opcodes your peripheral understands or emits:
api:
msg_types:
- name: CONFIGURE # word0 = channel_count, word1 = sample_period
value: 0x0052
dir: host_to_fpga
payload_bytes: 8
- name: START_STREAM
value: 0x0054
dir: host_to_fpga
payload_bytes: 0
- name: STOP_STREAM
value: 0x0055
dir: host_to_fpga
payload_bytes: 0
- name: DATA_FRAME # one frame of channel_count samples
value: 0x0056
dir: fpga_to_host
payload_bytes: variable| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | A valid identifier. |
value | int | yes | The 16-bit opcode (0x0000..0xFFFF) carried in the frame header. |
dir | enum | yes | host_to_fpga or fpga_to_host. |
payload_bytes | int or "variable" | yes | Fixed byte count, or the literal variable. |
verification
verification:
cocotb_tests:
- test/tb/test_axon_test_source.py # run by `synapsectl peripherals gateware sim`
sv_tests: []Module Generation and Development
After editing peripheral.yaml, regenerate the top module so that your new module, ports, and pins are reflected in the generated top and constraints:
synapsectl peripherals gateware generateThis allocates any included dev_peripheral_id, re-emits the generated files, and validates the project. It does not touch your peripheral SystemVerilog or the user-append region of the constraint files. If you hand-edited a generated file, generate refuses to overwrite it until you pass --force.
Once you have generated the module scaffolding, you can begin writing custom RTL. Your RTL should replace the example's logic with a state machine of your own. At minimum it should:
- Capture the header on the first beat and decode
msg_typeagainst the command table you define. - Drive your chip, its SPI clock and pins, in response. Declare every chip-facing pin in
peripheral.yaml(above). - Frame your chip's samples back out on
tx_axis: emit a header beat, then payload beats, assertingtlaston the last.
Simulation and Constraints
Simulation
The Axon Peripheral SDK provides infrastructure for simulation of the RTL against a testbench.
synapsectl peripherals gateware simThis runs the cocotb suites listed in verification.cocotb_tests. Edit test/tb/test_axon_test_source.py to drive real frames at your msg_type decoder and assert on what comes back.
Constraints
Your project carries two constraint files: src/scir_sdk.pdc (physical: pin placement, I/O standards) and src/scir_sdk.sdc (timing: clocks, top-level timing). Both are mixed-ownership files, split into two regions by a pair of sentinel marker comments:
# >>> AXON PERIPHERAL SDK FRAMEWORK CONSTRAINTS (generated; do not edit) >>>
# ... board clocks / board pins, written by the SDK ...
# <<< AXON PERIPHERAL SDK FRAMEWORK CONSTRAINTS <<<
# everything below here is yours — add your peripheral pin/timing constraints- Framework region (between the markers) — board-level clocks, board pins, and top-level timing. The SDK re-emits this region on every
axon-peripheral-sdk generate, overwriting it unconditionally. Do not edit inside the markers; any change there is reverted on the next run. There is no per-block checksum and no conflict prompt for this region — the rule is simply "do not edit above the end marker." - User-append region (everything after the end marker) — yours. This is where you add the pin placement and timing constraints for the ports your peripheral declares in
fpga.io. Everything below the end marker is preserved verbatim acrossgenerate.
The generated files src/via_top.sv and src/scir_sdk.rdf carry an // AUTO-GENERATED — checksum: <sha256> header. You may hand-edit them, but the checksum will act as a failsafe. Before regenerating, the CLI re-hashes the file and refuses to overwrite a hand-edited file until you pass --force. Your peripheral SystemVerilog is never auto-overwritten.
Build the gateware
After it generates and simulates cleanly, build the gateware code on its own:
synapsectl peripherals build gateware .This regenerates the wiring and runs Radiant in the gateware container to produce the FPGA bitstream (.bit), packaged into a .deb in dist/. Add --clean to build from a clean tree, and set LM_LICENSE_FILE first — the Radiant build aborts immediately without a license.
To build and deploy your code together, see CLI Reference; the driver code is covered in Driver.