A full AES67 Audio-over-IP implementation built around a single FPGA. The FPGA owns the entire data plane — Ethernet MAC, IEEE 1588 (PTPv2), a PTP-disciplined wallclock, media-clock derivation, RTP packetisation and TDM/I2S audio I/O — while a separate, swappable control plane handles the non-realtime work (network management, PTP grandmaster selection, stream announcement/discovery, configuration, web UI).
The defining feature of the design is that the control plane is not fixed to one host. The FPGA exposes its whole register set as a Wishbone-slave bridge (LiteX CSR map), and that bridge can be driven by:
- an integrated LiteX RISC-V softcore (VexRiscv) running Zephyr RTOS on the same FPGA (single-chip endpoint), or
- an external MCU (e.g. an ESP32-S3) running the same Zephyr control-plane firmware over SPI, or
- an external Linux host (e.g. a Raspberry Pi): the
aes67_ethkernel driver turns the FPGA into a normalnet_devicewith a PTP hardware clock (so stockptp4ldisciplines the wallclock) and owns the SPI→Wishbone (spibone) bus, and the Rustconfig_toolstack (daemon/CLI/web/discovery) runs on top of that driver, reaching the FPGA registers through it.
- System Overview
- Control-Plane Backends
- FPGA Architecture (Data Plane)
- Control Interface (Wishbone / LiteX CSR)
- LiteX SoC & Bus Bridges
- Host Control Plane — Linux (
config_tool) - Linux Kernel Driver & Software PTP
- Firmware — Integrated Softcore (Zephyr)
- Supported Boards
- Building
- Status & Known Issues
- Technical Notes
- License
flowchart TB
phy(["Ethernet PHY"])
phy <== MII ===> yol_mac
aout(["TDM OUT"])
ain --> tx_tdm_demux
rx_tdm_mux --> aout
ptp_nco --> aclk
ain(["TDM IN"])
aclk(["MCLK · BCLK · LRCK"])
wishbone@{ shape: lean-r, label: "Wishbone Bus" }
wishbone ==> tx_arbiter
rx_filter ==> wishbone
rx_config === wishbone
tx_config === wishbone
spibone === wishbone
uartbone === wishbone
subgraph FPGA["FPGA Data Plane (aes67_top)"]
direction TB
tsu["Timestamping Unit"]
tsu --> ptp_parser
yol_mac & phy --> tsu
subgraph mac["Ethernet MAC"]
tx_arbiter["TX Arbiter"]
rx_filter["RX Filter"]
yol_mac["YOL Ethernet Mac"]
yol_mac --> rx_filter
tx_arbiter --> yol_mac
end
subgraph ptp["PTP Implementation"]
direction TB
ptp_parser["Parser"]
ptp_bmca["BMCA"]
ptp_sender["Sender"]
ptp_controller["Controller"]
ptp_servo["Servo"]
ptp_nco["NCO"]
ptp_mediaclock["Mediaclock"]
wallclock["Wallclock"]
ptp_parser -- timestamp --> ptp_servo -- ppb --> wallclock -- timestamp --> ptp_sender
ptp_parser --> ptp_bmca --> ptp_controller --> ptp_sender
wallclock --> ptp_nco
wallclock --> ptp_mediaclock
end
subgraph audio_rx["Audio RX"]
direction TB
rx_config[("Config")]
rx_parser["RTP Packet Parser"]
rx_buffer["Sample Buffer"]
rx_playout["Sample Playout"]
rx_tdm_mux["TDM Multiplexer"]
rx_parallel_out["Parallel Output"]
rx_config --> rx_parser --> rx_buffer --> rx_playout --> rx_tdm_mux & rx_parallel_out
end
subgraph audio_tx["Audio TX"]
direction TB
tx_config[("Config")]
tx_tdm_demux[TDM Demux]
tx_parallel_in["Parallel Input"]
tx_buffer["Sample Buffer"]
tx_router["Router"]
tx_transmitter["Sender"]
tx_tdm_demux & tx_parallel_in --> tx_buffer
tx_buffer & tx_router --> tx_transmitter
tx_config --> tx_router
end
ptp_mediaclock --> rx_playout
ptp_mediaclock --> tx_buffer
rx_filter --> ptp_parser & rx_parser
ptp_sender & tx_transmitter --> tx_arbiter
end
subgraph control_plane["Control Plane"]
direction BT
spibone["SPI Wishbone Bridge"]
uartbone["UART Wishbone Bridge"]
zephyr["Zephyr Firmware"]
linux["Linux Kernel Driver"]
linux_daemon["aes67d"]
ptp4l["ptp4l"]
spibone & uartbone <--> linux & zephyr
ptp4l & linux_daemon <--> linux
end
The data-plane logic is identical regardless of who drives it; only the Wishbone master in front of the aes67_wb_bridge changes. On the FPGA side the master is chosen by the SOC_TYPE generic on soc_top.vhd (and by which LiteX target is generated). On the host side, the Zephyr firmware (soc_firmware/app/), the Rust config_tool, and the aes67_eth kernel driver all speak to the same register names resolved from the generated CSR map.
| Backend | LiteX target / SOC_TYPE |
Host | Transport | Notes |
|---|---|---|---|---|
| Integrated softcore | cyclone10 / cyc1000 / gowin (LITEX_HRAM / LITEX_SDRAM) |
On-FPGA VexRiscv + Zephyr | LiteX CSR over Wishbone | Self-contained single-chip system; SoC boots from SPI flash. |
| External MCU (Zephyr) | spibone (LITEX_SPIBONE) |
ESP32-S3 (same Zephyr firmware) | SPI → Wishbone | Active target; the SPI driver still needs porting from the old spictrl byte protocol to spibone. |
| External Linux host | aes67_bridge + --ptp-in-software, driven over spibone |
Linux (e.g. Raspberry Pi) | SPI→Wishbone via aes67_eth.ko |
The aes67_eth kernel driver provides net_device + PHC (stock ptp4l disciplines the wallclock) and owns the bus; the Rust config_tool stack runs on top of it (daemon, CLI, web, discovery, TAP bridge). |
For bring-up/debug,
config_toolcan also open aspibone/uartbonebridge directly (aes67cfg --spi/--uart) without the kernel driver — but the full Linux control plane (withptp4lhardware PTP) runs throughaes67_eth.ko, the single Wishbone master.The earlier external-MCU path used a hand-rolled SPI byte-command protocol in
spictrl.vhd. That FPGA block and itsconfig_ram_address_map.mdhave been removed in favour of the CPU-less Wishbone bridge (spibone/uartbone); the ESP32-S3 driver just needs porting onto it.
All time-critical audio and timing logic lives in FPGA/. New logic is VHDL; a few audio-clock helpers are Verilog.
- FPGA/aes67_top.vhd — the data-plane core (MAC + PTP + wallclock + audio TX/RX).
- FPGA/aes67_wb_bridge.vhd — wraps the core and exposes its register set + the Ethernet control-plane buffer (
eth_buf) as a Wishbone slave. This is the single point all control-plane hosts talk to. - FPGA/soc_top.vhd — board/SoC wrapper: instantiates the data plane plus the chosen LiteX core (full VexRiscv SoC, or a CPU-less
spibone/uartbonemaster) selected by theSOC_TYPEgeneric. - FPGA/wb_bridge_top.vhd — a leaner CPU-less wrapper (bridge + master only, no softcore).
| Module | File | Description |
|---|---|---|
| Ethernet MAC | FPGA/FPGA_Ethernet/ | Fork of the YOL MAC with start-of-frame timestamp output (git submodule) |
| RMII/SMII bridge | FPGA/mii_rmii/ | RMII↔MII glue for 100 Mbit PHYs (git submodule) |
| MII converters | FPGA/mii_converters.vhd | MII width/type adaptation between PHY and MAC |
| MII timestamp | FPGA/ethernet_timestamp_mii.vhd | Latches the 48b:32b wallclock at the SOF delimiter |
| TX arbiter | FPGA/eth_tx_arbiter.vhd | Arbitrates PTP / audio / control-plane egress onto the MAC |
| Packet aggregator | FPGA/ethernet_packet_aggregator.vhd | Assembles outgoing frames |
| Eth control buffer | FPGA/litex_eth_buffer_bridge.vhd | Dual-port RX/TX packet buffers (eth_buf) between MAC and the control host; appends the RX hardware-timestamp trailer in software-PTP mode |
| Module | File | Description |
|---|---|---|
| PTP module | FPGA/ptp/ptp_module.vhd | Top-level PTP block; selects hardware vs software PTP via PTP_IN_SOFTWARE |
| Controller | FPGA/ptp/ptpv2_controller.vhd | State machine: Sync, Follow_Up, Announce, Delay_Req/Resp |
| Parser | FPGA/ptp/ptpv2_parser.vhd | Extracts timestamps, computes offset / mean path delay |
| Servo | FPGA/ptp/ptpv2_servo.vhd | PI controller for clock discipline (PPB output) |
| Moving average | FPGA/ptp/average.vhd | Configurable moving-average filter (PTP_MOVING_AVERAGE_DEPTH) |
| Wallclock | FPGA/ptp/wallclock.vhd | PTP-disciplined 48-bit seconds + 32-bit nanoseconds + media clock |
In hardware PTP mode the FPGA runs the full PTPv2 engine on-chip and the control plane only runs the BMC (best-master-clock) decision, feeding the resulting grandmaster priorities/identity back over the bus. In software PTP mode (PTP_IN_SOFTWARE = true, gateware built with --ptp-in-software) the FPGA provides only hardware timestamping and CSR access to the wallclock, and a host PTP stack (ptp4l via the kernel driver, or Zephyr's CONFIG_PTP) disciplines it.
| Module | File | Description |
|---|---|---|
| Wallclock + media clock | FPGA/ptp/wallclock.vhd | NCO phase reference + media-clock counter for RTP timestamps |
| Audio clock gen | FPGA/audioclock_generator_sysclk.vhd | Derives the BCLK/LRCK/TDM-frame domain from sysclk |
| System PLL gen | FPGA/sysclk_pll_gen.vhd | Per-platform system/PLL clock generation |
| PPB meter | FPGA/clock_ppb_meter.vhd | Measures NCO-vs-external-PLL phase → PPB correction |
| Packages | FPGA/packages/audioclks_pkg.vhd, FPGA/packages/wallclock_signals_pkg.vhd | Shared audio-clock and wallclock signal bundles |
| Module | File | Description |
|---|---|---|
| TX module | FPGA/audio_tx/audio_tx_module.vhd | Wraps the TX path |
| TX router | FPGA/audio_tx/tx_router.vhd | Per-stream config RAM, sample aggregation |
| TX transmitter | FPGA/audio_tx/tx_transmitter.vhd | RTP packet construction with SSRC |
| TX sample buffer | FPGA/audio_tx/tx_sample_buffer.vhd | Media-clock-paced ring buffer; integrated TDM demux |
| TDM8 in | FPGA/audio_tx/tdm8_in.vhd | 8-channel TDM input (legacy parallel path) |
| RX ringbuffer | FPGA/audio_rx/rx_ringbuffer.vhd | Stream demux + playout buffer |
| TDM8 out | FPGA/audio_rx/tdm8_out.vhd | 8-channel TDM output |
Audio framing is now TDM-centric: I2S is handled as a mode of the TDM path (TDM_I2S_MODE) rather than a separate serialiser, so a single set of modules covers I2S and TDM8.
The core is parameterised through the generics on aes67_top.vhd (mirrored by soc_top.vhd / wb_bridge_top.vhd). The most useful ones:
| Generic | Typical default | Purpose |
|---|---|---|
SOC_TYPE |
"LITEX_HRAM" |
Control-plane core: LITEX_HRAM, LITEX_SDRAM, LITEX_SPIBONE, LITEX_UARTBONE |
platform |
"ALTERA" |
"ALTERA" or "GOWIN" vendor glue |
ETHERNET_TYPE / MII_WIDTH |
"RMII" / 2 |
PHY interface (RMII 100 Mbit or RGMII Gigabit) and MII data width |
SYS_CLK_NS_PER_TICK / MII_CLK_NS_PER_TICK |
8 / 20 |
System (125 MHz) and MII clock periods — keep in sync with the actual clocks |
TX_MAX_STREAMS / RX_MAX_STREAMS |
8 / 8 |
Maximum concurrent TX / RX RTP streams |
TX_CHANNELS / RX_CHANNELS |
16 / 16 |
Audio channel count (×2 for I2S, ×8 for TDM8) |
TX_BYTE_DEPTH / RX_BYTE_DEPTH |
3 / 3 |
Sample width in bytes (3 = 24-bit) |
TX_SAMPLE_BUFFER_DEPTH |
64 |
TX ring depth — must be a power of two (media-clock write pointer) |
RX_SAMPLE_BUFFER_DEPTH |
256 |
RX playout buffer depth (latency vs. jitter tolerance) |
AUDIO_TX/RX_TDM_CHANNELS / ..._TDM_INPUTS/OUTPUTS |
8 / 1–2 |
TDM lane width and number of TDM data lines per direction |
TDM_I2S_MODE / TDM_BCLK_MULT / TDM_FSCLK_50DUTY |
false / 256 / false |
I2S framing mode, BCLK multiplier, 50 %-duty frame sync |
AUDIO_TX/RX_USE_PARALLEL_INTERFACE |
false |
false = integrates tdm mux/demux true exposes raw sample values |
USE_EXTERNAL_PLL |
true |
true = drive audio clocks from the external Si5351A; false = use the on-chip NCO-generated clocks directly |
ENABLE_METERING |
true |
Per-channel signal/clip metering; set false to drop it and save logic |
STATIC_PTP_CONF |
true |
true = compile-time PTP servo/parser config; false = runtime-tunable from the control plane |
PTP_MOVING_AVERAGE_DEPTH |
8 |
Depth of the PTP offset/delay moving-average filter |
PTP_IN_SOFTWARE |
false |
true = host runs PTP (timestamping only in HW); false = full HW PTP engine |
MIIM_CLOCK_DIVIDER / MIIM_PHY_ADDRESS |
50 / 0 |
MDIO clock divider and PHY management address |
Defaults give a 48 kHz / 24-bit endpoint with up to 8 TX and 8 RX streams over 16 channels each.
flowchart LR
wcmc(["Wallclock + media clock<br/>(PTP-disciplined)"])
subgraph TXP["TX path"]
direction LR
ti(["TDM / I2S in"]) --> tsb["tx_sample_buffer"] --> trt["tx_router"] --> ttx["tx_transmitter"] --> arb["eth_tx_arbiter"] --> tmac["MAC"] --> tw(["wire"])
tcfg[["TX stream config RAM<br/>(control plane)"]] -.-> trt
end
subgraph RXP["RX path"]
direction LR
rw(["wire"]) --> rmac["MAC"] --> udp["UDP parse"] --> rrb["rx_ringbuffer<br/>(stream demux)"] --> ro(["TDM / I2S out"])
rcfg[["RX stream config RAM<br/>(control plane)"]] -.-> rrb
end
wcmc == "write ptr / RTP timestamp" ==> tsb
wcmc == "playout timing" ==> ro
Every control-plane host reaches the data plane through the Wishbone-slave register set in aes67_wb_bridge.vhd, exposed as a LiteX CSR map. There is no longer a bespoke byte-command protocol — the bridge is a standard memory-mapped register block, so:
- The integrated VexRiscv accesses CSRs directly over its internal Wishbone bus.
- An external Linux host reaches the same CSRs over a CPU-less bridge —
spibone(SPI→Wishbone) oruartbone(UART→Wishbone) — generated by LiteX. The host issues wordpeek/pokereads/writes at byte addresses; each bridge applies its own address convention internally. - The register map is resolved at runtime from the LiteX-generated
csr.csv/csr.json(for theaes67_bridgebuild). Regenerate the gateware and every host tracks the new addresses automatically — nothing is hard-coded.
Logical register groups exposed by the bridge include: device info; MAC/IP; control flags and per-domain reset (aes67_csr_reset, all-held at power-on, active-high); PTP grandmaster params and servo/parser tuning; PTP status (offset, mean path delay, grandmaster identity, lock); clocking/PPB status; channel metering; the Ethernet control buffer (eth_buf, RX-drain / TX-inject); and the write-only TX/RX stream config RAMs (up to 8 streams each). The authoritative, byte-exact field conventions live in the on-target driver eth_litex.c and are mirrored by the Rust aes67-config crate.
The LiteX SoC and the CPU-less bridges are generated by litex_soc/generate.py, which drives the aes67_soc Python package (one module per concern: platform, CRG, SoC assembly, and peripherals/). It emits portable Verilog into litex_soc/build/; the FPGA top-level feeds it clocks (no SoC-internal PLL).
source soc_firmware/.venv/bin/activate
python litex_soc/generate.py --target <name>| Target | Kind | Role |
|---|---|---|
aes67_bridge |
Wishbone slave | The AES67 peripheral/register block — the only target that builds the AES67 peripherals; every other target is a master to it. Produces the canonical csr.csv. |
cyclone10 |
Full VexRiscv SoC | Integrated softcore + HyperRAM (Cyclone 10LP eval) |
cyc1000 |
Full VexRiscv SoC | Integrated softcore + SDRAM (CYC1000) |
gowin |
Full VexRiscv SoC | Integrated softcore + DDR3 (Tang Primer 20K) |
spibone |
CPU-less master | SPI→Wishbone bridge for an external host |
uartbone |
CPU-less master | UART→Wishbone bridge for an external host |
The CPU-less targets drop everything CPU-adjacent (VexRiscv, BIOS, boot stub) and just present a Wishbone master that drives the aes67_bridge slave across the FPGA top-level. The spibone master uses a forked spi_bone.py with burst commands for throughput.
| Resource | Details |
|---|---|
| CPU | VexRiscv RISC-V (sys clock typ. 75–80 MHz, supplied by top-level) |
| RAM | HyperRAM (Cyclone 10LP) or SDRAM (CYC1000) / DDR3 (Gowin) @ 0x20000000 |
| Flash | SPI flash (BIOS + firmware) @ 0x30000000 |
| CSR | Peripheral registers @ 0xf0000000 |
| Ethernet | MAC ↔ SoC via litex_eth_buffer_bridge.vhd (dual-port packet buffers) |
| I2C / SPI / UART | Display + Si5351A PLL, SD card, console |
Boot flow (integrated softcore): FPGA configures from its own flash → a RISC-V boot stub at the SPI-flash reset vector (litex_soc/boot_stub/) copies the LiteX BIOS into RAM and sets the HyperRAM latency (6 CK) → the BIOS loads the Zephyr .fbi image (binary + length + CRC-32) → Zephyr boots and starts DHCP and the application threads.
Firmware merges the CPU SoC's CSR header with the
aes67_bridgeCSR header via merge_litex_csr.py (run from CMake), so the AES67 register addresses match the standalone bridge build.
config_tool/ is a Rust workspace that runs the control plane on an external Linux host (typically a Raspberry Pi). In the normal Linux deployment it runs on top of the aes67_eth kernel driver (next section): the driver owns the SPI→Wishbone bus and provides the net_device (+PHC for ptp4l), and config_tool reaches FPGA registers through the driver's /dev/aes67ctl peek/poke char device. It has no native C dependencies, so it cross-compiles cleanly to ARM. See config_tool/README.md for the full guide.
aes67d(daemon) — serves a control API over a Unix socket and persists FPGA settings to a JSON config, replaying them on restart. Drives IGMP membership on behalf of the data plane, triggers DHCP on link-up, mirrors Linux IP → FPGA IP CSR, and runs a staged reset bring-up with warm-restart and auto-recovery (the gateware powers up with all reset domains held). With the kernel driver it works through thenet_device//dev/aes67ctl; in standalone bring-up mode it can instead own aspibone/uartbonelink directly and bridgeeth_bufinto its own TAP.aes67cfg(CLI) — read/write registers by name (get/set/dump/peek/poke), set MAC/IP/PTP grandmaster, configure RX/TX streams, list discovered streams. Talks to the daemon by default, or directly to the FPGA with--spi/--uartfor bring-up.aes67web— a standalone monitoring/config web dashboard (embedded HTML, REST API), itself just another client of the daemon.- Discovery — SAP/SDP announcement + foreign-stream discovery, plus mDNS/DNS-SD and RAVENNA RTSP (server for local TX streams, client
subscribefor remote sessions), all in pure-Rust crates (sap, sdp, mdns, rtsp).
Crate layout: transport (HAL: UART/SPI bridges) → config (CSR map + by-name device + stream helpers) → proto/client (daemon wire protocol) → daemon/cli/web. The whole stack is documented in config_tool/docs/control-plane-plan.md.
driver/aes67_eth/ is an out-of-tree Linux driver that turns the FPGA (aes67_bridge gateware built with --ptp-in-software) into a first-class network device with hardware PTP timestamping, so stock ptp4l disciplines the FPGA wallclock. This is "Phase 5 — PTP offload" of the control-plane plan.
net_devicecarrying the FPGAeth_bufdatapath in-kernel (the RX-drain / TX-inject protocol the daemon used).- PHC (
/dev/ptpN, clockaes67_wallclock) mapping the wallclock CSRs:gettime/settime/adjtime/adjfine. - HW timestamps — TX from the
tx_timestamp_*CSRs, RX from the 5-byte trailer the FPGA appends after the payload (payload | FCS(4) | seconds(1) | nanoseconds_LE(4)). /dev/aes67ctl— a peek/poke char device so userspace (aes67d/aes67cfg) keeps reaching FPGA registers while the kernel owns the SPI bus (single Wishbone master).
make # build against running kernel (or KDIR=… ARCH=… CROSS_COMPILE=… to cross-compile)
insmod aes67_eth_drv.ko
ethtool -T eth0 # shows HW TX/RX + a PHC index
ptp4l -H -i eth0 -m # hardware timestamping, disciplines the FPGA wallclockaes67_regs.h is generated from the LiteX csr.csv (make regs CSV=…) so register addresses track the gateware. The same software-PTP idea also runs on the integrated softcore: build the Zephyr firmware with the ptpsw.conf overlay (CONFIG_PTP + the ptp_clock_aes67.c PHC driver) to discipline the wallclock from Zephyr's IEEE 1588 stack instead of the FPGA hardware PTP.
Zephyr RTOS application in soc_firmware/app/ (Zephyr v4.2.0; west manifest at soc_firmware/app/west-manifest/west.yml). This is the control plane for the single-chip (integrated softcore) builds. main.c is the full-feature entry point; src/main_spi_bringup.c is a minimal SPI-only bring-up for porting to a new external-MCU board.
| Module | File | Description |
|---|---|---|
| Main | src/main.c | Init, DHCP, network setup |
| PTP BMC | src/ptp_bmc.c | IEEE 1588 best-master-clock on 224.0.1.129:320 |
| SAP/SDP | src/sap_sdp.c | Session announcement (239.255.255.255:9875) + foreign-stream discovery |
| SDP utils | src/aes67_sdp_utils.c | SDP parse/format, PTP clock-ID formatting |
| RTSP | src/rtsp.c | RAVENNA RTSP server/client (subscription, session control) |
| mDNS / DNS-SD | src/mdns_sd.c | RFC 6762/6763 responder + service advertisement |
| Webserver | src/webserver.c | REST API + gzipped static web UI |
| Config | src/aes67_config.c | Centralised runtime configuration + defaults |
| Config JSON | src/config_json.c | JSON (de)serialisation shared by SD & flash storage |
| SD config | src/sd_config.c | SD-card persistence (FAT, crash-safe A/B slots) |
| Flash config | src/flash_config.c | SPI-flash config storage (8 KB slots, CRC-32) |
| FW update | src/fw_update.c | HTTP + shell firmware update, FBI verification |
| Card manager | src/card_manager.c | I2C board detect + runtime I/O-card selection |
| UI display | src/ui_display.c | SSD1306 OLED status |
| FPGA regs / poll | src/fpga_regs.c, src/fpga_poll.c | High-level register helpers + PTP-lock/link polling |
| PLL ctrl | src/pll_ctrl.c | Si5351A PPB correction from FPGA measurements |
| Driver | Path | Description |
|---|---|---|
| FPGA HAL | drivers/fpga_hal/ | Backend-agnostic register access (LiteX CSR / external-MCU SPI) |
| LiteX Ethernet + PHC | drivers/eth_litex/ | Zephyr netif via LiteX CSR + Wishbone buffers; optional software-PTP clock (ptp_clock_aes67.c) |
| Si5351A | drivers/si5351a/ | I2C clock generator with PPB correction |
| SPI flash | drivers/spi_flash/ | LiteSPI master for FW update & config |
| Display ctrl | drivers/display_ctrl/ | LED / button / 7-seg + SSD1306 |
| I/O cards | drivers/mi_card/, lo_card/, io_card/ | Analog I/O card control (I2C) |
Targets are at various maturity levels — the build matrix is still being shaken out (see todo.md).
| Board | FPGA | Control plane | RAM | Status |
|---|---|---|---|---|
Cyclone 10LP eval (c10_evalkit) |
10CL025YU256I7G | Integrated softcore / External | HyperRAM | Primary single-chip target |
C10 006 (c10_006) |
Cyclone 10LP | External / spibone | none | Working |
CYC1000 (trenz_cyc1000) |
10CL025YU256C8G | Integrated softcore / External | SDRAM | Working |
Lattice (boards/lattice) |
Lattice | Integrated softcore / CPU-less bridge | — | New / experimental |
| Tang Primer 20K (Gowin) | Gowin GW2A-18C | Integrated softcore | DDR3 | Experimental (Gowin EDA Ethernet clock-tree issues) |
FPGA board projects/pinouts live under FPGA/boards/ (Altera / Lattice / Gowin), each with its own top_*.vhd wrapping soc_top; Zephyr board configs under soc_firmware/app/boards/.
Pull submodules first:
git submodule update --init --recursivesource soc_firmware/.venv/bin/activate
python litex_soc/generate.py --target aes67_bridge # AES67 register block + csr.csv
python litex_soc/generate.py --target cyclone10 # integrated VexRiscv SoC
python litex_soc/generate.py --target spibone # (or uartbone) CPU-less host bridgeOutputs land in litex_soc/build/<target>/. Regenerate after editing the aes67_soc package; the generated csr.csv is what every host (firmware, config_tool, kernel driver) resolves register names against.
Open the per-board Quartus project under FPGA/boards/ (e.g. FPGA/boards/altera/c10_evalkit/FPGA.qpf) in Intel Quartus Prime 25.1 (primary device 10CL025YU256I7G), or the Gowin/Lattice flow for those boards. Pick the control-plane core via the SOC_TYPE generic on soc_top (LITEX_HRAM / LITEX_SDRAM / LITEX_SPIBONE / LITEX_UARTBONE).
cd soc_firmware/app
source ../.venv/bin/activate
west build -b litex_vexriscv_cyclone10 -p # single-chip (HyperRAM)
west build -b litex_vexriscv_cyc1000 -p # single-chip (SDRAM)
# software-PTP variant (host/Zephyr disciplines the wallclock):
west build . -b litex_vexriscv_cyc1000 -d build-ptpsw -- -DEXTRA_CONF_FILE=ptpsw.confLiteX builds produce a .fbi flash image (binary + length + CRC-32) for loading via the LiteX BIOS.
cd config_tool
cargo build --release # binaries: aes67d, aes67cfg, aes67web
# cross-compile to a Pi, e.g.:
cargo build --release --target aarch64-unknown-linux-gnucd driver/aes67_eth
make # against the running kernel
make regs CSV=../../litex_soc/build/aes67_bridge/csr.csv # regenerate register headerRough numbers measured on a Cyclone 10LP (10CL025, ~24.6k LEs); Gowin (Tang Primer 20K) lands in the same ballpark. "LE" = logic element / 4-input-LUT-equivalent.
| Block | ~LEs | Notes |
|---|---|---|
| TX path | 1600 | sample buffer, router, transmitter, TX arbiter |
| RX path | 600 | UDP parse, ringbuffer/demux, output |
| PTP (controller + parser + servo + sender) | 5500 | the dominant cost — discipline maths and timestamping |
| Data-plane core total | ~8800 | everything in FPGA Architecture |
spibone/uartbone bridge |
small | CPU-less Wishbone master (external-host build) |
| LiteX SoC (full config) | ~7000 | VexRiscv + Wishbone + peripherals (integrated build) |
So a single-chip integrated build is roughly core + SoC ≈ 8800 + 7000, while an external-host build is core + bridge with the rest of the control plane living on the Linux host. Disabling ENABLE_METERING, trimming stream/channel counts or buffer depths trades features for area; PTP is where the big wins would be — building with PTP_IN_SOFTWARE and disciplining the wallclock from a host (ptp4l) removes most of the 5500-LE PTP engine.
In hardware mode ptpv2_servo.vhd is a PI controller: it filters offset measurements (configurable moving-average depth via average.vhd), outputs a frequency correction in PPB, and has lock detection with hysteresis (defaults 500 ns lock / 5 µs unlock) and message-interval-aware gain scaling. With STATIC_PTP_CONF=false it is tunable from the control plane. In software mode the FPGA only timestamps and exposes the wallclock; the host PI loop (ptp4l) does the discipline via the PHC.
wallclock.vhd generates all audio clocks directly from an NCO. The 48-bit phase accumulator (32 integer + 16 fractional bits) is disciplined straight from the PTP frequency correction (freq_correction_ppb), so it is locked to the wallclock without any external clock chip in the generation path. Its MSB is MCLK (≈24.576 MHz = fs×512); a counter over the NCO MSB edges (mclk_cnt, 0..511) divides that down into BCLK, LRCK/fsclk and the TDM/I2S frame clocks, plus the sample_pulse at fs. The media-clock counter is a pure function of the wallclock (seconds × 48000 + sample_in_second) and drives the RTP timestamps; the NCO-derived sample phase is pulled toward it so all PTP-locked boards stay sample-coherent.
See LICENSE.md. Third-party cores retain their own licenses: the Ethernet MAC and RMII bridge are pulled in as submodules under their respective upstream licenses; LiteX and its spibone/uartbone cores under their upstream licenses.