☰
STM32CubeMX配置TIM1定时器中断:从时钟树到回调函数全解析
2026/9/25 4:49:15 网站建设 项目流程

说实话,第一次看到“5分钟搞定定时器中断”这类标题,我内心是抵触的。真正自己去配置TIM1的时候,光是搞明白PSC和ARR哪个该减一、为什么APB2总线的72MHz到了定时器这里还要再走一遍时钟树,就能卡住整整一个晚上。这篇不是把CubeMX的截图复述一遍,而是把TIM1这个“高级定时器”在定时中断场景下的完整链路拆开讲清楚:为什么优先选TIM1、1ms这个时间到底是从哪段时钟算出来的、生成工程代码之后有哪些动作必须亲手补上,以及我实际调试中踩过的“中断不进、时间不对”这类问题。

如果你刚接触STM32CubeMX,或者之前一直用的是TIM2、TIM3这类通用定时器,这次想把TIM1用起来,这篇文章应该能帮你少走不少弯路。看完之后你会发现,所谓“高级定时器”,在单纯的定时中断需求下,其实并没有想象中那么高不可攀。

1. 为什么用TIM1:从LED闪烁需求反推定时器选型

LED闪烁这个需求,本质上是“每经过固定时间间隔执行一次状态翻转”。这个“固定时间间隔”在单片机里就是时基。STM32能产生时基的资源有好几种,选型决定了后续代码的写法和可扩展性,所以我建议先停下来想清楚,而不是一上来就在CubeMX里点一个定时器。

1.1 TIM1在F103系列里的定位,以及和SysTick的本质区别

STM32F103系列里,能够产生定时中断的资源大体分三类:内核自带的SysTick、通用定时器(TIM2/TIM3/TIM4等)、高级定时器(TIM1/TIM8)。它们都能做1ms中断,但使用场景不一样。

SysTick是Cortex-M3内核里的24位向下计数定时器,HAL库的HAL_GetTick()就是用它在数毫秒。它默认被HAL库拿来当系统节拍,如果你再把它拿去驱动自己的任务,可能跟库函数的时间基准打架。虽然可以在中断里叠加逻辑,但工程复杂之后很难受。

通用定时器TIM2~TIM5挂在APB1总线上,高级定时器TIM1和TIM8挂在APB2总线上。单从纯定时中断角度讲,TIM1和TIM2的用法没有任何区别:都要配置预分频器、计数器周期、使能更新中断、写回调函数。TIM1多出来的刹车输入、互补输出、重复计数器、主输出使能这些功能,都是为电机控制这类场景准备的,我们做LED闪烁根本碰不到。

那么为什么题目里选择TIM1而不是TIM2?几个理由:第一,TIM1挂在APB2上,在F103系列里APB2的最高频率就是72MHz,跟系统主频一致,理论上你可以拿到更高的计数精度,TIM2虽然也能通过倍频达到72MHz,但源头是36MHz的APB1,中间多一层换算逻辑;第二,很多人一看“高级定时器”就觉得复杂而避开,实际上从定时中断角度看它并不复杂,把它吃透之后以后做PWM呼吸灯、电机控制都是顺路的事;第三,TIM1和TIM8是一对,懂了一个另一个基本就懂了。

1.2 1ms中断背后的时钟数学

很多教程让你直接填Prescaler=71、Period=999,但没解释为什么。如果只抄参数,遇到外部晶振不是8MHz的板子,或者改了系统时钟,时间就不对了。这里我把计算过程完整捋一遍。

STM32F103C8T6这种经典芯片,常见配置是外部8MHz晶振(HSE),经过PLL锁相环9倍频得到72MHz系统主频。系统主频经过总线预分频器分配给各个外设:

  • APB2预分频=1,所以APB2总线频率=72MHz,TIM1挂在APB2上,直接使用72MHz。
  • APB1预分频=2,所以APB1总线频率=36MHz,但请注意STM32F1系列的特殊规则:如果APB1预分频大于1,那么挂在APB1上的定时器时钟会自动乘以2,恢复为72MHz。这就是为什么CubeMX的时钟树页面里,TIM2的时钟也是72MHz的原因。

有了TIM1时钟72MHz这个前提,接下来是定时器本身的工作原理。定时器内部有一个预分频器PSC(Prescaler)和一个自动重装载寄存器ARR(Auto-Reload Register)。实际信号流向是:

TIM1_CLK → PSC分频 → 计数时钟 → 计数器CNT从0加到ARR → 溢出 → 触发更新事件/中断

