STM32输入捕获测PWM频率:Proteus仿真与工程实践
2026/9/19 12:06:15 网站建设 项目流程

1. 项目缘起与整体设计思路

搞嵌入式开发的朋友大概率都遇到过这样的场景:手头有个PWM信号源,不管是传感器输出的转速脉冲、电机驱动板的反馈信号,还是自己用555搭的方波发生器,你总得知道它到底跑在什么频率上。用示波器看当然最省事,但产品量产测试、设备长期在线监测这类场合,总不可能给每台设备配一台示波器。这时候用STM32的输入捕获功能去测PWM频率,就成了一个性价比极高、也足够精准的方案。

这次我选的是STM32F103C8T6这颗经典芯片,配合Proteus做纯仿真验证。为什么要在Proteus里先跑一遍?原因很实在:输入捕获涉及定时器配置、中断优先级、信号边沿检测这些容易出错的环节,直接在硬件上调试,一旦测不准你很难判断是代码问题还是信号本身的问题。Proteus的好处是信号源参数完全可控,你可以精确知道输入是1kHz还是10kHz,测出来的结果对不对一目了然。等仿真里跑通了,再往真实硬件上移植,心里就有底了。

这个方案适合谁参考?我觉得三类人最合适:一是刚学STM32定时器、对输入捕获概念还比较模糊的初学者,仿真环境能让你直观看到寄存器和中断的变化;二是需要做频率测量类项目的工程师,比如电机转速反馈、流量计脉冲采集、超声波回波测距,这套代码框架可以直接改改用;三是习惯用Proteus做教学演示或方案预研的朋友,整个工程文件结构清晰,方便二次修改。

核心思路其实不复杂:用一个定时器产生PWM信号(作为被测对象),用另一个定时器的输入捕获通道去捕捉这个信号的上升沿,通过计算两个上升沿之间的时间差,换算成频率。这里有个关键选择——是用PWM输入模式还是普通输入捕获模式?PWM输入模式是STM32定时器的一个特殊功能,一个通道同时映射到两个IC寄存器,能自动测量周期和占空比,硬件开销小、精度高。但它对引脚映射有硬性要求,只有特定通道组合才能用。普通输入捕获模式则更灵活,任何带IC功能的通道都能用,代价是需要自己处理边沿检测和计数器溢出。考虑到很多实际项目里信号引脚是硬件定死的,我这次选择用普通输入捕获模式来实现,通用性更强,移植到其他STM32型号也更容易。

提示:Proteus仿真STM32F103C8T6时,建议使用8.9及以上版本,元件库对C8T6的支持比较完整。低版本可能找不到对应的VSM模型,或者仿真时定时器行为异常。

整个工程我分成三个部分来设计:PWM信号发生模块、输入捕获测量模块、以及串口输出模块。PWM用TIM3的通道1在PA6引脚输出,频率设为2kHz、占空比50%,方便计算和观察。输入捕获用TIM2的通道1在PA0引脚接收,配置为上升沿捕获,开启捕获中断。串口用USART1在PA9/PA10,把测到的频率值打印出来。这样在Proteus里,你可以用虚拟终端直接看到测量结果,不用额外接LCD。

为什么选TIM2和TIM3?STM32F103C8T6的定时器资源里,TIM1是高级定时器,功能最全但配置也最复杂;TIM2到TIM4是通用定时器,输入捕获和PWM输出都支持。TIM2和TIM3的引脚在C8T6上都有引出,PA0是TIM2_CH1,PA6是TIM3_CH1,布线方便,不会打架。而且这两个定时器挂在APB1总线上,时钟频率都是72MHz,计算预分频和重装载值时不用考虑总线差异,省心。

关于时钟配置,这里多说一句。STM32F103C8T6最高主频72MHz,但定时器的时钟不是直接等于系统时钟。APB1总线默认是36MHz,但定时器时钟有个倍频机制:当APB预分频系数不为1时,定时器时钟是APB时钟的2倍。所以TIM2和TIM3的实际时钟是72MHz。这个细节在计算定时器周期时非常关键,很多人第一次配定时器发现时间不对,就是栽在这里。我在代码里会把这个计算过程写清楚,你照着改参数就行。

