Capstone: a two-input packet merger
Join two packet streams into one from a brief: two FIFOs, a round-robin arbiter that holds its grant until the last word, a mux and an output register.
- Lesson
- R5.6
- Time
- 40 min
- Runs on
- Your browser
- Check
- 5 questions
A merger joined two packet streams into one link. Its arbiter took turns word by word, so the first packet out held two words of port A and two of port B. The consumer split packets at each last and could rebuild neither.
o_valid and o_ready are 1. o_data names each word: A0.1 is port A, packet 0, word 1.- Cycle 0
Cycle e4: A0.0 leaves. Both FIFOs hold words.
- Cycle 1
Cycle e5: the arbiter has taken B's turn, so B0.0 follows. Packet A0 is now split.
- Cycle 4
Cycle e8: B0.1 carries
last. The consumer closes a packet of four words: A0.0 B0.0 A0.1 B0.1. - Cycle 5
Cycle e9: A0.2, the true end of packet A0, arrives as a packet of its own.
Measured on the lab bench, cycles e4 to e11. Six of the eight packets that leave are cut.
The brief and the sketch
A packet is a run of words whose last one carries last = 1 (Protocols P1.4). A merger takes packets from two ports and sends them on one. Its consumer finds packets by last alone, so the words of one packet must leave together.
| Port | Direction | Width | Meaning |
|---|---|---|---|
| clk | input | 1 | clock |
| rst_n | input | 1 | reset, asynchronous, active-low |
| a_valid | input | 1 | A has a word; a word moves at an edge where its valid and ready are both 1; a source holds its word until then |
| a_data | input | [7:0] | the word from A |
| a_last | input | 1 | the last word of A's packet |
| a_ready | output | 1 | the merger takes A's word; a full FIFO drops its ready |
| b_valid, b_data, b_last | input | 1, [7:0], 1 | the same for B |
| b_ready | output | 1 | the same for B |
| o_valid | output | 1 | the output has a word; comes from a flop |
| o_data | output | [7:0] | the word out; comes from a flop |
| o_last | output | 1 | the last word of a packet; comes from a flop |
| o_ready | input | 1 | the consumer may stall with o_ready = 0 |
- Parameters
- Buffering: a FIFO of 4 words on each input; a full FIFO drops its
ready - Corner cases
- one-word packets; a source that pauses inside a packet; a stalled consumer; both ports starting together. Packet rule: the words of one packet leave together, in order; a new packet may start only after the word with
lasthas left. Fairness: when both ports have a packet waiting, they take turns, packet by packet; A goes first after reset
- Buffer
Each source writes into its own FIFO whenever the FIFO has room. A source never waits for the other port.
- Choose a port
The arbiter sees which FIFOs hold a word. Both do, and A goes first after reset.
- Send the packet
The mux passes A's words into the output register while the consumer takes them. The arbiter stays on A until the word with
lasthas gone.Follow the calls, values and waits
- data A0.0 .. A0.2: FIFO A → Mux
- data A0.0 .. A0.2: Mux → Output register
- data A0.0 .. A0.2: Output register → Consumer
- Switch
Only then does B get its turn, for its whole packet.
Follow the calls, values and waits
- data B0.0, B0.1: FIFO B → Mux
- data B0.0, B0.1: Mux → Output register
- data B0.0, B0.1: Output register → Consumer
Read the diagram as text
- FIFO A: 4 words of {last, data}
- FIFO B: 4 words of {last, data}
- Arbiter: round robin, holds to last
- Mux: 2:1, select pick_b
- Output register: o_valid, o_data, o_last
- Consumer: splits at last; may stall
- FIFO A → Mux: a_word
- FIFO B → Mux: b_word
- FIFO A → Arbiter: not empty
- FIFO B → Arbiter: not empty
- Arbiter → Mux: pick_b
- Mux → Output register: word
- Output register → Consumer: o_valid, o_data, o_last
Every part comes from this module: FIFO pointers with an extra bit (R5.2), depth from bursts (R5.3), the arbiter (R5.4) and the mux (R5.5).
Hold the grant until last
The shipped arbiter is the round robin of R5.4 between two ports, with one change of scale: its turn is one word. The fix makes its turn one packet. A register open_q remembers that a packet has started and its last word has not yet been taken.
A turn is one word
always_comb begin
if (!a_empty && !b_empty) pick_b = !last_b_q;
else pick_b = !b_empty;
end
// on take:
last_b_q <= pick_b;
Six of eight packets leave cut in two.
A turn is one packet
always_comb begin
if (open_q) pick_b = last_b_q;
else if (!a_empty && !b_empty) pick_b = !last_b_q;
else pick_b = !b_empty;
end
// on take:
last_b_q <= pick_b;
open_q <= !word[8]; // bit 8 is last
Eight packets leave whole, four from each port.
While open_q is 1, the arbiter stays on the port it served last, even when that FIFO is empty. A source that pauses in the middle of a packet makes the output wait; it does not hand the link to the other port.
open is open_q, the register that holds the grant.- Cycle 0
Cycle e4: A0.0 is leaving and
openis 1. The arbiter stays on A though B has words. - Cycle 2
Cycle e6: A0.2, the word with
last, sits in the output register. It was taken from the FIFO at edge 6, which clearedopen. - Cycle 4
Cycle e8: B0.0. B got its turn only after A's last word had left its FIFO.
- Cycle 7
Cycle e11: A1.0, a one-word packet, opens and closes in the same word.
Measured on the lab bench, cycles e4 to e11. The stall in e6 delays A0.2 by a cycle, and the turn waits with it.
What the output register does
The output register loads a word when it is empty or when its word is leaving: take = word ready && (!o_valid || o_ready). So the merger can move one word per cycle while the consumer keeps o_ready at 1. The outputs leave from flops, and the arbiter and mux logic ends inside the merger.
The o_ready input still reaches the FIFO pops through take in the same cycle. A skid buffer (R4.4) would cut that path too, at the price of a second register.
| Merger | Packets cut (of 8) | Per port | Cells (flops) | Levels of logic |
|---|---|---|---|---|
| turns by word (shipped) | 6 | A 4, B 4 | 240 (95) | 7 |
| turns by packet | 0 | A 4, B 4 | 245 (96) | 10 |
Measured on the lab bench and with Yosys 0.70 on this flow. 72 of the flops are FIFO storage: 2 FIFOs x 4 words x 9 bits.
Holding the grant costs one flop and 5 cells, and three levels of logic: open_q now sits in front of the select, the mux and the pops. The FIFOs dominate the area, as they will in most mergers.
The lab
Run the lab: the bench sends packets of 3, 1, 4 and 2 words on A and 2, 3, 1 and 4 on B, both at once. The consumer stalls in every third cycle. Each line is one packet as the consumer split it, with the edge where its last word left.
Six lines end in "cut". Now apply the right-hand side of the compare block: declare open_q, add the first branch to the arbiter, update open_q beside last_b_q, and reset it to 0. Run again: eight whole packets and "0 cut".
Asked in interviews
Design a two-port packet merger: which blocks?
A FIFO per input, so a source can keep sending while the other port has the link. A round-robin arbiter that holds its choice until the word with last has gone. A 2:1 mux, and an output register so the outputs come from flops.
How do you keep packets whole?
Arbitrate per packet. Keep a bit that says a packet is open: set it when a word without last is taken, clear it when the last word is taken. While it is set, do not change ports, even if the open port's FIFO runs empty.
How do you size its FIFOs?
From the brief's burst arithmetic (R5.3): while one port sends its longest packet, the other port's FIFO must absorb what its source sends, or that source is stalled through ready. A stall is legal; overflow is not.
Try it
Each line is one packet as the consumer sees it: the words between two last flags, and the edge where the last of them left. The words of each port still arrive in order, so the summary counts 0 out of order. What breaks is the grouping: six packets hold words of both ports.
With turns by packet the run prints e8 A0.0 A0.1 A0.2, e11 B0.0 B0.1, e12 A1.0, e17 B1.0 B1.1 B1.2, e23 A2.0 A2.1 A2.2 A2.3, e24 B2.0, e27 A3.0 A3.1, e33 B3.0 B3.1 B3.2 B3.3. The summary reads 8 packets out, 0 cut; A 4, B 4; 0 out of order. Synthesize reports 245 cells, 96 flops and 10 levels of logic.
Synthesize the design to measure its cells and inspect the mapped gates.
e9 A0.0 B0.0 A0.1 B0.1 cut e11 A0.2 e14 B1.0 A1.0 cut e18 B1.1 A2.0 B1.2 cut e21 A2.1 B2.0 cut e26 A2.2 B3.0 A2.3 cut e32 B3.1 A3.0 B3.2 A3.1 cut e33 B3.3 8 packets out, 6 cut; A 4, B 4; 0 out of orderOriginal design · 240 cells · 95 flip-flops · 0 latches · 7 logic levels. Synthesis uses Yosys; counts depend on the toolchain.
Read included file: tb.sv
`timescale 1ns/1ps
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
logic a_valid = 0, a_last = 0, b_valid = 0, b_last = 0, o_ready = 0;
logic [7:0] a_data = 0, b_data = 0;
logic a_ready, b_ready, o_valid, o_last;
logic [7:0] o_data;
bit quiet = 0;
int len_a[$] = '{3, 1, 4, 2}, len_b[$] = '{2, 3, 1, 4}; // packet lengths in words
int pa = 0, wa = 0, pb = 0, wb = 0; // the packet and word each source offers next
int next_a = 0, next_b = 0; // the {packet, word} each port should deliver next
int first_port = -1, mixed = 0, packets = 0, cut = 0, order = 0, from_a = 0, from_b = 0;
string line = "", order_s = "order:";
function automatic string name(input logic [7:0] w);
return $sformatf("%s%0d.%0d", w[7] ? "B" : "A", w[6:4], w[3:0]);
endfunction
merger dut (.clk(clk), .rst_n(rst_n), .a_valid(a_valid), .a_data(a_data), .a_last(a_last), .a_ready(a_ready),
.b_valid(b_valid), .b_data(b_data), .b_last(b_last), .b_ready(b_ready),
.o_valid(o_valid), .o_data(o_data), .o_last(o_last), .o_ready(o_ready));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
if (edge_n >= 2) begin
// The sources offer their next words and hold them until ready; the consumer may stall.
a_valid = (pa < len_a.size()) && !(0);
a_data = {1'b0, 3'(pa), 4'(wa)}; a_last = a_valid && (wa == len_a[pa] - 1);
b_valid = (pb < len_b.size());
b_data = {1'b1, 3'(pb), 4'(wb)}; b_last = b_valid && (wb == len_b[pb] - 1);
o_ready = !(edge_n % 3 == 0);
#1;
if (a_valid && a_ready) begin wa++; if (a_last) begin pa++; wa = 0; end end
if (b_valid && b_ready) begin wb++; if (b_last) begin pb++; wb = 0; end end
if (o_valid && o_ready) begin // this word leaves at the next edge
if (first_port < 0) first_port = o_data[7];
else if (o_data[7] != first_port) mixed = 1;
if (o_data[7]) begin if (o_data[6:0] != next_b) order++; next_b = o_last ? (o_data[6:4] + 1) * 16 : o_data[6:0] + 1; end
else begin if (o_data[6:0] != next_a) order++; next_a = o_last ? (o_data[6:4] + 1) * 16 : o_data[6:0] + 1; end
line = {line, line == "" ? "" : " ", name(o_data)};
if (o_last) begin
packets++; if (mixed) cut++;
if (o_data[7]) from_b++; else from_a++;
order_s = {order_s, $sformatf(" %s%0d", o_data[7] ? "B" : "A", o_data[6:4])};
if (!quiet && mixed) $display("e%0d %s cut", edge_n + 1, line);
else if (!quiet) $display("e%0d %s", edge_n + 1, line);
line = ""; first_port = -1; mixed = 0;
end
end
end
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
repeat (40) tick();
$display("%0d packets out, %0d cut; A %0d, B %0d; %0d out of order", packets, cut, from_a, from_b, order);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- A merger is two FIFOs, an arbiter, a 2:1 mux and an output register; the FIFOs let each source keep sending while the other port has the link.
- Arbitrate per packet: one register says a packet is open, set by a word without
lastand cleared by the word with it. - While a packet is open the arbiter waits for that port, even if its FIFO runs empty; it never hands the link to the other port mid-packet.
- On this flow the hold cost 5 cells and one flop, and three levels of logic; the FIFO storage is 72 of the 96 flops.