因此,定时器溢出一次的周期公式是:

Tout = (PSC + 1) × (ARR + 1) / Fclk

注意这里两个值都要加1,因为PSC和ARR都是从0开始计数的。比如PSC=0时,预分频器的分频系数是1,不是0。

要得到1ms中断:

  • 目标:Tout = 1ms = 0.001s
  • Fclk = 72MHz = 72,000,000Hz
  • 取PSC=71:分频后计数频率 = 72,000,000 / (71+1) = 1,000,000Hz,也就是1MHz,每1us计数一次
  • 取ARR=999:溢出周期 = (999+1) × 1us = 1000us = 1ms

用公式验证一遍就是:

(71+1) × (999+1) / 72,000,000 = 72 × 1000 / 72,000,000 = 0.001s

完全对得上。以后如果想要其他定时周期,可以直接套这个公式反推。

目标中断周期PSCARR实际计数频率说明
1ms719991MHz最经典组合
10ms7199991MHz计数器范围变大
100ms71999991MHz注意16位定时器上限65535
1us07172MHz72个时钟周期溢出一次
1s35991999920kHz示例,32位/16位需注意溢出

这里面有一个新手容易忽略的点:如果直接用72MHz作为计数频率,测量短延时精度高,但计数速度也快,16位计数器(ARR最大65535)在72MHz下溢出一次最快约910us,最慢也只有约910us×65535,也就是大约59.6秒。如果需要更长周期,要么提高PSC降低计数频率,要么换用32位的TIM2/TIM5。F103里TIM2和TIM5是32位定时器,TIM1反而只是16位,这个细节在实际项目中很容易踩坑。

2. CubeMX图形化配置:时钟树、TIM1参数与NVIC一次性搞定

我以STM32F103C8T6搭配STM32CubeMX为例,从新建工程开始走一遍完整配置流程。CubeMX版本不同界面可能略有差异,但核心选项是一致的。

2.1 新建工程与基础外设RCC、SYS的配置

打开CubeMX后,首先通过“File → New Project”进入芯片选择界面,输入STM32F103C8T6并选中,双击后会生成一个空白工程。此时左侧Categories面板里能看到各种外设分类,我们需要先处理两个最基础的模块。

第一个是RCC(Reset and Clock Control),也就是时钟控制。在System Core → RCC里,把High Speed Clock(HSE)设置为“Crystal/Ceramic Resonator”。这里对应的是板子上的外部晶振,大部分F103核心板用的是8MHz,如果你用的是12MHz晶振的板子,后面时钟树里的参数要重新计算,不能照抄8MHz的配置。

第二个是SYS,在System Core → SYS里,把Debug设置为“Serial Wire”。这一步比较容易被忽略,但它非常关键。STM32F103的PA13/PA14引脚默认功能就是SWD调试引脚,如果不在CubeMX里显式配置为Serial Wire,生成代码后这两个引脚可能会被当作普通GPIO重新初始化,结果就是下载器连不上芯片,尤其是第二次下载时会提示找不到设备。先用Serial Wire把调试口锁住,后面哪怕GPIO规划混乱也不至于锁死芯片。

做完这两步,切到“Clock Configuration”标签页。这里会看到一整张时钟树,需要手工配置的部分是PLL倍频系数。把PLL Source选为HSE,然后在PLL Multiplier输入9,此时上方SYSCLK那一格应该显示72MHz,下方APB1显示36MHz,APB2显示72MHz。记住,只要外部晶振是8MHz,这套配置就是F103最主流的72MHz主频方案。

2.2 TIM1参数面板的逐项解释,而不是照抄数值

在左侧Categories里展开Timers,找到TIM1,勾选“Activated”。然后点击TIM1进入配置面板,在Parameter Settings里设置以下参数:

  • Prescaler:填71
  • Counter Mode:选择Up(向上计数)
  • Counter Period:填999
  • Internal Clock Division:保持默认“No Division”
  • Auto-Reload Preload:选择Enable
  • Repetition Counter:填0

这些参数里,Prescaler和Counter Period的计算原理上面已经讲过,一个是分频系数决定计数频率,一个是预装载值决定溢出周期。这里重点说一下另外两个容易让人迷惑的选项。

