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.
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 name | R/W | Default value | Bit |
|---|---|---|---|
| Reserved | – | – | 31:12 |
| Count | RW | 0x0 | 11:4 |
| Reserved | – | – | 3:1 |
| Enable | RW | 0x0 | 0 |
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 name | R/W | Default value | Bit |
|---|---|---|---|
| Reserved | – | – | 31:1 |
| Alarm | W1C | 0x0 | 0 |
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 ;
endmoduleHere, 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;
endmoduleBy 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;
endmoduleTimer 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
endmoduleLet's check the simulation results with a waveform.
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)