chore(tests): retire v1 cross-layer iverilog cosim tier

The v1-era tb_cross_layer_ft2232h.v cosim TB no longer matches
production after the protocol-v2 / opcode dispatch rework (PR-G).
Equivalent v2 coverage now lives in the FPGA regression's
tb_usb_protocol_v2.v and tb_system_opcodes.v.

Removed:
  - tb_cross_layer_ft2232h.v (716 lines)
  - Tier 2 (Verilog cosimulation) from test_cross_layer_contract.py
  - iverilog/vvp tool detection and CI install step in ci-tests.yml

Tier 1 (static parser) and Tier 3 (C stub execution) remain. CI
no longer needs apt-get install iverilog.

contract_parser.py updated to reflect the slimmer two-tier model.
This commit is contained in:
Jason
2026-05-04 21:06:34 +05:45
parent b84aa6a6f3
commit 416601d1d0
4 changed files with 196 additions and 978 deletions
+122 -36
View File
@@ -128,7 +128,16 @@ def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]
def parse_python_packet_constants(filepath: Path | None = None) -> dict[str, PacketConstants]:
"""Extract HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, packet sizes."""
"""
Extract HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, STATUS_PACKET_SIZE.
Note on the data packet: PR-G replaced the fixed 11-byte v1 frame with a
variable-length bulk frame (header + optional sections + footer), so a
single ``DATA_PACKET_SIZE`` constant no longer characterizes the data
layer. Python keeps ``DATA_PACKET_SIZE = 11`` as a back-compat alias for
legacy FT601 log files; we deliberately do NOT cross-check it against
the FPGA, which has no equivalent localparam in either USB module.
"""
if filepath is None:
filepath = GUI_DIR / "radar_protocol.py"
text = filepath.read_text()
@@ -140,14 +149,11 @@ def parse_python_packet_constants(filepath: Path | None = None) -> dict[str, Pac
val = m.group(1)
return int(val, 16) if val.startswith("0x") else int(val)
header = _find(r'HEADER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)')
footer = _find(r'FOOTER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)')
status_header = _find(r'STATUS_HEADER_BYTE\s*=\s*(0x[0-9a-fA-F]+|\d+)')
data_size = _find(r'DATA_PACKET_SIZE\s*=\s*(\d+)')
status_size = _find(r'STATUS_PACKET_SIZE\s*=\s*(\d+)')
return {
"data": PacketConstants(header=header, footer=footer, size=data_size),
"status": PacketConstants(header=status_header, footer=footer, size=status_size),
}
@@ -372,39 +378,106 @@ def parse_verilog_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry
return opcodes
def _resolve_verilog_value(rhs: str, macros: dict[str, int]) -> int | None:
"""
Resolve a Verilog RHS expression to an integer. Supports:
* Plain decimal: ``1234``
* Verilog literal: ``16'd1234``, ``8'hAA``, ``2'b01``, ``6'b000_111``
* Macro reference: ```MACRO_NAME`` (looked up in *macros*)
* Width-prefixed macro: ``16'd`MACRO_NAME``
Returns None when the RHS can't be resolved (e.g. RHS is a wire name,
concatenation, or an undefined macro).
"""
rhs = rhs.strip()
# Width-prefixed form: "16'd...", "8'h...", "2'b...", "4'o..."
width_match = re.match(r"^(\d+)'([bdho])(.*)$", rhs)
if width_match:
base_char = width_match.group(2)
rest = width_match.group(3).strip()
# Width-prefixed macro reference: 16'd`RP_DEF_FOO
if rest.startswith("`"):
return macros.get(rest[1:].strip())
digits = rest.replace("_", "")
if not re.fullmatch(r"[0-9a-fA-F]+", digits):
return None
base = {"b": 2, "d": 10, "h": 16, "o": 8}[base_char]
try:
return int(digits, base)
except ValueError:
return None
# Bare macro reference: `MACRO_NAME
if rhs.startswith("`"):
return macros.get(rhs[1:].strip())
# Plain decimal
if rhs.isdigit():
return int(rhs)
return None
def parse_radar_params_macros(filepath: Path | None = None) -> dict[str, int]:
"""
Parse `define directives in radar_params.vh into a name → integer map.
Resolves up to two macro→macro indirections (none expected today, kept
for forward compatibility).
"""
if filepath is None:
filepath = FPGA_DIR / "radar_params.vh"
text = filepath.read_text()
raw: dict[str, str] = {}
for line in text.splitlines():
m = re.match(r"^\s*`define\s+(\w+)\s+(\S.*?)(?:\s*//.*)?\s*$", line)
if m:
raw[m.group(1)] = m.group(2).strip()
macros: dict[str, int] = {}
for _ in range(3): # bounded fixed-point — file is small, runs in microseconds
progressed = False
for name, rhs in raw.items():
if name in macros:
continue
val = _resolve_verilog_value(rhs, macros)
if val is not None:
macros[name] = val
progressed = True
if not progressed:
break
return macros
def parse_verilog_reset_defaults(filepath: Path | None = None) -> dict[str, int]:
"""
Parse the reset block from radar_system_top.v.
Returns {register_name: reset_value}.
Expands ```RP_DEF_*`` style macro references against radar_params.vh so
that fields like ``host_long_chirp_cycles <= 16'd`RP_DEF_LONG_CHIRP_CYCLES``
resolve to integers instead of being silently dropped.
"""
if filepath is None:
filepath = FPGA_DIR / "radar_system_top.v"
text = filepath.read_text()
macros = parse_radar_params_macros()
defaults: dict[str, int] = {}
# Match patterns like: host_radar_mode <= 2'b01;
# Also: host_detect_threshold <= 16'd10000;
for m in re.finditer(
r'(host_\w+)\s*<=\s*(\d+\'[bdho][0-9a-fA-F_]+|\d+)\s*;',
text
):
# Capture every "host_X <= <expr>;" assignment, regardless of RHS form.
# Resolution to an integer happens via _resolve_verilog_value, which
# rejects (returns None for) RHSes that aren't statically known
# constants (e.g. concatenations or wire names from the opcode decode
# block — those land below the reset block, and we keep only the first
# occurrence anyway).
for m in re.finditer(r'(host_\w+)\s*<=\s*([^;]+?)\s*;', text):
reg = m.group(1)
val_str = m.group(2)
# Parse Verilog literal
if "'" in val_str:
base_char = val_str.split("'")[1][0].lower()
digits = val_str.split("'")[1][1:].replace("_", "")
base = {"b": 2, "d": 10, "h": 16, "o": 8}[base_char]
value = int(digits, base)
else:
value = int(val_str)
# Only keep first occurrence (the reset block comes before the
# opcode decode which also has <= assignments)
if reg not in defaults:
defaults[reg] = value
if reg in defaults:
continue # reset block precedes opcode block; first wins
val = _resolve_verilog_value(m.group(2), macros)
if val is not None:
defaults[reg] = val
return defaults
@@ -435,7 +508,15 @@ def parse_verilog_register_widths(filepath: Path | None = None) -> dict[str, int
def parse_verilog_packet_constants(
filepath: Path | None = None,
) -> dict[str, PacketConstants]:
"""Extract HEADER, FOOTER, STATUS_HEADER, packet size localparams."""
"""
Extract HEADER, FOOTER, STATUS_HEADER, STATUS_PKT_LEN localparams.
Note: ``DATA_PKT_LEN`` was retired in PR-G when the data path moved to a
variable-length bulk frame (9-byte header + optional sections + 1-byte
footer). There is no equivalent constant in the v2 module; the data
layer is exercised separately by `parse_verilog_data_mux()` against the
fixed 9-byte header section.
"""
if filepath is None:
filepath = FPGA_DIR / "usb_data_interface_ft2232h.v"
text = filepath.read_text()
@@ -454,15 +535,11 @@ def parse_verilog_packet_constants(
return int(vlog_m.group(1))
return int(val, 16) if val.startswith("0x") else int(val)
header_val = _find(r"localparam\s+HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)")
footer_val = _find(r"localparam\s+FOOTER\s*=\s*(\d+'h[0-9a-fA-F]+)")
status_hdr = _find(r"localparam\s+STATUS_HEADER\s*=\s*(\d+'h[0-9a-fA-F]+)")
data_size = _find(r"DATA_PKT_LEN\s*=\s*(\d+'d\d+)")
status_size = _find(r"STATUS_PKT_LEN\s*=\s*(\d+'d\d+)")
return {
"data": PacketConstants(header=header_val, footer=footer_val, size=data_size),
"status": PacketConstants(header=status_hdr, footer=footer_val, size=status_size),
}
@@ -582,26 +659,35 @@ def parse_verilog_data_mux(
filepath: Path | None = None,
) -> list[DataPacketField]:
"""
Parse the data_pkt_byte mux from usb_data_interface_ft2232h.v.
Returns fields with byte positions and signal names.
Parse the v2 data-frame 9-byte fixed header mux from
usb_data_interface_ft2232h.v. Returns fields with byte positions and
signal names.
PR-G replaced the v1 11-byte fixed data packet (combinational
``always @(*) begin case (wr_byte_idx) ... data_pkt_byte = ...``) with
a clocked FSM that emits a fixed 9-byte header followed by optional
variable-length sections. This parser walks the WR_FRAME_HEADER
``case (wr_byte_idx[3:0]) ... ft_data_out <= ...`` block.
"""
if filepath is None:
filepath = FPGA_DIR / "usb_data_interface_ft2232h.v"
text = filepath.read_text()
# Find the data mux case block
# Find the WR_FRAME_HEADER mux: the case block that drives ft_data_out
# from the low 4 bits of the byte index, one assignment per fixed-header
# byte (4'd0..4'd8).
match = re.search(
r'always\s+@\(\*\)\s+begin\s+case\s*\(wr_byte_idx\)(.*?)endcase',
r'case\s*\(\s*wr_byte_idx\s*\[\s*3\s*:\s*0\s*\]\s*\)(.*?)endcase',
text, re.DOTALL
)
if not match:
raise ValueError("Could not find data_pkt_byte mux")
raise ValueError("Could not find v2 data-frame header mux")
mux_body = match.group(1)
entries: list[tuple[int, str]] = []
for m in re.finditer(
r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);",
r"4'd(\d+)\s*:\s*ft_data_out\s*<=\s*(.+?);",
mux_body, re.DOTALL
):
idx = int(m.group(1))