2. 核心细节解析与实操要点

2.1 输入捕获的测频原理与参数计算

输入捕获测频率的本质,就是测量信号一个完整周期的时间,然后取倒数。STM32的定时器在捕获到指定边沿时,会把当前计数器的值锁存到捕获寄存器里,同时可以触发中断。我们在中断里读取这个值,和上一次捕获的值做差,就得到了一个周期内的计数值。再用这个计数值乘以定时器的计数周期,就是信号周期,取倒数就是频率。

听起来简单,但有几个细节直接决定测量精度。第一个是定时器的计数频率。如果计数频率太低,比如1MHz,那测1kHz信号时一个周期只有1000个计数,分辨率就是1Hz;测10kHz信号时只有100个计数,分辨率变成10Hz,误差就大了。所以计数频率要远高于被测信号频率。我这次把TIM2的预分频器设为71,这样计数频率是72MHz/(71+1)=1MHz,计数周期1微秒。测2kHz信号时,一个周期有500个计数,分辨率约0.2%,足够用了。

第二个细节是计数器溢出。TIM2是16位定时器,最大计数值65535。在1MHz计数频率下,最多能测约65.5毫秒的周期,对应最低频率约15Hz。如果被测信号频率低于这个值,计数器就会溢出,测量结果就错了。解决办法有两个:一是降低计数频率,比如把预分频改成719,计数频率100kHz,能测最低1.5Hz,但分辨率会下降;二是开启溢出中断,记录溢出次数,在计算时把溢出次数乘上65536加进去。我这次为了代码简洁,先不处理溢出,因为2kHz远高于下限。但你在实际项目里如果测低频信号,一定要加上溢出处理,这个坑我后面会详细说。

第三个细节是捕获中断的处理时间。中断里要做的事情越少越好,否则可能错过下一个边沿。我的做法是在中断里只做三件事:读捕获值、算差值、置标志位。频率的具体计算放到主循环里做,这样中断执行时间最短。

2.2 Proteus元件选型与电路连接

Proteus里搭建这个仿真电路,需要的元件不多,但有几个地方容易踩坑。核心元件是STM32F103C8T6,在元件库里的名字是STM32F103C8,注意没有T6后缀。放置后双击,在Program File里加载Keil编译出来的hex文件,Crystal Frequency填8MHz(外部晶振)或者直接选72MHz内部时钟,这个要和代码里的时钟配置一致。

PWM信号源我直接用STM32自己的TIM3产生,这样省去了外部信号发生器,也方便调频率。如果你非要用外部信号源,Proteus里有555定时器可以搭,但555的频率稳定性在仿真里不如直接用定时器输出。TIM3的CH1在PA6,用一根导线连到PA0(TIM2_CH1)就行。注意Proteus里STM32的引脚是按端口分组的,PA0和PA6都在PORTA那一组里,找的时候别找错。

串口输出需要用到虚拟终端。在元件库搜VIRTUAL TERMINAL,放置后把RXD接到PA9(USART1_TX),TXD接到PA10(USART1_RX),波特率在终端属性里设为9600。这样程序里printf出来的频率值就能在终端窗口看到。有个小细节:Proteus的虚拟终端默认可能不显示,需要在仿真运行时右键终端,勾选Echo Typed Characters,不然你只能看到输出但看不到自己输入的内容。

电源部分别忘了。STM32F103C8T6的VDD和VSS要接上,Proteus里虽然不接也能跑,但有时候会出现莫名其妙的问题。我一般把VDD接+3.3V,VSS接地,VDDA和VSSA也接上,NRST通过一个10k电阻上拉到3.3V。BOOT0和BOOT1接地,确保从主闪存启动。

