[Verilog] Timer RTL design

이번에는 APB interface와 counter를 이용해서 간단한 Timer를 설계해 보겠습니다. 이전 글을 먼저 보고 오시는 것을 추천합니다!!

Timer specification

IP를 설계할 때는 가장 먼저 IP의 spec을 정리합니다. 저는 ppt로 정리한 뒤 최종적으로 Word로 Data sheet를 작성했어요.

Block diagram

가장 먼저 block diagram을 그려봅시다.

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

위 그림이 이해되시나요? intr는 interrupt로 IP에서 CPU로 보내는 신호입니다. CPU에서는 계속 실행 중인 프로그램이 있는데요, 한 core 당 하나의 프로세스만 처리할 수 있습니다. 그래서 IP에서 interrupt를 CPU로 보내면 CPU는 해당 interrupt를 먼저 처리하고 다시 원래 프로세스로 돌아갑니다. 여러 IP에서 interrupt가 발생하면 CPU는 우선순위가 높은 순서대로 interrupt를 처리합니다.

Register map

Register map은 다음과 같이 설정했습니다.

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

Count: Timer의 cnt 값입니다. 내부 cnt가 설정된 값이 될 때마다 intr가 발생합니다. (알람 기능)

Enable: Timer enable signal

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

CPU가 intr를 감지하면 status register를 확인하고 alarm을 초기화하도록 설계할 겁니다.

Timer RTL design

APB interface 설계

먼저 interface부터 설계해 볼까요?

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

여기서 paddr[31:0]가 아니라 paddr[5:2]를 사용했는데요, Verilog가 단순 코딩이 아니라 실제 하드웨어를 설계하는 언어여서 wire를 줄이기 위해 진짜 사용하는 line만 사용했습니다. 그러니까

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

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

이렇게 표현할 수 있는 겁니다. 사실, 이 IP는 register address가 두 개밖에 없어서 paddr width를 더 줄여도 됩니다.

alarm_clr를 register로 설정하지 않은 이유는 alarm이 W1C이기 때문입니다. 이 부분이 이해되셨으면 좋겠네요;;

Counter 설계

다음으로 실제 Timer 기능이 작동하는 counter를 설계해 봅시다.

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

이렇게 설계하면 Timer가 enable 될 때 입력된 value만큼 cnt가 올라가면 alarm이 켜지고 alarm_clr를 통해 counter module이 초기화되도록 만들 수 있습니다.

Block integration

그럼 top block을 다음과 같이 작성합시다.

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 검증

마지막으로 APB BFM을 사용해서 IP가 제대로 작동하는지 검증해 봅시다.

`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

Waveform으로 simulation 결과를 확인해볼까요?

첫번째 intr
첫번째 intr
두번째 intr
두번째 intr

Count_value를 바꿔줌에 따라 alarm(intr)이 발생하는 시간이 달라지고, clear 하면 alarm(intr)이 꺼지는 것을 확인할 수 있습니다.

이렇게 alarm 기능이 있는 간단한 IP를 설계해 봤습니다.

참고: CPU interrupt 설명(wiki)

유사한 게시물