- You Cannot Detect a Clock Edge with Another Clock
- Fix Attempts
- Two signals carrying the same information
- Startup Alignment:
counter[1]vsclk25
50.002 Computation Structures
Information Systems Technology and Design
Singapore University of Technology and Design
You Cannot Detect a Clock Edge with Another Clock
The Problem
Suppose you have two clocks from the same MMCM at 0-degree phase: clk100 (100 MHz) and clk25 (25 MHz), both with 50% duty cycle. A natural instinct is to detect the rising or falling edge of clk25 inside the clk100 domain using the standard edge detector pattern:
reg clk25_d;
always @(posedge clk100) begin
clk25_d <= clk25;
end
wire clk25_rising = clk25 & ~clk25_d;
wire clk25_falling = ~clk25 & clk25_d;
This does not work. It is structurally impossible to make this reliable.
Why It Fails
When the Clocking Wizard (MMCM) generates multiple output clocks, all outputs are derived from the same PLL and launched from the same internal reference. With all outputs configured at 0-degree phase (the default in Vivado’s Clocking Wizard), the edges of every output clock are aligned to the same reference instant.
For clk100 (10 ns period) and clk25 (40 ns period), the alignment looks like this:
| Time | clk100 |
clk25 |
|---|---|---|
| 0 ns | posedge | posedge |
| 10 ns | posedge | |
| 20 ns | posedge | negedge |
| 30 ns | posedge | |
| 40 ns | posedge | posedge |
Every transition of clk25 lands on a posedge clk100. Two out of every four clk100 posedges coincide with a clk25 transition, and those are exactly the edges you care about detecting. The flip-flop capturing clk25_d <= clk25 is trying to sample a signal that is changing at that exact nominal instant.
A flip-flop requires data to be stable for some time before the clock edge (setup time) and after the clock edge (hold time). Here, the MMCM launches both transitions from the same PLL at the same instant. After leaving the MMCM:
clk100travels the clock tree (BUFG network) to the flip-flop’s CLK pinclk25travels the data routing fabric to the flip-flop’s D pin
These two paths have different, uncontrolled propagation delays. Depending on which signal arrives at the flip-flop first, you violate either setup or hold. The outcome can vary across builds, across different locations on the die, and even with temperature. The flip-flop may go metastable.
The phase relationship guarantees that every sample you actually want to detect is taken at a data transition. There is no stable sampling point.
Fix Attempts
Attempt 1: Fix with Phase Offset
Vivado’s Clocking Wizard lets you set a phase offset (in degrees) on each output clock independently. For example, setting clk25 to 180 degrees shifts all its edges by half of its own period (20 ns), so that clk25 transitions now land on clk100 negedges instead of posedges. This would give you 5 ns of margin when sampling clk25 on posedge clk100.
This works to a limited extent, but has practical drawbacks:
- The correctness of your entire shared RAM design depends on a single IP configuration field buried in the Clocking Wizard GUI. Anyone regenerating or reconfiguring the IP can silently break the design.
- If you change frequencies later (say, moving to
clk50instead ofclk100), you need to recalculate and update the phase offset to maintain the correct relationship. - The phase offset is specified in degrees of the output clock’s own period, not in nanoseconds. 180 degrees on a 25 MHz output means 20 ns of shift, but 180 degrees on a 50 MHz output means 10 ns. This can be confusing when multiple outputs are involved.
Attempt 2: Fix with a Counter Running on the Fast Clock
Instead of trying to observe clk25 as a data signal, generate the same slot information entirely within the clk100 domain. Just run a free-running counter on clk100 and tap the appropriate bit:
reg [1:0] counter;
always @(posedge clk100) begin
if (rst)
counter <= 2'b0;
else
counter <= counter + 1;
end
counter increments every clk100 cycle: 0, 1, 2, 3, 0, 1, 2, 3, …
Each bit of this counter is a clock divider like we learned in our labs:
| Bit | Frequency | Period | What it tells you |
|---|---|---|---|
counter[0] |
50 MHz | 20 ns | Which half of a 50 MHz cycle you are in |
counter[1] |
25 MHz | 40 ns | Which half of a 25 MHz cycle you are in |
To know whether you are in the CPU slot or GPU slot, just look at counter[1]. It carries the same information as clk25: it alternates between 0 and 1 at 25 MHz. But unlike clk25, it is a registered output in the clk100 domain. It transitions at posedge clk100 with a small clk-to-q delay, and by the next posedge clk100 it has been stable for nearly 10 ns. Sampling it is textbook single-clock-domain logic:
// Mux select for shared RAM read port
assign ram_raddr = (counter[1] == 1'b1) ? cpu_addr : gpu_addr;
// GPU cache enable, one half-period delayed
always @(posedge clk100) begin
en_gpu <= ~counter[1];
end
Two signals carrying the same information
It is perfectly fine (and standard practice) to have both clk25 from the MMCM and counter[1] from fabric in the same design. They represent the same information but serve different physical roles:
| Signal | Source | Travels on | Used as | Drives |
|---|---|---|---|---|
clk25 |
MMCM output | Clock tree (BUFG) | Clock | VGA driver, CPU PC, regfile CLK pins |
counter[1] |
clk100 FF |
Data routing fabric | Data | Mux select, en_gpu, any clk100 control logic |
clk25 should only be used as a clock (connected to CLK pins of flip-flops). counter[1] should only be used as data (connected to D pins, LUT inputs, mux selects).
You should NEVER use counter[1] as clk input port for other modules in your design. Use proper clk25 for that.
Startup Alignment: counter[1] vs clk25
Background
After reset, two signals in the design both toggle at 25 MHz:
counter[1]is derived fromclk100by counting. For example, it can control which device owns a shared RAM read port:1= CPU slot,0= GPU slot.clk25comes out of the MMCM (the clock wizard). It drives the VGA pixel logic and the CPU pipeline.
Both toggle at the same rate, but you might wonder if after reset, they might start with opposite polarity. Is there any guarantee that counter[1] goes high at the same moment clk25 goes high?
Matching Polarity by Design (Phase) at Startup
Since clk100 and clk25 both come from the same MMCM at 0 degrees of phase, clk25 completes exactly one full cycle every 4 clk100 posedges. counter[1] also completes one full cycle every 4 clk100 posedges. Both are locked to the same clock source, so their relationship is fixed and deterministic.
There will be a one clk100 cycle offset though, because the counter is a register, but this shouldn’t matter much and the 1-100Mhz cycle latency is expected:
clk100 posedge: 1 2 3 4 5 6 7 8
counter: 01 10 11 00 01 10 11 00
counter[1]: 0 1 1 0 0 1 1 0
clk25: 1 1 0 0 1 1 0 0
counter[1] is clk25 delayed by one clk100 cycle. This offset is always the same, never varies between power cycles, because the MMCM enforces the phase relationship and the counter always starts at 0.
So if you use counter == 2'b00 to detect “clk25 just fell,” you are actually detecting it 10ns after the actual falling edge. Whether that 10ns matters depends on your design. But the key point is: it is a fixed, predictable delay, not a random polarity mismatch. every 4 clk100 posedges. counter[1] also completes one full cycle every 4 clk100 posedges. Both are locked to the same clock source, so their relationship is fixed and deterministic.
There will be a one clk100 cycle offset though, because the counter is a register:
clk100 posedge: 1 2 3 4 5 6 7 8
counter: 01 10 11 00 01 10 11 00
counter[1]: 0 1 1 0 0 1 1 0
clk25: 1 1 0 0 1 1 0 0
counter[1] is clk25 delayed by one clk100 cycle. This offset is always the same, never varies between power cycles, because the MMCM enforces the phase relationship and the counter always starts at 0.
So if you use counter == 2'b00 to detect “clk25 just fell,” you are actually detecting it 10ns after the actual falling edge. Whether that 10ns matters depends on your design. But the key point is: it is a fixed, predictable delay, not a random polarity mismatch.
Do not reset the counter
The counter always increments by 1 on every clk100 posedge. But the issue is: when reset releases, the counter goes to 00, while clk25 is at some arbitrary point in its cycle because the MMCM never stopped.
These are the two cases:
Reset releases when clk25 happens to be falling:
| clk100 posedge | counter | counter[1] | clk25 | match? |
|---|---|---|---|---|
| reset releases | 00 | 0 | falls to 0 | yes |
| +1 | 01 | 0 | 0 | yes |
| +2 | 10 | 1 | rises to 1 | yes |
| +3 | 11 | 1 | 1 | yes |
Reset releases when clk25 happens to be rising:
| clk100 posedge | counter | counter[1] | clk25 | match? |
|---|---|---|---|---|
| reset releases | 00 | 0 | rises to 1 | no |
| +1 | 01 | 0 | 1 | no |
| +2 | 10 | 1 | falls to 0 | no |
| +3 | 11 | 1 | 0 | no |
The counter always counts correctly. But clk25 did not reset with it because the MMCM kept running. So depending on when your reset happens to release, you get same polarity or opposite polarity.
At initial power-on it works (same polarity) because both start together. A mid-operation reset only resets the counter, not the MMCM. Due to this, never reset your reference clk25 counter.
50.002 CS