Clocks, edges and sampling

Say when a value on a bus is driven and when it is read, keep every change away from the sampling edge, read an active-low signal correctly, and know what the bus may carry during reset.

Lesson
P1.2
Time
25 min
Runs on
Your browser
Check
5 questions

A scoreboard reported a duplicated beat and a missing one, and the bus looked clean in the waveform. The monitor sampled at the falling edge "to be safe". The stimulus changed data at the falling edge too, a habit from another project. Whenever both acted at the same edge, the monitor logged whichever value happened to be there first, and the target, sampling at the rising edge, saw something else.

One bus, two observers, two logsThe target samples at the rising edge. The monitor samples at the falling edge, where the driver also changes the value.
dataseenSourcechanges data mid-cycleTargetsamples at the rising edgeMonitorsamples at the falling edge

The upper pill is what the target takes; the lower pill is what the monitor logs.

01234clkvaliddatax5a3cffxmonitorx5affx
  1. Cycle 1

    Beat 5a. The target takes it at the rising edge, the monitor logs it at the falling edge. They agree.

  2. Cycle 2

    Beat 3c. The driver and the monitor both act at the falling edge. This time the monitor ran first and logged 5a again.

  3. Cycle 3

    Beat ff. Now the monitor ran second and saw the new value. One log, two orders, and the bus itself was never wrong.

Which process runs first at a shared edge is not fixed by the standard. The cure is a rule about edges, not a faster monitor.

Driven after the edge, sampled at the next

A synchronous bus shares one clock, and every signal on it follows one rule: it is changed just after a rising edge and read at the next one. In between, the value has the whole period to travel and settle. On a 100 MHz clock that is 10 ns, minus the small setup time a flip-flop needs before the edge.

A value takes one clock period to arriveThe source drives after each rising edge. The target's register q shows the value from the next cycle on.
validdataqSourcedrives after the edgeTargetregister q, loads at the edge

The pill is the value in flight during the cycle; q is what the target holds after the edge.

01234clkvaliddatax5a3cxqx5a3c
  1. Cycle 1

    Just after edge 1 the source drives valid with 5a. Nothing reads it until the edge that ends this cycle.

  2. Cycle 2

    That edge loaded 5a into q. The source already drives the next value, 3c, for the next edge.

  3. Cycle 3

    q now holds 3c. Every value arrives one period after it was driven, by design and on every synchronous bus.

In a testbench the same rule reads: drive one time unit after the rising edge, or from a clocked block with nonblocking assignments.

The one-period lag is not a cost to be removed. It is the margin that lets a value cross the chip and settle before anyone reads it. Every rule in this module counts in edges because of it.

Stable before the edge

Silicon needs a value to be stable for the setup time before the edge and the hold time after it. Simulation has no picoseconds, so the rule shrinks to one line: never change a signal at the edge that samples it. A change at the sampling edge gives old or new, and which one depends on the order the simulator chose for its processes.

That is what the scoreboard in the opening story met. The driver and the monitor shared the falling edge, so the monitor read old or new at random. The fix was not a cleverer monitor. The driver moved to one time unit after the rising edge, and the monitor moved to the rising edge, where the target reads.

Driver and monitor on the same edge

// driver: changes data at the falling edge
always @(negedge clk) data = next;

// monitor: reads data at the falling edge
always @(negedge clk) log(data);   // old or new

Two processes act at one edge. The log depends on their order.

Driver after the edge, monitor at the edge

// driver: changes data just after the rising edge
always @(posedge clk) #1 data = next;

// monitor: reads where the target reads
always @(posedge clk) log(data);   // the value the target took

The monitor sees exactly what the target sampled.

A monitor samples where the design samples. Half a cycle early can work while every source behaves, as the lab below shows, and it stops working the day one source changes a value mid-cycle.

What asserted means

A signal is asserted when it is at its active level, whatever that level is. Most bus signals are active-high, so asserted means 1. Resets, chip selects and interrupt lines are often active-low, marked with a trailing n or _n. For them, asserted means 0. "Reset is asserted" and "rst_n is 0" say the same thing.

