fix(fpga): TX-N9 — sim-only payload-hold checker on cmd CDC

cmd_data / cmd_opcode / cmd_addr / cmd_value feed downstream CDC sync
chains; the safety property is that they only change on the cycle
cmd_valid rises (RD_PROCESS), and stay held on every other cycle so the
receiver's 2-FF synchronizer sees a clean payload regardless of where
its sample window lands. The FSM satisfies this implicitly today, but
nothing flagged a regression that introduced a stray write somewhere
in the same always block.

Added an `ifdef SIMULATION block at the bottom of both
usb_data_interface.v (FT601 / ft601_clk_in / ft601_reset_n) and
usb_data_interface_ft2232h.v (FT2232H / ft_clk / ft_reset_n). It
snapshots the payload + cmd_valid each cycle and fires
[ASSERT FAIL] TX-N9: cmd_<field> changed while cmd_valid=0 (old -> new)
on any payload change while cmd_valid is low. Local regs suffixed _n9
to avoid future name collisions. Synthesis-inert.

Quick FPGA regression unchanged: USB Data Interface 91/91 PASS, overall
28/29 (same baseline; the 1 fail is the pre-existing iverilog/Xilinx-IP
RX-NEW-3 gap).
This commit is contained in:
Jason
2026-04-28 10:03:08 +05:45
parent 26f8d1fa72
commit 5d334bfdd6
2 changed files with 106 additions and 0 deletions

View File

@@ -682,4 +682,52 @@ end
assign ft601_clk_out = ft601_clk_in;
`endif
// ============================================================================
// TX-N9: payload-hold checker (simulation only)
//
// cmd_data / cmd_opcode / cmd_addr / cmd_value feed downstream CDC sync
// chains; safety property is that they only change on the cycle cmd_valid
// rises (RD_PROCESS), and are held stable on every other cycle. The FSM
// satisfies this implicitly today; this checker fires `[ASSERT FAIL]` on
// any payload change while cmd_valid is low so a future regression is
// caught in the simulation log. Synthesis-inert.
// ============================================================================
`ifdef SIMULATION
reg [31:0] cmd_data_prev_n9;
reg [7:0] cmd_opcode_prev_n9;
reg [7:0] cmd_addr_prev_n9;
reg [15:0] cmd_value_prev_n9;
reg cmd_valid_prev_n9;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n) begin
cmd_data_prev_n9 <= 32'd0;
cmd_opcode_prev_n9 <= 8'd0;
cmd_addr_prev_n9 <= 8'd0;
cmd_value_prev_n9 <= 16'd0;
cmd_valid_prev_n9 <= 1'b0;
end else begin
if (!cmd_valid && !cmd_valid_prev_n9) begin
if (cmd_data !== cmd_data_prev_n9)
$display("[ASSERT FAIL] TX-N9: cmd_data changed while cmd_valid=0 (%h -> %h)",
cmd_data_prev_n9, cmd_data);
if (cmd_opcode !== cmd_opcode_prev_n9)
$display("[ASSERT FAIL] TX-N9: cmd_opcode changed while cmd_valid=0 (%h -> %h)",
cmd_opcode_prev_n9, cmd_opcode);
if (cmd_addr !== cmd_addr_prev_n9)
$display("[ASSERT FAIL] TX-N9: cmd_addr changed while cmd_valid=0 (%h -> %h)",
cmd_addr_prev_n9, cmd_addr);
if (cmd_value !== cmd_value_prev_n9)
$display("[ASSERT FAIL] TX-N9: cmd_value changed while cmd_valid=0 (%h -> %h)",
cmd_value_prev_n9, cmd_value);
end
cmd_data_prev_n9 <= cmd_data;
cmd_opcode_prev_n9 <= cmd_opcode;
cmd_addr_prev_n9 <= cmd_addr;
cmd_value_prev_n9 <= cmd_value;
cmd_valid_prev_n9 <= cmd_valid;
end
end
`endif
endmodule

View File

@@ -967,4 +967,62 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
end
end
// ============================================================================
// TX-N9: payload-hold checker (simulation only)
//
// The cmd_* outputs feed a CDC sync chain in the consumer clock domain.
// Safety property: cmd_data / cmd_opcode / cmd_addr / cmd_value must only
// change on the cycle that cmd_valid rises (RD_PROCESS in this module).
// On every other cycle they must be held stable so the receiver's 2-FF
// synchronizer sees a clean payload regardless of where its sample window
// lands relative to cmd_valid.
//
// The current FSM satisfies this implicitly the payload regs are only
// written in RD_PROCESS, never elsewhere but until now nothing flagged
// a regression that introduced a stray write somewhere in the always
// block. This checker fires `[ASSERT FAIL]` on any payload change while
// cmd_valid is low, surfacing the violation in the simulation log
// without affecting synthesis.
// ============================================================================
`ifdef SIMULATION
reg [31:0] cmd_data_prev;
reg [7:0] cmd_opcode_prev;
reg [7:0] cmd_addr_prev;
reg [15:0] cmd_value_prev;
reg cmd_valid_prev;
always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_reset_n) begin
cmd_data_prev <= 32'd0;
cmd_opcode_prev <= 8'd0;
cmd_addr_prev <= 8'd0;
cmd_value_prev <= 16'd0;
cmd_valid_prev <= 1'b0;
end else begin
// Payload may legally change only on the cycle cmd_valid rises
// (cmd_valid_prev=0, cmd_valid=1). Any other change is a hold
// violation.
if (!cmd_valid && !cmd_valid_prev) begin
if (cmd_data !== cmd_data_prev)
$display("[ASSERT FAIL] TX-N9: cmd_data changed while cmd_valid=0 (%h -> %h)",
cmd_data_prev, cmd_data);
if (cmd_opcode !== cmd_opcode_prev)
$display("[ASSERT FAIL] TX-N9: cmd_opcode changed while cmd_valid=0 (%h -> %h)",
cmd_opcode_prev, cmd_opcode);
if (cmd_addr !== cmd_addr_prev)
$display("[ASSERT FAIL] TX-N9: cmd_addr changed while cmd_valid=0 (%h -> %h)",
cmd_addr_prev, cmd_addr);
if (cmd_value !== cmd_value_prev)
$display("[ASSERT FAIL] TX-N9: cmd_value changed while cmd_valid=0 (%h -> %h)",
cmd_value_prev, cmd_value);
end
cmd_data_prev <= cmd_data;
cmd_opcode_prev <= cmd_opcode;
cmd_addr_prev <= cmd_addr;
cmd_value_prev <= cmd_value;
cmd_valid_prev <= cmd_valid;
end
end
`endif
endmodule