05-定时器与看门狗.md 23 KB


title: 定时器与看门狗 tags: [定时器, 看门狗, EPIT, GPT, IMX6ULL, 裸机开发, 按键消抖] created: 2026-09-16 updated: 2026-09-17 pdf_ref:

  • "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第18章 EPIT定时器实验"
  • "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第19章 定时器按键消抖实验" ---

定时器与看门狗

💡 关联知识: [[FreeRTOS学习笔记/08-时间片调度与延时管理]] | [[STM32学习笔记/08-SysTick与通用定时器TIM]]


Part 1: 定时器

1.1 IMX6ULL 定时器系统总览

定时器 数量 分辨率 用途
EPIT 2 32位 周期性中断定时
GPT 2 32位 PWM输出/输入捕获
SNVS 1 32位 低功耗定时器
WDOG 2 16位 看门狗

1.2 EPIT 定时器概述

EPIT(Enhanced Periodic Interrupt Timer)增强的周期中断定时器,I.MX6U 中最常用的定时器。与 STM32 不同,EPIT 仅完成周期性中断定时,输入捕获、PWM 输出等功能由 GPT 等其他外设完成。

核心特点:

  • 时钟源可选的 32 位向下计数器
  • 12 位分频值(0~4095,对应 1~4096 分频)
  • 计数值与比较值相等时产生中断

EPIT 工作原理框图:

时钟源选择(多路选择器)
    │
    ▼
┌─────────────┐
│ 12位分频器   │  PRESCALAR (0~4095 → 1~4096分频)
└──────┬──────┘
       │
       ▼
┌──────────────────────────────────────────┐
│              EPIT 内部                    │
│                                          │
│  ┌──────────┐  ┌──────────┐  ┌────────┐  │
│  │  LR      │  │  CNR     │  │  CMPR  │  │
│  │ 加载寄存器│→ │ 计数寄存器│→ │ 比较器 │──┼──→ 比较中断(定时中断)
│  │ (初值)   │  │ (当前值)  │  │        │  │
│  └──────────┘  └──────────┘  └────────┘  │
│       ↑              ↑                    │
│       └──计数到0时重载──┘                    │
└──────────────────────────────────────────┘

三种可选时钟源:

时钟源 频率 说明
ipg_clk 66MHz 外设总线时钟(最常用)
ipg_clk_32k 32.768kHz 低频参考时钟
ipg_clk_highfreq 高频 高频参考时钟

两种工作模式:

模式 RLD位 行为
set-and-forget RLD=1 计数到0后从LR重新加载,循环递减
free-running RLD=0 计数到0后从0xFFFFFFFF重新开始

set-and-forget 是最常用的模式。配置为1后,计数器从LR获取初值,每次减到0自动从LR重载,周而复始。


1.3 EPIT 寄存器详解

EPITx_CR — 控制寄存器(偏移 0x000)

位域 名称 功能
bit25:24 CLKSRC 时钟源选择:0=关闭,1=ipg_clk,2=ipg_clk_highfreq,3=ipg_clk_32k
bit15:4 PRESCALAR 分频值:0~4095 → 1~4096分频
bit3 RLD 工作模式:0=free-running,1=set-and-forget
bit2 OCIEN 比较中断使能:0=关闭,1=使能
bit1 ENMOD 计数器初始值来源:0=上次关闭时的值,1=从LR加载
bit0 EN EPIT使能:0=关闭,1=使能
#define EPIT_CR_EN          (1 << 0)
#define EPIT_CR_SWR         (1 << 1)    /* 软件复位 */
#define EPIT_CR_ENMOD       (1 << 1)
#define EPIT_CR_OCIEN       (1 << 2)
#define EPIT_CR_RLD         (1 << 3)
#define EPIT_CR_PRESCALAR(x)(((x) & 0xFFF) << 4)
#define EPIT_CR_OM          (3 << 22)   /* 输出模式 */
#define EPIT_CR_CLKSRC      (1 << 24)   /* 时钟源: 01=ipg_clk */

EPITx_SR — 状态寄存器(偏移 0x004)

位域 名称 功能
bit0 OCIF 比较中断标志位:0=无事件,1=有比较事件。写1清零

EPITx_LR — 加载寄存器(偏移 0x008)

32位加载值,计数器每次减到0后从该寄存器重新加载。

EPITx_CMPR — 比较寄存器(偏移 0x00C)

32位比较值,与计数寄存器比较,相等时产生中断。设为0时,计数器减到0即触发中断

EPITx_CNR — 计数寄存器(偏移 0x010)

32位当前计数值,只读。

中断周期计算公式

