HC32 Timer0 定时中断 — 双通道周期翻转 LED

HC32F460 Timer0 实验:ChannelA 用异步 XTAL32、ChannelB 用同步 PCLK1,各自定时触发中断翻转 LED,演示定时中断与 HC32 中断注册流程。

Timer0 定时中断:双通道周期翻转 LED

源码路径:example/HC32F460JEUA/3.timer0/BaseTimer/source/main.c

1. 功能简介

上一个 GPIO 例子里,我们用 Ddl_Delay1ms(500) 死等 500 毫秒来闪灯。这么做 CPU 在等待期间什么也干不了,纯属浪费。

这个例子换个高级做法:用 Timer0 定时器来控制时间。定时器自己在后台数数,数到设定值就触发一次中断,在中断函数里翻转 LED。整个过程 CPU 不用等待,可以去干别的事——时间到了定时器自然会「叫」它。

具体做了什么:

  • ChannelA(A 通道):用异步模式(外部 32.768kHz 晶振),定时约 0.5 秒,中断里翻转绿灯
  • ChannelB(B 通道):用同步模式(内部 PCLK1 时钟),定时约 0.5 秒,中断里翻转蓝灯

两个通道独立运行,各自闪各自的灯,演示 Timer0 两种时钟模式可以同时工作。

顺便说 PWM:如果把「翻转 LED」改成「翻转一个输出引脚」,Timer0 定时中断产生的就是一个固定频率的方波——这就是软件方波/软件 PWM 的雏形。真正的硬件 PWM(占空比可调、不占 CPU)要用 TimerA/TimerB,思路一脉相承。


2. 硬件准备

用 HC32F460JEUA KIT 开发板,板载 LED,无需接线

  • 绿灯:PA1(ChannelA 控制)
  • 蓝灯:PA2(ChannelB 控制)

异步模式用到外部 32.768kHz 晶振(XTAL32)。开发板通常已经焊好这颗表晶。如果你用的是自制板、没焊 XTAL32,那 A 通道会不工作(数数没时钟源),此时只有蓝灯闪。

1
2
3
4
5
6
7
   HC32F460 开发板
 ┌───────────────────┐
 │  PA1 ──▶ 绿灯      │  ← Timer0 ChannelA(异步 XTAL32)翻转
 │  PA2 ──▶ 蓝灯      │  ← Timer0 ChannelB(同步 PCLK1)翻转
 │                   │
 │  XTAL32 (32.768k) │  ← 异步模式的时钟源,板载表晶
 └───────────────────┘

3. 完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
#include "hc32_ddl.h"

/* Timer0 单元和中断源定义 */
#define TMR_UNIT       (M4_TMR02)          // 用 Timer0 的第 2 个单元
#define TMR_INI_GCMA   (INT_TMR02_GCMA)    // A 通道比较匹配中断源
#define TMR_INI_GCMB   (INT_TMR02_GCMB)    // B 通道比较匹配中断源

/* 开 Timer0_2 外设时钟的宏 */
#define ENABLE_TMR0()  (PWC_Fcg2PeriphClockCmd(PWC_FCG2_PERIPH_TIM02, Enable))

__IO uint16_t u16cmp = 0u;   // __IO = volatile,防编译器把它优化掉
__IO uint16_t u16cnt = 0u;

/* ---------- A 通道中断回调:翻转绿灯 ---------- */
void Timer0A_CallBack(void)
{
    BSP_LED_Toggle(LED_GREEN);
}

/* ---------- B 通道中断回调:翻转蓝灯 ---------- */
void Timer0B_CallBack(void)
{
    BSP_LED_Toggle(LED_BLUE);
}