注意:Proteus仿真STM32时,如果程序跑不起来,先检查Program File路径有没有中文,hex文件是不是最新编译的。这两个问题占了仿真失败原因的八成以上。

2.3 Keil工程配置与代码框架

Keil5里新建工程,器件选STM32F103C8。这里有个选项要注意:Run-Time Environment里要不要勾选CMSIS的Core和Device Startup?我建议勾上,这样启动文件和系统初始化代码自动包含,省得自己加。但如果你用的是标准外设库而不是HAL库,那就不要勾,手动添加库文件更可控。我这次用标准外设库,因为代码量小、执行效率高,适合这种功能单一的场景。

工程里需要添加的文件包括:启动文件startup_stm32f10x_md.s(中容量器件用md)、标准外设库的stm32f10x_gpio.c、stm32f10x_rcc.c、stm32f10x_tim.c、stm32f10x_usart.c,以及misc.c(中断优先级配置用)。主函数里按顺序初始化时钟、GPIO、TIM3 PWM、TIM2输入捕获、USART1,然后开中断,进主循环。

时钟初始化用SystemInit(),这个函数在system_stm32f10x.c里,默认会把系统时钟配到72MHz。但如果你用的是外部晶振,要确保HSE_VALUE宏定义是8000000,并且Proteus里晶振频率也设成8MHz。如果不想折腾外部晶振,可以直接用内部HSI,但HSI精度不如外部晶振,测频率时误差会大一些。仿真环境下两者差别不大,但养成用外部晶振的习惯对实际项目有好处。

中断优先级配置容易被忽略。TIM2的捕获中断和USART1的中断如果同时开启,要确保捕获中断的优先级更高,否则串口发送时可能延迟捕获响应。我用NVIC_PriorityGroup_2,抢占优先级和响应优先级各2位。TIM2捕获中断设为抢占1、响应1,USART1设为抢占2、响应2。这样捕获中断能打断串口中断,保证测量实时性。

3. 实操过程与核心环节实现

3.1 TIM3 PWM输出配置:产生被测信号

先看PWM信号发生部分。TIM3挂在APB1上,时钟72MHz。我要产生2kHz、50%占空比的方波。计算过程如下:定时器计数频率设为72MHz/(71+1)=1MHz,计数周期1微秒。2kHz信号周期500微秒,所以自动重装载值ARR设为500-1=499。占空比50%,比较值CCR设为250。

配置步骤用标准外设库写出来是这样的:

void TIM3_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 499; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 250; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }

这段代码里有个地方值得展开说:GPIO模式必须设为GPIO_Mode_AF_PP,也就是复用推挽输出。如果你设成普通的推挽输出,定时器的PWM信号出不来,PA6一直是高电平或低电平。这个错误很隐蔽,因为代码编译没问题,仿真里就是没波形。我当初第一次调的时候在这卡了半小时,后来用Proteus的电压探针测PA6才发现一直是高电平。

另外TIM_OCMode_PWM1和PWM2的区别也要搞清楚。PWM1模式下,计数器小于CCR时输出高电平,大于时输出低电平;PWM2反过来。我选PWM1,配合TIM_OCPolarity_High,出来的波形就是先高后低,占空比50%。如果你发现波形占空比反了,先检查这两个参数。

3.2 TIM2输入捕获配置:捕捉上升沿

输入捕获的配置比PWM输出稍微复杂一点,因为涉及中断和滤波器。先看代码:

void TIM2_Capture_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 65535; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &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 = 0x0; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM2, ENABLE); }

这里有几个关键点。GPIO模式设为GPIO_Mode_IPD,也就是下拉输入。为什么用下拉?因为PA0如果悬空,电平不确定,可能乱触发捕获。下拉之后,没有信号时PA0是低电平,只有真正的上升沿才会触发。如果你用外部信号源,信号空闲时是低电平,那下拉没问题;如果空闲是高电平,就要改成上拉,捕获极性也要相应改成下降沿。

