A structural hazard appears when two stages need the same hardware in the same cycle. A common case is one memory port shared by instruction fetch and data access.
Only one stage can use the port, so the other waits. Fixed priority gives the port to the older (later) stage and stalls the front.
The full listing sits under Testbench below. The arbiter and front_stage are arbitration and storage fragments: holding a register alone does not define a downstream transfer. The front_boundary wrapper connects them for a port that completes every granted request at the rising edge; a variable-latency memory would need a separate response contract.
In the wrapper, fetch_want is the stored valid bit. Downstream consumes only when issue_valid = fetch_grant, so a conflict inserts a downstream bubble even though the front still holds valid work. Upstream may replace the stored item only when new_ready = !valid || !mem_want. While new_valid && !new_ready, it must retain its offer. An empty stage can accept an item even while Memory owns the port.
Edge (pre-edge state) | Upstream offer | Memory wants port | new_ready / issue_valid | Transfers; state after edge |
|---|---|---|---|---|
0: empty | A = 0x96 | 0 | 1 / 0 | Accept A; hold A |
1: A | B = 0x3C | 1 | 0 / 0 | No front transfer; downstream bubble; retain A and offer B |
2: A | B (unchanged) | 1 | 0 / 0 | Another bubble; retain A and B |
3: A | B (unchanged) | 0 | 1 / 1 | Deliver A once and accept B; hold B |
4: B | None | 0 | 1 / 1 | Deliver B once; become empty |
5: empty | None | 0 | 1 / 0 | No transfer; downstream bubble |
Fix | Cost | When |
|---|---|---|
Stall the loser | Lost cycles, simple logic | Conflicts are rare |
Duplicate the resource (two ports, separate I/D memories) | Area | Conflicts are frequent |
Time-multiplex at double rate | Clock/timing pressure | Resource is cheap to speed up |
A stall signal that fans out to many registers is a high-fanout, often timing-critical net. Pipeline-friendly designs prefer the valid/ready local handshake over a global stall; see valid/ready flow control.
Yes, if the back stage requests the port every cycle. If that is possible, use round-robin or a rule that guarantees the front stage a turn (see the arbiter lesson).
The testbench first checks every arbitration combination and deliberately changes storage inputs during stalls to isolate hold behavior. Those changing inputs are not a legal ready/valid source. A second, directed boundary check follows the table above, retains the blocked B offer, and scores accepted versus delivered items with a queue and a source stability monitor.
module stall_demo (
input logic clk, rst_n,
input logic fetch_want, // front stage has a request ready
input logic mem_want, // back stage has a request ready
output logic fetch_grant,
output logic mem_grant,
output logic stall_front // freeze everything upstream of the back stage
);
// fixed priority: the back (older) stage always wins
assign mem_grant = mem_want;
assign fetch_grant = fetch_want && !mem_want;
assign stall_front = fetch_want && mem_want; // front wanted the port and lost
endmodule
module front_stage (
input logic clk, rst_n,
input logic stall,
input logic [7:0] new_item,
input logic new_valid,
output logic [7:0] item,
output logic valid
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) valid <= 1'b0;
else if (!stall) valid <= new_valid; // when stalled: HOLD both valid and data
always_ff @(posedge clk)
if (!stall && new_valid) item <= new_item;
endmodule
module front_boundary (
input logic clk, rst_n, mem_want,
input logic new_valid,
input logic [7:0] new_item,
output logic new_ready, issue_valid,
output logic [7:0] issue_item
);
logic valid, mem_grant, conflict;
assign new_ready = !valid || !mem_want;
stall_demo arb (.clk, .rst_n, .fetch_want(valid), .mem_want,
.fetch_grant(issue_valid), .mem_grant, .stall_front(conflict));
front_stage storage (.clk, .rst_n, .stall(!new_ready), .new_item,
.new_valid, .item(issue_item), .valid);
endmodule