fpga: lib: add ctrlport endpoints for I2C, UART and high-throughput SPI

Co-authored-by: Wade Fife <wade.fife@ettus.com>
Co-authored-by: Tony Liecthy <tony.liechty@ni.com>
This commit is contained in:
Javier Valenzuela 2026-07-23 10:59:31 -05:00 committed by Marian Koop
parent e98bf35840
commit dd708a46f4
19 changed files with 4982 additions and 48 deletions

View File

@ -18,17 +18,22 @@
//
// Parameters:
//
// DATA_W : The bit width of tdata for all ports.
// NUM_PORTS : The number of output ports on which to duplicate the input.
// INPUT_REG : Set to 1 to add an input register stage to break combinatorial
// paths. Set to 0 to allow combinatorial input paths.
// DATA_W : The bit width of tdata for all ports.
// NUM_PORTS : The number of output ports on which to duplicate the
// input.
// INPUT_REG : Set to 1 to add an input register stage to break
// combinatorial paths. Set to 0 to allow combinatorial input
// paths.
// OUT_FIFO_SIZE : Configures the per-port output FIFO size. Defaults to a
// short (32-deep) FIFO.
//
module axis_split #(
parameter DATA_W = 32,
parameter NUM_PORTS = 4,
parameter INPUT_REG = 0
parameter DATA_W = 32,
parameter NUM_PORTS = 4,
parameter INPUT_REG = 0,
parameter OUT_FIFO_SIZE = 5
) (
input wire clk,
input wire rst,
@ -106,11 +111,20 @@ module axis_split #(
genvar i;
for (i = 0; i < NUM_PORTS; i = i + 1) begin : gen_ports
// We use axi_fifo_short specifically because it can tolerate tvalid
// de-asserting at any time. This is normally not allowed by AXI-Stream.
axi_fifo_short #(
.WIDTH (DATA_W)
) axi_fifo_short_i (
// We add a FIFO on each output port because our axi_fifo tolerates tvalid
// de-asserting at any time, which is normally not allowed by AXI-Stream.
// This may occur here because the output streams are independent and we
// block all transfers when any one output stalls. To ensure we have this
// protection built in, we do not allow bypass mode.
if (OUT_FIFO_SIZE < 0) begin : gen_bypass_mode
OUT_FIFO_SIZE_must_be_non_negative();
end
axi_fifo #(
.WIDTH (DATA_W),
.SIZE (OUT_FIFO_SIZE)
) axi_fifo_i (
.clk (clk),
.reset (rst),
.clear (1'b0),

View File

@ -18,9 +18,15 @@ axi_forwarding_cam.v \
axi_setting_reg.v \
axi_slave_mux.v \
axi_test_vfifo.v \
axis_to_ctrlport.sv \
ctrlport_to_axis.sv \
axis_spi.sv \
bin2gray.v \
binary_encoder.v \
clock_div.v \
../rfnoc/core/ctrlport_pkg.sv \
ctrlport_spi_adrv_pkg.sv \
ctrlport_spi_adrv.sv \
db_control.v \
fe_control.v \
filter_bad_sid.v \
@ -65,6 +71,9 @@ axil_ctrlport_master.v\
handshake.v\
handshake_latch.sv\
ctrlport_to_regport.v \
ctrlport_to_wb_i2c.v \
ctrlport_to_wb_uart.v \
ctrlport_to_simple_spi.sv \
glitch_free_mux.v \
regport_if.sv \
))

View File

@ -0,0 +1,380 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_spi
//
// Description:
//
// SPI master (CPOL=0, CPHA=0) with AXI-Stream TX and RX interfaces. Each
// byte presented on the input stream is shifted out MSB-first on MOSI while
// MISO bits are simultaneously assembled into bytes and presented on the
// output stream (full-duplex).
//
// CS_N is asserted (driven low) and held low for the duration of the
// AXI-Stream packet. Between words, CS_N stays asserted and SCLK simply
// pauses if the next byte in the packet is not yet available (s_axis_tvalid
// == 0) or if there's back-pressure on the output (m_axis_tready == 0).
//
// After the last SCLK falling edge, CS_N deassertion timing is controlled by
// CS_HOLD and CS_GUARD:
//
// CS_HOLD=0 : CS_N deasserts on the same clock edge as the last SCLK
// falling edge.
// CS_HOLD=1 : CS_N stays low for one additional half-period before being
// deasserted.
//
// After CS_N deasserts, CS_GUARD determines the number of half-period
// cycles before the next CS_N assertion for the next packet:
//
// CS_GUARD=1 : One half SCLK period before next CS_N assertion.
// CS_GUARD=2 : One full SCLK period before next CS_N assertion.
//
// SPI Clock Rate:
//
// The SPI clock rate is controlled by setting the desired number of clock
// cycles per half SPI clock period, minus 1. In other words, the actual
// SPI clock frequency for a given clock frequency will be:
//
// f_sclk = f_clk / (2 * (half_per + 1))
//
// Or, the required half period setting to approximate given SPI clock
// frequency can be calculated as:
//
// half_per = round((f_clk / (2 * f_sclk)) - 1)
//
// Parameters:
//
// HALF_PER_W : Width of the half-period input port in bits. This determines
// the slowest SPI clock rate that can be achieved.
// CS_HOLD : Number of half SCLK periods to hold CS_N low after last
// falling edge of SCLK. Can be 0 or 1.
// CS_GUARD : Number of half SCLK periods to hold CS_N between
// transactions. Can be 1 or 2.
//
// Signals:
//
// clk / rst : Clock and active-high synchronous reset.
// half_per : Half-period value; compared against an internal counter
// each cycle. SCLK toggles every (half_per + 1) clock cycles.
// Minimum value is 0, which yields the fastest SCLK rate of
// clk/2. This port must remain stable for the duration of any
// active transaction; it is expected to be driven by a static
// constant or an external register that is only updated while
// the core is idle.
// s_axis_* : AXI-Stream slave (TX data in). Each byte is serialized
// MSB-first.
// m_axis_* : AXI-Stream master (RX data out). Each byte is serialized
// MSB-first.
// sclk : SPI clock output
// cs_n : Active-low chip select
// mosi : Master-out / slave-in
// miso : Master-in / slave-out input
//
`default_nettype none
module axis_spi #(
int HALF_PER_W = 8,
bit CS_HOLD = 1,
int CS_GUARD = 2,
localparam int DATA_W = 8
) (
input wire clk,
input wire rst,
// Half-period control
input wire [HALF_PER_W-1:0] half_per,
// AXI-Stream Slave (TX data in)
input wire [DATA_W-1:0] s_axis_tdata,
input wire s_axis_tvalid,
output logic s_axis_tready,
input wire s_axis_tlast,
// AXI-Stream Master (RX data out)
output logic [DATA_W-1:0] m_axis_tdata,
output logic m_axis_tvalid,
input wire m_axis_tready,
output logic m_axis_tlast,
// SPI Interface
output logic sclk = 1'b0,
output logic cs_n = 1'b1,
output logic mosi = 1'b0,
input wire miso
);
//---------------------------------------------------------------------------
// Parameter Checks
//---------------------------------------------------------------------------
if (CS_HOLD < 0 || CS_HOLD > 1) begin : gen_hold_check
$error("axis_spi: CS_HOLD must be 0 or 1 (setting is %0d)", CS_HOLD);
end
if (CS_GUARD < 1 || CS_GUARD > 2) begin : gen_guard_check
$error("axis_spi: CS_GUARD must be 1 or 2 (setting is %0d)", CS_GUARD);
end
//---------------------------------------------------------------------------
// Local Parameters
//---------------------------------------------------------------------------
// Enough bits to count 0 .. DATA_W-1
localparam int BIT_CNT_W = $clog2(DATA_W);
//---------------------------------------------------------------------------
// State Machine Type
//---------------------------------------------------------------------------
typedef enum logic [2:0] {
ST_IDLE,
ST_SHIFT_A,
ST_SHIFT_B,
ST_HOLD,
ST_GUARD1,
ST_GUARD2
} state_t;
state_t state;
//---------------------------------------------------------------------------
// Internal Signals
//---------------------------------------------------------------------------
// Half-period counter. Done asserts when it reaches half_per.
logic [HALF_PER_W-1:0] half_per_cnt;
logic half_per_done;
// Bit position counter (0 to DATA_W-1)
logic [BIT_CNT_W-1:0] bit_cnt;
// TX / RX shift registers
logic [DATA_W-1:0] mosi_sr;
logic [DATA_W-1:0] miso_sr;
// tlast latched to indicate the end of the SPI transaction
logic tlast_reg;
// True when CS_N is currently asserted (mid-packet)
logic in_packet;
// One-entry registered output buffer
logic [DATA_W-1:0] out_data;
logic out_valid;
logic out_last;
// True when the output buffer is free this cycle (empty or being drained)
logic out_buf_free;
//---------------------------------------------------------------------------
// Combinational Assignments
//---------------------------------------------------------------------------
assign half_per_done = (half_per_cnt == half_per);
assign out_buf_free = !out_valid || m_axis_tready;
// AXI-Stream output driven from the registered output buffer
assign m_axis_tdata = out_data;
assign m_axis_tvalid = out_valid;
assign m_axis_tlast = out_last;
// Accept input when idle and the output buffer has room. In ST_IDLE the SPI
// bus is paused (SCLK stopped), so CS_N stays asserted while we wait for
// the next word when in_packet=1.
assign s_axis_tready = (state == ST_IDLE) && out_buf_free;
//---------------------------------------------------------------------------
// Half-Period Counter
//---------------------------------------------------------------------------
//
// Counts up every cycle while a SPI transaction is occurring. Resets to zero
// the same cycle half_per_done fires so each half-period is exactly
// (half_per + 1) clock cycles.
//
//---------------------------------------------------------------------------
always_ff @(posedge clk) begin
if (rst) begin
half_per_cnt <= '0;
end else begin
if (state == ST_IDLE) begin
half_per_cnt <= '0;
end else begin
if (half_per_done) begin
half_per_cnt <= '0;
end else begin
half_per_cnt <= half_per_cnt + 1'b1;
end
end
end
end
//---------------------------------------------------------------------------
// State Machine
//---------------------------------------------------------------------------
always_ff @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
sclk <= 1'b0;
cs_n <= 1'b1;
mosi <= 1'b0;
mosi_sr <= 'X;
miso_sr <= 'X;
bit_cnt <= 'X;
tlast_reg <= 1'b0;
in_packet <= 1'b0;
out_data <= 'X;
out_valid <= 1'b0;
out_last <= 'X;
end else begin
//-----------------------------------------------------------------------
// Output buffer drain
//-----------------------------------------------------------------------
if (m_axis_tready && out_valid) begin
out_valid <= 1'b0;
end
unique case (state)
//---------------------------------------------------------------------
// IDLE: SCLK stopped. CS_N follows in_packet (stays asserted between
// bytes of a packet, deasserted before/after).
//
// On input handshake: latch word and tlast, pre-drive MOSI with the
// MSB, assert CS_N, and enter the shift loop.
//---------------------------------------------------------------------
ST_IDLE: begin
sclk <= 1'b0;
cs_n <= !in_packet;
bit_cnt <= '0;
if (s_axis_tvalid && out_buf_free) begin
mosi_sr <= s_axis_tdata;
tlast_reg <= s_axis_tlast;
// Assert CS_N and pre-drive MSB onto MOSI before the first
// SCLK rising edge (CPHA=0 requirement).
cs_n <= 1'b0;
mosi <= s_axis_tdata[DATA_W-1];
in_packet <= 1'b1;
state <= ST_SHIFT_A;
end
end
//---------------------------------------------------------------------
// SHIFT_A: First (low) half of the current SCLK bit period.
//
// SCLK rises at the end of this half, MISO is sampled on the rising
// edge.
//---------------------------------------------------------------------
ST_SHIFT_A: begin
if (half_per_done) begin
sclk <= 1'b1;
// Sample MISO into the LSB of the receive shift register
miso_sr <= {miso_sr[DATA_W-2:0], miso};
state <= ST_SHIFT_B;
end
end
//---------------------------------------------------------------------
// SHIFT_B: Second (high) half of the current SCLK bit period.
//
// SCLK falls at the end of this half, MOSI advances to the next bit on
// the falling edge.
//
// On the last bit, push the assembled received byte to the output
// buffer and choose the next state.
//---------------------------------------------------------------------
ST_SHIFT_B: begin
if (half_per_done) begin
sclk <= 1'b0;
bit_cnt <= bit_cnt + 1;
// Advance MOSI shift register and drive the next bit
mosi_sr <= {mosi_sr[DATA_W-2:0], 1'b0};
mosi <= mosi_sr[DATA_W-2];
if (bit_cnt == DATA_W-1) begin
// Last bit don. Push received word to output buffer. out_valid
// guaranteed 0 here (see block header).
out_data <= miso_sr;
out_valid <= 1'b1;
out_last <= tlast_reg;
bit_cnt <= '0;
if (tlast_reg) begin
// End of packet. Update CS_N based on CS_HOLD.
in_packet <= 1'b0;
if (CS_HOLD) begin
state <= ST_HOLD;
end else begin
cs_n <= 1'b1;
state <= ST_GUARD1;
end
end else begin
// More words to come. Return to IDLE to accept the next word.
// CS_N stays asserted while we wait.
state <= ST_IDLE;
end
end else begin
state <= ST_SHIFT_A;
end
end
end
//---------------------------------------------------------------------
// HOLD: Hold CS_N asserted for one extra half-period after the last
// SCLK falling edge (only entered if CS_HOLD=1), then deassert CS_N
// and proceed to the guard entry state.
//---------------------------------------------------------------------
ST_HOLD: begin
if (half_per_done) begin
cs_n <= 1'b1;
state <= ST_GUARD1;
end
end
//---------------------------------------------------------------------
// GUARD1/GUARD2: Hold CS_N deasserted for CS_GUARD half-periods after
// the end of the transaction.
//---------------------------------------------------------------------
ST_GUARD1: begin
if (half_per_done) begin
state <= (CS_GUARD > 1) ? ST_GUARD2 : ST_IDLE;
end
end
ST_GUARD2: begin
if (half_per_done) begin
state <= ST_IDLE;
end
end
default: begin
state <= ST_IDLE;
end
endcase
end
end
endmodule : axis_spi
`default_nettype wire

