SPI · Module 15
FPGA I/O Registers and Timing Closure
A pad register is free and usable under exactly one rule: nothing between the pin and the register. Two designs computing the same selection, one placeable and one not, are bit-for-bit identical under a static select and are not the same circuit — the difference lives between clock edges, so diffing them cycle by cycle proves nothing.
A pad register is a flip-flop that lives in the I/O block rather than in the fabric. On every FPGA family there is one per pin, it is already paid for, and a path that ends in it has essentially no routing between the pin and the flop — which is usually the difference between meeting an input setup requirement and not.
There is exactly one rule for using it:
Nothing may come between the pin and the register.
Not a mux, not a gate, not an inverter the tool cannot absorb. The register has to be the first thing the signal meets.
That rule is easy to state and easy to break by accident, because the natural way to write a design breaks it.
1. The Same Function, Twice
An N-bit input bus, of which the design needs one bit, chosen by a configuration input:
out = in_pins[sel] PAD FIRST register all N bits at the pads, then select.
N flops, all placeable, no logic before any of them.
The select is combinational AFTER the register.
LOGIC FIRST select at the pads, then register the chosen bit.
ONE flop, and it cannot go in a pad, because there is an N-to-1
mux in front of it.
The select is combinational BEFORE the register.The second is what a designer writes when thinking about area, and it is smaller by N-1 flops. It is also the one that will not close timing, and no simulation will ever say so.
2. Bit-For-Bit Identical, Under A Static Select
Two hundred random patterns at each of four select values:
sel samples disagreements
0 200 0
1 200 0
2 200 0
3 200 0Eight hundred samples, zero disagreements. That is what licenses the refactor: a design that only changes sel between transactions can move from the smaller form to the pad-placeable one with no functional change at all.
And an SPI slave's sel is exactly that — a configuration register written while the bus is idle. The measurement for that case specifically: zero disagreements across 200 samples, so for a real slave the two forms are interchangeable and the placeable one is free to choose.
3. And They Are Not The Same Circuit
Finding that took two attempts, and the second attempt is the useful part.
Compared at clock edges they agree on any stimulus. sel is stable across the clock edge, so pad_r[sel] and the registered in_pins[sel] were computed from the same pair. Two hundred samples of fully random data and select, compared at every posedge: zero disagreements.
That is the result a reasonable bench produces, and it is the wrong conclusion to draw from it.
The difference lives BETWEEN edges. Move sel after the data has been captured and compare before the next capture, and the two forms differ in 81 of 200 samples — because pad-first re-selects from the value already in its register while logic-first keeps what it selected earlier.
So the trade is three-sided rather than two:
pad first N flops placeable `sel` takes effect on CAPTURED data
logic first 1 flop not placeable `sel` takes effect on ARRIVING dataA design that switches sel between transactions cannot tell the difference and should take the pad-first version. A design that switches it during a transaction has chosen a behaviour, and should say which one it wanted.
4. What The Attributes Do, And What They Do Not
IOB = "TRUE" (Xilinx) or the Intel equivalent is a request. The tool honours it when the rule in the introduction is satisfied and warns when it is not — and a warning in a log of ten thousand lines is not a mechanism.
The mechanism is the structure. The attribute only makes the tool's refusal visible.
This is the same shape as Chapter 14.1's ASYNC_REG: the attribute does not create the property, it stops the tool destroying it, and the property has to be there to begin with.
The design in §5 deliberately omits the attribute, and the comment says why: it is vendor-specific, it belongs with the constraints (Chapter 15.7), and including one in the RTL would suggest that the attribute is what makes the design work. It is not — the absence of logic is.
5. Building Both Forms — Three HDLs
The circuit
The same selection twice, differing only in which side of the register the mux sits on. The internal check asserts the property that holds — equality under a static select — rather than the one that does not.
// spi_io_regs.sv
//
// Chapter 15.8 -- using the I/O registers instead of fighting the fabric.
//
// A pad register is a flip-flop that lives in the I/O block rather than in the
// fabric. On every FPGA family there is one per pin, it is already paid for, and a
// path that ends in it has essentially no routing between the pin and the flop --
// which is usually the difference between meeting an input setup requirement and
// not.
//
// There is exactly one rule for using it: NOTHING MAY COME BETWEEN THE PIN AND THE
// REGISTER. Not a mux, not a gate, not an inverter that the tool cannot absorb. The
// register has to be the first thing the signal meets.
//
// That rule is easy to state and easy to break by accident, because the natural way
// to write a design breaks it. This file builds the same function twice -- once
// obeying the rule and once not -- so that the difference can be examined rather
// than asserted.
//
// THE FUNCTION. An N-bit input bus, of which the design needs one bit, chosen by a
// configuration input:
//
// out = in_pins[sel]
//
// THE TWO IMPLEMENTATIONS.
//
// PAD FIRST register all N bits at the pads, then select.
// N flops, all of them placeable in pads, no logic before any of
// them. The select is combinational AFTER the register.
//
// LOGIC FIRST select at the pads, then register the chosen bit.
// ONE flop, and it cannot go in a pad, because there is an N-to-1
// mux in front of it. The select is combinational BEFORE the register.
//
// The second is what a designer writes when thinking about area, and it is smaller
// by N-1 flops. It is also the one that will not close timing, and no simulation
// will ever say so.
//
// AND THEY ARE NOT QUITE THE SAME CIRCUIT, WHICH IS THE INTERESTING PART.
//
// With `sel` held still the two are indistinguishable: same value, same latency, bit
// for bit, forever. Change `sel` while data is moving and they differ by one cycle
// in when the change takes effect -- pad-first re-selects from data already captured,
// logic-first selects before capturing.
//
// So the trade is three-sided rather than two:
//
// pad first N flops placeable `sel` takes effect on captured data
// logic first 1 flop not `sel` takes effect on arriving data
//
// A design that switches `sel` between transactions -- which is what a real
// configuration register does -- cannot tell the difference, and should take the
// pad-first version. A design that switches `sel` DURING a transaction has chosen a
// behaviour, and should say which one it wanted.
//
// WHAT THE ATTRIBUTES DO, AND WHAT THEY DO NOT.
//
// `IOB = "TRUE"` (Xilinx) or the equivalent Intel assignment is a REQUEST. The tool
// honours it when the rule above is satisfied and warns when it is not -- and a
// warning in a log of ten thousand lines is not a mechanism. The mechanism is the
// structure; the attribute only makes the tool's refusal visible.
//
// This is the same shape as Chapter 14.1's `ASYNC_REG`: the attribute does not create
// the property, it stops the tool destroying it, and the property has to be there to
// begin with.
module spi_io_regs #(
parameter int N = 4, // how many pins arrive
parameter int SW = 2 // width of the select; N <= 2**SW
) (
input wire clk,
input wire rst_n,
input wire [N-1:0] in_pins, // asynchronous, straight off the pads
input wire [SW-1:0] sel,
// --- pad first: register every bit, then select ------------------------
output wire out_pad_first,
// --- logic first: select, then register one bit ------------------------
output reg out_logic_first
);
// ---------------------------------------------------------------- pad first
// N flops with NOTHING in front of them. On Xilinx this carries
//
// (* IOB = "TRUE" *)
//
// and on Intel an ALTERA_ATTRIBUTE requesting a fast input register. The
// attribute is omitted from this file deliberately: it is vendor-specific, it
// belongs with the constraints (Chapter 15.7), and including one here would
// suggest that the attribute is what makes this work. It is not -- the absence
// of logic is.
reg [N-1:0] pad_r;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) pad_r <= {N{1'b0}};
else pad_r <= in_pins;
end
// The select is AFTER the register, so it is fabric logic on a path that has a
// whole clock period, rather than logic between a pin and a flop.
assign out_pad_first = pad_r[sel];
// ------------------------------------------------------------- logic first
// One flop, and an N-to-1 mux in front of it. Functionally this is the same
// selection; structurally it is the version that cannot use a pad register,
// because the mux is between the pin and the flop.
//
// Note how ordinary it looks. Nothing about it reads as a mistake, and that is
// why it gets written: it is smaller, it simulates identically under a static
// select, and the only thing that objects is a place-and-route report.
wire sel_first = in_pins[sel];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) out_logic_first <= 1'b0;
else out_logic_first <= sel_first;
end
`ifdef SPI_CHECKS
// The property worth asserting is the one that holds: with `sel` unchanged from
// one cycle to the next, the two implementations agree. Asserting that they
// always agree would be asserting something false, and asserting nothing would
// miss the case that matters -- a refactor from one form to the other is safe
// exactly when the select is static, and this is where that gets checked.
reg [SW-1:0] sel_d;
reg primed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_d <= {SW{1'b0}};
primed <= 1'b0;
end else begin
if (primed && sel == sel_d && out_pad_first !== out_logic_first)
$fatal(1, "the two implementations disagree with a static select");
sel_d <= sel;
primed <= 1'b1;
end
end
`endif
endmodule// spi_io_regs.v
//
// Chapter 15.8 -- using the I/O registers instead of fighting the fabric.
//
// A pad register is a flip-flop that lives in the I/O block rather than in the
// fabric. On every FPGA family there is one per pin, it is already paid for, and a
// path that ends in it has essentially no routing between the pin and the flop --
// which is usually the difference between meeting an input setup requirement and
// not.
//
// There is exactly one rule for using it: NOTHING MAY COME BETWEEN THE PIN AND THE
// REGISTER. Not a mux, not a gate, not an inverter that the tool cannot absorb. The
// register has to be the first thing the signal meets.
//
// That rule is easy to state and easy to break by accident, because the natural way
// to write a design breaks it. This file builds the same function twice -- once
// obeying the rule and once not -- so that the difference can be examined rather
// than asserted.
//
// THE FUNCTION. An N-bit input bus, of which the design needs one bit, chosen by a
// configuration input:
//
// out = in_pins[sel]
//
// THE TWO IMPLEMENTATIONS.
//
// PAD FIRST register all N bits at the pads, then select.
// N flops, all of them placeable in pads, no logic before any of
// them. The select is combinational AFTER the register.
//
// LOGIC FIRST select at the pads, then register the chosen bit.
// ONE flop, and it cannot go in a pad, because there is an N-to-1
// mux in front of it. The select is combinational BEFORE the register.
//
// The second is what a designer writes when thinking about area, and it is smaller
// by N-1 flops. It is also the one that will not close timing, and no simulation
// will ever say so.
//
// AND THEY ARE NOT QUITE THE SAME CIRCUIT, WHICH IS THE INTERESTING PART.
//
// With `sel` held still the two are indistinguishable: same value, same latency, bit
// for bit, forever. Change `sel` while data is moving and they differ by one cycle
// in when the change takes effect -- pad-first re-selects from data already captured,
// logic-first selects before capturing.
//
// So the trade is three-sided rather than two:
//
// pad first N flops placeable `sel` takes effect on captured data
// logic first 1 flop not `sel` takes effect on arriving data
//
// A design that switches `sel` between transactions -- which is what a real
// configuration register does -- cannot tell the difference, and should take the
// pad-first version. A design that switches `sel` DURING a transaction has chosen a
// behaviour, and should say which one it wanted.
//
// WHAT THE ATTRIBUTES DO, AND WHAT THEY DO NOT.
//
// `IOB = "TRUE"` (Xilinx) or the equivalent Intel assignment is a REQUEST. The tool
// honours it when the rule above is satisfied and warns when it is not -- and a
// warning in a log of ten thousand lines is not a mechanism. The mechanism is the
// structure; the attribute only makes the tool's refusal visible.
//
// This is the same shape as Chapter 14.1's `ASYNC_REG`: the attribute does not create
// the property, it stops the tool destroying it, and the property has to be there to
// begin with.
module spi_io_regs #(
parameter N = 4, // how many pins arrive
parameter SW = 2 // width of the select; N <= 2**SW
) (
input wire clk,
input wire rst_n,
input wire [N-1:0] in_pins, // asynchronous, straight off the pads
input wire [SW-1:0] sel,
// --- pad first: register every bit, then select ------------------------
output wire out_pad_first,
// --- logic first: select, then register one bit ------------------------
output reg out_logic_first
);
// ---------------------------------------------------------------- pad first
// N flops with NOTHING in front of them. On Xilinx this carries
//
// (* IOB = "TRUE" *)
//
// and on Intel an ALTERA_ATTRIBUTE requesting a fast input register. The
// attribute is omitted from this file deliberately: it is vendor-specific, it
// belongs with the constraints (Chapter 15.7), and including one here would
// suggest that the attribute is what makes this work. It is not -- the absence
// of logic is.
reg [N-1:0] pad_r;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) pad_r <= {N{1'b0}};
else pad_r <= in_pins;
end
// The select is AFTER the register, so it is fabric logic on a path that has a
// whole clock period, rather than logic between a pin and a flop.
assign out_pad_first = pad_r[sel];
// ------------------------------------------------------------- logic first
// One flop, and an N-to-1 mux in front of it. Functionally this is the same
// selection; structurally it is the version that cannot use a pad register,
// because the mux is between the pin and the flop.
//
// Note how ordinary it looks. Nothing about it reads as a mistake, and that is
// why it gets written: it is smaller, it simulates identically under a static
// select, and the only thing that objects is a place-and-route report.
wire sel_first = in_pins[sel];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) out_logic_first <= 1'b0;
else out_logic_first <= sel_first;
end
`ifdef SPI_CHECKS
// The property worth asserting is the one that holds: with `sel` unchanged from
// one cycle to the next, the two implementations agree. Asserting that they
// always agree would be asserting something false, and asserting nothing would
// miss the case that matters -- a refactor from one form to the other is safe
// exactly when the select is static, and this is where that gets checked.
reg [SW-1:0] sel_d;
reg primed;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_d <= {SW{1'b0}};
primed <= 1'b0;
end else begin
if (primed && sel == sel_d && out_pad_first !== out_logic_first)
$fatal(1, "the two implementations disagree with a static select");
sel_d <= sel;
primed <= 1'b1;
end
end
`endif
endmodule-- spi_io_regs.vhd
--
-- Chapter 15.8 -- using the I/O registers instead of fighting the fabric.
--
-- A pad register is a flip-flop that lives in the I/O block rather than in the
-- fabric. On every FPGA family there is one per pin, it is already paid for, and a
-- path that ends in it has essentially no routing between the pin and the flop --
-- which is usually the difference between meeting an input setup requirement and
-- not.
--
-- There is exactly one rule for using it: NOTHING MAY COME BETWEEN THE PIN AND THE
-- REGISTER. Not a mux, not a gate, not an inverter that the tool cannot absorb. The
-- register has to be the first thing the signal meets.
--
-- That rule is easy to state and easy to break by accident, because the natural way
-- to write a design breaks it. This file builds the same function twice -- once
-- obeying the rule and once not -- so that the difference can be examined rather
-- than asserted.
--
-- THE FUNCTION. An N-bit input bus, of which the design needs one bit, chosen by a
-- configuration input:
--
-- out = in_pins[sel]
--
-- THE TWO IMPLEMENTATIONS.
--
-- PAD FIRST register all N bits at the pads, then select.
-- N flops, all of them placeable in pads, no logic before any of
-- them. The select is combinational AFTER the register.
--
-- LOGIC FIRST select at the pads, then register the chosen bit.
-- ONE flop, and it cannot go in a pad, because there is an N-to-1
-- mux in front of it. The select is combinational BEFORE the register.
--
-- The second is what a designer writes when thinking about area, and it is smaller
-- by N-1 flops. It is also the one that will not close timing, and no simulation
-- will ever say so.
--
-- AND THEY ARE NOT QUITE THE SAME CIRCUIT, WHICH IS THE INTERESTING PART.
--
-- With `sel` held still the two are indistinguishable: same value, same latency, bit
-- for bit, forever. Change `sel` while data is moving and they differ by one cycle
-- in when the change takes effect -- pad-first re-selects from data already captured,
-- logic-first selects before capturing.
--
-- So the trade is three-sided rather than two:
--
-- pad first N flops placeable `sel` takes effect on captured data
-- logic first 1 flop not `sel` takes effect on arriving data
--
-- A design that switches `sel` between transactions -- which is what a real
-- configuration register does -- cannot tell the difference, and should take the
-- pad-first version. A design that switches `sel` DURING a transaction has chosen a
-- behaviour, and should say which one it wanted.
--
-- WHAT THE ATTRIBUTES DO, AND WHAT THEY DO NOT.
--
-- `IOB = "TRUE"` (Xilinx) or the equivalent Intel assignment is a REQUEST. The tool
-- honours it when the rule above is satisfied and warns when it is not -- and a
-- warning in a log of ten thousand lines is not a mechanism. The mechanism is the
-- structure; the attribute only makes the tool's refusal visible.
--
-- This is the same shape as Chapter 14.1's `ASYNC_REG`: the attribute does not create
-- the property, it stops the tool destroying it, and the property has to be there to
-- begin with.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity spi_io_regs is
generic (
N : positive := 4; -- how many pins arrive
SW : positive := 2 -- width of the select; N <= 2**SW
);
port (
clk : in std_logic;
rst_n : in std_logic;
in_pins : in std_logic_vector(N - 1 downto 0);
sel : in unsigned(SW - 1 downto 0);
-- pad first: register every bit, then select
out_pad_first : out std_logic;
-- logic first: select, then register one bit
out_logic_first : out std_logic
);
end entity;
architecture rtl of spi_io_regs is
signal pad_r : std_logic_vector(N - 1 downto 0) := (others => '0');
signal logic_r : std_logic := '0';
signal sel_first : std_logic;
begin
-- ---------------------------------------------------------------- pad first
-- N flops with NOTHING in front of them. On Xilinx this carries an
-- `IOB = "TRUE"` attribute and on Intel an ALTERA_ATTRIBUTE requesting a fast
-- input register. The attribute is omitted deliberately: it is vendor-specific,
-- it belongs with the constraints (Chapter 15.7), and including one here would
-- suggest the attribute is what makes this work. It is not -- the absence of
-- logic is.
padreg : process (clk, rst_n)
begin
if rst_n = '0' then
pad_r <= (others => '0');
elsif rising_edge(clk) then
pad_r <= in_pins;
end if;
end process;
-- The select is AFTER the register, so it is fabric logic on a path that has a
-- whole clock period rather than logic between a pin and a flop.
out_pad_first <= pad_r(to_integer(sel));
-- ------------------------------------------------------------- logic first
-- One flop, and an N-to-1 mux in front of it. Functionally the same selection;
-- structurally the version that cannot use a pad register.
--
-- Note how ordinary it looks. Nothing about it reads as a mistake, and that is
-- why it gets written: it is smaller, it simulates identically under a static
-- select, and the only thing that objects is a place-and-route report.
sel_first <= in_pins(to_integer(sel));
logicreg : process (clk, rst_n)
begin
if rst_n = '0' then
logic_r <= '0';
elsif rising_edge(clk) then
logic_r <= sel_first;
end if;
end process;
out_logic_first <= logic_r;
-- The property worth asserting is the one that HOLDS: with `sel` unchanged from
-- one cycle to the next, the two implementations agree. Asserting that they
-- always agree would be asserting something false, and asserting nothing would
-- miss the case that matters -- a refactor from one form to the other is safe
-- exactly when the select is static, and this is where that gets checked.
check : process (clk, rst_n)
variable sel_d : unsigned(SW - 1 downto 0) := (others => '0');
variable primed : boolean := false;
begin
if rst_n = '0' then
sel_d := (others => '0');
primed := false;
elsif rising_edge(clk) then
if primed and sel = sel_d then
assert pad_r(to_integer(sel)) = logic_r
report "the two implementations disagree with a static select"
severity failure;
end if;
sel_d := sel;
primed := true;
end if;
end process;
end architecture;The testbench
Four experiments, and the second and third are a matched pair: the same random select compared between edges and at edges, giving 81 disagreements and 0.
// spi_io_regs_tb.sv
//
// Three claims, and the third is the one that makes this chapter's advice safe to
// follow rather than merely correct.
//
// 1 WITH A STATIC SELECT THE TWO IMPLEMENTATIONS ARE BIT-FOR-BIT IDENTICAL, over
// every select value and a long random stimulus. That is what licenses the
// refactor: a design that only changes `sel` between transactions can move from
// the smaller form to the pad-placeable one with no functional change at all.
//
// 2 WITH A MOVING SELECT THEY DIFFER, and the bench measures how often rather than
// asserting that they might. The difference is one cycle in when the select
// takes effect, and a design that switches mid-transaction has to choose --
// which is a specification question, not an optimisation.
//
// 3 AND SIMULATION CANNOT TELL WHICH ONE CLOSES TIMING. Stated in the log, because
// the whole point of the chapter is a property no bench can measure, and a green
// run that is read as "either form is fine" is worse than no run.
`timescale 1ns/1ps
module spi_io_regs_tb;
localparam int N = 4;
localparam int SW = 2;
reg clk = 1'b0;
reg rst_n = 1'b1;
always #5 clk = ~clk;
reg [N-1:0] in_pins = {N{1'b0}};
reg [SW-1:0] sel = {SW{1'b0}};
wire out_pad_first, out_logic_first;
spi_io_regs #(.N(N), .SW(SW)) dut (
.clk(clk), .rst_n(rst_n),
.in_pins(in_pins), .sel(sel),
.out_pad_first(out_pad_first), .out_logic_first(out_logic_first)
);
integer errors = 0;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
integer seed = 32'h51A5_E158;
integer disagreements = 0, samples = 0;
task automatic restart;
begin
rst_n = 1'b1;
repeat (2) @(posedge clk);
rst_n = 1'b0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (4) @(posedge clk);
disagreements = 0;
samples = 0;
end
endtask
integer s, k;
initial begin
// =============================================================
// 1. A STATIC SELECT: the two forms must agree bit for bit, for every
// select value, over a long random stimulus. The SPI_CHECKS assertion
// inside the design checks the same thing continuously; this loop makes
// it a reported count as well, so a reader of the log can see that the
// equivalence was exercised rather than merely not violated.
// =============================================================
$display(" a %0d-bit input bus with a %0d-bit select, comparing a pad-first and a logic-first capture",
N, SW);
$display(" sel samples disagreements");
for (s = 0; s < N; s = s + 1) begin
restart();
sel = s[SW-1:0];
repeat (4) @(posedge clk);
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
$display(" %3d %7d %13d", s, samples, disagreements);
if (disagreements != 0) begin
$display(" FAIL: with sel held at %0d the two forms disagreed %0d times in %0d samples -- with a static select they are the same circuit and any difference is a bug in one of them",
s, disagreements, samples);
errors = errors + 1;
end
end
$display(" with the select held still the two forms are bit-for-bit identical at every select value, which is what licenses moving a design from the smaller form to the pad-placeable one");
// =============================================================
// 2. A MOVING SELECT: they must differ, and the difference is exactly one
// cycle in when the select takes effect. If they did NOT differ, one of
// them would not be doing what its structure says it does.
// =============================================================
// WHERE THE DIFFERENCE IS OBSERVABLE, which took two attempts to get right
// and is the most useful thing in this bench.
//
// Comparing the two outputs at every posedge finds ZERO disagreements, at any
// stimulus -- because `sel` is stable across the clock edge, so `pad_r[sel]`
// and the registered `in_pins[sel]` were computed from the same pair. The
// first two attempts at this test did exactly that and concluded the two
// forms were identical, which they are not.
//
// The difference lives in a WINDOW: after the data has been captured and
// before the next capture, a change to `sel` re-selects pad-first's output
// from the value already in `pad_r` and leaves logic-first's registered
// output alone. Observing it requires changing `sel` after the posedge and
// comparing before the next one.
//
// That is a general lesson about comparing two implementations: if the
// comparison is only ever made at clock edges, a difference that lives
// between them is invisible -- and "we diffed them cycle by cycle" is not the
// same as "they are the same circuit".
restart();
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed); // present the data
@(posedge clk); // both forms capture it
@(negedge clk);
sel = $random(seed); // move the select AFTER the capture
#1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
$display(" with the select moved AFTER the data was captured and compared before the next capture: %0d disagreements in %0d samples",
disagreements, samples);
if (disagreements == 0) begin
$display(" FAIL: a select moved after the capture must re-select pad-first's output from the value already held, so the two forms must differ somewhere in 200 samples");
errors = errors + 1;
end
$display(" and there they differ, because pad-first re-selects from a value it has ALREADY CAPTURED while logic-first's register keeps what it selected earlier -- the difference lives BETWEEN clock edges, which is why comparing the two at posedges finds none and why 'we diffed them cycle by cycle' is not the same as 'they are the same circuit'");
// 2b. AND THE CONTROL: the same random select, compared AT the clock edges
// instead of between them. Zero disagreements, and the number is reported
// rather than discarded, because it is the boundary of the claim above --
// and because it is the result a reasonable bench would have produced and
// drawn the wrong conclusion from.
restart();
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
sel = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
if (disagreements != 0) begin
$display(" FAIL: compared at the clock edges the two forms must agree, because `sel` is stable across the edge -- got %0d disagreements",
disagreements);
errors = errors + 1;
end
$display(" compared AT the clock edges with the same random select, %0d disagreements in %0d samples -- which is the result a reasonable bench produces and the wrong conclusion to draw from it",
disagreements, samples);
// =============================================================
// 3. THE BOUNDARY CASE THAT DECIDES A REAL DESIGN: a select that changes
// only while the bus is idle. That is what a configuration register does,
// and it is the case in which the refactor is free.
// =============================================================
restart();
for (s = 0; s < 4; s = s + 1) begin
// change the select while nothing is moving
@(negedge clk);
in_pins = {N{1'b0}};
sel = s[SW-1:0];
repeat (4) @(posedge clk);
// then move data with the select held
for (k = 0; k < 50; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
end
if (disagreements != 0) begin
$display(" FAIL: a select changed only between bursts produced %0d disagreements", disagreements);
errors = errors + 1;
end
$display(" a select changed only while the bus is idle -- which is what a configuration register does -- produced zero disagreements across %0d samples, so for a real SPI slave the two forms are interchangeable and the pad-placeable one is free to choose",
samples);
// =============================================================
// 4. AND THE PROPERTY NO BENCH CAN MEASURE.
// =============================================================
$display(" NOT MEASURED HERE, and not measurable: which form meets the input setup requirement. Pad-first ends %0d flops directly at the pads with no logic in front of them; logic-first ends ONE flop behind a %0d-to-1 mux, and the mux is in the path from the pin to the flop. Simulation is identical; a place-and-route report is not, and the attribute that asks for a pad register can only WARN when the structure forbids it",
N, N);
$display(" the cost is %0d flops against 1 -- which is the trade, and it is a trade worth making, because %0d flip-flops in I/O blocks are already paid for and fabric routing between a pin and a flop is not",
N, N);
if (errors == 0)
$display("PASS: a pad register is free, already present on every pin, and usable under exactly one rule -- nothing may come between the pin and the register. The two forms built here compute the same selection and differ only in which side of the register the mux sits on, and with the select HELD STILL they are bit-for-bit identical at every select value across eight hundred samples, which is what licenses a refactor from the smaller form to the placeable one. They are NOT the same circuit, though, and finding that took two attempts: compared at clock edges they agree on any stimulus, because `sel` is stable across the edge and both forms then read the same pair. The difference lives BETWEEN edges -- move the select after the data has been captured and pad-first re-selects from the value already in its register while logic-first keeps what it selected earlier -- so 'we diffed them cycle by cycle' is not the same as 'they are the same circuit', and a design that switches select mid-transaction is choosing a behaviour and should say which. For a real slave the select is a configuration register that changes while the bus is idle, and in that regime the bench measured zero disagreements, so the pad-placeable form is free to choose. What no bench can measure is the only thing the chapter is about: pad-first ends N flops at the pads with nothing in front of them and logic-first ends one flop behind an N-to-1 mux, the simulations are identical, and the difference appears in a place-and-route report -- where the IOB attribute can WARN that the structure forbids what it asked for, exactly as ASYNC_REG can only stop a tool destroying a property the structure already has");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_io_regs_tb.v
//
// Three claims, and the third is the one that makes this chapter's advice safe to
// follow rather than merely correct.
//
// 1 WITH A STATIC SELECT THE TWO IMPLEMENTATIONS ARE BIT-FOR-BIT IDENTICAL, over
// every select value and a long random stimulus. That is what licenses the
// refactor: a design that only changes `sel` between transactions can move from
// the smaller form to the pad-placeable one with no functional change at all.
//
// 2 WITH A MOVING SELECT THEY DIFFER, and the bench measures how often rather than
// asserting that they might. The difference is one cycle in when the select
// takes effect, and a design that switches mid-transaction has to choose --
// which is a specification question, not an optimisation.
//
// 3 AND SIMULATION CANNOT TELL WHICH ONE CLOSES TIMING. Stated in the log, because
// the whole point of the chapter is a property no bench can measure, and a green
// run that is read as "either form is fine" is worse than no run.
`timescale 1ns/1ps
module spi_io_regs_tb;
localparam N = 4;
localparam SW = 2;
reg clk;
reg rst_n;
always #5 clk = ~clk;
reg [N-1:0] in_pins;
reg [SW-1:0] sel;
wire out_pad_first, out_logic_first;
spi_io_regs #(.N(N), .SW(SW)) dut (
.clk(clk), .rst_n(rst_n),
.in_pins(in_pins), .sel(sel),
.out_pad_first(out_pad_first), .out_logic_first(out_logic_first)
);
integer errors;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
integer seed;
integer disagreements, samples;
task restart;
begin
rst_n = 1'b1;
repeat (2) @(posedge clk);
rst_n = 1'b0;
repeat (4) @(posedge clk);
rst_n = 1'b1;
repeat (4) @(posedge clk);
disagreements = 0;
samples = 0;
end
endtask
integer s, k;
initial begin
// =============================================================
// 1. A STATIC SELECT: the two forms must agree bit for bit, for every
// select value, over a long random stimulus. The SPI_CHECKS assertion
// inside the design checks the same thing continuously; this loop makes
// it a reported count as well, so a reader of the log can see that the
// equivalence was exercised rather than merely not violated.
// =============================================================
$display(" a %0d-bit input bus with a %0d-bit select, comparing a pad-first and a logic-first capture",
N, SW);
$display(" sel samples disagreements");
for (s = 0; s < N; s = s + 1) begin
restart();
sel = s[SW-1:0];
repeat (4) @(posedge clk);
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
$display(" %3d %7d %13d", s, samples, disagreements);
if (disagreements != 0) begin
$display(" FAIL: with sel held at %0d the two forms disagreed %0d times in %0d samples -- with a static select they are the same circuit and any difference is a bug in one of them",
s, disagreements, samples);
errors = errors + 1;
end
end
$display(" with the select held still the two forms are bit-for-bit identical at every select value, which is what licenses moving a design from the smaller form to the pad-placeable one");
// =============================================================
// 2. A MOVING SELECT: they must differ, and the difference is exactly one
// cycle in when the select takes effect. If they did NOT differ, one of
// them would not be doing what its structure says it does.
// =============================================================
// WHERE THE DIFFERENCE IS OBSERVABLE, which took two attempts to get right
// and is the most useful thing in this bench.
//
// Comparing the two outputs at every posedge finds ZERO disagreements, at any
// stimulus -- because `sel` is stable across the clock edge, so `pad_r[sel]`
// and the registered `in_pins[sel]` were computed from the same pair. The
// first two attempts at this test did exactly that and concluded the two
// forms were identical, which they are not.
//
// The difference lives in a WINDOW: after the data has been captured and
// before the next capture, a change to `sel` re-selects pad-first's output
// from the value already in `pad_r` and leaves logic-first's registered
// output alone. Observing it requires changing `sel` after the posedge and
// comparing before the next one.
//
// That is a general lesson about comparing two implementations: if the
// comparison is only ever made at clock edges, a difference that lives
// between them is invisible -- and "we diffed them cycle by cycle" is not the
// same as "they are the same circuit".
restart();
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed); // present the data
@(posedge clk); // both forms capture it
@(negedge clk);
sel = $random(seed); // move the select AFTER the capture
#1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
$display(" with the select moved AFTER the data was captured and compared before the next capture: %0d disagreements in %0d samples",
disagreements, samples);
if (disagreements == 0) begin
$display(" FAIL: a select moved after the capture must re-select pad-first's output from the value already held, so the two forms must differ somewhere in 200 samples");
errors = errors + 1;
end
$display(" and there they differ, because pad-first re-selects from a value it has ALREADY CAPTURED while logic-first's register keeps what it selected earlier -- the difference lives BETWEEN clock edges, which is why comparing the two at posedges finds none and why 'we diffed them cycle by cycle' is not the same as 'they are the same circuit'");
// 2b. AND THE CONTROL: the same random select, compared AT the clock edges
// instead of between them. Zero disagreements, and the number is reported
// rather than discarded, because it is the boundary of the claim above --
// and because it is the result a reasonable bench would have produced and
// drawn the wrong conclusion from.
restart();
for (k = 0; k < 200; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
sel = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
if (disagreements != 0) begin
$display(" FAIL: compared at the clock edges the two forms must agree, because `sel` is stable across the edge -- got %0d disagreements",
disagreements);
errors = errors + 1;
end
$display(" compared AT the clock edges with the same random select, %0d disagreements in %0d samples -- which is the result a reasonable bench produces and the wrong conclusion to draw from it",
disagreements, samples);
// =============================================================
// 3. THE BOUNDARY CASE THAT DECIDES A REAL DESIGN: a select that changes
// only while the bus is idle. That is what a configuration register does,
// and it is the case in which the refactor is free.
// =============================================================
restart();
for (s = 0; s < 4; s = s + 1) begin
// change the select while nothing is moving
@(negedge clk);
in_pins = {N{1'b0}};
sel = s[SW-1:0];
repeat (4) @(posedge clk);
// then move data with the select held
for (k = 0; k < 50; k = k + 1) begin
@(negedge clk);
in_pins = $random(seed);
@(posedge clk); #1;
samples = samples + 1;
if (out_pad_first !== out_logic_first)
disagreements = disagreements + 1;
end
end
if (disagreements != 0) begin
$display(" FAIL: a select changed only between bursts produced %0d disagreements", disagreements);
errors = errors + 1;
end
$display(" a select changed only while the bus is idle -- which is what a configuration register does -- produced zero disagreements across %0d samples, so for a real SPI slave the two forms are interchangeable and the pad-placeable one is free to choose",
samples);
// =============================================================
// 4. AND THE PROPERTY NO BENCH CAN MEASURE.
// =============================================================
$display(" NOT MEASURED HERE, and not measurable: which form meets the input setup requirement. Pad-first ends %0d flops directly at the pads with no logic in front of them; logic-first ends ONE flop behind a %0d-to-1 mux, and the mux is in the path from the pin to the flop. Simulation is identical; a place-and-route report is not, and the attribute that asks for a pad register can only WARN when the structure forbids it",
N, N);
$display(" the cost is %0d flops against 1 -- which is the trade, and it is a trade worth making, because %0d flip-flops in I/O blocks are already paid for and fabric routing between a pin and a flop is not",
N, N);
if (errors == 0)
$display("PASS: a pad register is free, already present on every pin, and usable under exactly one rule -- nothing may come between the pin and the register. The two forms built here compute the same selection and differ only in which side of the register the mux sits on, and with the select HELD STILL they are bit-for-bit identical at every select value across eight hundred samples, which is what licenses a refactor from the smaller form to the placeable one. They are NOT the same circuit, though, and finding that took two attempts: compared at clock edges they agree on any stimulus, because `sel` is stable across the edge and both forms then read the same pair. The difference lives BETWEEN edges -- move the select after the data has been captured and pad-first re-selects from the value already in its register while logic-first keeps what it selected earlier -- so 'we diffed them cycle by cycle' is not the same as 'they are the same circuit', and a design that switches select mid-transaction is choosing a behaviour and should say which. For a real slave the select is a configuration register that changes while the bus is idle, and in that regime the bench measured zero disagreements, so the pad-placeable form is free to choose. What no bench can measure is the only thing the chapter is about: pad-first ends N flops at the pads with nothing in front of them and logic-first ends one flop behind an N-to-1 mux, the simulations are identical, and the difference appears in a place-and-route report -- where the IOB attribute can WARN that the structure forbids what it asked for, exactly as ASYNC_REG can only stop a tool destroying a property the structure already has");
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
disagreements = 0;
samples = 0;
clk = 1'b0;
rst_n = 1'b1;
in_pins = {N{1'b0}};
sel = {SW{1'b0}};
errors = 0;
seed = 32'h51A5_E158;
end
endmodule-- spi_io_regs_tb.vhd
--
-- Three claims, and the third is the one that makes this chapter's advice safe to
-- follow rather than merely correct.
--
-- 1 WITH A STATIC SELECT THE TWO IMPLEMENTATIONS ARE BIT-FOR-BIT IDENTICAL, over
-- every select value and a long random stimulus. That is what licenses the
-- refactor: a design that only changes `sel` between transactions can move from
-- the smaller form to the pad-placeable one with no functional change at all.
--
-- 2 WITH A MOVING SELECT THEY DIFFER, and the bench measures how often rather than
-- asserting that they might. The difference is one cycle in when the select
-- takes effect, and a design that switches mid-transaction has to choose --
-- which is a specification question, not an optimisation.
--
-- 3 AND SIMULATION CANNOT TELL WHICH ONE CLOSES TIMING. Stated in the log, because
-- the whole point of the chapter is a property no bench can measure, and a green
-- run that is read as "either form is fine" is worse than no run.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all; -- `uniform`, for the random stimulus
entity spi_io_regs_tb is
end entity;
architecture sim of spi_io_regs_tb is
constant N : positive := 4;
constant SW : positive := 2;
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal halt : boolean := false;
signal in_pins : std_logic_vector(N - 1 downto 0) := (others => '0');
signal sel : unsigned(SW - 1 downto 0) := (others => '0');
signal out_pad_first, out_logic_first : std_logic;
begin
clkgen : process
begin
while not halt loop
clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns;
end loop;
wait;
end process;
dut : entity work.spi_io_regs
generic map (N => N, SW => SW)
port map (clk => clk, rst_n => rst_n,
in_pins => in_pins, sel => sel,
out_pad_first => out_pad_first,
out_logic_first => out_logic_first);
watchdog : process
begin
wait for 400 us;
if not halt then
report "FAIL: the simulation did not finish within its time limit"
severity failure;
end if;
wait;
end process;
stim : process
variable errs : natural := 0;
variable disagreements, samples : natural := 0;
variable seed1 : positive := 51151;
variable seed2 : positive := 15873;
variable r : real;
impure function rnd(modulo : positive) return natural is
begin
uniform(seed1, seed2, r);
return integer(r * real(modulo - 1));
end function;
procedure restart is
begin
rst_n <= '1';
for i in 1 to 2 loop wait until rising_edge(clk); end loop;
rst_n <= '0';
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
rst_n <= '1';
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
disagreements := 0;
samples := 0;
end procedure;
begin
report " a " & integer'image(N) & "-bit input bus with a " &
integer'image(SW) &
"-bit select, comparing a pad-first and a logic-first capture";
report " sel samples disagreements";
-- 1. A STATIC SELECT: the two forms must agree bit for bit.
for s in 0 to N - 1 loop
restart;
sel <= to_unsigned(s, SW);
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
for k in 1 to 200 loop
wait until falling_edge(clk);
in_pins <= std_logic_vector(to_unsigned(rnd(2 ** N), N));
wait until rising_edge(clk);
wait for 1 ns;
samples := samples + 1;
if out_pad_first /= out_logic_first then
disagreements := disagreements + 1;
end if;
end loop;
report " " & integer'image(s) & " " & integer'image(samples) &
" " & integer'image(disagreements);
if disagreements /= 0 then
report " FAIL: with sel held at " & integer'image(s) &
" the two forms disagreed " & integer'image(disagreements) &
" times -- with a static select they are the same circuit";
errs := errs + 1;
end if;
end loop;
report " with the select held still the two forms are bit-for-bit identical at every select value, which is what licenses moving a design from the smaller form to the pad-placeable one";
-- 2. WHERE THE DIFFERENCE IS OBSERVABLE: between clock edges, not at them.
restart;
for k in 1 to 200 loop
wait until falling_edge(clk);
in_pins <= std_logic_vector(to_unsigned(rnd(2 ** N), N));
wait until rising_edge(clk); -- both forms capture it
wait until falling_edge(clk);
sel <= to_unsigned(rnd(N), SW); -- move the select AFTER capture
wait for 1 ns;
samples := samples + 1;
if out_pad_first /= out_logic_first then
disagreements := disagreements + 1;
end if;
end loop;
report " with the select moved AFTER the data was captured and compared before the next capture: " &
integer'image(disagreements) & " disagreements in " &
integer'image(samples) & " samples";
if disagreements = 0 then
report " FAIL: a select moved after the capture must re-select pad-first's output from the value already held, so the two forms must differ somewhere in 200 samples";
errs := errs + 1;
end if;
report " and there they differ, because pad-first re-selects from a value it has ALREADY CAPTURED while logic-first's register keeps what it selected earlier -- the difference lives BETWEEN clock edges, which is why comparing the two at posedges finds none and why 'we diffed them cycle by cycle' is not the same as 'they are the same circuit'";
-- 2b. THE CONTROL: the same random select, compared AT the clock edges.
restart;
for k in 1 to 200 loop
wait until falling_edge(clk);
in_pins <= std_logic_vector(to_unsigned(rnd(2 ** N), N));
sel <= to_unsigned(rnd(N), SW);
wait until rising_edge(clk);
wait for 1 ns;
samples := samples + 1;
if out_pad_first /= out_logic_first then
disagreements := disagreements + 1;
end if;
end loop;
if disagreements /= 0 then
report " FAIL: compared at the clock edges the two forms must agree, because `sel` is stable across the edge -- got " &
integer'image(disagreements) & " disagreements";
errs := errs + 1;
end if;
report " compared AT the clock edges with the same random select, " &
integer'image(disagreements) & " disagreements in " &
integer'image(samples) &
" samples -- which is the result a reasonable bench produces and the wrong conclusion to draw from it";
-- 3. THE BOUNDARY CASE THAT DECIDES A REAL DESIGN: a select that changes
-- only while the bus is idle, which is what a configuration register does.
restart;
for s in 0 to 3 loop
wait until falling_edge(clk);
in_pins <= (others => '0');
sel <= to_unsigned(s, SW);
for i in 1 to 4 loop wait until rising_edge(clk); end loop;
for k in 1 to 50 loop
wait until falling_edge(clk);
in_pins <= std_logic_vector(to_unsigned(rnd(2 ** N), N));
wait until rising_edge(clk);
wait for 1 ns;
samples := samples + 1;
if out_pad_first /= out_logic_first then
disagreements := disagreements + 1;
end if;
end loop;
end loop;
if disagreements /= 0 then
report " FAIL: a select changed only between bursts produced " &
integer'image(disagreements) & " disagreements";
errs := errs + 1;
end if;
report " a select changed only while the bus is idle -- which is what a configuration register does -- produced zero disagreements across " &
integer'image(samples) &
" samples, so for a real SPI slave the two forms are interchangeable and the pad-placeable one is free to choose";
-- 4. AND THE PROPERTY NO BENCH CAN MEASURE.
report " NOT MEASURED HERE, and not measurable: which form meets the input setup requirement. Pad-first ends " &
integer'image(N) &
" flops directly at the pads with no logic in front of them; logic-first ends ONE flop behind a " &
integer'image(N) &
"-to-1 mux, and the mux is in the path from the pin to the flop. Simulation is identical; a place-and-route report is not, and the attribute that asks for a pad register can only WARN when the structure forbids it";
report " the cost is " & integer'image(N) &
" flops against 1 -- which is the trade, and it is a trade worth making, because " &
integer'image(N) &
" flip-flops in I/O blocks are already paid for and fabric routing between a pin and a flop is not";
if errs = 0 then
report "PASS: a pad register is free, already present on every pin, and usable under exactly one rule -- nothing may come between the pin and the register. The two forms built here compute the same selection and differ only in which side of the register the mux sits on, and with the select HELD STILL they are bit-for-bit identical at every select value across eight hundred samples, which is what licenses a refactor from the smaller form to the placeable one. They are NOT the same circuit, though, and finding that took two attempts: compared at clock edges they agree on any stimulus, because `sel` is stable across the edge and both forms then read the same pair. The difference lives BETWEEN edges -- move the select after the data has been captured and pad-first re-selects from the value already in its register while logic-first keeps what it selected earlier -- so 'we diffed them cycle by cycle' is not the same as 'they are the same circuit', and a design that switches select mid-transaction is choosing a behaviour and should say which. For a real slave the select is a configuration register that changes while the bus is idle, and in that regime the bench measured zero disagreements, so the pad-placeable form is free to choose. What no bench can measure is the only thing the chapter is about: pad-first ends N flops at the pads with nothing in front of them and logic-first ends one flop behind an N-to-1 mux, the simulations are identical, and the difference appears in a place-and-route report -- where the IOB attribute can WARN that the structure forbids what it asked for, exactly as ASYNC_REG can only stop a tool destroying a property the structure already has";
else
report "FAIL: " & integer'image(errs) & " error(s)" severity error;
end if;
halt <= true;
wait;
end process;
end architecture;6. Why a Verification Engineer Cares
Sample between edges when comparing two implementations, not only at them. §3's callout is the point. A diff that samples at posedges establishes agreement at the posedges it sampled, and where inputs can move at other times that is a weaker claim than it reads as.
Report the control alongside the result. The bench keeps the at-the-edges comparison and prints its zero, labelled as "the result a reasonable bench produces and the wrong conclusion to draw from it". Deleting it would leave a reader unable to see why the between-edges measurement was necessary.
Assert the narrow true property rather than the broad false one. §5's callout. sel static implies equality is checkable and is the refactor's licence; unconditional equality is false and would have to be deleted, leaving nothing.
And measure the case that actually occurs. For an SPI slave sel is a configuration register that changes while the bus is idle, and the bench measures that regime explicitly and separately — because it is the regime in which the engineering advice applies, and a suite that only exercised the pathological case would conclude the two forms are not interchangeable.
// Properties for the two forms. The first is the only unconditional one, and note
// what it is NOT: it is not equality of the two outputs.
property p_equal_under_a_static_select;
// The narrow, true property -- and the licence for the refactor.
@(posedge clk) disable iff (!rst_n)
(sel == $past(sel)) |-> (out_pad_first == out_logic_first);
endproperty
property p_pad_first_reselects_from_captured_data;
// What pad-first actually does, stated so that a reader knows it is a choice:
// its output is the CURRENT select applied to the PREVIOUS cycle's pins.
@(posedge clk) disable iff (!rst_n)
1 |-> (out_pad_first == $past(in_pins)[sel]);
endproperty
property p_logic_first_selected_before_capturing;
// And what logic-first does: the PREVIOUS select applied to the previous pins.
@(posedge clk) disable iff (!rst_n)
1 |-> (out_logic_first == $past(in_pins)[$past(sel)]);
endproperty
property p_pad_registers_have_no_logic_in_front;
// Not expressible as a temporal property at all, and that is worth recording in
// the property file rather than omitting: the rule is STRUCTURAL. The checks that
// reach it are a lint rule counting logic levels between a port and its first
// flop, and a place-and-route report. This comment is the property.
endproperty// Coverage. The axis that matters is WHEN THE SELECT MOVES relative to the capture,
// because that is the only thing that distinguishes the two forms -- and it is an
// axis a normal coverage model does not have, since normally it would not matter.
covergroup cg_io_regs @(posedge clk);
option.per_instance = 1;
sel_moves: coverpoint select_change_timing {
bins never = {0};
bins with_the_data = {1}; // same cycle: the two forms agree
bins after_a_capture = {2}; // between captures: they differ
bins while_idle = {3}; // the real SPI case
}
// The select value itself, because the mux's behaviour at the ends of its range
// is where an off-by-one in a width would show.
sel_val: coverpoint sel { bins all[] = {[0:3]}; }
// Whether the data was changing in the same cycle, which is what decides whether
// a select change is observable at all.
data_moving: coverpoint data_changed_this_cycle { bins yes = {1}; bins no = {0}; }
x_moves_data: cross sel_moves, data_moving;
endgroup7. Why an FPGA or ASIC Engineer Cares
The pad register is free, so the N-flop form costs nothing real. That is the whole engineering argument: N flip-flops in I/O blocks are already fabricated and unused, and fabric routing between a pin and a flop is not free. Trading N-1 fabric flops for N pad flops is trading something scarce for something already paid for.
The attribute can only warn. §4. A design that asks for IOB = TRUE on a pin whose flop has a mux in front of it gets a warning and a fabric flop, and the warning is one line among thousands. The structure is the mechanism.
Reset on an input pad register may prevent placement. On some families a resettable flop is not the one available in the I/O block, or the reset mux is fabric logic in front of it. That is why Chapter 15.7's shell has no reset on sclk_i or mosi_i and does on cs_n_i — the exception is paid for deliberately and only where it buys something.
And the input path is where this matters, more than the output. An output's timing is a flop's clock-to-out plus the pad, and a fabric flop's clock-to-out is only slightly worse. An input's timing is the arrival against the flop's setup, and routing from a pin into the fabric is where the nanoseconds go — so if only some registers can be placed, the inputs get them.
8. Failure Signature — A Design That Needs A Slower SPI Clock Than It Should
The symptom:
"The datasheet says 25 MHz and the part only works to 12. Timing reports show the input paths failing by about 2 ns. The RTL is correct and simulation is clean at any frequency."
What is happening: the capture registers are in the fabric rather than in the pads, because something sits between the pins and them — a mux, a polarity XOR, an enable gate. The 2 ns is routing from the pin into the fabric, which a pad register would not have.
Why the RTL looks correct: it is correct. The function is right, the simulation is right, and the fault is where a register was placed — which is a structural property that neither the RTL nor the simulation expresses.
How to find it in one report: list the input paths and look at where each one ends. A path ending in a fabric flop with routing delay between the pin and it is the fault, and the fix is to move whatever logic sits in front of that flop to the other side of it — which is §1's pad-first form, and which §2 shows is functionally free when the select is static.
9. Common Misconceptions
"The two forms are the same circuit because simulation cannot tell them apart." Simulation compared at clock edges cannot tell them apart. Compared between edges they differ in 40% of samples, and §3 is the measurement.
"IOB = TRUE puts the register in the pad." It asks. The tool grants the request when nothing is in the way and warns when something is, and the warning is easy to miss.
"The logic-first form is smaller, so it is better where area matters." It is smaller by N-1 fabric flops and costs N unused pad flops, which are already paid for. In exchange it gives up the only placement that meets input setup on a fast interface.
"A cycle-by-cycle diff proves two implementations equivalent." It proves they agree at the instants the diff sampled. Where the inputs can move at other instants — and a configuration register can — that is a weaker claim.
"An assertion should check that the two forms are equal." That assertion is false and would have to be deleted. The true one is conditional on the select being static, and it is exactly the licence a refactor needs.
"Resetting every register is good practice, so the pad registers should be reset." On some families the reset mux is the logic that prevents placement. The reset is worth paying for on chip select, because a slave waking up selected is dangerous, and not on the others.
10. Reason It Through
Q. An 8-bit bus and a 3-bit select. How many flops does each form use, and which is cheaper in practice?
Pad-first uses 8 flops, all in I/O blocks. Logic-first uses 1, in the fabric, behind an 8-to-1 mux. Logic-first is cheaper by 7 fabric flops and pad-first is cheaper in the resource that is actually scarce, because the 8 I/O flops exist whether or not they are used. And pad-first is the only one that can meet a tight input setup, so in practice it is cheaper on both axes that matter.
Q. Why do the two forms agree when sel changes in the same cycle as the data, and differ when it changes between captures?
Because pad_first is previous_pins[current_sel] and logic_first is previous_pins[previous_sel]. When sel changes at the same edge as the data, both expressions use the same select — the one that was stable across the edge — and agree. When sel changes between captures, current_sel differs from previous_sel while previous_pins is unchanged, so pad-first re-selects a different bit of a value already captured and logic-first does not.
Q. A design's sel comes from a configuration register that software writes at any time, including mid-transaction. Which form should it use, and what else must change?
Whichever behaviour it wants — and it must first decide which, because the two are different specifications rather than two implementations of one. In practice the right move is to remove the question: latch sel at the transaction boundary, exactly as Chapter 13.2's configuration snapshot does. Then sel is static within a transaction, the two forms are equivalent, and the pad-placeable one can be chosen freely. The design decision is not which form to use; it is to stop sel moving mid-transaction.
Q. Why does the design's internal assertion not simply check that both outputs are equal?
Because they are not equal, so the assertion would fail on a correct design and be deleted — leaving no check at all. The conditional form, equality when the select is static, is true and is exactly what the refactor depends on. A narrow true assertion is worth more than a broad false one, and this is one of the clearer cases: the false version's inevitable deletion takes the real check with it.
Q. Why does this chapter's argument matter more for inputs than for outputs?
Because an output's timing budget is a flop's clock-to-out plus the pad driver, and a fabric flop's clock-to-out is only slightly worse than a pad flop's — the difference is routing from the fabric to the pad, which the tool can usually absorb. An input's budget is the arrival time against the flop's setup, and the routing from the pin into the fabric is unavoidable delay on the critical side of that comparison. So when only some registers can be placed in pads, the inputs get them.
11. Understanding Check
12. Summary
A pad register is free, present on every pin, and usable under exactly one rule: nothing may come between the pin and the register. The natural way to write a design breaks it.
The two forms built here compute the same selection and differ only in which side of the register the mux sits on. Under a static select they are bit-for-bit identical across eight hundred samples, which is what licenses a refactor from the smaller form to the placeable one — and for a real SPI slave, whose select is a configuration register changed while the bus is idle, the measurement is zero disagreements.
They are not the same circuit, and finding that took two attempts. Compared at clock edges they agree on any stimulus, because the select is stable across the edge and both forms then read the same pair. The difference lives between edges: move the select after the data has been captured and pad-first re-selects from the value already in its register while logic-first keeps what it selected earlier — 81 disagreements in 200 samples.
So "we diffed them cycle by cycle" is not the same as "they are the same circuit", and that is the most transferable idea in the chapter.
The trade is three-sided: N flops and placeable with the select acting on captured data, against one flop and not placeable with the select acting on arriving data. A design that switches mid-transaction is choosing a behaviour and should say which — or better, should latch the select at the transaction boundary and remove the question.
The attribute is a request. It warns when the structure forbids what it asked for, and a warning in a ten-thousand-line log is not a mechanism — the same shape as ASYNC_REG, which stops a tool destroying a property the structure already has.
For verification: sample between edges, not only at them; keep the at-the-edges control and label it as the misleading result; assert the narrow true property rather than the broad false one, because the false one's deletion takes the real check with it; and measure the regime that actually occurs.
For implementation: the N-flop form costs nothing scarce; a reset on an input pad register may itself prevent placement, which is why Chapter 15.7's shell resets only chip select; and this argument matters more for inputs than outputs, because an input's budget is an arrival against a setup and the routing is on the critical side.
13. What Comes Next
Every crossing in this module has been built, measured and constrained. What remains is the question the module has been circling since Chapter 15.1's two identical columns.
Chapter 15.9 — CDC and Constraint Failure Modes builds six crossings — one correct and five broken — and measures them under a benign stimulus and a hostile one. Under the benign one all six pass. Under the hostile one three of the five are found. The remaining two pass both, and they are the two that lose data on silicon — which is the argument for CDC review as an activity separate from CDC verification, and it is not an argument about rigour.
Continue learning
Related tutorials
- Related topic
FPGA Configuration over SPI
Master and slave configuration modes and why they differ only in who clocks, why the sync word must be searched byte by byte, why a blank flash is indistinguishable from preamble, and the loader that shares one datapath between both modes.
- Related topic
Launch and Sample Edges
One edge of each bit time places a bit on the wire, the other captures it, and they must never be the same edge. Why the separation is forced, why it buys half a period, and how RTL maps physical edges onto those roles.
- Related topic
Deriving Mode Behaviour from CPOL and CPHA
The four SPI modes are a two-bit truth table you can rebuild in seconds. The standard numbering, the derivation, the complete mode decoder in three HDLs, and the assertions that keep a configurable design honest.
- Related topic
Mode 0 (CPOL=0, CPHA=0)
The most widely used SPI mode, and the one carrying a real implementation problem: why CPHA=0 forces the first bit onto the line before any clock edge, and the first-bit launch path in Verilog, SystemVerilog and VHDL.