int32_t main(void)
{
    stc_tim0_base_init_t stcTimerCfg;      // Timer0 基础配置结构体
    stc_irq_regi_conf_t  stcIrqRegiConf;   // 中断注册配置结构体
    stc_port_init_t      stcPortInit;

    uint32_t u32Pclk1;
    stc_clk_freq_t stcClkTmp;
    uint32_t u32tmp;

    /* 三个结构体都先清零 */
    MEM_ZERO_STRUCT(stcTimerCfg);
    MEM_ZERO_STRUCT(stcIrqRegiConf);
    MEM_ZERO_STRUCT(stcPortInit);

    BSP_CLK_Init();          // 系统时钟初始化
    BSP_LED_Init();          // LED 初始化

    /* 读取当前 PCLK1 频率(同步模式算比较值要用)*/
    CLK_GetClockFreq(&stcClkTmp);
    u32Pclk1 = stcClkTmp.pclk1Freq;

    CLK_Xtal32Cmd(Enable);   // 使能外部 32.768kHz 晶振(异步模式的时钟源)

    ENABLE_TMR0();           // 开 Timer0 外设时钟

    /* ========== 配置 A 通道:异步模式,XTAL32 时钟 ========== */
    stcTimerCfg.Tim0_CounterMode      = Tim0_Async;      // 异步
    stcTimerCfg.Tim0_AsyncClockSource = Tim0_XTAL32;     // 用 32.768kHz 晶振
    stcTimerCfg.Tim0_ClockDivision    = Tim0_ClkDiv4;    // 4 分频
    /* 比较值:32000/4/2-1,定时约 0.5 秒 */
    stcTimerCfg.Tim0_CmpValue         = (uint16_t)(32000u/4u/2u - 1u);
    TIMER0_BaseInit(TMR_UNIT, Tim0_ChannelA, &stcTimerCfg);

    TIMER0_IntCmd(TMR_UNIT, Tim0_ChannelA, Enable);      // 使能 A 通道中断

    /* --- 注册 A 通道中断(HC32 特色四件套)--- */
    stcIrqRegiConf.enIRQn      = Int001_IRQn;            // 用向量号 1
    stcIrqRegiConf.enIntSrc    = TMR_INI_GCMA;           // 中断源 = A 通道比较匹配
    stcIrqRegiConf.pfnCallback = &Timer0A_CallBack;      // 绑定回调
    enIrqRegistration(&stcIrqRegiConf);                  // 注册
    NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn);         // 清挂起
    NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIORITY_15);  // 优先级
    NVIC_EnableIRQ(stcIrqRegiConf.enIRQn);               // 使能 NVIC

    /* ========== 配置 B 通道:同步模式,PCLK1 时钟 ========== */
    stcTimerCfg.Tim0_CounterMode      = Tim0_Sync;       // 同步
    stcTimerCfg.Tim0_SyncClockSource  = Tim0_Pclk1;      // 用 PCLK1
    stcTimerCfg.Tim0_ClockDivision    = Tim0_ClkDiv1024; // 1024 分频
    /* 比较值:PCLK1/1024/2-1,定时约 0.5 秒 */
    stcTimerCfg.Tim0_CmpValue         = (uint16_t)(u32Pclk1/1024ul/2ul - 1ul);
    TIMER0_BaseInit(TMR_UNIT, Tim0_ChannelB, &stcTimerCfg);

    TIMER0_IntCmd(TMR_UNIT, Tim0_ChannelB, Enable);

    /* --- 注册 B 通道中断 --- */
    stcIrqRegiConf.enIRQn      = Int002_IRQn;            // 换个向量号 2
    stcIrqRegiConf.enIntSrc    = TMR_INI_GCMB;           // 中断源 = B 通道
    stcIrqRegiConf.pfnCallback = &Timer0B_CallBack;
    enIrqRegistration(&stcIrqRegiConf);
    NVIC_ClearPendingIRQ(stcIrqRegiConf.enIRQn);
    NVIC_SetPriority(stcIrqRegiConf.enIRQn, DDL_IRQ_PRIORITY_15);
    NVIC_EnableIRQ(stcIrqRegiConf.enIRQn);

    /* ========== 启动两个通道 ========== */
    TIMER0_Cmd(TMR_UNIT, Tim0_ChannelA, Enable);
    TIMER0_Cmd(TMR_UNIT, Tim0_ChannelB, Enable);

    while(1)
    {
        /* 读 B 通道计数值(同步模式可直接读)*/
        u16cnt = TIMER0_GetCntReg(TMR_UNIT, Tim0_ChannelB);
        u16cmp = TIMER0_GetCntReg(TMR_UNIT, Tim0_ChannelB);

        /* 读 A 通道计数值(异步模式要先停再读,读完再开)*/
        TIMER0_Cmd(TMR_UNIT, Tim0_ChannelA, Disable);
        u16cnt = TIMER0_GetCntReg(TMR_UNIT, Tim0_ChannelA);
        u16cmp = TIMER0_GetCntReg(TMR_UNIT, Tim0_ChannelA);
        TIMER0_Cmd(TMR_UNIT, Tim0_ChannelA, Enable);

        /* 简单空转延时 */
        u32tmp = 0xFFFFFul;
        while(u32tmp--){ ; }
    }
}