View File

@ -0,0 +1,135 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: axis_to_ctrlport
//
// Description:
//
// Converts an AXI-Stream bus into a readable CtrlPort interface. Read
// requests get responded to as soon as data becomes available on the
// AXI-Stream input.
//
// Parameters:
//
// WIDTH : Width of the data path in bits
// FIFO_SIZE_LOG2 : Log base 2 of the depth of the input AXI-Stream FIFO.
// This FIFO is optional. Depth can be set to -1 to remove
// it entirely.
//
// Signals:
//
// clk : Clock
// rst : Active-high synchronous reset
// s_axis_tdata : AXI-Stream data input
// s_axis_tvalid : AXI-Stream valid
// s_axis_tready : AXI-Stream ready
// ctrl_rd : CtrlPort read request strobe (single-cycle pulse)
// ctrl_data : CtrlPort read data output
// ctrl_ack : CtrlPort acknowledgement strobe (single-cycle pulse)
//
`default_nettype none
module axis_to_ctrlport #(
parameter int WIDTH = 32,
parameter int FIFO_SIZE_LOG2 = 0
) (
input wire clk,
input wire rst,
// AXI-Stream slave
input wire [WIDTH-1:0] s_axis_tdata,
input wire s_axis_tvalid,
output wire s_axis_tready,
// CtrlPort read interface
input wire ctrl_rd,
output logic [WIDTH-1:0] ctrl_data,
output logic ctrl_ack
);
//---------------------------------------------------------------------------
// Declarations
//---------------------------------------------------------------------------
typedef enum logic { ST_IDLE, ST_WAIT } state_t;
state_t state;
// Internal stream between the input FIFO and the FSM.
logic [WIDTH-1:0] fifo_o_tdata;
logic fifo_o_tvalid;
logic fifo_o_tready;
//---------------------------------------------------------------------------
// Input FIFO
//---------------------------------------------------------------------------
axi_fifo #(
.WIDTH (WIDTH),
.SIZE (FIFO_SIZE_LOG2)
) input_fifo_i (
.clk (clk),
.reset (rst),
.clear (1'b0),
.i_tdata (s_axis_tdata),
.i_tvalid (s_axis_tvalid),
.i_tready (s_axis_tready),
.o_tdata (fifo_o_tdata),
.o_tvalid (fifo_o_tvalid),
.o_tready (fifo_o_tready),
.space (),
.occupied ()
);
//---------------------------------------------------------------------------
// State Machine
//---------------------------------------------------------------------------
assign ctrl_data = fifo_o_tdata;
assign fifo_o_tready = ctrl_ack;
always_ff @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
ctrl_ack <= 1'b0;
end else begin
// Default: deassert ACK every cycle.
ctrl_ack <= 1'b0;
case (state)
ST_IDLE: begin
if (ctrl_rd) begin
if (fifo_o_tvalid) begin
// If we have data available, then ACK on next clock cycle.
ctrl_ack <= 1'b1;
end else begin
// If we don't have data yet, then wait until it arrives.
state <= ST_WAIT;
end
end
end
ST_WAIT: begin
// ACK after we get a new data word
if (fifo_o_tvalid) begin
ctrl_ack <= 1'b1;
state <= ST_IDLE;
end
end
default: begin
state <= ST_IDLE;
end
endcase
end
end
endmodule : axis_to_ctrlport
`default_nettype wire

View File

