1. 为什么需要优化定时器延时在嵌入式开发中定时器延时是最基础却又最容易出问题的功能之一。就拿我最近用复旦微FM33G0开发板做的一个充电控制项目来说原本以为简单的3秒延时却让我踩了不少坑。当时的需求是在初始化时让某个引脚保持低电平3秒相当于上电等电容充电完成后再拉高。听起来很简单对吧但实际实现时却遇到了各种意外情况。最开始我直接用了芯片自带的Systick定时器想着用Delay_ms(1000)连续调用三次就能实现3秒延时。结果发现程序运行不到3秒就会复位或者直接跑飞。经过排查才发现Systick是24位定时器在16MHz时钟频率下最大只能实现大约1秒的延时。更麻烦的是我还开启了看门狗功能溢出周期设置为2秒。这意味着如果不在2秒内喂狗系统就会自动复位。这就是为什么连续三次1秒延时会失败的原因。2. Systick定时器的延时实现与局限2.1 Systick的基本工作原理Systick是ARM Cortex-M系列内核自带的一个简易定时器它最大的优势就是使用简单、不占用额外硬件资源。在FM33G0上Systick是一个24位递减计数器时钟源可以选择内核时钟或外部时钟。默认情况下它使用内部16MHz的高频RC振荡器作为时钟源。计算一下就知道24位计数器最大值是16,777,215在16MHz时钟下一个计数周期是1/16,000,000秒所以最大可以计时的时长是16,777,215/16,000,000≈1.048秒。这就是为什么直接用Systick无法实现超过1秒延时的根本原因。2.2 改进的Systick延时实现虽然Systick有硬件限制但通过软件方法还是可以实现较长时间的延时。我参考了社区里的一个巧妙实现重写了us和ms级别的延时函数void Delay_us(uint32_t nus) { uint32_t tnow, tcnt 0; uint32_t reload SysTick-LOAD; // 获取重装载值 uint32_t told SysTick-VAL; // 获取当前计数值 uint32_t ticks nus * 16; // 计算需要的节拍数 (16MHz/1MHz16) while(1) { tnow SysTick-VAL; // 读取当前计数值 if(tnow ! told) { // 如果计数值有变化 if(tnow told) { // 正常递减情况 tcnt told - tnow; } else { // 计数器重装载情况 tcnt reload - tnow told; } told tnow; if(tcnt ticks) { // 达到需要的节拍数 break; } } } } void Delay_ms(uint32_t nms) { for(uint32_t i0; inms; i) { Delay_us(1000); // 调用1000次us延时 } }这个实现的关键点在于精确计算us级延时需要的时钟节拍数处理计数器重装载时的特殊情况通过累加多个小延时实现更长延时2.3 结合看门狗的注意事项即使改进了延时函数在使用时还需要特别注意看门狗的问题。我的看门狗设置是2秒溢出所以在实现3秒延时时必须在中间适时喂狗void Delay_LD3s(void) { PA11_ON; // 拉低引脚 Delay_ms(1000); // 延时1秒 IWDT_Clr(); // 清看门狗 Delay_ms(1000); // 再延时1秒 IWDT_Clr(); // 清看门狗 Delay_ms(1000); // 最后1秒 IWDT_Clr(); // 清看门狗 PA11_OFF; // 拉高引脚 }这种方式虽然能工作但明显不够优雅而且会占用CPU资源。这就是为什么我们需要考虑使用更专业的定时器——ETIM。3. ETIM定时器的优势与应用3.1 ETIM与Systick的核心区别ETIM(Extended Timer)是FM33G0芯片上的高级定时器模块相比Systick有几个明显优势32位计数器可以支持更长的定时周期独立时钟源不受系统时钟影响支持硬件自动重装载可以产生中断不占用CPU资源更灵活的预分频设置在实际项目中如果需要精确的长延时ETIM显然是更好的选择。特别是在有看门狗的场景下ETIM的中断特性可以确保及时喂狗避免系统复位。3.2 ETIM的两种使用模式3.2.1 轮询模式实现轮询模式适合简单的延时场景代码结构直观volatile uint8_t delay3sFlag 0; void Delay_LD3s_Polling(void) { PA11_ON; // 拉低引脚 ETIMx_ETxCR_CEN_Setable(ETIM2, ENABLE); // 启动ETIM2定时器 while(!delay3sFlag) { // 等待标志位 IWDT_Clr(); // 喂狗 } delay3sFlag 0; // 清除标志 PA11_OFF; // 拉高引脚 }这种方式的优点是实现简单缺点是需要CPU不断轮询标志位效率不高。3.2.2 中断模式实现中断模式是更专业的做法可以释放CPU资源volatile uint8_t delay3sFlag 0; void ETIM2_IRQHandler(void) { if(ETIM_GetITStatus(ETIM2, ETIM_IT_Update) ! RESET) { ETIM_ClearITPendingBit(ETIM2, ETIM_IT_Update); delay3sFlag 1; // 设置完成标志 ETIMx_ETxCR_CEN_Setable(ETIM2, DISABLE); // 关闭定时器 } } void Delay_LD3s_IRQ(void) { PA11_ON; // 拉低引脚 delay3sFlag 0; ETIMx_ETxCR_CEN_Setable(ETIM2, ENABLE); // 启动定时器 } int main(void) { // 初始化代码... while(1) { if(delay3sFlag) { delay3sFlag 0; PA11_OFF; // 拉高引脚 } IWDT_Clr(); // 主循环中喂狗 } }中断模式的优点很明显CPU不需要忙等待可以执行其他任务定时精度更高看门狗喂狗操作可以放在主循环更安全4. 实战中的调试技巧与避坑指南4.1 定时器初始化配置要点在使用ETIM时正确的初始化配置至关重要。以下是一个典型的3秒定时配置void ETIM2_Init(void) { ETIM_TimeBaseInitTypeDef ETIM_InitStruct; ETIM_InitStruct.ETIM_Prescaler 15999; // 预分频16000-1 (16MHz/160001KHz) ETIM_InitStruct.ETIM_CounterMode ETIM_CounterMode_Up; ETIM_InitStruct.ETIM_Period 3000; // 计数值3000 (1ms*30003s) ETIM_InitStruct.ETIM_ClockDivision ETIM_CKD_DIV1; ETIM_InitStruct.ETIM_RepetitionCounter 0; ETIM_TimeBaseInit(ETIM2, ETIM_InitStruct); ETIM_ITConfig(ETIM2, ETIM_IT_Update, ENABLE); NVIC_EnableIRQ(ETIM2_IRQn); }关键参数说明预分频(Prescaler)将16MHz时钟分频到1KHz计数模式选择向上计数周期(Period)设置为3000对应3秒重复计数器设置为0表示不重复4.2 常见问题排查在实际项目中我遇到过几个典型问题定时不准检查时钟源配置是否正确确认预分频和周期计算无误。有时硬件连接问题也会导致时钟不稳定。中断不触发确认NVIC中断已使能检查中断服务函数名称是否正确确保在初始化时调用了ETIM_ITConfig()看门狗复位在长时间延时中确保定期喂狗考虑使用中断模式在主循环中统一喂狗标志位竞争对共享标志位使用volatile修饰在读写标志位时考虑禁用中断4.3 性能优化建议对于需要多个延时的场景可以配置多个ETIM通道避免频繁重配置。如果系统中有多个定时需求可以考虑使用一个基础定时器通过软件计数器实现多个虚拟定时器。在低功耗应用中可以结合芯片的低功耗模式让定时器唤醒系统进一步降低功耗。对于特别精确的定时需求可以考虑使用ETIM的PWM模式或者输出比较功能。
复旦微FM33G0定时器实战:从Systick到ETIM的延时优化
发布时间:2026/6/17 14:07:51
1. 为什么需要优化定时器延时在嵌入式开发中定时器延时是最基础却又最容易出问题的功能之一。就拿我最近用复旦微FM33G0开发板做的一个充电控制项目来说原本以为简单的3秒延时却让我踩了不少坑。当时的需求是在初始化时让某个引脚保持低电平3秒相当于上电等电容充电完成后再拉高。听起来很简单对吧但实际实现时却遇到了各种意外情况。最开始我直接用了芯片自带的Systick定时器想着用Delay_ms(1000)连续调用三次就能实现3秒延时。结果发现程序运行不到3秒就会复位或者直接跑飞。经过排查才发现Systick是24位定时器在16MHz时钟频率下最大只能实现大约1秒的延时。更麻烦的是我还开启了看门狗功能溢出周期设置为2秒。这意味着如果不在2秒内喂狗系统就会自动复位。这就是为什么连续三次1秒延时会失败的原因。2. Systick定时器的延时实现与局限2.1 Systick的基本工作原理Systick是ARM Cortex-M系列内核自带的一个简易定时器它最大的优势就是使用简单、不占用额外硬件资源。在FM33G0上Systick是一个24位递减计数器时钟源可以选择内核时钟或外部时钟。默认情况下它使用内部16MHz的高频RC振荡器作为时钟源。计算一下就知道24位计数器最大值是16,777,215在16MHz时钟下一个计数周期是1/16,000,000秒所以最大可以计时的时长是16,777,215/16,000,000≈1.048秒。这就是为什么直接用Systick无法实现超过1秒延时的根本原因。2.2 改进的Systick延时实现虽然Systick有硬件限制但通过软件方法还是可以实现较长时间的延时。我参考了社区里的一个巧妙实现重写了us和ms级别的延时函数void Delay_us(uint32_t nus) { uint32_t tnow, tcnt 0; uint32_t reload SysTick-LOAD; // 获取重装载值 uint32_t told SysTick-VAL; // 获取当前计数值 uint32_t ticks nus * 16; // 计算需要的节拍数 (16MHz/1MHz16) while(1) { tnow SysTick-VAL; // 读取当前计数值 if(tnow ! told) { // 如果计数值有变化 if(tnow told) { // 正常递减情况 tcnt told - tnow; } else { // 计数器重装载情况 tcnt reload - tnow told; } told tnow; if(tcnt ticks) { // 达到需要的节拍数 break; } } } } void Delay_ms(uint32_t nms) { for(uint32_t i0; inms; i) { Delay_us(1000); // 调用1000次us延时 } }这个实现的关键点在于精确计算us级延时需要的时钟节拍数处理计数器重装载时的特殊情况通过累加多个小延时实现更长延时2.3 结合看门狗的注意事项即使改进了延时函数在使用时还需要特别注意看门狗的问题。我的看门狗设置是2秒溢出所以在实现3秒延时时必须在中间适时喂狗void Delay_LD3s(void) { PA11_ON; // 拉低引脚 Delay_ms(1000); // 延时1秒 IWDT_Clr(); // 清看门狗 Delay_ms(1000); // 再延时1秒 IWDT_Clr(); // 清看门狗 Delay_ms(1000); // 最后1秒 IWDT_Clr(); // 清看门狗 PA11_OFF; // 拉高引脚 }这种方式虽然能工作但明显不够优雅而且会占用CPU资源。这就是为什么我们需要考虑使用更专业的定时器——ETIM。3. ETIM定时器的优势与应用3.1 ETIM与Systick的核心区别ETIM(Extended Timer)是FM33G0芯片上的高级定时器模块相比Systick有几个明显优势32位计数器可以支持更长的定时周期独立时钟源不受系统时钟影响支持硬件自动重装载可以产生中断不占用CPU资源更灵活的预分频设置在实际项目中如果需要精确的长延时ETIM显然是更好的选择。特别是在有看门狗的场景下ETIM的中断特性可以确保及时喂狗避免系统复位。3.2 ETIM的两种使用模式3.2.1 轮询模式实现轮询模式适合简单的延时场景代码结构直观volatile uint8_t delay3sFlag 0; void Delay_LD3s_Polling(void) { PA11_ON; // 拉低引脚 ETIMx_ETxCR_CEN_Setable(ETIM2, ENABLE); // 启动ETIM2定时器 while(!delay3sFlag) { // 等待标志位 IWDT_Clr(); // 喂狗 } delay3sFlag 0; // 清除标志 PA11_OFF; // 拉高引脚 }这种方式的优点是实现简单缺点是需要CPU不断轮询标志位效率不高。3.2.2 中断模式实现中断模式是更专业的做法可以释放CPU资源volatile uint8_t delay3sFlag 0; void ETIM2_IRQHandler(void) { if(ETIM_GetITStatus(ETIM2, ETIM_IT_Update) ! RESET) { ETIM_ClearITPendingBit(ETIM2, ETIM_IT_Update); delay3sFlag 1; // 设置完成标志 ETIMx_ETxCR_CEN_Setable(ETIM2, DISABLE); // 关闭定时器 } } void Delay_LD3s_IRQ(void) { PA11_ON; // 拉低引脚 delay3sFlag 0; ETIMx_ETxCR_CEN_Setable(ETIM2, ENABLE); // 启动定时器 } int main(void) { // 初始化代码... while(1) { if(delay3sFlag) { delay3sFlag 0; PA11_OFF; // 拉高引脚 } IWDT_Clr(); // 主循环中喂狗 } }中断模式的优点很明显CPU不需要忙等待可以执行其他任务定时精度更高看门狗喂狗操作可以放在主循环更安全4. 实战中的调试技巧与避坑指南4.1 定时器初始化配置要点在使用ETIM时正确的初始化配置至关重要。以下是一个典型的3秒定时配置void ETIM2_Init(void) { ETIM_TimeBaseInitTypeDef ETIM_InitStruct; ETIM_InitStruct.ETIM_Prescaler 15999; // 预分频16000-1 (16MHz/160001KHz) ETIM_InitStruct.ETIM_CounterMode ETIM_CounterMode_Up; ETIM_InitStruct.ETIM_Period 3000; // 计数值3000 (1ms*30003s) ETIM_InitStruct.ETIM_ClockDivision ETIM_CKD_DIV1; ETIM_InitStruct.ETIM_RepetitionCounter 0; ETIM_TimeBaseInit(ETIM2, ETIM_InitStruct); ETIM_ITConfig(ETIM2, ETIM_IT_Update, ENABLE); NVIC_EnableIRQ(ETIM2_IRQn); }关键参数说明预分频(Prescaler)将16MHz时钟分频到1KHz计数模式选择向上计数周期(Period)设置为3000对应3秒重复计数器设置为0表示不重复4.2 常见问题排查在实际项目中我遇到过几个典型问题定时不准检查时钟源配置是否正确确认预分频和周期计算无误。有时硬件连接问题也会导致时钟不稳定。中断不触发确认NVIC中断已使能检查中断服务函数名称是否正确确保在初始化时调用了ETIM_ITConfig()看门狗复位在长时间延时中确保定期喂狗考虑使用中断模式在主循环中统一喂狗标志位竞争对共享标志位使用volatile修饰在读写标志位时考虑禁用中断4.3 性能优化建议对于需要多个延时的场景可以配置多个ETIM通道避免频繁重配置。如果系统中有多个定时需求可以考虑使用一个基础定时器通过软件计数器实现多个虚拟定时器。在低功耗应用中可以结合芯片的低功耗模式让定时器唤醒系统进一步降低功耗。对于特别精确的定时需求可以考虑使用ETIM的PWM模式或者输出比较功能。