Tout = ((frac + 1) × value) / Tclk
  • frac:PRESCALAR 分频值
  • value:LR 加载值
  • Tclk:输入时钟频率(Hz)
  • Tout:溢出时间(秒)

示例: 500ms 中断周期

frac = 0 (1分频), Tclk = 66MHz
value = 66000000 / 2 = 33000000
Tout = ((0+1) × 33000000) / 66000000 = 0.5s

1.4 GPT 定时器概述

GPT(General Purpose Timer)通用定时器,比 EPIT 功能更丰富,支持输出比较输入捕获,适合 PWM 输出场景。

GPT 特点:

  • 32位向上计数器
  • 支持3个输出比较通道(OCR1/OCR2/OCR3)
  • 支持2个输入捕获通道(ICR1/ICR2)
  • 可选择时钟源和分频

GPT 核心寄存器

#define GPT_CR      0x000   /* 控制寄存器 */
#define GPT_PR      0x004   /* 预分频寄存器 */
#define GPT_SR      0x008   /* 状态寄存器 */
#define GPT_IR      0x00C   /* 中断使能寄存器 */
#define GPT_OCR1    0x010   /* 输出比较1 */
#define GPT_OCR2    0x014   /* 输出比较2 */
#define GPT_OCR3    0x018   /* 输出比较3 */
#define GPT_ICR1    0x01C   /* 输入捕获1 */
#define GPT_ICR2    0x020   /* 输入捕获2 */
#define GPT_CNT     0x024   /* 计数器 */

GPT 初始化示例

void gpt1_init(void)
{
    /* 复位GPT1 */
    GPT1_CR |= GPT_CR_SWR;
    while (GPT1_CR & GPT_CR_SWR) {}

    /* 配置GPT1: 选择ipg_clk, 使能GPT1 */
    GPT1_CR = 0;
    GPT1_CR |= (1 << 16);              /* 选择ipg_clk */
    GPT1_CR |= GPT_CR_EN;

    /* 预分频: 66MHz/66 = 1MHz */
    GPT1_PR = 65;

    /* 使能OC1比较中断 */
    GPT1_IR |= (1 << 0);

    /* 比较值: 1MHz下1000000=1秒中断 */
    GPT1_OCR1 = 1000000;
}

EPIT vs GPT 对比

特性 EPIT GPT
计数方向 向下 向上
核心功能 周期性中断 PWM/捕获/比较
输出比较通道 无(仅CMPR) 3个(OCR1/OCR2/OCR3)
输入捕获 2个(ICR1/ICR2)
工作模式 set-and-forget / free-running 多种模式
典型用途 精确延时、周期中断 PWM输出、脉冲计数
配置复杂度 简单 较复杂

选型建议: 简单定时/周期中断用 EPIT,需要 PWM 或输入捕获用 GPT。


1.5 定时器实验源码

头文件 bsp_epittimer.h

#ifndef _BSP_EPITTIMER_H
#define _BSP_EPITTIMER_H
#include "imx6ul.h"

void epit1_init(unsigned int frac, unsigned int value);
void epit1_irqhandler(void);

#endif

驱动文件 bsp_epittimer.c

#include "bsp_epittimer.h"
#include "bsp_int.h"
#include "bsp_led.h"

/*
 * @description    : 初始化EPIT定时器
 *                   EPIT定时器是32位向下计数器,时钟源使用ipg=66MHz
 * @param - frac   : 分频值,范围为0~4095,分别对应1~4096分频
 * @param - value  : 倒计数值(加载寄存器值)
 */
void epit1_init(unsigned int frac, unsigned int value)
{
    if (frac > 0xFFF)
        frac = 0xFFF;

    EPIT1->CR = 0;  /* 先清零CR寄存器 */

    /*
     * CR寄存器配置:
     * bit25:24 = 01  时钟源选择Peripheral clock=66MHz
     * bit15:4  = frac 分频值
     * bit3     = 1   set-and-forget模式,计数器到0从LR重载
     * bit2     = 1   比较中断使能
     * bit1     = 1   初始计数值来源于LR寄存器值
     * bit0     = 0   先关闭EPIT1
     */
    EPIT1->CR = (1 << 24) | (frac << 4) | (1 << 3) | (1 << 2) | (1 << 1);
    EPIT1->LR = value;   /* 加载寄存器值 */
    EPIT1->CMPR = 0;     /* 比较寄存器为0,减到0触发中断 */

    /* 使能GIC中对应的中断 */
    GIC_EnableIRQ(EPIT1_IRQn);

    /* 注册中断服务函数 */
    system_register_irqhandler(EPIT1_IRQn,
                               (system_irq_handler_t)epit1_irqhandler,
                               NULL);
    EPIT1->CR |= (1 << 0);  /* 使能EPIT1 */
}