Auto-Reload Preload,翻译过来是“自动重装载预装载”。设置为Enable后,计数器溢出时ARR寄存器的值不会立即生效,而是先存入影子寄存器,等到发生更新事件时才真正加载进计数器。这种机制的好处是避免在计数过程中修改ARR导致时间跳变。在我们的Demo里,ARR在初始化时就被写死了,Enable和Disable效果上没有区别,但养成Enable的习惯是好事情,以后做PWM动态调频率时会少踩很多坑。

Repetition Counter是高级定时器TIM1/TIM8特有的参数,通用定时器没有。它的作用是“每隔N+1次溢出才触发一次更新事件”。默认是0,也就是每次溢出都会触发更新事件,对应我们的1ms中断。如果你把它设为1,TIM1就要计数2次溢出才产生一次更新,中断周期就变成了2ms。F103的重复计数器是8位,最大能设到255。在纯定时中断的用法里面,保持0即可,这个参数主要是给PWM和电机控制场景用的。

这里要额外说一点:不需要配置任何Channel。Channel是定时器的输入捕获/输出比较通道,用于PWM输出或者信号测量。我们只想在定时器溢出的时候进中断,完全不需要走通道,通道配置面板留空即可。很多第一次用TIM1的人看到那么多Channel选项就发慌,其实它们和定时中断没有关系。

2.3 NVIC设置与生成代码前的最后检查

参数配置完,点开“NVIC Settings”标签页。这里列出了TIM1的所有中断源,我们只需要勾选“TIM1 update interrupt”。这就是定时器溢出更新事件对应的中断,不勾选的话,即使计数器正常溢出,也不会触发中断服务函数。

勾选之后可以看到Preemption Priority和Sub Priority两个数值。这取决于你在NVIC分组里的优先级方案。CubeMX的HAL_Init()默认把优先级分组设置为Group4,也就是所有中断位都表示抢占优先级,没有子优先级。如果你没有特别改动过优先级分组,这里Preemption Priority填0~15都可以,数字越小优先级越高。我们的Demo只有一个中断,随便填一个,比如0。

生成代码之前,最后检查一下几个关键项:

  • Project Manager里的Toolchain选择:如果用Keil,选MDK-ARM V5;如果用STM32CubeIDE,保持默认即可。
  • Project Manager → Project里确认生成了独立的.c/.h文件。这个个人习惯,一般默认就行。
  • 确认之前所有配置都已保存,然后点击右上角的“GENERATE CODE”。

CubeMX会在你指定的路径下生成完整工程,第一次生成需要联网下载对应的固件包,如果卡住不动,多半是网络问题。可以提前在Help → Manage Embedded Software Packages里把STM32F1系列的固件包下好,版本选个4.x的稳定版即可。

3. 生成代码后的关键动作:启动中断、改写回调函数

CubeMX生成了工程,但这时烧录进去,LED是不会闪的。原因在于:CubeMX生成的MX_TIM1_Init()只负责把定时器的寄存器配置好,定时器本身处于停止状态,中断也没有启动。这个“最后一步”需要手动完成,而它恰恰是新手最容易遗漏的地方。

3.1 HAL_TIM_Base_Start_IT:新手最容易漏的一行

打开生成的main.c,在while(1)之前找到USER CODE BEGIN 2这个宏标记区。在这个区域里,添加一行:

HAL_TIM_Base_Start_IT(&htim1);

这行代码的作用是启动TIM1的时基中断。HAL库在定时器这一块把所有功能分得很细:定时中断是基础功能,所以启动函数是HAL_TIM_Base_Start_IT;如果是PWM输出,就要用HAL_TIM_PWM_Start;输入捕获用HAL_TIM_IC_Start_IT。函数名里的Base代表时基功能,IT代表中断使能。

如果不加这一行,现象非常典型:程序能正常跑,引脚也能正常输出高电平,但LED就是不闪烁。你把调试器停在主循环里看,htim1的状态是Init,没有任何报错。这个问题在没有经验的时候可能排查很久,因为编译不报错、运行不报错,唯一的问题就是定时器压根没启动。

3.2 回调函数的正确写法:不要去改中断服务函数

启动定时器中断之后,中断信号会先进入编译好的中断服务函数。在F103上,TIM1的更新中断服务函数是TIM1_UP_IRQHandler,它位于stm32f1xx_it.c文件中,由CubeMX自动生成。这个函数内部调用HAL_TIM_IRQHandler(&htim1),HAL库在中断处理过程中会根据中断状态判断是不是更新事件,如果是,就调用一个弱函数HAL_TIM_PeriodElapsedCallback。

