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.
| Width | Use | Source port type | Sink port type |
|---|---|---|---|
| 1 bit | std.lib.SynchronizerFF | in bool din | out bool dout |
| N bits | std.lib.SynchronizerMux | in push unsigned<width> din | out 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
- Standard library - synchronizers for the full parameter list.
- Properties - clocks for clock pairing rules (positional vs. named).
- Networks for the wiring syntax in depth.