Standard library
C⏚ ships a small library of built-in tasks for the building
blocks every hardware design needs. All built-ins live under the
std.* packages and are implementation-resolved by the compiler.
| Entity | Package | Purpose |
|---|---|---|
SynchronousFIFO | std.fifo | FIFO buffer, one clock domain |
AsynchronousFIFO | std.fifo | FIFO buffer, two clock domains (CDC) |
SynchronizerFF | std.lib | 1-bit clock-domain crossing |
SynchronizerMux | std.lib | N-bit clock-domain crossing |
SinglePortRAM | std.mem | One address port, read or write |
DualPortRAM | std.mem | Two address ports, independent reads and writes |
PseudoDualPortRAM | std.mem | One read port and one write port |
SynchronousFIFO
package std.fifo;
task SynchronousFIFO {
const int size, width;
stream {
in unsigned<width> din;
out unsigned<width> dout;
}
}| Parameter | Required | Meaning |
|---|---|---|
size | yes | Capacity in elements |
width | yes | Data width in bits |
The producer writes to din unless the FIFO is full. The
consumer reads from dout unless the FIFO is empty. Both ports
are stream, so the FIFO back-pressures the producer when full
and stalls the consumer when empty.
Using a FIFO
Instantiate the FIFO in a network with new, passing size and
width as named arguments, then wire it with the positional
reads() form: fifo.reads(producer.dout) connects the
producer's output to the FIFO's din, and consumer.reads(fifo.dout)
connects the FIFO's dout to the consumer's input.
network Pipe {
fifo = new std.fifo.SynchronousFIFO({size: 16, width: 8});
producer = new task {
out stream u8 dout;
u8 n = 0;
void loop() {
n++;
dout.write(n); // stalls automatically when the FIFO is full
}
};
consumer = new task {
in stream u8 din;
void loop() {
u8 v = din.read; // stalls automatically until data is available
print("got ", v, "\n");
}
};
fifo.reads(producer.dout); // producer.dout -> fifo.din
consumer.reads(fifo.dout); // fifo.dout -> consumer.din
}The stream handshake is automatic: a dout.write blocks while
the FIFO is full and a din.read blocks while it is empty, so you
never have to poll full/empty yourself. Read each value with a
single din.read and let the loop come back around for the next -
the FIFO holds the next element until you read it, even across an
idle() or other work in between.
AsynchronousFIFO
Dual-clock FIFO for clock-domain crossing (CDC). din is clocked
by din_clock; dout is clocked by dout_clock.
package std.fifo;
task AsynchronousFIFO {
properties { clocks: ["din_clock", "dout_clock"] }
const int size, width;
stream {
in unsigned<width> din;
out unsigned<width> dout;
}
}| Parameter | Required | Meaning |
|---|---|---|
size | yes | Capacity in elements |
width | yes | Data width in bits |
Same back-pressure semantics as
SynchronousFIFO: writes stall when full,
reads stall when empty. Use this when the producer and consumer
run on independent clocks; otherwise prefer the synchronous
variant.
SynchronizerFF
Clock-domain crossing for a 1-bit signal.
package std.lib;
task SynchronizerFF {
properties { clocks: ["din_clock", "dout_clock"] }
const int stages = 2;
in bool din;
out bool dout;
}A shift register of stages flip-flops, clocked by dout_clock.
Increase stages for higher clock speeds or fault-tolerant
designs. The default of 2 is the textbook minimum.
SynchronizerMux
Clock-domain crossing for an N-bit signal.
package std.lib;
task SynchronizerMux {
properties { clocks: ["din_clock", "dout_clock"] }
const int width = 16, stages = 2;
in push unsigned<width> din;
out unsigned<width> dout;
}| Parameter | Default | Meaning |
|---|---|---|
width | 16 | Data width in bits |
stages | 2 | Synchronizer FF depth (see SynchronizerFF) |
How it works:
- A producer writes a new value to
din(apushport). - A 1-bit sync signal crosses from the input domain to the
output domain via the internal
SynchronizerFF. - The input value must stay stable during the crossing - no new
write to
dinuntil the crossing completes. - When the sync signal arrives in the output domain, the value
is sampled and presented on
dout.
SinglePortRAM
package std.mem;
task SinglePortRAM {
properties { reset: null }
const int size, width, depth = sizeof(size - 1);
const bool writeShiftMode = false, addOutputRegister = false;
in unsigned<depth> address, push unsigned<width> data;
out unsigned<width> q;
}| Parameter | Required | Default | Meaning |
|---|---|---|---|
size | yes | - | Element count |
width | yes | - | Element width in bits |
writeShiftMode | no | false | false: write-through (new value on q). true: read-before-write (previous value on q). |
addOutputRegister | no | false | Adds an output register, delaying q by one cycle. Can help timing on FPGA. |
depth is computed from size; you don't set it.
| Port | Direction | Meaning |
|---|---|---|
address | in | Address to read or write |
data | in push | When valid, writes data at address; otherwise the RAM is in read mode |
q | out | Value read or written at the previous cycle's address |
Example
network N {
ram = new std.mem.SinglePortRAM({ size: 32, width: 128 });
ctrl = new task {
void loop() {
ram.address.write(8);
ram.data.write(13); // write 13 at address 8
ram.address.write(21);
ram.data.write(34); // write 34 at address 21
ram.address.write(8); // cycle 1: address arrives
ram.address.write(21); // cycle 2: address arrives
fence; // cycle 3: q reflects cycle-1 read
print("read @8 = ", ram.q.read());
print("read @21 = ", ram.q.read());
}
};
}A RAM read has one cycle of latency. Issuing two back-to-back reads (without a fence between them) lets the address pipeline amortize the latency: total time goes from 6 cycles to 4.
DualPortRAM
Two independent address ports, each with its own clock.
package std.mem;
task DualPortRAM {
properties { reset: null, clocks: ["clock_a", "clock_b"] }
const int size, width, depth = sizeof(size - 1);
in uint<depth> address_a, push uint<width> data_a;
out uint<width> q_a;
in uint<depth> address_b, push uint<width> data_b;
out uint<width> q_b;
}Parameters are the same as
SinglePortRAM. Ports ending in _a are
clocked by clock_a; ports in _b by clock_b. Each port
group behaves like a SinglePortRAM independently.
PseudoDualPortRAM
One read port, one write port, independent clocks. Smaller than a full DualPortRAM, throughput in between.
package std.mem;
task PseudoDualPortRAM {
properties { reset: null, clocks: ["rd_clock", "wr_clock"] }
const int size, width, depth = sizeof(size - 1);
in uint<depth> rd_address, wr_address, push uint<width> data;
out uint<width> q;
}| Port | Direction | Clock | Meaning |
|---|---|---|---|
rd_address | in | rd_clock | Read address |
wr_address | in | wr_clock | Write address |
data | in push | wr_clock | When valid, writes data at wr_address |
q | out | rd_clock | Value at the previous-cycle rd_address |
Reads and writes may run concurrently and at different rates.