From 3ddf9130d329f7f6623da708faa229859fd7cf8a Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 10 Jul 2026 17:30:51 +0300 Subject: [PATCH] fix: reg_pulse support --- axi/axi_reg/axi4l_reg_map.sv | 38 +++- axi/tb/axil_reg_cocotb/tb_axi4l_reg_map.sv | 4 +- axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py | 182 +++++++++++++++++-- 3 files changed, 198 insertions(+), 26 deletions(-) diff --git a/axi/axi_reg/axi4l_reg_map.sv b/axi/axi_reg/axi4l_reg_map.sv index 36544a8..2536b80 100644 --- a/axi/axi_reg/axi4l_reg_map.sv +++ b/axi/axi_reg/axi4l_reg_map.sv @@ -12,7 +12,8 @@ module axi4l_reg_map #( axi4l_if.slave s_axil, input logic [N_REGS-1:0][31:0] reg_i, - output logic [N_REGS-1:0][31:0] reg_o + output logic [N_REGS-1:0][31:0] reg_o, + output logic [N_REGS-1:0][31:0] reg_pulse ); import axi_pkg::*; @@ -105,6 +106,7 @@ module axi4l_reg_map #( rdata_q <= '0; reg_o <= REG_RST; end else begin + reg_pulse <= '0; for (int r = 0; r < N_REGS; r++) begin for (int bit_idx = 0; bit_idx < 32; bit_idx++) begin if (reg_bit_mode_t'(REG_MODE[r][bit_idx]) == REG_BIT_W1S) @@ -136,12 +138,33 @@ module axi4l_reg_map #( for (b = 0; b < 32; b = b + 1) begin if (wr_mask[b]) begin unique case (reg_bit_mode_t'(REG_MODE[wr_idx][b])) - REG_BIT_RSVD: begin end - REG_BIT_RO : begin bresp_q <= 2'b10; end - REG_BIT_RW : rw_new[b] = wr_data32[b]; - REG_BIT_W1S : if (wr_data32[b]) rw_new[b] = 1'b1; - REG_BIT_W1C : if (wr_data32[b]) rw_new[b] = 1'b0; - default : begin end + REG_BIT_RSVD: begin + end + + REG_BIT_RO: begin + bresp_q <= 2'b10; + end + + REG_BIT_RW: begin + rw_new[b] = wr_data32[b]; + end + + REG_BIT_W1S: begin + if (wr_data32[b]) begin + rw_new[b] = 1'b1; + reg_pulse[wr_idx][b] <= 1'b1; + end + end + + REG_BIT_W1C: begin + if (wr_data32[b]) begin + rw_new[b] = 1'b0; + reg_pulse[wr_idx][b] <= 1'b1; + end + end + + default: begin + end endcase end end @@ -174,6 +197,7 @@ module axi4l_reg_map #( REG_BIT_W1C : rd_word[b] = reg_o[rd_idx][b]; default : rd_word[b] = 1'b0; endcase + end end diff --git a/axi/tb/axil_reg_cocotb/tb_axi4l_reg_map.sv b/axi/tb/axil_reg_cocotb/tb_axi4l_reg_map.sv index 264f6f7..278f20b 100644 --- a/axi/tb/axil_reg_cocotb/tb_axi4l_reg_map.sv +++ b/axi/tb/axil_reg_cocotb/tb_axi4l_reg_map.sv @@ -92,6 +92,7 @@ module tb_axi4l_reg_map; logic [N_REGS-1:0][31:0] reg_i; logic [N_REGS-1:0][31:0] reg_o; + logic [N_REGS-1:0][31:0] reg_pulse; // debug probes visible from cocotb. they are useful for checking W1S pulse-like behavior logic [31:0] reg0_o; @@ -157,7 +158,8 @@ module tb_axi4l_reg_map; .rst_n (rst_n), .s_axil (s_axil_if.slave), .reg_i (reg_i), - .reg_o (reg_o) + .reg_o (reg_o), + .reg_pulse(reg_pulse) ); endmodule diff --git a/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py b/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py index f3ab50f..5c7b411 100644 --- a/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py +++ b/axi/tb/axil_reg_cocotb/test_axi4l_reg_map.py @@ -1,14 +1,12 @@ -# SPDX-License-Identifier: MIT - import cocotb from cocotb.clock import Clock -from cocotb.triggers import RisingEdge +from cocotb.triggers import ReadOnly, RisingEdge from cocotbext.axi import AxiLiteBus, AxiLiteMaster OKAY = 0 SLVERR = 2 -REG0_CTRL = 0x00 +REG0_CTRL = 0x00 REG1_STATUS = 0x04 REG2_CONFIG = 0x08 REG_INVALID = 0x0C # N_REGS=3, so index 3 is bad @@ -35,7 +33,8 @@ def _read_data(resp): class TB: def __init__(self, dut): self.dut = dut - self.axil = AxiLiteMaster(AxiLiteBus.from_prefix(dut, "s_axil"), dut.clk, dut.rst) + self.axil = AxiLiteMaster(AxiLiteBus.from_prefix( + dut, "s_axil"), dut.clk, dut.rst) async def reset(self): self.dut.rst.value = 1 @@ -114,28 +113,175 @@ async def test_rw_and_w1c_bits(dut): assert await tb.read32(REG2_CONFIG) == 0xDEADBEEF +def _reg0_pulse_value(dut): + """Return REG0 pulse bits regardless of how the test wrapper exposes them.""" + if hasattr(dut, "reg0_pulse"): + return int(dut.reg0_pulse.value) + + if hasattr(dut, "reg_pulse"): + # Packed SystemVerilog array [N_REGS-1:0][31:0]: REG0 occupies bits 31:0. + return int(dut.reg_pulse.value) & 0xFFFF_FFFF + + raise AssertionError( + "DUT must expose either reg0_pulse[31:0] or packed reg_pulse" + ) + + +async def _sample_reg0_pulse_cycles(dut, cycles): + """Sample REG0 pulse output after each active clock edge.""" + samples = [] + + for _ in range(cycles): + await RisingEdge(dut.clk) + await ReadOnly() + samples.append(_reg0_pulse_value(dut)) + + return samples + + +def _assert_single_cycle_bit_pulse(samples, bit, name): + """Check that one selected bit was high for exactly one sampled cycle.""" + high_cycles = [ + index for index, value in enumerate(samples) + if value & (1 << bit) + ] + assert len(high_cycles) == 1, ( + f"{name}: expected one high cycle, got {len(high_cycles)}; " + f"samples={[f'0x{value:08x}' for value in samples]}" + ) + return high_cycles[0] + + @cocotb.test() -async def test_w1s_seen_on_reg_o_probe(dut): - """Check that a W1S write creates a short-lived internal reg_o bit.""" +async def test_reg_pulse_is_zero_after_reset(dut): + """No W1 event may be reported after reset without a write.""" cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) tb = TB(dut) await tb.reset() - seen = False + samples = await _sample_reg0_pulse_cycles(dut, 4) + assert not any(samples), ( + "reg_pulse was asserted without a W1 write: " + f"{[f'0x{value:08x}' for value in samples]}" + ) - async def monitor_w1s_bit(): - nonlocal seen - for _ in range(20): - await RisingEdge(dut.clk) - if int(dut.reg0_o.value) & 0x1: - seen = True - mon = cocotb.start_soon(monitor_w1s_bit()) +@cocotb.test() +async def test_w1s_generates_single_cycle_reg_pulse(dut): + """Writing 1 to REG0[0] W1S must pulse REG0 pulse bit 0 once.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + monitor = cocotb.start_soon(_sample_reg0_pulse_cycles(dut, 20)) await tb.write32(REG0_CTRL, 0x00000001) - await mon + samples = await monitor - assert seen, "W1S bit was never observed on reg0_o[0]" - assert await tb.read32(REG0_CTRL) == 0x00000004 # W1S reads as 0, W1C reset bit remains set + _assert_single_cycle_bit_pulse(samples, bit=0, name="reg_pulse[0][0]") + + # No unrelated W1 bit may pulse. + assert not any(value & (1 << 2) for value in samples) + + # W1S is a command bit and is not returned by reads. + assert await tb.read32(REG0_CTRL) == 0x00000004 + + +@cocotb.test() +async def test_w1c_generates_single_cycle_reg_pulse(dut): + """Writing 1 to REG0[2] W1C must pulse bit 2 once and clear state.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + assert await tb.read32(REG0_CTRL) == 0x00000004 + + monitor = cocotb.start_soon(_sample_reg0_pulse_cycles(dut, 20)) + await tb.write32(REG0_CTRL, 0x00000004) + samples = await monitor + + _assert_single_cycle_bit_pulse(samples, bit=2, name="reg_pulse[0][2]") + + # No unrelated W1 bit may pulse. + assert not any(value & (1 << 0) for value in samples) + + assert await tb.read32(REG0_CTRL) == 0x00000000 + + +@cocotb.test() +async def test_zero_to_w1_and_rw_write_do_not_generate_reg_pulse(dut): + """Only written ones in W1 fields may create reg_pulse.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + monitor = cocotb.start_soon(_sample_reg0_pulse_cycles(dut, 20)) + + # bit1 is RW; both W1 fields receive zero. + await tb.write32(REG0_CTRL, 0x00000002) + samples = await monitor + + assert not any(samples), ( + "RW write or zero written to W1 fields generated reg_pulse: " + f"{[f'0x{value:08x}' for value in samples]}" + ) + assert await tb.read32(REG0_CTRL) == 0x00000006 + + +@cocotb.test() +async def test_w1s_and_w1c_pulse_together(dut): + """W1S and W1C ones in one AXI write must pulse on the same cycle.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + monitor = cocotb.start_soon(_sample_reg0_pulse_cycles(dut, 20)) + await tb.write32(REG0_CTRL, 0x00000005) + samples = await monitor + + w1s_cycle = _assert_single_cycle_bit_pulse( + samples, bit=0, name="reg_pulse[0][0]" + ) + w1c_cycle = _assert_single_cycle_bit_pulse( + samples, bit=2, name="reg_pulse[0][2]" + ) + + assert w1s_cycle == w1c_cycle, ( + "W1 bits written by one transaction pulsed on different cycles: " + f"W1S={w1s_cycle}, W1C={w1c_cycle}" + ) + + # Only bits 0 and 2 are allowed to pulse. + assert samples[w1s_cycle] == 0x00000005 + assert await tb.read32(REG0_CTRL) == 0x00000000 + + +@cocotb.test() +async def test_each_w1_write_creates_a_new_pulse(dut): + """Two separate W1 writes must create two separate one-cycle pulses.""" + cocotb.start_soon(Clock(dut.clk, 10, units="ns").start()) + tb = TB(dut) + await tb.reset() + + monitor = cocotb.start_soon(_sample_reg0_pulse_cycles(dut, 40)) + + await tb.write32(REG0_CTRL, 0x00000001) + for _ in range(3): + await RisingEdge(dut.clk) + await tb.write32(REG0_CTRL, 0x00000001) + + samples = await monitor + high_cycles = [ + index for index, value in enumerate(samples) + if value & 0x1 + ] + + assert len(high_cycles) == 2, ( + f"expected two W1S pulses, got cycles {high_cycles}; " + f"samples={[f'0x{value:08x}' for value in samples]}" + ) + assert high_cycles[1] > high_cycles[0] + 1, ( + f"separate writes did not produce separate pulses: {high_cycles}" + ) @cocotb.test()