
说实话第一次看到“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中断背后的时钟数学很多教程让你直接填Prescaler71、Period999但没解释为什么。如果只抄参数遇到外部晶振不是8MHz的板子或者改了系统时钟时间就不对了。这里我把计算过程完整捋一遍。STM32F103C8T6这种经典芯片常见配置是外部8MHz晶振HSE经过PLL锁相环9倍频得到72MHz系统主频。系统主频经过总线预分频器分配给各个外设APB2预分频1所以APB2总线频率72MHzTIM1挂在APB2上直接使用72MHz。APB1预分频2所以APB1总线频率36MHz但请注意STM32F1系列的特殊规则如果APB1预分频大于1那么挂在APB1上的定时器时钟会自动乘以2恢复为72MHz。这就是为什么CubeMX的时钟树页面里TIM2的时钟也是72MHz的原因。有了TIM1时钟72MHz这个前提接下来是定时器本身的工作原理。定时器内部有一个预分频器PSCPrescaler和一个自动重装载寄存器ARRAuto-Reload Register。实际信号流向是TIM1_CLK → PSC分频 → 计数时钟 → 计数器CNT从0加到ARR → 溢出 → 触发更新事件/中断因此定时器溢出一次的周期公式是Tout (PSC 1) × (ARR 1) / Fclk注意这里两个值都要加1因为PSC和ARR都是从0开始计数的。比如PSC0时预分频器的分频系数是1不是0。要得到1ms中断目标Tout 1ms 0.001sFclk 72MHz 72,000,000Hz取PSC71分频后计数频率 72,000,000 / (711) 1,000,000Hz也就是1MHz每1us计数一次取ARR999溢出周期 (9991) × 1us 1000us 1ms用公式验证一遍就是(711) × (9991) / 72,000,000 72 × 1000 / 72,000,000 0.001s完全对得上。以后如果想要其他定时周期可以直接套这个公式反推。目标中断周期PSCARR实际计数频率说明1ms719991MHz最经典组合10ms7199991MHz计数器范围变大100ms71999991MHz注意16位定时器上限655351us07172MHz72个时钟周期溢出一次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面板里能看到各种外设分类我们需要先处理两个最基础的模块。第一个是RCCReset and Clock Control也就是时钟控制。在System Core → RCC里把High Speed ClockHSE设置为“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显示36MHzAPB2显示72MHz。记住只要外部晶振是8MHz这套配置就是F103最主流的72MHz主频方案。2.2 TIM1参数面板的逐项解释而不是照抄数值在左侧Categories里展开Timers找到TIM1勾选“Activated”。然后点击TIM1进入配置面板在Parameter Settings里设置以下参数Prescaler填71Counter Mode选择Up向上计数Counter Period填999Internal Clock Division保持默认“No Division”Auto-Reload Preload选择EnableRepetition Counter填0这些参数里Prescaler和Counter Period的计算原理上面已经讲过一个是分频系数决定计数频率一个是预装载值决定溢出周期。这里重点说一下另外两个容易让人迷惑的选项。Auto-Reload Preload翻译过来是“自动重装载预装载”。设置为Enable后计数器溢出时ARR寄存器的值不会立即生效而是先存入影子寄存器等到发生更新事件时才真正加载进计数器。这种机制的好处是避免在计数过程中修改ARR导致时间跳变。在我们的Demo里ARR在初始化时就被写死了Enable和Disable效果上没有区别但养成Enable的习惯是好事情以后做PWM动态调频率时会少踩很多坑。Repetition Counter是高级定时器TIM1/TIM8特有的参数通用定时器没有。它的作用是“每隔N1次溢出才触发一次更新事件”。默认是0也就是每次溢出都会触发更新事件对应我们的1ms中断。如果你把它设为1TIM1就要计数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_IRQHandlerHAL库的设计已经帮你把中断处理封装好了你只需要在用户代码里实现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改成1APB1总线频率还是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代码有价值得多。