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 这种轻活可以,别在里面写 printf、delay 这种耗时操作,否则会拖累整个系统响应。
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 睡眠时继续计时,定时把系统唤醒,做省电的间歇采集设备。