An engineer new to a project asks why the on-chip network's links have counters at the sending end instead of ready signals. The answer is one sentence: "The sender always knows how many slots the receiver has free, so it never sends a word that has nowhere to go." The rest of this lesson is why that one sentence is enough.
How can a sender know, without waiting, that the receiver has room?
Stop-and-go (lesson 4.2) tells the sender about a full buffer after it fills, and must reserve a round trip of room above the threshold for words already in flight. That room is idle most of the time, and its size depends on a latency that can change. Worse, the receiver's state is only known approximately at the sender: it knows "not yet told to stop", not "how much room is left".
With credits, the receiver tells the sender, in advance, exactly how much room it has, and the sender keeps count:
At start-up the sender's credit counter is set to the number of free slots at the receiver (lesson 4.4).
To send a word, the sender must hold a credit, and spends it: the counter goes down by one.
When the receiver removes a word from its buffer, freeing a slot, it sends one credit back.
When a credit arrives, the sender's counter goes up by one.
A sender with zero credits does not send. Every word in flight or in the buffer used a credit, and no credit is ever returned until its slot is free, so the receiver can never overflow, however long the path and however late the credits arrive. This is the credit invariant: credits at the sender + words in flight + words in the buffer + credits in flight back = the buffer size, always.
The listing is the simplest credit link: a 2-slot receiver buffer, so 2 credits, one pipeline stage each way, and a consumer that pauses for a few cycles. A checker asserts the credit invariant every cycle and checks that the buffer never overflows and every word arrives in order.
// DCLink step 3: credit-based flow control in its simplest form. The receiver has a
// 2-slot buffer, so the sender starts with 2 credits. Sending a word spends a credit;
// when the receiver removes a word from its buffer it returns one credit. One pipeline
// stage each way. The receiver's consumer sometimes pauses, so the buffer fills, the
// credits run out, and the sender waits without ever overflowing the buffer.
module tb;
localparam int B = 2;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
int cyc = 0;
always @(posedge clk) if (rst_n) cyc <= cyc + 1;
// Sender
logic [2:0] credits = B;
logic [7:0] next_w = 0;
wire send = (credits != 0);
// Channel: one register each way
logic fwd_vld = 0, crd_in = 0;
logic [7:0] fwd_data = 0;
// Receiver: B-slot FIFO, consumer pauses in cycles 3-6
logic [7:0] fifo [0:B-1];
logic [1:0] wp = 0, rp = 0;
logic [2:0] occ = 0;
wire pause = (cyc >= 3 && cyc <= 6);
wire pop = (occ != 0) && !pause;
wire [7:0] rx_data = fifo[rp[0]];
always @(posedge clk) if (rst_n) begin
credits <= credits - send + crd_in;
if (send) next_w <= next_w + 1;
fwd_vld <= send; fwd_data <= next_w;
crd_in <= pop; // one credit back per word removed
if (fwd_vld) begin fifo[wp[0]] <= fwd_data; wp <= wp + 1; end
if (pop) rp <= rp + 1;
occ <= occ + fwd_vld - pop;
end
// Checker
logic [7:0] expect_w = 0; int errors = 0, got = 0;
always @(posedge clk) if (rst_n) begin
if (occ > B) errors++;
if (credits + fwd_vld + occ + crd_in != B) errors++; // every credit accounted for
if (pop) begin if (rx_data != expect_w) errors++; expect_w++; got++; end
end
initial begin
#200;
if (errors == 0 && got >= 6) $display("PASS: %0d words delivered in order; the 2-slot buffer never overflowed", got);
else $display("FAIL: errors %0d, delivered %0d", errors, got);
$finish;
end
endmoduleHow to read it, slot by slot:
Slots 0 and 1: the sender spends both credits (credits 2, 1) and sends two words (send). From slot 2 it has none, so it stops.
Slot 2: the first word has arrived (occ = 1) and is removed immediately (pop). Its credit comes back in slot 3 (crd_in), and the counter shows it in slot 4, when the sender sends a third word.
Slots 3 to 6: the consumer pauses (pause). Nothing is removed, the buffer fills to 2 (occ = 2 in slot 6), and no credits come back. The sender sits at 0 credits from slot 5 and waits. The buffer never exceeds its 2 slots.
Slots 7 to 9: the consumer resumes, removes words (pop), credits return (crd_in in slots 8 and 9), and the sender restarts in slot 9.
The sender stopped because it had no credits; it never needed a message saying "stop". With only two credits and a four-cycle credit round trip, this link can never exceed half throughput, which is lesson 4.5's sizing rule.
Choice | Option | Trade-off |
|---|---|---|
Credit unit | One per word (flit), or per byte group, or per packet | Finer units waste less buffer; coarser units need fewer credit messages |
Credit return | One pulse per credit, or a count, or a cumulative total | Pulses are simple; counts and totals survive lost messages better and save return bandwidth (lesson 4.7) |
Initial value | Fixed by design, or advertised at start-up | Advertising lets each receiver size its own buffers (lesson 4.4) |
Per class | One pool, or separate credits per traffic class | Separate pools stop one class from starving another (lesson 4.6) |
Protocol | Its credits |
|---|---|
PCI Express | Six credit types (posted, non-posted, completion, each header and data); data credits count 16-byte units; credits are advertised at link start-up and returned with UpdateFC packets; a packet waits until enough credits of its type are available. |
On-chip networks | Credits per link and per virtual channel, counting flits, with each router's buffers sized to its link's credit round trip. |
A designer builds a credit link but returns a credit when a word arrives at the receiver rather than when it leaves the buffer (illustrative). Under light traffic it works. When the consumer pauses, the receiver keeps returning credits for words it is still holding, the sender keeps sending, and the buffer overflows. A credit stands for a free slot, and a slot is free only when its word has left. The invariant check in the listing would flag this on the first pause.
It has spent both credits and none has come back yet: the first credit is still on its way. The sender only knows the receiver's state as of one return trip ago.
Credits no longer stand for free slots. While the consumer is paused, the sender gets credits for slots that are still occupied, and the buffer overflows.
credits + words in the forward stage + words in the buffer + credits in the return stage = 2 (the buffer size), in every cycle.
With credits, the sender spends one per word and gets one back each time the receiver frees a slot; with no credits it waits.
The receiver can never overflow, at any distance, because every word in flight or buffered used a credit.
The credit invariant (credits + in flight + buffered + returning = buffer size) is the first thing to assert.