ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GD32E230 TIMER5定时器中断实现1秒LED闪烁详解

GD32E230 TIMER5定时器中断实现1秒LED闪烁详解 1. 项目缘起与整体设计思路拿到 GD32E230 这块片子的时候我第一反应是这玩意儿跟 STM32F0 系列 pin-to-pin 兼容主频跑到 72MHz价格还便宜不少拿来替换一些老项目里的进口料再合适不过。但真上手才发现网上关于 GD32E230 的中文资料远不如 STM32 那么铺天盖地尤其是定时器这块很多人直接照搬 STM32 的代码结果要么中断进不去要么闪烁频率差得离谱。我自己就踩过这个坑——第一次用 TIMER5 做 1 秒 LED 闪烁代码写完了灯就是不亮查了半天才发现是时钟树配置和 STM32 有细微差别。所以这篇内容我想把 GD32E230 上用 TIMER5 实现 1 秒 LED 闪烁中断这件事从头到尾讲透。GD32E230是兆易创新基于 ARM Cortex-M23 内核的 32 位微控制器TIMER5是它内部的一个基本定时器LED闪烁是最直观的验证手段而中断配置则是整个功能的核心机制。这套东西适合谁看如果你刚接触 GD32 系列或者从 STM32 转过来发现代码跑不通又或者你只是想找个靠谱的定时器中断模板直接抄作业那这篇内容应该能帮你省下不少查手册的时间。整体设计思路其实不复杂TIMER5 作为一个基本定时器挂载在 APB1 总线上我们需要配置它的预分频器PSC和自动重装载寄存器CAR让它在每 1 秒产生一次更新中断然后在中断服务函数里翻转 LED 引脚的电平。听起来简单但里面有几个关键点时钟源频率到底是多少、预分频和重装载值怎么算、中断优先级怎么设、NVIC 怎么使能。这些细节如果有一个没对上灯就不会按你预期的节奏闪。我选择 TIMER5 而不是 TIMER0 或 TIMER2原因也很实际TIMER0 是高级定时器功能强但配置复杂用来点灯属于杀鸡用牛刀TIMER2 是通用定时器支持输入捕获、输出比较、PWM 等留着做其他功能更合适TIMER5 是基本定时器功能最纯粹就是计数和中断拿来练手或者做简单定时任务刚刚好。而且 TIMER5 的中断向量在启动文件里是独立的不会跟其他外设冲突调试起来也清爽。2. 核心细节解析与实操要点2.1 TIMER5 的时钟来源与频率计算这是整个项目最容易翻车的地方。GD32E230 的时钟树跟 STM32F0 很像但细节有差异。TIMER5 挂在 APB1 总线上而 APB1 的时钟又来自系统时钟CK_SYS经过 AHB 预分频器和 APB1 预分频器分频得到。关键点在于当 APB1 预分频系数不为 1 时定时器的时钟频率会是 APB1 时钟的 2 倍。这个规则跟 STM32 是一样的但很多人配时钟的时候忘了这一茬。假设我们用内部 8MHz 的 IRC8M 作为系统时钟源经过 PLL 倍频到 72MHz 作为 CK_SYS。然后 AHB 不分频APB1 也不分频那么 APB1 时钟就是 72MHz。因为 APB1 预分频系数为 1所以 TIMER5 的时钟频率就是 72MHz。但如果你把 APB1 设成 2 分频APB1 时钟变成 36MHz而 TIMER5 的时钟会变成 72MHz36MHz × 2。这个细节一定要在代码里确认清楚否则算出来的定时时间会差一倍。我一般会在初始化 TIMER5 之前先把时钟配置函数里的分频系数确认一遍。如果你用的是 GD32 官方固件库rcu_clock_freq_get(CK_APB1)这个函数可以直接读出 APB1 的频率但注意它返回的是 APB1 总线频率不是定时器时钟频率。定时器时钟频率需要你自己根据分频系数判断如果 APB1 分频系数是 1定时器时钟等于 APB1 频率否则定时器时钟等于 APB1 频率的 2 倍。2.2 预分频器与重装载值的计算逻辑TIMER5 是一个 16 位的基本定时器预分频器PSC和自动重装载寄存器CAR都是 16 位的所以它们的值范围都是 0 到 65535。定时器的溢出时间计算公式是Tout (PSC 1) × (CAR 1) / Tclk其中 Tclk 是定时器时钟频率。我们要实现 1 秒的定时假设 Tclk 72MHz那么 (PSC 1) × (CAR 1) 72,000,000。这个乘积太大了超过了 16 位寄存器的组合范围最大 65536 × 65536 ≈ 4.29e9其实够用但为了计算方便和精度考虑我们一般会让 PSC 和 CAR 的值尽量均衡。我常用的做法是先确定预分频值让定时器的计数频率降到 1MHz 左右这样 CAR 的值就是 1,000,000 减去 1但 1,000,000 超过了 65535所以不行。那就把计数频率降到 10kHz这样 CAR 100,000 - 1 99,999还是超了。继续降降到 1kHzCAR 1000 - 1 999这个可以。那么 PSC 的值就是 72MHz / 1kHz - 1 72,000 - 1 71,999。但 71,999 也在 16 位范围内小于 65535所以这组参数是可行的PSC 71999CAR 999。不过实际项目中我更喜欢让 PSC 和 CAR 的值都小一点方便调试和计算。比如取 PSC 7199这样计数频率是 10kHzCAR 10000 - 1 9999也在范围内。这样定时时间 (7199 1) × (9999 1) / 72,000,000 7200 × 10000 / 72,000,000 1 秒。这组参数的好处是 CAR 的值是 9999在示波器或者逻辑分析仪上抓波形的时候计数周期看起来更直观。2.3 中断优先级与 NVIC 配置GD32E230 的中断优先级配置跟 STM32 类似但寄存器名称和库函数略有不同。TIMER5 的中断向量是TIMER5_IRQHandler在启动文件startup_gd32e23x.s里已经定义好了弱符号我们只需要在 C 文件里重新实现这个函数就行。NVIC 的配置需要用到nvic_irq_enable()函数这个函数在 GD32 的固件库里有封装参数是中断向量号和优先级。优先级这块我一般会把定时器中断设成中等优先级比如 1 或者 2。因为 LED 闪烁对实时性要求不高设太高了没必要设太低了又怕被其他中断打断导致闪烁不均匀。GD32E230 的中断优先级寄存器是 8 位的但实际只用了高 4 位所以优先级范围是 0 到 15。我通常用 1 或者 2留出 0 给那些对实时性要求极高的中断比如电机控制里的故障保护。还有一个细节TIMER5 的中断使能需要两步。第一步是在 TIMER5 的寄存器里使能更新中断也就是timer_interrupt_enable(TIMER5, TIMER_INT_UP)第二步是在 NVIC 里使能 TIMER5 的中断通道也就是nvic_irq_enable(TIMER5_IRQn, 1, 0)。这两步缺一不可我见过有人只做了第一步结果中断死活进不去查了半天才发现 NVIC 没开。2.4 LED 引脚的选择与驱动方式LED 引脚的选择其实没什么讲究随便找一个 GPIO 就行。我一般用 PA1 或者 PC13因为这两个引脚在很多开发板上都接了 LED方便观察。配置 GPIO 的时候要设成推挽输出模式输出速度设成 2MHz 或者 50MHz 都行点灯而已不需要太高的速度。这里有个小经验如果你用的 LED 是低电平点亮也就是引脚输出低电平时 LED 亮那在中断里翻转电平的时候要注意初始状态。我一般会在初始化的时候先把引脚设成高电平让 LED 灭着然后中断里第一次翻转变成低电平LED 亮第二次翻转变成高电平LED 灭这样闪烁节奏就很清晰。如果你一开始就把引脚设成低电平那上电瞬间 LED 就是亮的第一次中断翻转后灭掉虽然也能闪但给人的感觉是“上电就亮了一下”不太符合“闪烁”的预期。3. 实操过程与核心环节实现3.1 时钟初始化与 TIMER5 时钟使能先看时钟配置。我用的是 GD32 官方固件库系统时钟配置函数一般在system_gd32e23x.c里默认会把 CK_SYS 配到 72MHz。如果你用的是自己写的时钟配置记得确认一下 PLL 的倍频系数和 APB1 的分频系数。我一般会在 main 函数开头加一句rcu_clock_freq_get(CK_SYS)把系统时钟频率读出来通过串口打印或者调试器观察确认是 72MHz 再往下走。TIMER5 的时钟使能函数是rcu_periph_clock_enable(RCU_TIMER5)这个函数会打开 TIMER5 的时钟门控。注意GD32 的外设时钟默认是关闭的不使能的话寄存器写不进去定时器也不会工作。同样GPIO 的时钟也要使能比如rcu_periph_clock_enable(RCU_GPIOA)。这两个使能语句我一般放在 main 函数的最前面确保后续配置能正常进行。rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_TIMER5);3.2 GPIO 初始化与 LED 引脚配置GPIO 初始化用gpio_init()函数参数是端口号、引脚号、模式、速度。推挽输出模式对应GPIO_MODE_OUT_PP速度用GPIO_OSPEED_2MHZ就够了。初始化完之后我习惯先把引脚电平设高让 LED 处于熄灭状态。gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_2MHZ, GPIO_PIN_1); gpio_bit_set(GPIOA, GPIO_PIN_1);这里有个细节gpio_bit_set()和gpio_bit_reset()是 GD32 固件库里的函数分别对应置高和置低。翻转电平可以用gpio_bit_set()和gpio_bit_reset()交替调用也可以用gpio_bit_write()配合读取当前电平来实现。我一般用后者代码更简洁gpio_bit_write(GPIOA, GPIO_PIN_1, (bit_status)(1 - gpio_input_bit_get(GPIOA, GPIO_PIN_1)));这行代码的意思是读取当前引脚电平取反然后写回去。gpio_input_bit_get()返回的是SET或RESET也就是 1 或 0取反后写回就实现了翻转。3.3 TIMER5 参数配置与中断使能TIMER5 的配置分几步先复位定时器然后设置预分频器和重装载值接着设置计数模式最后使能中断和启动定时器。timer_deinit(TIMER5); timer_init(TIMER5, TIMER_PSC_7199, TIMER_CAR_9999, TIMER_CKDIV_DIV1, TIMER_COUNTER_UP); timer_interrupt_enable(TIMER5, TIMER_INT_UP); nvic_irq_enable(TIMER5_IRQn, 1, 0); timer_enable(TIMER5);timer_init()函数的参数依次是定时器编号、预分频值、重装载值、时钟分频系数、计数模式。这里我用的 PSC 7199CAR 9999时钟分频系数是 1计数模式是向上计数。timer_interrupt_enable()使能更新中断nvic_irq_enable()使能 NVIC 通道优先级设为 1子优先级设为 0。最后timer_enable()启动定时器。注意timer_init()里的预分频值和重装载值是直接写入寄存器的不需要减 1。也就是说你传 7199 进去实际的分频系数就是 7199 1 7200。这个跟 STM32 的 HAL 库不太一样HAL 库里传的是实际分频值减 1而 GD32 的固件库传的就是寄存器值。我一开始用 STM32 的习惯传了 7200结果定时时间变成了 1.00014 秒虽然差别不大但如果你做的是精确计时这个误差就不能忽略了。3.4 中断服务函数的编写与 LED 翻转中断服务函数的名称必须跟启动文件里的向量表一致TIMER5 对应的是TIMER5_IRQHandler。函数里首先要判断是不是更新中断然后清除中断标志最后执行 LED 翻转。void TIMER5_IRQHandler(void) { if(timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP) SET) { timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP); gpio_bit_write(GPIOA, GPIO_PIN_1, (bit_status)(1 - gpio_input_bit_get(GPIOA, GPIO_PIN_1))); } }这里有个坑timer_interrupt_flag_get()和timer_interrupt_flag_clear()的参数是TIMER_INT_FLAG_UP而不是TIMER_INT_UP。前者是中断标志后者是中断使能虽然名字很像但用错了函数就编译不过或者行为异常。我见过有人把这两个搞混结果中断标志清不掉程序一直卡在中断里出不来。另外清除中断标志的语句一定要放在 LED 翻转之前或者之后都行但必须在中断服务函数里执行。如果你忘了清除标志中断会一直触发CPU 会一直停在中断服务函数里主循环根本跑不起来。这个现象在调试的时候表现为LED 要么常亮要么常灭不会闪烁而且程序看起来像死机了一样。3.5 完整代码整合与编译下载把上面的代码片段整合到一个完整的 main.c 文件里加上必要的头文件引用就可以编译下载了。我用的开发环境是 Keil MDK芯片包选的是 GigaDevice 的 GD32E23x 系列。编译之前记得在工程设置里把启动文件选对GD32E230 对应的启动文件是startup_gd32e23x.s选错了启动文件会导致中断向量表对不上中断进不去。下载的时候用 GD-Link 或者 J-Link 都行GD32E230 支持 SWD 调试接口。下载完成后复位你应该能看到 LED 以 1 秒的间隔闪烁也就是亮 1 秒、灭 1 秒。如果你手头有逻辑分析仪或者示波器可以抓一下 PA1 引脚的波形应该是一个周期为 2 秒、占空比为 50% 的方波。如果波形周期不对比如变成了 0.5 秒或者 2 秒那多半是时钟频率或者预分频值算错了回头检查一下 APB1 的分频系数和 TIMER5 的时钟频率。4. 常见问题与排查技巧实录4.1 中断进不去怎么办这是最常见的问题没有之一。LED 不闪首先怀疑中断没进去。排查步骤我一般是这样第一步在中断服务函数里加一句 GPIO 翻转或者变量自增然后用调试器看这个变量有没有变化。如果没变化说明中断确实没进去。第二步检查 NVIC 使能函数有没有调用nvic_irq_enable(TIMER5_IRQn, 1, 0)这行代码是不是漏了。第三步检查 TIMER5 的中断使能位有没有置位timer_interrupt_enable(TIMER5, TIMER_INT_UP)是不是写成了其他中断类型。第四步检查定时器有没有启动timer_enable(TIMER5)是不是忘了调用。第五步检查启动文件里的中断向量名是不是TIMER5_IRQHandler如果你用的是自己写的启动文件名字对不上也会导致中断进不去。还有一个隐蔽的坑GD32E230 的 TIMER5 中断向量号是 24但如果你在代码里用了TIMER5_IRQn这个宏它会被展开成正确的数值。但如果你手动写了nvic_irq_enable(24, 1, 0)而实际向量号不是 24那就错了。所以尽量用宏定义不要手写数值。4.2 闪烁频率不对怎么调如果 LED 闪烁的频率跟你预期的差了一倍或者几倍大概率是时钟频率算错了。我整理了一个排查表格按顺序检查排查项可能问题解决方法系统时钟PLL 配置错误CK_SYS 不是 72MHz用rcu_clock_freq_get(CK_SYS)读取确认APB1 分频分频系数不是 1但忘了定时器时钟翻倍确认 APB1 分频系数重新计算 TclkPSC 值传了实际分频值而不是寄存器值GD32 固件库传寄存器值即实际分频值减 1CAR 值传了实际计数值而不是寄存器值同上传实际计数值减 1中断标志忘了清除中断反复触发在中断服务函数里加清除语句举个例子如果你把 PSC 设成了 7200 而不是 7199那么实际分频系数是 7201定时时间变成 7201 × 10000 / 72,000,000 ≈ 1.00014 秒误差很小肉眼看不出来。但如果你把 APB1 设成了 2 分频而 TIMER5 的时钟频率实际上是 72MHz 而不是 36MHz你按 36MHz 算出来的 PSC 和 CAR 就会导致定时时间变成 0.5 秒LED 闪烁频率翻倍这个肉眼就能看出来。4.3 LED 常亮或常灭不闪烁这种情况一般是中断服务函数里出了问题。最常见的原因是忘了清除中断标志导致中断一直触发CPU 一直停在中断里主循环跑不起来LED 的状态就固定在某个值上不变了。另一个可能的原因是 GPIO 翻转的代码写错了比如用了gpio_bit_set()但忘了取反或者gpio_input_bit_get()读的是输入寄存器而不是输出寄存器。GD32 的gpio_input_bit_get()函数读的是输入数据寄存器GPIO_ISTAT而不是输出数据寄存器GPIO_OCTL。在推挽输出模式下如果你没有外部电路影响输入寄存器的值应该跟输出寄存器的值一致。但如果你外部接了上拉或者下拉电阻或者引脚被其他外设复用了读回来的值可能跟输出值不一样导致翻转逻辑出错。稳妥的做法是用一个全局变量记录 LED 的当前状态在中断里翻转这个变量然后根据变量值写 GPIO。volatile uint8_t led_state 0; void TIMER5_IRQHandler(void) { if(timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP) SET) { timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP); led_state !led_state; gpio_bit_write(GPIOA, GPIO_PIN_1, (bit_status)led_state); } }这样写的好处是不依赖 GPIO 输入寄存器的值逻辑更清晰也不怕外部电路干扰。4.4 中断优先级冲突导致闪烁不均匀如果你系统里还有其他中断比如串口接收中断、滴答定时器中断那就要注意优先级配置了。如果 TIMER5 的优先级太低被其他中断频繁打断LED 闪烁的节奏就会不均匀有时候快有时候慢。我一般会把 TIMER5 的优先级设成 1 或者 2比串口中断高一点但比故障保护中断低一点。GD32E230 的中断优先级寄存器是 8 位的但实际只用了高 4 位所以优先级数值范围是 0 到 15。数值越小优先级越高。nvic_irq_enable()函数的第二个参数是抢占优先级第三个参数是子优先级。抢占优先级高的中断可以打断抢占优先级低的中断子优先级只在抢占优先级相同时起作用。对于 LED 闪烁这种应用抢占优先级设 1子优先级设 0 就足够了。4.5 低功耗模式下的定时器行为如果你在做低功耗项目可能会用到睡眠模式或者深度睡眠模式。GD32E230 在深度睡眠模式下APB1 总线的时钟可能会被关闭TIMER5 也会停止工作。如果你希望定时器在低功耗模式下继续运行需要把定时器的时钟源切换到内部低速时钟或者外部低速晶振但 TIMER5 是 APB1 总线上的外设它的时钟源是固定的不能随便切换。所以如果你需要低功耗定时唤醒建议用 RTC 或者独立的看门狗定时器而不是 TIMER5。我在实际项目中遇到过这个问题设备进入深度睡眠后LED 不闪了一开始以为是程序跑飞了后来查手册才发现是 TIMER5 的时钟被关了。解决办法是在进入低功耗之前把 TIMER5 关掉唤醒后重新初始化或者干脆用 RTC 来做定时唤醒。这个坑在电池供电的项目里特别常见如果你有低功耗需求一定要提前规划好定时器的使用策略。4.6 调试技巧与工具推荐调试定时器中断的时候我一般会用这几个手段第一用调试器在中断服务函数里设断点看能不能进去第二用 GPIO 翻转配合示波器或者逻辑分析仪抓波形看周期和占空比第三用串口打印时间戳看两次中断之间的间隔是不是 1 秒。这三种手段各有优劣断点调试最直接但会打断程序运行影响定时精度示波器最直观但需要硬件支持串口打印最灵活但打印本身也会消耗时间可能影响定时精度。我个人的习惯是先用断点确认中断能进去然后用示波器抓波形确认周期最后用串口打印做长期稳定性测试。如果你手头没有示波器可以用一个便宜的 USB 逻辑分析仪几十块钱的那种就够用了抓个 1Hz 的方波绰绰有余。5. 进阶扩展与项目迁移思路5.1 从 1 秒闪烁扩展到多路定时任务学会了 TIMER5 的 1 秒中断你可以很容易地扩展到多路定时任务。比如你想要一个 1 秒的任务和一个 500 毫秒的任务可以用同一个 TIMER5在中断里维护一个计数器每进一次中断计数器加 1然后根据计数器的值判断该执行哪个任务。这种做法的好处是不需要额外的定时器资源缺点是任务的时间精度受中断频率限制。volatile uint32_t tick_count 0; void TIMER5_IRQHandler(void) { if(timer_interrupt_flag_get(TIMER5, TIMER_INT_FLAG_UP) SET) { timer_interrupt_flag_clear(TIMER5, TIMER_INT_FLAG_UP); tick_count; if(tick_count % 1000 0) { /* 1秒任务 */ } if(tick_count % 500 0) { /* 500毫秒任务 */ } } }这种写法在裸机项目里很常见相当于自己实现了一个简易的任务调度器。如果你需要更复杂的调度可以考虑移植一个轻量级的 RTOS比如 FreeRTOS 或者 RT-Thread Nano用系统滴答定时器来做任务切换TIMER5 就可以解放出来做其他事情。5.2 从 TIMER5 迁移到其他定时器如果你需要 PWM 输出、输入捕获或者输出比较功能TIMER5 就不够用了需要迁移到 TIMER2 或者 TIMER0。迁移的时候主要改这几个地方时钟使能函数改成对应的定时器中断向量名改成对应的TIMERx_IRQHandlerNVIC 使能函数改成对应的TIMERx_IRQn。其他配置逻辑基本一致预分频和重装载的计算方法也一样。不过要注意TIMER2 是通用定时器有 4 个独立通道可以同时做 PWM 输出和输入捕获。TIMER0 是高级定时器支持互补输出和死区插入适合做电机控制。迁移之前先想清楚你的应用需要哪些功能别为了省事随便选一个后面发现功能不够再换就麻烦了。5.3 代码移植到 STM32 平台的注意事项如果你想把这份代码移植到 STM32F0 或者 STM32F1 上大部分逻辑是通用的但有几个地方要改第一固件库函数名不一样STM32 的标准外设库或者 HAL 库跟 GD32 的固件库函数名有差异比如rcu_periph_clock_enable()在 STM32 里叫RCC_APB1PeriphClockCmd()第二中断向量名可能不一样STM32F0 的 TIMER5 中断向量是TIM5_IRQHandler注意是 TIM5 不是 TIMER5第三时钟树配置有差异STM32F0 的 APB1 分频规则跟 GD32E230 基本一致但 PLL 配置参数可能不同。我一般会在移植之前先把两个芯片的参考手册对照看一遍重点看时钟树和定时器章节把差异点列出来然后逐条修改。这样虽然前期花的时间多一点但后面调试的时候会省很多事。5.4 实际项目中的经验教训最后分享几个我在实际项目中踩过的坑。第一个坑有一次我做了一个 1 秒闪烁的 LED测试的时候发现灯闪得特别快查了半天发现是编译器优化把led_state变量优化掉了因为它在中断里被修改在主循环里没用到编译器认为它是死变量。解决办法是加volatile关键字告诉编译器这个变量可能被意外修改不要优化它。第二个坑有一次我用 TIMER5 做 1 秒中断同时用串口打印调试信息结果发现 LED 闪烁的节奏被串口打印打乱了。原因是串口打印是阻塞式的打印一次要好几毫秒如果打印频率太高就会影响中断的响应。解决办法是把串口打印改成非阻塞的或者降低打印频率只在关键节点打印。第三个坑有一次我把 TIMER5 的优先级设成了 0结果系统里的其他中断全被它压住了串口接收数据丢包严重。后来把优先级改成 2问题就解决了。这个教训是中断优先级不是越高越好要根据实际需求合理分配高优先级留给那些对实时性要求极高的中断普通定时任务用中等优先级就行。第四个坑有一次我在中断服务函数里调用了printf()结果程序直接卡死了。原因是printf()内部用了信号量或者互斥锁在中断上下文里调用会导致死锁。这个坑在 RTOS 环境下特别常见记住一个原则中断服务函数里只做最简单的事情复杂的处理放到主循环或者任务里去完成。
返回列表