Credit Stream
Overview
CreditStreamIF is a forward stream plus a reverse credit channel. The consumer periodically
reports how much it has consumed, and the producer uses that to know whether a write will fit —
before committing to it.
Why you would use one — and when you should not
A FIFO already implements credit. TREADY is credit, delivered implicitly, one unit at a
time, at the moment of use. An explicit credit channel is nothing but back-pressure moved earlier
and in bulk.
So you want one only when “at the moment of use” is too late: when the producer commits to a multi-word transaction it cannot abandon partway. A data converter is the motivating case — it presents samples whether or not the fabric is ready, so discovering halfway through a burst that there is no room is not a situation it can be in.
If your producer can simply block, it should. Use a plain StreamIF
and let write stall. Credit buys nothing there and costs you a second channel.
The other reverse channel answers the opposite question: Acked Stream reports what became of what you sent — after the fact, from the only party that can know.
Building one
Three objects, and the interface builds and wires the two underlying streams itself.
import numpy as np
from waveflow.hw.clock import Clock
from waveflow.hw.reverse_stream import (CreditStreamIF, CreditStreamMasterIF,
CreditStreamSlaveIF)
from waveflow.simulation.simulation import Simulation
sim = Simulation()
producer = CreditStreamMasterIF(name="producer", sim=sim, bitwidth=32)
consumer = CreditStreamSlaveIF(name="consumer", sim=sim, bitwidth=32)
chan = CreditStreamIF(name="chan", sim=sim, clk=Clock(freq=250e6),
bitwidth=32, depth=8, credit_depth=4)
chan.bind("master", producer)
chan.bind("slave", consumer)
print("forward:", chan.fwd_if.bitwidth, "bits |", "credit:", chan.crd_if.bitwidth, "bits")
print("forward depth:", producer.depth, "| avail at rest:", producer.avail)
forward: 32 bits | credit: 16 bits
forward depth: 8 | avail at rest: 7
The reverse channel is as wide as its counter, not as wide as a word. A credit value is a
ctr_bits-wide cumulative count, so the credit FIFO is sized for that regardless of how wide the
data is — a 64-bit data path does not buy a 64-bit credit channel.
The reverse channel’s master is the data slave, and bind does that wiring. Getting it
backwards would present as a hang rather than an error, which is why it is not left to a call
site.
The parameters
| on | parameter | meaning |
|---|---|---|
CreditStreamIF |
bitwidth |
word width of the forward channel |
depth |
forward queue depth. May not be None — credit is depth - outstanding, and an unbounded queue has no depth to compute it from |
|
credit_depth |
reverse queue depth | |
ctr_bits |
width of the cumulative counter (16), and therefore of the reverse channel | |
CreditStreamMasterIF |
resp_words |
headroom reserved so a response can never be refused for room |
CreditStreamSlaveIF |
queue_size |
optional bound on the consumer’s receive queue |
The methods
CreditStreamMasterIF — the producer:
poll_credit(n=1) |
take up to n credit values; returns how many were taken |
write_nb(words) |
write if the accounting says it fits, else refuse. Never blocks; returns bool |
write_resp_nb(resp) |
write a response, drawing on the reserved headroom so room cannot refuse it |
avail, depth |
what the accounting believes is free |
CreditStreamSlaveIF — the consumer:
get(nwords_max=None) |
consume a burst, then offer the new cumulative total back |
offer_credit() |
offer the current total explicitly. Non-blocking; may be dropped |
get already returns credit, so a consumer that reads normally keeps the channel honest without
doing anything. offer_credit is for a consumer that drains the forward channel some other way.
Usage
The typical loop:
- Producer calls
poll_credit(n)to absorb whatever the consumer has reported - Producer calls
write_nb(words); aFalsemeans no room, not an error — the producer decides whether to retry, drop, or stall - Consumer calls
get(), which consumes and reports the new total in one step
An example
taken = []
def run():
for i in range(3):
ok = yield from producer.write_nb(np.array([i], dtype=np.uint32))
taken.append(ok)
print("writes accepted:", taken, "| avail now:", producer.avail)
got = yield from consumer.get()
print("consumer read:", int(np.asarray(got).reshape(-1)[0]))
n = yield from producer.poll_credit(4)
print("credit values absorbed:", n, "| avail after:", producer.avail)
sim.env.process(run())
sim.env.run()
writes accepted: [True, True, True] | avail now: 4
consumer read: 0
credit values absorbed: 1 | avail after: 5
Four rules that will bite you
Reverse values are cumulative, never incremental. The consumer reports a running total, not “I freed three”. A lost incremental update would be lost forever; a lost cumulative one is superseded by the next.
Both directions are non-blocking. write_nb refuses rather than waiting; offer_credit drops
rather than waiting. A producer that cannot abandon a transaction also cannot be made to wait.
Poll a bounded number, never drain-to-empty. poll_credit(n) takes up to n. In the
synthesizable twin n is a compile-time constant that unrolls; while (got): ... has no
translation.
A saturated reverse channel is not stale-but-safe — it is permanently wrong. If the credit FIFO
fills and offers start dropping, the producer’s view stops advancing and never recovers on its own,
because nothing retransmits. Size credit_depth so that cannot happen; it is a sizing violation,
not a transient.
See also
- Acked Stream — the other reverse channel, for what became of what I sent
StreamIF— the primitive both directions are built from, and the right answer when the producer can block- Derived interfaces — how a derived interface composes its primitives