4. 代码分段讲解

4.1 中断回调放在最前面

1
2
void Timer0A_CallBack(void) { BSP_LED_Toggle(LED_GREEN); }
void Timer0B_CallBack(void) { BSP_LED_Toggle(LED_BLUE); }

这两个函数就是「闹钟响了要做的事」。Timer0 A 通道数到点了 → 系统自动调用 Timer0A_CallBack → 翻转绿灯。B 通道同理翻转蓝灯。

中断回调里要尽量短、尽量快——只翻转个 LED 这种轻活可以,别在里面写 printfdelay 这种耗时操作,否则会拖累整个系统响应。

4.2 A 通道:异步模式

1
2
3
4
stcTimerCfg.Tim0_CounterMode      = Tim0_Async;
stcTimerCfg.Tim0_AsyncClockSource = Tim0_XTAL32;   // 32.768kHz 晶振
stcTimerCfg.Tim0_ClockDivision    = Tim0_ClkDiv4;
stcTimerCfg.Tim0_CmpValue         = (uint16_t)(32000u/4u/2u - 1u);

异步模式用外部低速晶振数数。比较值 32000/4/2-1 = 3999

  • 32000 是晶振频率(约 32.768k,代码取整算)
  • /4 是分频
  • /2 是因为「翻转」需要两次中断才完成一个亮灭周期,所以半周期用 0.5 秒
  • -1 是因为从 0 数起,数到 3999 其实是 4000 个数

用异步模式前必须先 CLK_Xtal32Cmd(Enable) 把这颗晶振打开。

4.3 B 通道:同步模式

1
2
3
4
stcTimerCfg.Tim0_CounterMode     = Tim0_Sync;
stcTimerCfg.Tim0_SyncClockSource = Tim0_Pclk1;
stcTimerCfg.Tim0_ClockDivision   = Tim0_ClkDiv1024;
stcTimerCfg.Tim0_CmpValue        = (uint16_t)(u32Pclk1/1024ul/2ul - 1ul);

同步模式用内部 PCLK1(频率比 32k 高得多,所以要 1024 大分频才不至于溢出)。比较值这次是动态算的——先用 CLK_GetClockFreq() 读出当前 PCLK1 的实际频率,再套公式。这样即使换了时钟配置,定时时间也能自动保持 0.5 秒,比写死一个数字更稳。

4.4 中断注册——HC32 的独特之处

1
2
3
4
5
6
7
stcIrqRegiConf.enIRQn      = Int001_IRQn;       // A 用向量 1
stcIrqRegiConf.enIntSrc    = TMR_INI_GCMA;      // 绑定 A 通道中断源
stcIrqRegiConf.pfnCallback = &Timer0A_CallBack; // 绑定回调
enIrqRegistration(&stcIrqRegiConf);
NVIC_ClearPendingIRQ(...);
NVIC_SetPriority(..., DDL_IRQ_PRIORITY_15);     // 15 是最低优先级
NVIC_EnableIRQ(...);

这是 HC32 和 STM32 最大的不同。STM32 中断函数名固定;HC32 要你手动把中断源绑到某个向量号。注意 A 用 Int001_IRQn、B 用 Int002_IRQn——两个中断必须用不同的向量号,否则会互相冲突。

