嵌入式通用定时器原理与应用:从PWM生成到输入捕获实战

发布时间:2026/8/27 3:57:03
嵌入式通用定时器原理与应用:从PWM生成到输入捕获实战 1. 从“闹钟”到“心脏起搏器”为什么我们需要通用定时器在嵌入式开发的世界里时间或者说对时间的精准控制是一切复杂行为的基础。想象一下你正在设计一个智能家居的温控系统。它需要每隔5分钟读取一次温度传感器如果温度超过阈值就启动风扇同时它还要驱动一个LED指示灯以1Hz的频率闪烁表示系统正在运行并且它需要响应一个外部按键在按键按下后去抖动并执行相应操作。这些看似简单的任务——周期性执行、生成特定频率的信号、测量外部事件的时间间隔——如果只用主循环while(1)配合延时函数来实现代码很快就会变得臃肿、低效且难以维护。主循环会被各种延时阻塞无法及时响应其他事件整个系统的实时性会大打折扣。这时通用定时器General-Purpose Timer就扮演了系统中那个精准、可靠且多才多艺的“时间管家”角色。它就像微控制器内部一个独立运行的“小闹钟”或“节拍器”完全独立于CPU核心。你只需要对它进行一番设置告诉它“每隔1毫秒响一次”或者“从这个引脚上信号上升沿开始计时”它就能在后台默默工作。时间一到或条件满足它要么通过中断“喊”CPU来处理要么直接指挥其他硬件比如直接改变一个引脚的输出电平完全不需要CPU持续操心。这释放了CPU去处理更复杂的逻辑实现了真正的并行处理。因此理解通用定时器的基本原理是迈出从“简单单片机编程”到“设计真正嵌入式系统”的关键一步。它不仅仅是配置几个寄存器那么简单更是理解如何让硬件为你高效工作的思维转变。无论是STM32、GD32、ESP32还是其他任何主流MCU其通用定时器的核心思想都是相通的。掌握了它你就掌握了让嵌入式系统“活”起来并按照精确节奏运行的核心能力。2. 解剖一个通用定时器核心部件是如何协同工作的要理解定时器如何工作我们可以把它拆解成几个核心的功能模块。虽然不同厂商的定时器外设名称和细节略有差异但万变不离其宗其核心架构通常包含以下部分2.1 时钟源动力的来源定时器要“走时”首先需要一个时钟信号作为“心跳”。这个时钟源通常有多个选择内部时钟CK_INT最常见的选择通常来自系统主时钟如AHB、APB总线时钟经过一个预分频器PSC后提供。这是定时器最基础、最稳定的工作模式。外部时钟模式1定时器可以从特定的外部输入引脚TIx捕获外部信号的边沿用这个边沿来驱动计数器。这常用于测量外部脉冲的频率或占空比。外部时钟模式2使用另一个定时器Timer的输出或特定外部触发输入ETR作为时钟源。这可以实现定时器之间的级联构建更复杂的定时或计数系统。注意在STM32等基于ARM Cortex-M的芯片中定时器通常挂载在APB总线下。需要特别注意APB总线时钟可能会经过一个倍频器通常x1或x2后才作为定时器的实际时钟TIMxCLK。在计算定时时间时务必查阅参考手册确认最终的定时器输入时钟频率这是所有计算的基石。2.2 时基单元计时的心脏这是定时器的核心决定了“时间”是如何产生的。它主要由三个寄存器构成一个流水线预分频器寄存器PSC - Prescaler Register这是一个16位或更宽的寄存器。它的作用是对输入的时钟源进行“降速”。例如如果定时器输入时钟TIMxCLK是72MHz你将PSC设置为71那么实际驱动计数器的时钟频率就变成了72MHz / (711) 1MHz。这里“1”是因为分频器是从0开始计数的。PSC让你能够处理非常宽的定时范围从纳秒级到数秒甚至更长。计数器寄存器CNT - Counter Register这是一个16位或32位的向上/向下/中央对齐的计数器。它按照预分频后的时钟频率进行计数。每来一个时钟脉冲CNT的值就增加1向上计数或减少1向下计数。这是时间流逝的直接体现。自动重装载寄存器ARR - Auto-Reload Register这是一个16位或32位的寄存器它设定了计数器的周期或上限。在向上计数模式下当CNT的值从0递增到ARR时产生一个“更新事件”Update Event并且CNT自动清零重新开始计数。这样一个“定时周期”就完成了。定时周期T (ARR 1) * (PSC 1) / TIMxCLK。这三个寄存器共同构成了定时器的“时间基准”。通过灵活配置PSC和ARR我们可以得到几乎任意想要的定时周期。例如要产生一个1ms的定时中断假设TIMxCLK72MHz我们可以令(ARR1)*(PSC1) 72000。一种简单的配置是PSC71分频后1MHzARR999计数1000次这样每次计数1us1000次正好1ms。2.3 输入捕获与输出比较与外界交互的双手定时器不仅能自己数数还能感知外部世界和控制外部世界这是通过输入捕获和输出比较通道实现的。输入捕获Input Capture当指定的外部输入引脚如TI1发生跳变如上升沿时定时器会瞬间将当前计数器CNT的值“捕获”并锁存到一个专用的捕获/比较寄存器CCRx中。通过连续捕获两个边沿的CNT值我们就可以计算出这个外部脉冲的周期或高电平宽度。这是测量频率、脉宽、编码器信号的基石。输出比较Output Compare定时器会持续将计数器CNT的值与预先设定在捕获/比较寄存器CCRx中的值进行比较。当两者匹配时硬件会自动根据配置做出反应比如将对应的输出引脚如OCx置高、置低、翻转或者产生中断。这是生成PWM波、单脉冲、特定频率方波的核心机制。2.4 中断与事件生成发出信号的“喉咙”定时器在特定条件满足时需要通知CPU或触发其他硬件。这通过两类机制实现中断当发生更新事件计数器溢出、捕获事件、比较匹配事件等时如果对应的中断使能位被打开定时器就会向CPU的NVIC嵌套向量中断控制器发送一个中断请求。CPU会暂停当前任务跳转到对应的中断服务函数ISR中执行你编写的处理代码。中断需要CPU参与。事件这是更高效的硬件级联动。当特定条件满足时如比较匹配定时器内部会产生一个事件信号这个信号可以直接触发其他内置硬件模块如ADC开始转换、DMA进行数据传输甚至触发另一个定时器。整个过程完全由硬件完成无需CPU干预极大地提高了系统的实时性和效率。3. 核心工作模式深度解析不止是简单的“定时”很多人初学定时器只把它当作一个“定时中断发生器”。这大大低估了它的能力。理解其不同的工作模式才能将其威力发挥到极致。3.1 定时与计数模式这是最基础的模式但细节值得深究。定时模式以内部恒定频率时钟驱动计数器。我们的关注点在于时间间隔。通过PSC和ARR的配合我们可以设定一个非常精确的周期性时间基准。例如为操作系统提供系统时钟节拍SysTick或者为周期性传感器采样提供触发信号。计数模式计数器由外部引脚上的脉冲边沿驱动。我们的关注点在于脉冲个数。例如连接一个光电编码器计数器每收到一个脉冲就加一从而直接读取旋转的角度或位移。在这种模式下ARR往往被设置为一个很大的值如最大值65535我们关心的是CNT累计的数值。实操心得在需要极高精度的定时应用如生成精准的PWM控制电机中要特别注意时钟树的配置。确保定时器的时钟源稳定并且避免在定时器运行期间动态修改PSC或ARR这可能导致不可预测的延迟或毛刺。如果必须修改应使用定时器的“预装载”功能让修改在下一个更新事件时才生效以保证当前周期的完整性。3.2 PWM生成模式输出比较的终极应用PWM脉冲宽度调制是控制亮度、速度、功率的万能钥匙而通用定时器是生成PWM的硬件主力。在输出比较模式的基础上通过设置输出比较模式为“PWM模式1或2”并配合ARR和CCRx寄存器就能轻松生成PWM。ARR决定了PWM的周期频率。频率 TIMxCLK / ((PSC1)*(ARR1))。CCRx决定了PWM的占空比。在PWM模式1下向上计数时当CNT CCRx输出有效电平通常为高当CNT CCRx输出无效电平。因此占空比 CCRx / (ARR1)。高级定时器还支持互补输出带死区插入这是驱动三相电机逆变桥的关键用于防止上下桥臂直通短路。通用定时器虽然通常没有死区插入硬件但通过巧妙配置多个通道和输出比较模式也能模拟实现简单的多路同步PWM。3.3 输入捕获模式测量时间的尺子当需要测量一个未知信号的周期、频率或高电平宽度时输入捕获模式大显身手。其工作流程通常如下配置捕获通道为上升沿触发。第一个上升沿到来硬件瞬间将当前CNT值存入CCR1并可以触发一个中断。在中断服务程序中记录下CCR1的值Value1并将捕获边沿改为下降沿或使能另一个通道的下降沿捕获。下降沿到来CNT值被存入CCR2或另一个CCR再次触发中断。在中断中记录Value2。那么高电平时间 |Value2 - Value1| * 计数周期。如果需要测周期则再改回上升沿捕获获取下一个Value3周期 |Value3 - Value1| * 计数周期。避坑指南输入捕获的一个经典陷阱是“计数器溢出”。如果被测脉冲宽度很长可能在两个边沿之间16位的CNT计数器已经溢出归零了一次或多次。如果不处理计算出的时间就是错误的。解决方案在捕获中断中不仅要读取CCRx的值还要维护一个软件计数器如Capture_Overflow_Count在定时器的“更新中断”溢出中断中对该软件计数器加一。最终计算时间时公式为时间 [ (溢出次数 * (ARR1)) 本次捕获值 ] * 计数周期。务必在使能捕获中断前先使能并清空更新中断标志以防漏计第一次溢出。3.4 编码器接口模式读懂旋转的艺术这是定时器的一个“杀手级”专用模式用于直接接口正交编码器。只需将编码器的A、B相信号接到定时器的两个特定输入通道TI1, TI2硬件会自动根据两路信号的相位关系控制CNT进行向上或向下计数。正转时CNT递增。反转时CNT递减。并且硬件会自动处理4倍频在A、B相的每个边沿都计数从而将编码器的物理分辨率提高4倍。你只需要定期去读取CNT的值就能知道位置和方向的变化量CPU开销极低。ARR在此模式下通常设置为编码器旋转一圈对应的计数值如线数*4配合更新中断可以方便地计算圈数。4. 高级特性与实战联动构建系统级能力通用定时器不是孤岛它与MCU内部其他模块的联动能构建出更强大的系统功能。4.1 与DMA的配合实现“零CPU开销”的数据搬运想象一个场景你需要用ADC以固定的高采样率比如1MHz采集一段音频信号。如果每个采样点都用ADC转换完成中断来读取CPU将疲于奔命。此时可以用定时器触发ADC转换同时将定时器与DMA直接存储器访问控制器联动。配置定时器在输出比较模式或更新事件下产生一个触发信号TRGO。将这个触发信号连接到ADC的触发源。配置DMA将外设地址设为ADC数据寄存器内存地址设为你的数据缓冲区。每当定时器触发一次ADC转换ADC转换完成的数据就会通过DMA自动搬运到内存中。整个过程CPU只需要在开始时配置好并在DMA传输完成中断中处理整块数据即可实现了后台高速、连续的数据采集。4.2 定时器级联扩展时间与创造复杂波形单个16位定时器即使PSC设为最大值其定时范围也有上限。为了获得更长的定时周期可以将多个定时器级联。例如将Timer1的更新事件作为Timer2的时钟源。这样Timer1每溢出一次Timer2才计数一次。Timer2就成了Timer1的“分频器”两者结合可以实现天文数字级别的超长定时。此外通过一个主定时器Master触发多个从定时器Slave同步启动可以生成多路严格同步的PWM波形这在多相电机控制、数字电源等应用中至关重要。4.3 输出比较的“单脉冲模式”这个模式非常实用。它允许定时器在收到一个内部或外部触发信号后产生一个宽度可编程的单次脉冲。配置步骤通常是设置定时器为单脉冲模式。配置输出比较通道的极性、CCRx值决定脉冲宽度和ARR值。使能定时器并等待触发信号。触发信号到来后定时器启动输出指定宽度脉冲后自动停止。这常用于生成精确的延时脉冲或驱动需要单次触发的外部设备。5. 从原理到配置一个1kHz PWM与输入捕获的综合实验理论说得再多不如动手实践。我们设计一个综合实验用一个通用定时器同时实现在通道1上输出一个1kHz、占空比50%的PWM波同时用通道2测量一个外部按钮按下的高电平持续时间消抖后。实验设定MCU为STM32F103系统时钟72MHz定时器TIM3。5.1 步骤一计算与规划首先进行核心计算PWM频率1kHz周期T 1/1000Hz 1ms。我们希望计数器有足够的精度来调节占空比因此让ARR尽可能大。选择计数器时钟为1MHz则计数一次为1us。ARR 周期 / 计数周期 - 1 1ms / 1us - 1 999。要得到1MHz的计数器时钟需对72MHz进行72分频。PSC 72MHz / 1MHz - 1 71。占空比50%CCR1 (ARR 1) * 50% - 1 1000 * 0.5 - 1 499。注意有些库函数或硬件设计占空比计算为CCR/(ARR1)此时CCR直接设为500即可需根据实际调整输入捕获我们使用通道2。计数器时钟同样是1MHz即分辨率1us。假设我们测量按钮按下时间预计在10ms~2s之间16位计数器最大值65535对应65.535ms可能不够。因此必须开启更新中断来处理溢出。5.2 步骤二初始化代码与关键注释以下是基于标准外设库SPL风格的核心初始化思路实际开发中可能使用HAL或LL库但原理一致。// 变量定义 volatile uint32_t Button_Press_Start 0; // 捕获开始时CNT的快照含溢出 volatile uint32_t Button_Press_Width 0; // 最终计算的脉宽 volatile uint8_t Is_Captured 0; // 捕获完成标志 volatile uint16_t Overflow_Count 0; // 软件溢出计数器 void TIM3_GPIO_Init(void) { // 初始化GPIO // PA6 - TIM3_CH1 复用推挽输出 (PWM输出) // PA7 - TIM3_CH2 浮空输入 (输入捕获) // ... GPIO配置代码 } void TIM3_Mode_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 时基单元配置 TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; // 时钟分频与滤波相关 TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 2. 输出比较通道1配置为PWM模式1 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 499; // CCR1值决定占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 有效电平为高 TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载防止修改时产生毛刺 // 3. 输入捕获通道2配置 TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 初始为上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; // 映射到TI2 TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; // 每个边沿都捕获 TIM_ICInitStructure.TIM_ICFilter 0x08; // 设置滤波器防抖动。0x08代表8个时钟周期滤波 TIM_ICInit(TIM3, TIM_ICInitStructure); // 4. 中断配置 // 使能更新中断用于溢出计数和捕获/比较2中断 TIM_ITConfig(TIM3, TIM_IT_Update | TIM_IT_CC2, ENABLE); // 配置NVIC NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 5. 启动定时器 TIM_Cmd(TIM3, ENABLE); }5.3 步骤三中断服务函数逻辑这是整个测量功能的核心包含了处理计数器溢出的关键逻辑。void TIM3_IRQHandler(void) { // 1. 处理更新中断计数器溢出 if (TIM_GetITStatus(TIM3, TIM_IT_Update) ! RESET) { Overflow_Count; // 软件溢出计数器加1 TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } // 2. 处理捕获/比较2中断 if (TIM_GetITStatus(TIM3, TIM_IT_CC2) ! RESET) { if (Is_Captured 0) { // 第一次捕获上升沿 // 在读取CCR2前先记录当前的溢出状态这是一个原子操作的关键点 uint32_t temp_overflow Overflow_Count; uint16_t temp_ccr TIM_GetCapture2(TIM3); // 计算真正的起始计数值考虑溢出 Button_Press_Start (temp_overflow 16) | temp_ccr; // 合并为32位值 // 改变捕获边沿为下降沿 TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Falling); Is_Captured 1; // 标记已捕获上升沿 } else { // 第二次捕获下降沿 uint32_t temp_overflow Overflow_Count; uint16_t temp_ccr TIM_GetCapture2(TIM3); uint32_t button_press_end (temp_overflow 16) | temp_ccr; // 计算高电平时间单位微秒 // 注意处理结束值可能小于开始值的情况因为Overflow_Count可能已清零 // 这里简化处理假设测量时间内Overflow_Count未发生回绕 Button_Press_Width button_press_end - Button_Press_Start; // 差值即为计数值 // 转换为微秒因为计数器时钟是1MHz1个计数1us // Button_Press_Width 的值就是以微秒为单位的时间 // 重置状态准备下一次测量 TIM_OC2PolarityConfig(TIM3, TIM_ICPolarity_Rising); Is_Captured 0; Overflow_Count 0; // 也可以不清零但每次计算需使用差值逻辑 } TIM_ClearITPendingBit(TIM3, TIM_IT_CC2); } }5.4 关键点分析与避坑滤波器配置TIM_ICFilter对于机械按钮必须配置输入滤波器上述代码中设为0x08这要求信号稳定8个时钟周期才被认为有效可以滤除大部分抖动。这是硬件消抖比软件延时消抖更可靠、更节省CPU。溢出处理的原子性在捕获中断中读取Overflow_Count和CCRx的顺序至关重要。如果先读CCRx再读Overflow_Count但在这之间恰好发生了一次更新中断导致Overflow_Count增加就会造成数据不一致CCRx是旧的Overflow_Count是新的。更稳健的做法是在进入捕获中断后先连续读取两次Overflow_Count如果值相同说明读取期间没有发生更新中断这个值是有效的如果不同则重新读取。或者可以暂时关闭更新中断读取后再打开但会影响溢出计数的准确性。上面的简化代码假设测量时间较短溢出计数器不会在两次读取间变化。32位时间计算我们将Overflow_Count软件16位和CCRx硬件16位合并成一个32位的绝对时间戳这是处理长周期测量的标准方法。计算脉宽时直接对32位时间戳做减法。预装载Preload对于PWM的CCR和ARR寄存器务必使能预装载TIM_OCxPreloadConfig和TIM_ARRPreloadConfig。这样你修改的值会先写入一个缓冲寄存器直到下次更新事件时才真正生效防止在一个PWM周期中间改变占空比导致输出畸形脉冲。通过这个综合实验你将亲身体会到通用定时器如何同时、独立地处理输入和输出任务以及如何处理实际工程中的细节问题如消抖、溢出和原子操作。这远比单纯配置一个定时中断要复杂但也正是这些细节构成了稳定可靠的嵌入式系统的基石。