C⏚ v2.0.0Updated 2026-05-12·Platform

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.

EntityPackagePurpose
SynchronousFIFOstd.fifoFIFO buffer, one clock domain
AsynchronousFIFOstd.fifoFIFO buffer, two clock domains (CDC)
SynchronizerFFstd.lib1-bit clock-domain crossing
SynchronizerMuxstd.libN-bit clock-domain crossing
SinglePortRAMstd.memOne address port, read or write
DualPortRAMstd.memTwo address ports, independent reads and writes
PseudoDualPortRAMstd.memOne 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;
  }
}
ParameterRequiredMeaning
sizeyesCapacity in elements
widthyesData 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;
  }
}
ParameterRequiredMeaning
sizeyesCapacity in elements
widthyesData 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;
}
ParameterDefaultMeaning
width16Data width in bits
stages2Synchronizer FF depth (see SynchronizerFF)

How it works:

  1. A producer writes a new value to din (a push port).
  2. A 1-bit sync signal crosses from the input domain to the output domain via the internal SynchronizerFF.
  3. The input value must stay stable during the crossing - no new write to din until the crossing completes.
  4. 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;
}
ParameterRequiredDefaultMeaning
sizeyes-Element count
widthyes-Element width in bits
writeShiftModenofalsefalse: write-through (new value on q). true: read-before-write (previous value on q).
addOutputRegisternofalseAdds an output register, delaying q by one cycle. Can help timing on FPGA.

depth is computed from size; you don't set it.

PortDirectionMeaning
addressinAddress to read or write
datain pushWhen valid, writes data at address; otherwise the RAM is in read mode
qoutValue 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;
}
PortDirectionClockMeaning
rd_addressinrd_clockRead address
wr_addressinwr_clockWrite address
datain pushwr_clockWhen valid, writes data at wr_address
qoutrd_clockValue at the previous-cycle rd_address

Reads and writes may run concurrently and at different rates.