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

Cross a clock domain

When two tasks run on different clocks, you cannot wire the producer's output directly to the consumer's input. The destination flip-flop can latch the signal mid-transition and enter a metastable state. The standard library provides two drop-in synchronizers that handle this for you.

This guide walks through the typical two-domain pattern: pick a synchronizer, parameterize its clocks, wire it into your network, and verify the result in the bytecode simulator. You should already be comfortable with tasks and networks, and you should have the runtime and command setup from Install.

Step 1 - Pick the synchronizer

Choose by signal width.

WidthUseSource port typeSink port type
1 bitstd.lib.SynchronizerFFin bool dinout bool dout
N bitsstd.lib.SynchronizerMuxin push unsigned<width> dinout unsigned<width> dout

SynchronizerFF is a shift register of stages flip-flops (default 2), clocked by the destination domain. It is the textbook two-flop synchronizer.

SynchronizerMux carries an N-bit payload. A small handshake signal crosses via an internal SynchronizerFF. The input value must stay stable across the crossing, so it accepts a push input. Don't write a new value until the crossing completes.

Step 2 - Name the two clocks in your network

Declare both clocks at the network level. The names you pick are arbitrary, but they have to match what the synchronizer expects: din_clock for the source side and dout_clock for the destination side.

package com.example;
 
network ClockCrossingExample {
  properties {
    clocks: ["clk_fast", "clk_slow"]
  }
  // …
}

Inside this network, clk_fast is the source domain and clk_slow is the destination. Each instantiated sub-entity will pair its own clocks against these positionally.

Step 3 - Instantiate the producer, the synchronizer, and the consumer

Wire the producer (on clk_fast) through a SynchronizerMux into the consumer (on clk_slow). Pair clocks explicitly when the ordering is not obvious. The example below uses the same .reads(...) wiring style described in Networks.

package com.example;
 
network ClockCrossingExample {
  import com.example.Producer;
  import com.example.Consumer;
  import std.lib.SynchronizerMux;
 
  properties {
    clocks: ["clk_fast", "clk_slow"]
  }
 
  producer = new Producer({ clock: "clk_fast" });
 
  sync = new SynchronizerMux({
    clocks: { din_clock: "clk_fast", dout_clock: "clk_slow" },
    width: 16
  });
 
  consumer = new Consumer({ clock: "clk_slow" });
 
  sync.reads(producer.data);
  consumer.reads(sync.dout);
}

Things to notice. producer.data is a push unsigned<16> output; SynchronizerMux.din matches. SynchronizerMux.dout is a plain (non-push) unsigned<16> in the destination domain, so the consumer reads it as a bare port. width: 16 parameterizes the synchronizer to match the payload.

For a 1-bit signal, swap SynchronizerMux for SynchronizerFF:

sync_done = new SynchronizerFF({
  clocks: { din_clock: "clk_fast", dout_clock: "clk_slow" }
});
 
sync_done.reads(producer.done);
consumer.reads(sync_done.dout);

SynchronizerFF has no width parameter (it is 1 bit by definition) but accepts stages if you need a deeper sync chain for a high-frequency design.

Step 4 - Verify in the bytecode simulator

The simulator schedules each task on its own logical clock, so a cross-domain hand-off is observable in the trace. Add a monitor on the destination side and assert that values arrive in order.

monitor = new task {
  properties { clock: "clk_slow" }
  bool finished;
 
  void setup() {
    for (u8 expected = 0; expected < 4; expected++) {
      u16 got = consumer.observed.read();
      print("slow-side got ", got);
      assert(got == expected);
    }
    finished = true;
  }
};

Run Fast Sim. If the assertions hold and the generated VCD shows the destination-clock signal lagging the source by the expected number of destination-clock ticks, the synchronizer is doing its job.

Why direct wiring is unsafe

A flip-flop's setup/hold window is small but non-zero. If the input transitions inside that window, the output can hover between 0 and 1 for a brief, unpredictable interval. Downstream logic can then sample two different views of the same bit, leading to glitches that are hard to reproduce in simulation but easy to hit in silicon. The synchronizers above shape the signal so the destination domain sees only stable transitions.

Where next