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.

Per-word turns cut the packetsThe merger's output on the lab bench. A word leaves at the edge after a cycle where o_valid and o_ready are 1. o_data names each word: A0.1 is port A, packet 0, word 1.
01234567clko_readyo_dataA0.0B0.0A0.1B0.1A0.2B1.0o_last
  1. Cycle 0

    Cycle e4: A0.0 leaves. Both FIFOs hold words.

  2. Cycle 1

    Cycle e5: the arbiter has taken B's turn, so B0.0 follows. Packet A0 is now split.

  3. Cycle 4

    Cycle e8: B0.1 carries last. The consumer closes a packet of four words: A0.0 B0.0 A0.1 B0.1.

  4. 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.

Design brief
PortDirectionWidthMeaning
clkinput1clock
rst_ninput1reset, asynchronous, active-low
a_validinput1A 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_datainput[7:0]the word from A
a_lastinput1the last word of A's packet
a_readyoutput1the merger takes A's word; a full FIFO drops its ready
b_valid, b_data, b_lastinput1, [7:0], 1the same for B
b_readyoutput1the same for B
o_validoutput1the output has a word; comes from a flop
o_dataoutput[7:0]the word out; comes from a flop
o_lastoutput1the last word of a packet; comes from a flop
o_readyinput1the 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 last has left. Fairness: when both ports have a packet waiting, they take turns, packet by packet; A goes first after reset
The mergerArrows carry words, except the two from the FIFOs to the arbiter, which carry "not empty", and the arbiter's select.
The mergerArrows carry words, except the two from the FIFOs to the arbiter, which carry "not empty", and the arbiter's select. queue FIFO A 4 words of {last,data} queue FIFO B 4 words of {last,data} logic Arbiter round robin, holds tolast logic Mux 2:1, select pick_b flop Output register o_valid, o_data,o_last target Consumer splits at last; maystall a_wordb_wordnot emptynot emptypick_bwordo_valid, o_data,o_last
  1. Buffer

    Each source writes into its own FIFO whenever the FIFO has room. A source never waits for the other port.

  2. Choose a port

    The arbiter sees which FIFOs hold a word. Both do, and A goes first after reset.

  3. 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 last has gone.

    Follow the calls, values and waits
    1. data A0.0 .. A0.2: FIFO A → Mux
    2. data A0.0 .. A0.2: Mux → Output register
    3. data A0.0 .. A0.2: Output register → Consumer
  4. Switch

    Only then does B get its turn, for its whole packet.

    Follow the calls, values and waits
    1. data B0.0, B0.1: FIFO B → Mux
    2. data B0.0, B0.1: Mux → Output register
    3. 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.

Turns by packetThe same bench with the fix. open is open_q, the register that holds the grant.
01234567clko_readyo_dataA0.0A0.1A0.2B0.0B0.1A1.0o_lastopen
  1. Cycle 0

    Cycle e4: A0.0 is leaving and open is 1. The arbiter stays on A though B has words.

  2. 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 cleared open.

  3. Cycle 4

    Cycle e8: B0.0. B got its turn only after A's last word had left its FIFO.

  4. 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.

MergerPackets cut (of 8)Per portCells (flops)Levels of logic
turns by word (shipped)6A 4, B 4240 (95)7
turns by packet0A 4, B 4245 (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
  1. 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.

  2. 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.

  3. 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.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: 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 order

Original 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

Predict the outputThe merger with turns by packet. Each port has two packets of two words, both start in the same cycle, and the consumer never stalls. The program lists the packets in the order they leave. What does it print?
// merger: joins two packet streams, A and B, into one. A word moves on a port at an edge where
// its valid and ready are both 1; the word that carries last = 1 ends its packet.

// fifo: DEPTH words of W bits, first in, first out. This lesson's own small FIFO: dout shows
// the oldest word in the same cycle (first-word fall-through), so the arbiter can see it.
module fifo #(parameter int W = 9, parameter int DEPTH = 4) (
  input  logic         clk,
  input  logic         rst_n,
  input  logic         push,         // write din at this edge (ignored when full)
  input  logic [W-1:0] din,
  output logic         full,
  input  logic         pop,          // drop the oldest word at this edge (ignored when empty)
  output logic [W-1:0] dout,         // the oldest word
  output logic         empty
);
  localparam int AW = $clog2(DEPTH);
  logic [W-1:0] mem [DEPTH];
  logic [AW:0]  wr_q, rd_q;          // one extra bit tells full from empty
  assign empty = (wr_q == rd_q);
  assign full  = (wr_q[AW] != rd_q[AW]) && (wr_q[AW-1:0] == rd_q[AW-1:0]);
  assign dout  = mem[rd_q[AW-1:0]];
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0;
    end else begin
      if (push && !full)  wr_q <= wr_q + 1'b1;
      if (pop  && !empty) rd_q <= rd_q + 1'b1;
    end
  always_ff @(posedge clk)                 // the storage has no reset
    if (push && !full) mem[wr_q[AW-1:0]] <= din;