TIM_ICFilter是输入滤波器,范围0到15。值越大滤波越强,能滤掉高频噪声,但也会引入延迟。仿真环境下信号很干净,设0就行。实际项目中如果信号有毛刺,可以设成0x4或0x8试试。这个参数我踩过坑:有一次测电机编码器信号,没开滤波,频率读数跳得厉害,后来把滤波器设成0x6,读数就稳了。

TIM_ICPrescaler设的是TIM_ICPSC_DIV1,也就是每个边沿都捕获。如果你测的是高频信号,中断太频繁影响CPU效率,可以设成DIV2或DIV4,每2个或4个边沿才捕获一次。但这样测出来的周期要乘以分频系数才是真实周期,计算时别忘了。

中断服务函数里做三件事:

volatile uint32_t capture_value = 0; volatile uint32_t period_ticks = 0; volatile uint8_t capture_flag = 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { static uint32_t last_capture = 0; uint32_t current_capture = TIM_GetCapture1(TIM2); if (last_capture != 0) { period_ticks = current_capture - last_capture; capture_flag = 1; } last_capture = current_capture; TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); } }

注意last_capture是静态变量,第一次捕获时它还是0,这时候不能算差值,否则会得到一个巨大的数。所以加了个if判断,只有第二次及以后的捕获才计算周期。这个细节看起来简单,但如果你忘了,第一次测量结果会完全错误,而且因为只错一次,很容易被忽略。

3.3 频率计算与串口输出

主循环里检测capture_flag,一旦置位就计算频率并通过串口打印:

int main(void) { SystemInit(); USART1_Init(); TIM3_PWM_Init(); TIM2_Capture_Init(); printf("PWM Frequency Measurement Start...\r\n"); while (1) { if (capture_flag) { capture_flag = 0; uint32_t freq = 1000000 / period_ticks; printf("Frequency: %lu Hz\r\n", freq); } } }

频率计算公式是1000000/period_ticks,因为TIM2的计数频率是1MHz,每个计数代表1微秒。period_ticks是一个周期内的计数个数,取倒数再乘以1000000就是频率。比如period_ticks=500,频率就是2000Hz,和设定的PWM频率一致。

串口初始化里,波特率设为9600,8位数据位,1位停止位,无校验。printf重定向需要实现fputc函数:

int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }

这个函数里等待TC标志位很关键。如果不等待,连续发送时会丢数据。我试过用TXE标志,也能工作,但TC更稳妥,因为TC表示整个帧发送完成,TXE只表示发送寄存器空。

提示:Keil里要勾选Use MicroLIB,否则printf可能无法正常工作。在Options for Target -> Target标签页里勾选。

3.4 Proteus仿真运行与结果验证

把Keil编译出来的hex文件加载到Proteus的STM32模型里,时钟频率设为8MHz,点击运行。虚拟终端里应该能看到不断打印的频率值。如果一切正常,显示的是2000Hz左右,可能有正负1Hz的波动,这是正常的,因为捕获时刻和计数器时钟不是严格同步的。

想验证测量是否准确,可以在TIM3的配置里改ARR值。比如把ARR改成999,PWM频率变成1kHz,终端里应该显示1000Hz。改成249,频率4kHz,终端显示4000Hz。我实测下来,在1kHz到10kHz范围内,测量误差不超过0.5%,对于大多数应用足够了。

如果你在Proteus里看不到输出,先检查虚拟终端的波特率是不是9600,再检查PA9有没有接到终端的RXD。还有一个容易忽略的地方:Proteus仿真STM32时,默认可能不启用串口输出,需要在STM32模型属性里确认USART功能是开启的。

4. 常见问题与排查技巧实录

4.1 测量值跳动大或完全不对