@ -0,0 +1,991 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_spi_adrv
//
// Description:
//
// SPI master for the ADI ADRV transceiver family. It is controlled via a
// CtrlPort slave interface and it drives a standard 4-wire SPI bus (CPOL=0,
// CPHA=0). It is designed to make efficient use of the CtrlPort bus by
// eliminating the need to write redundant address bytes (e.g., repeating the
// address for writes to the same address, or writes to sequential
// addresses), the need to write dummy bytes in order to flush out a read
// result, or the need to read back echo bytes in responses. Only the
// relevant bytes need to be communicated over CtrlPort.
//
// Furthermore, the SPI transactions are blocking in the sense that the
// CtrlPort interface will not ACK until the request is accepted and
// guaranteed to complete. This allows multiple SPI transactions to be
// enqueued in the underlying CtrlPort endpoint FIFO without the need for
// polling the core for status.
//
// Two methods are available for issuing SPI transactions: REG_CONTROl and
// REG_SI.
//
// REG_CONTROL Interface
//
// With this interface, the REG_CONTROL register controls the mode,
// direction, and the number of data bytes in the transaction. Four
// transaction modes are supported via the REG_CONTROL register.
//
// RAW : The exact byte stream in the data register is shifted out.
// The caller is responsible for embedding any SPI address.
//
// SI_SEQ : Single instruction, sequential. Each data byte is preceded
// by the 16-bit SPI address written in REG_CONTROL, which
// auto-increments by one for every byte. Three SPI bytes are
// sent per data byte.
//
// SI_REP : Single instruction, repeated. Same as SI_SEQ but the
// address does not increment. The same address is sent
// before every data byte.
//
// STREAMING : The 16-bit SPI address is sent once, followed by all data
// bytes back-to-back. Two address bytes plus num_bytes data
// bytes are sent total.
//
//
// For write operations in all the above modes, the RX state machine
// ignores the incoming bytes from the SPI interface without storing them.
// So, it is not necessary to read anything when doing a write.
//
// For read operations in RAW mode, all MISO bytes are returned such that
// you must read the same number of bytes that were written.
//
// For read operations in SI_SEQ, SI_REP, and STREAMING mode, the state
// machine automatically drives MOSI with zeros during the data phase, so
// that it is not necessary to write any data bytes to execute a read. And
// it automatically captures only the data bytes from MISO; the address
// echo bytes are discarded. So, you only need to read the number of data
// bytes that were requested to be written (excluding the address bytes).
//
// REG_SI Interface
//
// For a single-byte read or write (referred to as "single instruction" in
// the ADI code), there is also a REG_SI register to which a single 3-byte
// instruction can be written (addr_hi, addr_lo, and data byte). This is
// equivalent to using RAW mode with a length of 3 bytes. If the MSB of the
// address (bit 15) is set, then it is treated as a read and the 3 RX data
// bytes are inserted into the RX FIFO and can be read by reading the
// REG_DATA register.
//
// The benefit of using REG_SI is that you don't need to also write to the
// REG_CONTROL register. So, fewer CtrlPort writes are needed for three
// instructions or less compared to RAW mode. On the downside, each
// instruction is its own SPI transaction (i.e., CS_n deasserts between
// each instruction). So, for 2 or more instructions, additional time may
// be used on the SPI bus.
//
// RX Data FIFO
//
// Received bytes are packed into 32-bit words (first byte in bits [7:0],
// second byte in bits [15:8], etc.) and stored in the RX word FIFO. Each
// transaction ends on a word boundary; if the number of received bytes is
// not a multiple of 4, the remaining bytes in the last word are undefined.
// This means that the first byte of a transaction always starts with bits
// [7:0] of a new FIFO word, regardless of how many bytes the previous
// transaction produced.
//
// Each REG_SI read request always produces exactly 3 received bytes, which
// are padded to a full 32-bit word (one FIFO entry). So each REG_SI read
// fills one word of the FIFO, but only bytes [23:0] are valid.
//
// The host software must not overfill the FIFO. The default depth holds
// one complete maximum-size transaction.
//
// Data Word Byte Ordering
//
// Words are little-endian packed. That is, byte 0 (the first byte to be
// sent over SPI) occupies data[7:0], byte 1 occupies data[15:8], etc. Each
// byte is serialized MSB-first. That is, bit 7 leaves the MOSI/MISO pin
// first, bit 0 last.
//
// CS_N Behavior
//
// Asserts when REG_CONTROL is written and deasserts automatically after
// all num_bytes have been shifted and captured. CS_N remains asserted until
// the entire transaction has completed; the SPI clock simply pauses
// between data bytes when the TX buffer is empty.
//
// For REG_SI, CS_N asserts/deasserts for each single instruction.
//
// CtrlPort Flow Control
//
// REG_CONTROL write : ACKs after the control word has been accepted.
// REG_DATA write : ACKs after the write has been accepted and is
// guaranteed to complete.
// REG_DATA read : ACK'd when the data to be read becomes available.
//
// SPI Clock Rate
//
// The SPI clock rate is controlled by setting the desired number of clock
// cycles per half SPI clock period, minus 1. In other words, the actual
// SPI clock frequency for a given clock frequency will be:
//
// f_sclk = f_clk / (2 * (half_per + 1))
//
// Or, the required half period setting for a given SPI clock frequency can
// be calculated as:
//
// half_per = (f_clk / (2 * f_sclk)) - 1
//
// Parameters:
//
// BASE_ADDR : CtrlPort base address.
// NUM_BYTES_W : Log base 2 of maximum number data bytes supported per
// transaction. Must not exceed SPI_NUM_BYTES_W.
// HALF_PER : Default sclk half-period length in clk cycles
// HALF_PER_W : Width of the half-period register in bits.
// HALF_PER_EN : When 1, the half-period register (REG_HALF_PER) exists and
// is software-readable and writable. When 0, the register
// does not exist; the SCLK rate is fixed at the HALF_PER
// parameter value.
// CS_HOLD : 1 = hold CS_N low for one extra half-period after the last
// SCLK falling edge. 0 = deassert CS_N immediately on the
// last SCLK falling edge.
// CS_GUARD : Number of CS_N-high half-periods between transactions. Must
// be 1 or 2.
// RX_FIFO_SIZE : Log base 2 of the depth of the 32-bit wide RX word FIFO. By
// default, it holds one complete maximum-size transaction
// (2**NUM_BYTES_W bytes / 4 bytes per word). The host
// software must NOT overfill the FIFO.
//
// Signals:
//
// clk / rst : Clock and active-high synchronous reset.
// s_ctrlport_* : CtrlPort slave request/response signals.
// sclk : SPI clock output (registered).
// cs_n : Active-low chip select (registered).
// mosi : Master-out / slave-in data (registered).
// miso : Master-in / slave-out data input.
//
`default_nettype none
module ctrlport_spi_adrv
import ctrlport_pkg::*;
import ctrlport_spi_adrv_pkg::*;
#(
int BASE_ADDR = 0,
int NUM_BYTES_W = 8,
int HALF_PER = 9,
int HALF_PER_W = 8,
bit HALF_PER_EN = 1,
bit CS_HOLD = 1,
int CS_GUARD = 2,
int RX_FIFO_SIZE = NUM_BYTES_W-2
) (
input wire clk,
input wire rst,
// CtrlPort Slave Request
input wire s_ctrlport_req_wr,
input wire s_ctrlport_req_rd,
input wire [CTRLPORT_ADDR_W-1:0] s_ctrlport_req_addr,
input wire [CTRLPORT_DATA_W-1:0] s_ctrlport_req_data,
// CtrlPort Slave Response
output logic s_ctrlport_resp_ack,
output ctrlport_status_t s_ctrlport_resp_status,
output logic [CTRLPORT_DATA_W-1:0] s_ctrlport_resp_data,
// SPI Interface
output logic sclk,
output logic cs_n,
output logic mosi,
input wire miso
);
//---------------------------------------------------------------------------
// Parameter Checks
//---------------------------------------------------------------------------
if (NUM_BYTES_W > SPI_NUM_BYTES_W) begin : gen_error
$error("NUM_BYTES_W (%0d) must not exceed SPI_NUM_BYTES_W (%0d).",
NUM_BYTES_W, SPI_NUM_BYTES_W);
end : gen_error
//---------------------------------------------------------------------------
// Declarations
//---------------------------------------------------------------------------
// SPI clock half-period. Driven by a software-writable register when
// HALF_PER_EN=1; wired to the parameter value when HALF_PER_EN=0.
logic [HALF_PER_W-1:0] half_per;
// TX 32-bit AXI-Stream (from register block into TX width converter)
logic [31:0] tx_word_tdata;
logic [ 3:0] tx_word_tkeep;
logic tx_word_tvalid;
logic tx_word_tready;
logic tx_word_tlast;
// TX 8-bit AXI-Stream (from TX width converter into axis_spi)
logic [7:0] tx_byte_tdata;
logic tx_byte_tvalid;
logic tx_byte_tready;
logic tx_byte_tlast;
// RX 8-bit AXI-Stream (from axis_spi into RX FSM)
logic [7:0] spi_rx_tdata;
logic spi_rx_tvalid;
logic spi_rx_tready;
logic spi_rx_tlast;
// RX 8-bit AXI-Stream (from RX FSM into RX width converter)
logic [7:0] rx_byte_tdata;
logic rx_byte_tvalid;
logic rx_byte_tready;
logic rx_byte_tlast;
// RX 32-bit AXI-Stream (from RX width converter into RX FIFO)
logic [31:0] rx_word_tdata;
logic rx_word_tvalid;
logic rx_word_tready;
logic rx_word_tlast;
// RX FIFO output
logic [31:0] rx_fifo_tdata;
logic rx_fifo_tvalid;
logic rx_fifo_tready;
//---------------------------------------------------------------------------
// Address Decode
//---------------------------------------------------------------------------
logic address_in_range;
logic is_si_reg;
logic is_ctrl_reg;
logic is_data_win;
assign address_in_range =
(s_ctrlport_req_addr[CTRLPORT_ADDR_W-1:REG_ADDR_W] ==
BASE_ADDR[CTRLPORT_ADDR_W-1:REG_ADDR_W]);
assign is_si_reg = address_in_range &&
(s_ctrlport_req_addr[0+:REG_ADDR_W] == REG_SI);
assign is_ctrl_reg = address_in_range &&
(s_ctrlport_req_addr[0+:REG_ADDR_W] == REG_CONTROL);
assign is_data_win = address_in_range &&
(s_ctrlport_req_addr[0+:REG_ADDR_W] >= REG_DATA) &&
(s_ctrlport_req_addr[0+:REG_ADDR_W] < REG_DATA + 64);
//---------------------------------------------------------------------------
// Half-Period Register
//---------------------------------------------------------------------------
//
// When HALF_PER_EN=1 the register exists and is software-readable and
// writable. When HALF_PER_EN=0 none of this logic exists and half_per is a
// constant tied to the HALF_PER parameter.
//---------------------------------------------------------------------------
logic half_per_ack;
if (HALF_PER_EN) begin : gen_half_per_reg
logic is_half_per_reg;
assign is_half_per_reg = address_in_range &&
(s_ctrlport_req_addr[0+:REG_ADDR_W] == REG_HALF_PER);
always_ff @(posedge clk) begin
if (rst) begin
half_per <= HALF_PER;
half_per_ack <= 1'b0;
end else begin
half_per_ack <= 1'b0;
if (is_half_per_reg) begin
if (s_ctrlport_req_wr) begin
half_per <= s_ctrlport_req_data[HALF_PER_W-1:0];
half_per_ack <= 1'b1;
end else if (s_ctrlport_req_rd) begin
half_per_ack <= 1'b1;
end
end
end
end
end else begin : gen_half_per_fixed
assign half_per = HALF_PER;
assign half_per_ack = 1'b0;
end : gen_half_per_fixed
//---------------------------------------------------------------------------
// Control Register Write Handling
//---------------------------------------------------------------------------
// For writes to REG_SI, we must also write a control word equivalent to a
// 3-byte RAW control word, with read or write set based on the MSB of the
// address.
spi_adrv_ctrl_t si_ctrl_word;
assign si_ctrl_word = '{
dir: s_ctrlport_req_data[7] ? SPI_READ : SPI_WRITE,
rsvd1: 'X,
mode: MODE_RAW,
num_bytes: 3,
addr: 'X
};
logic [CTRLPORT_DATA_W-1:0] ctrl_data;
logic ctrl_wr;
logic ctrl_ack;
logic [CTRLPORT_DATA_W-1:0] ctrl_tdata;
logic ctrl_tvalid;
logic ctrl_tready;
// Control word split to TX and RX FSMs.
spi_adrv_ctrl_t ctrl_tx_tdata;
logic ctrl_tx_tvalid;
logic ctrl_tx_tready;
spi_adrv_ctrl_t ctrl_rx_tdata;
logic ctrl_rx_tvalid;
logic ctrl_rx_tready;
// REG_CONTROL writes pass the data word through. REG_SI writes use the
// synthesized control word above.
assign ctrl_data = is_si_reg ? si_ctrl_word : s_ctrlport_req_data;
assign ctrl_wr = s_ctrlport_req_wr && (is_ctrl_reg || is_si_reg);
ctrlport_to_axis #(
.WIDTH (CTRLPORT_DATA_W),
.FIFO_SIZE_LOG2 (1)
) ctrlport_to_axis_ctrl (
.clk (clk),
.rst (rst),
.ctrl_data (ctrl_data),
.ctrl_wr (ctrl_wr),
.ctrl_ack (ctrl_ack),
.m_axis_tdata (ctrl_tdata),
.m_axis_tvalid (ctrl_tvalid),
.m_axis_tready (ctrl_tready)
);
// Duplicate the control word to the TX FSM (port 0) and RX FSM (port 1).
axis_split #(
.DATA_W (CTRLPORT_DATA_W),
.NUM_PORTS (2),
.INPUT_REG (0),
.OUT_FIFO_SIZE (1)
) ctrl_split_i (
.clk (clk),
.rst (rst),
.s_axis_tdata (ctrl_tdata),
.s_axis_tvalid (ctrl_tvalid),
.s_axis_tready (ctrl_tready),
.m_axis_tdata ({ctrl_rx_tdata, ctrl_tx_tdata}),
.m_axis_tvalid ({ctrl_rx_tvalid, ctrl_tx_tvalid}),
.m_axis_tready ({ctrl_rx_tready, ctrl_tx_tready})
);
//---------------------------------------------------------------------------
// Data Register Write Handling
//---------------------------------------------------------------------------
logic data_wr;
logic data_ack;
assign data_wr = s_ctrlport_req_wr && (is_data_win || is_si_reg);
ctrlport_to_axis #(
.WIDTH (CTRLPORT_DATA_W),
.FIFO_SIZE_LOG2 (0)
) ctrlport_to_axis_data (
.clk (clk),
.rst (rst),
.ctrl_data (s_ctrlport_req_data),
.ctrl_wr (data_wr),
.ctrl_ack (data_ack),
.m_axis_tdata (tx_word_tdata),
.m_axis_tvalid (tx_word_tvalid),
.m_axis_tready (tx_word_tready)
);
//---------------------------------------------------------------------------
// Data Register Read Handling
//---------------------------------------------------------------------------
logic [CTRLPORT_DATA_W-1:0] data_a2c_data;
logic data_a2c_ack;
logic data_a2c_rd;
assign data_a2c_rd = s_ctrlport_req_rd && is_data_win;
axis_to_ctrlport #(
.WIDTH (CTRLPORT_DATA_W),
.FIFO_SIZE_LOG2 (0)
) axis_to_ctrlport_rx (
.clk (clk),
.rst (rst),
.s_axis_tdata (rx_fifo_tdata),
.s_axis_tvalid (rx_fifo_tvalid),
.s_axis_tready (rx_fifo_tready),
.ctrl_rd (data_a2c_rd),
.ctrl_data (data_a2c_data),
.ctrl_ack (data_a2c_ack)
);
//---------------------------------------------------------------------------
// CtrlPort Response Acknowledgement
//---------------------------------------------------------------------------
//
// A REG_SI write triggers both ctrl_wr and data_wr simultaneously, so
// ctrl_ack and data_ack fire independently at different times. This state
// machine ensures exactly one s_ctrlport_resp_ack pulse is issued per
// request, once all expected ACKs have arrived.
//
// ST_ACK_IDLE : Waiting for a write request.
// ST_ACK_WAIT_ONE : Expecting one more ACK (REG_CONTROL or REG_DATA write,
// or REG_SI write after one of the two ACKs has arrived).
// ST_ACK_WAIT_TWO : Expecting two ACKs (REG_SI write, neither arrived yet);
// if both arrive on the same cycle, coalesce immediately.
//
// data_a2c_ack (axis-to-ctrlport for REG_DATA reads) is always single-source
// and bypasses the state machine, ORing directly into s_ctrlport_resp_ack.
//
//---------------------------------------------------------------------------
typedef enum logic [1:0] {
ST_ACK_IDLE = 0,
ST_ACK_WAIT_ONE = 1,
ST_ACK_WAIT_TWO = 2
} ack_state_t;
ack_state_t ack_state;
logic ack_out;
always_ff @(posedge clk) begin
if (rst) begin
ack_state <= ST_ACK_IDLE;
ack_out <= 1'b0;
end else begin
ack_out <= 1'b0;
case (ack_state)
ST_ACK_IDLE : begin
if (s_ctrlport_req_wr) begin
if (is_si_reg)
ack_state <= ST_ACK_WAIT_TWO;
else
ack_state <= ST_ACK_WAIT_ONE;
end
end
ST_ACK_WAIT_ONE : begin
if (ctrl_ack || data_ack) begin
ack_out <= 1'b1;
ack_state <= ST_ACK_IDLE;
end
end
ST_ACK_WAIT_TWO : begin
if (ctrl_ack && data_ack) begin
// Both arrived on the same cycle.
ack_out <= 1'b1;
ack_state <= ST_ACK_IDLE;
end else if (ctrl_ack || data_ack) begin
// One arrived; wait for the other.
ack_state <= ST_ACK_WAIT_ONE;
end
end
default : ack_state <= ST_ACK_IDLE;
endcase
end
end
always_ff @(posedge clk) begin
if (rst) begin
s_ctrlport_resp_ack <= 1'b0;
s_ctrlport_resp_data <= 'X;
end else begin
s_ctrlport_resp_ack <= ack_out | data_a2c_ack | half_per_ack;
s_ctrlport_resp_data <= half_per_ack ? half_per : data_a2c_data;
end
end
assign s_ctrlport_resp_status = STS_OKAY;
//---------------------------------------------------------------------------
// TX Width Converter (32-bit -> 8-bit)
//---------------------------------------------------------------------------
axis_width_conv #(
.WORD_W (8),
.IN_WORDS (4),
.OUT_WORDS (1),
.SYNC_CLKS (1),
.PIPELINE ("OUT")
) axis_width_conv_tx (
.s_axis_aclk (clk),
.s_axis_rst (rst),
.s_axis_tdata (tx_word_tdata),
.s_axis_tkeep ('1),
.s_axis_tlast ('0),
.s_axis_tvalid (tx_word_tvalid),
.s_axis_tready (tx_word_tready),
.m_axis_aclk (clk),
.m_axis_rst (rst),
.m_axis_tdata (tx_byte_tdata),
.m_axis_tkeep (),
.m_axis_tlast (tx_byte_tlast),
.m_axis_tvalid (tx_byte_tvalid),
.m_axis_tready (tx_byte_tready)
);
//---------------------------------------------------------------------------
// TX State Machine
//---------------------------------------------------------------------------
// AXI-Stream from TX FSM into axis_spi.
logic [7:0] spi_tx_tdata;
logic spi_tx_tvalid;
logic spi_tx_tready;
logic spi_tx_tlast;
typedef enum logic [3:0] {
ST_TX_IDLE = 0,
ST_TX_RAW = 1,
ST_TX_SI_ADDR_HI = 2,
ST_TX_SI_ADDR_LO = 3,
ST_TX_SI_DATA = 4,
ST_TX_STREAM_ADDR_HI = 5,
ST_TX_STREAM_ADDR_LO = 6,
ST_TX_STREAM_DATA = 7,
ST_TX_DRAIN = 8
} tx_state_t;
tx_state_t tx_state;
logic [SPI_NUM_BYTES_W-1:0] tx_bytes_rem; // Data bytes remaining
logic [ 15:0] tx_cur_addr; // Running SPI address
logic [ 1:0] tx_drain_cnt; // Padding bytes to drain after transaction
always_ff @(posedge clk) begin
if (rst) begin
tx_state <= ST_TX_IDLE;
tx_bytes_rem <= '0;
tx_cur_addr <= '0;
tx_drain_cnt <= 2'b0;
end else begin
case (tx_state)
ST_TX_IDLE : begin
if (ctrl_tx_tvalid) begin
tx_bytes_rem <= ctrl_tx_tdata.num_bytes;
tx_cur_addr <= ctrl_tx_tdata.addr;
tx_drain_cnt <= 2'b0 - ctrl_tx_tdata.num_bytes[1:0];
case (ctrl_tx_tdata.mode)
MODE_RAW : tx_state <= ST_TX_RAW;
MODE_SI_SEQ,
MODE_SI_REP : tx_state <= ST_TX_SI_ADDR_HI;
MODE_STREAMING : tx_state <= ST_TX_STREAM_ADDR_HI;
default : tx_state <= ST_TX_RAW;
endcase
end
end
ST_TX_RAW : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_bytes_rem <= tx_bytes_rem - 1;
if (tx_bytes_rem == 1) begin
tx_state <= (tx_drain_cnt != 0) ? ST_TX_DRAIN : ST_TX_IDLE;
end
end
end
ST_TX_SI_ADDR_HI : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_state <= ST_TX_SI_ADDR_LO;
end
end
ST_TX_SI_ADDR_LO : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_state <= ST_TX_SI_DATA;
end
end
ST_TX_SI_DATA : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_bytes_rem <= tx_bytes_rem - 1;
if (tx_bytes_rem == 1) begin
if (ctrl_tx_tdata.dir == SPI_WRITE && tx_drain_cnt != 0) begin
tx_state <= ST_TX_DRAIN;
end else begin
tx_state <= ST_TX_IDLE;
end
end else begin
if (ctrl_tx_tdata.mode == MODE_SI_SEQ) begin
tx_cur_addr <= tx_cur_addr + 1;
end
tx_state <= ST_TX_SI_ADDR_HI;
end
end
end
ST_TX_STREAM_ADDR_HI : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_state <= ST_TX_STREAM_ADDR_LO;
end
end
ST_TX_STREAM_ADDR_LO : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_state <= ST_TX_STREAM_DATA;
end
end
ST_TX_STREAM_DATA : begin
if (spi_tx_tvalid && spi_tx_tready) begin
tx_bytes_rem <= tx_bytes_rem - 1;
if (tx_bytes_rem == 1) begin
if (ctrl_tx_tdata.dir == SPI_WRITE && tx_drain_cnt != 0) begin
tx_state <= ST_TX_DRAIN;
end else begin
tx_state <= ST_TX_IDLE;
end
end
end
end
ST_TX_DRAIN : begin
if (tx_byte_tvalid) begin
tx_drain_cnt <= tx_drain_cnt - 1;
if (tx_drain_cnt == 1) begin
tx_state <= ST_TX_IDLE;
end
end
end
default : begin
tx_state <= ST_TX_IDLE;
end
endcase
end
end
// ctrl_tx_tready is driven combinationally so that the control FIFO word is
// consumed on the exact same cycle that the last SPI byte is accepted by
// axis_spi. This ensures the FIFO advances before the FSM re-enters
// ST_TX_IDLE, so it sees the correct next control word.
always_comb begin
spi_tx_tdata = 8'h00;
spi_tx_tvalid = 1'b0;
spi_tx_tlast = 1'b0;
tx_byte_tready = 1'b0;
ctrl_tx_tready = 1'b0;
case (tx_state)
ST_TX_IDLE : begin
// Waiting; nothing to send.
end
ST_TX_RAW : begin
// RAW: pass tx_byte bytes straight through regardless of direction.
spi_tx_tdata = tx_byte_tdata;
spi_tx_tvalid = tx_byte_tvalid;
tx_byte_tready = spi_tx_tready;
spi_tx_tlast = (tx_bytes_rem == 1);
ctrl_tx_tready = spi_tx_tvalid && spi_tx_tready && (tx_bytes_rem == 1);
end
ST_TX_SI_ADDR_HI : begin
// addr[15:8] = addr_hi; first byte on the wire.
spi_tx_tdata = tx_cur_addr[15:8];
spi_tx_tvalid = 1'b1;
end
ST_TX_SI_ADDR_LO : begin
// addr[7:0] = addr_lo; second byte on the wire.
spi_tx_tdata = tx_cur_addr[7:0];
spi_tx_tvalid = 1'b1;
end
ST_TX_SI_DATA : begin
if (ctrl_tx_tdata.dir == SPI_READ) begin
// Drive MOSI=0 for reads; do not consume TX data FIFO.
spi_tx_tdata = 8'h00;
spi_tx_tvalid = 1'b1;
end else begin
spi_tx_tdata = tx_byte_tdata;
spi_tx_tvalid = tx_byte_tvalid;
tx_byte_tready = spi_tx_tready;
end
spi_tx_tlast = (tx_bytes_rem == 1);
ctrl_tx_tready = spi_tx_tvalid && spi_tx_tready && (tx_bytes_rem == 1);
end
ST_TX_STREAM_ADDR_HI : begin
spi_tx_tdata = tx_cur_addr[15:8];
spi_tx_tvalid = 1'b1;
end
ST_TX_STREAM_ADDR_LO : begin
spi_tx_tdata = tx_cur_addr[7:0];
spi_tx_tvalid = 1'b1;
end
ST_TX_STREAM_DATA : begin
if (ctrl_tx_tdata.dir == SPI_READ) begin
spi_tx_tdata = 8'h00;
spi_tx_tvalid = 1'b1;
end else begin
spi_tx_tdata = tx_byte_tdata;
spi_tx_tvalid = tx_byte_tvalid;
tx_byte_tready = spi_tx_tready;
end
spi_tx_tlast = (tx_bytes_rem == 1);
ctrl_tx_tready = spi_tx_tvalid && spi_tx_tready && (tx_bytes_rem == 1);
end
ST_TX_DRAIN : begin
// Consume width-converter padding bytes without sending them on MOSI.
tx_byte_tready = 1'b1;
end
default : begin
// Nothing
end
endcase
end
//---------------------------------------------------------------------------
// SPI Interface
//---------------------------------------------------------------------------
axis_spi #(
.HALF_PER_W (HALF_PER_W),
.CS_HOLD (CS_HOLD),
.CS_GUARD (CS_GUARD)
) axis_spi_i (
.clk (clk),
.rst (rst),
.half_per (half_per),
.s_axis_tdata (spi_tx_tdata),
.s_axis_tvalid (spi_tx_tvalid),
.s_axis_tready (spi_tx_tready),
.s_axis_tlast (spi_tx_tlast),
.m_axis_tdata (spi_rx_tdata),
.m_axis_tvalid (spi_rx_tvalid),
.m_axis_tready (spi_rx_tready),
.m_axis_tlast (spi_rx_tlast),
.sclk (sclk),
.cs_n (cs_n),
.mosi (mosi),
.miso (miso)
);
//---------------------------------------------------------------------------
// RX State Machine
//---------------------------------------------------------------------------
//
// Filters bytes from spi_rx_*, forwarding data-phase bytes to rx_byte_*
// (axis_width_conv_rx) per the mode table below:
//
// RAW write : Discard all bytes until tlast.
// RAW read : Forward all bytes until tlast.
// SI_SEQ/REP : For each group of 3, discard addr_hi, discard addr_lo,
// forward data byte; repeat until tlast.
// STREAMING : Discard addr_hi, discard addr_lo, then forward all bytes
// until tlast.
//
//---------------------------------------------------------------------------
typedef enum logic [3:0] {
ST_RX_IDLE = 0,
ST_RX_RAW_DROP = 1,
ST_RX_SI_DISCARD_HI = 2,
ST_RX_SI_DISCARD_LO = 3,
ST_RX_SI_DATA = 4,
ST_RX_STREAM_DISCARD_HI = 5,
ST_RX_STREAM_DISCARD_LO = 6,
ST_RX_PASS_DATA = 7
} rx_state_t;
rx_state_t rx_state;
always_ff @(posedge clk) begin
if (rst) begin
rx_state <= ST_RX_IDLE;
end else begin
case (rx_state)
ST_RX_IDLE : begin
if (ctrl_rx_tvalid) begin
case (ctrl_rx_tdata.mode)
MODE_RAW : begin
rx_state <= ctrl_rx_tdata.dir == SPI_READ ?
ST_RX_PASS_DATA : ST_RX_RAW_DROP;
end
MODE_SI_SEQ,
MODE_SI_REP : begin
rx_state <= ctrl_rx_tdata.dir == SPI_READ ?
ST_RX_SI_DISCARD_HI : ST_RX_RAW_DROP;
end
MODE_STREAMING : begin
rx_state <= ctrl_rx_tdata.dir == SPI_READ ?
ST_RX_STREAM_DISCARD_HI : ST_RX_RAW_DROP;
end
default : rx_state <= ST_RX_RAW_DROP;
endcase
end
end
ST_RX_PASS_DATA : begin
if (spi_rx_tvalid && spi_rx_tready && spi_rx_tlast) begin
rx_state <= ST_RX_IDLE;
end
end
ST_RX_RAW_DROP : begin
if (spi_rx_tvalid && spi_rx_tready && spi_rx_tlast) begin
rx_state <= ST_RX_IDLE;
end
end
ST_RX_SI_DISCARD_HI : begin
if (spi_rx_tvalid && spi_rx_tready) begin
rx_state <= ST_RX_SI_DISCARD_LO;
end
end
ST_RX_SI_DISCARD_LO : begin
if (spi_rx_tvalid && spi_rx_tready) begin
rx_state <= ST_RX_SI_DATA;
end
end
ST_RX_SI_DATA : begin
if (spi_rx_tvalid && spi_rx_tready) begin
if (spi_rx_tlast) begin
rx_state <= ST_RX_IDLE;
end else begin
rx_state <= ST_RX_SI_DISCARD_HI;
end
end
end
ST_RX_STREAM_DISCARD_HI : begin
if (spi_rx_tvalid && spi_rx_tready) begin
rx_state <= ST_RX_STREAM_DISCARD_LO;
end
end
ST_RX_STREAM_DISCARD_LO : begin
if (spi_rx_tvalid && spi_rx_tready) begin
rx_state <= ST_RX_PASS_DATA;
end
end
default : begin
rx_state <= ST_RX_IDLE;
end
endcase
end
end
always_comb begin
// Default: consume from spi_rx but don't forward (discard).
spi_rx_tready = 1'b0;
rx_byte_tdata = spi_rx_tdata;
rx_byte_tvalid = 1'b0;
rx_byte_tlast = spi_rx_tlast;
ctrl_rx_tready = 1'b0;
case (rx_state)
ST_RX_IDLE : begin
// Waiting; hold spi_rx backpressured.
end
ST_RX_PASS_DATA,
ST_RX_SI_DATA : begin
// Forward bytes to the width converter.
spi_rx_tready = rx_byte_tready;
rx_byte_tvalid = spi_rx_tvalid;
ctrl_rx_tready = spi_rx_tvalid && spi_rx_tready && spi_rx_tlast;
end
ST_RX_RAW_DROP : begin
// Consume and discard; do not forward.
spi_rx_tready = 1'b1;
ctrl_rx_tready = spi_rx_tvalid && spi_rx_tready && spi_rx_tlast;
end
ST_RX_SI_DISCARD_HI,
ST_RX_SI_DISCARD_LO,
ST_RX_STREAM_DISCARD_HI,
ST_RX_STREAM_DISCARD_LO : begin
// Consume and discard; do not forward.
spi_rx_tready = 1'b1;
end
default : begin
// Discard.
spi_rx_tready = 1'b1;
end
endcase
end
//---------------------------------------------------------------------------
// RX Width Converter (8-bit -> 32-bit)
//---------------------------------------------------------------------------
axis_width_conv #(
.WORD_W (8),
.IN_WORDS (1),
.OUT_WORDS (4),
.SYNC_CLKS (1),
.PIPELINE ("NONE")
) axis_width_conv_rx (
.s_axis_aclk (clk),
.s_axis_rst (rst),
.s_axis_tdata (rx_byte_tdata),
.s_axis_tkeep (1'b1),
.s_axis_tlast (rx_byte_tlast),
.s_axis_tvalid (rx_byte_tvalid),
.s_axis_tready (rx_byte_tready),
.m_axis_aclk (clk),
.m_axis_rst (rst),
.m_axis_tdata (rx_word_tdata),
.m_axis_tkeep (),
.m_axis_tlast (rx_word_tlast),
.m_axis_tvalid (rx_word_tvalid),
.m_axis_tready (rx_word_tready)
);
//---------------------------------------------------------------------------
// RX FIFO
//---------------------------------------------------------------------------
axi_fifo #(
.WIDTH (32),
.SIZE (RX_FIFO_SIZE)
) axi_fifo_i (
.clk (clk),
.reset (rst),
.clear (1'b0),
.i_tdata (rx_word_tdata),
.i_tvalid (rx_word_tvalid),
.i_tready (rx_word_tready),
.o_tdata (rx_fifo_tdata),
.o_tvalid (rx_fifo_tvalid),
.o_tready (rx_fifo_tready),
.space (),
.occupied ()
);
endmodule : ctrlport_spi_adrv
`default_nettype wire

