APM32E103基本定时器详解:时钟树配置与中断实现
2026/9/1 0:19:05 网站建设 项目流程

简介:本资源是一套面向嵌入式开发初学者与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,内部有HSI(8MHz高速内部时钟)、HSE(外部高速晶振,通常8MHz)和PLL(锁相环)。PLL倍频后得到系统时钟SYSCLK,SYSCLK经过AHB预分频得到HCLK(即AHB总线时钟),HCLK再经过APB1和APB2预分频得到外设总线时钟。重点来了:TIM6和TIM7挂在APB1总线上,但它们的时钟不是直接等于APB1时钟,而是遵循一个规则——如果APB1预分频系数为1,定时器时钟等于APB1时钟;如果APB1预分频系数大于1,定时器时钟等于APB1时钟的两倍。

我实测下来,APM32E103默认SDK工程里SystemClock_Config把SYSCLK配置为120MHz,AHB不分频HCLK就是120MHz,APB1预分频为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 - 1,ARR = 10 - 1。为什么这么取?因为计数时钟频率 = 120MHz / 12000 = 10kHz,也就是计数1次需要0.1ms,计数器从0数到9一共10次,正好1ms。这里PSC和ARR都要减1,因为这两个寄存器都是从0开始计数的,实际分频和重载值是寄存器值加1。

我建议把所有相关值整理成一张速查表,方便后面调参:

目标定时时间SYSCLK=120MHz, PSC=12000-1SYSCLK=96MHz, PSC=9600-1SYSCLK=72MHz, PSC=7200-1
1msARR = 10-1ARR = 10-1ARR = 10-1
10msARR = 100-1ARR = 100-1ARR = 100-1
100msARR = 1000-1ARR = 1000-1ARR = 1000-1
1sARR = 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. 工程搭建与环境准备:从解压工程包到点亮LED

3.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加到ARR;period和prescaler分别是刚才算好的ARR和PSC;repetitionCounter是重复计数寄存器,高级定时器才有,基本定时器写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并倍频到120MHz,GPIO_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两倍按TIMxCLK=120MHz重新计算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的配合方式。希望这份工程包和这篇实现笔记,能帮你少走我走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询