流程口诀:注册 → 清挂起 → 设优先级 → 使能 NVIC,四步一个都不能少。

4.5 主循环里读计数值的小细节

1
2
3
4
/* 异步通道要先停再读 */
TIMER0_Cmd(TMR_UNIT, Tim0_ChannelA, Disable);
u16cnt = TIMER0_GetCntReg(TMR_UNIT, Tim0_ChannelA);
TIMER0_Cmd(TMR_UNIT, Tim0_ChannelA, Enable);

主循环读计数值只是为了演示怎么读。异步模式有个坑:因为它用的是和 CPU 不同步的外部时钟,直接读计数寄存器可能读到「正在变化中」的错误值,所以要先停计数、读完再启动。同步模式没这问题,可以直接读。


5. 实验现象

烧录后:

  • 绿灯每约 1 秒闪一次(A 通道异步定时驱动)。
  • 蓝灯每约 1 秒闪一次(B 通道同步定时驱动)。

两个灯独立闪烁。和 GPIO 例子肉眼看着类似,但本质完全不同:这里 CPU 没有死等延时,闪灯完全由定时器中断驱动,CPU 主循环是空闲的(可以拿去做别的事)。

如果你的板子没有 XTAL32 晶振,绿灯不会闪,只有蓝灯闪。


6. 常见问题

Q1:绿灯不闪,只有蓝灯闪?

  • 异步模式依赖外部 32.768kHz 晶振。板子没焊 XTAL32、或 CLK_Xtal32Cmd(Enable) 没生效,A 通道就没时钟数不了数。

Q2:两个灯都不闪?

  • 检查 PWC_Fcg2PeriphClockCmd(PWC_FCG2_PERIPH_TIM02, Enable) 时钟开了没。
  • 检查中断注册是不是漏了某一步(尤其 NVIC_EnableIRQ)。

Q3:中断进不去 / 回调不执行?

  • A、B 两个通道的向量号是不是用了不同的(Int001 和 Int002)?用了同一个会冲突。
  • enIntSrc 是不是填对了(A 用 GCMA,B 用 GCMB)?

Q4:闪的频率不对,不是 1 秒?

  • 同步模式的比较值依赖 PCLK1 频率。如果 BSP_CLK_Init() 没调、或 CLK_GetClockFreq 读的值不对,算出来的比较值就偏。

Q5:__IO 是什么?能删吗?

  • __IO 就是 volatile。它告诉编译器「这个变量随时可能被中断改动,不要优化」。中断和主循环共享的变量都要加,删了可能出诡异 bug。

7. 小结

  • Timer0 定时中断 = 让定时器在后台数数,数到点自动触发中断,CPU 不用死等
  • 两种模式:异步(XTAL32,省电)同步(PCLK1,高速),可同时跑。
  • 定时时间靠比较值 + 分频算,公式:(比较值+1)×分频÷频率。
  • HC32 中断要手动注册,多个中断用不同向量号
  • 中断和主循环共享的变量记得加 __IO(volatile)。

8. 拓展应用

定时器是几乎所有正经项目的时间基准:

  • 精确定时任务:每 1 秒采集温度、每 100ms 读一次传感器——把 Toggle 换成你的任务函数即可。
  • 软件定时器框架:用一个定时器中断产生 1ms「滴答」,在中断里给多个计数器 +1,就能同时管理几十个不同周期的任务(这就是 RTOS 系统节拍 SysTick 的思想)。
  • 按键消抖 / 扫描:每 10ms 定时扫描一次按键状态,比在主循环里瞎测稳得多。
  • 产生方波 / 简易 PWM:在中断里翻转引脚就是方波发生器;想要占空比可调的真 PWM,进阶到 TimerA/TimerB。
  • 超时检测:启动定时器,若在规定时间内没等到某事件(如串口没收到回复),就判定超时——通信协议里非常常用。
  • 低功耗唤醒:异步模式 + XTAL32 能在 CPU 睡眠时继续计时,定时把系统唤醒,做省电的间歇采集设备。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计