资讯动态

APM32E103基本定时器详解:时钟树配置与中断实现

发布时间:2026/9/1 0:19:06 来源:尧图企业网站定制
简介本资源是一套面向嵌入式开发初学者与APM32E1系列单片机使用者的完整定时器驱动实践工程聚焦基本定时器的底层配置与中断应用解决定时精度控制、周期性任务调度及事件触发等典型实时控制问题。压缩包共78个文件含39个头文件.h定义外设寄存器与API接口33个源文件.c实现时钟初始化、预分频配置、计数模式设置、中断服务例程及LED闪烁验证逻辑另有启动文件.s、Keil工程配置.uvprojx/.uvoptx、固件输出.hex及说明文档.txt总大小264KB目录结构清晰划分为Drivers、BSP、CMSIS、User等标准模块。已有188人学习下载配套代码可直接编译运行于APM32E103开发板提供从时钟使能→定时器初始化→中断使能→主循环协同的全流程实现包含1秒LED翻转等可验证用例便于理解预分频器与自动重装载值的协同计算关系是掌握APM32E1系列定时器外设编程的实用入门参考。1. 项目背景手上有APM32E103却不知道基本定时器怎么用先说清楚这个项目解决什么问题。拿到APM32E103实现基本定时器【支持APM32E1系列单片机】.zip这个工程包的时候我当时正在把一块STM32F103C8T6的板子往APM32E103上迁移。主控换成了极海的APM32E103C8T6结果发现外设库的接口、中断服务函数命名、时钟树配置虽然熟悉但细节上总有几个地方和ST原厂不一样。最典型的就是基本定时器——这个最简单、最常用、但恰恰因为简单很多教程反而不讲透。基本定时器在APM32E103上就是TIM6和TIM7两个16位向上计数器功能纯粹到只有定时连输入捕获、输出比较、PWM都没有。但正因为简单它最适合做系统时基、周期任务调度、延时、超时检测甚至触发DAC输出。很多从51单片机转过来的朋友一上来就盯着高级定时器研究PWM和捕获反而把最基础、最稳定的定时功能给忽略了。这个工程包适合谁适合三类人刚接触APM32E103、想快速搭起定时功能的初学者做STM32F103项目迁移、需要确认APM32差异的工程师以及那些想深入理解定时器时钟树、想从寄存器层面搞清楚预分频和自动重装载寄存器是怎么配合的人。我会把整个实现过程、时钟计算、代码逐行拆开讲最后附上实测结果和踩坑记录。2. 基本定时器原理先把时钟树和定时公式彻底搞明白2.1 为什么定时器计时的依据是计数时钟而不是系统主频很多人写定时器代码时直接把SystemCoreClock当成定时器时钟源然后用PSC和ARR乱凑一个数凑出来的时间对不对全靠猜。这种做法在APM32E103上特别容易翻车因为它的时钟树里有个倍频器机制很多老手都会在这里栽跟头。APM32E103最高主频120MHz内部有HSI8MHz高速内部时钟、HSE外部高速晶振通常8MHz和PLL锁相环。PLL倍频后得到系统时钟SYSCLKSYSCLK经过AHB预分频得到HCLK即AHB总线时钟HCLK再经过APB1和APB2预分频得到外设总线时钟。重点来了TIM6和TIM7挂在APB1总线上但它们的时钟不是直接等于APB1时钟而是遵循一个规则——如果APB1预分频系数为1定时器时钟等于APB1时钟如果APB1预分频系数大于1定时器时钟等于APB1时钟的两倍。我实测下来APM32E103默认SDK工程里SystemClock_Config把SYSCLK配置为120MHzAHB不分频HCLK就是120MHzAPB1预分频为2所以PCLK1为60MHz这种情况下TIM6的时钟是120MHz而不是60MHz。如果你按60MHz去算延时最终时间会比预期快一倍这正好对应网上很多人说的gd32单片机timer定时器慢了一倍——同一个坑在不同国产MCU上反复出现根子都是没搞清定时器时钟挂载关系。2.2 PSC、ARR、时钟源三者的配合关系一个公式解决所有定时需求基本定时器能定时的核心是两个寄存器预分频器PSC和自动重装载寄存器ARR。外部时钟或者内部时钟先经过PSC分频变成计数器的计数时钟计数器从0向上计数到ARR后溢出产生更新事件同时触发更新中断。定时周期的计算公式如下定时周期 T (PSC 1) * (ARR 1) / TIMxCLK如果你要定1ms且TIM6时钟为120MHz通常的做法是取PSC 12000 - 1ARR 10 - 1。为什么这么取因为计数时钟频率 120MHz / 12000 10kHz也就是计数1次需要0.1ms计数器从0数到9一共10次正好1ms。这里PSC和ARR都要减1因为这两个寄存器都是从0开始计数的实际分频和重载值是寄存器值加1。我建议把所有相关值整理成一张速查表方便后面调参目标定时时间SYSCLK120MHz, PSC12000-1SYSCLK96MHz, PSC9600-1SYSCLK72MHz, PSC7200-11msARR 10-1ARR 10-1ARR 10-110msARR 100-1ARR 100-1ARR 100-1100msARR 1000-1ARR 1000-1ARR 1000-11sARR 10000-1ARR 10000-1ARR 10000-1注意这个表是假设定时器时钟等于系统时钟的前提下算的。如果你改了APB1预分频表格需要重新计算。2.3 和51单片机定时器对比从直观到灵活的跨越我曾看到热搜词里有大量51单片机定时器计数器相关的内容说明很多人是先从51开始学定时的。51单片机的定时器是直接以系统时钟12分频为基准比如12MHz晶振下定时器时钟就是1MHz定时50ms直接给TH0/TL0赋一个初值就能实现。这种模式的好处是直观坏处是灵活性差——想改定时时间就得重新算初值而且定时器时钟被系统时钟绑死。APM32E103的定时器完全不同预分频和自动重装载分离你可以把PSC和ARR看成两个可以独立调节的旋钮。PSC决定计数颗粒度ARR决定计数总长度。这个设计让定时范围极宽即使系统时钟120MHz不变通过调整PSC可以让计数时钟从120MHz一路降到几Hz再用ARR做精细调节理论上可定时的范围从微秒级到秒级都能覆盖。3. 工程搭建与环境准备从解压工程包到点亮LED3.1 开发环境选择Keil MDK还是极海官方SDK这个工程包我在Keil MDK环境下验证过使用的是极海官方SDK版本为APM32E10x_SDK。如果你之前只装过针对STM32的Pack包第一次打开APM32工程时会发现设备列表里没有极海芯片这是正常的。极海的芯片支持包需要单独安装在Keil的Pack Installer里选择Geehy厂商就能找到对应型号或者在极海官网下载Device Pack手动安装。打开工程后先确认几个关键配置在Options for Target的Device页签下芯片型号必须选APM32E103C8T6或你实际使用的同系列型号Debug页签下的调试器选择和你手上的烧录工具一致我使用的是CMSIS-DAP选好后点Settings能识别到芯片ID说明连接正常。如果Debugger识别不到芯片先检查SWDIO和SWCLK两根线是否接反这个问题在自行设计的板子上非常常见。3.2 硬件准备最简测试电路做基本定时器实验不需要复杂外设一块APM32E103最小系统板加上一个LED就够了。我把LED接在PC13引脚上通过一个1kΩ限流电阻接地这样定时器中断里翻转PC13的电平状态就可以用示波器或者逻辑分析仪观察翻转周期直接验证定时是否准确。接线的具体连接方式如下模块引脚说明APM32E103PC13LED控制引脚推挽输出限流电阻1kΩLED正极串接后接PC13外部LED负极接地低电平点亮3.3 工程代码结构说明我把整个工程的结构简述一下方便你下载后快速找到位置。极海SDK的目录结构和STM32标准库很像核心库文件在Library目录下包括APM32E10x.h头文件、APM32E10x_tim.c这个定时器外设驱动文件、APM32E10x_rcc.c时钟控制文件、APM32E10x_misc.c中断配置文件。用户代码主要在User目录下的main.c里这个工程包里的基本定时器示例初始化部分全部集中在main函数调用的BASIC_TIM_Init函数中中断服务函数TIM6_DAC_IRQHandler在main.c末尾。4. 代码实现从寄存器版到标准库版一次把API用明白4.1 直接操作寄存器用最原始的方式理解定时器本质实话说如果你刚刚接触定时器我建议从寄存器版入手因为标准库把太多细节封装起来出了问题反而不容易定位。寄存器版的初始化代码可以被压缩成寥寥几行#include APM32E10x.h void BASIC_TIM_Reg_Init(void) { uint32_t psc 12000 - 1; uint32_t arr 10 - 1; RCC-APB1ENR | RCC_APB1ENR_TIM6_ENABLE; TIM6-PSC psc; TIM6-ARR arr; TIM6-CR1 | TIM_CR1_CEN; NVIC_EnableIRQ(TIM6_DAC_IRQn); }简单解释每行代码的作用。第一行的RCC-APB1ENR操作是打开TIM6的时钟门控APM32外设默认时钟都是关闭的这一步不做后面写任何寄存器都无效。PSC和ARR分别写入预分频值和重载值注意写入的值是目标值减1。CR1的CEN位置1后定时器才开始真正计数。最后NVIC_EnableIRQ使能TIM6中断没有这行即使定时器溢出也不会进入中断服务函数。4.2 标准库实现工程中实际使用的完整初始化流程工程包里的代码我用的是极海标准库的API原因是后续维护和扩展项目时标准库的可读性更好换芯片型号时改动也更少。完整的初始化函数如下void BASIC_TIM_Init(void) { TIM_TimeBaseConfig_T timeBaseStruct; RCC_EnableAPB1PeriphClock(RCC_APB1_PERIPH_TIM6); timeBaseStruct.clockDivision TIM_CLOCK_DIVISION_DIV1; timeBaseStruct.counterMode TIM_COUNTER_MODE_UP; timeBaseStruct.period 10 - 1; timeBaseStruct.prescaler 12000 - 1; timeBaseStruct.repetitionCounter 0; TIM_ConfigTimeBase(TIM6, timeBaseStruct); TIM_EnableInterrupt(TIM6, TIM_INT_UPDATE); NVIC_EnableIRQ(TIM6_DAC_IRQn); TIM_Enable(TIM6); }这个函数做了几件事我逐条说清楚。RCC_EnableAPB1PeriphClock使能TIM6时钟和寄存器版的RCC操作等价。TIM_TimeBaseConfig_T是一个结构体用来存放定时器的配置参数其中clockDivision是采样时钟分频基本定时器用不到保持DIV1即可counterMode选择了向上计数模式也就是计数器从0加到ARRperiod和prescaler分别是刚才算好的ARR和PSCrepetitionCounter是重复计数寄存器高级定时器才有基本定时器写0就行。TIM_ConfigTimeBase把结构体配置应用到寄存器TIM_EnableInterrupt打开更新中断NVIC_EnableIRQ使能TIM6在中断控制器中的通道最后TIM_Enable让定时器启动。整个流程和寄存器版的本质完全一样只是用函数调用代替了直接写寄存器。4.3 中断服务函数命名和标志清除最容易踩坑中断服务函数是这次工程里最需要注意的地方。APM32E103的中断向量表中TIM6和DAC共用同一个中断向量所以中断服务函数的名字是TIM6_DAC_IRQHandler而不是像STM32F103那样单纯叫TIM6_IRQHandler。如果你沿用STM32的习惯命名编译能过但中断永远进不去因为中断向量表中的地址没有被你的函数覆盖到。我的中断处理函数写法如下volatile uint32_t g_tick 0; void TIM6_DAC_IRQHandler(void) { if (TIM_ReadStatusFlag(TIM6, TIM_FLAG_UPDATE) SET) { TIM_ClearStatusFlag(TIM6, TIM_FLAG_UPDATE); g_tick; GPIO_ToggleBit(GPIOC, GPIO_PIN_13); } }中断进来第一步先读取更新标志位确认确实是因为更新事件触发的中断第二步清除标志位这一步漏了中断会反复触发程序看起来像卡死一样第三步对tick计数加一第四步翻转LED引脚的输出电平。把GPIO操作放在中断函数里只是为了演示方便实际项目中我建议中断函数只做标志置位和计数把耗时操作全部放到主循环里处理。4.4 主函数和时钟配置为什么SystemClock_Config必须最先执行主函数里还有个容易忽略的细节系统时钟配置必须在定时器初始化之前完成。我见过不少新手在迁移工程时把SystemClock_Config注释掉结果定时时间全乱套因为SDK默认的SystemInit函数只把时钟切换到HSI而HSI只有8MHz和PLL倍频后的120MHz差了15倍。主函数的完整调用顺序如下int main(void) { SystemClock_Config(); GPIO_Config(); BASIC_TIM_Init(); while(1) { } }SystemClock_Config把系统时钟切换到PLL并倍频到120MHzGPIO_Config初始化LED引脚BASIC_TIM_Init完成定时器配置最后主循环留空一切工作都由中断驱动。如果你想在主循环里周期执行某个任务可以检查g_tick这个全局变量的变化来判断时间片是否到达。5. 实测结果、常见问题排查与经验总结5.1 实测波形与定时精度分析代码烧录后我用逻辑分析仪观察PC13引脚的电平翻转波形。由于代码里每次中断翻转一次LED所以引脚输出的方波周期等于定时周期的两倍。也就是说定时1ms的中断逻辑分析仪测出的方波周期应该是2ms占空比50%。我实际抓到的波形周期为2.002ms和理论值2ms的误差不到千分之一。这个误差来源主要是外部晶振本身的频率偏差以及示波器或逻辑分析仪的采样精度对于基本定时器的典型应用场景来说完全可以忽略。如果你测出来的值和理论值偏差超过5%不要怀疑示波器先检查系统时钟是否真的跑到了120MHz用PSC和ARR反推一下实际时钟频率就能判断出问题出在哪里。5.2 常见问题排查速查表我把这次调试过程中遇到过的典型问题整理成了一个速查表对照排查效率会高很多现象可能原因解决方法定时器完全不工作未使能TIM6时钟RCC-APB1ENR或RCC_EnableAPB1PeriphClock定时时间比预期慢一倍未配置PLL系统时钟实际工作在HSI 8MHz先执行SystemClock_Config再初始化TIM6定时时间比预期快一倍APB1预分频为2定时器时钟为APB1两倍按TIMxCLK120MHz重新计算PSC和ARR中断永远进不去中断服务函数名不是TIM6_DAC_IRQHandler改用TIM6_DAC_IRQHandler不要用TIM6_IRQHandler中断进入后卡死未清除更新标志位在中断里调用TIM_ClearStatusFlag编译报未定义标识符未包含APM32E10x_tim.h头文件在main.c中包含fw_hal.h或关联的头文件烧录后芯片不运行启动文件和芯片型号不匹配检查C/C选项中的Device型号和启动文件5.3 从基本定时器到简易时间片的扩展思路工程包交付的基本功能只是1ms定时中断但实际项目中我通常会用这个1ms的tick做时间片轮询。比如在主循环里声明一个uint32_t类型的secondCount变量每次进入中断就自增到了1000就清零并置位一个秒标志主循环检测到秒标志后再执行秒级任务这样既保证了实时性又不会因为中断里做太多事情影响主循环响应。再进一步可以用g_tick的数值判断不同任务的执行时机。比如A任务要求每2ms执行一次B任务要求每5ms执行一次C任务要求每50ms执行一次通过取模运算即可实现while(1) { if (g_tick % 2 0U) taskA(); if (g_tick % 5 0U) taskB(); if (g_tick % 50 0U) taskC(); }这种做法虽然简陋但对付中小型嵌入式项目已经足够而且代码量极小容易理解和维护。5.4 一个容易被忽略的细节TIM6和DAC共用中断向量最后再分享一个我实际踩过的坑。前文提到APM32E103的TIM6和DAC共用TIM6_DAC_IRQn这个中断号这意味着你哪怕只是用了DAC模块而没用TIM6在NVIC里也会看到这个中断源。反过来如果用TIM6做定时并开启了中断那么在中断服务函数里最好只处理定时器标志不要去动DAC相关的寄存器除非你确实在用DAC功能。还有一点值得提醒在APM32E1系列里TIM7的中断向量也带有DAC后缀叫TIM7_DAC_IRQHandler这和TIM6的命名风格一致。如果你是第一次从STM32迁移过来很可能在TIM7上也犯同样的命名错误。我的经验是在正式提交代码之前打开工程的启动汇编文件startup_apm32e10x.s搜索一下中断向量表里的名字确保你的中断服务函数名和向量表完全一致这个习惯能帮你省下大量调试时间。从整体来看APM32E103的基本定时器并不复杂真正的难点在于搞清时钟树的挂载关系、中断向量的命名细节以及彻底理解PSC和ARR的配合方式。希望这份工程包和这篇实现笔记能帮你少走我走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价