diff --git a/README.md b/README.md index 348a15c..f2b9c86 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ The Taxi transport library contains many smaller components that can be composed * PCIe AXI lite master for Xilinx UltraScale * MSI shim for Xilinx UltraScale * MSI-X with AXI lite control interface + * MSI-X with APB control interface * Primitives * Arbiter * Priority encoder diff --git a/src/pcie/rtl/taxi_pcie_msix_apb.sv b/src/pcie/rtl/taxi_pcie_msix_apb.sv new file mode 100644 index 0000000..157597a --- /dev/null +++ b/src/pcie/rtl/taxi_pcie_msix_apb.sv @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2022-2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * PCIe MSI-X module with APB interface + */ +module taxi_pcie_msix_apb # +( + // TLP interface configuration + parameter logic TLP_FORCE_64_BIT_ADDR = 1'b0 +) +( + input wire logic clk, + input wire logic rst, + + /* + * APB interface for MSI-X tables + */ + taxi_apb_if.slv s_apb, + + /* + * Interrupt request input + */ + taxi_axis_if.snk s_axis_irq, + + /* + * Memory write TLP output + */ + taxi_pcie_tlp_if.src tx_wr_req_tlp, + + /* + * Configuration + */ + input wire logic [7:0] bus_num, + input wire logic [7:0] func_num, + input wire logic msix_enable, + input wire logic msix_mask +); + +// extract parameters +localparam TLP_SEGS = tx_wr_req_tlp.SEGS; +localparam TLP_SEG_DATA_W = tx_wr_req_tlp.SEG_DATA_W; +localparam TLP_SEG_EMPTY_W = tx_wr_req_tlp.SEG_EMPTY_W; +localparam TLP_DATA_W = TLP_SEGS*TLP_SEG_DATA_W; +localparam TLP_HDR_W = tx_wr_req_tlp.HDR_W; +localparam FUNC_NUM_W = tx_wr_req_tlp.FUNC_NUM_W; + +localparam APB_DATA_W = s_apb.DATA_W; +localparam APB_ADDR_W = s_apb.ADDR_W; +localparam APB_STRB_W = s_apb.STRB_W; + +localparam IRQ_INDEX_W = s_axis_irq.DATA_W; + +localparam TLP_DATA_W_B = TLP_DATA_W/8; +localparam TLP_DATA_W_DW = TLP_DATA_W/32; + +localparam TBL_ADDR_W = IRQ_INDEX_W+1; +localparam PBA_ADDR_W = IRQ_INDEX_W > 6 ? IRQ_INDEX_W-6 : 0; +localparam PBA_ADDR_W_INT = PBA_ADDR_W > 0 ? PBA_ADDR_W : 1; + +localparam INDEX_SHIFT = $clog2(64/8); +localparam WORD_SELECT_SHIFT = $clog2(APB_DATA_W/8); +localparam WORD_SELECT_W = 64 > APB_DATA_W ? $clog2((64+7)/8) - $clog2(APB_DATA_W/8) : 0; + +// bus width assertions +if (APB_STRB_W * 8 != APB_DATA_W) + $fatal(0, "Error: APB interface requires byte (8-bit) granularity (instance %m)"); + +if (APB_DATA_W > 64) + $fatal(0, "Error: APB data width must be 64 or less (instance %m)"); + +if (APB_ADDR_W < IRQ_INDEX_W+5) + $fatal(0, "Error: APB address width too narrow (instance %m)"); + +if (IRQ_INDEX_W > 11) + $fatal(0, "Error: IRQ index width must be 11 or less (instance %m)"); + +localparam [2:0] + TLP_FMT_3DW = 3'b000, + TLP_FMT_4DW = 3'b001, + TLP_FMT_3DW_DATA = 3'b010, + TLP_FMT_4DW_DATA = 3'b011, + TLP_FMT_PREFIX = 3'b100; + +localparam [1:0] + STATE_IDLE = 2'd0, + STATE_READ_TBL_1 = 2'd1, + STATE_READ_TBL_2 = 2'd2, + STATE_SEND_TLP = 2'd3; + +logic [1:0] state_reg = STATE_IDLE, state_next; + +logic [IRQ_INDEX_W-1:0] irq_index_reg = '0, irq_index_next; + +logic [63:0] vec_addr_reg = '0, vec_addr_next; +logic [31:0] vec_data_reg = '0, vec_data_next; +logic vec_mask_reg = 1'b0, vec_mask_next; + +logic [127:0] tlp_hdr; + +logic tbl_apb_mem_rd_en; +logic tbl_apb_mem_wr_en; +logic [7:0] tbl_apb_mem_wr_be; +logic [63:0] tbl_apb_mem_wr_data; +logic pba_apb_mem_rd_en; + +logic tbl_mem_rd_en; +logic [TBL_ADDR_W-1:0] tbl_mem_addr; +logic pba_mem_rd_en; +logic pba_mem_wr_en; +logic [PBA_ADDR_W-1:0] pba_mem_addr; +logic [63:0] pba_mem_wr_data; + +logic s_apb_pready_reg = 1'b0, s_apb_pready_next; +logic [APB_DATA_W-1:0] s_apb_prdata_reg = '0, s_apb_prdata_next; + +logic irq_ready_reg = 1'b0, irq_ready_next; + +logic [31:0] tx_wr_req_tlp_data_reg = '0, tx_wr_req_tlp_data_next; +logic [TLP_HDR_W-1:0] tx_wr_req_tlp_hdr_reg = '0, tx_wr_req_tlp_hdr_next; +logic tx_wr_req_tlp_valid_reg = 0, tx_wr_req_tlp_valid_next; + +logic msix_enable_reg = 1'b0; +logic msix_mask_reg = 1'b0; + +// MSI-X table +(* ramstyle = "no_rw_check, mlab" *) +logic [63:0] tbl_mem[2**TBL_ADDR_W]; + +// MSI-X PBA +(* ram_style = "distributed", ramstyle = "no_rw_check, mlab" *) +logic [63:0] pba_mem[2**PBA_ADDR_W]; + +logic tbl_rd_data_valid_reg = 1'b0, tbl_rd_data_valid_next; +logic pba_rd_data_valid_reg = 1'b0, pba_rd_data_valid_next; +logic [WORD_SELECT_W-1:0] rd_data_shift_reg = '0, rd_data_shift_next; + +logic [63:0] tbl_mem_rd_data_reg = '0; +logic [63:0] pba_mem_rd_data_reg = '0; +logic [63:0] tbl_apb_mem_rd_data_reg = '0; +logic [63:0] pba_apb_mem_rd_data_reg = '0; + +wire [TBL_ADDR_W-1:0] s_apb_paddr_index = s_apb.paddr[INDEX_SHIFT +: TBL_ADDR_W]; +wire [WORD_SELECT_W-1:0] s_apb_paddr_word = APB_DATA_W < 64 ? s_apb.paddr[WORD_SELECT_SHIFT +: WORD_SELECT_W] : 0; + +assign s_apb.pready = s_apb_pready_reg; +assign s_apb.prdata = s_apb_prdata_reg; +assign s_apb.pslverr = 1'b0; +assign s_apb.pruser = '0; +assign s_apb.pbuser = '0; + +assign s_axis_irq.tready = irq_ready_reg; + +assign tx_wr_req_tlp.data = TLP_DATA_W'(tx_wr_req_tlp_data_reg); +assign tx_wr_req_tlp.empty = '1; +assign tx_wr_req_tlp.hdr = tx_wr_req_tlp_hdr_reg; +assign tx_wr_req_tlp.seq = '0; +assign tx_wr_req_tlp.bar_id = '0; +assign tx_wr_req_tlp.func_num = '0; +assign tx_wr_req_tlp.error = '0; +assign tx_wr_req_tlp.valid = tx_wr_req_tlp_valid_reg; +assign tx_wr_req_tlp.sop = 1'b1; +assign tx_wr_req_tlp.eop = 1'b1; + +initial begin + for (integer i = 0; i < 2**TBL_ADDR_W; i = i + 1) begin + tbl_mem[i] = '0; + end + for (integer i = 0; i < 2**PBA_ADDR_W; i = i + 1) begin + pba_mem[i] = '0; + end +end + +always_comb begin + state_next = STATE_IDLE; + + tbl_mem_rd_en = 1'b0; + tbl_mem_addr = {irq_index_reg, 1'b0}; + + pba_mem_rd_en = 1'b0; + pba_mem_wr_en = 1'b0; + pba_mem_addr = PBA_ADDR_W_INT'(irq_index_reg >> 6); + pba_mem_wr_data = '0; + + irq_index_next = irq_index_reg; + + vec_addr_next = vec_addr_reg; + vec_data_next = vec_data_reg; + vec_mask_next = vec_mask_reg; + + irq_ready_next = 1'b0; + + tx_wr_req_tlp_data_next = tx_wr_req_tlp_data_reg; + tx_wr_req_tlp_hdr_next = tx_wr_req_tlp_hdr_reg; + tx_wr_req_tlp_valid_next = tx_wr_req_tlp_valid_reg && !tx_wr_req_tlp.ready; + + // TLP header + // DW 0 + if (((vec_addr_reg[63:2] >> 30) != 0) || TLP_FORCE_64_BIT_ADDR) begin + tlp_hdr[127:125] = TLP_FMT_4DW_DATA; // fmt - 4DW with data + end else begin + tlp_hdr[127:125] = TLP_FMT_3DW_DATA; // fmt - 3DW with data + end + tlp_hdr[124:120] = 5'b00000; // type - write + tlp_hdr[119] = 1'b0; // T9 + tlp_hdr[118:116] = 3'b000; // TC + tlp_hdr[115] = 1'b0; // T8 + tlp_hdr[114] = 1'b0; // attr + tlp_hdr[113] = 1'b0; // LN + tlp_hdr[112] = 1'b0; // TH + tlp_hdr[111] = 1'b0; // TD + tlp_hdr[110] = 1'b0; // EP + tlp_hdr[109:108] = 2'b00; // attr + tlp_hdr[107:106] = 2'b00; // AT + tlp_hdr[105:96] = 10'd1; // length + // DW 1 + tlp_hdr[95:88] = bus_num; // requester ID (bus number) + tlp_hdr[87:80] = func_num; // requester ID (device/function number) + tlp_hdr[79:72] = 8'd0; // tag + tlp_hdr[71:68] = 4'b0000; // last BE + tlp_hdr[67:64] = 4'b1111; // first BE + if ((vec_addr_reg[63:32] != 0) || TLP_FORCE_64_BIT_ADDR) begin + // DW 2+3 + tlp_hdr[63:2] = vec_addr_reg[63:2]; // address + tlp_hdr[1:0] = 2'b00; // PH + end else begin + // DW 2 + tlp_hdr[63:34] = vec_addr_reg[31:2]; // address + tlp_hdr[33:32] = 2'b00; // PH + // DW 3 + tlp_hdr[31:0] = 32'd0; + end + + case (state_reg) + STATE_IDLE: begin + irq_ready_next = 1'b1; + + if (s_axis_irq.tvalid && s_axis_irq.tready) begin + // new request + irq_ready_next = 1'b0; + irq_index_next = s_axis_irq.tdata; + + tbl_mem_rd_en = 1'b1; + tbl_mem_addr = {irq_index_next, 1'b0}; + + pba_mem_rd_en = 1'b1; + pba_mem_addr = PBA_ADDR_W_INT'(irq_index_next >> 6); + + state_next = STATE_READ_TBL_1; + end else if (!s_axis_irq.tvalid && msix_enable_reg && !msix_mask_reg) begin + // no new request waiting, scan PBA for masked requests + + if (pba_mem_rd_data_reg[6'(irq_index_reg)] != 0) begin + // PBA bit for current index is set, try issuing it + irq_ready_next = 1'b0; + + tbl_mem_rd_en = 1'b1; + tbl_mem_addr = {irq_index_next, 1'b0}; + + pba_mem_rd_en = 1'b1; + pba_mem_addr = PBA_ADDR_W_INT'(irq_index_next >> 6); + + state_next = STATE_READ_TBL_1; + end else begin + // PBA bit for current index is not set + if (pba_mem_rd_data_reg != 0) begin + // at least one bit set in current group, move to next index + irq_index_next = irq_index_reg + 1; + end else begin + // no bits set in current group, move to next group + irq_index_next = (irq_index_reg & ({IRQ_INDEX_W{1'b1}} << 6)) + 'd64; + end + + pba_mem_rd_en = 1'b1; + pba_mem_addr = PBA_ADDR_W_INT'(irq_index_next >> 6); + + state_next = STATE_IDLE; + end + end else begin + state_next = STATE_IDLE; + end + end + STATE_READ_TBL_1: begin + // handle first table read + tbl_mem_rd_en = 1'b1; + tbl_mem_addr = {irq_index_reg, 1'b1}; + + vec_addr_next = {tbl_mem_rd_data_reg[63:2], 2'b00}; + + state_next = STATE_READ_TBL_2; + end + STATE_READ_TBL_2: begin + // handle second table read + vec_data_next = tbl_mem_rd_data_reg[31:0]; + vec_mask_next = tbl_mem_rd_data_reg[32]; + + if (msix_enable_reg && !msix_mask_reg && !vec_mask_next) begin + // send TLP + state_next = STATE_SEND_TLP; + end else begin + // set PBA bit + pba_mem_wr_en = 1'b1; + pba_mem_wr_data = pba_mem_rd_data_reg | (1 << 6'(irq_index_reg)); + irq_ready_next = 1'b1; + state_next = STATE_IDLE; + end + end + STATE_SEND_TLP: begin + if (!tx_wr_req_tlp.valid || tx_wr_req_tlp.ready) begin + // send TLP + tx_wr_req_tlp_data_next = vec_data_reg; + tx_wr_req_tlp_hdr_next = tlp_hdr; + tx_wr_req_tlp_valid_next = 1'b1; + + // clear PBA bit + pba_mem_wr_en = 1'b1; + pba_mem_wr_data = pba_mem_rd_data_reg & ~(1 << 6'(irq_index_reg)); + + // increment index so we don't check the same PBA bit immediately + irq_index_next = irq_index_reg + 1; + + irq_ready_next = 1'b1; + state_next = STATE_IDLE; + end else begin + state_next = STATE_SEND_TLP; + end + end + endcase +end + +always_ff @(posedge clk) begin + state_reg <= state_next; + + irq_index_reg <= irq_index_next; + + vec_addr_reg <= vec_addr_next; + vec_data_reg <= vec_data_next; + vec_mask_reg <= vec_mask_next; + + irq_ready_reg <= irq_ready_next; + + tx_wr_req_tlp_data_reg <= tx_wr_req_tlp_data_next; + tx_wr_req_tlp_hdr_reg <= tx_wr_req_tlp_hdr_next; + tx_wr_req_tlp_valid_reg <= tx_wr_req_tlp_valid_next; + + msix_enable_reg <= msix_enable; + msix_mask_reg <= msix_mask; + + if (tbl_mem_rd_en) begin + tbl_mem_rd_data_reg <= tbl_mem[tbl_mem_addr]; + end + + if (pba_mem_wr_en) begin + pba_mem[pba_mem_addr] <= pba_mem_wr_data; + end else if (pba_mem_rd_en) begin + pba_mem_rd_data_reg <= pba_mem[pba_mem_addr]; + end + + if (rst) begin + state_reg <= STATE_IDLE; + + irq_ready_reg <= 1'b0; + + tx_wr_req_tlp_valid_reg <= 1'b0; + end +end + +// APB interface +always_comb begin + tbl_apb_mem_rd_en = 1'b0; + tbl_apb_mem_wr_en = 1'b0; + tbl_apb_mem_wr_be = 8'(s_apb.pstrb << (s_apb_paddr_word * APB_STRB_W)); + tbl_apb_mem_wr_data = {2**WORD_SELECT_W{s_apb.pwdata}}; + pba_apb_mem_rd_en = 1'b0; + + tbl_rd_data_valid_next = 1'b0; + pba_rd_data_valid_next = 1'b0; + rd_data_shift_next = rd_data_shift_reg; + + s_apb_pready_next = 1'b0; + s_apb_prdata_next = s_apb_prdata_reg; + + if (tbl_rd_data_valid_reg || pba_rd_data_valid_reg) begin + s_apb_pready_next = !s_apb_pready_reg; + tbl_rd_data_valid_next = 1'b0; + pba_rd_data_valid_next = 1'b0; + + if (tbl_rd_data_valid_reg) begin + if (APB_DATA_W < 64) begin + s_apb_prdata_next = APB_DATA_W'(tbl_apb_mem_rd_data_reg >> rd_data_shift_reg*APB_DATA_W); + end else begin + s_apb_prdata_next = APB_DATA_W'(tbl_apb_mem_rd_data_reg); + end + end else begin + if (APB_DATA_W < 64) begin + s_apb_prdata_next = APB_DATA_W'(pba_apb_mem_rd_data_reg >> rd_data_shift_reg*APB_DATA_W); + end else begin + s_apb_prdata_next = APB_DATA_W'(pba_apb_mem_rd_data_reg); + end + end + end + + if (s_apb.psel && s_apb.penable) begin + rd_data_shift_next = s_apb_paddr_word; + + if (s_apb.pwrite) begin + s_apb_pready_next = !s_apb_pready_reg; + if (s_apb.paddr[IRQ_INDEX_W+5-1] == 0) begin + tbl_apb_mem_wr_en = !s_apb_pready_reg; + end + end else begin + if (s_apb.paddr[IRQ_INDEX_W+5-1] == 0) begin + tbl_apb_mem_rd_en = 1'b1; + tbl_rd_data_valid_next = !s_apb_pready_reg; + end else begin + pba_apb_mem_rd_en = 1'b1; + pba_rd_data_valid_next = !s_apb_pready_reg; + end + end + end +end + +always_ff @(posedge clk) begin + tbl_rd_data_valid_reg <= tbl_rd_data_valid_next; + pba_rd_data_valid_reg <= pba_rd_data_valid_next; + rd_data_shift_reg <= rd_data_shift_next; + + s_apb_pready_reg <= s_apb_pready_next; + s_apb_prdata_reg <= s_apb_prdata_next; + + if (tbl_apb_mem_rd_en) begin + tbl_apb_mem_rd_data_reg <= tbl_mem[s_apb_paddr_index]; + end else begin + for (integer i = 0; i < 8; i = i + 1) begin + if (tbl_apb_mem_wr_en && tbl_apb_mem_wr_be[i]) begin + tbl_mem[s_apb_paddr_index][8*i +: 8] <= tbl_apb_mem_wr_data[8*i +: 8]; + end + end + end + + if (pba_apb_mem_rd_en) begin + pba_apb_mem_rd_data_reg <= pba_mem[s_apb_paddr_index[PBA_ADDR_W-1:0]]; + end + + if (rst) begin + tbl_rd_data_valid_reg <= 1'b0; + pba_rd_data_valid_reg <= 1'b0; + + s_apb_pready_reg <= 1'b0; + end +end + +endmodule + +`resetall diff --git a/src/pcie/tb/taxi_pcie_msix_apb/Makefile b/src/pcie/tb/taxi_pcie_msix_apb/Makefile new file mode 100644 index 0000000..e163725 --- /dev/null +++ b/src/pcie/tb/taxi_pcie_msix_apb/Makefile @@ -0,0 +1,56 @@ +# SPDX-License-Identifier: CERN-OHL-S-2.0 +# +# Copyright (c) 2021-2026 FPGA Ninja, LLC +# +# Authors: +# - Alex Forencich + +TOPLEVEL_LANG = verilog + +SIM ?= verilator +WAVES ?= 0 + +COCOTB_HDL_TIMEUNIT = 1ns +COCOTB_HDL_TIMEPRECISION = 1ps + +RTL_DIR = ../../rtl +LIB_DIR = ../../lib +TAXI_SRC_DIR = $(LIB_DIR)/taxi/src + +DUT = taxi_pcie_msix_apb +COCOTB_TEST_MODULES = test_$(DUT) +COCOTB_TOPLEVEL = test_$(DUT) +MODULE = $(COCOTB_TEST_MODULES) +TOPLEVEL = $(COCOTB_TOPLEVEL) +VERILOG_SOURCES += $(COCOTB_TOPLEVEL).sv +VERILOG_SOURCES += $(RTL_DIR)/$(DUT).sv +VERILOG_SOURCES += $(RTL_DIR)/taxi_pcie_tlp_if.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/axis/rtl/taxi_axis_if.sv +VERILOG_SOURCES += $(TAXI_SRC_DIR)/apb/rtl/taxi_apb_if.sv + +# handle file list files +process_f_file = $(call process_f_files,$(addprefix $(dir $1),$(shell cat $1))) +process_f_files = $(foreach f,$1,$(if $(filter %.f,$f),$(call process_f_file,$f),$f)) +uniq_base = $(if $1,$(call uniq_base,$(foreach f,$1,$(if $(filter-out $(notdir $(lastword $1)),$(notdir $f)),$f,))) $(lastword $1)) +VERILOG_SOURCES := $(call uniq_base,$(call process_f_files,$(VERILOG_SOURCES))) + +# module parameters +export PARAM_IRQ_INDEX_W := 11 +export PARAM_APB_DATA_W := 32 +export PARAM_APB_ADDR_W := $(shell expr $(PARAM_IRQ_INDEX_W) + 5 ) +export PARAM_TLP_FORCE_64_BIT_ADDR := 0 + +ifeq ($(SIM), icarus) + PLUSARGS += -fst + + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-P $(COCOTB_TOPLEVEL).$(subst PARAM_,,$(v))=$($(v))) +else ifeq ($(SIM), verilator) + COMPILE_ARGS += $(foreach v,$(filter PARAM_%,$(.VARIABLES)),-G$(subst PARAM_,,$(v))=$($(v))) + + ifeq ($(WAVES), 1) + COMPILE_ARGS += --trace-fst + VERILATOR_TRACE = 1 + endif +endif + +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/src/pcie/tb/taxi_pcie_msix_apb/pcie_if.py b/src/pcie/tb/taxi_pcie_msix_apb/pcie_if.py new file mode 120000 index 0000000..10502b0 --- /dev/null +++ b/src/pcie/tb/taxi_pcie_msix_apb/pcie_if.py @@ -0,0 +1 @@ +../pcie_if.py \ No newline at end of file diff --git a/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.py b/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.py new file mode 100644 index 0000000..0e94a5e --- /dev/null +++ b/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: CERN-OHL-S-2.0 +""" + +Copyright (c) 2022-2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +""" + +import itertools +import logging +import os +import re +import sys +from contextlib import contextmanager + +import cocotb_test.simulator +import pytest + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from cocotb.regression import TestFactory + +from cocotbext.axi import ApbBus, ApbMaster +from cocotbext.axi import AxiStreamBus, AxiStreamSource + + +try: + from pcie_if import PcieIfSink, PcieIfTxBus +except ImportError: + # attempt import from current directory + sys.path.insert(0, os.path.join(os.path.dirname(__file__))) + try: + from pcie_if import PcieIfSink, PcieIfTxBus + finally: + del sys.path[0] + + +@contextmanager +def assert_raises(exc_type, pattern=None): + try: + yield + except exc_type as e: + if pattern: + assert re.match(pattern, str(e)), \ + "Correct exception type caught, but message did not match pattern" + pass + else: + raise AssertionError("{} was not raised".format(exc_type.__name__)) + + +class TB(object): + def __init__(self, dut): + self.dut = dut + + self.log = logging.getLogger("cocotb.tb") + self.log.setLevel(logging.DEBUG) + + cocotb.start_soon(Clock(dut.clk, 4, units="ns").start()) + + self.apb_master = ApbMaster(ApbBus.from_entity(dut.s_apb), dut.clk, dut.rst) + + self.irq_source = AxiStreamSource(AxiStreamBus.from_entity(dut.s_axis_irq), dut.clk, dut.rst) + + self.tlp_sink = PcieIfSink(PcieIfTxBus.from_entity(dut.tx_wr_req_tlp), dut.clk, dut.rst) + + dut.bus_num.setimmediatevalue(0) + dut.func_num.setimmediatevalue(0) + dut.msix_enable.setimmediatevalue(0) + dut.msix_mask.setimmediatevalue(0) + + def set_idle_generator(self, generator=None): + if generator: + self.apb_master.set_pause_generator(generator()) + + def set_backpressure_generator(self, generator=None): + if generator: + self.tlp_sink.set_pause_generator(generator()) + + async def cycle_reset(self): + self.dut.rst.setimmediatevalue(0) + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 1 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + self.dut.rst.value = 0 + await RisingEdge(self.dut.clk) + await RisingEdge(self.dut.clk) + + +async def run_test_table_write(dut, data_in=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.apb_master.byte_lanes + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + for length in range(1, byte_lanes*4): + for offset in range(byte_lanes): + tb.log.info("length %d, offset %d", length, offset) + addr = offset+0x100 + test_data = bytearray([x % 256 for x in range(length)]) + + await tb.apb_master.write(addr-4, b'\xaa'*(length+8)) + + await tb.apb_master.write(addr, test_data) + + data = await tb.apb_master.read(addr-1, length+2) + + assert data.data == b'\xaa'+test_data+b'\xaa' + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_table_read(dut, data_in=None, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + byte_lanes = tb.apb_master.byte_lanes + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + for length in range(1, byte_lanes*4): + for offset in range(byte_lanes): + tb.log.info("length %d, offset %d", length, offset) + addr = offset+0x100 + test_data = bytearray([x % 256 for x in range(length)]) + + await tb.apb_master.write(addr, test_data) + + data = await tb.apb_master.read(addr, length) + + assert data.data == test_data + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +async def run_test_msix(dut, idle_inserter=None, backpressure_inserter=None): + + tb = TB(dut) + + tbl_offset = 0 + pba_offset = 2**(tb.apb_master.address_width-1) + + await tb.cycle_reset() + + tb.set_idle_generator(idle_inserter) + tb.set_backpressure_generator(backpressure_inserter) + + dut.msix_enable.value = 1 + + tb.log.info("Init table") + + for k in range(2**len(dut.s_axis_irq.tdata)): + await tb.apb_master.write_qword(tbl_offset+k*16+0, 0x1234567800000000 + k*4) + await tb.apb_master.write_dword(tbl_offset+k*16+8, k) + await tb.apb_master.write_dword(tbl_offset+k*16+12, 0) + + tb.log.info("Test unmasked interrupts") + + for k in range(8): + await tb.irq_source.send([k]) + + for k in range(8): + frame = await tb.tlp_sink.recv() + tlp = frame.to_tlp() + + tb.log.info("TLP: %s", tlp) + + assert tlp.address == 0x1234567800000000 + k*4 + assert tlp.data == k.to_bytes(4, 'little') + assert tlp.first_be == 0xf + + val = await tb.apb_master.read_dword(pba_offset+0) + + tb.log.info("PBA value: 0x%02x", val) + + assert val == 0x00 + + tb.log.info("Test global mask") + + dut.msix_mask.value = 1 + + for k in range(8): + await tb.irq_source.send([k]) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + while int(dut.s_axis_irq.tvalid.value): + await RisingEdge(dut.clk) + for k in range(10): + await RisingEdge(dut.clk) + + val = await tb.apb_master.read_dword(pba_offset+0) + + tb.log.info("PBA value: 0x%02x", val) + + assert val == 0xff + + dut.msix_mask.value = 0 + + for k in range(8): + frame = await tb.tlp_sink.recv() + tlp = frame.to_tlp() + + tb.log.info("TLP: %s", tlp) + + assert tlp.address == 0x1234567800000000 + k*4 + assert tlp.data == k.to_bytes(4, 'little') + assert tlp.first_be == 0xf + + val = await tb.apb_master.read_dword(pba_offset+0) + + tb.log.info("PBA value: 0x%02x", val) + + assert val == 0x00 + + tb.log.info("Test vector masks") + + for k in range(8): + await tb.apb_master.write_dword(tbl_offset+k*16+12, 1) + + for k in range(8): + await tb.irq_source.send([k]) + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + while int(dut.s_axis_irq.tvalid.value): + await RisingEdge(dut.clk) + for k in range(10): + await RisingEdge(dut.clk) + + val = await tb.apb_master.read_dword(pba_offset+0) + + tb.log.info("PBA value: 0x%02x", val) + + assert val == 0xff + + for k in range(8): + await tb.apb_master.write_dword(tbl_offset+k*16+12, 0) + + for k in range(8): + frame = await tb.tlp_sink.recv() + tlp = frame.to_tlp() + + tb.log.info("TLP: %s", tlp) + + assert tlp.address == 0x1234567800000000 + k*4 + assert tlp.data == k.to_bytes(4, 'little') + assert tlp.first_be == 0xf + + val = await tb.apb_master.read_dword(pba_offset+0) + + tb.log.info("PBA value: 0x%02x", val) + + assert val == 0x00 + + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + +def cycle_pause(): + return itertools.cycle([1, 1, 1, 0]) + + +if getattr(cocotb, 'top', None) is not None: + + for test in [ + run_test_table_write, + run_test_table_read, + run_test_msix + ]: + + factory = TestFactory(test) + factory.add_option("idle_inserter", [None, cycle_pause]) + factory.add_option("backpressure_inserter", [None, cycle_pause]) + factory.generate_tests() + + +# cocotb-test + +tests_dir = os.path.abspath(os.path.dirname(__file__)) +rtl_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'rtl')) +lib_dir = os.path.abspath(os.path.join(tests_dir, '..', '..', 'lib')) +taxi_src_dir = os.path.abspath(os.path.join(lib_dir, 'taxi', 'src')) + + +def process_f_files(files): + lst = {} + for f in files: + if f[-2:].lower() == '.f': + with open(f, 'r') as fp: + l = fp.read().split() + for f in process_f_files([os.path.join(os.path.dirname(f), x) for x in l]): + lst[os.path.basename(f)] = f + else: + lst[os.path.basename(f)] = f + return list(lst.values()) + + +@pytest.mark.parametrize("apb_data_w", [32, 64]) +def test_taxi_pcie_msix_apb(request, apb_data_w): + dut = "taxi_pcie_msix_apb" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = module + + verilog_sources = [ + os.path.join(tests_dir, f"{toplevel}.sv"), + os.path.join(rtl_dir, f"{dut}.sv"), + os.path.join(rtl_dir, "taxi_pcie_tlp_if.sv"), + os.path.join(taxi_src_dir, "axis", "rtl", "taxi_axis_if.sv"), + os.path.join(taxi_src_dir, "apb", "rtl", "taxi_apb_if.sv"), + ] + + parameters = {} + + parameters['IRQ_INDEX_W'] = 11 + parameters['APB_DATA_W'] = apb_data_w + parameters['APB_ADDR_W'] = parameters['IRQ_INDEX_W']+5 + parameters['TLP_FORCE_64_BIT_ADDR'] = 0 + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + + sim_build = os.path.join(tests_dir, "sim_build", + request.node.name.replace('[', '-').replace(']', '')) + + cocotb_test.simulator.run( + simulator="verilator", + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + ) diff --git a/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.sv b/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.sv new file mode 100644 index 0000000..df39c25 --- /dev/null +++ b/src/pcie/tb/taxi_pcie_msix_apb/test_taxi_pcie_msix_apb.sv @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: CERN-OHL-S-2.0 +/* + +Copyright (c) 2026 FPGA Ninja, LLC + +Authors: +- Alex Forencich + +*/ + +`resetall +`timescale 1ns / 1ps +`default_nettype none + +/* + * PCIe MSI-X module testbench + */ +module test_taxi_pcie_msix_apb # +( + /* verilator lint_off WIDTHTRUNC */ + parameter IRQ_INDEX_W = 11, + parameter TLP_SEG_DATA_W = 64, + parameter TLP_SEGS = 1, + parameter APB_DATA_W = 32, + parameter APB_ADDR_W = IRQ_INDEX_W+5, + parameter logic TLP_FORCE_64_BIT_ADDR = 1'b0 + /* verilator lint_on WIDTHTRUNC */ +) +(); + +logic clk; +logic rst; + +taxi_apb_if #( + .DATA_W(APB_DATA_W), + .ADDR_W(APB_ADDR_W), + .PAUSER_EN(1'b0), + .PWUSER_EN(1'b0), + .PBUSER_EN(1'b0), + .PRUSER_EN(1'b0) +) s_apb(); + +taxi_axis_if #( + .DATA_W(IRQ_INDEX_W), + .KEEP_EN(0), + .KEEP_W(1) +) s_axis_irq(); + +taxi_pcie_tlp_if #( + .SEGS(TLP_SEGS), + .SEG_DATA_W(TLP_SEG_DATA_W), + .FUNC_NUM_W(8) +) tx_wr_req_tlp(); + +logic [7:0] bus_num; +logic [7:0] func_num; +logic msix_enable; +logic msix_mask; + +taxi_pcie_msix_apb #( + .TLP_FORCE_64_BIT_ADDR(TLP_FORCE_64_BIT_ADDR) +) +uut ( + .clk(clk), + .rst(rst), + + /* + * APB interface for MSI-X tables + */ + .s_apb(s_apb), + + /* + * Interrupt request input + */ + .s_axis_irq(s_axis_irq), + + /* + * Memory write TLP output + */ + .tx_wr_req_tlp(tx_wr_req_tlp), + + /* + * Configuration + */ + .bus_num(bus_num), + .func_num(func_num), + .msix_enable(msix_enable), + .msix_mask(msix_mask) +); + +endmodule + +`resetall