OctoSPI FPGA IP Core
An OctoSPI slave IP core for Xilinx FPGAs with an AXI4-Lite bridge, paired with an Embassy/Rust async driver for the STM32U595 — a high-speed MCU ↔ FPGA link over an 8-bit single-transfer-rate interface.
System Overview
STM32U595 ──OctoSPI──▶ octo_spi_slave ──▶ octo_spi_axi_master ──▶ BRAM A/B/C/D
Embassy/Rust driver Protocol FSM AXI4-Lite bridge axi_addr_decoder
(dual-buffer (req/rsp bridge)
prefetch)The STM32U595 drives the link as OctoSPI master using a driver built on the Embassy async runtime. All IO[7:0] transfers run in 8-bit STR (single-transfer-rate) mode.
Inside the FPGA, octo_spi_slave parses the frame with a protocol FSM and issues AXI4-Lite requests through octo_spi_axi_master. An address decoder routes each transaction to one of four BRAM slaves based on addr[17:16].
Clock domains: a 54 MHz board input feeds a PLL that generates aclk at 100 MHz for the whole FPGA design, while the OctoSPI clock itself is timing-constrained for up to 25 MHz.
Tech Stack
RTL: SystemVerilog
Toolchain: Vivado 2024.x / 2025.x
Bus: AXI4-Lite
MCU: STM32U595
Firmware: Rust + Embassy
Links
Protocol Frame Structure
All fields are transferred in 8-bit STR mode on IO[7:0]. AUX carries the pop count for READ_FIXED and is zero otherwise; ADDR is the 32-bit word-aligned FPGA address. The DUMMY cycles exist so the slave can get AXI prefetch reads in flight before the data phase starts — writes skip the phase entirely.
Command Opcodes
| Opcode | Hex | Description |
|---|---|---|
| CMD_WRITE_INCR | 0xCA | Burst write, auto-increment address |
| CMD_WRITE_FIXED | 0xFE | Repeated write to a fixed address (FIFO push) |
| CMD_READ_INCR | 0xBA | Burst read, auto-increment address |
| CMD_READ_FIXED | 0xBE | Repeated read from a fixed address (FIFO pop) |
Module Responsibilities
| Module | File | Role |
|---|---|---|
| octo_spi_slave | ospi/rtl/octo_spi_slave.sv | Protocol parser FSM, dual-buffer prefetch, write accumulator |
| octo_spi_axi_master | ospi/rtl/octo_spi_axi_master.sv | AXI4-Lite request/response bridge |
| octo_spi_top | ospi/rtl/octo_spi_top.sv | I/O synchronizer wrapper |
| octospi_bram_test_top | eval/rtl/octospi_bram_test_top.sv | Board top: PLL, reset synchronizer, 2-to-1 AXI arbiter |
| axi_addr_decoder | eval/rtl/axi_addr_decoder.sv | Routes AXI transactions to BRAM A/B/C/D via addr[17:16] |
| axi_lite_bram | eval/rtl/axi_lite_bram.sv | Plain synchronous BRAM (BRAM C) |
| axi_lite_bram_fifo | eval/rtl/axi_lite_bram_fifo.sv | Circular BRAM with a software FIFO API (BRAM D) |
| bram_incr_fill_master | eval/rtl/bram_incr_fill_master.sv | Auto-fills BRAM A with a counter on each read request |
| bram_fifo_master | eval/rtl/bram_fifo_master.sv | Hardware FIFO producer for BRAM B |
Paths are relative to fpga/ in the repository.
BRAM Address Map
| Example | BRAM | AXI Base Address | Function |
|---|---|---|---|
| bram_a | A | 0x0000_0000 | READ_INCR — triggers an auto counter fill on read |
| bram_b | B | 0x0001_0000 | READ_FIXED — pops from a hardware FIFO |
| bram_c | C | 0x0002_0000 | WRITE_INCR + READ_INCR roundtrip |
| bram_d | D | 0x0003_0000 | WRITE_FIXED + READ_FIXED software FIFO |
ILA Probe Reference
Trigger position: 3000 pre-trigger + 1096 post-trigger = 4096 total depth. Clock domain: aclk at 100 MHz. Primary trigger probe: probe7 (req_valid).
| Probe | Width | Signal |
|---|---|---|
| probe0 | 24 | {state[3:0], cnt[7:0], captured_byte[7:0], driving, initial_valid, prefetch_valid, word1_done} |
| probe1 | 32 | shift_out — OctoSPI shift register contents |
| probe2 | 32 | initial_buf — first AXI read response buffer |
| probe3 | 32 | prefetch_buf — prefetch read response buffer |
| probe4 | 8 | ospi_io_in — IO pins driven by the STM32 |
| probe5 | 8 | ospi_io_out — IO pins driven by the FPGA |
| probe6 | 5 | {aresetn, csn_sync, sclk_sync, sclk_rise, byte_ready} |
| probe7 | 34 | {req_valid, req_read, req_addr[31:0]} ← primary trigger |
| probe8 | 33 | {rsp_valid, rsp_rdata[31:0]} — AXI read return data |
FSM State Decode (probe0[23:20])
| Value | State | Meaning |
|---|---|---|
| 0 | ST_IDLE | Waiting for CSn assertion |
| 1 | ST_CMD | Receiving the CMD byte |
| 2 | ST_AUX | Receiving the AUX bytes |
| 3 | ST_ADDR | Receiving the address bytes |
| 4 | ST_DUMMY | Dummy cycles — prefetch AXI reads issued here |
| 5 | ST_DATA | Data read/write phase |
| 6 | ST_DONE | Reserved in state_t; unused by the current FSM |
Quick Start
Build the FPGA bitstream
Requires Vivado 2024.x or 2025.x. Synthesis and implementation take roughly 30 minutes.
cd fpga make bitstream # or: make project && make gui
Program the FPGA
Start hw_server first, or open the Hardware Manager in the Vivado GUI.
cd fpga make program
Run the STM32 tests
Requires Rust stable with the thumbv8m.main-none-eabihf target and probe-rs ≥ 0.24.
cd stm32 export PROBE_RS_PROBE=<your-probe-serial> cargo run --example bram_a # READ_INCR auto-fill cargo run --example bram_b # READ_FIXED FIFO pop cargo run --example bram_c # WRITE + READ roundtrip cargo run --example bram_d # WRITE + READ software FIFO