diff --git a/.github/workflows/regression.yaml b/.github/workflows/regression.yaml index 33378f9..af77e4a 100644 --- a/.github/workflows/regression.yaml +++ b/.github/workflows/regression.yaml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: [3.12] group: [1, 2, 3, 4, 5] steps: @@ -24,7 +24,7 @@ jobs: sudo apt install -y --no-install-recommends make g++ perl python3 autoconf flex bison libfl2 libfl-dev zlibc zlib1g zlib1g-dev git clone https://github.com/verilator/verilator.git cd verilator - git checkout v4.106 + git checkout v5.026 autoconf ./configure make -j $(nproc) @@ -40,7 +40,7 @@ jobs: SIM: verilator CXX: 'g++' CC: 'gcc' - PYTHON_VERSION: 3.9 + PYTHON_VERSION: 3.12 FULL_REGRESSION: 1 TOX_TESTENV_PASSENV: GITHUB_ACTIONS run: tox -- --splits 5 --group ${{ matrix.group }} diff --git a/run_docker.sh b/run_docker.sh new file mode 100755 index 0000000..340ce28 --- /dev/null +++ b/run_docker.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker run -it --rm -v $(pwd):/temp/ -w /temp/ aignacio/rtl bash diff --git a/src/cdc_async_fifo_w_ocup.sv b/src/cdc_async_fifo_w_ocup.sv new file mode 100644 index 0000000..e29e4df --- /dev/null +++ b/src/cdc_async_fifo_w_ocup.sv @@ -0,0 +1,173 @@ +/** + * File: async_gp_fifo.sv + * Description: General purpose Asynchronous FIFO, + * based on the following articles: + * -> http://www.sunburst-design.com/papers/CummingsSNUG2002SJ_FIFO1.pdf + * -> https://zipcpu.com/blog/2018/07/06/afifo.html* File: async_fifo.sv + * Author: Anderson Ignacio da Silva + * + * MIT License + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +module cdc_async_fifo_w_ocup # ( + parameter int SLOTS = 2, + parameter int WIDTH = 8 +) ( + // Clock domain 1 + input clk_wr, + input arst_wr, + input wr_en_i, + input [WIDTH-1:0] wr_data_i, + output logic wr_full_o, + output logic [$clog2(SLOTS):0] ocup_o, + + // Clock domain 2 + input clk_rd, + input arst_rd, + input rd_en_i, + output logic [WIDTH-1:0] rd_data_o, + output logic rd_empty_o +); + `define IDX_PTR w_wr_bin_ptr_ff[$clog2(SLOTS)-1:0] // Valid index pointer + + // Naming convention + // ptr - pointer + // wr - write + // rd - read + // bin - binary + // + // Pointers convention used follows the below pattern: + // -> CLKDOMAIN(w/r)_TYPE(wr/rd)_ENCODING(bin/gray)_ptr + // Example: + // w_wr_bin_ptr - Write binary pointer in write clock domain + // r_wr_gray_ptr - Write gray pointer in read clock domain + // META_... - Metastable transition FF + typedef logic [$clog2(SLOTS):0] ptr_t; + logic [SLOTS-1:0] [WIDTH-1:0] array_fifo_ff; + + ptr_t w_wr_bin_ptr_ff, next_w_wr_bin_ptr; + ptr_t w_rd_gry_ptr_ff, w_rd_bin_ptr; // We only bring to wr domain + // the rd gray encoded ptr once + // we only use it to gen the full + // flag, and crossing gray encoding + // it's more stable than bin encoding + + ptr_t r_rd_bin_ptr_ff, next_r_rd_bin_ptr; + ptr_t r_wr_gry_ptr_ff; // We only bring to rd domain + // the wr gray encoded ptr once + // we only use it to gen the empty + // flag, and crossing gray encoding + // it's more stable than bin encoding + (* async_reg = "true" *) ptr_t META_w_rd_gry_ff; + (* async_reg = "true" *) ptr_t META_r_wr_gry_ff; + + //************************ + // Functions + //************************ + function automatic ptr_t bin_to_gray (ptr_t input_bin); + ptr_t value; + value = (input_bin >> 1) ^ input_bin; + return value; + endfunction + + function automatic ptr_t gray_to_bin (ptr_t input_gray); + ptr_t value; + value = input_gray; + for (int i=$clog2(SLOTS);i>0;i--) + value[i-1] = value[i]^value[i-1]; + return value; + endfunction + + //************************ + // Write logic + //************************ + always_comb begin : wr_pointer + next_w_wr_bin_ptr = w_wr_bin_ptr_ff; + w_rd_bin_ptr = gray_to_bin(w_rd_gry_ptr_ff); + + ocup_o = w_wr_bin_ptr_ff - w_rd_bin_ptr; + + wr_full_o = (w_wr_bin_ptr_ff[$clog2(SLOTS)] == ~w_rd_bin_ptr[$clog2(SLOTS)]) && + (w_wr_bin_ptr_ff[$clog2(SLOTS)-1:0] == w_rd_bin_ptr[$clog2(SLOTS)-1:0]); + + if (wr_en_i && ~wr_full_o) begin + next_w_wr_bin_ptr = w_wr_bin_ptr_ff + 'd1; + end + end + + always_ff @ (posedge clk_wr or posedge arst_wr) begin + if (arst_wr) begin + w_wr_bin_ptr_ff <= ptr_t'(0); + META_w_rd_gry_ff <= ptr_t'(0); + w_rd_gry_ptr_ff <= ptr_t'(0); + //array_fifo_ff <= '0; // --> Let's make it "low power" + end + else begin + w_wr_bin_ptr_ff <= next_w_wr_bin_ptr; + // 2FF Synchronizer: + // Bring RD ptr to WR domain + META_w_rd_gry_ff <= bin_to_gray(r_rd_bin_ptr_ff); + w_rd_gry_ptr_ff <= META_w_rd_gry_ff; + + if (wr_en_i && ~wr_full_o) begin + array_fifo_ff[`IDX_PTR] <= wr_data_i; + end + end + end + + //************************ + // Read logic + //************************ + always_comb begin : rd_pointer + next_r_rd_bin_ptr = r_rd_bin_ptr_ff; + + rd_empty_o = (bin_to_gray(r_rd_bin_ptr_ff) == r_wr_gry_ptr_ff); + + if (rd_en_i && ~rd_empty_o) begin + next_r_rd_bin_ptr = r_rd_bin_ptr_ff + 'd1; + end + + rd_data_o = array_fifo_ff[r_rd_bin_ptr_ff[$clog2(SLOTS)-1:0]]; + end + + always_ff @ (posedge clk_rd or posedge arst_rd) begin + if (arst_rd) begin + r_rd_bin_ptr_ff <= ptr_t'(0); + META_r_wr_gry_ff <= ptr_t'(0); + r_wr_gry_ptr_ff <= ptr_t'(0); + end + else begin + r_rd_bin_ptr_ff <= next_r_rd_bin_ptr; + // 2FF Synchronizer: + // Bring RD ptr to WR domain + META_r_wr_gry_ff <= bin_to_gray(w_wr_bin_ptr_ff); + r_wr_gry_ptr_ff <= META_r_wr_gry_ff; + end + end + +`ifndef NO_ASSERTIONS + initial begin + illegal_fifo_slot : assert (2**$clog2(SLOTS) == SLOTS) + else $error("ASYNC FIFO Slots must be power of 2"); + + min_fifo_size_2 : assert (SLOTS >= 2) + else $error("ASYNC FIFO size of SLOTS defined is illegal!"); + end +`endif +endmodule diff --git a/tb/test_afifo_w_ocup.py b/tb/test_afifo_w_ocup.py new file mode 100644 index 0000000..9e59baa --- /dev/null +++ b/tb/test_afifo_w_ocup.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# File : test_afifo_w_ocup.py +# License : MIT license +# Author : Anderson Ignacio da Silva (aignacio) +# Date : 12.05.2021 +# Last Modified Date: 10.09.2024 +# Last Modified By : Anderson Ignacio da Silva (aignacio) +import random +import cocotb +import os +import logging +import pytest + +from logging.handlers import RotatingFileHandler +from cocotb.log import SimColourLogFormatter, SimLog, SimTimeContextFilter +from cocotb.regression import TestFactory +from cocotb_test.simulator import run +from cocotb.regression import TestFactory +from cocotb.clock import Clock +from cocotb_bus.drivers import Driver +from cocotb.triggers import ClockCycles, FallingEdge, RisingEdge, Timer +from collections import namedtuple + +CLK_100MHz = (10, "ns") +CLK_50MHz = (20, "ns") +RST_CYCLES = 2 +WAIT_CYCLES = 2 + +class AFIFODriver(): + def __init__(self, signals, debug=False, slots=0, width=0): + level = logging.DEBUG if debug else logging.WARNING + self.log = SimLog("afifo.log") + file_handler = RotatingFileHandler("sim.log", maxBytes=(5 * 1024 * 1024), backupCount=2, mode='w') + file_handler.setFormatter(SimColourLogFormatter()) + self.log.addHandler(file_handler) + self.log.addFilter(SimTimeContextFilter()) + self.log.setLevel(level) + self.log.info("SEED ======> %s",str(cocotb.RANDOM_SEED)) + + self.clk_wr = signals.clk_wr + self.valid_wr = signals.wr_en_i + self.data_wr = signals.wr_data_i + self.ready_wr = signals.wr_full_o + self.clk_rd = signals.clk_rd + self.valid_rd = signals.rd_empty_o + self.data_rd = signals.rd_data_o + self.ready_rd = signals.rd_en_i + self.valid_wr <= 0 + self.ready_rd <= 0 + self.log.setLevel(level) + + async def write(self, data, sync=True, **kwargs): + self.log.info("[AFIFO driver] write => %x"%data) + while True: + await FallingEdge(self.clk_wr) + self.valid_wr <= 1 + self.data_wr <= data + await RisingEdge(self.clk_wr) + if self.ready_wr == 0: + break + elif kwargs["exit_full"] == True: + return "FULL" + self.valid_wr <= 0 + return 0 + + async def read(self, sync=True, **kwargs): + while True: + await FallingEdge(self.clk_rd) + if self.valid_rd == 0: + data = self.data_rd.value # We capture before we incr. rd ptr + self.ready_rd <= 1 + await RisingEdge(self.clk_rd) + break + elif kwargs["exit_empty"] == True: + return "EMPTY" + self.log.info("[AFIFO-driver] read => %x"%data) + self.ready_rd <= 0 + return data + +async def setup_dut(dut, clk_mode): + dut._log.info("Configuring clocks... -- %d", clk_mode) + if clk_mode == 0: + dut._log.info("50MHz - wr clk / 100MHz - rd clk") + cocotb.fork(Clock(dut.clk_wr, *CLK_50MHz).start()) + cocotb.fork(Clock(dut.clk_rd, *CLK_100MHz).start()) + else: + dut._log.info("50MHz - rd clk / 100MHz - wr clk") + cocotb.fork(Clock(dut.clk_rd, *CLK_50MHz).start()) + cocotb.fork(Clock(dut.clk_wr, *CLK_100MHz).start()) + +async def reset_dut(dut): + dut.arst_wr.setimmediatevalue(0) + dut.arst_rd.setimmediatevalue(0) + dut._log.info("Reseting DUT") + dut.arst_wr <= 1 + dut.arst_rd <= 1 + await ClockCycles(dut.clk_wr, RST_CYCLES) + dut.arst_wr <= 1 + dut.arst_rd <= 1 + await ClockCycles(dut.clk_rd, RST_CYCLES) + dut.arst_rd <= 0 + dut.arst_wr <= 0 + +def randomly_switch_config(): + return random.randint(0, 1) + +async def run_test(dut, config_clock): + logging.basicConfig(filename='sim.log', encoding='utf-8', level=logging.DEBUG) + MAX_SLOTS_FIFO = int(os.environ['PARAM_SLOTS']) + MAX_WIDTH_FIFO = int(os.environ['PARAM_WIDTH']) + TEST_RUNS = int(os.environ['TEST_RUNS']) + await setup_dut(dut, config_clock) + await reset_dut(dut) + ff_driver = AFIFODriver(signals=dut,debug=True,slots=MAX_SLOTS_FIFO,width=MAX_WIDTH_FIFO) + for i in range(TEST_RUNS): + samples = [random.randint(0,(2**MAX_WIDTH_FIFO)-1) for i in range(random.randint(0,MAX_SLOTS_FIFO))] + for i in samples: + await ff_driver.write(i,exit_full=False) + for i in samples: + assert (read_value := await ff_driver.read(exit_empty=False)) == i, "%d != %d" % (read_value, i) + # Testing FIFO full flag + await reset_dut(dut) + samples = [random.randint(0,(2**MAX_WIDTH_FIFO)-1) for i in range(MAX_SLOTS_FIFO)] + for i in samples: + await ff_driver.write(i,exit_full=False) + assert (value := await ff_driver.write(samples[0],exit_full=True)) == "FULL", "AFIFO not signaling full correctly" + # Testing FIFO empty flag + await reset_dut(dut) + assert (value := await ff_driver.read(exit_empty=True)) == "EMPTY", "AFIFO not signaling empty correctly" + +if cocotb.SIM_NAME: + factory = TestFactory(run_test) + factory.add_option('config_clock', [0, 1]) + factory.generate_tests() + +@pytest.mark.parametrize("slots",[2,4,8,16,32,64,128]) +def test_fifo_async_w_ocup(slots): + tests_dir = os.path.dirname(os.path.abspath(__file__)) + rtl_dir = tests_dir + dut = "cdc_async_fifo_w_ocup" + module = os.path.splitext(os.path.basename(__file__))[0] + toplevel = dut + verilog_sources = [ + os.path.join(rtl_dir, f"../src/{dut}.sv"), + ] + parameters = {} + parameters['SLOTS'] = slots + parameters['WIDTH'] = 2**random.randint(2,8) + + extra_env = {f'PARAM_{k}': str(v) for k, v in parameters.items()} + extra_env['TEST_RUNS'] = str(random.randint(2,10)) + extra_env['COCOTB_HDL_TIMEUNIT'] = "1ns" + extra_env['COCOTB_HDL_TIMEPRECISION'] = "1ns" + + sim_build = os.path.join(tests_dir, "../run_dir/sim_build_afifo_w_ocup_"+"_".join(("{}={}".format(*i) for i in parameters.items()))) + + run( + python_search=[tests_dir], + verilog_sources=verilog_sources, + toplevel=toplevel, + module=module, + parameters=parameters, + sim_build=sim_build, + extra_env=extra_env, + extra_args=["--trace-fst","--trace-structs"], + plus_args=["--trace"], + waves=True + ) diff --git a/tox.ini b/tox.ini index b4ff9de..b1d3e5e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] -envlist = py39 +envlist = py312 skipsdist = True [gh-actions] python = - 3.9: py39 + 3.12: py312 [testenv] setenv = @@ -18,9 +18,9 @@ deps = pytest-sugar pytest-xdist pytest-split - cocotb-bus == 0.1.1 - cocotb-test == 0.2.0 - cocotb == 1.5.1 + cocotb-bus >= 0.1.1 + cocotb-test >=0.2.0 + cocotb >= 1.8.1 commands = pytest -n auto {posargs}