/*
 * @description   : EPIT中断处理函数
 */
void epit1_irqhandler(void)
{
    static unsigned char state = 0;
    state = !state;

    if (EPIT1->SR & (1 << 0))       /* 判断比较事件发生 */
    {
        led_switch(LED0, state);     /* 定时器周期到,反转LED */
    }
    EPIT1->SR |= (1 << 0);          /* 写1清除中断标志位 */
}

main.c

#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_epittimer.h"

int main(void)
{
    int_init();                     /* 初始化中断(一定要最先调用!) */
    imx6u_clkinit();                /* 初始化系统时钟 */
    clk_enable();                   /* 使能所有的时钟 */
    led_init();                     /* 初始化LED */
    beep_init();                    /* 初始化BEEP */
    key_init();                     /* 初始化KEY */

    /* 初始化EPIT1: 1分频, 计数值66000000/2, 定时周期500ms */
    epit1_init(0, 66000000 / 2);

    while (1)
    {
        delay(500);
    }
    return 0;
}

关键分析:

  • 第22行:epit1_init(0, 66000000/2) → 分频值0(1分频),LR=33000000
  • 时钟源 ipg_clk=66MHz,中断周期 = 33000000 / 66000000 = 500ms
  • 中断处理函数 epit1_irqhandler 中翻转 LED0,实现 500ms 闪烁

1.6 定时器消抖

消抖原理

按键机械结构在按下/松开瞬间会产生 5~20ms 的电平抖动。传统做法是用软件延时等待抖动结束,但存在两个问题:

  1. 延时函数空跑 CPU,浪费处理器性能
  2. 中断中不能使用延时函数,违反中断快进快出原则

定时器消抖流程:

按键按下(下降沿)
    │
    ▼
触发GPIO中断 ──→ 重启定时器(10ms)
                    │
    ┌───────────────┘
    │
    ▼
定时器计时中...  ←── 抖动期间每次GPIO中断都重启定时器
    │
    ▼(10ms内无新GPIO中断)
触发定时器中断
    │
    ▼
读取GPIO电平 ──→ 仍为低?──→ 有效按键,执行处理
                    │
                    └──→ 高电平?→ 无效,忽略

消抖时序图:

电平:  __|--|__|--|__|____________________
       t1 t2 t3   t4(稳定按下)
       ↑  ↑  ↑
       GPIO中断触发定时器
       每次都重启定时器
              ↑
              t3启动的定时器完整运行10ms
              → t4时刻触发定时器中断 → 读取按键

t1~t3 为抖动期(<10ms),每次 GPIO 中断都重启定时器。只有 t3 时刻启动的定时器能完整运行 10ms 并触发中断,此时按键已稳定。

消抖实验源码

bsp_keyfilter.c:

#include "bsp_key.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_beep.h"
#include "bsp_keyfilter.h"

/*
 * @description  : 按键初始化(带消抖)
 */
void filterkey_init(void)
{
    gpio_pin_config_t key_config;

    /* 1、初始化IO:UART1_CTS -> GPIO1_IO18 */
    IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);

    /* 2、初始化GPIO为中断模式:下降沿触发 */
    key_config.direction = kGPIO_DigitalInput;
    key_config.interruptMode = kGPIO_IntFallingEdge;
    key_config.outputLogic = 1;
    gpio_init(GPIO1, 18, &key_config);

    /* 3、使能GPIO中断,注册中断处理函数 */
    GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
    system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
        (system_irq_handler_t)gpio1_16_31_irqhandler, NULL);

    gpio_enableint(GPIO1, 18);              /* 使能GPIO1_IO18中断 */
    filtertimer_init(66000000 / 100);       /* 初始化定时器, 10ms */
}

/*
 * @description  : 初始化消抖定时器,默认关闭
 * @param - value: EPIT计数值
 */
void filtertimer_init(unsigned int value)
{
    EPIT1->CR = 0;
    EPIT1->CR = (1 << 24) | (1 << 3) | (1 << 2) | (1 << 1);
    EPIT1->LR = value;
    EPIT1->CMPR = 0;

    GIC_EnableIRQ(EPIT1_IRQn);
    system_register_irqhandler(EPIT1_IRQn,
        (system_irq_handler_t)filtertimer_irqhandler, NULL);
}

/*
 * @description  : 关闭定时器
 */
void filtertimer_stop(void)
{
    EPIT1->CR &= ~(1 << 0);    /* 清除EN位,关闭定时器 */
}

