Previously I designed a simple configurable countdown Timer with asynchronous reset, enable and limit. Now I will add a pre-scaler to the Timer. What exactly is a pre-scaler? You can think of the pre-scaler as an integer value that scales (or divides) the timer clock signal down to a lower frequency.
Why Do We Need a Pre-Scaler?
Remember that the Artix-7 FPGA on the Basys 3 runs at 100Mhz. And as a result, a clock cycle lasts only 20ns. Therefore, an 8-bit countdown timer counts down from 255 to 0 in a little more than 5.1 microseconds (20ns/clock cycle * 256). With an 8-bit countdown timer we can only time 5 microsecond intervals.
Ah, but you may rightly recall from last time that the bit-count of the timer we created is configurable. And so you might reasonably suggest that we simply create a timer using more bits, say 16-bits or 24, or more. While we can configure a 16-bit timer to count longer intervals, such a design costs more in terms of FPGA resources (ie. logic cells, input and output ports) than an 8-bit timer. And that cost scales linearly with the number of bits used by the timer.
The pre-scaler input value represents the power of 2 exponent of the divisor for the timer clock. So an input of 0 results in a clock divisor of 1 (2 ** 0), while an input of 4 results in a clock divisor of 16 (2 ** 4). This requires fewer FPGA resources.
Before we dive into the changes required to add the pre-scaler let’s review the code from last time. For reference, below is the complete Verilog code for the countdown timer.
// configurable timer module
module timer
#(parameter BITS = 8)
(
input clk, // clock input
input rst, // async reset
input en, // timer enable
input [BITS - 1 : 0] d_in, // timer start value
output [BITS - 1 : 0] q, // timer current value
output tick // tick event raised when counter reaches zero
);
// register for countdown timer
reg [BITS - 1 : 0] rCounter;
// holds the next value of internal counter
wire [BITS - 1 : 0] rNext;
// update or reset the timer on the clock tick
always @(posedge clk, posedge rst)
begin
if (rst)
rCounter <= d_in;
else
// only update counter when it is enabled
if (en) begin
rCounter <= rNext;
end
end
// reset timer when it reaches zero, otherwise
assign rNext = (rCounter == 0) ? d_in : rCounter - 1;
// raise tick when counter reaches 0
assign tick = (rCounter == 0) ? 1'b1 : 1'b0;
// make current count available on q
assign q = rCounter;
endmodule
Adding the Pre-Scaler to the Timer
In order to add a pre-scaler we need to make a few changes to the Timer code. If you want to peek ahead the complete code is shown below. The first change is the addition of a new configuration parameter I call SCALER_BITS which defaults to 2. This parameter represents how many bits of pre-scaler input the timer accepts. The next change is the addition of the pre-scaler input port ps at line 12.
Since the pre-scaler is effectively a counter the divides the clock input we must add some additional state to drive the counter logic. I will add this in the form of a register and some interconnect signals to drive that logic. I’ll call these signals reg rScaler and wire scalerNext. These show up on lines 21 and 27 respectively.
Note that in this code I’ve renamed rNext to counterNext for clarity
To the always block starting on line 30 I add logic to zero the scaler register on rst, and to update the scaler on each clock tick only if en is true.
The most interesting part of the pre-scaler is the combinational logic which drives both the pre-scaler counter and the timer itself. Starting on line 43 you can see how I calculate the value of scalerNext. This code looks tricky because it is a conditional within a conditional. But it is simpler than it looks.
The code says that the value of scalerNext is 0 if rScaler has reached its limit of 2 ** ps – 1. For a pre-scaler an input of 4 the limit is 15 (2 ** 4 – 1). Otherwise the value of scalerNext is 0 if rCounter has reached 0. If neither of those two conditionals are true, then I set the value of scalerNext to rScaler + 1.
On line 46 you can see that the logic for computing counterNext has also become a little more complicated. This code says that the value of counterNext is d_in if rCounter has reached 0. Otherwise, counterNext is given the value rCounter – 1 if scalerNext is 0, which happens when the pre-scaler reaches its limit. And finally, if neither of those cases are true, then scalerNext is given the current value of rCounter.
// configurable timer module with pre-scaler
module timer_scaled
#(
parameter TIMER_BITS = 8,
SCALER_BITS = 2
)
(
input clk, // clock input
input rst, // async reset
input en, // timer enable
input [TIMER_BITS - 1 : 0] d_in, // timer start value,
input [SCALER_BITS - 1 : 0] ps, // pre-scale factor
output [TIMER_BITS - 1 : 0] q, // timer current value
output tick // tick event raised when timer reaches zero
);
// register for countdown timer
reg [TIMER_BITS - 1 : 0] rCounter;
// register for scaler
reg [2 ** SCALER_BITS - 1: 0] rScaler;
// holds the next value of internal counter
wire [TIMER_BITS - 1 : 0] counterNext;
// holds the next value of the scaler
wire [2 ** SCALER_BITS -1 : 0] scalerNext;
// update or reset the timer on the clock tick
always @(posedge clk, posedge rst)
begin
if (rst) begin
rCounter <= d_in;
rScaler <= 0;
end else
// only update counter when it is enabled
if (en) begin
rCounter <= counterNext;
rScaler <= scalerNext;
end
end
// compute the next scaler value
assign scalerNext = (rScaler == 2 ** ps - 1) ? 0 : (rCounter == 0) ? 0 : rScaler + 1;
// compute the next counter value
assign counterNext = (rCounter == 0) ? d_in : (scalerNext == 0) ? rCounter - 1 : rCounter;
// raise tick when counter reaches 0
assign tick = (rCounter == 0) ? 1'b1 : 1'b0;
// make current count available on q
assign q = rCounter;
endmodule
The Simulation Results
The simulation test bench code for this project is available on github. The waveform for the simulation results is below. Looking at the ps you can see that the value of the pre-scaler is 4. So the divisor should be 16 (2 ** 4). If you could the number of clk cycles before the countdown timer value q changes from 0xFF to 0xFE you will see that it is in fact 16.
And that’s it for now. The source code for this project is on github.
Please like and share these posts. And remember if you have feedback, questions or suggestions regarding this post, please leave a comment!