所以整个中断链路是:

TIM1溢出 → 硬件触发TIM1_UP_IRQHandler → HAL_TIM_IRQHandler → HAL_TIM_PeriodElapsedCallback → 你的逻辑

这里最重要的一个概念是:你永远不应该修改stm32f1xx_it.c里面的TIM1_UP_IRQHandler,HAL库的设计已经帮你把中断处理封装好了,你只需要在用户代码里实现HAL_TIM_PeriodElapsedCallback这个回调函数即可。

正确的做法是打开main.c,找到USER CODE BEGIN 4标记区,在这里重写回调函数。CubeMX生成的回调函数原型是:

const char *TAG = "TIM";

不对,重新来。正确的原型是:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { }

你需要在main.c的USER CODE BEGIN 4区域里,写一个完整版本:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } }

需要注意几个细节。第一,HAL_TIM_PeriodElapsedCallback是一个__weak弱函数,你在用户文件里重新定义它不会造成符号重复,而是覆盖弱定义。第二,既然一个工程里可能有多个定时器,比如TIM1和TIM2都在跑中断,它们溢出时都会进入同一个回调函数,所以一定要通过htim->Instance判断是哪个定时器触发的。只用一个定时器时判断可以省略,但从项目规范来讲,加上判断是必须的好习惯。第三,回调函数是在中断上下文里执行的,代码要尽量精简,不能在里面做耗时操作。像HAL_GPIO_TogglePin这种寄存器级操作没问题,但如果在这里面放一个Delay延时,整个系统的实时性就毁了。

3.3 完整Demo代码与LED闪烁逻辑

前面铺垫了这么多,现在可以看完整的代码逻辑了。我这里给出两个版本。

第一个版本,纯1ms中断翻转。注意,LED在1ms中断里直接被翻转,它的频率是500Hz,肉眼看到的效果是LED亮度降低大概一半,而不是闪烁。人眼能清楚分辨闪烁的频率通常在几赫兹到几十赫兹,500Hz完全看不出来。这个版本存在的意义是验证中断确实以1ms周期在触发。

/* main.c */ /* USER CODE BEGIN PV */ volatile uint16_t timer_tick = 0; /* USER CODE END PV */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_Base_Start_IT(&htim1); /* USER CODE END 2 */ while (1) { } } /* USER CODE BEGIN 4 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } /* USER CODE END 4 */

第二个版本,软件分频实现经典LED闪烁。每1ms进一次中断,计数加一,累计到500次也就是500ms,才翻转一次LED。这样LED亮500ms、灭500ms,就是一个完整的一秒周期,肉眼看起来就是标准LED闪烁效果。

/* USER CODE BEGIN PV */ volatile uint16_t timer_tick = 0; /* USER CODE END PV */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { timer_tick++; if (timer_tick >= 500) { timer_tick = 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } }

两个版本里我都给timer_tick加了volatile修饰。这个关键字容易被忽略,但它在嵌入式里非常重要。timer_tick在中断里被修改,如果主循环里还要读取并判断它的值,没有volatile的话,编译器可能把变量的值优化到寄存器里缓存,导致主循环读到的永远是旧值。只要存在“一个变量在中断里被修改,在普通代码里被读取”这种情况,就一定要加volatile。

4. 实测验证与踩坑记录:为什么我的中断就是不进

代码写完了,烧录下载,一切正常当然好。但大多数情况下,事情不会那么顺利。这里我把自己实际调试中遇到过的问题按排查链路整理出来,每一条都是真实踩过的坑。

4.1 验证方法:先用肉眼,再用仪器

最简单的验证方式是把LED闪烁周期设置成1秒左右,直接观察闪烁是否正常。但肉眼观察只能判断“有没有问题”,不能精确验证1ms的准确性。如果手头有示波器或逻辑分析仪,可以把探针夹在LED引脚或者直接夹在GPIO输出引脚上,测量翻转频率。LED引脚上输出的是方波,周期应该是2秒翻转一次的软件分频结果,或者500Hz的1ms中断直接翻转结果。

如果没有示波器,还有一个土办法:把闪烁周期设置成10秒(比如分频到10000次),用手机秒表和真实时间对比。10秒的误差如果肉眼都无法分辨,说明大概在正常范围。但如果闪烁频率明显偏快或偏慢,那问题九成出在时钟树的配置上,我们往下看。

4.2 中断完全没进入:从现象到根因的完整排查链路

