Debug

Embedded logic analyzer

logic_analyzer

Pulse arm to start. Every clock the probe bits go into a DEPTH-sample buffer; the trigger fires on the first sample, after PRETRIGGER samples are kept, where (probe & trigger_mask) equals (trigger_value & trigger_mask), and a zero mask fires at once. After the trigger the buffer fills, then the whole window (PRETRIGGER samples before the trigger, the rest after) goes out on tx as one frame with a checksum (8N1, CLKS_PER_BIT clocks per bit). Connect tx to the board's USB-serial pin, or to a spare pin with a USB-serial adapter. The buffer uses block RAM on FPGAs; it is a debug aid, not part of the design to fabricate.

Verified by Tapeo5164 generic cells at the defaultslogic analyzerdebug captureUART

Instantiate it

Paste into your design and connect the signals.

systemverilog
logic_analyzer #(.WIDTH(8), .DEPTH(256), .PRETRIGGER(32), .CLKS_PER_BIT(234)) u_ila (
  .clk(clk), .rst(rst),
  .probe({state, valid, ready, data[4:0]}), .trigger_mask(8'h01), .trigger_value(8'h01),
  .arm(button_pressed), .tx(uart_tx_pin), .armed(), .sending()
);

Parameters

NameDefaultMeaning
WIDTH8Probe bits, 1 to 32.
DEPTH256Samples per capture, a power of two from 4 to 4096.
PRETRIGGER32Samples kept from before the trigger (less than DEPTH).
CLKS_PER_BIT868Clock cycles per UART bit: clock frequency divided by baud rate (100 MHz / 115200 = 868).

Ports

NameDirectionWidthMeaning
clkinput1Clock; everything changes on its rising edge.
rstinput1Synchronous reset, active high.
probeinputWIDTHThe signals to record, sampled every clock.
trigger_maskinputWIDTHWhich probe bits the trigger compares; all zero triggers at once.
trigger_valueinputWIDTHThe value those bits must have.
arminput1Pulse for one cycle to start a capture.
txoutput1Serial line carrying the capture; idles high.
armedoutput1High while recording or waiting for the trigger.
sendingoutput1High while the capture is being sent.

Add it to a project

It is copied to ip/logic_analyzer/logic_analyzer.sv with a README (ip/logic_analyzer/README.md). When your design needs it, the Architect (new designs) or the Architect (changes) suggests it and asks before using it. Or add it yourself:

How it is verified

Before every release Tapeo checks this block: its self-checking testbench passes in Icarus Verilog, Verilator -Wall reports nothing, and Yosys synthesizes it (5164 generic cells at the default parameters). That shows it works for the tested cases; your copy is part of your project, and your own testbench checks it in your design.

Source