View File

@ -0,0 +1,72 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Package: ctrlport_spi_adrv_pkg
//
// Description:
//
// Shared constants and types for ctrlport_spi_adrv and its testbench.
//
//
package ctrlport_spi_adrv_pkg;
//---------------------------------------------------------------------------
// Register offsets (byte addresses relative to BASE_ADDRESS)
//---------------------------------------------------------------------------
localparam int REG_HALF_PER = 'h00;
localparam int REG_SI = 'h04;
localparam int REG_CONTROL = 'h3C;
localparam int REG_DATA = 'h40;
// Actual address space required for this block.
localparam int REG_ADDR_W = 7;
//---------------------------------------------------------------------------
// CONTROL register field widths
//---------------------------------------------------------------------------
localparam int SPI_MODE_W = 2;
localparam int SPI_NUM_BYTES_W = 12;
localparam int SPI_ADDR_W = 16;
//---------------------------------------------------------------------------
// SPI direction encoding
//---------------------------------------------------------------------------
typedef enum logic {
SPI_WRITE = 1'b0,
SPI_READ = 1'b1
} spi_adrv_dir_t;
//---------------------------------------------------------------------------
// SPI mode encoding
//---------------------------------------------------------------------------
typedef enum logic [SPI_MODE_W-1:0] {
MODE_RAW = 0, // Raw byte stream
MODE_SI_SEQ = 1, // Single Instruction, sequential addresses
MODE_SI_REP = 2, // Single Instruction, same address repeated
MODE_STREAMING = 3 // Send address once, followed by all data bytes back-to-back
} spi_adrv_mode_t;
//---------------------------------------------------------------------------
// CONTROL register
//---------------------------------------------------------------------------
typedef struct packed {
spi_adrv_dir_t dir; // [31]
logic rsvd1; // [30]
spi_adrv_mode_t mode; // [29:28]
logic [SPI_NUM_BYTES_W-1:0] num_bytes; // [27:16]
logic [ SPI_ADDR_W-1:0] addr; // [15:0]
} spi_adrv_ctrl_t;
endpackage : ctrlport_spi_adrv_pkg