假设LED完全没反应,电平稳定在高或者低,说明GPIO翻转的代码压根没执行。这时候按顺序排查:

第一步,检查是否调用了HAL_TIM_Base_Start_IT(&htim1)。这是最常见的原因。CubeMX生成的代码里没有这一行,如果你忘了加,定时器永远不会启动。在我的经验里,这个坑的命中率高得惊人。

第二步,检查NVIC是否勾选了TIM1 update interrupt。回到CubeMX,打开TIM1配置,查看NVIC Settings。如果这里没勾选,中断请求会被交给外设,但CPU的NVIC不会响应该中断,中断服务函数永远不会被触发。有些朋友在CubeMX里明明看到“TIM1 update interrupt”,但没注意前面的勾选框是灰色的还是打钩的,这也是问题来源。

第三步,检查回调函数名是否拼写正确。HAL_TIM_PeriodElapsedCallback这个函数名有两处容易写错:PeriodElapsed中间不能漏字母,Callback的C不能写成大写。因为CubeMX生成的是弱定义,你重新定义的名字只要有一点点不同,比如PeriodElapsedCallBack,编译器不会报错,因为这是另一个新函数,中断地址还是指向原来的弱函数。这种“编译通过但没有实际效果”的错误最难查,需要逐字母对照原函数名。

第四步,检查是否有其他地方把TIM1的时钟关掉了。正常情况下CubeMX会在HAL_RCC_TIM1_CLK_ENABLE()里自动使能TIM1时钟,但如果你的代码里手动操作过RCC寄存器,或者用了低功耗模式,APB2总线上有时钟门控,TIM1可能被关掉了。这个在简单Demo里极少出现,但如果在复杂工程里发现中断不进,可以查一下。

4.3 时间不对:时钟树才是大多数问题的根源

另一种典型现象是:LED能闪,但闪烁周期明显不对。比如设置500ms翻转一次,实际却是2秒才翻一次。这种问题基本都出在时钟树配置上。

最常见的情况是外部晶振频率和CubeMX配置不一致。CubeMX工程里如果你没有手动改过PLL参数,系统默认可能用的是HSI内部8MHz RC振荡器,而不是外部8MHz晶振。内部RC振荡器精度比外部晶振差一些,时序会有一点偏差,但对于LED闪烁这种需求,偏差不会大到4倍。如果你看到时间偏差好几倍,那通常是时钟树页面里配置的主频和实际运行的PLL参数对不上。STM32F103C8T6从8MHz外部晶振倍频到72MHz,需要设PLL源为HSE、倍频系数9。如果某个配置把倍频系数写成了4.5或者别的值,时间就会成倍偏移。

还有一类问题出在“定时器时钟自动倍频”的机制上。前面提到过,F103系列有一个特殊规则:APB1或APB2的预分频系数如果大于1,那么该总线上的定时器时钟会自动乘以2。这意味着如果你把APB1的分频从2改成1,APB1总线频率还是36MHz(因为主频固定72MHz时,APB1分频1就是72MHz,分频2才是36MHz,这里需要看Clock Configuration里实际显示值)。实际上在CubeMX的时钟树页面里,所有定时器的时钟都会自动计算并显示。不管怎么改,盯住“Timer1 Clock”这一栏,确保它是72MHz,算出来的时间就不会错。

另外要注意一点:如果你用的是内置HSI而不是HSE,即使PLL配置正确,因为内部RC振荡器精度一般,长时间运行下来,时钟偏差会比外部晶振明显。对工业级定时来说,外部晶振几乎是必须的。

4.4 调试器的迷惑行为:下载后没反应

烧录完程序但板子没反应,很多人第一反应是代码问题,但有时候问题出在调试器和下载流程上。

ST-Link下载后默认不会自动复位运行芯片。也就是说,程序已经下载进Flash了,但CPU可能还停在复位状态,或者停在调试状态下,需要手动按一下板子上的复位按键才会开始运行。第一次接触的时候,这个“没反应”能让人以为是代码写错了。解决办法是在Keil或STM32CubeIDE的调试器设置里,找到Flash Download相关选项,勾选“Reset and Run”,这样下载完程序后芯片会自动复位运行。

调试模式下还有一个容易误解的现象。如果你打断点在HAL_TIM_PeriodElapsedCallback回调函数里,程序一进中断就会停住,然后其他代码不再执行,看起来就像整个系统卡死了。这其实不是BUG,而是调试器的正常行为,只是它放大了中断的执行频率。如果你把断点设在主循环里,因为中断软件分频的关系,主循环可能一直被“抢断”,导致观察到的程序流不稳定。建议在调试阶段少用断点,多看变量的实时变化:把timer_tick加入Watch窗口,如果它在持续增加,说明中断在正常触发。