endmodule

module merger (
  input  logic       clk,
  input  logic       rst_n,              // active-low, asynchronous
  input  logic       a_valid,
  input  logic [7:0] a_data,
  input  logic       a_last,
  output logic       a_ready,
  input  logic       b_valid,
  input  logic [7:0] b_data,
  input  logic       b_last,
  output logic       b_ready,
  output logic       o_valid,
  output logic [7:0] o_data,
  output logic       o_last,
  input  logic       o_ready
);
  logic [8:0] a_word, b_word, word;      // {last, data}
  logic       a_empty, b_empty, a_full, b_full;
  logic       pick_b;                    // 1: the next word comes from B
  logic       take;                      // a word moves into the output register at this edge

  fifo #(.W(9), .DEPTH(4)) u_fa (.clk(clk), .rst_n(rst_n), .push(a_valid), .din({a_last, a_data}), .full(a_full),
                                 .pop(take && !pick_b), .dout(a_word), .empty(a_empty));
  fifo #(.W(9), .DEPTH(4)) u_fb (.clk(clk), .rst_n(rst_n), .push(b_valid), .din({b_last, b_data}), .full(b_full),
                                 .pop(take && pick_b), .dout(b_word), .empty(b_empty));
  assign a_ready = !a_full;
  assign b_ready = !b_full;

  // The arbiter: round robin between the two FIFOs, one packet at a time.
  logic last_b_q;                        // the last word taken came from B
  logic open_q;                          // a packet has started and its last word has not left
  always_comb begin
    if (open_q)                    pick_b = last_b_q;   // finish the open packet first
    else if (!a_empty && !b_empty) pick_b = !last_b_q;  // both have a word: the other port's turn
    else                           pick_b = !b_empty;   // only one has a word (or none)
  end
  assign word = pick_b ? b_word : a_word;           // the 2:1 mux
  assign take = (pick_b ? !b_empty : !a_empty) && (!o_valid || o_ready);

  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      last_b_q <= 1'b1; open_q <= 1'b0;  // so that A goes first after reset
    end else if (take) begin
      last_b_q <= pick_b;
      open_q   <= !word[8];              // open until the word with last leaves
    end

  // The output register: it loads when it is empty or its word is leaving.
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      o_valid <= 1'b0; o_data <= '0; o_last <= 1'b0;
    end else if (take) begin
      o_valid <= 1'b1; {o_last, o_data} <= word;
    end else if (o_ready) begin
      o_valid <= 1'b0;
    end
endmodule

`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 = 1;
  int len_a[$] = '{2, 2}, len_b[$] = '{2, 2};   // 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 = !(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 (20) tick();
    $display("%s", order_s);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule
Spot the bugEach option is the line that updates open_q when a word is taken. Source A pauses for two cycles inside its first packet; the bench expects four whole packets. Which line fails?
ConceptWhy must this merger arbitrate per packet and not per word?
ConceptThe merger registers o_valid, o_data and o_last. What does that buy?
Read the waveformThe merger with turns by packet, on the lab bench. In which cycle does o_data first show a word of port B, and why then?
Turns by packet
01234567o_readyo_dataA0.0A0.1A0.2B0.0B0.1A1.0o_last

Cycles e4 to e11, left to right; o_valid is 1 throughout.

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 last and 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.