/*
 * @description  : 重启定时器
 * @param - value: EPIT计数值
 */
void filtertimer_restart(unsigned int value)
{
    EPIT1->CR &= ~(1 << 0);    /* 先关闭定时器 */
    EPIT1->LR = value;          /* 设置新的计数值 */
    EPIT1->CR |= (1 << 0);     /* 重新打开定时器 */
}

/*
 * @description  : 定时器中断处理函数(消抖完成后的处理)
 */
void filtertimer_irqhandler(void)
{
    static unsigned char state = OFF;

    if (EPIT1->SR & (1 << 0))           /* 比较事件发生 */
    {
        filtertimer_stop();              /* 关闭定时器 */
        if (gpio_pinread(GPIO1, 18) == 0)   /* 读取按键值,仍按下 */
        {
            state = !state;
            beep_switch(state);          /* 反转蜂鸣器 */
        }
    }
    EPIT1->SR |= (1 << 0);              /* 清除中断标志位 */
}

/*
 * @description  : GPIO中断处理函数(按键触发)
 */
void gpio1_16_31_irqhandler(void)
{
    filtertimer_restart(66000000 / 100); /* 重启定时器10ms */
    gpio_clearintflags(GPIO1, 18);       /* 清除GPIO中断标志 */
}

消抖关键点总结:

要素 说明
消抖时间 10ms(66000000/100 = 660000个时钟周期)
GPIO中断 每次按键边沿都重启定时器
定时器中断 10ms内无新按键时触发,确认有效按键
重启机制 filtertimer_restart 先关再开,重置计数器

Part 2: 看门狗

2.1 看门狗概述

看门狗(Watchdog Timer)是一种硬件定时器,用于在软件跑飞或死锁时自动复位系统。

工作原理:

程序正常运行:
    喂狗 → 喂狗 → 喂狗 → 喂狗 → ... (周期性重置计数器)

程序跑飞/死锁:
    喂狗 → (无喂狗)→ (无喂狗)→ 超时 → 系统复位

应用场景:

  • 工业控制系统防死机
  • 嵌入式设备无人值守运行
  • 汽车电子、医疗设备等安全关键系统
  • Linux 系统中由内核或应用层定期喂狗

2.2 看门狗寄存器

WDOG_WCR — 看门狗控制寄存器(偏移 0x000)

位域 名称 功能
bit0 WDZST 停止计数:1=在调试/低功耗模式停止
bit1 WDBG 调试模式:1=调试模式停止计数
bit2 WDE 看门狗使能:0=禁止,1=使能
bit3 WDT 定时器复位使能:1=超时产生复位
bit4 SRS 软件复位:写0产生复位
bit5 WDA 软件看门狗:写1暂停看门狗
bit8 WDHO 超时输出高
bit11 WDTOP 超时周期选择
bit15:8 WCR[15:8] 超时时间配置位

WDOG_WSR — 服务寄存器(偏移 0x002)

喂狗寄存器。必须依次写入 0x55550xAAAA 才能完成一次喂狗操作。

WDOG_WRSR — 复位状态寄存器(偏移 0x004)

记录复位原因:看门狗超时复位、软件复位等。


2.3 看门狗实验源码

#define WDOG1_BASE      0x020BC000
#define WDOG1_WCR       (*(volatile unsigned short *)(WDOG1_BASE + 0x000))
#define WDOG1_WSR       (*(volatile unsigned short *)(WDOG1_BASE + 0x002))
#define WDOG1_WRSR      (*(volatile unsigned short *)(WDOG1_BASE + 0x004))

/* 看门狗初始化 */
void wdog_init(void)
{
    /* 1、关闭看门狗(先配置再使能) */
    WDOG1_WCR = 0;

    /* 2、配置超时时间
     * bit15:8 = WCR[15:8],超时时间 = 2^14 / 66MHz ≈ 0.25秒
     * bit2 WDE = 1 使能看门狗
     * bit3 WDT = 1 超时产生复位
     */
    WDOG1_WCR |= (5 << 8);          /* 超时配置 */
    WDOG1_WCR |= (1 << 2);          /* 使能看门狗 */
    WDOG1_WCR |= (1 << 3);          /* 超时复位使能 */
}

/* 喂狗函数:依次写入0x5555和0xAAAA */
void wdog_feed(void)
{
    WDOG1_WSR = 0x5555;              /* 第一次写入 */
    WDOG1_WSR = 0xAAAA;              /* 第二次写入 */
}

看门狗配置要点:

操作 说明
初始化 先清零WCR,配置超时时间和使能位
喂狗 必须先写0x5555再写0xAAAA,顺序不能错
超时时间 由WCR[15:8]位控制,与看门狗时钟源频率相关
注意 使能后必须在主循环中定期喂狗,否则系统会复位

