diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18683d2..3e2413a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,12 @@ jobs: uses: astral-sh/setup-uv@v3 with: python-version: ${{ matrix.python-version }} + + - name: Install Verilator + run: | + sudo apt-get update + sudo apt-get install -y verilator + verilator --version - name: Install dependencies run: | diff --git a/hdl-src/apb3_intf.sv b/hdl-src/apb3_intf.sv index d18d3a0..f56f23b 100644 --- a/hdl-src/apb3_intf.sv +++ b/hdl-src/apb3_intf.sv @@ -2,6 +2,10 @@ interface apb3_intf #( parameter DATA_WIDTH = 32, parameter ADDR_WIDTH = 32 ); + // Clocking + logic PCLK; + logic PRESETn; + // Command logic PSEL; logic PENABLE; @@ -15,6 +19,9 @@ interface apb3_intf #( logic PSLVERR; modport master ( + input PCLK, + input PRESETn, + output PSEL, output PENABLE, output PWRITE, @@ -27,6 +34,9 @@ interface apb3_intf #( ); modport slave ( + input PCLK, + input PRESETn, + input PSEL, input PENABLE, input PWRITE, diff --git a/hdl-src/apb4_intf.sv b/hdl-src/apb4_intf.sv index 4a554f8..0bf39e2 100644 --- a/hdl-src/apb4_intf.sv +++ b/hdl-src/apb4_intf.sv @@ -2,6 +2,10 @@ interface apb4_intf #( parameter DATA_WIDTH = 32, parameter ADDR_WIDTH = 32 ); + // Clocking + logic PCLK; + logic PRESETn; + // Command logic PSEL; logic PENABLE; @@ -17,6 +21,9 @@ interface apb4_intf #( logic PSLVERR; modport master ( + input PCLK, + input PRESETn, + output PSEL, output PENABLE, output PWRITE, @@ -31,6 +38,9 @@ interface apb4_intf #( ); modport slave ( + input PCLK, + input PRESETn, + input PSEL, input PENABLE, input PWRITE, diff --git a/hdl-src/axi4lite_intf.sv b/hdl-src/axi4lite_intf.sv index befda61..f68356a 100644 --- a/hdl-src/axi4lite_intf.sv +++ b/hdl-src/axi4lite_intf.sv @@ -2,6 +2,9 @@ interface axi4lite_intf #( parameter DATA_WIDTH = 32, parameter ADDR_WIDTH = 32 ); + logic ACLK; + logic ARESETn; + logic AWREADY; logic AWVALID; logic [ADDR_WIDTH-1:0] AWADDR; @@ -27,6 +30,9 @@ interface axi4lite_intf #( logic [1:0] RRESP; modport master ( + input ACLK, + input ARESETn, + input AWREADY, output AWVALID, output AWADDR, @@ -53,15 +59,18 @@ interface axi4lite_intf #( ); modport slave ( + input ACLK, + input ARESETn, + output AWREADY, - // input AWVALID, - // input AWADDR, + input AWVALID, + input AWADDR, input AWPROT, output WREADY, - // input WVALID, - // input WDATA, - // input WSTRB, + input WVALID, + input WDATA, + input WSTRB, input BREADY, output BVALID, @@ -73,8 +82,8 @@ interface axi4lite_intf #( input ARPROT, input RREADY, - // output RVALID, - // output RDATA, - // output RRESP + output RVALID, + output RDATA, + output RRESP ); endinterface diff --git a/src/peakrdl_busdecoder/cpuif/apb3/apb3_cpuif_flat.py b/src/peakrdl_busdecoder/cpuif/apb3/apb3_cpuif_flat.py index 82804e3..576155a 100644 --- a/src/peakrdl_busdecoder/cpuif/apb3/apb3_cpuif_flat.py +++ b/src/peakrdl_busdecoder/cpuif/apb3/apb3_cpuif_flat.py @@ -12,14 +12,14 @@ class APB3CpuifFlat(BaseCpuif): return [ f"input logic {self.signal('PCLK', child)}", f"input logic {self.signal('PRESETn', child)}", - f"input logic {self.signal('PSELx', child)}", - f"input logic {self.signal('PENABLE', child)}", - f"input logic {self.signal('PWRITE', child)}", - f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", - f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}", - f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", - f"output logic {self.signal('PREADY', child)}", - f"output logic {self.signal('PSLVERR', child)}", + f"output logic {self.signal('PSELx', child)}", + f"output logic {self.signal('PENABLE', child)}", + f"output logic {self.signal('PWRITE', child)}", + f"output logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", + f"output logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}", + f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", + f"input logic {self.signal('PREADY', child)}", + f"input logic {self.signal('PSLVERR', child)}", ] @property @@ -48,9 +48,10 @@ class APB3CpuifFlat(BaseCpuif): node: AddressableNode | None = None, idx: str | int | None = None, ) -> str: + mapped_signal = "PSELx" if signal == "PSEL" else signal if node is None: # Node is none, so this is a slave signal - return f"s_apb_{signal}" + return f"s_apb_{mapped_signal}" # Master signal base = f"m_apb_{node.inst_name}" @@ -58,12 +59,12 @@ class APB3CpuifFlat(BaseCpuif): # Not an array or an unrolled element if node.current_idx is not None: # This is a specific instance of an unrolled array - return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}" - return f"{base}_{signal}" + return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}" + return f"{base}_{mapped_signal}" # Is an array if idx is not None: - return f"{base}_{signal}[{idx}]" - return f"{base}_{signal}[N_{node.inst_name.upper()}S]" + return f"{base}_{mapped_signal}[{idx}]" + return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]" def fanout(self, node: AddressableNode) -> str: fanout: dict[str, str] = {} diff --git a/src/peakrdl_busdecoder/cpuif/apb4/apb4_cpuif_flat.py b/src/peakrdl_busdecoder/cpuif/apb4/apb4_cpuif_flat.py index 4793ff8..8c6ec3c 100644 --- a/src/peakrdl_busdecoder/cpuif/apb4/apb4_cpuif_flat.py +++ b/src/peakrdl_busdecoder/cpuif/apb4/apb4_cpuif_flat.py @@ -12,16 +12,16 @@ class APB4CpuifFlat(BaseCpuif): return [ f"input logic {self.signal('PCLK', child)}", f"input logic {self.signal('PRESETn', child)}", - f"input logic {self.signal('PSELx', child)}", - f"input logic {self.signal('PENABLE', child)}", - f"input logic {self.signal('PWRITE', child)}", - f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", - f"input logic [2:0] {self.signal('PPROT', child)}", - f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}", - f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}", - f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", - f"output logic {self.signal('PREADY', child)}", - f"output logic {self.signal('PSLVERR', child)}", + f"output logic {self.signal('PSELx', child)}", + f"output logic {self.signal('PENABLE', child)}", + f"output logic {self.signal('PWRITE', child)}", + f"output logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}", + f"output logic [2:0] {self.signal('PPROT', child)}", + f"output logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}", + f"output logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}", + f"input logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}", + f"input logic {self.signal('PREADY', child)}", + f"input logic {self.signal('PSLVERR', child)}", ] @property @@ -52,9 +52,10 @@ class APB4CpuifFlat(BaseCpuif): node: AddressableNode | None = None, idx: str | int | None = None, ) -> str: + mapped_signal = "PSELx" if signal == "PSEL" else signal if node is None: # Node is none, so this is a slave signal - return f"s_apb_{signal}" + return f"s_apb_{mapped_signal}" # Master signal base = f"m_apb_{node.inst_name}" @@ -62,12 +63,12 @@ class APB4CpuifFlat(BaseCpuif): # Not an array or an unrolled element if node.current_idx is not None: # This is a specific instance of an unrolled array - return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}" - return f"{base}_{signal}" + return f"{base}_{mapped_signal}_{'_'.join(map(str, node.current_idx))}" + return f"{base}_{mapped_signal}" # Is an array if idx is not None: - return f"{base}_{signal}[{idx}]" - return f"{base}_{signal}[N_{node.inst_name.upper()}S]" + return f"{base}_{mapped_signal}[{idx}]" + return f"{base}_{mapped_signal}[N_{node.inst_name.upper()}S]" def fanout(self, node: AddressableNode) -> str: fanout: dict[str, str] = {} diff --git a/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif.py b/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif.py index dead15c..1745235 100644 --- a/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif.py +++ b/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif.py @@ -10,7 +10,7 @@ class AXI4LiteCpuif(BaseCpuif): template_path = "axi4lite_tmpl.sv" is_interface = True - def _port_declaration(self, child: AddressableNode) -> str: + def _port_declaration(self, child: AddressableNode) -> list[str]: base = f"axi4lite_intf.master m_axil_{child.inst_name}" # When unrolled, current_idx is set - append it to the name @@ -20,22 +20,25 @@ class AXI4LiteCpuif(BaseCpuif): # Only add array dimensions if this should be treated as an array if self.check_is_array(child): assert child.array_dimensions is not None - return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}" + return [f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"] - return base + return [base] @property def port_declaration(self) -> str: """Returns the port declaration for the AXI4-Lite interface.""" slave_ports: list[str] = ["axi4lite_intf.slave s_axil"] - master_ports: list[str] = list(map(self._port_declaration, self.addressable_children)) + + master_ports: list[str] = [] + for child in self.addressable_children: + master_ports.extend(self._port_declaration(child)) return ",\n".join(slave_ports + master_ports) @overload def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ... @overload - def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ... + def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ... def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: if node is None or indexer is None: # Node is none, so this is a slave signal diff --git a/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif_flat.py b/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif_flat.py index db22333..a8e83f1 100644 --- a/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif_flat.py +++ b/src/peakrdl_busdecoder/cpuif/axi4lite/axi4_lite_cpuif_flat.py @@ -2,94 +2,91 @@ from typing import overload from systemrdl.node import AddressableNode -from ...utils import get_indexed_path -from ..base_cpuif import BaseCpuif +from .axi4_lite_cpuif import AXI4LiteCpuif -class AXI4LiteCpuifFlat(BaseCpuif): +class AXI4LiteCpuifFlat(AXI4LiteCpuif): + """Verilator-friendly variant that flattens the AXI4-Lite interface ports.""" + template_path = "axi4lite_tmpl.sv" - is_interface = True + is_interface = False - def _port_declaration(self, child: AddressableNode) -> str: - base = f"axi4lite_intf.master m_axil_{child.inst_name}" - - # When unrolled, current_idx is set - append it to the name - if child.current_idx is not None: - base = f"{base}_{'_'.join(map(str, child.current_idx))}" - - # Only add array dimensions if this should be treated as an array - if self.check_is_array(child): - assert child.array_dimensions is not None - return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}" - - return base + def _port_declaration(self, child: AddressableNode) -> list[str]: + return [ + f"input logic {self.signal('AWREADY', child)}", + f"output logic {self.signal('AWVALID', child)}", + f"output logic [{self.addr_width - 1}:0] {self.signal('AWADDR', child)}", + f"output logic [2:0] {self.signal('AWPROT', child)}", + f"input logic {self.signal('WREADY', child)}", + f"output logic {self.signal('WVALID', child)}", + f"output logic [{self.data_width - 1}:0] {self.signal('WDATA', child)}", + f"output logic [{self.data_width_bytes - 1}:0] {self.signal('WSTRB', child)}", + f"output logic {self.signal('BREADY', child)}", + f"input logic {self.signal('BVALID', child)}", + f"input logic [1:0] {self.signal('BRESP', child)}", + f"input logic {self.signal('ARREADY', child)}", + f"output logic {self.signal('ARVALID', child)}", + f"output logic [{self.addr_width - 1}:0] {self.signal('ARADDR', child)}", + f"output logic [2:0] {self.signal('ARPROT', child)}", + f"output logic {self.signal('RREADY', child)}", + f"input logic {self.signal('RVALID', child)}", + f"input logic [{self.data_width - 1}:0] {self.signal('RDATA', child)}", + f"input logic [1:0] {self.signal('RRESP', child)}", + ] @property def port_declaration(self) -> str: - """Returns the port declaration for the AXI4-Lite interface.""" - slave_ports: list[str] = ["axi4lite_intf.slave s_axil"] - master_ports: list[str] = list(map(self._port_declaration, self.addressable_children)) + slave_ports: list[str] = [ + f"input logic {self.signal('ACLK')}", + f"input logic {self.signal('ARESETn')}", + f"output logic {self.signal('AWREADY')}", + f"input logic {self.signal('AWVALID')}", + f"input logic [{self.addr_width - 1}:0] {self.signal('AWADDR')}", + f"input logic [2:0] {self.signal('AWPROT')}", + f"output logic {self.signal('WREADY')}", + f"input logic {self.signal('WVALID')}", + f"input logic [{self.data_width - 1}:0] {self.signal('WDATA')}", + f"input logic [{self.data_width_bytes - 1}:0] {self.signal('WSTRB')}", + f"input logic {self.signal('BREADY')}", + f"output logic {self.signal('BVALID')}", + f"output logic [1:0] {self.signal('BRESP')}", + f"output logic {self.signal('ARREADY')}", + f"input logic {self.signal('ARVALID')}", + f"input logic [{self.addr_width - 1}:0] {self.signal('ARADDR')}", + f"input logic [2:0] {self.signal('ARPROT')}", + f"input logic {self.signal('RREADY')}", + f"output logic {self.signal('RVALID')}", + f"output logic [{self.data_width - 1}:0] {self.signal('RDATA')}", + f"output logic [1:0] {self.signal('RRESP')}", + ] + + master_ports: list[str] = [] + for child in self.addressable_children: + master_ports.extend(self._port_declaration(child)) return ",\n".join(slave_ports + master_ports) @overload def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ... + @overload - def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ... - def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: - if node is None or indexer is None: - # Node is none, so this is a slave signal - return f"s_axil.{signal}" + def signal(self, signal: str, node: AddressableNode, indexer: str | None = None) -> str: ... - # Master signal - return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}" - - def fanout(self, node: AddressableNode) -> str: - fanout: dict[str, str] = {} - - wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}" - rd_sel = f"cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}" - - # Write address channel - fanout[self.signal("AWVALID", node, "gi")] = wr_sel - fanout[self.signal("AWADDR", node, "gi")] = self.signal("AWADDR") - fanout[self.signal("AWPROT", node, "gi")] = self.signal("AWPROT") - - # Write data channel - fanout[self.signal("WVALID", node, "gi")] = wr_sel - fanout[self.signal("WDATA", node, "gi")] = "cpuif_wr_data" - fanout[self.signal("WSTRB", node, "gi")] = "cpuif_wr_byte_en" - - # Write response channel (master -> slave) - fanout[self.signal("BREADY", node, "gi")] = self.signal("BREADY") - - # Read address channel - fanout[self.signal("ARVALID", node, "gi")] = rd_sel - fanout[self.signal("ARADDR", node, "gi")] = self.signal("ARADDR") - fanout[self.signal("ARPROT", node, "gi")] = self.signal("ARPROT") - - # Read data channel (master -> slave) - fanout[self.signal("RREADY", node, "gi")] = self.signal("RREADY") - - return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items()) - - def fanin(self, node: AddressableNode | None = None) -> str: - fanin: dict[str, str] = {} + def signal( + self, + signal: str, + node: AddressableNode | None = None, + indexer: str | None = None, + ) -> str: if node is None: - fanin["cpuif_rd_ack"] = "'0" - fanin["cpuif_rd_err"] = "'0" - else: - # Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR) - fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i") - fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]" + return f"s_axil_{signal}" - return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items()) + base = f"m_axil_{node.inst_name}_{signal}" + if not self.check_is_array(node): + if node.current_idx is not None: + return f"{base}_{'_'.join(map(str, node.current_idx))}" + return base - def readback(self, node: AddressableNode | None = None) -> str: - fanin: dict[str, str] = {} - if node is None: - fanin["cpuif_rd_data"] = "'0" - else: - fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i") - - return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items()) + if indexer is None: + return f"{base}[N_{node.inst_name.upper()}S]" + return f"{base}[{indexer}]" diff --git a/src/peakrdl_busdecoder/decode_logic_gen.py b/src/peakrdl_busdecoder/decode_logic_gen.py index bffda3c..d221701 100644 --- a/src/peakrdl_busdecoder/decode_logic_gen.py +++ b/src/peakrdl_busdecoder/decode_logic_gen.py @@ -63,11 +63,16 @@ class DecodeLogicGenerator(BusDecoderListener): l_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})") u_bound_comp.append(f"({addr_width}'(i{i})*{SVInt(stride, addr_width)})") - # Generate Conditions - return [ - f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})", - f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})", - ] + lower_expr = f"{self._flavor.cpuif_address} >= ({'+'.join(l_bound_comp)})" + upper_expr = f"{self._flavor.cpuif_address} < ({'+'.join(u_bound_comp)})" + + predicates: list[str] = [] + # Avoid generating a redundant >= 0 comparison, which triggers Verilator warnings. + if not (l_bound.value == 0 and len(l_bound_comp) == 1): + predicates.append(lower_expr) + predicates.append(upper_expr) + + return predicates def cpuif_prot_predicate(self, node: AddressableNode) -> list[str]: if self._flavor == DecodeLogicFlavor.READ: diff --git a/src/peakrdl_busdecoder/module_tmpl.sv b/src/peakrdl_busdecoder/module_tmpl.sv index 2ee1829..8125ac6 100644 --- a/src/peakrdl_busdecoder/module_tmpl.sv +++ b/src/peakrdl_busdecoder/module_tmpl.sv @@ -44,7 +44,7 @@ module {{ds.module_name}} //-------------------------------------------------------------------------- // Slave <-> Internal CPUIF <-> Master //-------------------------------------------------------------------------- - {{-cpuif.get_implementation()|indent(4)}} + {{cpuif.get_implementation()|indent(4)}} //-------------------------------------------------------------------------- // Write Address Decoder diff --git a/tests/README.md b/tests/README.md index 6377092..7b42ebf 100644 --- a/tests/README.md +++ b/tests/README.md @@ -72,16 +72,28 @@ Run examples to see generated code for different configurations: python tests/cocotb/examples.py ``` +#### Simulation layout + +Simulation-oriented tests are grouped by CPU interface under +`tests/cocotb///`. For example, the APB4 smoke test lives in +`tests/cocotb/apb4/smoke/` alongside its pytest runner module. Each runner +compiles the appropriate SystemRDL design, adds the interface wrapper from +`hdl-src/`, and invokes cocotb via Verilator. + #### Full simulation tests (requires simulator) -To run the full cocotb simulation tests: +To execute the smoke tests for every supported interface: ```bash -# Run all cocotb simulation tests -pytest tests/cocotb/testbenches/test_*_runner.py -v +pytest tests/cocotb/*/smoke/test_runner.py -v +``` -# Run specific interface tests -pytest tests/cocotb/testbenches/test_apb4_runner.py -v +To target a single interface, point pytest at that runner module: + +```bash +pytest tests/cocotb/apb3/smoke/test_runner.py -v +pytest tests/cocotb/apb4/smoke/test_runner.py -v +pytest tests/cocotb/axi4lite/smoke/test_runner.py -v ``` For more information about cocotb tests, see [`tests/cocotb/README.md`](cocotb/README.md). diff --git a/tests/body/__init__.py b/tests/body/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/body/test_body.py b/tests/body/test_body.py new file mode 100644 index 0000000..623af29 --- /dev/null +++ b/tests/body/test_body.py @@ -0,0 +1,50 @@ + +from peakrdl_busdecoder.body import Body + + +class TestBody: + """Test the base Body class.""" + + def test_empty_body(self) -> None: + """Test empty body returns empty string.""" + body = Body() + assert str(body) == "" + assert not body # Should be falsy when empty + + def test_add_single_line(self) -> None: + """Test adding a single line to body.""" + body = Body() + body += "line1" + assert str(body) == "line1" + assert body # Should be truthy when not empty + + def test_add_multiple_lines(self) -> None: + """Test adding multiple lines to body.""" + body = Body() + body += "line1" + body += "line2" + body += "line3" + expected = "line1\nline2\nline3" + assert str(body) == expected + + def test_add_returns_self(self) -> None: + """Test that add operation returns self for chaining.""" + body = Body() + body += "line1" + body += "line2" + # Chaining works because += returns self + assert len(body.lines) == 2 + + def test_add_nested_body(self) -> None: + """Test adding another body as a line.""" + outer = Body() + inner = Body() + inner += "inner1" + inner += "inner2" + outer += "outer1" + outer += inner + outer += "outer2" + expected = "outer1\ninner1\ninner2\nouter2" + assert str(outer) == expected + + diff --git a/tests/body/test_combinational_body.py b/tests/body/test_combinational_body.py new file mode 100644 index 0000000..4fcb899 --- /dev/null +++ b/tests/body/test_combinational_body.py @@ -0,0 +1,39 @@ +from peakrdl_busdecoder.body import CombinationalBody, IfBody + + +class TestCombinationalBody: + """Test the CombinationalBody class.""" + + def test_simple_combinational_block(self) -> None: + """Test simple combinational block.""" + body = CombinationalBody() + body += "assign1 = value1;" + body += "assign2 = value2;" + + result = str(body) + assert "always_comb" in result + assert "begin" in result + assert "assign1 = value1;" in result + assert "assign2 = value2;" in result + assert "end" in result + + def test_empty_combinational_block(self) -> None: + """Test empty combinational block.""" + body = CombinationalBody() + result = str(body) + assert "always_comb" in result + assert "begin" in result + assert "end" in result + + def test_combinational_with_if_statement(self) -> None: + """Test combinational block with if statement.""" + cb = CombinationalBody() + ifb = IfBody() + with ifb.cm("condition") as b: + b += "assignment = value;" + cb += ifb + + result = str(cb) + assert "always_comb" in result + assert "if (condition)" in result + assert "assignment = value;" in result diff --git a/tests/body/test_for_loop_body.py b/tests/body/test_for_loop_body.py new file mode 100644 index 0000000..da7ff63 --- /dev/null +++ b/tests/body/test_for_loop_body.py @@ -0,0 +1,46 @@ +from peakrdl_busdecoder.body import ForLoopBody + + +class TestForLoopBody: + """Test the ForLoopBody class.""" + + def test_genvar_for_loop(self) -> None: + """Test genvar-style for loop.""" + body = ForLoopBody("genvar", "i", 4) + body += "statement1;" + body += "statement2;" + + result = str(body) + assert "for (genvar i = 0; i < 4; i++)" in result + assert "statement1;" in result + assert "statement2;" in result + assert "end" in result + + def test_int_for_loop(self) -> None: + """Test int-style for loop.""" + body = ForLoopBody("int", "j", 8) + body += "assignment = value;" + + result = str(body) + assert "for (int j = 0; j < 8; j++)" in result + assert "assignment = value;" in result + assert "end" in result + + def test_empty_for_loop(self) -> None: + """Test empty for loop.""" + body = ForLoopBody("genvar", "k", 2) + result = str(body) + # Empty for loop should still have structure + assert "for (genvar k = 0; k < 2; k++)" in result + + def test_nested_for_loops(self) -> None: + """Test nested for loops.""" + outer = ForLoopBody("genvar", "i", 3) + inner = ForLoopBody("genvar", "j", 2) + inner += "nested_statement;" + outer += inner + + result = str(outer) + assert "for (genvar i = 0; i < 3; i++)" in result + assert "for (genvar j = 0; j < 2; j++)" in result + assert "nested_statement;" in result diff --git a/tests/body/test_if_body.py b/tests/body/test_if_body.py new file mode 100644 index 0000000..d02c9a9 --- /dev/null +++ b/tests/body/test_if_body.py @@ -0,0 +1,85 @@ +from peakrdl_busdecoder.body import IfBody +class TestIfBody: + """Test the IfBody class.""" + + def test_simple_if(self): + """Test simple if statement.""" + body = IfBody() + with body.cm("condition1") as b: + b += "statement1;" + + result = str(body) + assert "if (condition1)" in result + assert "statement1;" in result + assert "end" in result + + def test_if_else(self): + """Test if-else statement.""" + body = IfBody() + with body.cm("condition1") as b: + b += "if_statement;" + with body.cm(None) as b: # None for else + b += "else_statement;" + + result = str(body) + assert "if (condition1)" in result + assert "if_statement;" in result + assert "else" in result + assert "else_statement;" in result + + def test_if_elif_else(self): + """Test if-elif-else chain.""" + body = IfBody() + with body.cm("condition1") as b: + b += "statement1;" + with body.cm("condition2") as b: + b += "statement2;" + with body.cm(None) as b: # None for else + b += "statement3;" + + result = str(body) + assert "if (condition1)" in result + assert "statement1;" in result + assert "else if (condition2)" in result + assert "statement2;" in result + assert "else" in result + assert "statement3;" in result + + def test_multiple_elif(self): + """Test multiple elif statements.""" + body = IfBody() + with body.cm("cond1") as b: + b += "stmt1;" + with body.cm("cond2") as b: + b += "stmt2;" + with body.cm("cond3") as b: + b += "stmt3;" + + result = str(body) + assert "if (cond1)" in result + assert "else if (cond2)" in result + assert "else if (cond3)" in result + + def test_empty_if_branches(self): + """Test if statement with empty branches.""" + body = IfBody() + with body.cm("condition"): + pass + + result = str(body) + assert "if (condition)" in result + + def test_nested_if(self): + """Test nested if statements.""" + outer = IfBody() + with outer.cm("outer_cond") as outer_body: + inner = IfBody() + with inner.cm("inner_cond") as inner_body: + inner_body += "nested_statement;" + outer_body += inner + + result = str(outer) + assert "if (outer_cond)" in result + assert "if (inner_cond)" in result + assert "nested_statement;" in result + diff --git a/tests/body/test_struct_body.py b/tests/body/test_struct_body.py new file mode 100644 index 0000000..594f2db --- /dev/null +++ b/tests/body/test_struct_body.py @@ -0,0 +1,59 @@ +from peakrdl_busdecoder.body import StructBody + + +class TestStructBody: + """Test the StructBody class.""" + + def test_simple_struct(self) -> None: + """Test simple struct definition.""" + body = StructBody("my_struct_t", packed=True, typedef=True) + body += "logic [7:0] field1;" + body += "logic field2;" + + result = str(body) + assert "typedef struct packed" in result + assert "my_struct_t" in result + assert "logic [7:0] field1;" in result + assert "logic field2;" in result + + def test_unpacked_struct(self) -> None: + """Test unpacked struct definition.""" + body = StructBody("unpacked_t", packed=False, typedef=True) + body += "int field1;" + + result = str(body) + assert "typedef struct" in result + assert "packed" not in result or "typedef struct {" in result + assert "unpacked_t" in result + + def test_struct_without_typedef(self) -> None: + """Test struct without typedef.""" + body = StructBody("my_struct", packed=True, typedef=False) + body += "logic field;" + + result = str(body) + # When typedef=False, packed is not used + assert "struct {" in result + assert "typedef" not in result + assert "my_struct" in result + + def test_empty_struct(self) -> None: + """Test empty struct.""" + body = StructBody("empty_t", packed=True, typedef=True) + result = str(body) + assert "typedef struct packed" in result + assert "empty_t" in result + + def test_nested_struct(self) -> None: + """Test struct with nested struct.""" + outer = StructBody("outer_t", packed=True, typedef=True) + inner = StructBody("inner_t", packed=True, typedef=True) + inner += "logic field1;" + outer += "logic field2;" + outer += str(inner) # Include inner struct as a string + + result = str(outer) + assert "outer_t" in result + assert "field2;" in result + # Inner struct should appear in the string + assert "inner_t" in result diff --git a/tests/cocotb/IMPLEMENTATION_SUMMARY.md b/tests/cocotb/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index fb8cfb9..0000000 --- a/tests/cocotb/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,142 +0,0 @@ -# Cocotb Testbench Implementation Summary - -## Overview - -This implementation adds comprehensive cocotb-based testbenches for validating generated SystemVerilog bus decoder RTL across multiple CPU interface types (APB3, APB4, and AXI4-Lite). - -## Files Added - -### Test Infrastructure -- **pyproject.toml** - Added cocotb-test dependency group -- **tests/cocotb/common/utils.py** - Utilities for RDL compilation and code generation -- **tests/cocotb/common/apb4_master.py** - APB4 Bus Functional Model -- **tests/cocotb/Makefile.common** - Makefile template for cocotb simulations - -### Testbenches -- **tests/cocotb/testbenches/test_apb4_decoder.py** - APB4 interface tests (3 test cases) - - test_simple_read_write: Basic read/write operations - - test_multiple_registers: Multiple register access - - test_byte_strobe: Byte strobe functionality - -- **tests/cocotb/testbenches/test_apb3_decoder.py** - APB3 interface tests (2 test cases) - - test_simple_read_write: Basic read/write operations - - test_multiple_registers: Multiple register access - -- **tests/cocotb/testbenches/test_axi4lite_decoder.py** - AXI4-Lite interface tests (3 test cases) - - test_simple_read_write: Basic read/write operations - - test_multiple_registers: Multiple register access - - test_byte_strobe: Byte strobe functionality - -- **tests/cocotb/testbenches/test_apb4_runner.py** - Pytest wrapper for running APB4 tests - -- **tests/cocotb/testbenches/test_integration.py** - Integration tests (9 test cases) - - Tests code generation for all three interfaces - - Tests utility functions - - Validates generated code structure - -### Documentation & Examples -- **tests/cocotb/README.md** - Comprehensive cocotb test documentation -- **tests/cocotb/examples.py** - Example script demonstrating code generation -- **tests/README.md** - Updated with cocotb test instructions - -## Features - -### Bus Functional Models (BFMs) -Each CPU interface has both master and slave BFMs: -- **APB4Master/APB4SlaveResponder**: Full APB4 protocol with PSTRB support -- **APB3Master/APB3SlaveResponder**: APB3 protocol without PSTRB -- **AXI4LiteMaster/AXI4LiteSlaveResponder**: Full AXI4-Lite protocol with separate channels - -### Test Coverage -1. **Simple read/write operations**: Verify basic decoder functionality -2. **Multiple registers**: Test address decoding for multiple targets -3. **Register arrays**: Validate array handling -4. **Byte strobes**: Test partial word writes (APB4, AXI4-Lite) -5. **Nested address maps**: Validate hierarchical structures - -### Code Generation Tests -The integration tests validate: -- Code generation for all three CPU interfaces -- Custom module/package naming -- Register arrays -- Nested address maps -- Generated code structure and content - -## How to Run - -### Integration Tests (No Simulator Required) -```bash -pytest tests/cocotb/testbenches/test_integration.py -v -``` - -### Example Script -```bash -python -m tests.cocotb.examples -``` - -### Full Simulation Tests (Requires Simulator) -```bash -# Install simulator first -apt-get install iverilog # or verilator - -# Install cocotb -uv sync --group cocotb-test - -# Run tests (when simulator available) -pytest tests/cocotb/testbenches/test_*_runner.py -v -``` - -## Test Results - -### ✅ Integration Tests: 9/9 Passing -- test_apb4_simple_register -- test_apb3_multiple_registers -- test_axi4lite_nested_addrmap -- test_register_array -- test_get_verilog_sources -- test_compile_rdl_and_export_with_custom_names -- test_cpuif_generation[APB3Cpuif-apb3_intf] -- test_cpuif_generation[APB4Cpuif-apb4_intf] -- test_cpuif_generation[AXI4LiteCpuif-axi4lite_intf] - -### ✅ Existing Unit Tests: 56/56 Passing -- No regressions introduced -- 4 pre-existing failures in test_unroll.py remain unchanged - -### ✅ Security Checks -- No vulnerabilities found in new dependencies (cocotb, cocotb-bus) -- CodeQL analysis: 0 alerts - -## Design Decisions - -1. **Separate integration tests**: Created tests that run without a simulator for CI/CD friendliness -2. **Relative imports**: Used proper Python package structure instead of sys.path manipulation -3. **Multiple CPU interfaces**: Comprehensive coverage of all supported interfaces -4. **BFM architecture**: Reusable Bus Functional Models for each protocol -5. **Example script**: Provides easy-to-run demonstrations of code generation - -## Future Enhancements - -1. **CI Integration**: Add optional CI job with simulator for full cocotb tests -2. **More test scenarios**: Coverage tests, error handling, corner cases -3. **Avalon MM support**: Add testbenches for Avalon Memory-Mapped interface -4. **Waveform verification**: Automated protocol compliance checking -5. **Performance tests**: Bus utilization and throughput testing - -## Dependencies - -### Required -- peakrdl-busdecoder (existing) -- systemrdl-compiler (existing) - -### Optional (for simulation) -- cocotb >= 1.8.0 -- cocotb-bus >= 0.2.1 -- iverilog or verilator (HDL simulator) - -## Notes - -- Integration tests run in CI without requiring a simulator -- Full simulation tests are marked with pytest.skip when simulator not available -- All code follows project conventions (ruff formatting, type hints) -- Documentation includes both uv (project standard) and pip alternatives diff --git a/tests/cocotb/Makefile.common b/tests/cocotb/Makefile.common deleted file mode 100644 index c097ad3..0000000 --- a/tests/cocotb/Makefile.common +++ /dev/null @@ -1,39 +0,0 @@ -# Makefile for cocotb simulation -# This makefile can be used to run cocotb tests with Icarus Verilog or other simulators - -# Defaults -SIM ?= icarus -TOPLEVEL_LANG ?= verilog - -# Project paths -REPO_ROOT := $(shell git rev-parse --show-toplevel) -HDL_SRC_DIR := $(REPO_ROOT)/hdl-src -TEST_DIR := $(REPO_ROOT)/tests/cocotb -COMMON_DIR := $(TEST_DIR)/common - -# Python paths -export PYTHONPATH := $(REPO_ROOT):$(TEST_DIR):$(PYTHONPATH) - -# Simulator options -COMPILE_ARGS += -g2012 # SystemVerilog 2012 -EXTRA_ARGS += --trace --trace-structs - -# Common sources (interfaces) -VERILOG_SOURCES += $(HDL_SRC_DIR)/apb4_intf.sv -VERILOG_SOURCES += $(HDL_SRC_DIR)/apb3_intf.sv -VERILOG_SOURCES += $(HDL_SRC_DIR)/axi4lite_intf.sv - -# Test-specific configuration -# These should be overridden by test-specific makefiles - -# Example usage: -# To run APB4 test: -# make -f Makefile.apb4 test_simple -# -# To run all tests: -# make -f Makefile.apb4 test -# make -f Makefile.apb3 test -# make -f Makefile.axi4lite test - -# Include cocotb's make rules to build/run simulation -include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/tests/cocotb/README.md b/tests/cocotb/README.md deleted file mode 100644 index 19fd010..0000000 --- a/tests/cocotb/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Cocotb Integration Tests - -This directory contains cocotb-based integration tests that verify the functionality -of generated bus decoder RTL for different CPU interfaces. - -## Overview - -These tests: -1. Generate SystemVerilog decoder modules from SystemRDL specifications -2. Simulate the generated RTL using cocotb -3. Verify read/write operations work correctly for different bus protocols - -## Supported CPU Interfaces - -- APB3 (AMBA APB3) -- APB4 (AMBA APB4 with strobe support) -- AXI4-Lite (AMBA AXI4-Lite) - -## Running the Tests - -### Install Dependencies - -```bash -# Using uv (recommended) -uv sync --group test - -# Or using pip -pip install -e .[test] -``` - -### Run All Cocotb Tests - -```bash -pytest tests/cocotb/testbenches/ -``` - -### Run Specific Interface Tests - -```bash -# Test APB4 interface -pytest tests/cocotb/testbenches/test_apb4_decoder.py - -# Test APB3 interface -pytest tests/cocotb/testbenches/test_apb3_decoder.py - -# Test AXI4-Lite interface -pytest tests/cocotb/testbenches/test_axi4lite_decoder.py -``` - -## Test Structure - -- `common/`: Shared utilities and base classes for cocotb tests -- `testbenches/`: Individual testbenches for each CPU interface diff --git a/tests/cocotb/__init__.py b/tests/cocotb/__init__.py index 01550ed..e69de29 100644 --- a/tests/cocotb/__init__.py +++ b/tests/cocotb/__init__.py @@ -1 +0,0 @@ -"""Cocotb tests package.""" diff --git a/tests/cocotb/apb3/__init__.py b/tests/cocotb/apb3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/apb3/smoke/__init__.py b/tests/cocotb/apb3/smoke/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/apb3/smoke/test_register_access.py b/tests/cocotb/apb3/smoke/test_register_access.py new file mode 100644 index 0000000..e64bf94 --- /dev/null +++ b/tests/cocotb/apb3/smoke/test_register_access.py @@ -0,0 +1,123 @@ +"""APB3 smoke tests for generated multi-register design.""" + +import cocotb +from cocotb.triggers import Timer + +WRITE_ADDR = 0x0 +READ_ADDR = 0x8 +WRITE_DATA = 0xCAFEBABE +READ_DATA = 0x0BAD_F00D + + +class _Apb3SlaveShim: + def __init__(self, dut): + prefix = "s_apb" + self.PSEL = getattr(dut, f"{prefix}_PSELx") + self.PENABLE = getattr(dut, f"{prefix}_PENABLE") + self.PWRITE = getattr(dut, f"{prefix}_PWRITE") + self.PADDR = getattr(dut, f"{prefix}_PADDR") + self.PWDATA = getattr(dut, f"{prefix}_PWDATA") + self.PRDATA = getattr(dut, f"{prefix}_PRDATA") + self.PREADY = getattr(dut, f"{prefix}_PREADY") + self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") + + +class _Apb3MasterShim: + def __init__(self, dut, base: str): + self.PSEL = getattr(dut, f"{base}_PSELx") + self.PENABLE = getattr(dut, f"{base}_PENABLE") + self.PWRITE = getattr(dut, f"{base}_PWRITE") + self.PADDR = getattr(dut, f"{base}_PADDR") + self.PWDATA = getattr(dut, f"{base}_PWDATA") + self.PRDATA = getattr(dut, f"{base}_PRDATA") + self.PREADY = getattr(dut, f"{base}_PREADY") + self.PSLVERR = getattr(dut, f"{base}_PSLVERR") + + +def _apb3_slave(dut): + return getattr(dut, "s_apb", None) or _Apb3SlaveShim(dut) + + +def _apb3_master(dut, base: str): + return getattr(dut, base, None) or _Apb3MasterShim(dut, base) + + +@cocotb.test() +async def test_apb3_read_write_paths(dut): + """Exercise APB3 slave interface and observe master fanout.""" + s_apb = _apb3_slave(dut) + masters = { + "reg1": _apb3_master(dut, "m_apb_reg1"), + "reg2": _apb3_master(dut, "m_apb_reg2"), + "reg3": _apb3_master(dut, "m_apb_reg3"), + } + + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + s_apb.PWRITE.value = 0 + s_apb.PADDR.value = 0 + s_apb.PWDATA.value = 0 + + for master in masters.values(): + master.PRDATA.value = 0 + master.PREADY.value = 0 + master.PSLVERR.value = 0 + + await Timer(1, units="ns") + + # Write to reg1 + masters["reg1"].PREADY.value = 1 + s_apb.PADDR.value = WRITE_ADDR + s_apb.PWDATA.value = WRITE_DATA + s_apb.PWRITE.value = 1 + s_apb.PSEL.value = 1 + s_apb.PENABLE.value = 1 + + await Timer(1, units="ns") + + assert int(masters["reg1"].PSEL.value) == 1, "reg1 should be selected for write" + assert int(masters["reg1"].PWRITE.value) == 1, "Write should propagate to master" + assert int(masters["reg1"].PADDR.value) == WRITE_ADDR, "Address should reach selected master" + assert int(masters["reg1"].PWDATA.value) == WRITE_DATA, "Write data should fan out" + + for name, master in masters.items(): + if name != "reg1": + assert int(master.PSEL.value) == 0, f"{name} must idle during reg1 write" + + assert int(s_apb.PREADY.value) == 1, "Ready must reflect selected master" + assert int(s_apb.PSLVERR.value) == 0, "Write should not signal error" + + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + s_apb.PWRITE.value = 0 + masters["reg1"].PREADY.value = 0 + await Timer(1, units="ns") + + # Read from reg3 + masters["reg3"].PRDATA.value = READ_DATA + masters["reg3"].PREADY.value = 1 + masters["reg3"].PSLVERR.value = 0 + + s_apb.PADDR.value = READ_ADDR + s_apb.PSEL.value = 1 + s_apb.PENABLE.value = 1 + s_apb.PWRITE.value = 0 + + await Timer(1, units="ns") + + assert int(masters["reg3"].PSEL.value) == 1, "reg3 should be selected for read" + assert int(masters["reg3"].PWRITE.value) == 0, "Read should clear write" + assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Address should reach read target" + + for name, master in masters.items(): + if name != "reg3": + assert int(master.PSEL.value) == 0, f"{name} must idle during reg3 read" + + assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return to slave" + assert int(s_apb.PREADY.value) == 1, "Read should acknowledge" + assert int(s_apb.PSLVERR.value) == 0, "Read should not signal error" + + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + masters["reg3"].PREADY.value = 0 + await Timer(1, units="ns") diff --git a/tests/cocotb/apb3/smoke/test_runner.py b/tests/cocotb/apb3/smoke/test_runner.py new file mode 100644 index 0000000..210338e --- /dev/null +++ b/tests/cocotb/apb3/smoke/test_runner.py @@ -0,0 +1,50 @@ +"""Pytest wrapper launching the APB3 cocotb smoke test.""" + +from pathlib import Path + +import pytest + +from peakrdl_busdecoder.cpuif.apb3.apb3_cpuif_flat import APB3CpuifFlat + +try: # pragma: no cover - optional dependency shim + from cocotb.runner import get_runner +except ImportError: # pragma: no cover + from cocotb_tools.runner import get_runner + +from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources + + +@pytest.mark.simulation +@pytest.mark.verilator +def test_apb3_smoke(tmp_path: Path) -> None: + """Compile the APB3 design and execute the cocotb smoke test.""" + repo_root = Path(__file__).resolve().parents[4] + + module_path, package_path = compile_rdl_and_export( + str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), + "multi_reg", + tmp_path, + cpuif_cls=APB3CpuifFlat, + ) + + sources = get_verilog_sources( + module_path, + package_path, + [repo_root / "hdl-src" / "apb3_intf.sv"], + ) + + runner = get_runner("verilator") + build_dir = tmp_path / "sim_build" + + runner.build( + sources=sources, + hdl_toplevel=module_path.stem, + build_dir=build_dir, + ) + + runner.test( + hdl_toplevel=module_path.stem, + test_module="tests.cocotb.apb3.smoke.test_register_access", + build_dir=build_dir, + log_file=str(tmp_path / "sim.log"), + ) diff --git a/tests/cocotb/apb4/__init__.py b/tests/cocotb/apb4/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/apb4/smoke/__init__.py b/tests/cocotb/apb4/smoke/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/apb4/smoke/test_register_access.py b/tests/cocotb/apb4/smoke/test_register_access.py new file mode 100644 index 0000000..abb2a1e --- /dev/null +++ b/tests/cocotb/apb4/smoke/test_register_access.py @@ -0,0 +1,138 @@ +"""APB4 smoke tests using generated multi-register design.""" + +import cocotb +from cocotb.triggers import Timer + +WRITE_ADDR = 0x4 +READ_ADDR = 0x8 +WRITE_DATA = 0x1234_5678 +READ_DATA = 0x89AB_CDEF + + +class _Apb4SlaveShim: + def __init__(self, dut): + prefix = "s_apb" + self.PSEL = getattr(dut, f"{prefix}_PSELx") + self.PENABLE = getattr(dut, f"{prefix}_PENABLE") + self.PWRITE = getattr(dut, f"{prefix}_PWRITE") + self.PADDR = getattr(dut, f"{prefix}_PADDR") + self.PPROT = getattr(dut, f"{prefix}_PPROT") + self.PWDATA = getattr(dut, f"{prefix}_PWDATA") + self.PSTRB = getattr(dut, f"{prefix}_PSTRB") + self.PRDATA = getattr(dut, f"{prefix}_PRDATA") + self.PREADY = getattr(dut, f"{prefix}_PREADY") + self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") + + +class _Apb4MasterShim: + def __init__(self, dut, base: str): + self.PSEL = getattr(dut, f"{base}_PSELx") + self.PENABLE = getattr(dut, f"{base}_PENABLE") + self.PWRITE = getattr(dut, f"{base}_PWRITE") + self.PADDR = getattr(dut, f"{base}_PADDR") + self.PPROT = getattr(dut, f"{base}_PPROT") + self.PWDATA = getattr(dut, f"{base}_PWDATA") + self.PSTRB = getattr(dut, f"{base}_PSTRB") + self.PRDATA = getattr(dut, f"{base}_PRDATA") + self.PREADY = getattr(dut, f"{base}_PREADY") + self.PSLVERR = getattr(dut, f"{base}_PSLVERR") + + +def _apb4_slave(dut): + return getattr(dut, "s_apb", None) or _Apb4SlaveShim(dut) + + +def _apb4_master(dut, base: str): + return getattr(dut, base, None) or _Apb4MasterShim(dut, base) + + +@cocotb.test() +async def test_apb4_read_write_paths(dut): + """Drive APB4 slave signals and observe master activity.""" + s_apb = _apb4_slave(dut) + masters = { + "reg1": _apb4_master(dut, "m_apb_reg1"), + "reg2": _apb4_master(dut, "m_apb_reg2"), + "reg3": _apb4_master(dut, "m_apb_reg3"), + } + + # Default slave side inputs + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + s_apb.PWRITE.value = 0 + s_apb.PADDR.value = 0 + s_apb.PWDATA.value = 0 + s_apb.PPROT.value = 0 + s_apb.PSTRB.value = 0 + + for master in masters.values(): + master.PRDATA.value = 0 + master.PREADY.value = 0 + master.PSLVERR.value = 0 + + await Timer(1, units="ns") + + # ------------------------------------------------------------------ + # Write transfer to reg2 + # ------------------------------------------------------------------ + masters["reg2"].PREADY.value = 1 + s_apb.PADDR.value = WRITE_ADDR + s_apb.PWDATA.value = WRITE_DATA + s_apb.PSTRB.value = 0xF + s_apb.PPROT.value = 0 + s_apb.PWRITE.value = 1 + s_apb.PSEL.value = 1 + s_apb.PENABLE.value = 1 + + await Timer(1, units="ns") + + assert int(masters["reg2"].PSEL.value) == 1, "reg2 must be selected for write" + assert int(masters["reg2"].PWRITE.value) == 1, "Write strobes should propagate" + assert int(masters["reg2"].PADDR.value) == WRITE_ADDR, "Address should fan out" + assert int(masters["reg2"].PWDATA.value) == WRITE_DATA, "Write data should fan out" + + for name, master in masters.items(): + if name != "reg2": + assert int(master.PSEL.value) == 0, f"{name} should remain idle on write" + + assert int(s_apb.PREADY.value) == 1, "Ready should mirror selected master" + assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful write" + + # Return to idle + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + s_apb.PWRITE.value = 0 + masters["reg2"].PREADY.value = 0 + await Timer(1, units="ns") + + # ------------------------------------------------------------------ + # Read transfer from reg3 + # ------------------------------------------------------------------ + masters["reg3"].PRDATA.value = READ_DATA + masters["reg3"].PREADY.value = 1 + masters["reg3"].PSLVERR.value = 0 + + s_apb.PADDR.value = READ_ADDR + s_apb.PSEL.value = 1 + s_apb.PENABLE.value = 1 + s_apb.PWRITE.value = 0 + + await Timer(1, units="ns") + + assert int(masters["reg3"].PSEL.value) == 1, "reg3 must be selected for read" + assert int(masters["reg3"].PWRITE.value) == 0, "Read should deassert write" + assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Read address should propagate" + + for name, master in masters.items(): + if name != "reg3": + assert int(master.PSEL.value) == 0, f"{name} should remain idle on read" + + assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return from master" + assert int(s_apb.PREADY.value) == 1, "Ready must follow selected master" + assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful read" + + # Back to idle + s_apb.PSEL.value = 0 + s_apb.PENABLE.value = 0 + masters["reg3"].PREADY.value = 0 + await Timer(1, units="ns") diff --git a/tests/cocotb/apb4/smoke/test_runner.py b/tests/cocotb/apb4/smoke/test_runner.py new file mode 100644 index 0000000..69f4003 --- /dev/null +++ b/tests/cocotb/apb4/smoke/test_runner.py @@ -0,0 +1,50 @@ +"""Pytest wrapper launching the APB4 cocotb smoke test.""" + +from pathlib import Path + +import pytest + +from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat + +try: # pragma: no cover - optional dependency shim + from cocotb.runner import get_runner +except ImportError: # pragma: no cover + from cocotb_tools.runner import get_runner + +from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources + + +@pytest.mark.simulation +@pytest.mark.verilator +def test_apb4_smoke(tmp_path: Path) -> None: + """Compile the APB4 design and execute the cocotb smoke test.""" + repo_root = Path(__file__).resolve().parents[4] + + module_path, package_path = compile_rdl_and_export( + str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), + "multi_reg", + tmp_path, + cpuif_cls=APB4CpuifFlat, + ) + + sources = get_verilog_sources( + module_path, + package_path, + [repo_root / "hdl-src" / "apb4_intf.sv"], + ) + + runner = get_runner("verilator") + build_dir = tmp_path / "sim_build" + + runner.build( + sources=sources, + hdl_toplevel=module_path.stem, + build_dir=build_dir, + ) + + runner.test( + hdl_toplevel=module_path.stem, + test_module="tests.cocotb.apb4.smoke.test_register_access", + build_dir=build_dir, + log_file=str(tmp_path / "sim.log"), + ) diff --git a/tests/cocotb/axi4lite/__init__.py b/tests/cocotb/axi4lite/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/axi4lite/smoke/__init__.py b/tests/cocotb/axi4lite/smoke/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/cocotb/axi4lite/smoke/test_register_access.py b/tests/cocotb/axi4lite/smoke/test_register_access.py new file mode 100644 index 0000000..d9f20bb --- /dev/null +++ b/tests/cocotb/axi4lite/smoke/test_register_access.py @@ -0,0 +1,161 @@ +"""AXI4-Lite smoke test ensuring address decode fanout works.""" + +import cocotb +from cocotb.triggers import Timer + +WRITE_ADDR = 0x4 +READ_ADDR = 0x8 +WRITE_DATA = 0x1357_9BDF +READ_DATA = 0x2468_ACED + + +class _AxilSlaveShim: + def __init__(self, dut): + prefix = "s_axil" + self.AWREADY = getattr(dut, f"{prefix}_AWREADY") + self.AWVALID = getattr(dut, f"{prefix}_AWVALID") + self.AWADDR = getattr(dut, f"{prefix}_AWADDR") + self.AWPROT = getattr(dut, f"{prefix}_AWPROT") + self.WREADY = getattr(dut, f"{prefix}_WREADY") + self.WVALID = getattr(dut, f"{prefix}_WVALID") + self.WDATA = getattr(dut, f"{prefix}_WDATA") + self.WSTRB = getattr(dut, f"{prefix}_WSTRB") + self.BREADY = getattr(dut, f"{prefix}_BREADY") + self.BVALID = getattr(dut, f"{prefix}_BVALID") + self.BRESP = getattr(dut, f"{prefix}_BRESP") + self.ARREADY = getattr(dut, f"{prefix}_ARREADY") + self.ARVALID = getattr(dut, f"{prefix}_ARVALID") + self.ARADDR = getattr(dut, f"{prefix}_ARADDR") + self.ARPROT = getattr(dut, f"{prefix}_ARPROT") + self.RREADY = getattr(dut, f"{prefix}_RREADY") + self.RVALID = getattr(dut, f"{prefix}_RVALID") + self.RDATA = getattr(dut, f"{prefix}_RDATA") + self.RRESP = getattr(dut, f"{prefix}_RRESP") + + +class _AxilMasterShim: + def __init__(self, dut, base: str): + self.AWREADY = getattr(dut, f"{base}_AWREADY") + self.AWVALID = getattr(dut, f"{base}_AWVALID") + self.AWADDR = getattr(dut, f"{base}_AWADDR") + self.AWPROT = getattr(dut, f"{base}_AWPROT") + self.WREADY = getattr(dut, f"{base}_WREADY") + self.WVALID = getattr(dut, f"{base}_WVALID") + self.WDATA = getattr(dut, f"{base}_WDATA") + self.WSTRB = getattr(dut, f"{base}_WSTRB") + self.BREADY = getattr(dut, f"{base}_BREADY") + self.BVALID = getattr(dut, f"{base}_BVALID") + self.BRESP = getattr(dut, f"{base}_BRESP") + self.ARREADY = getattr(dut, f"{base}_ARREADY") + self.ARVALID = getattr(dut, f"{base}_ARVALID") + self.ARADDR = getattr(dut, f"{base}_ARADDR") + self.ARPROT = getattr(dut, f"{base}_ARPROT") + self.RREADY = getattr(dut, f"{base}_RREADY") + self.RVALID = getattr(dut, f"{base}_RVALID") + self.RDATA = getattr(dut, f"{base}_RDATA") + self.RRESP = getattr(dut, f"{base}_RRESP") + + +def _axil_slave(dut): + return getattr(dut, "s_axil", None) or _AxilSlaveShim(dut) + + +def _axil_master(dut, base: str): + return getattr(dut, base, None) or _AxilMasterShim(dut, base) + + +@cocotb.test() +async def test_axi4lite_read_write_paths(dut): + """Drive AXI4-Lite slave channels and validate master side wiring.""" + s_axil = _axil_slave(dut) + masters = { + "reg1": _axil_master(dut, "m_axil_reg1"), + "reg2": _axil_master(dut, "m_axil_reg2"), + "reg3": _axil_master(dut, "m_axil_reg3"), + } + + # Default slave-side inputs + s_axil.AWVALID.value = 0 + s_axil.AWADDR.value = 0 + s_axil.AWPROT.value = 0 + s_axil.WVALID.value = 0 + s_axil.WDATA.value = 0 + s_axil.WSTRB.value = 0 + s_axil.BREADY.value = 0 + s_axil.ARVALID.value = 0 + s_axil.ARADDR.value = 0 + s_axil.ARPROT.value = 0 + s_axil.RREADY.value = 0 + + for master in masters.values(): + master.AWREADY.value = 0 + master.WREADY.value = 0 + master.BVALID.value = 0 + master.BRESP.value = 0 + master.ARREADY.value = 0 + master.RVALID.value = 0 + master.RDATA.value = 0 + master.RRESP.value = 0 + + await Timer(1, units="ns") + + # -------------------------------------------------------------- + # Write transaction targeting reg2 + # -------------------------------------------------------------- + s_axil.AWADDR.value = WRITE_ADDR + s_axil.AWPROT.value = 0 + s_axil.AWVALID.value = 1 + s_axil.WDATA.value = WRITE_DATA + s_axil.WSTRB.value = 0xF + s_axil.WVALID.value = 1 + s_axil.BREADY.value = 1 + + await Timer(1, units="ns") + + assert int(masters["reg2"].AWVALID.value) == 1, "reg2 AWVALID should follow slave" + assert int(masters["reg2"].WVALID.value) == 1, "reg2 WVALID should follow slave" + assert int(masters["reg2"].AWADDR.value) == WRITE_ADDR, "AWADDR should fan out" + assert int(masters["reg2"].WDATA.value) == WRITE_DATA, "WDATA should fan out" + assert int(masters["reg2"].WSTRB.value) == 0xF, "WSTRB should propagate" + + for name, master in masters.items(): + if name != "reg2": + assert int(master.AWVALID.value) == 0, f"{name} AWVALID should stay low" + assert int(master.WVALID.value) == 0, f"{name} WVALID should stay low" + + # Release write channel + s_axil.AWVALID.value = 0 + s_axil.WVALID.value = 0 + s_axil.BREADY.value = 0 + await Timer(1, units="ns") + + # -------------------------------------------------------------- + # Read transaction targeting reg3 + # -------------------------------------------------------------- + masters["reg3"].RVALID.value = 1 + masters["reg3"].RDATA.value = READ_DATA + masters["reg3"].RRESP.value = 0 + + s_axil.ARADDR.value = READ_ADDR + s_axil.ARPROT.value = 0 + s_axil.ARVALID.value = 1 + s_axil.RREADY.value = 1 + + await Timer(1, units="ns") + + assert int(masters["reg3"].ARVALID.value) == 1, "reg3 ARVALID should follow slave" + assert int(masters["reg3"].ARADDR.value) == READ_ADDR, "ARADDR should fan out" + + for name, master in masters.items(): + if name != "reg3": + assert int(master.ARVALID.value) == 0, f"{name} ARVALID should stay low" + + assert int(s_axil.RVALID.value) == 1, "Slave should raise RVALID when master responds" + assert int(s_axil.RDATA.value) == READ_DATA, "Read data should return to slave" + assert int(s_axil.RRESP.value) == 0, "No error expected for read" + + # Return to idle + s_axil.ARVALID.value = 0 + s_axil.RREADY.value = 0 + masters["reg3"].RVALID.value = 0 + await Timer(1, units="ns") diff --git a/tests/cocotb/axi4lite/smoke/test_runner.py b/tests/cocotb/axi4lite/smoke/test_runner.py new file mode 100644 index 0000000..6b1982f --- /dev/null +++ b/tests/cocotb/axi4lite/smoke/test_runner.py @@ -0,0 +1,50 @@ +"""Pytest wrapper launching the AXI4-Lite cocotb smoke test.""" + +from pathlib import Path + +import pytest + +from peakrdl_busdecoder.cpuif.axi4lite.axi4_lite_cpuif_flat import AXI4LiteCpuifFlat + +try: # pragma: no cover - optional dependency shim + from cocotb.runner import get_runner +except ImportError: # pragma: no cover + from cocotb_tools.runner import get_runner + +from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources + + +@pytest.mark.simulation +@pytest.mark.verilator +def test_axi4lite_smoke(tmp_path: Path) -> None: + """Compile the AXI4-Lite design and execute the cocotb smoke test.""" + repo_root = Path(__file__).resolve().parents[4] + + module_path, package_path = compile_rdl_and_export( + str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), + "multi_reg", + tmp_path, + cpuif_cls=AXI4LiteCpuifFlat, + ) + + sources = get_verilog_sources( + module_path, + package_path, + [repo_root / "hdl-src" / "axi4lite_intf.sv"], + ) + + runner = get_runner("verilator") + build_dir = tmp_path / "sim_build" + + runner.build( + sources=sources, + hdl_toplevel=module_path.stem, + build_dir=build_dir, + ) + + runner.test( + hdl_toplevel=module_path.stem, + test_module="tests.cocotb.axi4lite.smoke.test_register_access", + build_dir=build_dir, + log_file=str(tmp_path / "sim.log"), + ) diff --git a/tests/cocotb/common/__init__.py b/tests/cocotb/common/__init__.py deleted file mode 100644 index fd56681..0000000 --- a/tests/cocotb/common/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Common cocotb utilities package.""" - -from .utils import compile_rdl_and_export, get_verilog_sources - -__all__ = ["compile_rdl_and_export", "get_verilog_sources"] diff --git a/tests/cocotb/common/apb4_master.py b/tests/cocotb/common/apb4_master.py deleted file mode 100644 index 6405c81..0000000 --- a/tests/cocotb/common/apb4_master.py +++ /dev/null @@ -1,123 +0,0 @@ -"""APB4 Master Bus Functional Model for cocotb.""" - -import cocotb -from cocotb.triggers import RisingEdge, Timer - - -class APB4Master: - """APB4 Master Bus Functional Model.""" - - def __init__(self, dut, name, clock): - """ - Initialize APB4 Master. - - Args: - dut: The device under test - name: Signal name prefix (e.g., 's_apb') - clock: Clock signal to use for synchronization - """ - self.dut = dut - self.clock = clock - self.name = name - - # Get signals - self.psel = getattr(dut, f"{name}_PSEL") - self.penable = getattr(dut, f"{name}_PENABLE") - self.pwrite = getattr(dut, f"{name}_PWRITE") - self.paddr = getattr(dut, f"{name}_PADDR") - self.pwdata = getattr(dut, f"{name}_PWDATA") - self.pstrb = getattr(dut, f"{name}_PSTRB") - self.pprot = getattr(dut, f"{name}_PPROT") - self.prdata = getattr(dut, f"{name}_PRDATA") - self.pready = getattr(dut, f"{name}_PREADY") - self.pslverr = getattr(dut, f"{name}_PSLVERR") - - def reset(self): - """Reset the bus to idle state.""" - self.psel.value = 0 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = 0 - self.pwdata.value = 0 - self.pstrb.value = 0 - self.pprot.value = 0 - - async def write(self, addr, data, strb=None): - """ - Perform APB4 write transaction. - - Args: - addr: Address to write to - data: Data to write - strb: Byte strobe mask (default: all bytes enabled) - - Returns: - True if write succeeded, False if error - """ - # Calculate strobe if not provided - if strb is None: - data_width_bytes = len(self.pwdata) // 8 - strb = (1 << data_width_bytes) - 1 - - # Setup phase - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 1 - self.paddr.value = addr - self.pwdata.value = data - self.pstrb.value = strb - self.pprot.value = 0 - - # Access phase - await RisingEdge(self.clock) - self.penable.value = 1 - - # Wait for ready - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - error = self.pslverr.value == 1 - break - - # Return to idle - self.psel.value = 0 - self.penable.value = 0 - - return not error - - async def read(self, addr): - """ - Perform APB4 read transaction. - - Args: - addr: Address to read from - - Returns: - Tuple of (data, error) where error is True if read failed - """ - # Setup phase - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = addr - self.pprot.value = 0 - - # Access phase - await RisingEdge(self.clock) - self.penable.value = 1 - - # Wait for ready - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - data = self.prdata.value.integer - error = self.pslverr.value == 1 - break - - # Return to idle - self.psel.value = 0 - self.penable.value = 0 - - return data, error diff --git a/tests/cocotb/examples.py b/tests/cocotb/examples.py deleted file mode 100644 index 25ead63..0000000 --- a/tests/cocotb/examples.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -""" -Example script showing how to generate and test bus decoders. - -This script demonstrates: -1. Compiling RDL specifications -2. Generating SystemVerilog decoders for different CPU interfaces -3. Validating the generated code (syntax check only, no simulation) - -To run actual cocotb simulations, you need: -- Icarus Verilog, Verilator, or other HDL simulator -- cocotb and cocotb-bus Python packages -""" - -import sys -import tempfile -from pathlib import Path - -from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif -from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif - -from .common.utils import compile_rdl_and_export - - -def example_apb4_simple_register(): - """Generate APB4 decoder for a simple register.""" - print("\n" + "=" * 70) - print("Example 1: APB4 Decoder with Simple Register") - print("=" * 70) - - rdl_source = """ - addrmap simple_test { - name = "Simple Register Test"; - desc = "A simple register for testing"; - - reg { - name = "Test Register"; - desc = "32-bit test register"; - - field { - sw=rw; - hw=r; - desc = "Data field"; - } data[31:0]; - } test_reg @ 0x0; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - print(f"\nGenerating SystemVerilog in: {tmpdir}") - module_path, package_path = compile_rdl_and_export( - rdl_source, "simple_test", tmpdir, APB4Cpuif - ) - - print(f"✓ Generated module: {module_path.name}") - print(f"✓ Generated package: {package_path.name}") - - # Show snippet of generated code - with open(module_path) as f: - lines = f.readlines()[:20] - print("\n--- Generated Module (first 20 lines) ---") - for line in lines: - print(line, end="") - - -def example_apb3_multiple_registers(): - """Generate APB3 decoder for multiple registers.""" - print("\n" + "=" * 70) - print("Example 2: APB3 Decoder with Multiple Registers") - print("=" * 70) - - rdl_source = """ - addrmap multi_reg { - name = "Multiple Register Block"; - - reg { - name = "Control Register"; - field { sw=rw; hw=r; } data[31:0]; - } ctrl @ 0x0; - - reg { - name = "Status Register"; - field { sw=r; hw=w; } status[15:0]; - } status @ 0x4; - - reg { - name = "Data Register"; - field { sw=rw; hw=r; } data[31:0]; - } data @ 0x8; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - print(f"\nGenerating SystemVerilog in: {tmpdir}") - module_path, package_path = compile_rdl_and_export( - rdl_source, "multi_reg", tmpdir, APB3Cpuif - ) - - print(f"✓ Generated module: {module_path.name}") - print(f"✓ Generated package: {package_path.name}") - - # Count registers in generated code - with open(module_path) as f: - content = f.read() - print(f"\n✓ Found 'ctrl' in generated code: {'ctrl' in content}") - print(f"✓ Found 'status' in generated code: {'status' in content}") - print(f"✓ Found 'data' in generated code: {'data' in content}") - - -def example_axi4lite_nested_addrmap(): - """Generate AXI4-Lite decoder for nested address map.""" - print("\n" + "=" * 70) - print("Example 3: AXI4-Lite Decoder with Nested Address Map") - print("=" * 70) - - rdl_source = """ - addrmap inner_block { - name = "Inner Block"; - reg { - field { sw=rw; hw=r; } data[31:0]; - } inner_reg @ 0x0; - }; - - addrmap outer_block { - name = "Outer Block"; - inner_block inner @ 0x0; - - reg { - field { sw=rw; hw=r; } outer_data[31:0]; - } outer_reg @ 0x100; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - print(f"\nGenerating SystemVerilog in: {tmpdir}") - module_path, package_path = compile_rdl_and_export( - rdl_source, "outer_block", tmpdir, AXI4LiteCpuif - ) - - print(f"✓ Generated module: {module_path.name}") - print(f"✓ Generated package: {package_path.name}") - - # Check for nested structure - with open(module_path) as f: - content = f.read() - print(f"\n✓ Found 'inner' in generated code: {'inner' in content}") - print(f"✓ Found 'outer_reg' in generated code: {'outer_reg' in content}") - - -def example_register_array(): - """Generate decoder with register arrays.""" - print("\n" + "=" * 70) - print("Example 4: Decoder with Register Arrays") - print("=" * 70) - - rdl_source = """ - addrmap array_test { - name = "Register Array Test"; - - reg { - field { sw=rw; hw=r; } data[31:0]; - } regs[8] @ 0x0 += 0x4; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - print(f"\nGenerating SystemVerilog in: {tmpdir}") - module_path, package_path = compile_rdl_and_export( - rdl_source, "array_test", tmpdir, APB4Cpuif - ) - - print(f"✓ Generated module: {module_path.name}") - print(f"✓ Generated package: {package_path.name}") - - with open(module_path) as f: - content = f.read() - print(f"\n✓ Found 'regs' in generated code: {'regs' in content}") - - -def main(): - """Run all examples.""" - print("\n") - print("*" * 70) - print("*" + " " * 68 + "*") - print("*" + " PeakRDL-BusDecoder: Code Generation Examples".center(68) + "*") - print("*" + " " * 68 + "*") - print("*" * 70) - - try: - example_apb4_simple_register() - example_apb3_multiple_registers() - example_axi4lite_nested_addrmap() - example_register_array() - - print("\n" + "=" * 70) - print("All examples completed successfully!") - print("=" * 70) - print( - """ -To run actual simulations with cocotb: -1. Install simulator: apt-get install iverilog (or verilator) -2. Install cocotb: pip install cocotb cocotb-bus -3. Run tests: pytest tests/cocotb/testbenches/ - -For more information, see: tests/cocotb/README.md -""" - ) - - except Exception as e: - print(f"\n✗ Error: {e}") - import traceback - - traceback.print_exc() - return 1 - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/cocotb/testbenches/__init__.py b/tests/cocotb/testbenches/__init__.py deleted file mode 100644 index e8f27f6..0000000 --- a/tests/cocotb/testbenches/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Cocotb testbenches package.""" diff --git a/tests/cocotb/testbenches/test_apb3_decoder.py b/tests/cocotb/testbenches/test_apb3_decoder.py deleted file mode 100644 index 6d84293..0000000 --- a/tests/cocotb/testbenches/test_apb3_decoder.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Cocotb tests for APB3 bus decoder.""" - -import cocotb -from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer - - -class APB3Master: - """APB3 Master Bus Functional Model (no PSTRB support).""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - self.psel = getattr(dut, f"{name}_PSEL") - self.penable = getattr(dut, f"{name}_PENABLE") - self.pwrite = getattr(dut, f"{name}_PWRITE") - self.paddr = getattr(dut, f"{name}_PADDR") - self.pwdata = getattr(dut, f"{name}_PWDATA") - self.prdata = getattr(dut, f"{name}_PRDATA") - self.pready = getattr(dut, f"{name}_PREADY") - self.pslverr = getattr(dut, f"{name}_PSLVERR") - - def reset(self): - """Reset the bus to idle state.""" - self.psel.value = 0 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = 0 - self.pwdata.value = 0 - - async def write(self, addr, data): - """Perform APB3 write transaction.""" - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 1 - self.paddr.value = addr - self.pwdata.value = data - await RisingEdge(self.clock) - self.penable.value = 1 - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - error = self.pslverr.value == 1 - break - self.psel.value = 0 - self.penable.value = 0 - return not error - - async def read(self, addr): - """Perform APB3 read transaction.""" - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = addr - await RisingEdge(self.clock) - self.penable.value = 1 - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - data = self.prdata.value.integer - error = self.pslverr.value == 1 - break - self.psel.value = 0 - self.penable.value = 0 - return data, error - - -class APB3SlaveResponder: - """Simple APB3 Slave responder.""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - self.psel = getattr(dut, f"{name}_PSEL") - self.penable = getattr(dut, f"{name}_PENABLE") - self.pwrite = getattr(dut, f"{name}_PWRITE") - self.paddr = getattr(dut, f"{name}_PADDR") - self.pwdata = getattr(dut, f"{name}_PWDATA") - self.prdata = getattr(dut, f"{name}_PRDATA") - self.pready = getattr(dut, f"{name}_PREADY") - self.pslverr = getattr(dut, f"{name}_PSLVERR") - self.storage = {} - - async def run(self): - """Run the slave responder.""" - while True: - await RisingEdge(self.clock) - if self.psel.value == 1 and self.penable.value == 1: - addr = self.paddr.value.integer - if self.pwrite.value == 1: - data = self.pwdata.value.integer - self.storage[addr] = data - self.pready.value = 1 - self.pslverr.value = 0 - else: - data = self.storage.get(addr, 0) - self.prdata.value = data - self.pready.value = 1 - self.pslverr.value = 0 - else: - self.pready.value = 0 - self.pslverr.value = 0 - - -@cocotb.test() -async def test_simple_read_write(dut): - """Test simple read and write operations on APB3.""" - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - master = APB3Master(dut, "s_apb", dut.clk) - slave = APB3SlaveResponder(dut, "m_apb_test_reg", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - cocotb.start_soon(slave.run()) - - for _ in range(5): - await RisingEdge(dut.clk) - - # Write test - dut._log.info("Writing 0xABCD1234 to address 0x0") - success = await master.write(0x0, 0xABCD1234) - assert success, "Write operation failed" - - # Read test - dut._log.info("Reading from address 0x0") - data, error = await master.read(0x0) - assert not error, "Read operation returned error" - assert data == 0xABCD1234, f"Read data mismatch: expected 0xABCD1234, got 0x{data:08X}" - - dut._log.info("Test passed!") - - -@cocotb.test() -async def test_multiple_registers(dut): - """Test operations on multiple registers with APB3.""" - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - master = APB3Master(dut, "s_apb", dut.clk) - slave1 = APB3SlaveResponder(dut, "m_apb_reg1", dut.clk) - slave2 = APB3SlaveResponder(dut, "m_apb_reg2", dut.clk) - slave3 = APB3SlaveResponder(dut, "m_apb_reg3", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - cocotb.start_soon(slave1.run()) - cocotb.start_soon(slave2.run()) - cocotb.start_soon(slave3.run()) - - for _ in range(5): - await RisingEdge(dut.clk) - - # Test each register - test_data = [0x11111111, 0x22222222, 0x33333333] - for i, data in enumerate(test_data): - addr = i * 4 - dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}") - success = await master.write(addr, data) - assert success, f"Write to address 0x{addr:X} failed" - - dut._log.info(f"Reading from address 0x{addr:X}") - read_data, error = await master.read(addr) - assert not error, f"Read from address 0x{addr:X} returned error" - assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}" - - dut._log.info("Test passed!") diff --git a/tests/cocotb/testbenches/test_apb4_decoder.py b/tests/cocotb/testbenches/test_apb4_decoder.py deleted file mode 100644 index c4abe44..0000000 --- a/tests/cocotb/testbenches/test_apb4_decoder.py +++ /dev/null @@ -1,264 +0,0 @@ -"""Cocotb tests for APB4 bus decoder.""" - -import os -import tempfile -from pathlib import Path - -import cocotb -import pytest -from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer - -from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif - -# Import the common test utilities -import sys - -sys.path.insert(0, str(Path(__file__).parent.parent)) -from common.utils import compile_rdl_and_export - - -# APB4 Master BFM -class APB4Master: - """APB4 Master Bus Functional Model.""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - self.psel = getattr(dut, f"{name}_PSEL") - self.penable = getattr(dut, f"{name}_PENABLE") - self.pwrite = getattr(dut, f"{name}_PWRITE") - self.paddr = getattr(dut, f"{name}_PADDR") - self.pwdata = getattr(dut, f"{name}_PWDATA") - self.pstrb = getattr(dut, f"{name}_PSTRB") - self.pprot = getattr(dut, f"{name}_PPROT") - self.prdata = getattr(dut, f"{name}_PRDATA") - self.pready = getattr(dut, f"{name}_PREADY") - self.pslverr = getattr(dut, f"{name}_PSLVERR") - - def reset(self): - """Reset the bus to idle state.""" - self.psel.value = 0 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = 0 - self.pwdata.value = 0 - self.pstrb.value = 0 - self.pprot.value = 0 - - async def write(self, addr, data, strb=None): - """Perform APB4 write transaction.""" - if strb is None: - strb = 0xF - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 1 - self.paddr.value = addr - self.pwdata.value = data - self.pstrb.value = strb - self.pprot.value = 0 - await RisingEdge(self.clock) - self.penable.value = 1 - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - error = self.pslverr.value == 1 - break - self.psel.value = 0 - self.penable.value = 0 - return not error - - async def read(self, addr): - """Perform APB4 read transaction.""" - await RisingEdge(self.clock) - self.psel.value = 1 - self.penable.value = 0 - self.pwrite.value = 0 - self.paddr.value = addr - self.pprot.value = 0 - await RisingEdge(self.clock) - self.penable.value = 1 - while True: - await RisingEdge(self.clock) - if self.pready.value == 1: - data = self.prdata.value.integer - error = self.pslverr.value == 1 - break - self.psel.value = 0 - self.penable.value = 0 - return data, error - - -# APB4 Slave responder -class APB4SlaveResponder: - """Simple APB4 Slave responder that acknowledges all transactions.""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - self.psel = getattr(dut, f"{name}_PSEL") - self.penable = getattr(dut, f"{name}_PENABLE") - self.pwrite = getattr(dut, f"{name}_PWRITE") - self.paddr = getattr(dut, f"{name}_PADDR") - self.pwdata = getattr(dut, f"{name}_PWDATA") - self.pstrb = getattr(dut, f"{name}_PSTRB") - self.prdata = getattr(dut, f"{name}_PRDATA") - self.pready = getattr(dut, f"{name}_PREADY") - self.pslverr = getattr(dut, f"{name}_PSLVERR") - # Storage for register values - self.storage = {} - - async def run(self): - """Run the slave responder.""" - while True: - await RisingEdge(self.clock) - if self.psel.value == 1 and self.penable.value == 1: - addr = self.paddr.value.integer - if self.pwrite.value == 1: - # Write operation - data = self.pwdata.value.integer - self.storage[addr] = data - self.pready.value = 1 - self.pslverr.value = 0 - else: - # Read operation - data = self.storage.get(addr, 0) - self.prdata.value = data - self.pready.value = 1 - self.pslverr.value = 0 - else: - self.pready.value = 0 - self.pslverr.value = 0 - - -@cocotb.test() -async def test_simple_read_write(dut): - """Test simple read and write operations.""" - # Start clock - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - # Create master and slave - master = APB4Master(dut, "s_apb", dut.clk) - slave = APB4SlaveResponder(dut, "m_apb_test_reg", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - # Start slave responder - cocotb.start_soon(slave.run()) - - # Wait a few cycles - for _ in range(5): - await RisingEdge(dut.clk) - - # Write test - dut._log.info("Writing 0xDEADBEEF to address 0x0") - success = await master.write(0x0, 0xDEADBEEF) - assert success, "Write operation failed" - - # Read test - dut._log.info("Reading from address 0x0") - data, error = await master.read(0x0) - assert not error, "Read operation returned error" - assert data == 0xDEADBEEF, f"Read data mismatch: expected 0xDEADBEEF, got 0x{data:08X}" - - dut._log.info("Test passed!") - - -@cocotb.test() -async def test_multiple_registers(dut): - """Test operations on multiple registers.""" - # Start clock - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - # Create master and slaves - master = APB4Master(dut, "s_apb", dut.clk) - slave1 = APB4SlaveResponder(dut, "m_apb_reg1", dut.clk) - slave2 = APB4SlaveResponder(dut, "m_apb_reg2", dut.clk) - slave3 = APB4SlaveResponder(dut, "m_apb_reg3", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - # Start slave responders - cocotb.start_soon(slave1.run()) - cocotb.start_soon(slave2.run()) - cocotb.start_soon(slave3.run()) - - # Wait a few cycles - for _ in range(5): - await RisingEdge(dut.clk) - - # Test each register - test_data = [0x12345678, 0xABCDEF00, 0xCAFEBABE] - for i, data in enumerate(test_data): - addr = i * 4 - dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}") - success = await master.write(addr, data) - assert success, f"Write to address 0x{addr:X} failed" - - dut._log.info(f"Reading from address 0x{addr:X}") - read_data, error = await master.read(addr) - assert not error, f"Read from address 0x{addr:X} returned error" - assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}" - - dut._log.info("Test passed!") - - -@cocotb.test() -async def test_byte_strobe(dut): - """Test byte strobe functionality.""" - # Start clock - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - # Create master and slave - master = APB4Master(dut, "s_apb", dut.clk) - slave = APB4SlaveResponder(dut, "m_apb_test_reg", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - # Start slave responder - cocotb.start_soon(slave.run()) - - # Wait a few cycles - for _ in range(5): - await RisingEdge(dut.clk) - - # Write full word - await master.write(0x0, 0x12345678, strb=0xF) - - # Read back - data, error = await master.read(0x0) - assert not error - assert data == 0x12345678 - - # Write only lower byte - await master.write(0x0, 0x000000AB, strb=0x1) - data, error = await master.read(0x0) - assert not error - assert (data & 0xFF) == 0xAB - - dut._log.info("Test passed!") - diff --git a/tests/cocotb/testbenches/test_apb4_runner.py b/tests/cocotb/testbenches/test_apb4_runner.py deleted file mode 100644 index ddf071f..0000000 --- a/tests/cocotb/testbenches/test_apb4_runner.py +++ /dev/null @@ -1,260 +0,0 @@ -"""Pytest test runner for APB4 cocotb tests.""" - -import os -import shutil -import tempfile -from pathlib import Path - -import pytest - -from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif - -# Import the common test utilities -import sys - -sys.path.insert(0, str(Path(__file__).parent.parent)) -from common.utils import compile_rdl_and_export - - -# Check if Icarus Verilog is available -def is_simulator_available(): - """Check if Icarus Verilog simulator is available.""" - return shutil.which("iverilog") is not None - - -# Skip tests if simulator is not available -skip_if_no_simulator = pytest.mark.skipif( - not is_simulator_available(), - reason="Requires Icarus Verilog or other simulator to be installed" -) - - -def generate_testbench_wrapper(top_name, slave_ports, tmpdir_path): - """ - Generate a testbench wrapper that exposes interface signals. - - Args: - top_name: Name of the top-level module - slave_ports: List of slave port names - tmpdir_path: Path to temporary directory - - Returns: - Path to generated testbench file - """ - tb_path = tmpdir_path / f"tb_{top_name}.sv" - with open(tb_path, "w") as f: - f.write(f""" -module tb_{top_name} ( - input logic clk, - input logic rst -); - // Instantiate APB4 interfaces - apb4_intf #( - .DATA_WIDTH(32), - .ADDR_WIDTH(32) - ) s_apb (); - -""") - # Create interface instances for each slave port - for port in slave_ports: - f.write(f""" - apb4_intf #( - .DATA_WIDTH(32), - .ADDR_WIDTH(32) - ) {port} (); -""") - - # Wire master signals - f.write(""" - // Wire master signals from interface to top level for cocotb access - logic s_apb_PSEL; - logic s_apb_PENABLE; - logic s_apb_PWRITE; - logic [31:0] s_apb_PADDR; - logic [31:0] s_apb_PWDATA; - logic [3:0] s_apb_PSTRB; - logic [2:0] s_apb_PPROT; - logic [31:0] s_apb_PRDATA; - logic s_apb_PREADY; - logic s_apb_PSLVERR; - - assign s_apb.PSEL = s_apb_PSEL; - assign s_apb.PENABLE = s_apb_PENABLE; - assign s_apb.PWRITE = s_apb_PWRITE; - assign s_apb.PADDR = s_apb_PADDR; - assign s_apb.PWDATA = s_apb_PWDATA; - assign s_apb.PSTRB = s_apb_PSTRB; - assign s_apb.PPROT = s_apb_PPROT; - assign s_apb_PRDATA = s_apb.PRDATA; - assign s_apb_PREADY = s_apb.PREADY; - assign s_apb_PSLVERR = s_apb.PSLVERR; - -""") - - # Wire slave signals - for port in slave_ports: - f.write(f""" - logic {port}_PSEL; - logic {port}_PENABLE; - logic {port}_PWRITE; - logic [31:0] {port}_PADDR; - logic [31:0] {port}_PWDATA; - logic [3:0] {port}_PSTRB; - logic [31:0] {port}_PRDATA; - logic {port}_PREADY; - logic {port}_PSLVERR; - - assign {port}_PSEL = {port}.PSEL; - assign {port}_PENABLE = {port}.PENABLE; - assign {port}_PWRITE = {port}.PWRITE; - assign {port}_PADDR = {port}.PADDR; - assign {port}_PWDATA = {port}.PWDATA; - assign {port}_PSTRB = {port}.PSTRB; - assign {port}.PRDATA = {port}_PRDATA; - assign {port}.PREADY = {port}_PREADY; - assign {port}.PSLVERR = {port}_PSLVERR; - -""") - - # Instantiate DUT - f.write(f""" - // Instantiate DUT - {top_name} dut ( - .s_apb(s_apb)""") - - for port in slave_ports: - f.write(f",\n .{port}({port})") - - f.write(""" - ); - - // Dump waves - initial begin - $dumpfile("dump.vcd"); - $dumpvars(0, tb_{top_name}); - end -endmodule -""".format(top_name=top_name)) - - return tb_path - - -@skip_if_no_simulator -def test_apb4_simple_register(): - """Test APB4 decoder with a simple register.""" - rdl_source = """ - addrmap simple_test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } test_reg @ 0x0; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir) - - # Compile RDL and export SystemVerilog - module_path, package_path = compile_rdl_and_export( - rdl_source, "simple_test", str(tmpdir_path), APB4Cpuif - ) - - # Generate testbench wrapper - tb_path = generate_testbench_wrapper( - "simple_test", ["m_apb_test_reg"], tmpdir_path - ) - - # Get HDL source directory - hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src" - - # Run simulation using cocotb.runner - from cocotb.runner import get_runner - - runner = get_runner("icarus") - runner.build( - verilog_sources=[ - str(hdl_src_dir / "apb4_intf.sv"), - str(package_path), - str(module_path), - str(tb_path), - ], - hdl_toplevel="tb_simple_test", - always=True, - build_dir=str(tmpdir_path / "sim_build"), - ) - - runner.test( - hdl_toplevel="tb_simple_test", - test_module="test_apb4_decoder", - build_dir=str(tmpdir_path / "sim_build"), - ) - - -@skip_if_no_simulator -def test_apb4_multiple_registers(): - """Test APB4 decoder with multiple registers.""" - rdl_source = """ - addrmap multi_reg { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } reg1 @ 0x0; - - reg { - field { - sw=r; - hw=w; - } status[15:0]; - } reg2 @ 0x4; - - reg { - field { - sw=rw; - hw=r; - } control[7:0]; - } reg3 @ 0x8; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - tmpdir_path = Path(tmpdir) - - # Compile RDL and export SystemVerilog - module_path, package_path = compile_rdl_and_export( - rdl_source, "multi_reg", str(tmpdir_path), APB4Cpuif - ) - - # Generate testbench wrapper - tb_path = generate_testbench_wrapper( - "multi_reg", ["m_apb_reg1", "m_apb_reg2", "m_apb_reg3"], tmpdir_path - ) - - # Get HDL source directory - hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src" - - # Run simulation - from cocotb.runner import get_runner - - runner = get_runner("icarus") - runner.build( - verilog_sources=[ - str(hdl_src_dir / "apb4_intf.sv"), - str(package_path), - str(module_path), - str(tb_path), - ], - hdl_toplevel="tb_multi_reg", - always=True, - build_dir=str(tmpdir_path / "sim_build"), - ) - - runner.test( - hdl_toplevel="tb_multi_reg", - test_module="test_apb4_decoder", - test_args=["--test-case=test_multiple_registers"], - build_dir=str(tmpdir_path / "sim_build"), - ) diff --git a/tests/cocotb/testbenches/test_axi4lite_decoder.py b/tests/cocotb/testbenches/test_axi4lite_decoder.py deleted file mode 100644 index 574d8b2..0000000 --- a/tests/cocotb/testbenches/test_axi4lite_decoder.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Cocotb tests for AXI4-Lite bus decoder.""" - -import cocotb -from cocotb.clock import Clock -from cocotb.triggers import RisingEdge, Timer - - -class AXI4LiteMaster: - """AXI4-Lite Master Bus Functional Model.""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - - # Write address channel - self.awvalid = getattr(dut, f"{name}_AWVALID") - self.awready = getattr(dut, f"{name}_AWREADY") - self.awaddr = getattr(dut, f"{name}_AWADDR") - self.awprot = getattr(dut, f"{name}_AWPROT") - - # Write data channel - self.wvalid = getattr(dut, f"{name}_WVALID") - self.wready = getattr(dut, f"{name}_WREADY") - self.wdata = getattr(dut, f"{name}_WDATA") - self.wstrb = getattr(dut, f"{name}_WSTRB") - - # Write response channel - self.bvalid = getattr(dut, f"{name}_BVALID") - self.bready = getattr(dut, f"{name}_BREADY") - self.bresp = getattr(dut, f"{name}_BRESP") - - # Read address channel - self.arvalid = getattr(dut, f"{name}_ARVALID") - self.arready = getattr(dut, f"{name}_ARREADY") - self.araddr = getattr(dut, f"{name}_ARADDR") - self.arprot = getattr(dut, f"{name}_ARPROT") - - # Read data channel - self.rvalid = getattr(dut, f"{name}_RVALID") - self.rready = getattr(dut, f"{name}_RREADY") - self.rdata = getattr(dut, f"{name}_RDATA") - self.rresp = getattr(dut, f"{name}_RRESP") - - def reset(self): - """Reset the bus to idle state.""" - self.awvalid.value = 0 - self.awaddr.value = 0 - self.awprot.value = 0 - self.wvalid.value = 0 - self.wdata.value = 0 - self.wstrb.value = 0 - self.bready.value = 1 - self.arvalid.value = 0 - self.araddr.value = 0 - self.arprot.value = 0 - self.rready.value = 1 - - async def write(self, addr, data, strb=None): - """Perform AXI4-Lite write transaction.""" - if strb is None: - strb = 0xF - - # Write address phase - await RisingEdge(self.clock) - self.awvalid.value = 1 - self.awaddr.value = addr - self.awprot.value = 0 - - # Write data phase - self.wvalid.value = 1 - self.wdata.value = data - self.wstrb.value = strb - - # Wait for address accept - while True: - await RisingEdge(self.clock) - if self.awready.value == 1: - self.awvalid.value = 0 - break - - # Wait for data accept - while self.wready.value != 1: - await RisingEdge(self.clock) - self.wvalid.value = 0 - - # Wait for write response - self.bready.value = 1 - while self.bvalid.value != 1: - await RisingEdge(self.clock) - - error = self.bresp.value != 0 - await RisingEdge(self.clock) - - return not error - - async def read(self, addr): - """Perform AXI4-Lite read transaction.""" - # Read address phase - await RisingEdge(self.clock) - self.arvalid.value = 1 - self.araddr.value = addr - self.arprot.value = 0 - - # Wait for address accept - while True: - await RisingEdge(self.clock) - if self.arready.value == 1: - self.arvalid.value = 0 - break - - # Wait for read data - self.rready.value = 1 - while self.rvalid.value != 1: - await RisingEdge(self.clock) - - data = self.rdata.value.integer - error = self.rresp.value != 0 - await RisingEdge(self.clock) - - return data, error - - -class AXI4LiteSlaveResponder: - """Simple AXI4-Lite Slave responder.""" - - def __init__(self, dut, name, clock): - self.dut = dut - self.clock = clock - self.name = name - - # Get all signals - self.awvalid = getattr(dut, f"{name}_AWVALID") - self.awready = getattr(dut, f"{name}_AWREADY") - self.awaddr = getattr(dut, f"{name}_AWADDR") - self.wvalid = getattr(dut, f"{name}_WVALID") - self.wready = getattr(dut, f"{name}_WREADY") - self.wdata = getattr(dut, f"{name}_WDATA") - self.wstrb = getattr(dut, f"{name}_WSTRB") - self.bvalid = getattr(dut, f"{name}_BVALID") - self.bready = getattr(dut, f"{name}_BREADY") - self.bresp = getattr(dut, f"{name}_BRESP") - self.arvalid = getattr(dut, f"{name}_ARVALID") - self.arready = getattr(dut, f"{name}_ARREADY") - self.araddr = getattr(dut, f"{name}_ARADDR") - self.rvalid = getattr(dut, f"{name}_RVALID") - self.rready = getattr(dut, f"{name}_RREADY") - self.rdata = getattr(dut, f"{name}_RDATA") - self.rresp = getattr(dut, f"{name}_RRESP") - - self.storage = {} - self.write_pending = False - self.pending_addr = 0 - self.pending_data = 0 - - async def run(self): - """Run the slave responder.""" - while True: - await RisingEdge(self.clock) - - # Handle write address channel - if self.awvalid.value == 1 and not self.write_pending: - self.awready.value = 1 - self.pending_addr = self.awaddr.value.integer - self.write_pending = True - else: - self.awready.value = 0 - - # Handle write data channel - if self.wvalid.value == 1 and self.write_pending: - self.wready.value = 1 - self.pending_data = self.wdata.value.integer - self.storage[self.pending_addr] = self.pending_data - # Send write response - self.bvalid.value = 1 - self.bresp.value = 0 - self.write_pending = False - else: - self.wready.value = 0 - if self.bvalid.value == 1 and self.bready.value == 1: - self.bvalid.value = 0 - - # Handle read address channel - if self.arvalid.value == 1: - self.arready.value = 1 - addr = self.araddr.value.integer - data = self.storage.get(addr, 0) - self.rdata.value = data - self.rvalid.value = 1 - self.rresp.value = 0 - else: - self.arready.value = 0 - if self.rvalid.value == 1 and self.rready.value == 1: - self.rvalid.value = 0 - - -@cocotb.test() -async def test_simple_read_write(dut): - """Test simple read and write operations on AXI4-Lite.""" - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - master = AXI4LiteMaster(dut, "s_axi", dut.clk) - slave = AXI4LiteSlaveResponder(dut, "m_axi_test_reg", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - cocotb.start_soon(slave.run()) - - for _ in range(5): - await RisingEdge(dut.clk) - - # Write test - dut._log.info("Writing 0xFEEDFACE to address 0x0") - success = await master.write(0x0, 0xFEEDFACE) - assert success, "Write operation failed" - - # Read test - dut._log.info("Reading from address 0x0") - data, error = await master.read(0x0) - assert not error, "Read operation returned error" - assert data == 0xFEEDFACE, f"Read data mismatch: expected 0xFEEDFACE, got 0x{data:08X}" - - dut._log.info("Test passed!") - - -@cocotb.test() -async def test_multiple_registers(dut): - """Test operations on multiple registers with AXI4-Lite.""" - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - master = AXI4LiteMaster(dut, "s_axi", dut.clk) - slave1 = AXI4LiteSlaveResponder(dut, "m_axi_reg1", dut.clk) - slave2 = AXI4LiteSlaveResponder(dut, "m_axi_reg2", dut.clk) - slave3 = AXI4LiteSlaveResponder(dut, "m_axi_reg3", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - cocotb.start_soon(slave1.run()) - cocotb.start_soon(slave2.run()) - cocotb.start_soon(slave3.run()) - - for _ in range(5): - await RisingEdge(dut.clk) - - # Test each register - test_data = [0xAAAAAAAA, 0xBBBBBBBB, 0xCCCCCCCC] - for i, data in enumerate(test_data): - addr = i * 4 - dut._log.info(f"Writing 0x{data:08X} to address 0x{addr:X}") - success = await master.write(addr, data) - assert success, f"Write to address 0x{addr:X} failed" - - dut._log.info(f"Reading from address 0x{addr:X}") - read_data, error = await master.read(addr) - assert not error, f"Read from address 0x{addr:X} returned error" - assert read_data == data, f"Data mismatch at 0x{addr:X}: expected 0x{data:08X}, got 0x{read_data:08X}" - - dut._log.info("Test passed!") - - -@cocotb.test() -async def test_byte_strobe(dut): - """Test byte strobe functionality with AXI4-Lite.""" - clock = Clock(dut.clk, 10, units="ns") - cocotb.start_soon(clock.start()) - - master = AXI4LiteMaster(dut, "s_axi", dut.clk) - slave = AXI4LiteSlaveResponder(dut, "m_axi_test_reg", dut.clk) - - # Reset - dut.rst.value = 1 - master.reset() - await Timer(100, units="ns") - await RisingEdge(dut.clk) - dut.rst.value = 0 - await RisingEdge(dut.clk) - - cocotb.start_soon(slave.run()) - - for _ in range(5): - await RisingEdge(dut.clk) - - # Write full word - await master.write(0x0, 0x12345678, strb=0xF) - - # Read back - data, error = await master.read(0x0) - assert not error - assert data == 0x12345678 - - # Write only lower byte - await master.write(0x0, 0x000000CD, strb=0x1) - data, error = await master.read(0x0) - assert not error - assert (data & 0xFF) == 0xCD - - dut._log.info("Test passed!") diff --git a/tests/cocotb/testbenches/test_integration.py b/tests/cocotb/testbenches/test_integration.py deleted file mode 100644 index ffd73d1..0000000 --- a/tests/cocotb/testbenches/test_integration.py +++ /dev/null @@ -1,216 +0,0 @@ -""" -Integration tests for cocotb testbench infrastructure. - -These tests validate that the code generation and testbench setup works correctly -without requiring an actual HDL simulator. They check: -- RDL compilation and SystemVerilog generation -- Generated code contains expected elements -- Testbench utilities work correctly -""" - -import tempfile -from pathlib import Path - -import pytest - -from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif -from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif - -from ..common.utils import compile_rdl_and_export, get_verilog_sources - - -class TestCodeGeneration: - """Test code generation for different CPU interfaces.""" - - def test_apb4_simple_register(self): - """Test APB4 code generation for simple register.""" - rdl_source = """ - addrmap simple_test { - reg { - field { sw=rw; hw=r; } data[31:0]; - } test_reg @ 0x0; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, "simple_test", tmpdir, APB4Cpuif - ) - - # Verify files exist - assert module_path.exists() - assert package_path.exists() - - # Verify module content - module_content = module_path.read_text() - assert "module simple_test" in module_content - assert "apb4_intf.slave s_apb" in module_content - assert "test_reg" in module_content - - # Verify package content - package_content = package_path.read_text() - assert "package simple_test_pkg" in package_content - - def test_apb3_multiple_registers(self): - """Test APB3 code generation for multiple registers.""" - rdl_source = """ - addrmap multi_reg { - reg { field { sw=rw; hw=r; } data[31:0]; } reg1 @ 0x0; - reg { field { sw=r; hw=w; } status[15:0]; } reg2 @ 0x4; - reg { field { sw=rw; hw=r; } control[7:0]; } reg3 @ 0x8; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, "multi_reg", tmpdir, APB3Cpuif - ) - - assert module_path.exists() - assert package_path.exists() - - module_content = module_path.read_text() - assert "module multi_reg" in module_content - assert "apb3_intf.slave s_apb" in module_content - assert "reg1" in module_content - assert "reg2" in module_content - assert "reg3" in module_content - - def test_axi4lite_nested_addrmap(self): - """Test AXI4-Lite code generation for nested address map.""" - rdl_source = """ - addrmap inner_block { - reg { field { sw=rw; hw=r; } data[31:0]; } inner_reg @ 0x0; - }; - - addrmap outer_block { - inner_block inner @ 0x0; - reg { field { sw=rw; hw=r; } outer_data[31:0]; } outer_reg @ 0x100; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, "outer_block", tmpdir, AXI4LiteCpuif - ) - - assert module_path.exists() - assert package_path.exists() - - module_content = module_path.read_text() - assert "module outer_block" in module_content - assert "axi4lite_intf.slave s_axi" in module_content - assert "inner" in module_content - assert "outer_reg" in module_content - - def test_register_array(self): - """Test code generation with register arrays.""" - rdl_source = """ - addrmap array_test { - reg { field { sw=rw; hw=r; } data[31:0]; } regs[4] @ 0x0 += 0x4; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, "array_test", tmpdir, APB4Cpuif - ) - - assert module_path.exists() - assert package_path.exists() - - module_content = module_path.read_text() - assert "module array_test" in module_content - assert "regs" in module_content - - -class TestUtilityFunctions: - """Test utility functions for testbench setup.""" - - def test_get_verilog_sources(self): - """Test that get_verilog_sources returns correct file list.""" - hdl_src_dir = Path(__file__).parent.parent.parent.parent / "hdl-src" - - module_path = Path("/tmp/test_module.sv") - package_path = Path("/tmp/test_pkg.sv") - intf_files = [ - hdl_src_dir / "apb4_intf.sv", - hdl_src_dir / "apb3_intf.sv", - ] - - sources = get_verilog_sources(module_path, package_path, intf_files) - - # Verify order: interfaces first, then package, then module - assert len(sources) == 4 - assert str(intf_files[0]) in sources[0] - assert str(intf_files[1]) in sources[1] - assert str(package_path) in sources[2] - assert str(module_path) in sources[3] - - def test_compile_rdl_and_export_with_custom_names(self): - """Test code generation with custom module and package names.""" - rdl_source = """ - addrmap test_map { - reg { field { sw=rw; hw=r; } data[31:0]; } test_reg @ 0x0; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, - "test_map", - tmpdir, - APB4Cpuif, - module_name="custom_module", - package_name="custom_pkg", - ) - - # Verify custom names - assert module_path.name == "custom_module.sv" - assert package_path.name == "custom_pkg.sv" - - # Verify content uses custom names - module_content = module_path.read_text() - assert "module custom_module" in module_content - - package_content = package_path.read_text() - assert "package custom_pkg" in package_content - - -class TestMultipleCpuInterfaces: - """Test that all CPU interfaces generate valid code.""" - - @pytest.mark.parametrize( - "cpuif_cls,intf_name", - [ - (APB3Cpuif, "apb3_intf"), - (APB4Cpuif, "apb4_intf"), - (AXI4LiteCpuif, "axi4lite_intf"), - ], - ) - def test_cpuif_generation(self, cpuif_cls, intf_name): - """Test code generation for each CPU interface type.""" - rdl_source = """ - addrmap test_block { - reg { - field { sw=rw; hw=r; } data[31:0]; - } test_reg @ 0x0; - }; - """ - - with tempfile.TemporaryDirectory() as tmpdir: - module_path, package_path = compile_rdl_and_export( - rdl_source, "test_block", tmpdir, cpuif_cls - ) - - assert module_path.exists() - assert package_path.exists() - - module_content = module_path.read_text() - assert "module test_block" in module_content - assert intf_name in module_content - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/cocotb_lib/__init__.py b/tests/cocotb_lib/__init__.py new file mode 100644 index 0000000..3b33873 --- /dev/null +++ b/tests/cocotb_lib/__init__.py @@ -0,0 +1,3 @@ +from pathlib import Path + +rdls = map(Path, ["simple.rdl", "multiple_reg.rdl"]) diff --git a/tests/cocotb_lib/multiple_reg.rdl b/tests/cocotb_lib/multiple_reg.rdl new file mode 100644 index 0000000..8a1141d --- /dev/null +++ b/tests/cocotb_lib/multiple_reg.rdl @@ -0,0 +1,22 @@ +addrmap multi_reg { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } reg1 @ 0x0; + + reg { + field { + sw=r; + hw=w; + } status[15:0]; + } reg2 @ 0x4; + + reg { + field { + sw=rw; + hw=r; + } control[7:0]; + } reg3 @ 0x8; +}; \ No newline at end of file diff --git a/tests/cocotb_lib/simple.rdl b/tests/cocotb_lib/simple.rdl new file mode 100644 index 0000000..f84d892 --- /dev/null +++ b/tests/cocotb_lib/simple.rdl @@ -0,0 +1,8 @@ +addrmap simple_test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } test_reg @ 0x0; +}; \ No newline at end of file diff --git a/tests/cocotb/common/utils.py b/tests/cocotb_lib/utils.py similarity index 67% rename from tests/cocotb/common/utils.py rename to tests/cocotb_lib/utils.py index 6a611f3..cd9f7a7 100644 --- a/tests/cocotb/common/utils.py +++ b/tests/cocotb_lib/utils.py @@ -1,24 +1,22 @@ """Common utilities for cocotb testbenches.""" -import os -import tempfile from pathlib import Path -from tempfile import NamedTemporaryFile from typing import Any from systemrdl import RDLCompiler +from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif from peakrdl_busdecoder.exporter import BusDecoderExporter def compile_rdl_and_export( - rdl_source: str, top_name: str, output_dir: str, cpuif_cls: Any, **kwargs: Any + rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any ) -> tuple[Path, Path]: """ Compile RDL source and export to SystemVerilog. Args: - rdl_source: SystemRDL source code as a string + rdl_source: SystemRDL source code path top_name: Name of the top-level addrmap output_dir: Directory to write generated files cpuif_cls: CPU interface class to use @@ -30,23 +28,12 @@ def compile_rdl_and_export( # Compile RDL source compiler = RDLCompiler() - # Write source to temporary file - with NamedTemporaryFile("w", suffix=".rdl", dir=output_dir, delete=False) as tmp_file: - tmp_file.write(rdl_source) - tmp_file.flush() - tmp_path = tmp_file.name + compiler.compile_file(rdl_source) + top = compiler.elaborate(top_name) - try: - compiler.compile_file(tmp_path) - top = compiler.elaborate(top_name) - - # Export to SystemVerilog - exporter = BusDecoderExporter() - exporter.export(top, output_dir, cpuif_cls=cpuif_cls, **kwargs) - finally: - # Clean up temporary RDL file - if os.path.exists(tmp_path): - os.unlink(tmp_path) + # Export to SystemVerilog + exporter = BusDecoderExporter() + exporter.export(top, str(output_dir), cpuif_cls=cpuif_cls, **kwargs) # Return paths to generated files module_name = kwargs.get("module_name", top_name) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4beba1c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,60 @@ +"""Pytest fixtures for unit tests.""" + +from __future__ import annotations + +collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py"] + +import os +from collections.abc import Callable +from pathlib import Path +from tempfile import NamedTemporaryFile + +import pytest +from systemrdl import RDLCompileError, RDLCompiler # type:ignore +from systemrdl.node import AddrmapNode + +_SHIM_DIR = Path(__file__).resolve().parents[1] / "tools" / "shims" +os.environ["PATH"] = f"{_SHIM_DIR}{os.pathsep}{os.environ.get('PATH', '')}" + + +@pytest.fixture +def compile_rdl(tmp_path: Path) -> Callable[..., AddrmapNode]: + """Compile inline SystemRDL source and return the elaborated root node. + + Parameters + ---------- + tmp_path: + Temporary directory provided by pytest. + """ + + def _compile( + source: str, + *, + top: str | None = None, + defines: dict[str, str] | None = None, + include_paths: list[Path | str] | None = None, + ) -> AddrmapNode: + compiler = RDLCompiler() + # Use delete=False to keep the file around after closing + with NamedTemporaryFile("w", suffix=".rdl", dir=tmp_path, delete=False) as tmp_file: + tmp_file.write(source) + tmp_file.flush() + + try: + compiler.compile_file( + tmp_file.name, + incl_search_paths=(list(map(str, include_paths)) if include_paths else None), + defines=defines, + ) + if top is not None: + root = compiler.elaborate(top) # type:ignore + return root.top + root = compiler.elaborate() # type:ignore + return root.top + except RDLCompileError: + # Print error messages if available + if hasattr(compiler, "print_messages"): + compiler.print_messages() # type:ignore + raise + + return _compile diff --git a/tests/exporter/__init__.py b/tests/exporter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_exporter.py b/tests/exporter/test_bus_decoder_exporter.py similarity index 50% rename from tests/unit/test_exporter.py rename to tests/exporter/test_bus_decoder_exporter.py index 16c4896..0b5ad6e 100644 --- a/tests/unit/test_exporter.py +++ b/tests/exporter/test_bus_decoder_exporter.py @@ -1,19 +1,16 @@ -"""Integration tests for the BusDecoderExporter.""" - - -import os +from collections.abc import Callable from pathlib import Path -import pytest +from systemrdl.node import AddrmapNode +from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -from peakrdl_busdecoder.exporter import BusDecoderExporter class TestBusDecoderExporter: """Test the top-level BusDecoderExporter.""" - def test_simple_register_export(self, compile_rdl, tmp_path): + def test_simple_register_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting a simple register.""" rdl_source = """ addrmap simple_reg { @@ -46,7 +43,7 @@ class TestBusDecoderExporter: package_content = package_file.read_text() assert "package simple_reg_pkg" in package_content - def test_register_array_export(self, compile_rdl, tmp_path): + def test_register_array_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting a register array.""" rdl_source = """ addrmap reg_array { @@ -72,7 +69,7 @@ class TestBusDecoderExporter: assert "module reg_array" in module_content assert "my_regs" in module_content - def test_nested_addrmap_export(self, compile_rdl, tmp_path): + def test_nested_addrmap_export(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting nested addrmaps.""" rdl_source = """ addrmap inner_block { @@ -103,7 +100,7 @@ class TestBusDecoderExporter: assert "inner" in module_content assert "inner_reg" in module_content - def test_custom_module_name(self, compile_rdl, tmp_path): + def test_custom_module_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting with custom module name.""" rdl_source = """ addrmap my_addrmap { @@ -131,7 +128,7 @@ class TestBusDecoderExporter: module_content = module_file.read_text() assert "module custom_module" in module_content - def test_custom_package_name(self, compile_rdl, tmp_path): + def test_custom_package_name(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting with custom package name.""" rdl_source = """ addrmap my_addrmap { @@ -156,7 +153,7 @@ class TestBusDecoderExporter: package_content = package_file.read_text() assert "package custom_pkg" in package_content - def test_multiple_registers(self, compile_rdl, tmp_path): + def test_multiple_registers(self, compile_rdl: Callable[..., AddrmapNode], tmp_path: Path) -> None: """Test exporting multiple registers.""" rdl_source = """ addrmap multi_reg { @@ -196,134 +193,3 @@ class TestBusDecoderExporter: assert "reg1" in module_content assert "reg2" in module_content assert "reg3" in module_content - - -class TestAPB4Interface: - """Test APB4 CPU interface generation.""" - - def test_apb4_port_declaration(self, compile_rdl, tmp_path): - """Test that APB4 interface ports are generated.""" - rdl_source = """ - addrmap apb_test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="apb_test") - - exporter = BusDecoderExporter() - output_dir = str(tmp_path) - exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) - - module_file = tmp_path / "apb_test.sv" - module_content = module_file.read_text() - - # Check for APB4 signals - assert "PSEL" in module_content or "psel" in module_content - assert "PENABLE" in module_content or "penable" in module_content - assert "PWRITE" in module_content or "pwrite" in module_content - assert "PADDR" in module_content or "paddr" in module_content - assert "PWDATA" in module_content or "pwdata" in module_content - assert "PRDATA" in module_content or "prdata" in module_content - assert "PREADY" in module_content or "pready" in module_content - - def test_apb4_read_write_logic(self, compile_rdl, tmp_path): - """Test that APB4 read/write logic is generated.""" - rdl_source = """ - addrmap apb_rw { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="apb_rw") - - exporter = BusDecoderExporter() - output_dir = str(tmp_path) - exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) - - module_file = tmp_path / "apb_rw.sv" - module_content = module_file.read_text() - - # Basic sanity checks for logic generation - assert "always" in module_content or "assign" in module_content - assert "my_reg" in module_content - - def test_nested_addrmap_with_array_stride(self, compile_rdl, tmp_path): - """Test that nested addrmaps with arrays use correct stride values.""" - rdl_source = """ - addrmap inner_block { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } inner_reg @ 0x0; - }; - - addrmap outer_block { - inner_block inner[4] @ 0x0 += 0x100; - - reg { - field { - sw=rw; - hw=r; - } outer_data[31:0]; - } outer_reg @ 0x400; - }; - """ - top = compile_rdl(rdl_source, top="outer_block") - - exporter = BusDecoderExporter() - output_dir = str(tmp_path) - exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) - - module_file = tmp_path / "outer_block.sv" - module_content = module_file.read_text() - - # Check that the generated code uses the correct stride (0x100 = 256) - # not the array dimension (4) - # The decode logic should contain something like: i0)*11'h100 or i0)*256 - assert "i0)*11'h100" in module_content or "i0)*'h100" in module_content, \ - "Array stride should be 0x100 (256), not the dimension value (4)" - - # Ensure it's NOT using the incorrect dimension value - assert ( - "i0)*11'h4" not in module_content - and "i0)*4" not in module_content - ), "Should not use array dimension (4) as stride" - - def test_multidimensional_array_strides(self, compile_rdl, tmp_path): - """Test that multidimensional arrays calculate correct strides for each dimension.""" - rdl_source = """ - addrmap test_block { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg[2][3] @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test_block") - - exporter = BusDecoderExporter() - output_dir = str(tmp_path) - exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) - - module_file = tmp_path / "test_block.sv" - module_content = module_file.read_text() - # For a [2][3] array where each register is 4 bytes: - # i0 (leftmost/slowest) should have stride = 3 * 4 = 12 (0xc) - # i1 (rightmost/fastest) should have stride = 4 (0x4) - assert ("i0)*5'hc" in module_content or "i0)*12" in module_content), \ - "i0 should use stride 12 (0xc) for [2][3] array" - assert ("i1)*5'h4" in module_content or "i1)*4" in module_content), \ - "i1 should use stride 4 for [2][3] array" diff --git a/tests/generator/__init__.py b/tests/generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/generator/test_decode_logic_generator.py b/tests/generator/test_decode_logic_generator.py new file mode 100644 index 0000000..bb29c4f --- /dev/null +++ b/tests/generator/test_decode_logic_generator.py @@ -0,0 +1,96 @@ +from collections.abc import Callable + +from systemrdl.node import AddrmapNode + +from peakrdl_busdecoder.decode_logic_gen import DecodeLogicFlavor, DecodeLogicGenerator +from peakrdl_busdecoder.design_state import DesignState + + +class TestDecodeLogicGenerator: + """Test the DecodeLogicGenerator.""" + + def test_decode_logic_read(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test decode logic generation for read operations.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) + + # Basic sanity check - it should initialize + assert gen is not None + assert gen._flavor == DecodeLogicFlavor.READ # type: ignore + + def test_decode_logic_write(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test decode logic generation for write operations.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE) + + assert gen is not None + assert gen._flavor == DecodeLogicFlavor.WRITE # type: ignore + + def test_cpuif_addr_predicate(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test address predicate generation.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x100; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) + + # Get the register node + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + predicates = gen.cpuif_addr_predicate(reg_node) + + # Should return a list of conditions + assert isinstance(predicates, list) + assert len(predicates) > 0 + # Should check address bounds + for pred in predicates: + assert "cpuif_rd_addr" in pred or ">=" in pred or "<" in pred + + def test_decode_logic_flavor_enum(self) -> None: + """Test DecodeLogicFlavor enum values.""" + assert DecodeLogicFlavor.READ.value == "rd" + assert DecodeLogicFlavor.WRITE.value == "wr" + + assert DecodeLogicFlavor.READ.cpuif_address == "cpuif_rd_addr" + assert DecodeLogicFlavor.WRITE.cpuif_address == "cpuif_wr_addr" + + assert DecodeLogicFlavor.READ.cpuif_select == "cpuif_rd_sel" + assert DecodeLogicFlavor.WRITE.cpuif_select == "cpuif_wr_sel" diff --git a/tests/generator/test_design_state.py b/tests/generator/test_design_state.py new file mode 100644 index 0000000..636a085 --- /dev/null +++ b/tests/generator/test_design_state.py @@ -0,0 +1,125 @@ + +from collections.abc import Callable + +from systemrdl.node import AddrmapNode + +from peakrdl_busdecoder.design_state import DesignState + + +class TestDesignState: + """Test the DesignState class.""" + + def test_design_state_basic(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test basic DesignState initialization.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + + assert ds.top_node == top + assert ds.module_name == "test" + assert ds.package_name == "test_pkg" + assert ds.cpuif_data_width == 32 # Should infer from 32-bit field + assert ds.addr_width > 0 + + def test_design_state_custom_module_name(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test DesignState with custom module name.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"module_name": "custom_module"}) + + assert ds.module_name == "custom_module" + assert ds.package_name == "custom_module_pkg" + + def test_design_state_custom_package_name(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test DesignState with custom package name.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"package_name": "custom_pkg"}) + + assert ds.package_name == "custom_pkg" + + def test_design_state_custom_address_width(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test DesignState with custom address width.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"address_width": 16}) + + assert ds.addr_width == 16 + + def test_design_state_unroll_arrays(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test DesignState with cpuif_unroll option.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_regs[4] @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"cpuif_unroll": True}) + + assert ds.cpuif_unroll is True + + def test_design_state_64bit_registers(self, compile_rdl:Callable[..., AddrmapNode])->None: + """Test DesignState with wider data width.""" + rdl_source = """ + addrmap test { + reg { + regwidth = 32; + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + + # Should infer 32-bit data width from field + assert ds.cpuif_data_width == 32 diff --git a/tests/generator/test_struct_generator.py b/tests/generator/test_struct_generator.py new file mode 100644 index 0000000..4f0f444 --- /dev/null +++ b/tests/generator/test_struct_generator.py @@ -0,0 +1,98 @@ +from collections.abc import Callable + +from systemrdl.node import AddrmapNode + +from peakrdl_busdecoder.design_state import DesignState +from peakrdl_busdecoder.struct_gen import StructGenerator + + +class TestStructGenerator: + """Test the StructGenerator.""" + + def test_simple_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test struct generation for simple register.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Should generate struct definition + assert gen is not None + result = str(gen) + + # Should contain struct declaration + assert "struct" in result or "typedef" in result + + def test_nested_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test struct generation for nested addrmaps.""" + rdl_source = """ + addrmap inner { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } inner_reg @ 0x0; + }; + + addrmap outer { + inner my_inner @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="outer") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Walk the tree to generate structs + from systemrdl.walker import RDLWalker + + walker = RDLWalker() + walker.walk(top, gen, skip_top=True) + + result = str(gen) + + # Should contain struct declaration + assert "struct" in result or "typedef" in result + # The struct should reference the inner component + assert "my_inner" in result + + def test_array_struct_generation(self, compile_rdl: Callable[..., AddrmapNode]) -> None: + """Test struct generation for register arrays.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_regs[4] @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Walk the tree to generate structs + from systemrdl.walker import RDLWalker + + walker = RDLWalker() + walker.walk(top, gen, skip_top=True) + + result = str(gen) + + # Should contain array notation + assert "[" in result and "]" in result + # Should reference the register + assert "my_regs" in result diff --git a/tests/pytest.ini b/tests/pytest.ini index 6daf5f6..d8c8096 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,3 +1,5 @@ [pytest] -testpaths = unit python_files = test_*.py testcase.py +markers = + simulation: marks hardware simulation tests requiring cocotb/verilator + verilator: marks tests that invoke the Verilator toolchain diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index 58d2945..0000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit test package for PeakRDL BusDecoder.""" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 35f8e7f..047a0f7 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -5,52 +5,71 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Iterable, Mapping, Optional +from collections.abc import Callable import pytest -from systemrdl import RDLCompileError, RDLCompiler +from systemrdl.node import AddrmapNode @pytest.fixture -def compile_rdl(tmp_path: Path): - """Compile inline SystemRDL source and return the elaborated root node. +def external_nested_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode: + """Create an RDL design with external nested addressable components. - Parameters - ---------- - tmp_path: - Temporary directory provided by pytest. + This tests the scenario where an addrmap contains external children + that themselves have external addressable children. + The decoder should only generate select signals for the top-level + external children, not their internal structure. """ + rdl_source = """ + mem queue_t { + name = "Queue"; + mementries = 1024; + memwidth = 64; + }; - def _compile( - source: str, - *, - top: Optional[str] = None, - defines: Optional[Mapping[str, object]] = None, - include_paths: Optional[Iterable[Path | str]] = None, - ): - compiler = RDLCompiler() + addrmap port_t { + name = "Port"; + desc = ""; - for key, value in (defines or {}).items(): - compiler.define(key, value) + external queue_t common[3] @ 0x0 += 0x2000; + external queue_t response @ 0x6000; + }; - for include_path in include_paths or (): - compiler.add_include_path(str(include_path)) + addrmap buffer_t { + name = "Buffer"; + desc = ""; - # Use delete=False to keep the file around after closing - with NamedTemporaryFile("w", suffix=".rdl", dir=tmp_path, delete=False) as tmp_file: - tmp_file.write(source) - tmp_file.flush() + port_t multicast @ 0x0; + port_t port [16] @ 0x8000 += 0x8000; + }; + """ + return compile_rdl(rdl_source, top="buffer_t") - try: - compiler.compile_file(tmp_file.name) - if top is not None: - root = compiler.elaborate(top) - return root.top - root = compiler.elaborate() - return root.top - except RDLCompileError: - # Print error messages if available - if hasattr(compiler, "print_messages"): - compiler.print_messages() - raise - return _compile +@pytest.fixture +def nested_addrmap_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode: + """Create an RDL design with nested non-external addrmaps for testing depth control.""" + rdl_source = """ + addrmap level2 { + reg { + field { sw=rw; hw=r; } data2[31:0]; + } reg2 @ 0x0; + + reg { + field { sw=rw; hw=r; } data2b[31:0]; + } reg2b @ 0x4; + }; + + addrmap level1 { + reg { + field { sw=rw; hw=r; } data1[31:0]; + } reg1 @ 0x0; + + level2 inner2 @ 0x10; + }; + + addrmap level0 { + level1 inner1 @ 0x0; + }; + """ + return compile_rdl(rdl_source, top="level0") diff --git a/tests/unit/test_body.py b/tests/unit/test_body.py deleted file mode 100644 index 8ee6418..0000000 --- a/tests/unit/test_body.py +++ /dev/null @@ -1,285 +0,0 @@ -"""Tests for body classes used in code generation.""" - - - -import pytest - -from peakrdl_busdecoder.body import ( - Body, - CombinationalBody, - ForLoopBody, - IfBody, - StructBody, -) - - -class TestBody: - """Test the base Body class.""" - - def test_empty_body(self): - """Test empty body returns empty string.""" - body = Body() - assert str(body) == "" - assert not body # Should be falsy when empty - - def test_add_single_line(self): - """Test adding a single line to body.""" - body = Body() - body += "line1" - assert str(body) == "line1" - assert body # Should be truthy when not empty - - def test_add_multiple_lines(self): - """Test adding multiple lines to body.""" - body = Body() - body += "line1" - body += "line2" - body += "line3" - expected = "line1\nline2\nline3" - assert str(body) == expected - - def test_add_returns_self(self): - """Test that add operation returns self for chaining.""" - body = Body() - body += "line1" - body += "line2" - # Chaining works because += returns self - assert len(body.lines) == 2 - - def test_add_nested_body(self): - """Test adding another body as a line.""" - outer = Body() - inner = Body() - inner += "inner1" - inner += "inner2" - outer += "outer1" - outer += inner - outer += "outer2" - expected = "outer1\ninner1\ninner2\nouter2" - assert str(outer) == expected - - -class TestForLoopBody: - """Test the ForLoopBody class.""" - - def test_genvar_for_loop(self): - """Test genvar-style for loop.""" - body = ForLoopBody("genvar", "i", 4) - body += "statement1;" - body += "statement2;" - - result = str(body) - assert "for (genvar i = 0; i < 4; i++)" in result - assert "statement1;" in result - assert "statement2;" in result - assert "end" in result - - def test_int_for_loop(self): - """Test int-style for loop.""" - body = ForLoopBody("int", "j", 8) - body += "assignment = value;" - - result = str(body) - assert "for (int j = 0; j < 8; j++)" in result - assert "assignment = value;" in result - assert "end" in result - - def test_empty_for_loop(self): - """Test empty for loop.""" - body = ForLoopBody("genvar", "k", 2) - result = str(body) - # Empty for loop should still have structure - assert "for (genvar k = 0; k < 2; k++)" in result - - def test_nested_for_loops(self): - """Test nested for loops.""" - outer = ForLoopBody("genvar", "i", 3) - inner = ForLoopBody("genvar", "j", 2) - inner += "nested_statement;" - outer += inner - - result = str(outer) - assert "for (genvar i = 0; i < 3; i++)" in result - assert "for (genvar j = 0; j < 2; j++)" in result - assert "nested_statement;" in result - - -class TestIfBody: - """Test the IfBody class.""" - - def test_simple_if(self): - """Test simple if statement.""" - body = IfBody() - with body.cm("condition1") as b: - b += "statement1;" - - result = str(body) - assert "if (condition1)" in result - assert "statement1;" in result - assert "end" in result - - def test_if_else(self): - """Test if-else statement.""" - body = IfBody() - with body.cm("condition1") as b: - b += "if_statement;" - with body.cm(None) as b: # None for else - b += "else_statement;" - - result = str(body) - assert "if (condition1)" in result - assert "if_statement;" in result - assert "else" in result - assert "else_statement;" in result - - def test_if_elif_else(self): - """Test if-elif-else chain.""" - body = IfBody() - with body.cm("condition1") as b: - b += "statement1;" - with body.cm("condition2") as b: - b += "statement2;" - with body.cm(None) as b: # None for else - b += "statement3;" - - result = str(body) - assert "if (condition1)" in result - assert "statement1;" in result - assert "else if (condition2)" in result - assert "statement2;" in result - assert "else" in result - assert "statement3;" in result - - def test_multiple_elif(self): - """Test multiple elif statements.""" - body = IfBody() - with body.cm("cond1") as b: - b += "stmt1;" - with body.cm("cond2") as b: - b += "stmt2;" - with body.cm("cond3") as b: - b += "stmt3;" - - result = str(body) - assert "if (cond1)" in result - assert "else if (cond2)" in result - assert "else if (cond3)" in result - - def test_empty_if_branches(self): - """Test if statement with empty branches.""" - body = IfBody() - with body.cm("condition"): - pass - - result = str(body) - assert "if (condition)" in result - - def test_nested_if(self): - """Test nested if statements.""" - outer = IfBody() - with outer.cm("outer_cond") as outer_body: - inner = IfBody() - with inner.cm("inner_cond") as inner_body: - inner_body += "nested_statement;" - outer_body += inner - - result = str(outer) - assert "if (outer_cond)" in result - assert "if (inner_cond)" in result - assert "nested_statement;" in result - - -class TestCombinationalBody: - """Test the CombinationalBody class.""" - - def test_simple_combinational_block(self): - """Test simple combinational block.""" - body = CombinationalBody() - body += "assign1 = value1;" - body += "assign2 = value2;" - - result = str(body) - assert "always_comb" in result - assert "begin" in result - assert "assign1 = value1;" in result - assert "assign2 = value2;" in result - assert "end" in result - - def test_empty_combinational_block(self): - """Test empty combinational block.""" - body = CombinationalBody() - result = str(body) - assert "always_comb" in result - assert "begin" in result - assert "end" in result - - def test_combinational_with_if_statement(self): - """Test combinational block with if statement.""" - cb = CombinationalBody() - ifb = IfBody() - with ifb.cm("condition") as b: - b += "assignment = value;" - cb += ifb - - result = str(cb) - assert "always_comb" in result - assert "if (condition)" in result - assert "assignment = value;" in result - - -class TestStructBody: - """Test the StructBody class.""" - - def test_simple_struct(self): - """Test simple struct definition.""" - body = StructBody("my_struct_t", packed=True, typedef=True) - body += "logic [7:0] field1;" - body += "logic field2;" - - result = str(body) - assert "typedef struct packed" in result - assert "my_struct_t" in result - assert "logic [7:0] field1;" in result - assert "logic field2;" in result - - def test_unpacked_struct(self): - """Test unpacked struct definition.""" - body = StructBody("unpacked_t", packed=False, typedef=True) - body += "int field1;" - - result = str(body) - assert "typedef struct" in result - assert "packed" not in result or "typedef struct {" in result - assert "unpacked_t" in result - - def test_struct_without_typedef(self): - """Test struct without typedef.""" - body = StructBody("my_struct", packed=True, typedef=False) - body += "logic field;" - - result = str(body) - # When typedef=False, packed is not used - assert "struct {" in result - assert "typedef" not in result - assert "my_struct" in result - - def test_empty_struct(self): - """Test empty struct.""" - body = StructBody("empty_t", packed=True, typedef=True) - result = str(body) - assert "typedef struct packed" in result - assert "empty_t" in result - - def test_nested_struct(self): - """Test struct with nested struct.""" - outer = StructBody("outer_t", packed=True, typedef=True) - inner = StructBody("inner_t", packed=True, typedef=True) - inner += "logic field1;" - outer += "logic field2;" - outer += str(inner) # Include inner struct as a string - - result = str(outer) - assert "outer_t" in result - assert "field2;" in result - # Inner struct should appear in the string - assert "inner_t" in result diff --git a/tests/unit/test_external_nested.py b/tests/unit/test_external_nested.py index a7bc799..ad70bd6 100644 --- a/tests/unit/test_external_nested.py +++ b/tests/unit/test_external_nested.py @@ -1,79 +1,16 @@ """Test handling of external nested addressable components.""" +from collections.abc import Callable from pathlib import Path from tempfile import TemporaryDirectory -import pytest +from systemrdl.node import AddrmapNode from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -@pytest.fixture -def external_nested_rdl(compile_rdl): - """Create an RDL design with external nested addressable components. - - This tests the scenario where an addrmap contains external children - that themselves have external addressable children. - The decoder should only generate select signals for the top-level - external children, not their internal structure. - """ - rdl_source = """ - mem queue_t { - name = "Queue"; - mementries = 1024; - memwidth = 64; - }; - - addrmap port_t { - name = "Port"; - desc = ""; - - external queue_t common[3] @ 0x0 += 0x2000; - external queue_t response @ 0x6000; - }; - - addrmap buffer_t { - name = "Buffer"; - desc = ""; - - port_t multicast @ 0x0; - port_t port [16] @ 0x8000 += 0x8000; - }; - """ - return compile_rdl(rdl_source, top="buffer_t") - - -@pytest.fixture -def nested_addrmap_rdl(compile_rdl): - """Create an RDL design with nested non-external addrmaps for testing depth control.""" - rdl_source = """ - addrmap level2 { - reg { - field { sw=rw; hw=r; } data2[31:0]; - } reg2 @ 0x0; - - reg { - field { sw=rw; hw=r; } data2b[31:0]; - } reg2b @ 0x4; - }; - - addrmap level1 { - reg { - field { sw=rw; hw=r; } data1[31:0]; - } reg1 @ 0x0; - - level2 inner2 @ 0x10; - }; - - addrmap level0 { - level1 inner1 @ 0x0; - }; - """ - return compile_rdl(rdl_source, top="level0") - - -def test_external_nested_components_generate_correct_decoder(external_nested_rdl): +def test_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None: """Test that external nested components generate correct decoder logic. The decoder should: @@ -109,7 +46,7 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl assert "logic [15:0]port;" in content -def test_external_nested_components_generate_correct_interfaces(external_nested_rdl): +def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None: """Test that external nested components generate correct interface ports. The module should have: @@ -140,7 +77,7 @@ def test_external_nested_components_generate_correct_interfaces(external_nested_ assert "m_apb_response" not in content -def test_non_external_nested_components_are_descended(compile_rdl): +def test_non_external_nested_components_are_descended(compile_rdl: Callable[..., AddrmapNode]) -> None: """Test that non-external nested components are still descended into. This is a regression test to ensure we didn't break normal nested @@ -175,7 +112,7 @@ def test_non_external_nested_components_are_descended(compile_rdl): assert "inner_reg" in content -def test_max_decode_depth_parameter_exists(compile_rdl): +def test_max_decode_depth_parameter_exists(compile_rdl: Callable[..., AddrmapNode]) -> None: """Test that max_decode_depth parameter can be set.""" rdl_source = """ addrmap simple { diff --git a/tests/unit/test_generators.py b/tests/unit/test_generators.py deleted file mode 100644 index c2687de..0000000 --- a/tests/unit/test_generators.py +++ /dev/null @@ -1,312 +0,0 @@ -"""Tests for code generation classes.""" - - - -import pytest - -from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -from peakrdl_busdecoder.decode_logic_gen import DecodeLogicFlavor, DecodeLogicGenerator -from peakrdl_busdecoder.design_state import DesignState -from peakrdl_busdecoder.exporter import BusDecoderExporter -from peakrdl_busdecoder.struct_gen import StructGenerator - - -class TestDecodeLogicGenerator: - """Test the DecodeLogicGenerator.""" - - def test_decode_logic_read(self, compile_rdl): - """Test decode logic generation for read operations.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) - - # Basic sanity check - it should initialize - assert gen is not None - assert gen._flavor == DecodeLogicFlavor.READ - - def test_decode_logic_write(self, compile_rdl): - """Test decode logic generation for write operations.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE) - - assert gen is not None - assert gen._flavor == DecodeLogicFlavor.WRITE - - def test_cpuif_addr_predicate(self, compile_rdl): - """Test address predicate generation.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x100; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) - - # Get the register node - reg_node = None - for child in top.children(): - if child.inst_name == "my_reg": - reg_node = child - break - assert reg_node is not None - - predicates = gen.cpuif_addr_predicate(reg_node) - - # Should return a list of conditions - assert isinstance(predicates, list) - assert len(predicates) > 0 - # Should check address bounds - for pred in predicates: - assert "cpuif_rd_addr" in pred or ">=" in pred or "<" in pred - - def test_decode_logic_flavor_enum(self): - """Test DecodeLogicFlavor enum values.""" - assert DecodeLogicFlavor.READ.value == "rd" - assert DecodeLogicFlavor.WRITE.value == "wr" - - assert DecodeLogicFlavor.READ.cpuif_address == "cpuif_rd_addr" - assert DecodeLogicFlavor.WRITE.cpuif_address == "cpuif_wr_addr" - - assert DecodeLogicFlavor.READ.cpuif_select == "cpuif_rd_sel" - assert DecodeLogicFlavor.WRITE.cpuif_select == "cpuif_wr_sel" - - -class TestStructGenerator: - """Test the StructGenerator.""" - - def test_simple_struct_generation(self, compile_rdl): - """Test struct generation for simple register.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - gen = StructGenerator(ds) - - # Should generate struct definition - assert gen is not None - result = str(gen) - - # Should contain struct declaration - assert "struct" in result or "typedef" in result - - def test_nested_struct_generation(self, compile_rdl): - """Test struct generation for nested addrmaps.""" - rdl_source = """ - addrmap inner { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } inner_reg @ 0x0; - }; - - addrmap outer { - inner my_inner @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="outer") - - ds = DesignState(top, {}) - gen = StructGenerator(ds) - - # Walk the tree to generate structs - from systemrdl.walker import RDLWalker - - walker = RDLWalker() - walker.walk(top, gen, skip_top=True) - - result = str(gen) - - # Should contain struct declaration - assert "struct" in result or "typedef" in result - # The struct should reference the inner component - assert "my_inner" in result - - def test_array_struct_generation(self, compile_rdl): - """Test struct generation for register arrays.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_regs[4] @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - gen = StructGenerator(ds) - - # Walk the tree to generate structs - from systemrdl.walker import RDLWalker - - walker = RDLWalker() - walker.walk(top, gen, skip_top=True) - - result = str(gen) - - # Should contain array notation - assert "[" in result and "]" in result - # Should reference the register - assert "my_regs" in result - - -class TestDesignState: - """Test the DesignState class.""" - - def test_design_state_basic(self, compile_rdl): - """Test basic DesignState initialization.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - - assert ds.top_node == top - assert ds.module_name == "test" - assert ds.package_name == "test_pkg" - assert ds.cpuif_data_width == 32 # Should infer from 32-bit field - assert ds.addr_width > 0 - - def test_design_state_custom_module_name(self, compile_rdl): - """Test DesignState with custom module name.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {"module_name": "custom_module"}) - - assert ds.module_name == "custom_module" - assert ds.package_name == "custom_module_pkg" - - def test_design_state_custom_package_name(self, compile_rdl): - """Test DesignState with custom package name.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {"package_name": "custom_pkg"}) - - assert ds.package_name == "custom_pkg" - - def test_design_state_custom_address_width(self, compile_rdl): - """Test DesignState with custom address width.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {"address_width": 16}) - - assert ds.addr_width == 16 - - def test_design_state_unroll_arrays(self, compile_rdl): - """Test DesignState with cpuif_unroll option.""" - rdl_source = """ - addrmap test { - reg { - field { - sw=rw; - hw=r; - } data[31:0]; - } my_regs[4] @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {"cpuif_unroll": True}) - - assert ds.cpuif_unroll is True - - def test_design_state_64bit_registers(self, compile_rdl): - """Test DesignState with wider data width.""" - rdl_source = """ - addrmap test { - reg { - regwidth = 32; - field { - sw=rw; - hw=r; - } data[31:0]; - } my_reg @ 0x0; - }; - """ - top = compile_rdl(rdl_source, top="test") - - ds = DesignState(top, {}) - - # Should infer 32-bit data width from field - assert ds.cpuif_data_width == 32 diff --git a/tests/unroll/__init__.py b/tests/unroll/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unroll/conftest.py b/tests/unroll/conftest.py new file mode 100644 index 0000000..dc7e099 --- /dev/null +++ b/tests/unroll/conftest.py @@ -0,0 +1,40 @@ +from collections.abc import Callable + +import pytest +from systemrdl.node import AddrmapNode + + +@pytest.fixture +def sample_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode: + """Create a simple RDL design with an array.""" + rdl_source = """ + addrmap top { + reg my_reg { + field { + sw=rw; + hw=r; + } data[31:0]; + }; + + my_reg regs[4] @ 0x0 += 0x4; + }; + """ + return compile_rdl(rdl_source) + + +@pytest.fixture +def multidim_array_rdl(compile_rdl: Callable[..., AddrmapNode]) -> AddrmapNode: + """Create an RDL design with a multi-dimensional array.""" + rdl_source = """ + addrmap top { + reg my_reg { + field { + sw=rw; + hw=r; + } data[31:0]; + }; + + my_reg matrix[2][3] @ 0x0 += 0x4; + }; + """ + return compile_rdl(rdl_source) diff --git a/tests/unit/test_unroll.py b/tests/unroll/test_unroll.py similarity index 79% rename from tests/unit/test_unroll.py rename to tests/unroll/test_unroll.py index 7f93c26..c761e81 100644 --- a/tests/unit/test_unroll.py +++ b/tests/unroll/test_unroll.py @@ -1,34 +1,14 @@ -"""Test the --unroll CLI argument functionality.""" - from pathlib import Path from tempfile import TemporaryDirectory -import pytest +from systemrdl.node import AddrmapNode from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder.cpuif.apb3 import APB3Cpuif from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif -@pytest.fixture -def sample_rdl(compile_rdl): - """Create a simple RDL design with an array.""" - rdl_source = """ - addrmap top { - reg my_reg { - field { - sw=rw; - hw=r; - } data[31:0]; - }; - - my_reg regs[4] @ 0x0 += 0x4; - }; - """ - return compile_rdl(rdl_source) - - -def test_unroll_disabled_creates_array_interface(sample_rdl): +def test_unroll_disabled_creates_array_interface(sample_rdl: AddrmapNode) -> None: """Test that with unroll=False, array nodes are kept as arrays.""" with TemporaryDirectory() as tmpdir: exporter = BusDecoderExporter() @@ -38,17 +18,17 @@ def test_unroll_disabled_creates_array_interface(sample_rdl): cpuif_cls=APB4Cpuif, cpuif_unroll=False, ) - + # Read the generated module module_file = Path(tmpdir) / "top.sv" content = module_file.read_text() - + # Should have a single array interface with [4] dimension assert "m_apb_regs [4]" in content - + # Should have a parameter for array size assert "N_REGSS = 4" in content - + # Should NOT have individual indexed interfaces assert "m_apb_regs_0" not in content assert "m_apb_regs_1" not in content @@ -56,7 +36,7 @@ def test_unroll_disabled_creates_array_interface(sample_rdl): assert "m_apb_regs_3" not in content -def test_unroll_enabled_creates_individual_interfaces(sample_rdl): +def test_unroll_enabled_creates_individual_interfaces(sample_rdl: AddrmapNode) -> None: """Test that with unroll=True, array elements are unrolled into separate instances.""" with TemporaryDirectory() as tmpdir: exporter = BusDecoderExporter() @@ -66,31 +46,31 @@ def test_unroll_enabled_creates_individual_interfaces(sample_rdl): cpuif_cls=APB4Cpuif, cpuif_unroll=True, ) - + # Read the generated module module_file = Path(tmpdir) / "top.sv" content = module_file.read_text() - + # Should have individual interfaces without array dimensions assert "m_apb_regs_0," in content or "m_apb_regs_0\n" in content assert "m_apb_regs_1," in content or "m_apb_regs_1\n" in content assert "m_apb_regs_2," in content or "m_apb_regs_2\n" in content assert "m_apb_regs_3" in content - + # Should NOT have array interface assert "m_apb_regs [4]" not in content - + # Should NOT have individual interfaces with array dimensions (the bug we're fixing) assert "m_apb_regs_0 [4]" not in content assert "m_apb_regs_1 [4]" not in content assert "m_apb_regs_2 [4]" not in content assert "m_apb_regs_3 [4]" not in content - + # Should NOT have array size parameter when unrolled assert "N_REGSS" not in content -def test_unroll_with_apb3(sample_rdl): +def test_unroll_with_apb3(sample_rdl: AddrmapNode) -> None: """Test that unroll works correctly with APB3 interface.""" with TemporaryDirectory() as tmpdir: exporter = BusDecoderExporter() @@ -100,40 +80,22 @@ def test_unroll_with_apb3(sample_rdl): cpuif_cls=APB3Cpuif, cpuif_unroll=True, ) - + # Read the generated module module_file = Path(tmpdir) / "top.sv" content = module_file.read_text() - + # Should have individual APB3 interfaces assert "m_apb_regs_0," in content or "m_apb_regs_0\n" in content assert "m_apb_regs_1," in content or "m_apb_regs_1\n" in content assert "m_apb_regs_2," in content or "m_apb_regs_2\n" in content assert "m_apb_regs_3" in content - + # Should NOT have array dimensions on unrolled interfaces assert "m_apb_regs_0 [4]" not in content -@pytest.fixture -def multidim_array_rdl(compile_rdl): - """Create an RDL design with a multi-dimensional array.""" - rdl_source = """ - addrmap top { - reg my_reg { - field { - sw=rw; - hw=r; - } data[31:0]; - }; - - my_reg matrix[2][3] @ 0x0 += 0x4; - }; - """ - return compile_rdl(rdl_source) - - -def test_unroll_multidimensional_array(multidim_array_rdl): +def test_unroll_multidimensional_array(multidim_array_rdl: AddrmapNode) -> None: """Test that unroll works correctly with multi-dimensional arrays.""" with TemporaryDirectory() as tmpdir: exporter = BusDecoderExporter() @@ -143,11 +105,11 @@ def test_unroll_multidimensional_array(multidim_array_rdl): cpuif_cls=APB4Cpuif, cpuif_unroll=True, ) - + # Read the generated module module_file = Path(tmpdir) / "top.sv" content = module_file.read_text() - + # Should have individual interfaces for each element in the 2x3 array # Format should be m_apb_matrix_0_0, m_apb_matrix_0_1, ..., m_apb_matrix_1_2 assert "m_apb_matrix_0_0" in content @@ -156,7 +118,7 @@ def test_unroll_multidimensional_array(multidim_array_rdl): assert "m_apb_matrix_1_0" in content assert "m_apb_matrix_1_1" in content assert "m_apb_matrix_1_2" in content - + # Should NOT have array dimensions on any of the unrolled interfaces for i in range(2): for j in range(3): diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_utils.py b/tests/utils/test_get_indexed_path.py similarity index 60% rename from tests/unit/test_utils.py rename to tests/utils/test_get_indexed_path.py index 4e7fb5d..fc4b3ad 100644 --- a/tests/unit/test_utils.py +++ b/tests/utils/test_get_indexed_path.py @@ -1,96 +1,14 @@ -"""Tests for utility functions.""" +from collections.abc import Callable - - -import pytest -from systemrdl import RDLCompiler from systemrdl.node import AddrmapNode -from peakrdl_busdecoder.utils import clog2, get_indexed_path, is_pow2, roundup_pow2 - - -class TestMathUtils: - """Test mathematical utility functions.""" - - def test_clog2_basic(self): - """Test clog2 function with basic values.""" - assert clog2(1) == 0 - assert clog2(2) == 1 - assert clog2(3) == 2 - assert clog2(4) == 2 - assert clog2(5) == 3 - assert clog2(8) == 3 - assert clog2(9) == 4 - assert clog2(16) == 4 - assert clog2(17) == 5 - assert clog2(32) == 5 - assert clog2(33) == 6 - assert clog2(64) == 6 - assert clog2(128) == 7 - assert clog2(256) == 8 - assert clog2(1024) == 10 - - def test_is_pow2_true_cases(self): - """Test is_pow2 returns True for powers of 2.""" - assert is_pow2(1) is True - assert is_pow2(2) is True - assert is_pow2(4) is True - assert is_pow2(8) is True - assert is_pow2(16) is True - assert is_pow2(32) is True - assert is_pow2(64) is True - assert is_pow2(128) is True - assert is_pow2(256) is True - assert is_pow2(512) is True - assert is_pow2(1024) is True - - def test_is_pow2_false_cases(self): - """Test is_pow2 returns False for non-powers of 2.""" - assert is_pow2(0) is False - assert is_pow2(3) is False - assert is_pow2(5) is False - assert is_pow2(6) is False - assert is_pow2(7) is False - assert is_pow2(9) is False - assert is_pow2(10) is False - assert is_pow2(15) is False - assert is_pow2(17) is False - assert is_pow2(100) is False - assert is_pow2(255) is False - assert is_pow2(1000) is False - - def test_roundup_pow2_already_power_of_2(self): - """Test roundup_pow2 with values that are already powers of 2.""" - assert roundup_pow2(1) == 1 - assert roundup_pow2(2) == 2 - assert roundup_pow2(4) == 4 - assert roundup_pow2(8) == 8 - assert roundup_pow2(16) == 16 - assert roundup_pow2(32) == 32 - assert roundup_pow2(64) == 64 - assert roundup_pow2(128) == 128 - assert roundup_pow2(256) == 256 - - def test_roundup_pow2_non_power_of_2(self): - """Test roundup_pow2 with values that are not powers of 2.""" - assert roundup_pow2(3) == 4 - assert roundup_pow2(5) == 8 - assert roundup_pow2(6) == 8 - assert roundup_pow2(7) == 8 - assert roundup_pow2(9) == 16 - assert roundup_pow2(15) == 16 - assert roundup_pow2(17) == 32 - assert roundup_pow2(31) == 32 - assert roundup_pow2(33) == 64 - assert roundup_pow2(100) == 128 - assert roundup_pow2(255) == 256 - assert roundup_pow2(257) == 512 +from peakrdl_busdecoder.utils import get_indexed_path class TestGetIndexedPath: """Test get_indexed_path function.""" - def test_simple_path(self, compile_rdl): + def test_simple_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test simple path without arrays.""" rdl_source = """ addrmap my_addrmap { @@ -111,7 +29,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node) assert path == "my_reg" - def test_nested_path(self, compile_rdl): + def test_nested_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test nested path without arrays.""" rdl_source = """ addrmap inner_map { @@ -143,7 +61,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node) assert path == "inner.my_reg" - def test_array_path(self, compile_rdl): + def test_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test path with array indices.""" rdl_source = """ addrmap my_addrmap { @@ -163,7 +81,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node) assert path == "my_reg[i0]" - def test_multidimensional_array_path(self, compile_rdl): + def test_multidimensional_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test path with multidimensional arrays.""" rdl_source = """ addrmap my_addrmap { @@ -183,7 +101,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node) assert path == "my_reg[i0][i1]" - def test_nested_array_path(self, compile_rdl): + def test_nested_array_path(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test path with nested arrays.""" rdl_source = """ addrmap inner_map { @@ -215,7 +133,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node) assert path == "inner[i0].my_reg[i1]" - def test_custom_indexer(self, compile_rdl): + def test_custom_indexer(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test path with custom indexer name.""" rdl_source = """ addrmap my_addrmap { @@ -235,7 +153,7 @@ class TestGetIndexedPath: path = get_indexed_path(top, reg_node, indexer="idx") assert path == "my_reg[idx0]" - def test_skip_kw_filter(self, compile_rdl): + def test_skip_kw_filter(self, compile_rdl: Callable[..., AddrmapNode]) -> None: """Test path with keyword filtering skipped.""" rdl_source = """ addrmap my_addrmap { diff --git a/tests/utils/test_math_utils.py b/tests/utils/test_math_utils.py new file mode 100644 index 0000000..f19fb13 --- /dev/null +++ b/tests/utils/test_math_utils.py @@ -0,0 +1,79 @@ +from peakrdl_busdecoder.utils import clog2, is_pow2, roundup_pow2 + + +class TestMathUtils: + """Test mathematical utility functions.""" + + def test_clog2_basic(self) -> None: + """Test clog2 function with basic values.""" + assert clog2(1) == 0 + assert clog2(2) == 1 + assert clog2(3) == 2 + assert clog2(4) == 2 + assert clog2(5) == 3 + assert clog2(8) == 3 + assert clog2(9) == 4 + assert clog2(16) == 4 + assert clog2(17) == 5 + assert clog2(32) == 5 + assert clog2(33) == 6 + assert clog2(64) == 6 + assert clog2(128) == 7 + assert clog2(256) == 8 + assert clog2(1024) == 10 + + def test_is_pow2_true_cases(self) -> None: + """Test is_pow2 returns True for powers of 2.""" + assert is_pow2(1) is True + assert is_pow2(2) is True + assert is_pow2(4) is True + assert is_pow2(8) is True + assert is_pow2(16) is True + assert is_pow2(32) is True + assert is_pow2(64) is True + assert is_pow2(128) is True + assert is_pow2(256) is True + assert is_pow2(512) is True + assert is_pow2(1024) is True + + def test_is_pow2_false_cases(self) -> None: + """Test is_pow2 returns False for non-powers of 2.""" + assert is_pow2(0) is False + assert is_pow2(3) is False + assert is_pow2(5) is False + assert is_pow2(6) is False + assert is_pow2(7) is False + assert is_pow2(9) is False + assert is_pow2(10) is False + assert is_pow2(15) is False + assert is_pow2(17) is False + assert is_pow2(100) is False + assert is_pow2(255) is False + assert is_pow2(1000) is False + + def test_roundup_pow2_already_power_of_2(self) -> None: + """Test roundup_pow2 with values that are already powers of 2.""" + assert roundup_pow2(1) == 1 + assert roundup_pow2(2) == 2 + assert roundup_pow2(4) == 4 + assert roundup_pow2(8) == 8 + assert roundup_pow2(16) == 16 + assert roundup_pow2(32) == 32 + assert roundup_pow2(64) == 64 + assert roundup_pow2(128) == 128 + assert roundup_pow2(256) == 256 + + def test_roundup_pow2_non_power_of_2(self) -> None: + """Test roundup_pow2 with values that are not powers of 2.""" + assert roundup_pow2(3) == 4 + assert roundup_pow2(5) == 8 + assert roundup_pow2(6) == 8 + assert roundup_pow2(7) == 8 + assert roundup_pow2(9) == 16 + assert roundup_pow2(15) == 16 + assert roundup_pow2(17) == 32 + assert roundup_pow2(31) == 32 + assert roundup_pow2(33) == 64 + assert roundup_pow2(100) == 128 + assert roundup_pow2(255) == 256 + assert roundup_pow2(257) == 512 diff --git a/tools/shims/xargs b/tools/shims/xargs new file mode 100755 index 0000000..30a0e8c --- /dev/null +++ b/tools/shims/xargs @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +"""Minimal xargs replacement for environments where /usr/bin/xargs is unavailable. + +Supports the subset of functionality exercised by Verilator's generated makefiles: +optional -0 (NUL-delimited input) and -t (echo command) flags, followed by a command +invocation constructed from stdin tokens. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def main() -> int: + args = sys.argv[1:] + show_cmd = False + null_delimited = False + + while args and args[0].startswith("-") and args[0] != "-": + opt = args.pop(0) + if opt == "-0": + null_delimited = True + elif opt == "-t": + show_cmd = True + else: + sys.stderr.write(f"xargs shim: unsupported option {opt}\n") + return 1 + + if not args: + args = ["echo"] + + data = sys.stdin.buffer.read() + if not data.strip(): + return 0 + + if null_delimited: + items = [chunk.decode() for chunk in data.split(b"\0") if chunk] + else: + items = data.decode().split() + + if not items: + return 0 + + cmd = args + items + if show_cmd: + print(" ".join(cmd)) + + try: + subprocess.check_call(cmd) + except FileNotFoundError: + return 127 + except subprocess.CalledProcessError as exc: + return exc.returncode + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())