josuah.net | panoramix-labs.fr

cv | links | quotes | ascii | tgtimes | gopher | mail

FPGA ←SPI→ MCU: Crossing Clock Domains

For my SDR project, I want to combine a RP2040 and an ICE40.

It looks good on paper

Both a are widely available board and cover a lot of features together.

The RP2040 features would be expensive to do in an FPGA:

Fast Dual-Core MCU
More than enough to keep-up with the FPGA.
Plenty of RAM
Enough to organize complex applications.
USB support
It would spend a lot of gates to get that done on the FPGA.
PIO peripherals
Tiny programmable state machine for handling simple extra protocols: Handy for handling everything that does not fit the FPGA, or for custom interfaces.

An FPGA complements quite well what the RP2040 lacks:

More peripherals for the RP2040
By writing Wishbone peripherals on the FPGA, and writing an SPI-to-Wishbone bridge. It permits to implement peripherals on the FPGA, and write drivers on the RP2040 for them.
DSP front-end for RP2040
The FPGA can be placed as a front-end for the MCU: Receiving signals from the sensors. Then converting them onto an easy-to parse digital signal. Then transmitting them over a protocol the RP2040 likes.

Let's see if we can make that work in practice...

First challenge: Clock Domain Crossing

SPI comes with its own clock signal. It is convenient and reliable to use it as clock for the SPI core: @(posedge spi_clk) in Verilog instead of the @(posedge wb_clk_i) clock used by the rest. This means we have now two clock domains and need to plan cooperation between them.

This introduce me to the famous topic of Clock Domain Crossing: taking data from one clock domain to another.

Recommandations often encountered is to use a handshake protocol. I will stick to the simplest implementation I can come-up with and see if it works well in practice.

Handshake protocol for the Wishbone Clock Domain

Simple handshake protocol for crossing clock domain.

		  :   :   :   :   :   :   :   :   :   :   :   :
		__:_______________:_______________:______________
handshake_data	__X_______________X_______________X______________
		  :    _______________:   :   :   :    __________
handshake_req	______/   :   :   :   \_______________/   :   :
		  :   :   :   :_______________:   :   :   :   :__
handshake_ack	______________/   :   :   :   \_______________/
		  :   :   :   :   :   :   :   :   :   :   :   :
		 (1) (2) (3) (4) (1) (2) (3) (4) (1) (2) (3) (4)

Wire protocol

The Wishbone protocol uses many more wires than SPI for communicating, so a wire protocol for encoding Wishbone over another transport is required.

Here is what I came-up with, inspired by spibone.

Wishbone read transaction:

MCU   W000SSSS AAAAAAAA :::::::: :::::::: :::::::: :::::::: :::::::: ::::::::
      │   ├──┘ ├──────┘
FPGA  │:::│::: │::::::: 11111111 00000000 DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD
      │   │    │        ├──────┘ ├──────┘ ├─────────────────────────────────┘
      WE  SEL  ADR      WAIT     ACK      DAT

Wishbone write transaction:

MCU   W000SSSS AAAAAAAA DDDDDDDD DDDDDDDD DDDDDDDD DDDDDDDD :::::::: ::::::::
      │   ├──┘ ├──────┘ ├─────────────────────────────────┘
FPGA  │:::│::: │::::::: │::::::: :::::::: :::::::: :::::::: 11111111 00000000
      │   │    │        │                                   ├──────┘ ├──────┘
      WE  SEL  ADR      DAT                                 WAIT     ACK

Signals

While far from battle-tested, this seems to work at least a little:

signals shown in gtkwave

This is the state of the signals on the FPGA, with the clocks in red, the I/O signals in yellow, and the others in green, with the global state in violet.

The I/O block shows the clock from the MCU (or rather here, simulation), that are captured by the rx{} block as packets in handshake_data (8'h8F, 8'h00, 8'h12, ...),

These are then decoded by the state machine shown in state, and finally sent to the bus.

We can recognize the data payload 32'h12345678 sent packet per packet on spi_sdi.