设计并实现16位可编程定时器,支持多种工作模式和预分频。定时器是8位电脑的"心跳"——操作系统的任务调度、声音的频率产生、精确的时间延迟,都依赖定时器提供的时间基准。
定时器的本质是一个递减计数器:加载一个初始值,每个时钟周期减1,减到0时产生事件(中断或输出信号)。看似简单,但其中蕴含了丰富的设计选择:
16位计数器最大计数值65535。在10MHz时钟下,最长定时仅6.5535ms——远不够用。预分频器将时钟频率降低,扩展定时范围:
| 预分频 | 等效频率 | 最长定时(16位) | 典型用途 |
|---|---|---|---|
| /1 | 10 MHz | 6.55 ms | 高频精确定时、PWM |
| /8 | 1.25 MHz | 52.4 ms | 声音频率产生 |
| /64 | 156.25 KHz | 419.4 ms | 操作系统时钟(10-100Hz) |
| /256 | 39.0625 KHz | 1.678 s | 长定时、看门狗 |
// timer.v - 16位可编程定时器
module timer (
input wire clk,
input wire rst_n,
input wire cs,
input wire rw,
input wire reg_addr, // 0=低 1=高
input wire [7:0] cpu_data_in,
output reg [7:0] cpu_data_out,
output reg timer_irq,
output wire timer_out
);
reg [15:0] timer_count;
reg [15:0] timer_reload;
reg [7:0] timer_ctrl;
reg timer_flag;
wire ctrl_enable = timer_ctrl[0];
wire ctrl_mode = timer_ctrl[1]; // 0=单次 1=循环
wire ctrl_irq_en = timer_ctrl[2];
wire [1:0] ctrl_prescale = timer_ctrl[4:3];
// 预分频计数器
reg [7:0] prescale_cnt;
reg prescale_tick;
always @(*) begin
case (ctrl_prescale)
2'b00: prescale_tick = 1'b1;
2'b01: prescale_tick = (prescale_cnt[2:0] == 3'd0);
2'b10: prescale_tick = (prescale_cnt[5:0] == 6'd0);
2'b11: prescale_tick = (prescale_cnt == 8'd0);
endcase
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
prescale_cnt <= 8'd0;
else if (ctrl_enable)
prescale_cnt <= prescale_cnt + 8'd1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
timer_count <= 16'd0;
timer_reload <= 16'd0;
timer_ctrl <= 8'd0;
timer_flag <= 1'b0;
timer_irq <= 1'b0;
cpu_data_out <= 8'd0;
end else begin
timer_irq <= 1'b0;
// CPU写寄存器
if (cs && !rw) begin
if (!reg_addr)
timer_count[7:0] <= cpu_data_in;
else
timer_count[15:8] <= cpu_data_in;
timer_flag <= 1'b0;
end
// CPU读寄存器
if (cs && rw) begin
if (!reg_addr)
cpu_data_out <= timer_count[7:0];
else
cpu_data_out <= timer_count[15:8];
end
// 计数核心逻辑
if (ctrl_enable && prescale_tick) begin
if (timer_count == 16'd0) begin
timer_flag <= 1'b1;
if (ctrl_irq_en)
timer_irq <= 1'b1;
if (ctrl_mode)
timer_count <= timer_reload;
end else
timer_count <= timer_count - 16'd1;
end
end
end
assign timer_out = timer_flag;
endmodule
verilator --lint-only 检查,无错误无警告。
// tb_timer.v - 定时器仿真测试
module tb_timer;
reg clk, rst_n, cs, rw, reg_addr;
reg [7:0] cpu_data_in;
wire[7:0] cpu_data_out;
wire timer_irq, timer_out;
timer uut (.*);
initial clk = 0;
always #5 clk = ~clk; // 100MHz
task cpu_write;
input addr;
input [7:0] data;
begin
cs = 1; rw = 0; reg_addr = addr; cpu_data_in = data;
#10;
cs = 0;
end
endtask
task cpu_read;
input addr;
output [7:0] data;
begin
cs = 1; rw = 1; reg_addr = addr;
#10;
data = cpu_data_out;
cs = 0;
end
endtask
reg [7:0] rd_val;
initial begin
rst_n = 0; cs = 0; rw = 0; reg_addr = 0; cpu_data_in = 0;
#20; rst_n = 1;
// 测试1:单次模式,计数值10
$display("--- Test 1: Single-shot mode ---");
cpu_write(0, 8'd10); // count低=10
cpu_write(1, 8'd0); // count高=0 → count=10
cpu_write(0, 8'b00000101); // ctrl: EN=1, IRQ_EN=1
wait (timer_irq);
$display(" PASS: Timer interrupt fired");
// 验证单次模式后计数器停止
#100;
if (!timer_irq)
$display(" PASS: Single-shot stopped");
// 测试2:循环模式
$display("--- Test 2: Continuous mode ---");
cpu_write(0, 8'd5); // count=5
cpu_write(1, 8'd0);
// ctrl: EN=1, MODE=1, IRQ_EN=1, PRE=/1
cpu_write(0, 8'b00000111);
// 等待3次中断
integer irq_count;
irq_count = 0;
while (irq_count < 3) begin
@(posedge clk);
if (timer_irq) irq_count = irq_count + 1;
end
$display(" PASS: 3 continuous interrupts received");
$display("--- All timer tests complete ---");
$finish;
end
endmodule
; 配置定时器产生10Hz中断
; clk=10MHz, prescale=/64 → 156250Hz
; reload = 156250/10 - 1 = 15624 = $3D08
LDA #$08 ; 计数器低字节
STA $FF04
LDA #$3D ; 计数器高字节
STA $FF05
LDA #$07 ; EN=1, MODE=1(循环), IRQ=1, PRE=/64
STA $FF06
; 延时1ms (10000个时钟周期 @ 10MHz)
DELAY_1MS:
LDA #$10 ; 计数器低字节 (10000 = $2710)
STA $FF04
LDA #$27 ; 计数器高字节
STA $FF05
LDA #$01 ; EN=1, 单次, 无中断, /1
STA $FF06
WAIT_LOOP:
LDA $FF06 ; 读控制寄存器
AND #$80 ; 检查flag位(需扩展状态位)
BEQ WAIT_LOOP
RTS
将timer_out连接到蜂鸣器或音频输出。循环模式下,timer_out自动翻转,产生方波:
; 产生440Hz音调 (A4音符)
; prescale=/8 → 1.25MHz
; reload = 1250000/(440*2) - 1 = 1420 = $058C
; (*2是因为方波需要半个周期翻转一次)
LDA #$8C
STA $FF04
LDA #$05
STA $FF05
LDA #$0B ; EN=1, MODE=1, PRE=/8
STA $FF06
扩展定时器,添加输入捕获功能:当外部信号边沿到来时,将当前计数值锁存到捕获寄存器。这是测量脉冲宽度和频率的关键功能。提示:添加capture_pin输入和capture_reg寄存器,检测上升/下降沿时锁存timer_count。
实现PWM(脉宽调制)输出:使用两个比较寄存器,一个控制周期,一个控制占空比。编写Verilog实现8位PWM,可通过CPU接口调节占空比0-100%。应用场景:LED亮度调节、电机速度控制。
实现看门狗定时器(Watchdog Timer):如果CPU在规定时间内没有"喂狗"(写特定值到看门狗寄存器),则产生系统复位。这是嵌入式系统防止软件死锁的最后一道防线。看门狗与普通定时器的关键区别是什么?
参考6522 VIA的两个定时器设计,实现双通道定时器:Timer1和Timer2共享预分频器但独立运行。Timer1有输出和中断,Timer2可以计数外部事件。设计共享预分频器时的时序考量。
你实现了16位可编程定时器!这包括:
定时器是操作系统的脉搏——有了它,多任务调度、精确延时、声音产生都成为可能!