Loading...

Gateware

Gateware

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:

PortDirectionWidthNotes
clkinput1Main clock. 40 MHz on via-devkit.
rstinput1Synchronous, active-high reset — not rstn. Clear all state while it is high.
periph_addrinput32This peripheral's address. The transport already routes frames to and from your module, so most peripherals — including the example — leave it unused.
rx_axisaxi4_stream_interface.secondaryFrames in. Your module consumes this stream.
tx_axisaxi4_stream_interface.mainFrames 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:

SignalWidthMeaning
tdata32One 32-bit word per beat.
tkeep4Valid-byte mask for tdata. Payload is 32-bit aligned, so this is 4'hF on full words.
tvalid1The producer asserts it when tdata is valid.
tready1The consumer asserts it when it can accept a beat. A beat transfers when tvalid && tready.
tlast1Asserted on the final beat of a frame.
tid8Stream ID, carried through framing.
tdest1Destination routing bit.
tuser1Sideband user bit.

The modport fixes the direction of every signal for you:

  • On rx_axis (.secondary), your module reads tdata, tkeep, tvalid, tlast, tid, tdest, tuser and drives tready. Assert tready when you can accept a beat.
  • On tx_axis (.main), your module drives all signals and reads tready. Hold tvalid with valid tdata until you see tready high — 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 are len, 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.
  • tlast is asserted on the final payload beat. A header-only frame (len == 0) asserts tlast on 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:
FieldTypeRequiredNotes
schema_versionintyesMust be 1.
target_profilestringyesThe board profile.
projectobjectyesSee below.
buildobjectyesSee below.
peripheralslistyesOne 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
FieldTypeRequiredNotes
namestringyesA valid identifier (^[A-Za-z][A-Za-z0-9_]*$). Names files and the SV module.
dev_peripheral_idintnoOptional — omit it and generate allocates the lowest free ID and writes it back. If you set it, it must lie in 0xF001..0xFFFE.
typeenumyesrecord or stim.
descriptionstringnoFree text.
vendorstringnoFree text.
fpgaobjectyesThe gateware block. See below.
apiobjectnoDeclares msg_types. Defaults to empty.
verificationobjectnoLists 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.

FieldTypeRequiredNotes
modulestringyesThe SV top module name (a valid identifier). Must match your module declaration exactly.
sourceslist of stringsnoPaths to your SystemVerilog sources, relative to the project root.
constraintsobjectnosdc and pdc lists of constraint file paths.
iolistnoPin 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
FieldTypeRequiredNotes
namestringyesPort name (a valid identifier).
direnumyesinput, output, or inout.
pinstringyesThe package pin.
iostdstringnoI/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, pullstringnoOptional 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
FieldTypeRequiredNotes
namestringyesA valid identifier.
valueintyesThe 16-bit opcode (0x0000..0xFFFF) carried in the frame header.
direnumyeshost_to_fpga or fpga_to_host.
payload_bytesint or "variable"yesFixed 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 generate

This 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:

  1. Capture the header on the first beat and decode msg_type against the command table you define.
  2. Drive your chip, its SPI clock and pins, in response. Declare every chip-facing pin in peripheral.yaml (above).
  3. Frame your chip's samples back out on tx_axis: emit a header beat, then payload beats, asserting tlast on the last.

Simulation and Constraints

Simulation

The Axon Peripheral SDK provides infrastructure for simulation of the RTL against a testbench.

synapsectl peripherals gateware sim

This 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 across generate.

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.