SignalActive levelAsserted meansDeasserted means
valid, readyHigh10
rst_nLow0: reset is active1: the block runs
cs_n (chip select)Low0: this chip is addressed1: ignore the bus
irq_n (interrupt)Low0: an interrupt is pending1: nothing pending

Read the suffix before the level. if (rst_n) means "when reset is released".

Nothing on the bus is valid during reset

Reset is the one asynchronous signal on a synchronous bus. It is asserted without waiting for a clock, so a block can be forced quiet even if its clock has stopped. It is released at an edge, so every block leaves reset in the same cycle. While it is asserted, every valid on the bus is 0 and every FIFO is empty. The first offer may rise only after an edge that sampled reset as released.

Reset released, then the first offerrst_n is active-low: 0 is asserted. The first valid waits one full cycle after the release.
validdatareadySourcevalid is 0 in resetSinkFIFO emptied by reset

A ringed block is in reset. The first pill flies one cycle after the release.

012345clkrst_nvalidreadydatax5ax
  1. Cycle 0

    rst_n is 0: reset is asserted. Both blocks are held, valid is 0, the FIFO is empty.

  2. Cycle 2

    Reset was released after edge 1. The edge that ends this cycle is the first to sample it released, so nothing may be offered yet.

  3. Cycle 3

    The first offer, a full cycle after the release. ready is already 1, so the beat moves at the end of this cycle.

  4. Cycle 4

    A second beat in flight. The sink holds 5a; the rule cost one cycle and no block ever saw a half-reset bus.

The bench checker names this rule reset when valid is 1 while rst_n is 0.

A block that raises valid during reset breaks more than one rule. The sink may not be running yet, so the beat is lost, and the checker reports a transfer that never happened.

Where you will meet it

AXI samples every signal on the rising edge of its clock and its reset is active-low, released at a rising edge. APB and AHB share that clock rule. Avalon and Wishbone name the active-low reset differently and keep the same meaning. A testbench that drives one time unit after the rising edge, and samples at it, works with all of them.

Asked in interviews: clocks and sampling
  1. Why are buses synchronous?

    One clock gives every block the same moment to read, so a value only has to be stable at the edge. Without it each signal would need its own handshake just to be read safely.

  2. What happens to a signal that changes at the sampling edge?

    The reader gets the old value or the new one, and the standard does not say which. Change signals after the edge, and sample them at it.

  3. What does asserted mean for an active-low signal?

    It is at its active level, which is 0. "rst_n is asserted" means reset is in progress; "deasserted" means the block runs.

  4. Why is reset asserted asynchronously but released synchronously?

    Asserting without a clock forces a block quiet even when its clock has stopped. Releasing at an edge lets every block start in the same cycle.

  5. At which edge should a monitor sample?

    At the edge the design samples, normally the rising edge. Any other edge only matches while every source happens to behave.

Try it

The monitor watches the rising edge, like the sink, and its log names the same edges as the tasks. Run it, then set FALLING(1) and run again: the monitor now reports each beat half a cycle before the target takes it, and still counts three.

The logs agree only because the tasks drive one time unit after the rising edge. Replace the third send with @(negedge clk) valid = 1; data = 8'hc3; and the falling-edge monitor shares an edge with the driver: its log now depends on process order.

