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

APB Timer Register Block

Verilog Bus Protocols

A timer peripheral with RW, RO and write-1-to-clear registers and a level interrupt.

Open in IDE
design.v
// An APB timer peripheral: a small register block with an interrupt.
//   0x00  CTRL    read/write   bit 0 EN (count), bit 1 IE (interrupt enable)
//   0x04  LOAD    read/write   reload value
//   0x08  COUNT   read-only    current count; a write returns PSLVERR
//   0x0C  STATUS  write-1-to-clear   bit 0 EXPIRED, set when the count reaches 0
//   other         unmapped: PSLVERR
module apb_timer (
  input             PCLK,
  input             PRESETn,
  input             PSEL,
  input             PENABLE,
  input             PWRITE,
  input      [11:0] PADDR,
  input      [31:0] PWDATA,
  output reg [31:0] PRDATA,
  output            PREADY,
  output            PSLVERR,
  output            irq
);
  reg [1:0]  ctrl;
  reg [31:0] load;
  reg [31:0] count;
  reg        expired;

  wire access    = PSEL && PENABLE;
  wire wr        = access && PWRITE;
  wire hit_ctrl  = (PADDR == 12'h000);
  wire hit_load  = (PADDR == 12'h004);
  wire hit_count = (PADDR == 12'h008);
  wire hit_stat  = (PADDR == 12'h00C);
  wire mapped    = hit_ctrl || hit_load || hit_count || hit_stat;
  wire tick_zero = ctrl[0] && (count == 32'd0);   // the counter wraps this cycle

  assign PREADY  = 1'b1;
  assign PSLVERR = access && (!mapped || (hit_count && PWRITE));
  assign irq     = expired && ctrl[1];

  // Software-written registers
  always @(posedge PCLK) begin
    if (!PRESETn) begin
      ctrl <= 2'b00;
      load <= 32'h0;
    end
    else if (wr) begin
      if (hit_ctrl)
        ctrl <= PWDATA[1:0];
      else if (hit_load)
        load <= PWDATA;
    end
  end

  // The counter: reloads while disabled, counts down while enabled
  always @(posedge PCLK) begin
    if (!PRESETn)
      count <= 32'h0;
    else if (!ctrl[0] || tick_zero)
      count <= load;
    else
      count <= count - 32'd1;
  end

  // STATUS.EXPIRED: hardware sets it, software clears it by writing 1.
  // If both happen in the same cycle, the set wins so no event is lost.
  always @(posedge PCLK) begin
    if (!PRESETn)
      expired <= 1'b0;
    else if (tick_zero)
      expired <= 1'b1;
    else if (wr && hit_stat && PWDATA[0])
      expired <= 1'b0;
  end

  always @(*) begin
    case (PADDR)
      12'h000: PRDATA = {30'h0, ctrl};
      12'h004: PRDATA = load;
      12'h008: PRDATA = count;
      12'h00C: PRDATA = {31'h0, expired};
      default: PRDATA = 32'h0;
    endcase
  end
endmodule
testbench.v
// Testbench: program the APB timer, wait for its interrupt, clear it
module tb;
  reg         PCLK    = 0;
  reg         PRESETn = 0;
  reg         PSEL    = 0;
  reg         PENABLE = 0;
  reg         PWRITE  = 0;
  reg  [11:0] PADDR   = 0;
  reg  [31:0] PWDATA  = 0;
  wire [31:0] PRDATA;
  wire        PREADY;
  wire        PSLVERR;
  wire        irq;
  reg  [31:0] rdata;

  apb_timer dut (
    .PCLK    (PCLK),
    .PRESETn (PRESETn),
    .PSEL    (PSEL),
    .PENABLE (PENABLE),
    .PWRITE  (PWRITE),
    .PADDR   (PADDR),
    .PWDATA  (PWDATA),
    .PRDATA  (PRDATA),
    .PREADY  (PREADY),
    .PSLVERR (PSLVERR),
    .irq     (irq)
  );

  always #5 PCLK = ~PCLK;

  // Setup phase: PSEL high, PENABLE low. Access phase: PENABLE high until PREADY.
  task apb_write(input [11:0] addr, input [31:0] data);
    begin
      @(posedge PCLK);
      PSEL    <= 1;
      PENABLE <= 0;
      PWRITE  <= 1;
      PADDR   <= addr;
      PWDATA  <= data;
      @(posedge PCLK);
      PENABLE <= 1;
      @(posedge PCLK);
      while (!PREADY)
        @(posedge PCLK);
      $display("%0t  write 0x%h = 0x%h%s", $time, addr, data, PSLVERR ? "  PSLVERR" : "");
      PSEL    <= 0;
      PENABLE <= 0;
    end
  endtask

  task apb_read(input [11:0] addr, output [31:0] data);
    begin
      @(posedge PCLK);
      PSEL    <= 1;
      PENABLE <= 0;
      PWRITE  <= 0;
      PADDR   <= addr;
      @(posedge PCLK);
      PENABLE <= 1;
      @(posedge PCLK);
      while (!PREADY)
        @(posedge PCLK);
      data = PRDATA;
      $display("%0t  read  0x%h = 0x%h%s", $time, addr, data, PSLVERR ? "  PSLVERR" : "");
      PSEL    <= 0;
      PENABLE <= 0;
    end
  endtask

  always @(posedge irq)
    $display("%0t  irq rises", $time);

  always @(negedge irq)
    if (PRESETn)
      $display("%0t  irq falls", $time);

  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    #22 PRESETn = 1;
    apb_write(12'h004, 32'd3);           // LOAD = 3
    apb_write(12'h000, 32'h3);           // CTRL: EN = 1, IE = 1
    apb_read (12'h008, rdata);           // COUNT while it runs
    wait (irq);
    apb_read (12'h00C, rdata);           // STATUS: EXPIRED = 1
    apb_write(12'h000, 32'h2);           // stop the counter, keep IE
    apb_write(12'h00C, 32'h1);           // write 1 to clear EXPIRED
    apb_read (12'h00C, rdata);
    apb_write(12'h008, 32'd99);          // COUNT is read-only: PSLVERR
    #20 $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.