Clock domains

Synchronisers, and modules with more than one clock

MetaGuard{K}: a synchroniser

A signal that comes from outside the clock domain — a button, a pin from another chip, a flag from a block on another clock — must be resynchronised before the logic looks at it, or metastability will eventually bite. The standard cure is a chain of K flip-flops. A MetaGuard{K} is that chain:

@quartz struct Sampler
  @in  async_in::Bool
  @out safe::Bool
  async_in::MetaGuard{2}
end

@on Sampler posedge(clk) begin
  safe  async_in
end

Declare a guard with the same name as an input, and it is fed by that input automatically, from the block that reads it, on that block’s clock. Read bare, the guard is the synchronised bit — async_in inside the block means the output of the chain, two clocks behind the pin. Nothing else has to be said.

safes = Bool[]
let m = Sampler()
  for i in 1:5
    m = step(m; async_in=i >= 2)
    push!(safes, m.safe)
  end
end
safes
5-element Vector{Bool}:
 0
 0
 0
 1
 1

A guard can also be fed by hand, g ← v, when the bit does not come from an input of the same name. A reset flushes it.

TipIf you know Verilog

The emitted code is the two-flop chain sync0 <= pin; sync1 <= sync0;, with the block reading sync1. The declaration also records, in one place, that the input is asynchronous.

Modules with several clocks

A module may have blocks on different clocks. Each @on names its own:

@quartz struct TwoClocks
  @in  x::Bits{8}
  @out y::Bits{8}
  fast::Bits{8} = 0
  slow::Bits{8} = 0
end

@on TwoClocks posedge(clk) begin
  fast  x
end

@on TwoClocks posedge(clk_slow) begin
  slow  fast
  y  slow
end

Each clock becomes a clock port. step then takes the clock’s name, and one call is one edge of that clock:

m = TwoClocks()
m = step(m, :clk; x=Bits{8}(7))
m = step(m, :clk_slow; x=Bits{8}(7))
m.slow
Bits{8}(0x07)

The keywords of step are the union of every block’s inputs; a block only reads the ones it names. Several blocks on the same clock behave like Verilog’s concurrent always blocks: all read the old state, all write together.

For a design with several clocks, stepping by hand means advancing each clock in the right ratio. A Bench or Simulation takes a clock plan with real rates and advances every clock in the right order, so a slow-clock block sees exactly what it would on the board.

Crossing safely

A single bit crossing domains goes through a MetaGuard. A multi-bit value should not: the bits may arrive on different cycles. The usual pattern is to cross a flag through a guard and hold the data still until the flag has been seen:

@on Producer posedge(clk_a) begin
  if !busy
    data  next            # hold data still...
    flag  !flag           # ...and toggle a flag
  end
end

@on Consumer posedge(clk_b) begin
  flag_e  flag            # flag::MetaGuard{2} feeds flag_e::Edge
  rose(flag_e) && (got  data)
end

A Multicycle wire, in a later chapter, is the tool for a wide value that is known to hold still for a number of cycles: the compiler then writes the timing constraint that tells the vendor tools so.

Next

Pipelines: arithmetic that takes several cycles, cut into stages by the compiler.