RUNIcarus Verilog ยท in your browser
Edit the code, then run it
Expected: monitor (rising edge): beat a1 at edge 3 edge 3: beat a1 taken monitor (rising edge): beat b2 at edge 4 edge 4: beat b2 taken monitor (rising edge): beat c3 at edge 8 edge 8: beat c3 taken monitor counted 3 beats, the checker counted 3 transfers checker: 3 transfers, 3 stalled edges, 0 rules broken
Read included file: link_sink.sv
// link_sink.sv: the target of the generic link (read-only in this lesson).
// A DEPTH-entry FIFO behind valid/ready. It takes a beat at every rising edge where valid and
// ready are both 1, and hands beats on to its consumer at every edge where drain and out_valid
// are both 1. ready is 1 while the FIFO has room: it never waits for valid.
module link_sink #(parameter int DEPTH = 4, parameter int WIDTH = 8) (
  input  logic             clk,
  input  logic             rst_n,
  input  logic             valid,
  input  logic [WIDTH-1:0] data,
  input  logic             last,
  output logic             ready,
  input  logic             drain,       // the consumer behind the FIFO takes a beat while 1
  output logic             out_valid,
  output logic [WIDTH-1:0] out_data,
  output logic             out_last,
  output int               count        // beats held right now
);
  logic [WIDTH:0] mem [DEPTH];          // {last, data}
  int wr = 0, rd = 0;
  assign ready     = (count < DEPTH);
  assign out_valid = (count > 0);
  assign {out_last, out_data} = mem[rd];
  wire push = valid && ready;
  wire pop  = drain && out_valid;
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr <= 0; rd <= 0; count <= 0;
    end else begin
      if (push) begin mem[wr] <= {last, data}; wr <= (wr + 1) % DEPTH; end
      if (pop) rd <= (rd + 1) % DEPTH;
      count <= count + (push ? 1 : 0) - (pop ? 1 : 0);
    end
  end
endmodule
Read included file: link_checker.sv
// link_checker.sv: the rules of the generic link, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
//   reset   valid is 0 while reset is active;
//   hold    once valid is 1 and ready is 0, valid is still 1 at the next edge (an offer is not withdrawn);
//   stable  while an offer waits, data and last do not change;
//   known   data is not x while valid is 1.
// The one rule no checker can see on the wires, "valid must not wait for ready", is the stimulus's
// own responsibility (lesson P1.3).
// It also counts transfers and stalled edges, and prints one summary line at the end of the run.
module link_checker #(parameter int WIDTH = 8) (
  input logic             clk,
  input logic             rst_n,
  input logic             valid,
  input logic             ready,
  input logic [WIDTH-1:0] data,
  input logic             last
);
  int cycle = -1, transfers = 0, stalls = 0, broken = 0;
  logic             pv = 0, pr = 0, pl = 0;
  logic [WIDTH-1:0] pd = 'x;
  always @(posedge clk) begin
    cycle++;
    if (!rst_n) begin
      if (valid === 1) report("reset", "valid is 1 during reset");
    end else begin
      if (pv && !pr && valid !== 1) report("hold", "valid dropped before ready: the offer was withdrawn");
      if (pv && !pr && valid === 1 && data !== pd) report("stable", "data changed while the offer waited");
      if (pv && !pr && valid === 1 && last !== pl) report("stable", "last changed while the offer waited");
      if (valid === 1 && $isunknown(data)) report("known", "data is x while valid is 1");
      if (valid === 1 && ready === 1) transfers++;
      if (valid === 1 && ready !== 1) stalls++;
    end
    pv = (valid === 1); pr = (ready === 1); pd = data; pl = last;
  end
  function void report(input string rule, input string what);
    broken++;
    $display("RULE %s broken at edge %0d: %s", rule, cycle, what);
  endfunction
  final $display("checker: %0d transfers, %0d stalled edges, %0d rules broken", transfers, stalls, broken);
endmodule
Read included file: link_tasks.svh
// link_tasks.svh: the source side of the generic link, as tasks for the stimulus (read-only).
// Every task returns one time unit after the rising edge where its last beat transferred, so the
// next call starts driving in the following cycle. `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge clk) cycle = cycle + 1;

// Offer one beat and hold it until the sink takes it.
task automatic send(input logic [7:0] d, input logic is_last = 0);
  valid = 1; data = d; last = is_last;
  do @(posedge clk); while (!ready);
  #1 $display("edge %0d: beat %02h taken", cycle, d);
  valid = 0; data = 'x; last = 0;
endtask

