ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103输入捕获测量PWM占空比与周期详解

STM32F103输入捕获测量PWM占空比与周期详解 简介STM32F103输入捕获工程资源面向需要测量外部PWM信号周期与占空比的嵌入式开发者与学习者提供一套基于Keil5的完整工程示例。资源围绕定时器输入捕获原理展开从定时器初始化、输入捕获通道配置到中断服务程序编写再到占空比与周期的计算逻辑均有可直接运行的代码支撑同时涉及上升沿/下降沿捕获的计数值处理与滤波、极性等常用参数设置能帮助理解实际项目中的信号测量方法。包内共140个文件以h头文件、c源程序为主辅以uvprojx工程文件、hex/axf可执行文件以及map、lst、crf等编译链接产物压缩包约3.2MB结构清晰便于在Keil5中直接打开、编译和二次修改。已有3305人学习这份工程资料适合在电机控制、PWM波形分析、频率计等场景对照参考也可作为定时器捕获类项目调试与排错时的实用资料。 做嵌入式开发的人迟早会遇到一个需求手里有路PWM信号想测出它的频率和占空比。示波器当然能看但总不能每台设备都配一台示波器。用STM32F103的输入捕获功能来做这件事是性价比最高、也最贴合单片机应用场景的方案。这篇文章就围绕“stm32f103输入捕获得到占空比与周期”这个主题把原理、配置、代码、踩坑一次讲透。先说清楚这东西能干什么。输入捕获是定时器的一个工作模式它能在引脚出现指定边沿上升沿或下降沿时把定时器计数器的当前值“快照”到捕获寄存器里。借助两次相邻捕获值的差值就能反推出边沿之间的时间间隔也就是信号的周期或者高电平持续时间进而算出频率和占空比。适合用来测量PWM信号、编码器脉冲、遥控器信号等周期性波形。这篇文章适合刚接触STM32定时器、正在做电机调速/电源控制/信号检测类项目的朋友参考。1. 项目整体设计与思路拆解1.1 为什么用输入捕获而不是外部中断计时很多新手拿到这个问题第一反应是“用外部中断上升沿进中断读定时器计数不就行了”。理论上确实可以但实际工程里这么做会踩两个坑。第一个坑是中断延迟。外部中断服务函数从触发到真正读取计数器中间隔着压栈、跳转、读取等多条指令。如果这时候系统里还有其他中断在跑延迟会更大。而这个延迟是随机抖动的测出来的周期和占空比就会不停跳精度大打折扣。第二个坑是中断频率。如果被测信号是10kHz一秒就是1万次中断每次中断都要读计数值、做减法、存结果CPU占用率非常高干不了别的活。输入捕获完全靠硬件完成边沿检测和计数值锁存CPU只需要在捕获中断里把寄存器值读走就行负载小得多。输入捕获的本质是“硬件帮你在精确的时刻按下快门”这个“快照”动作由芯片内部逻辑硬连线完成不经过CPU因此时间戳的精度只取决于定时器时钟本身和代码执行路径无关。1.2 占空比和周期的测量模型PWM信号的一个完整周期包含高电平和低电平两段。要得到占空比需要两个时间量周期 T连续两个上升沿之间的时间高电平时间 Ton上升沿到下降沿之间的时间占空比 Ton / T × 100%单靠一个通道做输入捕获也能测这两个量——捕获上升沿得到周期捕获下降沿得到Ton。但这里有一个策略问题如果在上升沿和下降沿之间切换捕获极性代码逻辑会比较绕而且容易在边沿密集时漏事件。更省事的方案是用定时器的两个通道同时捕获通道1捕获上升沿通道2捕获下降沿。硬件各自独立检测互不干扰中断里只需要区分是哪个通道的事件分别记录计数值。下面要讲的实现就是这个双通道方案。1.3 定时器和引脚选型逻辑STM32F103有多达8个定时器其中TIM1和TIM8是高级定时器TIM2/3/4是通用定时器TIM6/7是基本定时器无输入捕获功能。做输入捕获通用定时器就够用。引脚的选择要看定时器的通道映射。以TIM3为例它的通道1默认在PA6但可以通过重映射功能挪到PB4。具体用哪组引脚需要结合板子的实际走线。大多数蓝板STM32F103C8T6最小系统板上PA6和PA7都是直接引出的所以TIM3_CH1PA6和TIM3_CH2PA7这一组组合就成了输入捕获的经典搭配几乎不需要额外飞线。2. 输入捕获核心原理与参数配置2.1 边沿检测与计数器快照的硬件行为想用好输入捕获得理解STM32定时器内部的工作流程。每个捕获通道都对应一个输入滤波器和边沿检测器。引脚信号进来后先经过一个可编程的采样滤波器滤掉毛刺然后边沿检测器根据你配置的触发条件上升沿、下降沿、双边沿产生一个触发事件。这个触发事件会做两件事把TIMx_CNT计数器当前值锁存到对应的捕获寄存器TIMx_CCRx同时把状态寄存器里对应的捕获标志位置1。如果你开启了捕获中断还会向NVIC发出中断请求。需要注意的关键点是捕获寄存器锁存的是“触发瞬间的计数值”你读到的时刻已经晚了几个周期也没关系因为值是硬件锁存下来的不会被覆盖除非下一次捕获又来了。这就像有人帮你把比赛冲线瞬间的照片拍好了你晚点去取照片照片依然是冲线那一帧。2.2 定时器时钟与分频系数计算测量精度和量程是矛盾的而分频系数正好是调这个矛盾的旋钮。STM32F103的内部定时器时钟来自APB1总线当APB1预分频为2时定时器时钟就是72MHz。如果直接拿72MHz去计数每个计数单位约13.89ns1秒溢出约27次16位计数器最大65535显然不够覆盖常见的PWM周期需要做预分频。预分频系数PSC决定计数器时钟计数频率 72MHz / (PSC 1)。16位计数器最大计满65536个脉冲。所以最大可测量时间 65536 × (PSC1) / 72MHz。需要测量周期范围在1kHz到10kHz中心值是1kHz1ms周期留足余量按2ms算。倒推最大可测时间要≥2ms即65536 × (PSC1) / 72MHz ≥ 2ms解得 PSC1 ≥ 72MHz × 0.002 / 65536 ≈ 2.2。取PSC18即PSC7计数频率9MHz分辨率约111ns。1ms周期对应9000个计数1kHz到100kHz都在16位范围内精度也够用。2.3 引脚、滤波器和中断优先级配置引脚配置用复用推挽输入模式注意不是普通输入。GPIO模式设置为GPIO_Mode_IPD下拉输入或GPIO_Mode_IPU上拉输入都不对必须用AFIO复用功能让引脚和定时器通道连起来。输入滤波器是为了应对信号抖动。RC滤波电路是物理层面的而STM32的输入滤波器是数字采样式的它对输入信号以某个频率连续采样只有连续N次采到相同电平才认为电平真的变了。采样频率由ICF位和定时器时钟决定。对于比较干净的数字信号滤波级别设到0或1即可如果是电机PWM这种带毛刺的信号适当加大滤波级别能省去外部滤波电路。中断优先级的设置要遵循一个原则捕获中断应该尽可能抢占优先级高避免被其他中断延迟太久。但不建议高于系统滴答定时器否则会影响延时函数的时间基准。实践中我习惯设为抢占优先级1子优先级0。3. 完整代码实现与关键配置解析3.1 定时器初始化双通道交替捕获配置直接用标准外设库来写代码清晰且可读性好。完整初始化函数如下void TIM3_Cap_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x00; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Falling; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x00; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_Cmd(TIM3, ENABLE); TIM_ClearFlag(TIM3, TIM_FLAG_CC1 | TIM_FLAG_CC2); NVIC_InitStructure.NVIC_IRQChannel TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); TIM_ITConfig(TIM3, TIM_IT_CC1 | TIM_IT_CC2, ENABLE); }需要注意GPIO模式用的是浮空输入。如果信号源是推挽输出浮空完全OK如果是开漏输出外部必须要有上拉电阻否则高电平读不准。有些资料建议用上拉输入这其实取决于信号源特性不必盲目照抄。3.2 中断服务函数周期与高电平时间的捕获逻辑中断处理是整个测量的核心。代码逻辑上通道1每次捕获到上升沿说明一个周期开始了通道2捕获到下降沿说明高电平结束。通过记录相邻两个上升沿的计数值差值就得到周期上升沿和下降沿的差值就得到Ton。直接上代码这段我实测过逻辑清晰u32 RisingVal 0; u32 FallingVal 0; u32 PeriodVal 0; u32 TonVal 0; float Frequency 0; float DutyCycle 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); if (RisingVal 0) { RisingVal TIM_GetCapture1(TIM3); } else { PeriodVal TIM_GetCapture1(TIM3) - RisingVal; if (TIM_GetCapture1(TIM3) RisingVal) { PeriodVal TIM_GetCapture1(TIM3) (u32)65535 - RisingVal; } Frequency 72000000.0f / ((float)(7 1) * (float)PeriodVal); RisingVal TIM_GetCapture1(TIM3); } } if (TIM_GetITStatus(TIM3, TIM_IT_CC2) ! RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_CC2); FallingVal TIM_GetCapture2(TIM3); TonVal FallingVal - RisingVal; } if (TonVal 0 PeriodVal 0 TonVal PeriodVal) { DutyCycle (float)TonVal / (float)PeriodVal * 100.0f; } }这段代码里有个容易被忽视的细节周期计算里做了“计数器回绕”判断。TIM3的计数器是16位的计数到ARR值后会清零重新开始。如果上升沿发生在计数值接近65535的位置下一次捕获时计数已经回绕到了很小的值直接做减法会得到负数。这里用了一个简单粗暴的办法如果新值小于旧值就加上65535再减。这个处理在信号周期接近量程上限时非常关键不加的话测出来的周期会突然跳变。3.3 高频抖动与测量错的边沿代码写完了不代表测量就准。实际测试中如果发现数据跳得厉害多半是边沿检测被噪声干扰了。这里的“干扰”分两种输入信号本身带毛刺以及代码层面漏读了捕获值。输入毛刺最好办初始化里把TIM_ICFilter从0调到3或者5就行。但要记住滤波级别调高会引入额外延迟并且这个延迟在上升沿和下降沿上是一致的所以对占空比计算影响不大。代码层面的漏读更隐蔽。如果主循环在读DutyCycle和PeriodVal的瞬间中断恰好更新了这两个变量就会出现“读到一半被改写”的问题。这个问题的正统解法是关中断读或者在中断里只置标志位由主循环统一读取。volatile u8 CapUpdated 0; volatile u32 PeriodValCopy 0; volatile u32 TonValCopy 0; int main(void) { while (1) { if (CapUpdated) { __disable_irq(); PeriodValCopy PeriodVal; TonValCopy TonVal; CapUpdated 0; __enable_irq(); float duty (float)TonValCopy / (float)PeriodValCopy * 100.0f; printf(Period:%u Ton:%u Duty:%.2f%%\r\n, PeriodValCopy, TonValCopy, duty); } } }短时间关中断在这个场景下是安全的因为整个复制过程只有几行赋值语句纳秒级别不会影响捕获功能。但要注意不能在关中断里做浮点运算或者printf那些操作耗时太长。3.4 预分频与自动重装值的配合再回到参数配置上初始化函数里的arr和psc有些微妙之处。如果arr设成65535也就是0xFFFF计数器就是最大量程也是最常用的做法。如果arr设小了比如设成10000计数器到10000就清零此时即使计数值没有超过65535也可能提前回绕必须保证被测信号周期对应的计数值小于ARR值。一个更完善的初始化可以这样调用TIM3_Cap_Init(0xFFFF, 7);这里0xFFFF对应最大量程PSC为7对应9MHz计数频率。按这个参数可测的最小频率约为 9MHz / 65536 ≈ 137Hz最大可测信号周期约7.28ms。如果被测信号周期更长就需要增大PSC代价是分辨率降低。比如PSC设为71计数频率1MHz可测周期最大65.5ms但每个计数单位是1us精度降到微秒级。分辨率、量程、精度三者不可兼得要根据实际信号范围做取舍。这是输入捕获参数配置的核心矛盾也是面试官最喜欢问的点。4. 常见问题与排查技巧实录4.1 捕获寄存器读出一直为0这个现象多半是引脚复用没配对。检查三处GPIO时钟是否开启、AFIO时钟是否开启、GPIO是否真的配置成了复用输入。还有一点容易忽略——如果代码里在别处把这个引脚复用成了其他外设比如USART1默认就在PA9/PA10但有人会把PA9手动复用成TIM2_CH3就会出现引脚冲突捕获自然不工作。更直接的办法是读一下GPIO配置寄存器的值确认不是被其他代码改掉了。4.2 周期正确但占空比总差一点如果周期数值稳定占空比却系统性偏大或偏小优先怀疑两个问题。第一个是滤波延迟。前面提到滤波级数加大后上升沿和下降沿的检测点都会延后但因为两级滤波参数相同理论上差值不变。实际由于信号上升/下降沿的斜率不对称滤波延后量可能不相等造成占空比偏移。解决方法是滤波级别尽量不要设太高能滤掉毛刺就行。第二个是代码时序。如果TonVal的计算用到的RisingVal是“当前还没有更新”的旧值就会出现固定偏差。我遇到过一种情况通道1中断更新RisingVal之后通道2中断马上来了但此时用了旧RisingVal导致Ton偏大或偏小一个周期。把中断里Ton的计算改成用“最近一次RisingVal”并且把RisingVal用static或volatile修饰就不会出现这个问题。4.3 高占空比90%以上和低占空比测量异常占空比接近极限时TonVal和PeriodVal的差值非常小此时计数误差的占比就很大。比如占空比99%PeriodVal10000TonVal9900如果边沿抖动导致TonVal误差10个计数占空比就偏差0.1%。看起来不多但在精密电源控制里这个误差不可接受。这种情况下有几个办法提高计数频率让每个周期对应的计数值更大从而稀释误差多次测量取平均或者换用PWM输入模式——有些STM32定时器专门支持这个模式用两个通道联动直接硬件测周期和占空比连软件计算都省了。F103没这个硬件特性只能靠软件做平滑。4.4 中断频率过高导致主循环卡死当被测信号频率很高比如100kHz以上每秒就有20万次中断主循环几乎被压缩到无法执行。这时候如果只在中断里置标志位主循环还在慢慢处理就会造成数据堆积。解决办法很简单不需要每次都计算可以每隔N次捕获才更新一次计算结果。在中断里加一个计数器每8次或16次中断才刷新一次PeriodVal和TonVal相当于软件降采样主循环的负载立刻降下来。这个技巧在测高频信号时尤其实用。5. 实测数据与精度分析拿一个NE555搭建的可调PWM信号源来实测输出频率设为1kHz通过串口打印测量结果。连续采集50次数据波动情况是这样的频率基本稳定在999.7Hz到1000.3Hz之间占空比在50%附近波动±0.15%左右。这个误差主要来自NE555自身的温漂和信号抖动输入捕获本身的量化误差在这个量级以下。如果把PSC从7改成3即4分频计数频率18MHz分辨率能提升到约55ns但最大可测周期缩短到3.64ms意味着最低只能测约275Hz的信号。PSC7在1kHz下的计数为9000T111ns量化误差约0.0012%远优于信号源本身的稳定性所以继续提高分辨率对实际测量结果改善有限。这个实测也验证了一个观点输入捕获的测量精度上限很高实际误差往往不是来自捕获机制而是来自信号源本身和滤波电路的相移。所以提高测量精度最有效的手段是保证信号干净、边沿陡峭而不是一味调高计数频率。6. 代码工程化与扩展建议6.1 代码健壮性溢出和复位处理上面的示例代码能跑但离工程化还有距离。几个需要补强的点第一计数器回绕处理。前面用了65535的土办法但如果在回绕瞬间恰好发生了两个上升沿极端情况计算会出错。更严谨的做法是开启更新中断在溢出时记录溢出次数计算差值时把溢出次数乘以65536加上去。第二如果长时间没有信号RisingVal停留在旧值TonVal和PeriodVal就会是垃圾数据。可以在主循环里加超时判断如果超过预期时间没有捕获中断就认为信号丢失输出0并报警。第三所有共享变量都要加volatile修饰防止编译器优化导致读取不到最新值。这在开启-O2优化时尤其重要不加volatile有时候会得到非常诡异的结果。6.2 功能扩展从测量到控制闭环输入捕获不只是用来“看”信号更常见的用途是作为闭环控制的一部分。比如做电机调速时编码器输出频率与转速成正比用输入捕获测出频率就能算出转速再与目标转速做PID运算输出PWM控制电机。这种方案比使用外部编码器模块成本更低很多时候能省掉一路专用IC。另一个典型扩展是多路信号测量。用TIM3的两个通道测一路信号如果要同时测多路可以启用TIM2、TIM4等其他定时器。每路信号独立测量互不干扰。项目里我最多同时测过4路不同频率的PWM效果很好各路之间完全独立。6.3 一个小技巧利用捕获值反推输入频率最后分享一个调试时经常用到的技巧。如果信号频率未知可以先用一个比较大的PSC比如71对应1MHz计数频率测出大概周期然后根据测到的计数区间自动调整PSC再次测量以获得更高分辨率。这个“自动量程”的思路类似于万用表的自动档位切换在固定频率场景下没必要做但面对未知信号时非常实用。实现也不复杂第一次测量得到PeriodVal如果PeriodVal小于8000太小说明频率高需要加大计数频率就把PSC调小如果PeriodVal接近65535快溢出了就把PSC调大。动态修改PSC需要在更新事件里重新加载执行TIM_PrescalerConfig(TIM3, newPsc, TIM_PSCReloadMode_Update)即可。用STM32F103做输入捕获测量PWM占空比和周期本身并不复杂但要把精度做上去、把异常情况处理干净靠的还是对定时器工作机制的理解和实际调试中积累的细节。这套方案从原理到代码我用了很久也踩了不少坑。如果你在配置过程中遇到什么问题优先检查引脚复用和计数器回绕这两个点这是八成问题的根源所在。本文还有配套的精品资源点击获取
返回列表