Digital Design / Module 5: Counters and frequency dividers
31. Edge detectors and pulse generators
Turn levels into single-cycle events.
See it move
The idea
Many blocks need to act once when a signal changes, not for as long as it stays high. An edge detector converts a level into a one-cycle pulse.
How it works
Keep a registered copy of the signal, which shows its value one cycle ago. Comparing past and present reveals the change.
always_ff @(posedge clk)
s_d <= s;
assign rise = s & ~s_d; // now 1, was 0
assign fall = ~s & s_d; // now 0, was 1
assign any = s ^ s_d; // changedRegistered or combinational output
Those outputs are combinational, so they pulse in the same cycle as the change. A registered output instead pulses one cycle later, but it is -free and has cleaner timing into the next block. Both are valid. Know which one your downstream logic expects, because this is a frequent off-by-one source.
Pulse stretching
Sometimes a pulse needs to last longer, perhaps so a slower block can see it. A can OR several delayed copies, or a small counter can hold the output high for N cycles.
Inputs from elsewhere
If s comes from another or a push button, synchronize it first. Otherwise s might change right near the edge, and because the edge logic looks at both s and s_d, different parts of the logic may see different values. A mechanical button also bounces, so it needs debouncing with a timer as well.
always_ff @(posedge clk) begin
sync1 <= async_in;
sync2 <= sync1;
sync3 <= sync2;
end
assign rise = sync2 & ~sync3;Common mistake
Using an asynchronous input straight in the edge detector. The result often works on the bench, then fails occasionally in the field, which makes this among the hardest bugs to find.
Key ideas
- Rising edge = signal AND NOT its registered copy.
- Output timing depends on which registered stage you compare.
- Inputs from another domain must be synchronized first.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
rise should be a one-cycle pulse. It stays high for as long as s is high, apart from the first cycle.The fix
assign rise = s & ~s_d;
Why it failed
s & s_d means "high now, and also high a cycle ago", which is a level that starts one cycle late. Inverting the registered copy detects the change itself, which is true for exactly one cycle.
Interview questions
coreDetect any edge?
XOR the signal with a registered copy of itself.
basicHow do you turn a level signal s into a one-cycle pulse when s rises?
Register s to get s_d, its value one clock ago. The rising edge is s & ~s_d: high in the one cycle where s is 1 now and was 0 before. After that cycle both are 1, so the pulse ends.
basicUsing s and its registered copy s_d, write the falling-edge and any-edge detectors.
Falling edge is ~s & s_d: now 0, one cycle ago 1. Any edge is s ^ s_d, which is 1 whenever the present and past values differ. Each gives a one-cycle pulse per change.
coreThis rising-edge detector was written without the NOT. If s is high for 5 cycles, what does rise do?
always @(posedge clk) s_d <= s; assign rise = s & s_d;
rise is high whenever both s and its delayed copy are 1. That is from the second cycle of the level to the end of it, so it is high for 4 cycles instead of pulsing once. It is simply the level, shortened by its first cycle, and everything downstream would act several times.
coreThis detector registers its output. How does its timing compare with the combinational version, and why might you choose it?
always @(posedge clk) begin s_d <= s; rise_r <= s & ~s_d; end
The registered pulse appears one cycle later than s & ~s_d computed combinationally. It comes straight from a flop, so it is glitch-free and gives the next block a full cycle of timing. Both are valid, but the downstream logic must expect the right cycle, as this is a common off-by-one source.
coreAn edge detector on an asynchronous input mostly works but occasionally counts one press twice or misses one. Why, and why does the fix compare sync2 with sync3?
always @(posedge clk) begin sync1 <= async_in; sync2 <= sync1; sync3 <= sync2; end assign rise = sync2 & ~sync3;
The input can change right at the clock edge, so the logic that uses s and the flop that makes s_d can see different values, and a flop can go metastable. The first two flops synchronise the input, and the edge detector then compares two stable, synchronised values, sync2 and its delayed copy sync3. Comparing the raw input or sync1 would bring the problem back.
coreA push button bounces for up to 5 ms. With a 50 MHz clock, how many cycles must the debounce timer count, and how many bits does it need?
5 ms x 50 MHz = 250,000 cycles. Counting 0 to 249,999 needs 18 bits, because 2^17 is 131,072, which is too small, and 2^18 is 262,144. The input is first synchronised, then accepted only after it has stayed at the new value for the whole count.
advancedA signal from another, faster domain produces a pulse that is high for less than one period of your clock. Will a synchroniser and edge detector on your side see it?
Not reliably. Your clock may never sample the pulse while it is high, so it can vanish completely, and a synchroniser cannot recover what it never sampled. The source domain must make the event long enough to be sampled, for example by toggling a level for each event and detecting any edge of that level on the receiving side, or by using a handshake.
advancedThis circuit widens a one-cycle pulse p. How long is wide, and what happens if two pulses arrive two cycles apart?
always @(posedge clk)
sr <= {sr[1:0], p};
assign wide = p | (|sr);wide is p ORed with its three delayed copies, so a single one-cycle pulse gives a 4-cycle output. Two pulses two cycles apart produce overlapping windows, so they merge into one longer pulse of 6 cycles and the receiver sees one event instead of two. If the count of events matters, the source must space them out or the receiver must use a different scheme.