// A burst of n beats with consecutive values; last marks the final beat.
task automatic send_burst(input logic [7:0] first, input int n);
  for (int i = 0; i < n; i++) begin
    valid = 1; data = first + i[7:0]; last = (i == n - 1);
    do @(posedge clk); while (!ready);
    #1 $display("edge %0d: beat %02h taken%s", cycle, first + i[7:0], last ? " (last)" : "");
  end
  valid = 0; data = 'x; last = 0;
endtask

// Drive nothing for n cycles.
task automatic idle(input int n);
  repeat (n) @(posedge clk);
  #1;
endtask
Read included file: edge_monitor.sv
`timescale 1ns/1ps
// edge_monitor.sv: a monitor that counts beats at the edge of your choice (read-only).
//   FALLING = 0 samples at the rising edge, where the sink samples.
//   FALLING = 1 samples at the falling edge, half a cycle earlier.
// Edge numbers follow the tasks: rising edge k is at 5 + 10k ns, the falling edge after it at 10 + 10k.
module edge_monitor #(parameter bit FALLING = 0) (
  input logic       clk,
  input logic       rst_n,
  input logic       valid,
  input logic       ready,
  input logic [7:0] data
);
  int beats = 0;
  generate
    if (FALLING) begin : fall
      always @(negedge clk) if (rst_n && valid && ready) begin
        beats++;
        $display("monitor (falling edge): beat %02h between edges %0d and %0d", data, ($time - 10) / 10, ($time - 10) / 10 + 1);
      end
    end else begin : rise
      always @(posedge clk) if (rst_n && valid && ready) begin
        beats++;
        $display("monitor (rising edge): beat %02h at edge %0d", data, ($time - 5) / 10);
      end
    end
  endgenerate
endmodule

Check yourself

Predict the outputThe target captures d at every rising edge. The source drives 5a after edge 0 and 3c after edge 1, then prints. What line appears?
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
  logic [7:0] d = 8'h00, q;
  always_ff @(posedge clk) q <= d;      // the target captures d at every rising edge
  initial begin
    @(posedge clk); #1 d = 8'h5a;       // driven after edge 0
    @(posedge clk); #1 d = 8'h3c;       // driven after edge 1
    $display("q=%02h d=%02h", q, d);
    $finish;
  end
endmodule
Conceptcs_n is a chip select. What does "cs_n is asserted" tell you?
Read the waveformThe target captures data at the rising edge that ends each cycle. Which value does its register hold during cycle 2?
Two beats driven after the edge
0123clkvaliddatax5a3cx

One value per cycle; the edge at the end of a cycle samples that cycle's values.

ConceptWhy does a synchronous bus read every signal at one clock edge instead of whenever it changes?
Spot the bugEach option counts beats on the link inside the testbench. The second beat stalls two edges before it is taken. Which counter disagrees with the checker?
Read included file: link_sink.sv
// link_sink.sv: the target of the generic link (read-only in this lesson).
// A DEPTH-entry FIFO behind valid/ready. It takes a beat at every rising edge where valid and
// ready are both 1, and hands beats on to its consumer at every edge where drain and out_valid
// are both 1. ready is 1 while the FIFO has room: it never waits for valid.
module link_sink #(parameter int DEPTH = 4, parameter int WIDTH = 8) (
  input  logic             clk,
  input  logic             rst_n,
  input  logic             valid,
  input  logic [WIDTH-1:0] data,
  input  logic             last,
  output logic             ready,
  input  logic             drain,       // the consumer behind the FIFO takes a beat while 1
  output logic             out_valid,
  output logic [WIDTH-1:0] out_data,
  output logic             out_last,
  output int               count        // beats held right now
);
  logic [WIDTH:0] mem [DEPTH];          // {last, data}
  int wr = 0, rd = 0;
  assign ready     = (count < DEPTH);
  assign out_valid = (count > 0);
  assign {out_last, out_data} = mem[rd];
  wire push = valid && ready;
  wire pop  = drain && out_valid;
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr <= 0; rd <= 0; count <= 0;
    end else begin
      if (push) begin mem[wr] <= {last, data}; wr <= (wr + 1) % DEPTH; end
      if (pop) rd <= (rd + 1) % DEPTH;
      count <= count + (push ? 1 : 0) - (pop ? 1 : 0);
    end
  end
endmodule
Read included file: link_checker.sv
// link_checker.sv: the rules of the generic link, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
//   reset   valid is 0 while reset is active;
//   hold    once valid is 1 and ready is 0, valid is still 1 at the next edge (an offer is not withdrawn);
//   stable  while an offer waits, data and last do not change;
//   known   data is not x while valid is 1.
// The one rule no checker can see on the wires, "valid must not wait for ready", is the stimulus's
// own responsibility (lesson P1.3).
// It also counts transfers and stalled edges, and prints one summary line at the end of the run.
module link_checker #(parameter int WIDTH = 8) (
  input logic             clk,
  input logic             rst_n,
  input logic             valid,
  input logic             ready,
  input logic [WIDTH-1:0] data,
  input logic             last
);
  int cycle = -1, transfers = 0, stalls = 0, broken = 0;
  logic             pv = 0, pr = 0, pl = 0;
  logic [WIDTH-1:0] pd = 'x;
  always @(posedge clk) begin
    cycle++;
    if (!rst_n) begin
      if (valid === 1) report("reset", "valid is 1 during reset");
    end else begin
      if (pv && !pr && valid !== 1) report("hold", "valid dropped before ready: the offer was withdrawn");
      if (pv && !pr && valid === 1 && data !== pd) report("stable", "data changed while the offer waited");
      if (pv && !pr && valid === 1 && last !== pl) report("stable", "last changed while the offer waited");
      if (valid === 1 && $isunknown(data)) report("known", "data is x while valid is 1");
      if (valid === 1 && ready === 1) transfers++;
      if (valid === 1 && ready !== 1) stalls++;
    end
    pv = (valid === 1); pr = (ready === 1); pd = data; pl = last;
  end
  function void report(input string rule, input string what);
    broken++;
    $display("RULE %s broken at edge %0d: %s", rule, cycle, what);
  endfunction
  final $display("checker: %0d transfers, %0d stalled edges, %0d rules broken", transfers, stalls, broken);
endmodule
Read included file: link_tasks.svh
// link_tasks.svh: the source side of the generic link, as tasks for the stimulus (read-only).
// Every task returns one time unit after the rising edge where its last beat transferred, so the
// next call starts driving in the following cycle. `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge clk) cycle = cycle + 1;

