Skip to content

Shift register

A shift register is a linear pipeline: tokens enter at one end and emerge at the other after a fixed number of stages, each contributing a per-stage delay. This example builds a parameterized, N-stage shift register with typed token payloads.

What this example demonstrates:

  • submodule_array and net_array for regular, indexed structure
  • for loops for parameterized connection generation
  • sitar::pack and sitar::unpack for typed integer payloads
  • Phase-disciplined token flow: pull in phase 0, push in phase 1
  • Back-pressure handling with retry loops

Structure

The ShiftRegister module is parameterized on depth N and per-stage latency DELAY. The top-level instantiation ShiftRegister<4> creates four stages each with a 1-cycle delay, giving a total pipeline depth of 4 cycles.

module Top
    submodule S : ShiftRegister<4>
end module

module ShiftRegister
    parameter int N     = 2   // number of pipeline stages
    parameter int DELAY = 1   // latency per stage in cycles

    submodule         prod        : Producer
    submodule         cons        : Consumer
    submodule_array   stage[N]    : Stage<DELAY>
    net_array         n[N+1]      : capacity 1 width 4

    prod.op   => n[0]
    cons.ip   <= n[N]
    for i in 0 to (N-1)
        stage[i].ip <= n[i]
        stage[i].op => n[i+1]
    end for
end module

The resulting topology for ShiftRegister<4>:

flowchart LR
    prod["Producer"]
    s0["stage[0]"]
    s1["stage[1]"]
    s2["stage[2]"]
    s3["stage[3]"]
    cons["Consumer"]
    prod -->|"n[0]"| s0
    s0  -->|"n[1]"| s1
    s1  -->|"n[2]"| s2
    s2  -->|"n[3]"| s3
    s3  -->|"n[4]"| cons

Token payload: pack and unpack

Tokens carry a 4-byte integer payload. The Producer packs each counter value before pushing; the Consumer unpacks after pulling.

token<4> t;
int val = 42;
sitar::pack(t, val);     // serialize val into t's 4-byte payload

sitar::unpack(t, val);   // deserialize back into val

sitar::pack and sitar::unpack accept any number of arguments; the total sizeof of all arguments must match the declared token width at compile time.


Producer

The Producer generates tokens 0, 1, 2, ... and pushes as many as the output net will accept each phase. push() returns false when the net is full; the inner while loop stops and the module advances to the next phase.

module Producer
    outport op : width 4

    decl $
    static const int NUM_TOKENS = 6;
    int      count;
    token<4> t;
    $
    init $count = 0;$

    behavior
        do
            wait until (this_phase == 1);
            $
            // Pack the current count into the token payload and push.
            // push() returns false if the net is full; the loop retries
            // each time the output net frees up a slot.
            sitar::pack(t, count);
            while (op.push(t)) {
                log << endl << " sent " << count;
                count++;
                if (count >= NUM_TOKENS) break;
                sitar::pack(t, count);
            }
            $;
            wait;
        while (count < NUM_TOKENS) end do;
    end behavior
end module

Pipeline stage

Each Stage pulls one token in phase 0, waits DELAY cycles, then pushes in phase 1. Both pull and push are written as retry loops: if the operation fails (net empty or full), the module advances one phase and tries again.

module Stage
    parameter int DELAY = 1

    inport  ip : width 4
    outport op : width 4

    decl $token<4> t; bool done;$

    behavior
        do
            // Phase 0: pull one token (block until one is available)
            $done = false;$;
            do
                wait until (this_phase == 0);
                $done = ip.pull(t);$;
                if (not done) then wait end if;
            while (not done) end do;

            // Hold for DELAY cycles — models pipeline stage latency
            wait(DELAY, 0);

            // Phase 1: push the token downstream (retry if net is full)
            $done = false;$;
            do
                wait until (this_phase == 1);
                $done = op.push(t);$;
                if (not done) then wait end if;
            while (not done) end do;
        while (1) end do;
    end behavior
end module

Phase discipline

The wait until (this_phase == 0) and wait until (this_phase == 1) guards ensure reads and writes occur in the correct phase. A Stage that pulls a token at cycle c, phase 0 and delays by DELAY=1 cycles will push it at cycle c+1, phase 1.


Consumer

The Consumer drains all available tokens in phase 0 each cycle, unpacking and logging each value. Simulation stops when NUM_TOKENS values have been received.

module Consumer
    inport ip : width 4

    decl $
    static const int NUM_TOKENS = 6;
    int      count;
    token<4> t;
    int      val;
    $
    init $count = 0;$

    behavior
        do
            // Phase 0: drain all available tokens
            wait until (this_phase == 0);
            $
            while (ip.pull(t)) {
                sitar::unpack(t, val);
                log << endl << " received " << val;
                count++;
            }
            $;
            wait;
        while (count < NUM_TOKENS) end do;
        stop simulation;
    end behavior
end module

Expected output

With N=4, DELAY=1, and NUM_TOKENS=6, the first token arrives at the Consumer after 4 pipeline stages (approximately cycle 5):

(0,1)  TOP.S.prod :  sent 0
(0,1)  TOP.S.prod :  sent 1
(0,1)  TOP.S.prod :  sent 2
...
(5,0)  TOP.S.cons :  received 0
(6,0)  TOP.S.cons :  received 1
(7,0)  TOP.S.cons :  received 2
...
Simulation stopped at time (10,0)

Changing the instantiation to ShiftRegister<8> extends the pipeline to 8 stages; no other change to the model is needed.