还有一点,如果SYS配置里没有勾选Serial Wire,下载器连接可能会有问题。一旦你发现第一次能下载、第二次就提示找不到设备,十有八九是PA13/PA14被重新初始化成GPIO了。这时候按住板子的复位键再点下载,同时松开复位,通常可以抢救回来。配置里把这个选项勾上之后再也不会遇到这个问题。

5. 从Demo走向实战:时间片、PWM与TIM1的更高用法

1ms定时中断打通之后,很多人第一个想法是这个功能太简单了。确实,但它是后面一系列复杂应用的基石。

5.1 用Tick计数器实现多周期任务调度

定时中断里最实用的扩展就是把它当作系统心跳,然后在这个心跳上做多周期任务。典型做法是维护一个全局的毫秒计数器,然后每个任务设置自己的周期门槛。

volatile uint32_t system_tick = 0; volatile uint8_t led_toggle_flag = 0; volatile uint8_t key_scan_flag = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { system_tick++; if (system_tick % 500 == 0) { led_toggle_flag = 1; } if (system_tick % 10 == 0) { key_scan_flag = 1; } } }

主循环里就可以这样处理:

while (1) { if (led_toggle_flag) { led_toggle_flag = 0; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } if (key_scan_flag) { key_scan_flag = 0; /* 按键扫描逻辑 */ } }

这种写法的好处是,中断里只做积极的小事——置标志、计数、必要时置位,而真正耗时的逻辑都挪到主循环里处理。它避免了中断函数过于冗长的问题,同时让整个程序的执行节奏变得非常清晰。这个模式在裸机开发里非常常用,本质上就是最简单的时间片轮询。

要注意的是mod运算在中断里做会有一定开销,如果周期不是很大的情况下这完全是可接受的。如果想更极致一点,可以用减法判断避免取模运算,但对单片机来说这类优化通常不是必要。

5.2 TIM1的PWM扩展与主输出使能问题

TIM1的PWM功能是这个Demo之后最自然的延伸。如果你想让LED从闪烁变成呼吸灯效果,不需要额外定时器,直接把TIM1的某个通道配置成PWM输出即可。CubeMX里需要在TIM1配置面板把Channel1选为“PWM Generation CH1”,然后在Parameter Settings设置Pulse值,也就是初始占空比。生成代码后,要调用:

HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);

但高级定时器在这里有一个和普通定时器不一样的地方:还需要使能主输出。在STM32F103上,把TIM1的PWM使能之后,还需要加一行:

TIM_CtrlPWMOutputs(TIM1, ENABLE);

如果不加这一行,GPIO引脚上可能始终是低电平,没有任何波形。这个坑在TIM1/TIM8这类高级定时器上特别容易出现,因为普通定时器根本没有这个步骤。需要说明的是,这个主输出使能只影响输出比较和PWM功能,不影响我们前面做的定时中断,所以如果你只做中断,是不需要关心这个函数的。

还有一个和PWM相关但经常被忽略的问题:高级定时器的刹车输入(Break Input)。TIM1在某些引脚上有多路刹车输入,如果刹车功能被意外使能了,输出通道会被强制关闭。CubeMX默认情况下刹车是关着的,但如果你复用引脚时不小心开启了某个Break功能,PWM波形就会消失。排查的时候要注意这个方向。

呼吸灯方向的代码也很简单,动态修改CCR寄存器就能改变占空比。基于TIM1 PWM通道,你可以把LED从闪烁升级到呼吸灯,甚至用同样的PWM原理驱动蜂鸣器、舵机、无刷电机。TIM1“高级”这个称呼,其实是给你留足了未来扩展的空间。

我从个人经验角度说一句:第一次做定时中断建议老老实实走一遍从RCC时钟到NVIC再到回调函数的完整流程,中途不要跳过任何一步。虽然题目说5分钟能搞定,但那是建立在对CubeMX界面和HAL库中断机制都有基本了解的前提上的。第一次接触的话,多留10分钟看看时钟树和NVIC的数值,动手改一改PSC和ARR,再通过LED的闪烁快慢观察时间变化,这些实验比单纯复制一份Demo代码有价值得多。

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

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

立即咨询