logic_analyzer.sv
// Embedded logic analyzer: records `probe` every clock into a DEPTH-sample
// buffer, keeps PRETRIGGER samples from before the trigger, and sends the
// capture over its own UART (8N1, LSB first) as one frame that
// `tapeo fpga capture` turns into a VCD. Tapeo Hub (verified by Tapeo).
//
// Pulse `arm` to start. The trigger fires on the first sample, after at least
// PRETRIGGER samples, where (probe & trigger_mask) == (trigger_value & trigger_mask);
// a zero mask fires at once. Frame, in bytes:
//   0xA5 0x5A WIDTH DEPTH[7:0] DEPTH[15:8] PRETRIGGER[7:0] PRETRIGGER[15:8]
//   DEPTH samples, oldest first, each ceil(WIDTH/8) bytes, least significant byte first
//   one checksum byte: the XOR of every byte before it
module logic_analyzer #(
  parameter integer WIDTH = 8,          // probe bits, 1 to 32
  parameter integer DEPTH = 256,        // samples per capture, a power of two from 4 to 4096
  parameter integer PRETRIGGER = 32,    // samples kept from before the trigger, less than DEPTH
  parameter integer CLKS_PER_BIT = 868  // clock frequency / baud rate, for example 100 MHz / 115200
) (
  input  logic             clk,
  input  logic             rst,
  input  logic [WIDTH-1:0] probe,
  input  logic [WIDTH-1:0] trigger_mask,
  input  logic [WIDTH-1:0] trigger_value,
  input  logic             arm,
  output logic             tx,
  output logic             armed,
  output logic             sending
);
  localparam integer ADDR_BITS = $clog2(DEPTH);
  localparam integer SAMPLE_BYTES = (WIDTH + 7) / 8;
  localparam integer HEADER_BYTES = 7;
  localparam integer FRAME_BYTES = HEADER_BYTES + DEPTH * SAMPLE_BYTES + 1;
  localparam integer COUNT_WIDTH = (CLKS_PER_BIT > 1) ? $clog2(CLKS_PER_BIT) : 1;
  localparam logic [COUNT_WIDTH-1:0] LAST_TICK = COUNT_WIDTH'(CLKS_PER_BIT - 1);
  localparam integer INDEX_BITS = $clog2(FRAME_BYTES + 1);
  localparam logic [ADDR_BITS-1:0] PRE = ADDR_BITS'(PRETRIGGER);
  localparam logic [ADDR_BITS:0] POST_SAMPLES = (ADDR_BITS + 1)'(DEPTH - PRETRIGGER);
  localparam logic [15:0] DEPTH16 = 16'(DEPTH);
  localparam logic [15:0] PRE16 = 16'(PRETRIGGER);

  typedef enum logic [2:0] { IDLE, FILL, WAIT, POST, SEND } state_t;
  state_t state;

  logic [WIDTH-1:0] buffer [DEPTH];
  logic [ADDR_BITS-1:0] write_addr, read_addr;
  logic [ADDR_BITS:0] recorded;
  logic [WIDTH-1:0] sample_q;
  logic [8*SAMPLE_BYTES-1:0] sample_wide;
  logic trigger_hit;

  // Byte sequencer and its UART.
  logic [INDEX_BITS-1:0] byte_index;
  logic [$clog2(SAMPLE_BYTES + 1)-1:0] sample_byte;
  logic [7:0] checksum, next_byte;
  logic [9:0] shift;
  logic [3:0] bits_left;
  logic [COUNT_WIDTH-1:0] ticks;
  logic uart_busy;

  assign trigger_hit = ((probe ^ trigger_value) & trigger_mask) == '0;
  assign sample_wide = (8 * SAMPLE_BYTES)'(sample_q);
  assign armed = (state == FILL) || (state == WAIT) || (state == POST);
  assign sending = (state == SEND);

  // The byte to send at byte_index: header, then samples, then the checksum.
  always_comb begin
    case (byte_index)
      INDEX_BITS'(0): next_byte = 8'hA5;
      INDEX_BITS'(1): next_byte = 8'h5A;
      INDEX_BITS'(2): next_byte = 8'(WIDTH);
      INDEX_BITS'(3): next_byte = DEPTH16[7:0];
      INDEX_BITS'(4): next_byte = DEPTH16[15:8];
      INDEX_BITS'(5): next_byte = PRE16[7:0];
      INDEX_BITS'(6): next_byte = PRE16[15:8];
      default: next_byte = (byte_index == INDEX_BITS'(FRAME_BYTES - 1)) ? checksum : sample_wide[8*sample_byte +: 8];
    endcase
  end

  // Synchronous read, so the buffer can map to block RAM.
  always_ff @(posedge clk) begin
    sample_q <= buffer[read_addr];
    if (state == FILL || state == WAIT || state == POST) buffer[write_addr] <= probe;
  end

  always_ff @(posedge clk) begin
    if (rst) begin
      state <= IDLE;
      write_addr <= '0;
      read_addr <= '0;
      recorded <= '0;
      byte_index <= '0;
      sample_byte <= '0;
      checksum <= '0;
      shift <= '1;
      bits_left <= '0;
      ticks <= '0;
      uart_busy <= 1'b0;
      tx <= 1'b1;
    end else begin
      case (state)
        IDLE: if (arm) begin
          write_addr <= '0;
          recorded <= '0;
          state <= (PRETRIGGER == 0) ? WAIT : FILL;
        end
        FILL: begin
          write_addr <= write_addr + 1'b1;
          if (recorded + 1'b1 == (ADDR_BITS + 1)'(PRETRIGGER)) state <= WAIT;
          recorded <= recorded + 1'b1;
        end
        WAIT: begin
          write_addr <= write_addr + 1'b1;
          if (trigger_hit) begin
            // This sample is the trigger; the window starts PRETRIGGER samples before it.
            read_addr <= write_addr - PRE;
            recorded <= (ADDR_BITS + 1)'(1);
            if (POST_SAMPLES == (ADDR_BITS + 1)'(1)) begin
              state <= SEND;
              byte_index <= '0;
              sample_byte <= '0;
              checksum <= '0;
            end else state <= POST;
          end
        end
        POST: begin
          write_addr <= write_addr + 1'b1;
          recorded <= recorded + 1'b1;
          if (recorded + 1'b1 == POST_SAMPLES) begin
            state <= SEND;
            byte_index <= '0;
            sample_byte <= '0;
            checksum <= '0;
          end
        end
        SEND: begin
          if (!uart_busy) begin
            if (byte_index == INDEX_BITS'(FRAME_BYTES)) state <= IDLE;
            else begin
              // Start, 8 data bits, stop; shifted out least significant bit first.
              shift <= {1'b1, next_byte, 1'b0};
              bits_left <= 4'd10;
              ticks <= '0;
              uart_busy <= 1'b1;
              checksum <= checksum ^ next_byte;
              byte_index <= byte_index + 1'b1;
              if (byte_index >= INDEX_BITS'(HEADER_BYTES) && byte_index < INDEX_BITS'(FRAME_BYTES - 1)) begin
                if (sample_byte == ($clog2(SAMPLE_BYTES + 1))'(SAMPLE_BYTES - 1)) begin
                  sample_byte <= '0;
                  read_addr <= read_addr + 1'b1;
                end else sample_byte <= sample_byte + 1'b1;
              end
            end
          end
        end
        default: state <= IDLE;
      endcase
      // The UART: one bit every CLKS_PER_BIT clocks.
      if (uart_busy) begin
        tx <= shift[0];
        if (ticks == LAST_TICK) begin
          ticks <= '0;
          shift <= {1'b1, shift[9:1]};
          bits_left <= bits_left - 1'b1;
          if (bits_left == 4'd1) uart_busy <= 1'b0;
        end else ticks <= ticks + 1'b1;
      end else tx <= 1'b1;
    end
  end
endmodule

← Every block in Tapeo Hub