sc_fifo in SystemC — Buffered Communication Between Processes
You already know that sc_signal holds a single value and defers writes to the update phase. But what happens when one process produces data faster than another consumes it — and you can't afford to lose any of it?
That is exactly the problem sc_fifo<T> solves. It is SystemC's built-in blocking first-in first-out channel: the writer blocks when it is full, the reader blocks when it is empty, and every value written is guaranteed to be read exactly once. This article covers the full sc_fifo API, how blocking works under the hood, when to use non-blocking calls, and how to wire it up through module ports.
1. Why sc_signal Isn't Enough
Consider a packet generator that produces one packet every 10 ns, and a CRC checker that needs 25 ns to process each one. With sc_signal, every write overwrites the previous value — by the time the checker reads, two out of every three packets are already gone. The signal is a wire, not a buffer.
// With sc_signal — packets are lost // t=0: generator writes pkt_A → checker starts processing // t=10: generator writes pkt_B → overwrites pkt_A (LOST) // t=20: generator writes pkt_C → overwrites pkt_B (LOST) // t=25: checker reads → sees pkt_C only
sc_fifo buffers all three packets. The checker reads them in order at its own pace. The generator blocks only when the buffer is completely full, naturally applying back-pressure — exactly as a hardware FIFO does.
// With sc_fifo(depth=4) — nothing is lost // t=0: generator writes pkt_A → fifo: [A] // t=10: generator writes pkt_B → fifo: [A, B] // t=20: generator writes pkt_C → fifo: [A, B, C] // t=25: checker reads → gets pkt_A, fifo: [B, C] // t=50: checker reads → gets pkt_B, fifo: [C] // t=75: checker reads → gets pkt_C, fifo: []
2. Declaration and Construction
sc_fifo<T> is a template channel. T can be any type that supports copy-construction and assignment — int, bool, sc_uint<N>, a struct, or even a std::vector.
#include <systemc.h> // Default depth: 16 elements sc_fifo<int> fifo_default; // Named with explicit depth sc_fifo<int> fifo_named("pkt_fifo", 32); // Anonymous with explicit depth — useful inside SC_MODULE sc_fifo<bool> flag_fifo(4); // Custom type — T must be copyable struct Packet { uint32_t id; uint8_t data[64]; }; sc_fifo<Packet> pkt_fifo(8);
write() will stall your simulation once 16 items accumulate. Always set depth explicitly for anything other than a quick test.
3. Blocking Read and Write
The primary API is a pair of blocking calls. Both are only legal inside an SC_THREAD — they may suspend the calling thread and resume it later. Using them inside SC_METHOD is a runtime error.
sc_fifo<int> fifo(4); // depth 4 // ── Blocking write ────────────────────────────────────────── // Inserts val at the tail. // If fifo is FULL: suspends until another thread reads, // then inserts and returns. fifo.write(42); // ── Blocking read ─────────────────────────────────────────── // Removes and returns the head item. // If fifo is EMPTY: suspends until another thread writes, // then removes and returns. int val = fifo.read();
Internally, write() calls wait(fifo.data_read_event()) when full, and read() calls wait(fifo.data_written_event()) when empty. These are the same sc_event objects you can access directly for custom sensitivity patterns (see Section 6).
SC_THREAD, which owns a coroutine stack. An SC_METHOD has no stack to suspend; calling write() or read() inside one triggers a fatal simulation error at runtime. For SC_METHOD, use the non-blocking API in Section 4.
4. Non-Blocking Reads and Writes
The non-blocking variants return immediately regardless of FIFO state. They return true on success and false if the operation could not be completed.
sc_fifo<int> fifo(4); // ── nb_write ───────────────────────────────────────────────── // Returns true if val was inserted, false if fifo was full. bool ok = fifo.nb_write(99); if (!ok) { // fifo was full — handle back-pressure } // ── nb_read ────────────────────────────────────────────────── // Returns true and fills val if data was available. // Returns false and leaves val unchanged if fifo was empty. int val; if (fifo.nb_read(val)) { // val is valid — process it } else { // fifo was empty — come back later } // ── Query state without reading ────────────────────────────── int available = fifo.num_available(); // items ready to read int free_slots = fifo.num_free(); // empty slots available
The idiomatic pattern in an SC_METHOD is: attempt nb_read(); if it returns false, add data_written_event() to the static sensitivity list and return. The scheduler wakes the method again when data arrives.
sc_fifo<int> fifo; void consumer_method() { int val; while (fifo.nb_read(val)) { // drain every available item this activation process(val); } // nothing left — wait until next write arrives next_trigger(fifo.data_written_event()); }
5. FIFO Events
sc_fifo exposes two sc_event objects that fire after every successful write or read. You can wait on them directly or use them in static sensitivity.
// data_written_event() — fires each time an item enters the FIFO // Useful for: consumer waiting for new data wait(fifo.data_written_event()); // SC_THREAD: suspend until next write sensitive << fifo.data_written_event(); // SC_METHOD: static sensitivity // data_read_event() — fires each time an item leaves the FIFO // Useful for: producer monitoring when space opens up wait(fifo.data_read_event()); // Combining events — wake on either condition wait(fifo.data_written_event() | timeout_event);
sc_signal::value_changed_event() which fires only when the value changes, data_written_event() fires every time an item is written — even if you write the same value repeatedly. This is intentional: the arrival of the item is the event, not its content.
6. sc_fifo_in and sc_fifo_out Ports
When a FIFO lives inside a module and must be wired to another module, you declare it as a port rather than directly as an sc_fifo member. The dedicated FIFO port types expose a subset of the FIFO interface appropriate to each side.
SC_MODULE(Producer) { sc_fifo_out<int> out; // write side: write(), nb_write(), num_free(), data_read_event() SC_CTOR(Producer) { SC_THREAD(gen); } void gen() { for (int i = 0; ; i++) { out.write(i); // blocks if full wait(10, SC_NS); } } }; SC_MODULE(Consumer) { sc_fifo_in<int> in; // read side: read(), nb_read(), num_available(), data_written_event() SC_CTOR(Consumer) { SC_THREAD(proc); } void proc() { while (true) { int val = in.read(); // blocks if empty wait(25, SC_NS); // slower consumer } } };
Binding is done in sc_main using the same port-binding syntax as sc_signal:
int sc_main(int, char**) { sc_fifo<int> fifo("channel", 8); // depth 8 Producer prod("prod"); Consumer cons("cons"); prod.out(fifo); // bind sc_fifo_out to sc_fifo cons.in(fifo); // bind sc_fifo_in to sc_fifo sc_start(200, SC_NS); return 0; }
sc_fifo_out does not expose read() or data_written_event() — only the write-side interface. sc_fifo_in does not expose write() or data_read_event(). This enforces the single-producer, single-consumer contract at compile time.
7. Complete Example — Packet Generator and CRC Checker
A realistic example tying everything together. The generator produces one packet every 10 ns. The checker takes 25 ns per packet. The FIFO absorbs the rate difference; back-pressure kicks in automatically once it fills up.
#include <systemc.h> #include <iostream> using namespace std; // ── Packet type ─────────────────────────────────────────────── struct Packet { int id; uint32_t crc; }; // ── Generator ───────────────────────────────────────────────── SC_MODULE(Generator) { sc_fifo_out<Packet> out; SC_CTOR(Generator) { SC_THREAD(run); } void run() { for (int i = 0; i < 10; i++) { Packet p; p.id = i; p.crc = (uint32_t)(i * 0x1234ABCD); out.write(p); // blocks if FIFO full — natural back-pressure cout << "[GEN] t=" << sc_time_stamp() << " sent pkt " << p.id << endl; wait(10, SC_NS); } cout << "[GEN] all packets sent" << endl; } }; // ── CRC Checker ─────────────────────────────────────────────── SC_MODULE(Checker) { sc_fifo_in<Packet> in; SC_CTOR(Checker) { SC_THREAD(run); } void run() { while (true) { Packet p = in.read(); // blocks if FIFO empty wait(25, SC_NS); // simulate CRC computation time bool ok = (p.crc == (uint32_t)(p.id * 0x1234ABCD)); cout << "[CHK] t=" << sc_time_stamp() << " pkt " << p.id << (ok ? " OK" : " FAIL") << endl; } } }; // ── sc_main ─────────────────────────────────────────────────── int sc_main(int, char**) { sc_fifo<Packet> ch("ch", 4); // depth 4 — fills quickly Generator gen("gen"); Checker chk("chk"); gen.out(ch); chk.in(ch); sc_start(400, SC_NS); return 0; }
Expected output (truncated):
[GEN] t=0 s sent pkt 0 [GEN] t=10 ns sent pkt 1 [GEN] t=20 ns sent pkt 2 [GEN] t=30 ns sent pkt 3 [CHK] t=35 ns pkt 0 OK [GEN] t=40 ns sent pkt 4 [CHK] t=60 ns pkt 1 OK [GEN] t=50 ns sent pkt 5 ...
Notice the generator stalls at pkt 3 (FIFO full at depth 4) until the checker reads pkt 0 at 35 ns, freeing one slot. From that point the two run at their natural rates with the FIFO acting as a shock absorber.
8. sc_fifo vs sc_signal — When to Use Which
| Dimension | sc_signal<T> | sc_fifo<T> |
|---|---|---|
| Values buffered | 1 (latest wins) | N (all preserved, FIFO order) |
| Write timing | Deferred (update phase) | Immediate (current delta) |
| Missed writes | Possible — overwritten | Never — writer blocks when full |
| Number of writers | 1 (enforced by standard) | 1 (enforced by port type) |
| Number of readers | Any — all see same value | 1 (enforced by port type) |
| Hardware analogy | Wire / register | UART FIFO, DMA buffer |
| Use in SC_METHOD | Yes — read/write any time | Only nb_read/nb_write |
| Back-pressure | None — writes always succeed | Built-in — writer stalls when full |
| Events | value_changed_event() | data_written_event(), data_read_event() |
The rule of thumb: use sc_signal when you are modelling hardware registers and wires where only the latest value matters. Use sc_fifo when every item must be processed — packets, transactions, commands, stimulus sequences.
9. Common Pitfalls
Pitfall 1 — Blocking call inside SC_METHOD
The most common mistake. read() and write() call wait() internally. SC_METHOD has no coroutine stack, so wait() is illegal and SystemC will terminate with a fatal error.
// WRONG — fatal at runtime if fifo is empty void bad_method() { int v = fifo.read(); // error: wait() not allowed in SC_METHOD } // CORRECT — use nb_read and re-trigger on event void good_method() { int v; if (!fifo.nb_read(v)) { next_trigger(fifo.data_written_event()); return; } process(v); next_trigger(fifo.data_written_event()); }
Pitfall 2 — Forgetting depth and hitting silent back-pressure
With the default depth of 16, a fast producer fills the FIFO in 16 cycles and then blocks. If nothing is consuming (perhaps your consumer thread is still in elaboration, or has a bug), the simulation stalls — not with an error, but with silence. Always set depth explicitly and make sure both sides are active.
// Always name your FIFO with an explicit depth sc_fifo<int> fifo("my_fifo", 32); // 32 elements, named for trace/debug
Pitfall 3 — Two threads deadlocking each other through a full FIFO
If thread A writes to FIFO_1 waiting for FIFO_2 to drain, and thread B writes to FIFO_2 waiting for FIFO_1 to drain, neither can proceed. SystemC will detect that no process is runnable and print a deadlock message, then exit. The fix is to break the cycle — increase depth, use non-blocking writes with retry logic, or restructure the pipeline.
Pitfall 4 — Reading an sc_fifo_in event from the wrong side
sc_fifo_in only exposes data_written_event() (new data arrived). To monitor when space opens up — for a producer to know it can write again — you need data_read_event(), which is only on sc_fifo_out. Confusing these causes processes to never wake up.
// Producer waiting for space: use data_read_event() on sc_fifo_out if (out.num_free() == 0) wait(out.data_read_event()); // fires when consumer reads and frees a slot // Consumer waiting for data: use data_written_event() on sc_fifo_in if (in.num_available() == 0) wait(in.data_written_event()); // fires when producer writes a new item
10. Quick API Reference
| Method | Available on | Behaviour |
|---|---|---|
write(val) | sc_fifo, sc_fifo_out | Blocking insert — suspends if full |
read() | sc_fifo, sc_fifo_in | Blocking remove — suspends if empty, returns value |
nb_write(val) | sc_fifo, sc_fifo_out | Non-blocking insert — returns false if full |
nb_read(val) | sc_fifo, sc_fifo_in | Non-blocking remove — returns false if empty |
num_available() | sc_fifo, sc_fifo_in | Items ready to read |
num_free() | sc_fifo, sc_fifo_out | Empty slots available for writing |
data_written_event() | sc_fifo, sc_fifo_in | Fires after every successful write |
data_read_event() | sc_fifo, sc_fifo_out | Fires after every successful read |
Summary
sc_fifo<T>is a buffered channel — every written item is preserved and consumed exactly once, in order.- Default depth is 16. Always set it explicitly:
sc_fifo<T>("name", depth). write()andread()are blocking — only legal inSC_THREAD.nb_write()andnb_read()are non-blocking — safe inSC_METHOD; combine withdata_written_event()/data_read_event()for re-triggering.sc_fifo_outexposes the write interface;sc_fifo_inexposes the read interface — single-producer, single-consumer enforced at compile time.- Use
sc_signalfor wires where latest value is all that matters. Usesc_fifowhen every item must be processed. - Deadlocks from full FIFOs produce no error until SystemC detects no runnable process — keep depth generous during development and add assertions on
num_available().
Part of the SystemC Foundations guide Browse all SystemC guides →