2.4 跨平台定时器对比

特性 IMX6ULL STM32F4 RK3568
基本定时器 EPIT (32位) TIM6/TIM7 (16位) 系统定时器
通用定时器 GPT (32位) TIM2~TIM5 (32位) 多种
高级定时器 TIM1/TIM8 (16位)
PWM输出 GPT TIM1~TIM14 PWM子系统
输入捕获 GPT TIM1~TIM14 PWM子系统
看门狗 WDOG1/WDOG2 IWDG(独立)/WWDG(窗口) WDOG
看门狗喂狗 0x5555+0xAAAA IWDG: 写KR=0xAAAA 写服务寄存器
时钟频率 ipg_clk=66MHz APB1=42MHz/APB2=84MHz 多时钟域
中断控制器 GIC NVIC GIC
计数方向 EPIT向下/GPT向上 多数向上 依外设而异

2.5 面试精选

Q1: EPIT 和 GPT 有什么区别?如何选择?

考察点: 定时器架构理解

答案:

维度 EPIT GPT
核心设计 周期中断专用 通用(PWM/捕获/比较)
计数方向 向下 向上
比较通道 仅1个(CMPR) 3个(OCR1/2/3)
输入捕获 2个(ICR1/2)
配置复杂度 简单 复杂

选择原则: 只需要周期中断/精确延时 → EPIT;需要 PWM 输出或输入捕获 → GPT。Linux 驱动开发中,EPIT 常用于实现 udelay/mdelay 等延时函数。


Q2: 如何实现精确的微秒级延时?

考察点: 定时器应用

答案:

  1. 使用 EPIT 配置为 1MHz(66分频:66MHz/66=1MHz,每个时钟周期=1μs)
  2. 设置 EPIT 为 free-running 模式,LR=0xFFFFFFFF
  3. 读取 EPIT1_CNR 获取当前计数值
  4. 循环读取计数值差值,直到达到目标延时

    /* 伪代码 */
    void delay_us(unsigned int us) {
    unsigned int start = EPIT1_CNR;
    while ((start - EPIT1_CNR) < us) {}  /* 向下计数,差值即μs数 */
    }
    

关键注意事项:

  • 必须使用硬件定时器,不能用软件循环(受编译器优化影响)
  • free-running 模式下需注意计数器溢出处理
  • 需要关闭中断以保证延时精度

Q3: 看门狗的喂狗操作为什么需要写两次特定值?

考察点: 看门狗安全机制

答案:

写入 0x5555 → 0xAAAA 的序列是 IMX6ULL 看门狗的安全握手协议。设计目的:

  1. 防止误操作:程序跑飞时偶然写入错误值不会意外喂狗
  2. 确认软件可控:必须按特定顺序写入两个不同值,证明软件处于正常控制状态
  3. 防止竞态:多任务系统中避免多次喂狗操作交叉

如果只写入一个值或写入顺序错误,看门狗不会被重置,超时后系统将复位。这比简单的"写任意值"方案更安全。


Q4: 定时器消抖和软件延时消抖有什么本质区别?

考察点: 消抖方案对比

答案:

维度 软件延时消抖 定时器消抖
CPU利用率 100%空跑(浪费) CPU可做其他任务
中断兼容性 不能在中断中使用 可在中断驱动架构中使用
精度 受编译器优化影响 硬件定时器,精度高
实现复杂度 简单 较复杂(需管理定时器状态)
Linux驱动 不使用 Linux标准做法

Linux 内核的按键驱动正是使用定时器消抖方案。核心思想:GPIO 中断负责触发,定时器中断负责延时确认,两者解耦。


Q5: IMX6ULL 有两个 EPIT 和两个 GPT,裸机开发中如何合理分配?

考察点: 资源规划

答案:

典型分配方案:

定时器 推荐用途 理由
EPIT1 系统延时基准 提供 delay_us/delay_ms
EPIT2 应用定时(如消抖) 独立于延时基准,互不干扰
GPT1 PWM输出(如舵机控制) GPT支持PWM功能
GPT2 输入捕获/脉冲计数 GPT支持捕获功能

关键原则:

  • EPIT 用于纯定时场景,简单高效
  • GPT 用于需要 PWM/捕获的场景
  • 每个定时器独立使用,避免资源共享冲突
  • 中断号不同(EPIT1_IRQn vs EPIT2_IRQn),可独立配置优先级

代码来源: 裸机例程 10_epit_timer (EPIT定时器), 11_key_filter (定时器消抖) 最后更新: 2026-09-17