// Offer one beat and hold it until the sink takes it.
task automatic send(input logic [7:0] d, input logic is_last = 0);
  valid = 1; data = d; last = is_last;
  do @(posedge clk); while (!ready);
  #1 $display("edge %0d: beat %02h taken", cycle, d);
  valid = 0; data = 'x; last = 0;
endtask

// A burst of n beats with consecutive values; last marks the final beat.
task automatic send_burst(input logic [7:0] first, input int n);
  for (int i = 0; i < n; i++) begin
    valid = 1; data = first + i[7:0]; last = (i == n - 1);
    do @(posedge clk); while (!ready);
    #1 $display("edge %0d: beat %02h taken%s", cycle, first + i[7:0], last ? " (last)" : "");
  end
  valid = 0; data = 'x; last = 0;
endtask

// Drive nothing for n cycles.
task automatic idle(input int n);
  repeat (n) @(posedge clk);
  #1;
endtask

Remember

  • On a synchronous bus a value is driven just after a rising edge and read at the next one. It arrives one period later, by design.
  • Never change a signal at the edge that samples it. A shared edge gives old or new, and the standard does not say which.
  • A monitor samples where the design samples. Sampling elsewhere matches only while every source behaves.
  • Asserted means at the active level: 1 for most signals, 0 for an active-low signal such as rst_n or cs_n.
  • Reset is asserted asynchronously and released at an edge. Every valid is 0 while it is asserted, and the first offer waits one cycle after the release.