这是最常见的问题,原因通常有三个。第一个是信号边沿不够陡。Proteus里如果PWM信号经过长导线或者经过其他元件,边沿可能变缓,导致捕获时刻不确定。解决办法是缩短连线,或者开启输入滤波器。第二个是中断优先级冲突。如果还有其他中断在跑,捕获中断被延迟,测出来的周期就会偏大。检查NVIC配置,确保TIM2中断优先级足够高。第三个是计数器溢出。如果你测的信号频率低于15Hz,TIM2会溢出,测量值完全错误。这时候需要开启溢出中断,在中断里累加溢出次数。

排查步骤我一般这样走:先用Proteus的虚拟示波器看PA0上的信号,确认边沿干净、频率正确;然后在中断里翻转一个IO口,用示波器看中断响应是否及时;最后检查计算过程,把period_ticks通过串口打印出来,看数值是否合理。

4.2 串口无输出或乱码

串口问题基本逃不出三个原因:波特率不匹配、引脚接错、时钟配置错误。Proteus虚拟终端的波特率要和代码里USART_InitStructure.USART_BaudRate一致,我设的是9600。引脚方面,PA9是TX,要接终端的RXD;PA10是RX,接终端的TXD。如果接反了,终端收不到数据。时钟配置错误比较隐蔽:USART1挂在APB2上,时钟是72MHz;如果你误以为挂在APB1上按36MHz算,波特率就会差一倍,终端显示乱码。

还有一个Keil特有的坑:如果你用了printf但没勾选Use MicroLIB,程序可能卡在fputc里出不来。表现是仿真运行后终端没输出,但程序也没跑飞。这时候检查Options for Target里的MicroLIB选项。

4.3 Proteus仿真速度慢或卡死

Proteus仿真STM32本来就比较吃CPU,如果电路里还有其他复杂元件,仿真速度会明显下降。几个优化技巧:把不用的外设时钟关掉,减少仿真负担;虚拟终端的刷新率调低,比如从默认的50ms改成200ms;如果只是验证频率测量,可以把串口输出改成每测10次打印一次,减少终端刷新次数。

仿真卡死通常是程序进入了死循环或者中断风暴。检查中断服务函数里有没有清中断标志位,如果忘了清,中断会反复触发,CPU一直在中断里出不来。另外,如果捕获中断太频繁,比如测100kHz信号,中断执行时间可能超过信号周期,导致CPU被中断占满。这时候要么降低中断频率(用捕获分频),要么改用DMA传输捕获值。

4.4 从仿真移植到硬件的注意事项

仿真跑通不代表硬件上一定没问题。移植时要注意几点:第一,Proteus里的信号是理想的,硬件上可能有噪声,输入滤波器参数要重新调;第二,仿真里时钟是精确的,硬件上晶振可能有偏差,如果对测量精度要求高,要用外部高精度晶振;第三,Proteus不模拟电源噪声和地弹,硬件上如果电源不稳,捕获可能误触发,必要时加施密特触发器整形。

我在实际项目里还遇到过一个情况:硬件上PA0引脚旁边有PWM输出线,产生了串扰,导致捕获频率偶尔跳变。后来把PA0的走线远离PWM线,问题就解决了。这种布局问题仿真里发现不了,只能在实际PCB设计时注意。

4.5 常见问题速查表

现象可能原因排查方法解决措施
频率显示为0捕获中断未触发检查PA0信号、NVIC配置确认GPIO模式为IPD,中断使能
频率值偏大计数器溢出打印period_ticks看是否接近65535降低计数频率或加溢出处理
频率值偏小中断响应延迟翻转IO口测中断延迟提高中断优先级,减少中断内操作
串口乱码波特率不匹配核对终端和代码波特率统一设为9600
仿真无波形GPIO模式错误用电压探针测PA6改为AF_PP复用推挽
测量值跳动信号噪声示波器看PA0边沿开启输入滤波器,缩短连线

5. 精度提升与功能扩展思路

5.1 提高测量精度的几个实用手段

