4. 定时器中断实现秒表

任务目标

在玄铁C906/C910平台上,利用硬件定时器产生周期性中断,在中断服务函数中维护计时变量,实现一个具备启动、停止、清零功能的秒表,并通过串口打印当前计时值。通过本案例掌握定时器中断的配置流程与中断处理程序的编写方法。

硬件与依赖

  • 开发板:玄铁C906/C910 EVB板
  • SDK:T-Head玄铁嵌入式SDK(含驱动库)
  • 外设:UART0(调试串口)、片上定时器(Timer0)
  • 依赖头文件:tmr.h(定时器驱动)、uart.h(串口驱动)、plic.h(中断控制器)

实现原理

定时器工作在周期模式,时钟源为系统主频分频后的固定频率。配置定时器溢出值,使中断频率为100Hz(即每10ms触发一次中断)。在中断服务函数中累加计数变量,当计数达到100时表示1秒,更新秒表显示值。主循环中通过串口命令控制秒表状态。

完整代码

#include <tmr.h>
#include <uart.h>
#include <plic.h>
#include <sysctl.h>
#include <stdio.h>
#include <string.h>

#define TIMER_NUM       0
#define TIMER_CHANNEL   0
#define TIMER_FREQ      100     // 100Hz中断
#define TICK_PER_SEC    100     // 每100次中断为1秒

volatile uint32_t tick_count = 0;
volatile uint32_t seconds = 0;
volatile uint8_t  running = 0;
volatile uint8_t  need_clear = 0;

// 定时器中断服务函数
void timer0_isr(void)
{
    tick_count++;
    if (tick_count >= TICK_PER_SEC) {
        tick_count = 0;
        seconds++;
    }
    // 清除中断标志
    tmr_irq_clear(TIMER_NUM, TIMER_CHANNEL);
}

// 定时器初始化
void timer_init(void)
{
    uint64_t clk_freq = sysctl_clock_get_freq(SYSCTL_CLOCK_CPU);
    uint64_t interval = clk_freq / TIMER_FREQ;

    tmr_init(TIMER_NUM);
    tmr_set_clock(TIMER_NUM, TIMER_CHANNEL, clk_freq);
    tmr_set_mode(TIMER_NUM, TIMER_CHANNEL, TMR_MODE_PERIOD);
    tmr_set_count(TIMER_NUM, TIMER_CHANNEL, 0);
    tmr_set_reload(TIMER_NUM, TIMER_CHANNEL, interval);
    tmr_set_enable(TIMER_NUM, TIMER_CHANNEL, 1);

    // 注册中断
    plic_irq_register(TMR_IRQN(TIMER_NUM, TIMER_CHANNEL), timer0_isr);
    plic_irq_enable(TMR_IRQN(TIMER_NUM, TIMER_CHANNEL));
}

// 串口命令处理
void process_command(char cmd)
{
    switch (cmd) {
    case 's':   // 启动
        running = 1;
        break;
    case 'p':   // 暂停
        running = 0;
        break;
    case 'c':   // 清零
        need_clear = 1;
        break;
    default:
        break;
    }
}

int main(void)
{
    char rx_char;
    uint32_t last_seconds = 0;

    uart_init(UART_DEV0, 115200);
    timer_init();
    printf("秒表已启动,命令: s=启动 p=暂停 c=清零\r\n");

    while (1) {
        // 接收串口命令
        if (uart_rx_poll(UART_DEV0, &rx_char)) {
            process_command(rx_char);
        }

        // 清零处理(在非中断上下文执行)
        if (need_clear) {
            need_clear = 0;
            tick_count = 0;
            seconds = 0;
        }

        // 每秒打印一次当前值
        if (running && seconds != last_seconds) {
            last_seconds = seconds;
            printf("计时: %u 秒\r\n", seconds);
        }
    }
    return 0;
}

编译与运行

  1. 将上述代码保存为 stopwatch.c,放入SDK的 samples/stopwatch/ 目录。
  2. 在SDK根目录执行交叉编译:
make stopwatch
  1. 将生成的 stopwatch.bin 烧录至开发板,使用串口工具连接UART0(波特率115200)。

验证方法

  • 启动秒表:发送字符 s,串口应每秒输出一次 计时: N 秒,N从0开始递增。
  • 暂停功能:发送 p,计时输出停止,数值保持不变。
  • 清零功能:发送 c,再发送 s,计时从0重新开始。
  • 中断验证:若注释掉 tmr_irq_clear() 行,程序将卡死在中断中(无法响应串口),反向证明中断服务函数被正确触发。

通过标准:串口输出每秒递增1,且启动/暂停/清零命令均能正确响应,即判定案例通过。

常见问题

  • 无输出:检查UART引脚配置及波特率是否匹配。
  • 计时不准确:确认 sysctl_clock_get_freq 返回的是CPU主频,若时钟源不同需调整分频系数。
  • 中断不触发:确认全局中断已使能(plic_init() 已在SDK启动代码中调用),且定时器通道号与中断号对应正确。