定时器中断实验全解析:从51到STM32的原理、代码与调试 📅 发布时间:2026/9/20 15:18:32 👁 浏览次数: 简介这是一份单片机定时器/中断程序设计的实验报告文档主要面向学习8051/8052单片机的初学者或相关课程学生帮助理解定时器/计数器T0、T1的工作原理、TMOD与TCON寄存器的配置方法并通过LED闪烁实验掌握中断控制的基本流程。文档内容包含完整的实验目的、设备清单、原理讲解、接线说明、程序流程图、C语言源代码及注释同时提供了Keil uVision4编译和PZ-ISP下载调试的实践指导读者可据此独立完成实验并撰写实验报告。整个资料包仅有1个doc文档压缩后大小约489KB内容集中精炼适合快速查阅和模仿练习。目前已有124人学习下载对于想要系统梳理定时器中断知识并完成课程实验的读者而言是一份简洁实用的参考材料。 定时器中断是嵌入式学习路上绕不开的一道坎也是绝大多数人第一次真正体会“程序不是在跑循环而是被事件驱动”这个概念的关键节点。这个实验看起来只是把LED翻一翻、把计数值打印到串口但背后牵涉到定时器的时钟树配置、预分频与重装载值的计算、中断服务函数的编写规范、以及中断与主循环之间的数据交互方式。我用51和STM32两套平台都完整跑过这个实验踩过不少坑也总结出了一套从原理到落地的完整思路。这篇文章就把整个实验从原理拆解到具体代码再到常见问题的排查链路一次讲清楚。1. 定时器中断的底层逻辑计数器、预分频器和重装载值是怎么配合的很多初学者拿到定时器中断实验的第一个反应是照着例程把寄存器或CubeMX配置一遍灯亮了就算完事。但这样做的后果是一旦让你换个定时周期、换颗芯片就完全不会改了。所以第一步必须先搞清楚定时器内部到底是怎样工作的。1.1 定时器的本质就是一个“数时钟脉冲”的计数器无论51单片机的Timer0/Timer1还是STM32的通用定时器TIM2/TIM3/TIM4其核心都是一个计数器Counter。这个计数器在时钟信号的驱动下不断加1当它的值增加到我们设定的上限也就是重装载值通常叫ARR或TH/TL组合值时就会产生一个更新事件Update Event同时计数器归零重新开始计数。这个“时钟信号”从哪来两种来源内部时钟来自芯片内部的时钟树比如51的机器周期12个振荡周期、STM32的APB1外设时钟。外部时钟从芯片的某个引脚输入脉冲信号计数器数的是外部脉冲这就是后面做输入捕获测频率的基础。定时器中断实验用的是内部时钟所以核心就变成两件事决定计数器每加1需要多少时间、决定计数器数到多少产生一次中断。1.2 预分频器PSC的真正作用把高频时钟降到你用得起的节奏先看一个具体场景。STM32F103的TIM2挂在APB1上时钟是72MHzAPB1预分频为2时定时器时钟倍频为72MHz意味着计数器每秒钟能数72000000下。如果我们想要一个1ms的定时中断也就是计数器从0数到某个值要用掉0.001秒那么这个“某个值”就是72000。直接数到72000行不行可以但如果你想定一个比较“奇怪”的时间比如1.7ms那就要算出一个带小数的计数值整数除法会带来误差。更重要的是ARR寄存器只有16位STM32基本定时器或32位TIM2/TIM5等计数范围有限。预分频器就是干这个用的它先把72MHz分频成更低频的时钟让计数器每次加1的时间变大。公式如下定时器时钟频率 外设时钟 / (PSC 1) 定时中断周期 (PSC 1) * (ARR 1) / 外设时钟比如要求1ms中断72MHz主频(PSC 1) * (ARR 1) 72000000 * 0.001 72000选PSC 71即分频系数72则ARR 1 1000即ARR 999。时钟频率 72MHz / 72 1MHz即计数器每1微秒加1 计数器数到1000次恰好1ms这个组合的妙处在于计数器每个计数的单位变成了1us程序里想要什么时间间隔直接算次数就行可读性和可维护性都强很多。1.3 51单片机的定时器计算方式思路完全一致51单片机没有预分频器寄存器除了一些增强型51但它有一个更原始的手段定时器的工作模式。以最常见的模式116位定时器为例它直接数机器周期机器周期 12 / 晶振频率。12MHz晶振下机器周期为1us所以定时器每加1就是1us。要定时1ms就是让计数器数1000次。16位计数器的最大值是65535所以初值要设为初值 65536 - 1000 64536 0xFC18也就是把TH0设为0xFCTL0设为0x18。中断发生后必须在服务函数里重新给TH0/TL0赋初值这就是51和STM32的一个显著区别——STM32的ARR自动重装载不需要软件干预而51需要手动“重新装弹”。注意51单片机的定时器一旦溢出如果想继续以同样周期中断必须在中断服务函数里手动重新装入初值。漏了这一步定时周期就会变成65536个机器周期约65.5ms现象就是LED闪烁频率突然慢得离谱。1.4 定时器时钟源选择内部时钟和外部时钟的差别在STM32的CubeMX配置里Timebase Source和定时器的Clock Source经常被混为一谈这是很多人容易犯糊涂的地方。Timebase Source是HAL库自己用来维护 tick 的时钟源一般选SysTick它是操作系统的“心跳”。如果改成了某个定时器HAL库的延时函数会依赖这个定时器必须初始化好它。定时器的Clock Source才是我们配置定时器时选择的时钟来源Internal Clock内部时钟用于普通定时External Clock Mode外部时钟模式用于数外部脉冲。做定时器中断实验只需要明确定时器的时钟源选Internal Clock即可Timebase Source保持SysTick或按CubeMX默认。我见过有人在配置时把Timebase Source改成TIM2然后发现程序卡死在HAL_Delay里就是因为库里先用了这个定时器但还没初始化它。1.5 手算一次完整的中断周期建立直觉根据我个人经验做这个实验前最好能手算一次哪怕后面完全靠CubeMX生成也要能验证生成的参数对不对。举一个STM32G474的例子内部时钟170MHzAPB1定时器时钟也是170MHz。想要10ms中断(PSC 1) * (ARR 1) 17000000 * 0.01 170000为了计数精度高选PSC 169分频170则计数时钟1MHz计数器每次加1用1us。此时ARR 1 170000 / 170 1000所以ARR 999于是配置Prescaler 169Period 999。这里有个小经验为了让延时精确尽量让计数时钟是一个好算的整数频率如1MHz、100kHz然后ARR直接就是目标时间对应的计数个数调试时心里特别有底。2. 中断服务函数的设计规范为什么不能在里面做延时和复杂运算定时器中断触发后CPU会跳转到对应的中断服务函数ISR。这个函数的写法直接决定了整个系统的实时性和稳定性。很多刚入门的同学喜欢把LED翻转、串口打印、数据处理全堆在中断里用不了多久就会遇到各种诡异问题。2.1 中断服务函数里到底该写什么ISR里最应该做的事情只有三件清除中断标志位、保存必要的数据、设置一个标志变量通知主循环去处理。为什么不能直接在中断里做大量工作原因在于中断服务函数执行期间其他同优先级或低优先级的中断无法被响应。如果你在ISR里写了一个延时1ms的函数这1ms内系统对外的响应能力基本等于0。更严重的情况是如果ISR执行时间超过了下一次中断的触发间隔中断就会丢失或者持续抢占主循环导致系统看起来像死机了一样。我的一个项目就踩过这个坑在定时器ISR里直接调用了一个软件I2C的读取函数结果I2C从设备偶尔会拉低时钟线等待ISR被阻塞了将近2ms导致高优先级的按键中断无法响应按键按下去经常没反应。排查了很久最后把I2C读取挪到主循环用标志位触发问题立刻消失。所以中断服务函数里比较稳妥的写法可以用51的代码来说明// 51单片机定时器0中断模式1 void Timer0_ISR(void) interrupt 1 { static unsigned int tickCount 0; TH0 0xFC; // 重新装载初值定时1ms TL0 0x18; tickCount; if (tickCount 500) // 累计500ms { tickCount 0; flag_500ms 1; // 置标志位主循环处理 } }主循环里void main() { Timer0_Init(); while (1) { if (flag_500ms 1) { flag_500ms 0; LED !LED; } } }这种“中断只置标志位主循环做处理”的结构是嵌入式编程里非常基础也非常重要的设计模式。它能保证中断响应时间极短系统的并发处理能力大幅提升。2.2 中断服务函数的命名规则与声明方式不同平台、不同库中断服务函数的写法差别很大标准51使用interrupt 关键字并指定中断号如void Timer0_ISR(void) interrupt 1。中断号对不上中断根本无法触发。STM32标准外设库在stm32f10x_it.c中实现void TIM2_IRQHandler(void)函数名由启动文件决定不能改。STM32 HAL库在TIM2_IRQHandler中调用HAL_TIM_PeriodElapsedCallback用户重写这个回调函数即可。HAL库的写法稍微特殊一点。CubeMX生成的代码里TIM2_IRQHandler是已经定义好的它内部会调用弱函数HAL_TIM_PeriodElapsedCallback。我们只需要在自己的文件中重写这个弱函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) // 判断是哪个定时器 { // 在这里翻转LED或置标志位 } }这里有个非常容易踩的坑多个定时器中断共用同一个回调函数。比如TIM2和TIM3都开启了周期中断HAL_TIM_PeriodElapsedCallback会被调用两次每次通过检查htim-Instance来区分是哪个定时器产生的事件。如果没有这个判断两个定时器的事件就会互相干扰。2.3 中断标志位清除的时机先清理还是后清理清除中断标志位是ISR里的标准动作。51单片机由于是硬件自动清零定时器溢出标志TF0在进入中断后由硬件自动清除所以不需要手动操作。但STM32不同HAL_TIM_PeriodElapsedCallback执行完之后HAL库内部会负责清除更新中断标志位不需要用户手动清除。不过如果是用寄存器方式开发STM32就必须在ISR里手动写void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 用户代码 } }清标志位的位置也有讲究。有些新手会把清标志位放在用户代码的最后如果一个意外导致前面的代码执行时间很长这个中断标志会一直挂在那里退出ISR后马上又进一次中断形成一个高频率的重复进入状态导致主循环永远得不到执行。稳妥的做法是先清除标志位再执行用户处理逻辑。2.4 为什么中断不能阻塞一个亲身经历的排查案例热搜词里有“中断为什么不能阻塞”这个说法我以前也调试过一个很头疼的问题。一个基于STM32F407的数据采集板通过串口接收上位机的命令同时用定时器中断做AD采样。现象是跑一段时间后串口就再也不响应了但LED还在闪。查到最后发现原因串口接收中断服务函数里为了解析一帧变长协议我写了一个循环等待数据接收完成的代码这个循环在任何条件下都没有设置退出超时。一旦协议数据中间丢了一个字节接收状态机就会卡在等待状态中断被阻塞后续所有中断都无法执行。定时器中断本来还能触发但因为串口中断优先级更高定时器中断也进不去系统就像挂了一样。这个教训让我养成了一个铁律ISR里严禁出现任何形式的等待循环wait loop。需要等数据、等外设状态、等标志位一律在主循环里用状态机处理ISR只做最简单、最确定、最快速的记录和置位操作。3. 51与STM32定时器中断实验的关键差异与操作细节同一个实验在51和STM32上做出来的体会完全不同。很多人是从51起步后来切换到STM32但两套框架的设计哲学差异如果不搞清楚代码很容易写得不伦不类。3.1 51单片机定时器实验寄存器操作直来直去适合理解原理51的定时器配置没有那么多中间层直接操作特殊功能寄存器SFR就行。以定时器0为例void Timer0_Init() { TMOD 0xF0; // 清除Timer0的配置位 TMOD | 0x01; // 模式116位定时器 TH0 0xFC; // 初值高字节 TL0 0x18; // 初值低字节 ET0 1; // 使能Timer0中断 EA 1; // 总中断使能 TR0 1; // 启动定时器 }这里有一个很多教程不讲的细节为什么每次配置TMOD之前要先清除低4位因为TMOD寄存器的高4位控制定时器1低4位控制定时器0。如果直接赋值TMOD 0x01会把定时器1的配置覆盖掉。在模块化编程时这个习惯能避免很多莫名其妙的问题。51的另一个特色是中断号。interrupt 1对应Timer0interrupt 3对应Timer1。如果写错程序编译不会报错但中断永远不会触发LED纹丝不动新手会在这个问题上卡很久。3.2 STM32定时器中断实验CubeMX配置与HAL库代码解析STM32推荐直接用STM32CubeMX生成初始化代码省去手动配置时钟树的麻烦。但前提是理解每一步在干什么。CubeMX里的关键配置项有Prescaler (PSC)预分频值最终分频结果是PSC 1。Counter Period (ARR)自动重装载值计数器数到这个值产生更新事件。Auto-Reload Preload自动重装载预装载。建议开启。开启后ARR寄存器的值在更新事件时才真正生效避免在运行中修改周期时出现“这次中断用了旧值下次用了新值”的错位现象。NVIC Settings使能TIM2 global interrupt否则中断不会被CPU响应。CubeMX生成的初始化代码看起来绕但其实逻辑很清晰static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig {0}; TIM_MasterConfigTypeDef sMasterConfig {0}; htim2.Instance TIM2; htim2.Init.Prescaler 71; // 72MHz / 72 1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 999; // 1MHz * 1000 1ms htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim2); // ... }然后在主函数里启动定时器中断HAL_TIM_Base_Start_IT(htim2);这句代码特别容易漏。很多人配置完CubeMX也写了回调函数但忘了调用HAL_TIM_Base_Start_IT导致定时器虽然初始化了但更新中断并没有使能。现象是程序正常运行但中断回调函数永远不执行。经验HAL_TIM_Base_Start_IT是启动定时器并使能更新中断的总开关。每次排查“定时器中断不触发”这类问题时先检查这一行有没有执行到。3.3 HAL库定时器回调函数的阻塞隐患与优化HAL库的设计目标是代码可移植性和易用性但代价是中断响应路径变长。TIM2_IRQHandler到HAL_TIM_PeriodElapsedCallback之间HAL库做了很多检查和处理这些都需要时间。如果你的系统对定时中断的实时性要求极高比如做电机控制器的电流环需要固定频率斩波用HAL库的定时器中断可能会引入几微秒到十几微秒的抖动。这种情况下建议的做法是不用HAL库的回调机制直接在启动文件里的TIM2_IRQHandler中写简洁的处理逻辑。或者使用寄存器操作比如直接操作TIM2-SR、TIM2-ARR这些寄存器减少中间层损耗。更高要求的场景就需要用定时器的DMA触发或硬件比较输出把核心逻辑下沉到硬件外设彻底绕开CPU中断响应。对于入门实验HAL库完全够用但要有这个意识回调函数也不是想写多重就写多重ISR的时间预算永远是稀缺资源。3.4 滴答定时器SysTick与定时器的区别它们不是一回事很多人在学定时器中断时会问SysTick不是也可以做定时吗为什么要另外用TIMSysTick是ARM Cortex-M内核自带的24位递减计数器它的主要用途是给操作系统提供一个时钟节拍tickHAL库的HAL_Delay和HAL_GetTick都依赖它。它的特点是配置简单、不占用外设定时器资源但它只有24位最大计数16777215且通常固定给系统使用。如果你在CubeMX里把Timebase Source改成某个定时器一定要保证这个定时器的时钟树、初始化顺序都正确否则HAL_Delay会失效。我在一个项目里把Timebase Source从SysTick改成了TIM6因为SysTick要留给RTOS结果忘了初始化TIM6程序启动就卡死在HAL_Delay的while循环里。这个坑只需要踩一次就记住了。4. 定时器中断程序设计实验的进阶方向从LED翻转走向真实应用LED翻转只是实验的起点。定时器中断真正的价值在于它为我们提供了一个稳定的时间基准在这个基础上可以搭建出各种实用的功能。4.1 按键消抖利用定时器中断实现“扫描式”按键检测机械按键按下和释放的瞬间电平会来回跳变这个现象叫抖动持续时间一般在5~20ms。最简单的消抖方式是用延时函数“抖过去”但延时期间CPU被占用系统无法做其他事。利用定时器中断可以设计一个按键扫描方案每隔5ms进入一次中断读取一次按键电平连续读到3次累计15ms相同电平才认为按键状态有效。这种思想其实是“时序采样”大大提高了按键检测的可靠性。如果配合状态机一个定时器中断甚至可以同时扫描多个按键// 伪代码示意 void Timer_ISR() { currentKey ReadKey(); if (currentKey lastKey) { debounceCount; if (debounceCount 3) { debounceCount 0; keyEvent currentKey; // 产生一个有效的按键事件 } } else { lastKey currentKey; debounceCount 0; } }4.2 利用定时器捕获模式测频率与脉宽用GPIO模拟测量信号频率只能靠翻转计数误差大且占用CPU。STM32的定时器输入捕获模式才是真正的处理手段。基本思路是配置定时器的一个通道为输入捕获模式外部信号从对应的引脚输入定时器检测到上升沿或下降沿时会把当前的计数器值锁存到捕获寄存器里。连续捕获两次上升沿两次锁存的CNT值之差就是信号的周期以计数时钟为单位。我做过一个用TIM3捕获任意波形频率的测试误差能控制在0.1%以内而且全程不占用CPU。核心配置就是把定时器时钟源保持内部时钟但通道模式选Input Capture。这个功能在测量传感器PWM输出、编码器频率等场景非常实用。4.3 使用定时器中断做多任务时间片轮转嵌入式开发中经常需要同时处理多个周期性任务比如每1ms扫描一次按键、每10ms刷新一次数码管、每100ms采集一次温度、每1s上报一次数据。如果每个任务都用独立的定时器芯片的定时器资源再丰富也不够用。更好的方案是只用一个定时器产生1ms的基准中断在中断里对一个计数器累加然后根据不同的阈值置不同的标志位void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { static unsigned short msCount 0; if (htim-Instance TIM2) { msCount; if (msCount % 10 0) flag_10ms 1; if (msCount % 100 0) flag_100ms 1; if (msCount 1000) { msCount 0; flag_1s 1; } // 1ms任务直接在这里做比如按键扫描 } }主循环里依次检查这些标志位执行对应任务。这种时间片轮转的结构能极大提高代码的架构清晰度也让多个功能模块之间的耦合大大降低。4.4 用Cron表达式或高级定时器做复杂调度有必要吗热搜词里出现了“cron定时器表达式前端组件”有些朋友会想单片机里能不能也搞一套复杂的定时调度我的观点是51或STM32上做复杂调度优先级应该给RTOS比如FreeRTOS而不是自己写一套套壳的定时器调度器。RTOS的任务延时、软件定时器、消息队列天然就是为这种需求设计的。但如果你目前还在裸机阶段又不希望引出一个大而全的系统那么时间片轮转 标志位这个模式配合一个结构体数组把每个任务的周期和上次执行时间记录下来已经能覆盖绝大多数应用场景。别再往复杂里堆保持简单永远是嵌入式软件的第一原则。5. 定时器中断程序设计的常见问题排查与实测心得这个实验做完总会遇到一些问题。有些问题你不提前了解排查起来可能要花半天甚至更久。下面是我实际调试过程中遇到过的、也帮别人定位过的问题清单。5.1 中断根本不触发先从这五个方向查定时器中断“完全没反应”是最常见的问题。排查顺序建议如下排查方向具体检查点定时器是否启动51查TR0/TR1是否为1STM32查HAL_TIM_Base_Start_IT是否被调用中断是否使能51查ET0和EASTM32查CubeMX的NVIC里是否勾选了TIM中断函数名是否正确51查中断号是否对应STM32查回调函数名是否拼写正确时钟配置是否正常用示波器或串口打印验证系统主频是否符合预期标志位是否被意外清除检查有没有多行代码重复清标志导致中断事件丢失如果这些都查完还是不行还有一个容易被忽略的点启动文件中没有包含中断处理函数。某些定制版开发板或非标准启动文件可能没有为某个外设的中断安装处理句柄需要手动在启动文件里补上。5.2 定时时间不准确误差在哪定时时间偏长或偏短通常不是定时器本身的问题而是系统和定时器时钟频率的实际值与理论值不一致。比如STM32F103的APB1最大是36MHz但定时器时钟是72MHz这个倍频关系很多人不清楚。APB1预分频系数为1时定时器时钟等于APB1时钟36MHzAPB1预分频系数不为1时定时器时钟自动变为APB1的两倍72MHz。CubeMX默认配置APB1预分频为2所以TIM2的时钟就是72MHz。如果你改动了时钟树配置这些频率都会跟着变之前算的参数就作废了。51单片机也一样用12MHz晶振时机器周期为1us如果用11.0592MHz晶振为了波特率准确机器周期就变成了约1.085us定时器的初值就要重新算。5.3 中断里修改定时周期为什么有时生效有时不生效在运行过程中动态修改定时周期是很常见的需求比如修改PWM频率、修改采样周期。直接写htim-Instance-ARR xxx或者TIM2-ARR xxx可能不会立刻生效因为自动重装载预装载功能Auto-Reload Preload会让新值在下一个更新事件才被加载。如果你希望修改立即生效可以用两种方式关闭自动重装载预装载Auto-Reload Preload Disable这样ARR的值直接写入影子寄存器立即生效。或者手动生成一个更新事件UG位置1强制把预装载值加载进去。我在做逆变器频率调整时就遇到过这个问题界面显示想要60Hz但输出的PWM频率要迟一个周期才切换到新值在开环控制下这个延迟通常无所谓闭环控制下就会引起短暂的系统震荡。最后还是利用了UG位手动刷新问题解决。5.4 中断标志位冲突同一定时器的多个事件源怎么区分STM32的一个定时器可以同时产生多种中断事件更新中断更新事件、捕获中断CC1IE、比较中断CC1IE但不同比较通道等。这些事件共享同一个中断入口TIMx_IRQHandler但用不同的状态位和中断使能位。在HAL库环境下如果同时使用了定时器的PWM输出和输入捕获又开启了更新中断回调函数里就必须区分事件类型。只判断htim-Instance是不够的还需要判断具体是哪一种事件触发了中断。用寄存器方式会更直白if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 更新事件处理 } if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); // 捕获事件处理 }顺序上有个讲究先处理捕获事件再处理更新事件因为捕获事件对实时性更敏感。这也算是一个实战细节吧。5.5 实测中发现的HAL库回调写法要点最后说一个关于HAL库回调函数的细节。CubeMX生成的stm32f1xx_it.c中的TIM2_IRQHandler是弱函数关联的如果你在别的文件中重新定义了HAL_TIM_PeriodElapsedCallback需要确保这个文件被正确编译链接。不同CubeMX版本生成的HAL库版本不同弱函数声明的位置也略有差异。一般来说在tim.c文件末尾追加回调函数即可但要注意有些项目里已经有一个HAL_TIM_PeriodElapsedCallback被其他模块占用了这时候再用同名函数就会导致链接错误。建议的做法是把所有定时器相关的回调集中放在一个文件里统一管理避免多个文件重复定义同名弱函数。我做PY32F003的一个项目时HAL库的回调机制就有一些小坑。后来发现不是因为HAL库本身有bug而是回调函数里硬件外设的时钟开关、中断优先级分组在芯片上电后还没有完全配置好回调函数一进入就访问了尚未使能时钟的外设寄存器。只要在初始化顺序上调整一下问题就消失了。6. 基于本项目可以扩展的几个练习思路实验做完之后建议不要立刻丢到一边。定时器中断这个知识点延展性强得惊人下面这几个小练习都由浅入深做完之后你对中断和定时器的理解会上一个台阶。PWM呼吸灯用一个定时器产生PWM在中断里逐步修改比较值让LED亮度从暗到亮再暗。注意更新中断频率和PWM频率之间的关系不要把更新中断开得比PWM频率还高。PWM输出占空比100%网上有人问STM32F103定时器输出占空比到不了100%原因通常是CCR寄存器的值等于ARR时某些定时器模式会忽略比较匹配需要在初始化时把输出的初始电平设为有效电平。这个问题正好用本实验的更新中断来观察比较事件。外部脉冲计数把定时器配成外部时钟模式用信号发生器输入已知频率的方波记录1秒内捕获到的脉冲数和频率计对照。这个练习能加深对外部时钟和内部时钟切换的理解。红外遥控解码红外遥控信号是38kHz载波调制的一组脉宽编码需要精确测量高低电平的持续时间。用定时器捕获输入引脚上的边沿每次捕获都记录计数器差值主循环里根据差值解析出数据位。这个项目用到输入捕获、时间片轮转、状态机复杂度适中做完收获会特别大。基于定时器的多通道软件串口这个方法比较进阶可以在没有UART外设的IO口上实现串口发送。利用一个高速定时器中断每个bit切换到对应的IO电平按波特率节拍发送数据。用它来佐证“定时器中断是系统的时钟心脏”这个说法再合适不过。写到这里定时器中断程序设计实验的完整思路和实操细节基本都覆盖了。从计数器的底层原理到中断服务函数的写法再到51和STM32两套平台的具体实现最后落到常见问题的排查方法都是实打实的经验。如果这个实验你做到了“看到现象就条件反射地知道中断怎么进的、周期怎么算的、参数怎么改”那这个知识点就算真正过关了。本文还有配套的精品资源点击获取