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);
Default depth is 16 — not unlimited. If you omit the depth argument and your producer is fast, a blocking 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).

Only in SC_THREAD. Blocking calls suspend the current thread — that is only possible in an 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);
Event fires on every operation, not on state change. Unlike 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;
}
Port method availability. 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 buffered1 (latest wins)N (all preserved, FIFO order)
Write timingDeferred (update phase)Immediate (current delta)
Missed writesPossible — overwrittenNever — writer blocks when full
Number of writers1 (enforced by standard)1 (enforced by port type)
Number of readersAny — all see same value1 (enforced by port type)
Hardware analogyWire / registerUART FIFO, DMA buffer
Use in SC_METHODYes — read/write any timeOnly nb_read/nb_write
Back-pressureNone — writes always succeedBuilt-in — writer stalls when full
Eventsvalue_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

MethodAvailable onBehaviour
write(val)sc_fifo, sc_fifo_outBlocking insert — suspends if full
read()sc_fifo, sc_fifo_inBlocking remove — suspends if empty, returns value
nb_write(val)sc_fifo, sc_fifo_outNon-blocking insert — returns false if full
nb_read(val)sc_fifo, sc_fifo_inNon-blocking remove — returns false if empty
num_available()sc_fifo, sc_fifo_inItems ready to read
num_free()sc_fifo, sc_fifo_outEmpty slots available for writing
data_written_event()sc_fifo, sc_fifo_inFires after every successful write
data_read_event()sc_fifo, sc_fifo_outFires after every successful read

Summary


📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.

Part of the SystemC Foundations guide Browse all SystemC guides →