View File

@ -0,0 +1,150 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_axis
//
// Description:
//
// Converts a CtrlPort write interface into an AXI-Stream bus. The module
// contains an internal FIFO and will not acknowledge the write until the
// FIFO has emptied enough to guarantee that another write can be accepted.
//
// Parameters:
//
// WIDTH : Width of the data path in bits.
// FIFO_SIZE_LOG2 : Log base 2 of the internal FIFO size.
//
// Signals:
//
// clk : Clock
// rst : Active-high synchronous reset
// ctrl_data : CtrlPort data word
// ctrl_wr : CtrlPort write request strobe (single-cycle pulse)
// ctrl_ack : CtrlPort acknowledgement strobe (single-cycle pulse)
// m_axis_tdata : AXI-Stream data output
// m_axis_tvalid : AXI-Stream valid
// m_axis_tready : AXI-Stream ready (from downstream)
//
`default_nettype none
module ctrlport_to_axis #(
parameter int WIDTH = 32,
parameter int FIFO_SIZE_LOG2 = 1
) (
input wire clk,
input wire rst,
// CtrlPort write interface
input wire [WIDTH-1:0] ctrl_data,
input wire ctrl_wr,
output logic ctrl_ack,
// AXI-Stream master
output wire [WIDTH-1:0] m_axis_tdata,
output wire m_axis_tvalid,
input wire m_axis_tready
);
//---------------------------------------------------------------------------
// Declarations
//---------------------------------------------------------------------------
typedef enum logic { ST_IDLE, ST_FULL } state_t;
state_t state;
logic fifo_i_tready;
logic [15:0] fifo_space;
//---------------------------------------------------------------------------
// State Machine
//---------------------------------------------------------------------------
always_ff @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
ctrl_ack <= 1'b0;
end else begin
// Default assignments
ctrl_ack <= 1'b0;
unique case (state)
ST_IDLE: begin
if (ctrl_wr) begin
if (fifo_space > 1 && FIFO_SIZE_LOG2 > 0) begin
// If there's 2 or more spaces in the FIFO, acknowledge
// immediately. If FIFO_SIZE_LOG2 is 0 (depth of 1), then the
// space can never be greater than 1.
ctrl_ack <= 1'b1;
end else begin
// If there's only one space, this write fills the FIFO, and we
// can't acknowledge until we have enough room to accept the next
// write.
state <= ST_FULL;
end
end
end
ST_FULL: begin
// Acknowledge the write as soon as space frees up in the FIFO.
if (fifo_space > 0) begin
ctrl_ack <= 1'b1;
state <= ST_IDLE;
end
end
default: begin
state <= ST_IDLE;
end
endcase
end
end
//---------------------------------------------------------------------------
// Output FIFO
//---------------------------------------------------------------------------
axi_fifo #(
.WIDTH (WIDTH),
.SIZE (FIFO_SIZE_LOG2)
) data_fifo_i (
.clk (clk),
.reset (rst),
.clear (1'b0),
.i_tdata (ctrl_data),
.i_tvalid (ctrl_wr),
.i_tready (fifo_i_tready),
.o_tdata (m_axis_tdata),
.o_tvalid (m_axis_tvalid),
.o_tready (m_axis_tready),
.space (fifo_space),
.occupied ()
);
//---------------------------------------------------------------------------
// Assertions
//---------------------------------------------------------------------------
if (FIFO_SIZE_LOG2 < 0) begin : gen_fifo_size_check
$error("FIFO_SIZE_LOG2 must be >= 0");
end
//synthesis translate_off
always_ff @(posedge clk) begin
if (ctrl_wr && !fifo_i_tready) begin
$error("ctrlport_to_axis: Write strobe occurred when the FIFO was not ready!");
end
end
//synthesis translate_on
endmodule : ctrlport_to_axis
`default_nettype wire

View File

