⏱️ 第22课:定时器

📖 本课目标

设计并实现16位可编程定时器,支持多种工作模式和预分频。定时器是8位电脑的"心跳"——操作系统的任务调度、声音的频率产生、精确的时间延迟,都依赖定时器提供的时间基准。

🧠 定时器原理深度解析

定时器的本质是一个递减计数器:加载一个初始值,每个时钟周期减1,减到0时产生事件(中断或输出信号)。看似简单,但其中蕴含了丰富的设计选择:

定时器工作原理 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 系统时钟 ──→ ┌─────────┐ ┌──────────┐ │ 预分频器 │ ──→ │ 16位计数器│ ──→ 中断/输出 │ /1,8,64,256│ │ 递减到0 │ └─────────┘ └────┬─────┘ │ ┌──────────▼──────────┐ │ 工作模式选择 │ │ • 单次模式:到0停止 │ │ • 循环模式:自动重载 │ └─────────────────────┘ 计时计算: T = (reload_value + 1) × prescale × (1/clk_freq) 例:clk=10MHz, prescale=/64, reload=15624 T = 15625 × 64 × 100ns = 100ms → 10Hz中断

预分频器的作用

16位计数器最大计数值65535。在10MHz时钟下,最长定时仅6.5535ms——远不够用。预分频器将时钟频率降低,扩展定时范围:

预分频等效频率最长定时(16位)典型用途
/110 MHz6.55 ms高频精确定时、PWM
/81.25 MHz52.4 ms声音频率产生
/64156.25 KHz419.4 ms操作系统时钟(10-100Hz)
/25639.0625 KHz1.678 s长定时、看门狗
💡 经典设计参考:6502系统中常用的6522 VIA定时器、Z80的CTC(Counter Timer Circuit)、以及8051的Timer 0/1/2,都采用了类似的预分频+计数器架构。我们的设计融合了这些经典设计的精华。

🏗️ 定时器架构设计

定时器控制器架构 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ┌────────────────────────────────────────────────┐ │ 定时器控制器 │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ │ │ │ 预分频器 │ │ 16位计数器 │ │ 控制寄存器│ │ │ │ 8位分频 │ │ count │ │ ctrl │ │ │ │ /1,8,64 │ │ │ │ bit0:EN │ │ │ │ /256 │ │ │ │ bit1:MODE│ │ │ └────┬─────┘ └─────┬─────┘ │ bit2:IRQ │ │ │ │ │ │ bit4-3:P │ │ │ └──────┬───────┘ └──────────┘ │ │ │ │ │ ┌──────────▼───────────┐ │ │ │ 16位重载寄存器 │ │ │ │ reload │ │ │ └──────────────────────┘ │ │ │ │ 输出: timer_irq timer_out timer_flag │ └────────────────────────────────────────────────┘ 内存映射: $FF04: 计数器低字节 (R/W) $FF05: 计数器高字节 (R/W) $FF06: 控制寄存器 (R/W)

🔧 Verilog实现

// 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验证通过 —— timer.v 通过 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

📋 定时器应用场景

场景1:系统心跳(10Hz时钟中断)

; 配置定时器产生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

场景2:精确延时(无中断轮询)

; 延时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

场景3:方波输出(蜂鸣器)

将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

🎯 练习

练习1:输入捕获模式

扩展定时器,添加输入捕获功能:当外部信号边沿到来时,将当前计数值锁存到捕获寄存器。这是测量脉冲宽度和频率的关键功能。提示:添加capture_pin输入和capture_reg寄存器,检测上升/下降沿时锁存timer_count。

练习2:PWM输出

实现PWM(脉宽调制)输出:使用两个比较寄存器,一个控制周期,一个控制占空比。编写Verilog实现8位PWM,可通过CPU接口调节占空比0-100%。应用场景:LED亮度调节、电机速度控制。

练习3:看门狗定时器

实现看门狗定时器(Watchdog Timer):如果CPU在规定时间内没有"喂狗"(写特定值到看门狗寄存器),则产生系统复位。这是嵌入式系统防止软件死锁的最后一道防线。看门狗与普通定时器的关键区别是什么?

练习4:多通道定时器

参考6522 VIA的两个定时器设计,实现双通道定时器:Timer1和Timer2共享预分频器但独立运行。Timer1有输出和中断,Timer2可以计数外部事件。设计共享预分频器时的时序考量。

🏆 成就解锁:时间领主

你实现了16位可编程定时器!这包括:

定时器是操作系统的脉搏——有了它,多任务调度、精确延时、声音产生都成为可能!