이번에는 APB interface와 counter를 이용해서 간단한 Timer를 설계해 보겠습니다. 이전 글을 먼저 보고 오시는 것을 추천합니다!!
Timer specification
IP를 설계할 때는 가장 먼저 IP의 spec을 정리합니다. 저는 ppt로 정리한 뒤 최종적으로 Word로 Data sheet를 작성했어요.
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 name | R/W | Default value | Bit |
|---|---|---|---|
| Reserved | – | – | 31:12 |
| Count | RW | 0x0 | 11:4 |
| Reserved | – | – | 3:1 |
| Enable | RW | 0x0 | 0 |
Count: Timer의 cnt 값입니다. 내부 cnt가 설정된 값이 될 때마다 intr가 발생합니다. (알람 기능)
Enable: Timer enable signal
- Status register (address: 0x4)
| Signal name | R/W | Default value | Bit |
|---|---|---|---|
| Reserved | – | – | 31:1 |
| Alarm | W1C | 0x0 | 0 |
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;
endmoduleTimer 검증
마지막으로 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
endmoduleWaveform으로 simulation 결과를 확인해볼까요?
Count_value를 바꿔줌에 따라 alarm(intr)이 발생하는 시간이 달라지고, clear 하면 alarm(intr)이 꺼지는 것을 확인할 수 있습니다.
이렇게 alarm 기능이 있는 간단한 IP를 설계해 봤습니다.