[Verilog] Timer RTL design

This time, we'll design a simple timer using the APB interface and a counter. I recommend reading the previous post first!

Timer specification

When designing an IP, I first organize the IP's specifications. I organized them in a PowerPoint presentation and then created a final data sheet in Word.

Block diagram

First, let's draw a block diagram.

글 설명 이미지, Timer block diagram
Block diagram

Do you understand the image above? "intr" is an interrupt, a signal sent from the IP to the CPU. The CPU can continuously run programs, but only one process can be processed per core. Therefore, when an IP sends an interrupt to the CPU, the CPU processes that interrupt first and then returns to the original process. If interrupts occur from multiple IPs, the CPU processes them in order of priority.

Register map

The register map is set as follows:

  • CTRL register (address: 0x0)
Signal nameR/WDefault valueBit
Reserved31:12
CountRW0x011:4
Reserved3:1
EnableRW0x00
CTRL register

Count: The cnt value of the Timer. An intr is generated whenever the internal cnt reaches the set value. (Alarm function)

Enable: Timer enable signal

  • Status register (address: 0x4)
Signal nameR/WDefault valueBit
Reserved31:1
AlarmW1C0x00
Status register

When the CPU detects an intr, it will be designed to check the status register and reset the alarm.

Timer RTL design

APB interface design

Should we design the interface first?

module timer_apb (
     //APB Interface
     input  wire        pclk
    ,input  wire        presetn
    ,input  wire        penable
    ,input  wire        psel
    ,input  wire [ 5:2] paddr
    ,input  wire        pwrite
    ,input  wire [31:0] pwdata
    ,output wire [31:0] prdata
 
     //Register Interface
    ,input  wire        alarm
    ,output wire        alarm_clr
    ,output wire        enable
    ,output wire [ 7:0] count_value
);

    //===================================================================
    // Local Parameters
    //===================================================================
    localparam INVALID_DATA = 32'hDEAD_DEAD;
    
    //===================================================================
    // Internal Signals
    //===================================================================
    wire        we = psel & ~penable &   pwrite;
    wire        re = psel & ~penable &  ~pwrite;
    reg  [31:0] RD ;
      
    //===================================================================
    // Address Decode 
    //===================================================================
    wire        ctrl    = (paddr[ 5:2] == 4'h0);
    wire        status  = (paddr[ 5:2] == 4'h1);
     
    //===================================================================
    // Write Enable
    //===================================================================
    wire        we_ctrl = we & ctrl;
    wire        we_st   = we & status;
    
    //===================================================================
    // Register Files 
    //===================================================================
    reg         r_enable;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)      r_enable <= 1'b0;
        else if (we_ctrl) r_enable <= pwdata[0];
    end
 
    reg  [ 7:0] r_count;
    always @(posedge pclk or negedge presetn) begin
        if(!presetn)      r_count <= 8'h0;
        else if (we_ctrl) r_count <= pwdata[11:4];
    end
 
    //===================================================================
    // Read Decode
    //===================================================================
    always @(*) begin
        if(re) begin
            case (paddr[5:2])
                4'h0    : RD = {20'h0, r_count, 3'b0, r_enable};
                4'h1    : RD = {31'h0, alarm};
                default : RD = INVALID_DATA;
            endcase
        end
        else RD = INVALID_DATA;
    end
    
    assign  prdata = RD;
    
    //===================================================================
    // Output Assign
    //===================================================================
    assign alarm_clr   = we_st & pwdata[0];
    assign enable      = r_enable;
    assign count_value = r_count ;

endmodule

Here, I used paddr[5:2] instead of paddr[31:0]. Since Verilog is not a simple coding language but a language for designing actual hardware, I only used the lines that are actually used to reduce the number of wires.

paddr[31:0]: 0x0 – 0x4 – 0x8 – 0xc

paddr[ 5:2]: 0x0 – 0x1 – 0x2 – 0x3

This can be expressed like this. In fact, this IP only has two register addresses, so the paddr width can be further reduced.

The reason alarm_clr is not set to register is because alarm is W1C. I hope you understand this part;;

Counter design

Next, let's design a counter that actually makes the Timer function work.