@ -0,0 +1,371 @@
//
// Copyright 2025 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_simple_spi
//
// Description:
// Implements a simple SPI core controlled via CtrlPort. This design
// is an adaptation of the simple_spi_core module.
//
// Settings reg map:
//
// BASE_ADDRESS+0 divider setting
// bits [15:0] spi clock divider
//
// BASE_ADDRESS+4 configuration input
// bits [23:0] slave select, bit0 = slave0 enabled
// bits [29:24] num bits (1 through 32)
// bit [30] data input edge = in data bit latched on rising edge of clock
// bit [31] data output edge = out data bit latched on rising edge of clock
//
// BASE_ADDRESS+8 output data
// Writing this register begins a spi transaction.
// Bits are latched out starting from bit 31.
// Therefore, data has to be MSB aligned for transmission
//
// BASE_ADDRESS+12 Status
// bit [0] ready signal, high when spi core can begin another transaction
//
// BASE_ADDRESS+16 Readback
// Readback data from the last SPI transaction
//
module ctrlport_to_simple_spi # (
//settings register base address
int BASE_ADDRESS = 0,
//width of serial enables (up to 24 is possible)
int SEN_WIDTH = 8,
//idle state of the spi clock
bit CLK_IDLE = 0,
//idle state of the serial enables
logic [23:0] SEN_IDLE = '1
)
(
input wire ctrlport_clk,
input wire ctrlport_rst,
// Request
input wire s_ctrlport_req_wr,
input wire s_ctrlport_req_rd,
input wire [19:0] s_ctrlport_req_addr,
input wire [31:0] s_ctrlport_req_data,
// Response
output logic s_ctrlport_resp_ack = 1'b0,
output logic [ 1:0] s_ctrlport_resp_status = 2'b0,
output logic [31:0] s_ctrlport_resp_data = 32'b0,
//spi interface, slave selects, clock, data in, data out
output logic [SEN_WIDTH-1:0] sen,
output logic sclk,
output logic mosi,
input wire miso,
//optional debug output
output logic [31:0] debug
);
`include "../rfnoc/core/ctrlport.vh"
`include "ctrlport_to_simple_spi_regmap_utils.vh"
wire address_in_range =
(s_ctrlport_req_addr >= BASE_ADDRESS) &&
(s_ctrlport_req_addr < BASE_ADDRESS + 'h14); // 5 registers (20 bytes)
logic [15:0] sclk_divider;
logic [23:0] slave_select;
logic [5:0] num_bits;
logic datain_edge, dataout_edge;
logic [31:0] mosi_data;
logic trigger_spi;
logic [31:0] datain_reg;
logic [23:0] readback_reg;
logic readback_stb;
logic ready;
logic force_cs;
always_ff @(posedge ctrlport_clk) begin
if (ctrlport_rst) begin
s_ctrlport_resp_ack <= '0;
s_ctrlport_resp_data <= 'x;
s_ctrlport_resp_status <= '0;
trigger_spi <= 1'b0;
end else begin
// Reset response
s_ctrlport_resp_ack <= '0;
s_ctrlport_resp_data <= 'x;
s_ctrlport_resp_status <= '0;
trigger_spi <= 1'b0;
// Write request
if (s_ctrlport_req_wr) begin
s_ctrlport_resp_ack <= '1;
case (s_ctrlport_req_addr)
BASE_ADDRESS + SCLK_DIVIDER: begin
sclk_divider <= s_ctrlport_req_data[SPI_CLK_DIV_MSB : SPI_CLK_DIV];
force_cs <= s_ctrlport_req_data[FORCE_SS];
end
BASE_ADDRESS + SPI_PERSONALITY: begin
dataout_edge <= s_ctrlport_req_data[SPI_MOSI_EDGE];
datain_edge <= s_ctrlport_req_data[SPI_MISO_EDGE];
num_bits <= s_ctrlport_req_data[SPI_NUM_BITS_MSB : SPI_NUM_BITS];
slave_select <= s_ctrlport_req_data[SPI_SLAVE_SELECT_MSB : SPI_SLAVE_SELECT];
end
BASE_ADDRESS + SPI_MOSI_DATA: begin
mosi_data <= s_ctrlport_req_data[SPI_DATA_OUT_MSB : SPI_DATA_OUT];
trigger_spi <= 1'b1; // trigger the SPI transaction
end
// No register implementation for provided address
default: begin
// Acknowledge and provide error status if address is in range
if (address_in_range) begin
s_ctrlport_resp_status <= CTRL_STS_CMDERR;
// No response if out of range
end else begin
s_ctrlport_resp_ack <= 1'b0;
end
end
endcase
end else if (s_ctrlport_req_rd) begin
// read request handling
case (s_ctrlport_req_addr)
BASE_ADDRESS + SCLK_DIVIDER: begin
// Divider setting readback
s_ctrlport_resp_data[SPI_CLK_DIV_MSB : SPI_CLK_DIV] <= sclk_divider[SPI_CLK_DIV_SIZE-1 : 0];
s_ctrlport_resp_data[FORCE_SS] <= force_cs;
s_ctrlport_resp_ack <= 1'b1;
end
BASE_ADDRESS + SPI_PERSONALITY: begin
// Control register readback
s_ctrlport_resp_data[SPI_MOSI_EDGE] <= dataout_edge;
s_ctrlport_resp_data[SPI_MISO_EDGE] <= datain_edge;
s_ctrlport_resp_data[SPI_NUM_BITS_MSB : SPI_NUM_BITS] <= num_bits;
s_ctrlport_resp_data[SPI_SLAVE_SELECT_MSB : SPI_SLAVE_SELECT] <= slave_select;
s_ctrlport_resp_ack <= 1'b1;
end
BASE_ADDRESS + SPI_STATUS: begin
// Data register readback
s_ctrlport_resp_data[SPI_READY] <= ready;
s_ctrlport_resp_ack <= 1'b1;
end
BASE_ADDRESS + SPI_READBACK: begin
// Readback data from the last SPI transaction
s_ctrlport_resp_data[SPI_READBACK_DATA_MSB : SPI_READBACK_DATA] <= readback_reg;
s_ctrlport_resp_ack <= 1'b1;
end
// No register implementation for provided address
default: begin
// Acknowledge and provide error status if address is in range
if (address_in_range) begin
s_ctrlport_resp_status <= CTRL_STS_CMDERR;
// No response if out of range
end else begin
s_ctrlport_resp_ack <= 1'b0;
end
end
endcase
end
end
end
// Latch readback signal
always_ff @(posedge ctrlport_clk) begin
if (ctrlport_rst) begin
readback_reg <= '0;
end else if(readback_stb) begin
readback_reg <= datain_reg[23:0]; // readback data is latched from the last SPI transaction
end
end
// SPI Engine Implementation
// The implementation below is leveraged from the simple_spi_core module.
typedef enum logic [2:0] {
WAIT_TRIG,
PRE_IDLE,
CLK_REG,
CLK_INV,
POST_IDLE,
IDLE_SEN
} case_t;
case_t spi_state;
logic ready_reg;
// De-assert ready when a new transaction is triggered. ready_reg will be deasserted
// on the clock cycle after the trigger, and keep the ready signal low until the
// transaction is done.
assign ready = ready_reg && ~trigger_spi;
// Serial clock either idles or is in one of two clock states
logic sclk_reg;
assign sclk = sclk_reg;
// Serial enables either idle or enabled (based on spi_state or forced
// via FORCE_SS register field).
// One pipeline stage to break critical path from register in I/O pads.
wire sen_is_idle = ((spi_state == WAIT_TRIG) ||
(spi_state == IDLE_SEN)) &&
~force_cs;
wire [23:0] sen24 = (sen_is_idle)? SEN_IDLE : (SEN_IDLE ^ slave_select);
logic [SEN_WIDTH-1:0] sen_reg = SEN_IDLE;
always_ff @(posedge ctrlport_clk) begin
if (ctrlport_rst) begin
sen_reg <= SEN_IDLE;
end else begin
sen_reg <= sen24[SEN_WIDTH-1:0];
end
end
assign sen = sen_reg;
// Data output shift register
// One pipeline stage to break critical path from register in I/O pads.
logic [31:0] dataout_reg;
wire [31:0] dataout_next = {dataout_reg[30:0], 1'b0};
always_ff @(posedge ctrlport_clk) begin
mosi <= dataout_reg[31];
end
// Data input shift register
// Two pipeline stages to break critical path from register in I/O pads.
logic miso_pipe, miso_pipe2;
always_ff @(posedge ctrlport_clk) begin
miso_pipe2 <= miso;
miso_pipe <= miso_pipe2;
end
wire [31:0] datain_next = {datain_reg[30:0], miso_pipe};
// Counter for spi clock
logic [15:0] sclk_counter;
wire sclk_counter_done = (sclk_counter == sclk_divider);
wire [15:0] sclk_counter_next = (sclk_counter_done)? 0 : sclk_counter + 1;
// Counter for latching bits miso/mosi
logic [6:0] bit_counter;
wire [6:0] bit_counter_next = bit_counter + 1;
wire bit_counter_done = (bit_counter_next == num_bits);
always_ff @(posedge ctrlport_clk) begin
if (ctrlport_rst) begin
spi_state <= WAIT_TRIG;
sclk_reg <= CLK_IDLE;
ready_reg <= 0;
readback_stb <= 1'b0;
end
else begin
case (spi_state)
WAIT_TRIG: begin
if (trigger_spi) begin
spi_state <= PRE_IDLE;
end
readback_stb <= 1'b0;
ready_reg <= ~trigger_spi;
dataout_reg <= mosi_data;
sclk_counter <= 0;
bit_counter <= 0;
sclk_reg <= CLK_IDLE;
end
// Drive clock to idle state. This also drives the serial enables
// to their active state.
PRE_IDLE: begin
if (sclk_counter_done) begin
spi_state <= CLK_REG;
end
sclk_counter <= sclk_counter_next;
sclk_reg <= CLK_IDLE;
end
// Clock generation states
CLK_REG: begin
if (sclk_counter_done) begin
spi_state <= CLK_INV;
if (datain_edge != CLK_IDLE) begin
datain_reg <= datain_next;
end
if (dataout_edge != CLK_IDLE && bit_counter != 0) begin
dataout_reg <= dataout_next;
end
sclk_reg <= ~CLK_IDLE; //transition to rising when CLK_IDLE == 0
end
sclk_counter <= sclk_counter_next;
end
CLK_INV: begin
if (sclk_counter_done) begin
// Have all bits been shifted out?
spi_state <= (bit_counter_done)? POST_IDLE : CLK_REG;
bit_counter <= bit_counter_next;
if (datain_edge == CLK_IDLE) begin
datain_reg <= datain_next;
end
if (dataout_edge == CLK_IDLE && ~bit_counter_done) begin
dataout_reg <= dataout_next;
end
sclk_reg <= CLK_IDLE; //transition to falling when CLK_IDLE == 0
end
sclk_counter <= sclk_counter_next;
end
// Drive clock to idle state
POST_IDLE: begin
if (sclk_counter_done) begin
spi_state <= IDLE_SEN;
end
sclk_counter <= sclk_counter_next;
sclk_reg <= CLK_IDLE;
end
// Drive serial enables to idle state
IDLE_SEN: begin
if (sclk_counter_done) begin
ready_reg <= 1'b1;
readback_stb <= 1'b1;
spi_state <= WAIT_TRIG;
end
sclk_counter <= sclk_counter_next;
sclk_reg <= CLK_IDLE;
end
default: spi_state <= WAIT_TRIG;
endcase //spi_state
end
end
assign debug = {
8'h00, //8
trigger_spi, spi_state, //4
sclk, mosi, miso, ready, //4
1'b0, bit_counter[6:0], //8
sclk_counter_done, bit_counter_done, //2
sclk_counter[5:0] //6
};
endmodule : ctrlport_to_simple_spi

View File

@ -0,0 +1,86 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_simple_spi_regmap_utils.vh
//
// Description:
//
// The constants in this file are autogenerated by XmlParse.
//
//===============================================================================
// A numerically ordered list of registers and their HDL source files
//===============================================================================
// SCLK_DIVIDER : 0x0 (b310_core.sv)
// SPI_PERSONALITY : 0x4 (b310_core.sv)
// SPI_MOSI_DATA : 0x8 (b310_core.sv)
// SPI_STATUS : 0xC (b310_core.sv)
// SPI_READBACK : 0x10 (b310_core.sv)
//===============================================================================
// RegTypes
//===============================================================================
//===============================================================================
// Register Group CTRLPORT_TO_SIMPLE_SPI_REGMAP
//===============================================================================
// SCLK_DIVIDER Register (from b310_core.sv)
localparam SCLK_DIVIDER = 'h0; // Register Offset
localparam SCLK_DIVIDER_SIZE = 32; // register width in bits
localparam SCLK_DIVIDER_MASK = 32'h1FFFF;
localparam SPI_CLK_DIV_SIZE = 16; //SCLK_DIVIDER:SPI_CLK_DIV
localparam SPI_CLK_DIV_MSB = 15; //SCLK_DIVIDER:SPI_CLK_DIV
localparam SPI_CLK_DIV = 0; //SCLK_DIVIDER:SPI_CLK_DIV
localparam FORCE_SS_SIZE = 1; //SCLK_DIVIDER:FORCE_SS
localparam FORCE_SS_MSB = 16; //SCLK_DIVIDER:FORCE_SS
localparam FORCE_SS = 16; //SCLK_DIVIDER:FORCE_SS
// SPI_PERSONALITY Register (from b310_core.sv)
localparam SPI_PERSONALITY = 'h4; // Register Offset
localparam SPI_PERSONALITY_SIZE = 32; // register width in bits
localparam SPI_PERSONALITY_MASK = 32'hFFFFFFFF;
localparam SPI_SLAVE_SELECT_SIZE = 24; //SPI_PERSONALITY:SPI_SLAVE_SELECT
localparam SPI_SLAVE_SELECT_MSB = 23; //SPI_PERSONALITY:SPI_SLAVE_SELECT
localparam SPI_SLAVE_SELECT = 0; //SPI_PERSONALITY:SPI_SLAVE_SELECT
localparam SPI_NUM_BITS_SIZE = 6; //SPI_PERSONALITY:SPI_NUM_BITS
localparam SPI_NUM_BITS_MSB = 29; //SPI_PERSONALITY:SPI_NUM_BITS
localparam SPI_NUM_BITS = 24; //SPI_PERSONALITY:SPI_NUM_BITS
localparam SPI_MISO_EDGE_SIZE = 1; //SPI_PERSONALITY:SPI_MISO_EDGE
localparam SPI_MISO_EDGE_MSB = 30; //SPI_PERSONALITY:SPI_MISO_EDGE
localparam SPI_MISO_EDGE = 30; //SPI_PERSONALITY:SPI_MISO_EDGE
localparam SPI_MOSI_EDGE_SIZE = 1; //SPI_PERSONALITY:SPI_MOSI_EDGE
localparam SPI_MOSI_EDGE_MSB = 31; //SPI_PERSONALITY:SPI_MOSI_EDGE
localparam SPI_MOSI_EDGE = 31; //SPI_PERSONALITY:SPI_MOSI_EDGE
// SPI_MOSI_DATA Register (from b310_core.sv)
localparam SPI_MOSI_DATA = 'h8; // Register Offset
localparam SPI_MOSI_DATA_SIZE = 32; // register width in bits
localparam SPI_MOSI_DATA_MASK = 32'hFFFFFFFF;
localparam SPI_DATA_OUT_SIZE = 32; //SPI_MOSI_DATA:SPI_DATA_OUT
localparam SPI_DATA_OUT_MSB = 31; //SPI_MOSI_DATA:SPI_DATA_OUT
localparam SPI_DATA_OUT = 0; //SPI_MOSI_DATA:SPI_DATA_OUT
// SPI_STATUS Register (from b310_core.sv)
localparam SPI_STATUS = 'hC; // Register Offset
localparam SPI_STATUS_SIZE = 32; // register width in bits
localparam SPI_STATUS_MASK = 32'h1;
localparam SPI_READY_SIZE = 1; //SPI_STATUS:SPI_READY
localparam SPI_READY_MSB = 0; //SPI_STATUS:SPI_READY
localparam SPI_READY = 0; //SPI_STATUS:SPI_READY
// SPI_READBACK Register (from b310_core.sv)
localparam SPI_READBACK = 'h10; // Register Offset
localparam SPI_READBACK_SIZE = 32; // register width in bits
localparam SPI_READBACK_MASK = 32'hFFFFFFFF;
localparam SPI_READBACK_DATA_SIZE = 32; //SPI_READBACK:SPI_READBACK_DATA
localparam SPI_READBACK_DATA_MSB = 31; //SPI_READBACK:SPI_READBACK_DATA
localparam SPI_READBACK_DATA = 0; //SPI_READBACK:SPI_READBACK_DATA
// function SCLK_DIVIDERRec not implemented for Verilog
// function SPI_PERSONALITYRec not implemented for Verilog
// function SPI_MOSI_DATARec not implemented for Verilog
// function SPI_STATUSRec not implemented for Verilog
// function SPI_READBACKRec not implemented for Verilog

View File

@ -11,14 +11,16 @@
//
// Parameters:
//
// BASE_ADDRESS : Base address for CtrlPort registers.
// BASE_ADDRESS : Base address for CtrlPort registers.
// REG_STRIDE_SIZE : Address separation between registers.
//
`default_nettype wire
module ctrlport_to_wb_i2c #(
parameter BASE_ADDRESS = 0
parameter BASE_ADDRESS = 0,
parameter REG_STRIDE_SIZE = 1
) (
//---------------------------------------------------------------
// ControlPort Slave
@ -64,10 +66,11 @@ module ctrlport_to_wb_i2c #(
wire wb_ack_o;
wire [31:0] wb_dat_o;
// Check for address to be in range [base_addr..base_addr+8)
localparam NUM_ADDRESSES = 8;
// Check for address to be in range [base_addr..base_addr+8*stride)
localparam REG_WINDOW_SIZE = 8*REG_STRIDE_SIZE;
wire address_in_range = (s_ctrlport_req_addr >= BASE_ADDRESS) &&
(s_ctrlport_req_addr < BASE_ADDRESS + NUM_ADDRESSES);
(s_ctrlport_req_addr < BASE_ADDRESS + REG_WINDOW_SIZE);
// Following chapter 3.2.3 (classic standard SINGLE WRITE cycle) of
// https://cdn.opencores.org/downloads/wbspec_b4.pdf
@ -100,25 +103,25 @@ module ctrlport_to_wb_i2c #(
end else if (s_ctrlport_req_wr) begin
// Assume there is a valid address
//1-1 translation of wb to ctrl port interface
if (s_ctrlport_req_addr < 8) begin
if (address_in_range) begin
wb_cyc_i <= 1'b1;
wb_we_i <= 1'b1;
wb_dat_i <= s_ctrlport_req_data;
end
case (s_ctrlport_req_addr)
BASE_ADDRESS + WB_PRER_LO: begin
BASE_ADDRESS + WB_PRER_LO * REG_STRIDE_SIZE: begin
wb_adr_i = WB_PRER_LO;
end
BASE_ADDRESS + WB_PRER_HI: begin
BASE_ADDRESS + WB_PRER_HI * REG_STRIDE_SIZE: begin
wb_adr_i = WB_PRER_HI;
end
BASE_ADDRESS + WB_CTR: begin
BASE_ADDRESS + WB_CTR * REG_STRIDE_SIZE: begin
wb_adr_i = WB_CTR;
end
BASE_ADDRESS + WB_TXR: begin
BASE_ADDRESS + WB_TXR * REG_STRIDE_SIZE: begin
wb_adr_i = WB_TXR;
end
BASE_ADDRESS + WB_CR: begin
BASE_ADDRESS + WB_CR * REG_STRIDE_SIZE: begin
wb_adr_i = WB_CR;
end
@ -126,33 +129,36 @@ module ctrlport_to_wb_i2c #(
// Read requests
end else if (s_ctrlport_req_rd) begin
// Assume there is a valid address
wb_cyc_i <= 1'b1;
wb_we_i <= 1'b0;
case (s_ctrlport_req_addr)
BASE_ADDRESS + WB_PRER_LO: begin
wb_adr_i <= WB_PRER_LO;
end
BASE_ADDRESS + WB_PRER_HI: begin
wb_adr_i <= WB_PRER_HI;
end
BASE_ADDRESS + WB_CTR: begin
wb_adr_i <= WB_CTR;
end
BASE_ADDRESS + WB_RXR: begin
wb_adr_i = WB_RXR;
end
BASE_ADDRESS + WB_SR: begin
wb_adr_i = WB_SR;
end
// Respond with 0
default: begin
s_ctrlport_resp_ack <= 1'b1;
s_ctrlport_resp_data <= 'b0;
end
if(address_in_range) begin
// Assume there is a valid address
wb_cyc_i <= 1'b1;
wb_we_i <= 1'b0;
endcase
case (s_ctrlport_req_addr)
BASE_ADDRESS + WB_PRER_LO * REG_STRIDE_SIZE: begin
wb_adr_i <= WB_PRER_LO;
end
BASE_ADDRESS + WB_PRER_HI * REG_STRIDE_SIZE: begin
wb_adr_i <= WB_PRER_HI;
end
BASE_ADDRESS + WB_CTR * REG_STRIDE_SIZE: begin
wb_adr_i <= WB_CTR;
end
BASE_ADDRESS + WB_RXR * REG_STRIDE_SIZE: begin
wb_adr_i = WB_RXR;
end
BASE_ADDRESS + WB_SR * REG_STRIDE_SIZE: begin
wb_adr_i = WB_SR;
end
// Respond with 0
default: begin
s_ctrlport_resp_ack <= 1'b1;
s_ctrlport_resp_data <= 'b0;
end
endcase
end
end
end
end

View File

@ -0,0 +1,190 @@
//
// Copyright 2025 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_wb_uart
//
// Description:
//
// This module wraps a simple UART and provides a ControlPort interface.
//
// Parameters:
//
// BASE_ADDRESS : Base address for CtrlPort registers.
// REG_STRIDE_SIZE : Address separation between registers.
//
`default_nettype wire
module ctrlport_to_wb_uart #(
parameter BASE_ADDRESS = 0,
parameter REG_STRIDE_SIZE = 1,
parameter CLKDIV_DEFAULT = 0,
parameter RX_SIZE = 8
) (
//---------------------------------------------------------------
// ControlPort Slave
//---------------------------------------------------------------
input wire ctrlport_clk,
input wire ctrlport_rst,
input wire s_ctrlport_req_wr,
input wire s_ctrlport_req_rd,
input wire [19:0] s_ctrlport_req_addr,
input wire [31:0] s_ctrlport_req_data,
output reg s_ctrlport_resp_ack,
output reg [ 1:0] s_ctrlport_resp_status = 0,
output reg [31:0] s_ctrlport_resp_data = 0,
//---------------------------------------------------------------
// UART signals
//---------------------------------------------------------------
input wire uart_rx,
output wire uart_tx
);
`include "../rfnoc/core/ctrlport.vh"
//---------------------------------------------------------------
// Translating CtrlPort <-> Wishbone UART core
//---------------------------------------------------------------
reg wb_cyc_i; // Active bus cycle
reg wb_we_i = 1'b0; // Write access
reg [ 4:0] wb_adr_i = 3'b0;
reg [31:0] wb_dat_i = 32'b0;
wire wb_ack_o;
wire [31:0] wb_dat_o;
localparam SUART_CLKDIV = 0;
localparam SUART_TXLEVEL = 1;
localparam SUART_RXLEVEL = 2;
localparam SUART_TXCHAR = 3;
localparam SUART_RXCHAR = 4;
// Check for address to be in range [base_addr..base_addr+5*stride)
localparam REG_WINDOW_SIZE = 5*REG_STRIDE_SIZE;
wire address_in_range = (s_ctrlport_req_addr >= BASE_ADDRESS) &&
(s_ctrlport_req_addr < BASE_ADDRESS + REG_WINDOW_SIZE);
// Following chapter 3.2.3 (classic standard SINGLE WRITE cycle) of
// https://cdn.opencores.org/downloads/wbspec_b4.pdf
always @(posedge ctrlport_clk) begin
// Reset internal registers and responses
if (ctrlport_rst) begin
s_ctrlport_resp_ack <= 0;
s_ctrlport_resp_data <= 0;
s_ctrlport_resp_status <= CTRL_STS_OKAY;
wb_cyc_i <= 0;
wb_we_i <= 0;
wb_dat_i <= 0;
wb_adr_i <= 0;
end else begin
// Request independent default assignments
s_ctrlport_resp_ack <= 1'b0;
// Wait for ack on active bus transactions
if (wb_cyc_i) begin
if (wb_ack_o) begin
// End bus cycle and generate response
wb_cyc_i <= 1'b0;
s_ctrlport_resp_ack <= 1'b1;
s_ctrlport_resp_data <= wb_dat_o;
//removed error condition since there is no error signal in UART
//core
s_ctrlport_resp_status <= CTRL_STS_OKAY;
end
// Write requests
end else if (s_ctrlport_req_wr) begin
// Assume there is a valid address
//1-1 translation of wb to ctrl port interface
if (address_in_range) begin
wb_cyc_i <= 1'b1;
wb_we_i <= 1'b1;
wb_dat_i <= s_ctrlport_req_data;
end
case (s_ctrlport_req_addr)
BASE_ADDRESS + SUART_CLKDIV * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_CLKDIV;
end
BASE_ADDRESS + SUART_TXLEVEL * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_TXLEVEL;
end
BASE_ADDRESS + SUART_RXLEVEL * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_RXLEVEL;
end
BASE_ADDRESS + SUART_TXCHAR * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_TXCHAR;
end
BASE_ADDRESS + SUART_RXCHAR * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_RXCHAR;
end
endcase
// Read requests
end else if (s_ctrlport_req_rd) begin
if(address_in_range) begin
// Assume there is a valid address
wb_cyc_i <= 1'b1;
wb_we_i <= 1'b0;
case (s_ctrlport_req_addr)
BASE_ADDRESS + SUART_CLKDIV * REG_STRIDE_SIZE: begin
wb_adr_i <= SUART_CLKDIV;
end
BASE_ADDRESS + SUART_TXLEVEL * REG_STRIDE_SIZE: begin
wb_adr_i <= SUART_TXLEVEL;
end
BASE_ADDRESS + SUART_RXLEVEL * REG_STRIDE_SIZE: begin
wb_adr_i <= SUART_RXLEVEL;
end
BASE_ADDRESS + SUART_TXCHAR * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_TXCHAR;
end
BASE_ADDRESS + SUART_RXCHAR * REG_STRIDE_SIZE: begin
wb_adr_i = SUART_RXCHAR;
end
// Respond with 0
default: begin
s_ctrlport_resp_ack <= 1'b1;
s_ctrlport_resp_data <= 'b0;
end
endcase
end
end
end
end
//wishbone-based UART core
// Makin default baud
simple_uart #(
.CLKDIV_DEFAULT(CLKDIV_DEFAULT),
.RX_SIZE(RX_SIZE)
) simple_uart_i (
.clk_i(ctrlport_clk),
.rst_i(ctrlport_rst),
.adr_i(wb_adr_i),
.dat_i(wb_dat_i),
.dat_o(wb_dat_o),
.we_i(wb_we_i),
.stb_i(wb_cyc_i),
.cyc_i(wb_cyc_i),
.ack_o(wb_ack_o),
.rx_int_o(),
.tx_int_o(),
.tx_o(uart_tx),
.rx_i(uart_rx)
);
endmodule

View File

@ -0,0 +1,49 @@
#
# Copyright 2026 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
#-------------------------------------------------
# Top-of-Makefile
#-------------------------------------------------
# Define BASE_DIR to point to the "top" dir.
BASE_DIR = $(abspath ../../../../top)
# Include viv_sim_preamble after defining BASE_DIR
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
#-------------------------------------------------
# Design Specific
#-------------------------------------------------
# Pull in FIFO sources (axi_fifo and its dependencies)
include $(BASE_DIR)/../lib/fifo/Makefile.srcs
include $(BASE_DIR)/../lib/axi/Makefile.srcs
include $(BASE_DIR)/../lib/control/Makefile.srcs
include $(BASE_DIR)/../lib/rfnoc/core/Makefile.srcs
include Makefile.srcs
DESIGN_SRCS += $(abspath \
$(FIFO_SRCS) \
$(AXI_SRCS) \
$(CONTROL_LIB_SRCS) \
$(RFNOC_CORE_SRCS) \
$(CTRLPORT_SPI_ADRV_SRCS) \
)
#-------------------------------------------------
# Testbench Specific
#-------------------------------------------------
SIM_TOP = ctrlport_spi_adrv_all_tb
SIM_SRCS = $(abspath \
ctrlport_spi_adrv_tb.sv \
ctrlport_spi_adrv_all_tb.sv \
)
#-------------------------------------------------
# Bottom-of-Makefile
#-------------------------------------------------
# Include all simulator specific makefiles here
# Each should define a unique target to simulate
# e.g. xsim, vsim, etc and a common "clean" target
include $(BASE_DIR)/../tools/make/viv_simulator.mak

View File

@ -0,0 +1,11 @@
#
# Copyright 2026 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
CTRLPORT_SPI_ADRV_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/control/, \
axis_spi.sv \
ctrlport_spi_adrv_pkg.sv \
ctrlport_spi_adrv.sv \
))

View File

@ -0,0 +1,29 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_spi_adrv_all_tb
//
// Description:
//
// Top-level testbench for ctrlport_spi_adrv, exercising multiple DUT
// configurations. Each instance runs sequentially via the semaphore in
// PkgTestExec::test.
//
`default_nettype none
module ctrlport_spi_adrv_all_tb;
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W( 4), .HALF_PER( 0), .HALF_PER_EN(0), .CS_HOLD(0), .CS_GUARD(1)) tb_hold_guard_01 ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W( 4), .HALF_PER( 0), .HALF_PER_EN(0), .CS_HOLD(1), .CS_GUARD(1)) tb_hold_guard_11 ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W( 4), .HALF_PER( 0), .HALF_PER_EN(0), .CS_HOLD(0), .CS_GUARD(2)) tb_hold_guard_02 ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W( 4), .HALF_PER( 0), .HALF_PER_EN(0), .CS_HOLD(1), .CS_GUARD(2)) tb_hold_guard_12 ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W( 4), .HALF_PER(255), .HALF_PER_EN(0), .CS_HOLD(1), .CS_GUARD(2)) tb_slow_clk ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(1), .NUM_BYTES_W(12), .HALF_PER( 0), .HALF_PER_EN(1), .CS_HOLD(1), .CS_GUARD(2)) tb_max_len ();
ctrlport_spi_adrv_tb #(.QUICK_TEST(0), .NUM_BYTES_W( 8), .HALF_PER( 4), .HALF_PER_EN(1), .CS_HOLD(1), .CS_GUARD(2)) tb_full_test ();
endmodule : ctrlport_spi_adrv_all_tb
`default_nettype wire

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
#
# Copyright 2026 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
#-------------------------------------------------
# Top-of-Makefile
#-------------------------------------------------
BASE_DIR = $(abspath ../../../../top)
include $(BASE_DIR)/../tools/make/viv_sim_preamble.mak
#-------------------------------------------------
# Design Specific
#-------------------------------------------------
include $(BASE_DIR)/../lib/rfnoc/core/Makefile.srcs
include Makefile.srcs
DESIGN_SRCS += $(abspath \
$(RFNOC_CORE_SRCS) \
$(CTRLPORT_TO_IOPORT2_STREAM_SRCS) \
)
#-------------------------------------------------
# Testbench Specific
#-------------------------------------------------
SIM_TOP = ctrlport_to_ioport2_stream_all_tb
SIM_SRCS = $(abspath \
ctrlport_to_ioport2_stream_tb.sv \
ctrlport_to_ioport2_stream_all_tb.sv \
)
#-------------------------------------------------
# Bottom-of-Makefile
#-------------------------------------------------
include $(BASE_DIR)/../tools/make/viv_simulator.mak

View File

@ -0,0 +1,12 @@
#
# Copyright 2026 Ettus Research, a National Instruments Brand
#
# SPDX-License-Identifier: LGPL-3.0-or-later
#
CTRLPORT_TO_IOPORT2_STREAM_SRCS = $(abspath $(addprefix $(BASE_DIR)/../lib/io_port2/, \
ioport2_msg_pkg.sv \
ioport2_msg_codec.v \
pcie_basic_regs.v \
ctrlport_to_ioport2_stream.sv \
))

View File

@ -0,0 +1,30 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_ioport2_stream_all_tb
//
// Description:
//
// Top-level testbench for ctrlport_to_ioport2_stream. Runs two parameter
// variants.
//
`default_nettype none
module ctrlport_to_ioport2_stream_all_tb;
ctrlport_to_ioport2_stream_tb #(
.SIGNATURE(32'h1357_2468),
.CLK_FREQ (32'd125_000_000)
) tb_0 ();
ctrlport_to_ioport2_stream_tb #(
.SIGNATURE(32'h89AB_CDEF),
.CLK_FREQ (32'd200_000_000)
) tb_1 ();
endmodule : ctrlport_to_ioport2_stream_all_tb
`default_nettype wire

View File

@ -0,0 +1,320 @@
//
// Copyright 2026 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Module: ctrlport_to_ioport2_stream_tb
//
// Description:
//
// Testbench for ctrlport_to_ioport2_stream.
//
// The test drives CtrlPort requests through ctrlport_bfm and checks:
// 1) End-to-end register behavior against pcie_basic_regs.
// 2) ioport2 stream-request encoding produced by the bridge.
//
`default_nettype none
module ctrlport_to_ioport2_stream_tb #(
parameter logic [31:0] SIGNATURE = 32'h1234_ABCD,
parameter logic [31:0] CLK_FREQ = 32'd200_000_000
);
`include "test_exec.svh"
import PkgTestExec::*;
import ctrlport_pkg::*;
import ctrlport_bfm_pkg::*;
import ioport2_msg_pkg::*;
localparam real CLK_PERIOD_NS = 10.0;
localparam int REG_ADDR_WIDTH = 20;
localparam int REG_DATA_WIDTH = 32;
// Register offsets extracted from pcie_basic_regs.
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_SIG_REG_ADDR = 20'h00000;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_COUNTER_LO_REG_ADDR = 20'h00004;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_COUNTER_HI_REG_ADDR = 20'h00008;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_COUNTER_FREQ_ADDR = 20'h0000C;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_SCRATCH_0_ADDR = 20'h00010;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_SCRATCH_1_ADDR = 20'h00014;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_MISC_STATUS_ADDR = 20'h00020;
localparam logic [REG_ADDR_WIDTH-1:0] PCIE_FPGA_USR_SIG_REG_ADDR = 20'h00030;
//---------------------------------------------------------------------------
// Clocks and reset
//---------------------------------------------------------------------------
bit clk;
bit rst;
sim_clock_gen #(.PERIOD(CLK_PERIOD_NS), .AUTOSTART(0))
clk_gen (.clk(clk), .rst(rst));
//---------------------------------------------------------------------------
// CtrlPort interface + BFM
//---------------------------------------------------------------------------
ctrlport_if cp_if (.clk(clk), .rst(rst));
ctrlport_bfm cp_bfm = new(cp_if);
//---------------------------------------------------------------------------
// Bridge <-> endpoint stream wiring
//---------------------------------------------------------------------------
logic [63:0] regi_tdata;
logic regi_tvalid;
logic regi_tready;
logic [63:0] rego_tdata;
logic rego_tvalid;
logic rego_tready;
logic [REG_DATA_WIDTH-1:0] misc_status = 32'hD00D_CAFE;
//---------------------------------------------------------------------------
// DUT + endpoint
//---------------------------------------------------------------------------
ctrlport_to_ioport2_stream dut (
.s_ctrlport (cp_if),
.regi_tdata (regi_tdata),
.regi_tvalid (regi_tvalid),
.regi_tready (regi_tready),
.rego_tdata (rego_tdata),
.rego_tvalid (rego_tvalid),
.rego_tready (rego_tready)
);
// Arbitrarily use pcie_basic_regs as a simple endpoint to check the
// correctness of the stream requests generated by the bridge.
pcie_basic_regs #(
.SIGNATURE(SIGNATURE),
.CLK_FREQ (CLK_FREQ)
) pcie_basic_regs_i (
.clk (clk),
.reset (rst),
.regi_tdata (regi_tdata),
.regi_tvalid (regi_tvalid),
.regi_tready (regi_tready),
.rego_tdata (rego_tdata),
.rego_tvalid (rego_tvalid),
.rego_tready (rego_tready),
.misc_status (misc_status)
);
//---------------------------------------------------------------------------
// Stream monitor for request-encoding checks
//---------------------------------------------------------------------------
typedef ioport2_msg_t req_msg_t;
mailbox #(req_msg_t) req_msg_mb = new();
// Populate req_msg_mb with decoded stream requests for checking
// in test tasks.
always @(posedge clk) begin
req_msg_t msg;
if (!rst && regi_tvalid && regi_tready) begin
msg = req_msg_t'(regi_tdata);
req_msg_mb.put(msg);
end
end
//---------------------------------------------------------------------------
// Helper tasks
//---------------------------------------------------------------------------
// Checks that a stream request message matches the expected encoding for a
// given CtrlPort request.
task automatic check_req_msg(
input bit is_write,
input logic [REG_ADDR_WIDTH-1:0] reg_addr,
input logic [REG_DATA_WIDTH-1:0] req_data
);
req_msg_t msg;
req_msg_mb.get(msg);
`ASSERT_ERROR(msg.rd_response == 1'b0,
$sformatf("Unexpected rd_response bit in request message: %0b", msg.rd_response));
`ASSERT_ERROR(msg.wr_request == is_write,
$sformatf("wr_request mismatch. got=%0b exp=%0b", msg.wr_request, is_write));
`ASSERT_ERROR(msg.rd_request == !is_write,
$sformatf("rd_request mismatch. got=%0b exp=%0b", msg.rd_request, !is_write));
`ASSERT_ERROR(msg.half_word == 1'b0,
$sformatf("half_word should be 0 for 32-bit ctrlport ops, got=%0b", msg.half_word));
`ASSERT_ERROR(msg.address == reg_addr,
$sformatf("address mismatch. got=0x%05h exp=0x%05h", msg.address, reg_addr));
`ASSERT_ERROR(msg.data == req_data,
$sformatf("data mismatch. got=0x%08h exp=0x%08h", msg.data, req_data));
endtask
task automatic cp_write_and_check(
input logic [REG_ADDR_WIDTH-1:0] reg_addr,
input logic [REG_DATA_WIDTH-1:0] wr_data
);
cp_bfm.write(reg_addr, wr_data, STS_OKAY);
check_req_msg(1'b1, reg_addr, wr_data);
endtask
task automatic cp_read_and_check(
input logic [REG_ADDR_WIDTH-1:0] reg_addr,
output logic [REG_DATA_WIDTH-1:0] rd_data
);
cp_bfm.read(reg_addr, rd_data, STS_OKAY);
// ctrlport_bfm drives request.data as zero for reads.
check_req_msg(1'b0, reg_addr, {REG_DATA_WIDTH{1'b0}});
endtask
//---------------------------------------------------------------------------
// Tests - Validate register endpoint read/write capabilities
//---------------------------------------------------------------------------
task automatic test_id_and_status_regs();
logic [REG_DATA_WIDTH-1:0] rd_data;
test.start_test("ID and status registers");
cp_read_and_check(PCIE_FPGA_SIG_REG_ADDR, rd_data);
`ASSERT_ERROR(rd_data == SIGNATURE,
$sformatf("SIGNATURE mismatch. got=0x%08h exp=0x%08h", rd_data, SIGNATURE));
cp_read_and_check(PCIE_FPGA_COUNTER_FREQ_ADDR, rd_data);
`ASSERT_ERROR(rd_data == CLK_FREQ,
$sformatf("CLK_FREQ mismatch. got=0x%08h exp=0x%08h", rd_data, CLK_FREQ));
cp_read_and_check(PCIE_FPGA_MISC_STATUS_ADDR, rd_data);
`ASSERT_ERROR(rd_data == misc_status,
$sformatf("misc_status mismatch. got=0x%08h exp=0x%08h", rd_data, misc_status));
test.end_test();
endtask
task automatic test_scratch_regs();
logic [REG_DATA_WIDTH-1:0] rd_data;
test.start_test("Scratch register read/write");
cp_read_and_check(PCIE_FPGA_SCRATCH_0_ADDR, rd_data);
`ASSERT_ERROR(rd_data == {REG_DATA_WIDTH{1'b0}},
$sformatf("scratch0 reset value mismatch. got=0x%08h", rd_data));
cp_read_and_check(PCIE_FPGA_SCRATCH_1_ADDR, rd_data);
`ASSERT_ERROR(rd_data == {REG_DATA_WIDTH{1'b0}},
$sformatf("scratch1 reset value mismatch. got=0x%08h", rd_data));
cp_write_and_check(PCIE_FPGA_SCRATCH_0_ADDR, 32'hDEAD_BEEF);
cp_write_and_check(PCIE_FPGA_SCRATCH_1_ADDR, 32'hFACE_CAFE);
cp_read_and_check(PCIE_FPGA_SCRATCH_0_ADDR, rd_data);
`ASSERT_ERROR(rd_data == 32'hDEAD_BEEF,
$sformatf("scratch0 value mismatch. got=0x%08h exp=0xDEADBEEF", rd_data));
cp_read_and_check(PCIE_FPGA_SCRATCH_1_ADDR, rd_data);
`ASSERT_ERROR(rd_data == 32'hFACE_CAFE,
$sformatf("scratch1 value mismatch. got=0x%08h exp=0xFACECAFE", rd_data));
test.end_test();
endtask
task automatic test_usr_signature_regs();
logic [REG_DATA_WIDTH-1:0] rd_data;
test.start_test("User signature register read/write");
cp_write_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00000, 32'h0123_4567);
cp_write_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00004, 32'h89AB_CDEF);
cp_write_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00008, 32'h0BAD_F00D);
cp_write_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h0000C, 32'h55AA_33CC);
cp_read_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00000, rd_data);
`ASSERT_ERROR(rd_data == 32'h0123_4567,
$sformatf("usr_signature[31:0] mismatch. got=0x%08h", rd_data));
cp_read_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00004, rd_data);
`ASSERT_ERROR(rd_data == 32'h89AB_CDEF,
$sformatf("usr_signature[63:32] mismatch. got=0x%08h", rd_data));
cp_read_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h00008, rd_data);
`ASSERT_ERROR(rd_data == 32'h0BAD_F00D,
$sformatf("usr_signature[95:64] mismatch. got=0x%08h", rd_data));
cp_read_and_check(PCIE_FPGA_USR_SIG_REG_ADDR + 20'h0000C, rd_data);
`ASSERT_ERROR(rd_data == 32'h55AA_33CC,
$sformatf("usr_signature[127:96] mismatch. got=0x%08h", rd_data));
test.end_test();
endtask
task automatic test_counter_and_default();
logic [REG_DATA_WIDTH-1:0] ctr_lo_0;
logic [REG_DATA_WIDTH-1:0] ctr_lo_1;
logic [REG_DATA_WIDTH-1:0] rd_data;
test.start_test("Counter activity and default readback");
cp_read_and_check(PCIE_FPGA_COUNTER_LO_REG_ADDR, ctr_lo_0);
cp_read_and_check(PCIE_FPGA_COUNTER_HI_REG_ADDR, rd_data);
cp_read_and_check(PCIE_FPGA_COUNTER_LO_REG_ADDR, ctr_lo_1);
`ASSERT_ERROR(ctr_lo_1 != ctr_lo_0,
$sformatf("counter low did not change. first=0x%08h second=0x%08h", ctr_lo_0, ctr_lo_1));
cp_read_and_check(20'h001FC, rd_data);
`ASSERT_ERROR(rd_data == {REG_DATA_WIDTH{1'b1}},
$sformatf("default readback mismatch. got=0x%08h exp=0x%08h", rd_data,
{REG_DATA_WIDTH{1'b1}}));
test.end_test();
endtask
//---------------------------------------------------------------------------
// Main test
//---------------------------------------------------------------------------
initial begin : main
string tb_name;
tb_name = $sformatf({"ctrlport_to_ioport2_stream_tb\n",
"SIGNATURE=0x%08h\n",
"CLK_FREQ=%0d"},
SIGNATURE, CLK_FREQ);
test.start_tb(tb_name, 20ms);
// Avoid extra simulation activity before this testbench acquires test_exec.
clk_gen.start();
cp_bfm.run();
// Reset for 8 cycles, then wait for release.
clk_gen.reset();
@(negedge rst);
test_id_and_status_regs();
test_scratch_regs();
test_usr_signature_regs();
test_counter_and_default();
test.end_tb(0);
clk_gen.kill();
end
endmodule : ctrlport_to_ioport2_stream_tb
`default_nettype wire