如果你对测量精度有更高要求,可以从这几个方向优化。第一个是提高计数频率。把TIM2预分频改成35,计数频率变成2MHz,分辨率翻倍。但这样计数器溢出更快,能测的最低频率变成约30Hz。第二个是用PWM输入模式。前面说过,PWM输入模式硬件自动测量周期和占空比,不占用CPU中断资源,精度也更高。代价是引脚必须用特定通道组合,比如TIM2的CH1和CH2要映射到同一引脚组。第三个是多次测量取平均。在软件里连续测10次,去掉最大最小值再平均,能有效抑制随机误差。我试过在1kHz信号上取16次平均,波动从正负2Hz降到正负0.5Hz。

还有一个容易被忽略的点:定时器时钟的准确性。STM32F103C8T6用外部8MHz晶振时,实际频率可能有几十ppm的偏差,换算到测量结果上就是几十Hz的误差。如果要求高,可以用TCXO或者校准过的信号源做参考,在软件里加一个校准系数。

5.2 测量低频信号时的溢出处理

前面提到,TIM2在1MHz计数频率下最低能测约15Hz。如果你要测更低的频率,比如心跳传感器输出的1Hz信号,就必须处理溢出。方法是在TIM2里开启更新中断,每次溢出时把一个溢出计数器加1。在捕获中断里,把溢出次数乘以65536加上当前捕获值,再减去上一次的值,就得到真实的周期计数。

代码上要注意,溢出中断和捕获中断可能同时发生,处理顺序会影响结果。我的做法是在捕获中断里先读溢出计数,再读捕获值,然后清中断标志。如果溢出中断在捕获中断之后触发,那这次溢出应该算到下一个周期里。这个逻辑有点绕,实际调试时可以在中断里翻转IO口,用示波器看时序,确认没有漏算或重复计算。

5.3 多通道测量与占空比计算

实际项目中经常需要同时测频率和占空比。用普通输入捕获模式的话,可以一个通道测上升沿,另一个通道测下降沿,两个通道配合算出高电平时间和周期。比如TIM2_CH1测上升沿,TIM2_CH2测下降沿,CH1捕获时记录周期起点,CH2捕获时记录高电平结束点,两者差值就是高电平时间,占空比就是高电平时间除以周期。

如果引脚不够用,也可以用单通道的PWM输入模式。把信号接到CH1,配置TIM_ICInitStructure.TIM_Channel为TIM_Channel_1,但把TIM_ICSelection设为TIM_ICSelection_IndirectTI,这样CH1的上升沿触发IC1,下降沿触发IC2,硬件自动把周期和占空比分别存到CCR1和CCR2里。读这两个寄存器就能直接得到结果,非常方便。

5.4 从仿真到产品的完整流程建议

如果你打算把这个方案用到实际产品里,我建议按这个流程走:先在Proteus里验证逻辑,确保代码框架没问题;然后在最小系统板上跑,用信号发生器输入已知频率,校准测量精度;接着在实际电路上测试,观察电源噪声和信号质量的影响;最后做温度测试和长时间稳定性测试,确认在恶劣环境下也能可靠工作。

代码层面,建议把频率测量封装成一个模块,对外提供初始化、启动测量、读取频率三个接口。这样换平台或者换定时器时,只需要改底层驱动,上层逻辑不用动。我现在的项目里就是这么做的,从STM32F103换到F407,只改了定时器初始化部分,测量逻辑一行没动。

提示:实际产品中如果测量结果要通过通信接口上报,建议在频率值上加一个滑动平均滤波,避免单次测量误差导致上报数据跳变。窗口大小取8或16比较合适,既能平滑数据,又不会引入太大延迟。

最后分享一个我在调试时的小技巧:在捕获中断里把一个空闲IO口翻转,然后用Proteus的虚拟示波器同时看PA0的输入信号和这个IO口的翻转波形。如果翻转发生在信号边沿之后很久,说明中断响应慢,需要优化;如果翻转位置和边沿对齐,说明捕获时机准确。这个方法比单纯看串口输出直观得多,能帮你快速定位是信号问题还是代码问题。

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

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

立即咨询