Pulse, Timeout and Edge: the bookkeeping you no longer write
Three patterns come up in almost every design: a flag that should be true for exactly one cycle, a countdown, and “did this signal just change?”. Each is a few lines of Verilog that are easy to get slightly wrong. QuartzHDL gives each a register type that does the bookkeeping itself. All three are stored as plain registers; the block that writes one advances it before its own statements run, so the block’s own write always wins the cycle.
Pulse: true for one cycle
A Pulse is a Bool that clears itself. Write true and it is true for one cycle, then false again until the next write:
@quartzstruct Strobe@in go::Bool =false@out fire::Pulseend@on Strobe posedge(clk) begin go && (fire ←true)endm =Strobe()fires =Bool[]for i in1:5global m =step(m; go = i ==2)push!(fires, m.fire)endfires
5-element Vector{Bool}:
0
1
0
0
0
It replaces the fire && (fire ← false) line you would otherwise write at the top of every block, and the bug where you forget it.
Timeout{N}: a countdown
A Timeout{N} is a Bits{N} that counts down to zero and holds there. Write a value to start it; expired(t) is true from the moment it reaches zero. Read bare, it is the count.
@quartzstruct Divider@in period::Bits{4} =3 div::Timeout{4}@out tick::Pulse hold::Timeout{4}@in arm::Bool =falseend@on Divider posedge(clk) beginifexpired(div) div ← period # reload on expiry: a divider tick ←trueend arm && (hold ←5) # a one-shot: runs down and stays expiredendm =Divider()for i in1:10global m =step(m; arm = i ==5)println("edge $i: div = $(Int(m.div)) tick = $(m.tick) hold = $(Int(m.hold))")end
edge 1: div = 3 tick = true hold = 0
edge 2: div = 2 tick = false hold = 0
edge 3: div = 1 tick = false hold = 0
edge 4: div = 0 tick = false hold = 0
edge 5: div = 3 tick = true hold = 5
edge 6: div = 2 tick = false hold = 4
edge 7: div = 1 tick = false hold = 3
edge 8: div = 0 tick = false hold = 2
edge 9: div = 3 tick = true hold = 1
edge 10: div = 2 tick = false hold = 0
Written 3, the timeout expires four edges later, so a divider written with period has a period of period + 1 clocks. A timeout that has run down stays at zero: expired keeps answering true until it is written again.
TipIf you know Verilog
Pulse and Timeout are ordinary registers in the emitted Verilog — a reg with a clear or a decrement at the top of the always block, followed by your writes. Look at the output of write(stdout, Divider, Verilog()) and you will find exactly the code you would have written by hand.
Edge: did it just change?
An Edge is a Bool register that also remembers the value it held before, so its transitions can be queried. Write it like any register and read it bare as the level:
@quartzstruct Detect@in x::Bool =false xe::Edge@out rises::Bits{8} =0@out falls::Bits{8} =0end@on Detect posedge(clk) begin xe ← xrose(xe) && (rises ← rises +1)fell(xe) && (falls ← falls +1)endm =Detect()for i in1:12 m =step(m; x =4<= i <=8)endm.rises, m.falls
(Bits{8}(01h), Bits{8}(01h))
rose(e) and fell(e) are the transition the last clock edge registered — exactly x_q && !x_qq — glitch-free, one cycle after the sample that made it, and true for exactly one cycle. On a cycle that leaves the edge unwritten the history settles by itself, so a gated feed (en && (e ← x)) cannot latch an event.
isrising(e, x) and isfalling(e, x) compare an incoming sample with the level instead — x && !e — and see the transition as it happens, with no cycle of delay. Keep them to signals already on this clock, and take an asynchronous pin through a MetaGuard first.
The default of an Edge is the level seen so far, so power-up and reset manufacture no event.
What they cannot be
An input cannot be a Pulse, a Timeout or an Edge, since an input has no storage. A @wire block cannot write one, since they advance on a clock. And an Edge is written whole — e[0] ← x is an error.
Next
Clock domains: MetaGuard, and modules with more than one clock.