Ready-to-run snippets

Code Examples

Ready-to-run Verilog, SystemVerilog, and UVM examples. Click any to open it live in the IDE.

← All examples

I2C bus monitor (find the bug)

Verilog Bus Protocols

A passive I2C monitor decodes the bus into transactions and checks the protocol; the target has a bug.

Open in IDE
design.v
// I2C controller (master) byte engine. It executes one command at a time:
//   START  (also used as a repeated START), STOP,
//   WRITE  sends wdata and returns the target's acknowledge in ack_in (0 = ACK),
//   READ   receives rdata and sends ack_out (0 = ACK, 1 = NACK for the last byte).
// SCL and SDA are open drain: scl_oe / sda_oe = 1 pulls the line low, 0 releases it.
// Each SCL period has four quarters of Q system clocks. While SCL is released the controller
// waits until it really is high, so a target can stretch the clock. While sending a 1 it checks
// SDA: if SDA is low, another controller has won the arbitration and arb_lost is set.
module i2c_master #(
  parameter Q = 5
) (
  input            clk,
  input            rst,
  input      [1:0] cmd,
  input            cmd_valid,
  output           cmd_ready,
  input      [7:0] wdata,
  input            ack_out,
  output reg [7:0] rdata,
  output reg       ack_in,
  output reg       done,               // one-cycle pulse when a command completes
  output reg       arb_lost,
  output reg       scl_oe,
  output reg       sda_oe,
  input            scl_i,
  input            sda_i
);
  localparam START = 2'd0, STOP = 2'd1, WRITE = 2'd2, READ = 2'd3;

  reg        busy;
  reg [1:0]  op;
  reg [3:0]  bitn;                     // bit within a byte: 0-7 data, 8 acknowledge
  reg [1:0]  qtr;                      // quarter of the current SCL period
  reg [15:0] cnt;
  reg [7:0]  sh;

  assign cmd_ready = !busy;

  // The value this controller puts on SDA for the current bit (1 = release)
  wire out_bit = (op == WRITE) ? ((bitn < 8) ? sh[7] : 1'b1) :
                 (op == READ)  ? ((bitn < 8) ? 1'b1 : ack_out) : 1'b1;

  always @(posedge clk) begin
    if (rst) begin
      busy     <= 1'b0;
      done     <= 1'b0;
      arb_lost <= 1'b0;
      scl_oe   <= 1'b0;
      sda_oe   <= 1'b0;
    end
    else begin
      done <= 1'b0;
      if (!busy) begin
        if (cmd_valid) begin
          busy     <= 1'b1;
          op       <= cmd;
          sh       <= wdata;
          bitn     <= 4'd0;
          qtr      <= 2'd0;
          cnt      <= 16'd0;
          arb_lost <= 1'b0;
        end
      end
      else if (qtr == 2'd2 && !scl_i && !scl_oe) begin
        // SCL released but still low: a target is stretching the clock, wait
        cnt <= 16'd0;
      end
      else if (cnt != Q - 1) begin
        cnt <= cnt + 16'd1;
      end
      else begin
        cnt <= 16'd0;
        qtr <= qtr + 2'd1;
        case (op)
          START: begin                 // SDA falls while SCL is high
            case (qtr)
              2'd0: sda_oe <= 1'b0;    // release SDA (needed for a repeated START)
              2'd1: scl_oe <= 1'b0;    // release SCL
              2'd2: sda_oe <= 1'b1;    // SDA low while SCL high: START
              2'd3: begin
                scl_oe <= 1'b1;
                busy   <= 1'b0;
                done   <= 1'b1;
              end
            endcase
          end
          STOP: begin                  // SDA rises while SCL is high
            case (qtr)
              2'd0: sda_oe <= 1'b1;    // SDA low while SCL is low
              2'd1: scl_oe <= 1'b0;    // release SCL
              2'd2: sda_oe <= 1'b0;    // SDA rises while SCL high: STOP
              2'd3: begin
                busy <= 1'b0;
                done <= 1'b1;
              end
            endcase
          end
          default: begin               // WRITE and READ: nine bits, SCL low - low - high - high
            case (qtr)
              2'd0: sda_oe <= !out_bit;          // change SDA while SCL is low
              2'd1: scl_oe <= 1'b0;              // release SCL
              2'd2: ;                            // SCL high: the bit is valid
              2'd3: begin                        // sample at the end of the high time
                scl_oe <= 1'b1;
                if (op == WRITE && bitn < 8 && out_bit && !sda_i) begin
                  arb_lost <= 1'b1;              // we sent 1 but the bus is 0: lost
                  sda_oe   <= 1'b0;
                  scl_oe   <= 1'b0;
                  busy     <= 1'b0;
                  done     <= 1'b1;
                end
                else if (bitn == 8) begin
                  if (op == WRITE)
                    ack_in <= sda_i;             // 0 = the target acknowledged
                  else
                    rdata <= sh;
                  sda_oe <= 1'b0;
                  busy   <= 1'b0;
                  done   <= 1'b1;
                end
                else begin
                  sh   <= (op == WRITE) ? {sh[6:0], 1'b0} : {sh[6:0], sda_i};
                  bitn <= bitn + 4'd1;
                end
              end
            endcase
          end
        endcase
      end
    end
  end
endmodule

// I2C target (slave) with a bug. Four 8-bit registers and a register pointer, oversampling SCL and SDA.
//   write: S, ADDR+W, ptr, data, data ... P        (the pointer increments after each byte)
//   read:  S, ADDR+W, ptr, Sr, ADDR+R, data ... P  (the controller NACKs the last byte)
//   STRETCH > 0 holds SCL low for that many clocks after acknowledging each written data byte.
module i2c_slave #(
  parameter [6:0] ADDR    = 7'h50,
  parameter       STRETCH = 0
) (
  input  clk,
  input  rst,
  input  scl_i,
  input  sda_i,
  output scl_oe,
  output reg sda_oe
);
  localparam IDLE = 3'd0, DEVADDR = 3'd1, POINTER = 3'd2, WDATA = 3'd3, RDATA = 3'd4;

  reg [7:0]  regs [0:3];
  reg [2:0]  scl_s;
  reg [2:0]  sda_s;
  reg [2:0]  role;
  reg [3:0]  bitc;
  reg [7:0]  sh;
  reg [1:0]  ptr;
  reg        master_ack;
  reg [15:0] hold;                     // clock stretching counter
  reg        late_release = 1'b0;
  integer    i;

  assign scl_oe = (hold != 16'd0);

  wire scl      = scl_s[1];
  wire sda      = sda_s[1];
  wire scl_rise = scl_s[1] && !scl_s[2];
  wire scl_fall = !scl_s[1] && scl_s[2];
  wire start_c  = scl && scl_s[2] && !sda && sda_s[2];   // SDA falls while SCL high
  wire stop_c   = scl && scl_s[2] && sda && !sda_s[2];   // SDA rises while SCL high

  always @(posedge clk) begin
    scl_s <= {scl_s[1:0], scl_i};
    sda_s <= {sda_s[1:0], sda_i};
  end

  always @(posedge clk) begin
    if (rst) begin
      role   <= IDLE;
      sda_oe <= 1'b0;
      hold   <= 16'd0;
      ptr    <= 2'd0;
      for (i = 0; i < 4; i = i + 1)
        regs[i] <= 8'h00;
    end
    else begin
      if (hold != 16'd0)
        hold <= hold - 16'd1;
      if (start_c) begin
        role   <= DEVADDR;               // every START, repeated or not, expects an address
        bitc   <= 4'd0;
        sda_oe <= 1'b0;
      end
      else if (stop_c) begin
        role   <= IDLE;
        sda_oe <= 1'b0;
      end
      else if (role != IDLE) begin
        if (scl_rise) begin              // bits are counted on rising SCL edges
          if (bitc == 4'd0 && role == WDATA && late_release) begin
            sda_oe       <= 1'b0;        // BUG: the ACK is released while SCL is high
            late_release <= 1'b0;
          end
          if (bitc < 8) begin
            if (role != RDATA)
              sh <= {sh[6:0], sda};      // receive a bit
          end
          else if (role == RDATA) begin
            master_ack <= !sda;          // the controller's ACK after a byte we sent
          end
          bitc <= bitc + 4'd1;
        end
        if (scl_fall) begin
          if (bitc == 4'd8) begin        // eight bits done: the acknowledge slot follows
            case (role)
              DEVADDR: begin
                if (sh[7:1] == ADDR)
                  sda_oe <= 1'b1;        // ACK: our address
                else
                  role <= IDLE;          // not for us: ignore until the next START
              end
              POINTER: begin
                ptr    <= sh[1:0];
                sda_oe <= 1'b1;
              end
              WDATA: begin
                regs[ptr] <= sh;
                ptr       <= ptr + 2'd1;
                sda_oe    <= 1'b1;
              end
              default: sda_oe <= 1'b0;   // RDATA: release SDA for the controller's ACK
            endcase
          end
          else if (bitc == 4'd9) begin   // the acknowledge slot ends
            bitc <= 4'd0;
            case (role)
              DEVADDR: begin
                if (sh[0]) begin         // R/W = 1: we transmit
                  role   <= RDATA;
                  sh     <= regs[ptr];
                  sda_oe <= !regs[ptr][7];
                  ptr    <= ptr + 2'd1;
                end
                else begin
                  role   <= POINTER;
                  sda_oe <= 1'b0;
                end
              end
              POINTER: begin
                role   <= WDATA;
                sda_oe <= 1'b0;
              end
              WDATA: begin
                late_release <= 1'b1;    // BUG: keep pulling SDA low after the ACK clock
                if (STRETCH != 0)
                  hold <= STRETCH;       // need time to store the byte: hold SCL low
              end
              default: begin             // RDATA
                if (master_ack) begin
                  sh     <= regs[ptr];
                  sda_oe <= !regs[ptr][7];
                  ptr    <= ptr + 2'd1;
                end
                else begin
                  role   <= IDLE;        // NACK: the controller wants no more
                  sda_oe <= 1'b0;
                end
              end
            endcase
          end
          else if (role == RDATA && bitc != 4'd0) begin
            sda_oe <= !sh[6];            // the next bit, MSB first
            sh     <= {sh[6:0], 1'b0};
          end
        end
      end
    end
  end
endmodule
testbench.v
// Testbench: I2C register accesses watched by a bus monitor that decodes and checks the protocol
module tb;
  reg        clk       = 0;
  reg        rst       = 1;
  reg  [1:0] cmd       = 0;
  reg        cmd_valid = 0;
  wire       cmd_ready;
  reg  [7:0] wdata     = 0;
  reg        ack_out   = 0;
  wire [7:0] rdata;
  wire       ack_in;
  wire       done;
  wire       arb_lost;
  wire       m_scl_oe, m_sda_oe;
  wire       t0_scl_oe, t0_sda_oe;
  wire       t1_scl_oe, t1_sda_oe;

  // The bus: open drain with pull-up resistors. Any device can pull a line low.
  tri1 scl;
  tri1 sda;
  assign scl = m_scl_oe  ? 1'b0 : 1'bz;
  assign sda = m_sda_oe  ? 1'b0 : 1'bz;
  assign scl = t0_scl_oe ? 1'b0 : 1'bz;
  assign sda = t0_sda_oe ? 1'b0 : 1'bz;
  assign scl = t1_scl_oe ? 1'b0 : 1'bz;
  assign sda = t1_sda_oe ? 1'b0 : 1'bz;

  localparam START = 2'd0, STOP = 2'd1, WRITE = 2'd2, READ = 2'd3;

  i2c_master #(
    .Q (5)
  ) ctrl (
    .clk       (clk),
    .rst       (rst),
    .cmd       (cmd),
    .cmd_valid (cmd_valid),
    .cmd_ready (cmd_ready),
    .wdata     (wdata),
    .ack_out   (ack_out),
    .rdata     (rdata),
    .ack_in    (ack_in),
    .done      (done),
    .arb_lost  (arb_lost),
    .scl_oe    (m_scl_oe),
    .sda_oe    (m_sda_oe),
    .scl_i     (scl),
    .sda_i     (sda)
  );

  i2c_slave #(
    .ADDR    (7'h50),
    .STRETCH (0)
  ) target0 (
    .clk    (clk),
    .rst    (rst),
    .scl_i  (scl),
    .sda_i  (sda),
    .scl_oe (t0_scl_oe),
    .sda_oe (t0_sda_oe)
  );

  i2c_slave #(
    .ADDR    (7'h51),
    .STRETCH (0)
  ) target1 (
    .clk    (clk),
    .rst    (rst),
    .scl_i  (scl),
    .sda_i  (sda),
    .scl_oe (t1_scl_oe),
    .sda_oe (t1_sda_oe)
  );

  // ---- Bus monitor: decodes the bus into transactions and checks the protocol ----
  reg  [7:0] m_byte;
  integer    m_bits   = 0;               // bits of the current byte (9 with the ACK)
  integer    m_errors = 0;
  reg        m_active = 0;               // between START and STOP
  reg        m_scl_d  = 1;
  reg        m_sda_d  = 1;
  reg  [8*80-1:0] m_line;
  reg        m_first;                    // the next byte is an address byte
  reg        m_bit;                      // SDA sampled while SCL is high
  reg        m_skip = 0;                 // the SCL fall right after a START is not a bit

  always @(posedge clk) begin
    m_scl_d <= scl;
    m_sda_d <= sda;
    // SDA changing while SCL is high: START or STOP
    if (scl === 1'b1 && m_scl_d === 1'b1 && sda !== m_sda_d) begin
      if (m_bits != 0) begin
        $display("%0t  MONITOR: %0s in the middle of a byte (bit %0d)", $time, sda ? "STOP" : "START", m_bits);
        m_errors = m_errors + 1;
      end
      if (!sda) begin
        if (m_active)
          $write(" Sr");
        else
          $write("%0t  monitor: S", $time);
        m_active = 1;
        m_first  = 1;
        m_skip   = 1;
      end
      else begin
        $display(" P");
        m_active = 0;
      end
      m_bits = 0;
    end
    // the bus is free after a STOP: SCL must not toggle until the next START
    else if (!m_active && scl === 1'b0 && m_scl_d === 1'b1) begin
      $display("%0t  MONITOR: SCL toggles while the bus is free (no START)", $time);
      m_errors = m_errors + 1;
    end
    // a bit is sampled while SCL is high and only counts once SCL falls again;
    // an SCL high period that ends in START or STOP is not a bit
    else if (m_active && scl === 1'b1 && m_scl_d === 1'b0) begin
      m_bit = sda;
    end
    else if (m_active && scl === 1'b0 && m_scl_d === 1'b1 && m_skip) begin
      m_skip = 0;
    end
    else if (m_active && scl === 1'b0 && m_scl_d === 1'b1) begin
      if (m_bits < 8) begin
        m_byte = {m_byte[6:0], m_bit};
        m_bits = m_bits + 1;
      end
      else begin
        if (m_first)
          $write(" [0x%h %0s]", m_byte[7:1], m_byte[0] ? "R" : "W");
        else
          $write(" 0x%h", m_byte);
        $write(" %0s", m_bit ? "NACK" : "ACK");
        m_first = 0;
        m_bits  = 0;
      end
    end
  end

  always #5 clk = ~clk;

  // Run one command and wait for it to finish
  task op(input [1:0] c, input [7:0] d, input a);
    begin
      @(posedge clk);
      cmd       <= c;
      wdata     <= d;
      ack_out   <= a;
      cmd_valid <= 1;
      @(posedge clk);
      cmd_valid <= 0;
      @(posedge done);
      @(posedge clk);
    end
  endtask

  task write_reg(input [6:0] dev, input [1:0] r, input [7:0] v);
    reg ok;
    begin
      op(START, 0, 0);
      op(WRITE, {dev, 1'b0}, 0);
      ok = !ack_in;
      if (ok) begin
        op(WRITE, {6'b0, r}, 0);
        op(WRITE, v, 0);
      end
      op(STOP, 0, 0);
      if (ok)
        $display("%0t  write 0x%h reg%0d = 0x%h", $time, dev, r, v);
      else
        $display("%0t  write 0x%h: no ACK (NACK)", $time, dev);
    end
  endtask

  task read_reg(input [6:0] dev, input [1:0] r);
    begin
      op(START, 0, 0);
      op(WRITE, {dev, 1'b0}, 0);         // address the target for writing the pointer
      op(WRITE, {6'b0, r}, 0);
      op(START, 0, 0);                   // repeated START
      op(WRITE, {dev, 1'b1}, 0);         // address it again, now for reading
      op(READ, 0, 1);                    // one byte, then NACK
      op(STOP, 0, 0);
      $display("%0t  read  0x%h reg%0d = 0x%h", $time, dev, r, rdata);
    end
  endtask

  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    #22 rst = 0;
    repeat (5) @(posedge clk);
    op(START, 0, 0);                     // a burst write: pointer, then two data bytes
    op(WRITE, {7'h50, 1'b0}, 0);
    op(WRITE, 8'h01, 0);
    op(WRITE, 8'hA7, 0);
    op(WRITE, 8'hC3, 0);
    op(STOP, 0, 0);
    read_reg(7'h50, 2'd1);
    read_reg(7'h50, 2'd2);
    write_reg(7'h52, 2'd0, 8'h00);       // nobody has address 0x52
    #200;
    if (m_errors == 0)
      $display("MONITOR: no protocol errors");
    else
      $display("MONITOR: %0d protocol errors", m_errors);
    $finish;
  end
endmodule
Admin mode — edit and delete controls are visible on each row. Use the IDE to add new snippets via File → Save as Snippet.