module timer_counter (
     input  wire       pclk
    ,input  wire       presetn

    ,input  wire       enable
    ,input  wire [7:0] count_value

    ,input  wire       alarm_clr
    ,output wire       alarm
);

    reg  [ 7:0] cnt;
    reg         r_alarm;
    wire        cnt_max;

    always @(posedge pclk or negedge presetn) begin
        if (!presetn)               cnt <= 8'h0;
        else if (alarm_clr)         cnt <= 8'h0;
        else if (enable & !r_alarm) cnt <= (cnt + 1'b1);
        else                        cnt <= cnt;
    end
	
    always @(posedge pclk or negedge presetn) begin
        if (!presetn)       r_alarm <= 1'h0;
        else if (alarm_clr) r_alarm <= 1'h0;
        else if (cnt_max)   r_alarm <= 1'h1;
        else                r_alarm <= r_alarm;
    end

    assign cnt_max = enable & (cnt == count_value);
    assign alarm   = r_alarm;

endmodule

By designing it this way, when the Timer is enabled and cnt increases by the entered value, the alarm is turned on and the counter module is initialized through alarm_clr.

Block integration

Then, let's write the top block as follows.

module timer (
     //APB Interface
     input  wire        pclk
    ,input  wire        presetn
    ,input  wire        penable
    ,input  wire        psel
    ,input  wire [ 5:2] paddr
    ,input  wire        pwrite
    ,input  wire [31:0] pwdata
    ,output wire [31:0] prdata

    //Interrupt
    ,output wire        intr
);

    wire        alarm;
    wire        alarm_clr;
    wire        enable;
    wire [ 7:0] count_value;

    //Instance
    timer_apb u_apb (
         .pclk        (pclk       )
        ,.presetn     (presetn    )
        ,.penable     (penable    )
        ,.psel        (psel       )
        ,.paddr       (paddr      )
        ,.pwrite      (pwrite     )
        ,.pwdata      (pwdata     )

        ,.alarm       (alarm      )
        ,.alarm_clr   (alarm_clr  )
        ,.enable      (enable     )
        ,.count_value (count_value)
    );

    timer_counter u_counter (
         .pclk        (pclk       )
        ,.presetn     (presetn    )
		
        ,.enable      (enable     )
        ,.count_value (count_value)
		
        ,.alarm_clr   (alarm_clr  )
        ,.alarm       (alarm      )
    );
	
    assign intr = alarm;

endmodule

Timer verification

Finally, let's use APB BFM to verify that the IP is working properly.

`timescale 1ns/10ps

module top();

    parameter period_pclk = 10;

    reg         pclk;
    reg         presetn;

    wire        psel;
    wire        penable;
    wire [31:0] paddr;
    wire        pwrite;
    wire [31:0] pwdata;
    wire        pready;
    wire [31:0] prdata;
    wire        pslverr;

    wire        intr;

    //clk
    always #(period_pclk*0.5) pclk = ~pclk;

    assign pready  = 1'b1;
    assign pslverr = 1'b0;

    //instance
    apb_bfm u_apb (
         .pclk    (pclk    )
        ,.presetn (presetn ) 
        ,.psel    (psel    ) 
        ,.penable (penable ) 
        ,.paddr   (paddr   ) 
        ,.pwrite  (pwrite  ) 
        ,.pwdata  (pwdata  ) 
        ,.pready  (pready  ) 
        ,.prdata  (prdata  ) 
        ,.pslverr (pslverr ) 
    );

    timer u_timer (
         .pclk    (pclk      )
        ,.presetn (presetn   ) 
        ,.psel    (psel      ) 
        ,.penable (penable   ) 
        ,.paddr   (paddr[5:2]) 
        ,.pwrite  (pwrite    ) 
        ,.pwdata  (pwdata    ) 
        ,.prdata  (prdata    ) 

        ,.intr    (intr      )
    );

    initial begin
        pclk    = 1'b0;
        presetn = 1'b0;

        #(period_pclk);
        presetn = 1'b1;

        //value = 0x10
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h100);

        //Timer enable
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h101);

        wait(intr);
		
        //Timer disable
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h100);
		
        //Intr clear
        #(10*period_pclk);
        u_apb.apb_write(32'h4,32'h1);

        //value = 0x20
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h200);

        //Timer enable
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h201);

        wait(intr);

        //Timer disable
        #(10*period_pclk);
        u_apb.apb_write(32'h0,32'h200);
		
        //Intr clear		
        #(10*period_pclk);
        u_apb.apb_write(32'h4,32'h1);

        #100 $finish;
    end

    //dump file
    initial begin
        $dumpfile ("test.vcd");
        $dumpvars();
    end

endmodule

Let's check the simulation results with a waveform.

첫번째 intr
First intr
두번째 intr
Second intr

You can see that the time at which alarm(intr) occurs changes as you change count_value, and that alarm(intr) turns off when you clear it.

I designed a simple IP with an alarm function like this.

References: CPU interrupt explanation (wiki)

Similar Posts