基于STM32定时器外部计数模式的频率计设计与Proteus仿真
2026/9/9 17:42:43 网站建设 项目流程

简介:面向嵌入式开发者与电子类专业学生的基于STM32 HAL的频率计Proteus仿真工程包,聚焦频率测量这一基础功能,帮助学习通过定时器/计数器捕获外部信号边沿并计算频率,同时掌握HAL库配置与Proteus联合仿真方法。资源共170个文件,压缩包约5.81MB,以Keil工程和Proteus设计文件为核心,包含大量C源码、HAL库头文件、编译中间文件及最终hex固件,同时附有pdsprj仿真工程、uvprojx工程配置和ioc配置,便于直接打开查看与二次修改。目前已有404人学习下载。借助这套资料,读者可以对照工程学习定时器输入捕获、中断服务程序编写、信号发生器模拟输入及频率显示流程,还能参考完整的目录结构理解STM32与Proteus联合开发的组织方式,适合作为课程设计、毕业设计或嵌入式入门实操的参考模板。 玩STM32做频率计这件事,我一开始其实是有点犹豫的。原因很简单:频率计这玩意儿,用纯硬件逻辑搭计数器,或者用一颗便宜的单片机专门干数脉冲的活,早就被各路方案做烂了。但这次不一样,我刚好看手头有个STM32F103C8T6,又想在Proteus里把整套逻辑跑通验证一下,于是就有了这篇内容。如果你是那种想搞懂“频率计到底怎么测频率”、“HAL库配定时器有哪些坑”、“Proteus仿真和真实板子差在哪”的人,这篇东西应该对你有用。

先说结论:用STM32的定时器外部计数模式,加另一个定时器做时间闸门,在1秒或者0.5秒的闸门时间内数外部脉冲的个数,就能直接算出频率。这个方法叫测频法,适合测中高频;如果是低频信号,就得换测周法。两种原理我都会在后面展开说清楚。整篇内容我尽量按照“方案怎么选→原理怎么理解→代码怎么写→仿真怎么搭→坑怎么避”的顺序来,已经跑通的工程最后也会给你一个清晰的复现思路。

1. 整体方案设计:芯片选型、HAL库与Proteus的角色分配

1.1 为什么锁定STM32F103C8T6

做频率计,方案其实可以选得很花哨。用FPGA做等精度测量、用CPLD辅助计数,甚至用纯运放和数字逻辑搭一个计数器阵列,这些都是很正经的思路。但对于学习和验证来说,STM32F103C8T6是性价比和上手难度都最均衡的选择。这颗芯片最常见,CubeMX原生支持,HAL库代码网上也一堆,真遇到问题,随便搜都能找到参考。更重要的是它内置了TIM定时器,支持外部时钟模式1和模式2,说白了就是可以用外部脉冲直接驱动定时器计数器,完全不需要CPU一条一条指令去读IO口电平。

我当时选型时也纠结过要不要上STM32F407,毕竟主频高、定时器更多。但回头一想,Proteus仿真里的瓶颈根本不在这颗芯片的性能上,仿真速度、信号源的精度、模型库的匹配程度才是关键。所以选F103C8T6完全够了,而且它默认72MHz主频下,定时器最大计数速度能跑到36MHz(APB1时钟为36MHz时,定时器时钟翻倍到72MHz),这个上限对Proteus里常见的几Hz到几十kHz信号来说绰绰有余。

1.2 HAL库和标准外设库之间的选择

关于用HAL还是标准外设库(StdPeriph),我估计每个社区都能吵上几页。我个人的判断很简单:如果你要写正式的项目代码、要维护、要移植,或者要配合CubeMX做快速原型验证,选HAL;如果你是那种喜欢把寄存器翻个底朝天、追求极简二进制体积的人,标准库可能更顺手。

但在这个项目里,我强烈建议用HAL库。原因不是HAL有多优雅,而是定时器初始化和时钟树配置用CubeMX生成,省下的时间足够你多写几百行应用逻辑。频率计的核心逻辑其实是“两个定时器配合”:一个定时外部脉冲,一个定时闸门时间。用CubeMX把这些配置全部可视化之后,生成的代码你只需要在回调函数里做加法,大大降低了出错概率。而且HAL库里定时器中断、外部计数模式的代码结构非常清晰,很容易在Proteus里联合调试。

1.3 Proteus仿真的优势与局限

Proteus做单片机仿真是一把双刃剑。优点大家都能说出来:不用买元器件、不怕烧板子、随时改设计;它能模拟STM32的启动流程、GPIO状态、定时器和中断,还可以用信号发生器模块直接提供任意频率的波形。

但局限同样明显。第一,Proteus对STM32外设的模型支持不如对51和AVR那么完善,某些外设(比如ADC的采样时序、DAC的真实输出)和真实芯片行为有偏差。第二,仿真里的“晶振”和信号源是理想模型,不会像真实板子上那样有温漂、毛刺、触发电压不稳等问题。第三,如果你用了虚拟串口屏、OLED一类的显示外设,仿真速度可能会被拖得很慢。

这个项目的目标是验证“定时器测频逻辑是否可行”,而不是做高精度计量仪器,所以Proteus的误差范围完全够用。我实测下来,10kHz以下信号测频误差基本能控制在0.1%以内,这对仿真验证来说是相当不错的结果。

2. 频率计核心原理:测频法、测周法与参数计算

2.1 三种测频方式到底怎么选

频率计的测量方法,往大了分就四条路:测频法、测周法、等精度测频法和多周期同步测频法。

测频法,就是固定一个闸门时间(比如1秒),在这段时间里数外部脉冲有多少个,频率就是计数结果除以闸门时间。简单直白,精度和闸门时间直接挂钩——闸门时间越长,测量结果的平均效应越强,精度越高。但低频信号就有问题了,比如测1Hz的信号,你读到的计数可能是0也可能是1,误差大得吓人。

测周法,正好反过来,是固定一个脉冲周期,去测这个周期包含了多少个标准时钟周期,然后用标准时钟频率除以计数值得到待测频率。这个办法适合低频,因为周期越长,你数到的标准时钟脉冲越多,分辨率越高。

等精度测频,则是一个“两头堵”的思路,用一个同步闸门同时测量标准时钟和待测信号的脉冲个数,再做比例计算。它克服了测频法低频不准、测周法高频不准的问题,精度和控制闸门时间有关,和被测频率大小关系不大。如果这是要做真正的商品级频率计,我肯定首推等精度。但在这个仿真项目里,测频法已经能覆盖我们最常见的信号范围,代码也简单得多,所以我选择了测频法作为主要方案。

2.2 基于定时器外部时钟模式的测频原理

用STM32的定时器做测频法,最核心的机制是“外部时钟模式”。我选了TIM2作为外部脉冲计数器,它有一个特性:可以接收来自内部时钟或外部引脚的脉冲信号,并且每来一个上升沿(或下降沿,取决于配置),计数器就加1。整个过程不需要CPU干预,完全是硬件级别的计数,非常高效。

与此同时,我用TIM3来做时间闸门。最简单的方式就是让TIM3的更新事件产生中断,中断里读取TIM2的计数值,然后清零TIM2重新计数。这样每隔“闸门时间”就采一次数。闸门时间我用的是50ms到1s可调,在Proteus里,50ms能看到响应比较快,1s测出来更准。如果你要显示到小数点后两位,建议用1s闸门。

注意:读取和清零TIM2计数器的时候,一定要确认TIM3的中断没有被更高级的中断长期打断,否则你读到的值可能是中途更新的,数据会错位。在裸机条件下,把TIM3中断优先级设为最高,防止正在读计数值时被打断。

2.3 闸门时间与分频系数的参数计算

闸门时间决定了测频法的精度,而分频系数决定了你的测频上限。这两个参数值得仔细算一笔账。

STM32F103C8T6内部,APB1时钟在系统主频72MHz时是36MHz,而定时器时钟是APB1的2倍,也就是72MHz。当TIM2作为外部计数器使用时,它不再需要内部时钟,而是直接数外部脉冲,所以此处内部时钟频率只影响TIM2的计数上限——外部脉冲的极限频率不能超过定时器时钟的1/2,理论上大约36MHz。不过Proteus仿真中信号源直接连着引脚,这个理论值基本能贴近实测。

TIM3作为闸门定时器时,需要内部时钟驱动。如果TIM3的预分频器设成7199,那定时器时钟就是72MHz / 7200 = 10kHz,即每个计数单位代表0.1毫秒。要得到1秒闸门,TIM3的自动重装载值(ARR)就设为9999。这样当TIM3从0计数到9999时,时间正好是1秒。

公式化表达一下:

  • 定时器时钟频率:FTimer = 72MHz / (PSC + 1)
  • 闸门时间:TGate = (ARR + 1) / FTimer
  • 测量频率:FMeasured = (TIM2->CNT) / TGate

这是我调试时贴在代码头部的三段注释,每次修改PSC和ARR之前心里都有数,不容易出错。

3. 代码实现与Proteus仿真搭建:从CubeMX到界面上出数字

3.1 CubeMX里的初始化配置要点

如果你是从零开始做这个项目,建议直接开一个CubeMX工程,芯片型号选STM32F103C8Tx,然后在RCC里把HSE设成Crystal/Ceramic Resonator,这样Proteus里的外部晶振才能和仿真模型对上。

时钟树设置成主频72MHz后,开始配置定时器。先把TIM2选为外部时钟模式(External Clock Mode 1),对应的引脚一般是PA0(TIM2的CH1,也是外部触发引脚)。这时TIM2的时钟源就不再是内部时钟了,而是PA0引脚上的边沿信号。PSC保持为0即可,因为我们希望每个外部脉冲都让计数器+1,不做任何分频。

接着配置TIM3作为闸门定时器。TIM3时钟源保持Internal Clock,预分频器PSC设为7199,自动重装载ARR设为9999。启动中断,并把中断优先级设成一个较高的水平,避免和串口中断或系统滴答冲突。

最后别忘记给TIM2也开一个中断,用途是处理计数器溢出——因为如果被测频率太高或者闸门时间太长,TIM2可能会在1秒内从0xFFFF翻了多次,你需要把TIM2的溢出次数也记录下来,否则测出来的频率就会奇低或者变负数。这个点也是很多人的坑,后文我会再强调一遍。

3.2 HAL库代码的四个关键改动

CubeMX生成的代码只有初始化,实际测频逻辑需要我们自己写。我用的是HAL库的定时器接口,主要改动集中在四个地方。

第一,在主循环启动定时器:

HAL_TIM_Base_Start_IT(&htim3); // 启动闸门定时器TIM3 __HAL_TIM_SET_COUNTER(&htim2, 0); // 清零TIM2 HAL_TIM_Base_Start(&htim2); // 启动外部计数器TIM2

注意TIM2不需要开中断,只需启动计数即可,开中断反而会引入额外开销。

第二,添加TIM3更新中断的回调。HAL库会在HAL_TIM_PeriodElapsedCallback里通知定时器更新事件,我们在这里读取TIM2的计数值。

volatile uint32_t freq_cnt = 0; volatile uint8_t freq_ready = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { freq_cnt = __HAL_TIM_GET_COUNTER(&htim2); // 读取当前计数值 __HAL_TIM_SET_COUNTER(&htim2, 0); // 清零计数器准备下一轮 freq_ready = 1; // 置标志位 } }

读取完之后,你可以直接在回调里算频率,也可以把freq_cnt留到主循环里处理。我建议只在中断里保存计数值,在主循环中做频率换算和显示,这样可以尽量减少中断内的计算量。

第三,处理TIM2的溢出。因为TIM2是16位计数器,最大值只有65535。如果我测一个100kHz的信号,1秒闸门内要计100000次,TIM2一定会在中途溢出。最简单的方式是开启TIM2的更新中断,在中断回调里对TIM2的溢出次数做累加:

volatile uint32_t cnt_overflow = 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { cnt_overflow++; } if (htim->Instance == TIM3) { freq_cnt = __HAL_TIM_GET_COUNTER(&htim2) + cnt_overflow * 65536; cnt_overflow = 0; __HAL_TIM_SET_COUNTER(&htim2, 0); freq_ready = 1; } }

我把这段逻辑合在一起写了,实际项目里两个中断回调都会进同一个函数,用Instance判断来源。有人会问:为什么不在TIM2溢出中断里直接清零计数器?因为TIM2正在被外部脉冲驱动,如果溢出中断发生后还有脉冲在持续输入,立刻清零会导致丢失脉冲。所以我的建议是只做溢出次数记录,不清零,到了闸门时间结束再统一处理。

第四,在主循环中把freq_cnt换算成真实的频率:

if (freq_ready) { freq_ready = 0; float frequency = (float)freq_cnt / 1.0f; // 闸门1s时,频率 = 计数 sprintf(buf, "Freq: %.2f Hz", frequency); OLED_ShowString(0, 0, (uint8_t*)buf); }

如果闸门时间改成了0.5秒,那就要除以0.5;0.2秒就除以0.2。这个除法不能省,很多人直接显示计数,结果频率对不上。

3.3 Proteus仿真电路搭建与信号源设置

Proteus里搭这个项目非常快。先放一个STM32F103C8T6模型,再接一个信号发生器模块(Signal Generator)。信号发生器的输出接到PA0,就是TIM2的外部计数输入引脚。为了方便看数据,可以再加一个虚拟示波器,确认信号源输出的波形没有问题。

然后是晶振部分。Proteus的STM32模型默认使用内部RC还是外部晶振,和你在CubeMX里选择HSE还是HSI有关。既然CubeMX里选的是HSE晶体,Proteus里就得放一个8MHz晶振,并接两个20pF左右的负载电容,再接到STM32的OSC_IN和OSC_OUT引脚。如果不接晶振,Proteus可能会报no stm32 target found或者芯片运行不了,新手常在这里卡住。

信号源的频率设置别一上来就扔一个1MHz的方波,先测1kHz、10kHz,等逻辑基本通了再往上加。虚拟示波器接PA0,方便判断信号是否真的到达了引脚。整个电路里不需要额外的放大整形电路,因为Proteus的比特级信号已经把方波模拟得很理想了。真实硬件上如果信号很弱,或者有缓慢上升沿,往往需要在前端加一个施密特触发器做整形,但仿真阶段这一块可以完全省略。

4. 实测问题与排查经验:Proteus仿真的四个典型坑

4.1 测出来的频率总是偏大或偏小,对不上信号源

这是仿真频率计最容易碰到的现象。先确认TIM3的闸门时间是否精确。Proteus里芯片运行速度和PC性能有关,但定时器的硬件逻辑模拟是建立在“时钟波形”上的,所以如果晶振模型和CubeMX配置不匹配,TIM3的计时基准就会错。常见的错误是CubeMX里选择了HSE,但Proteus晶振频率设成了72MHz。8MHz晶振经过PLL倍频到72MHz,和直接给72MHz晶振是完全两种结果,后者的TIM3定时会快到离谱。

排查技巧:在TIM3中断回调里翻转一个GPIO,接虚拟示波器看翻转周期。如果翻转周期是闸门时间的两倍,说明定时器配置正确。如果相差很大,优先检查晶振频率和时钟树里的HSE值是否一致。

4.2 TIM2计数器溢出不处理,高频信号测量结果异常

很多人的代码里没做TIM2溢出中断,低频信号测得很欢快,但一上100kHz就崩了。原因前面已经提过:TIM2是16位寄存器,计数到65535后会自动回绕到0。如果你在闸门结束时才发现计数器回绕过,计算出来的频率就少了一大截。

我踩过这个坑之后,把TIM2溢出处理写进了HAL_TIM_Base_Start_IT(&htim2),然后在回调里累加溢出次数。这里有个小细节:中断回调里cnt_overflow的类型一定要是volatile uint32_t,并且每次闸门结束时要清零后重新计数,否则下一轮测频的结果会把上一轮的溢出次数也算进去。

4.3 在Proteus仿真中无法运行或卡死

有时候放好电路、写入hex文件,点击运行后屏幕没有反应。这种问题八成和晶振配置、芯片VDD供电,或者复位引脚处理有关。Proteus的STM32F103C8T6模型要求NRST接一个上拉电阻到3.3V,BOOT0接GND,否则芯片可能一直处于复位或者加载BootLoader状态,不会进入用户程序。

另外一个比较隐蔽的问题是:CubeMX生成的HEX文件在Proteus里加载时,如果工程地址设置不对,也会出现“运行不起来”的现象。用Keil5编译时,把输出设置里的Create HEX File勾上,并确认Flash起始地址是0x08000000,下载类型选STM32F103C8。我之前有一次忘记调整Address范围,烧进去的二进制放错了位置,结果在Proteus里要么白屏要么跑飞,排查了好久。

4.4 仿真速度慢与程序响应延迟

Proteus对STM32的仿真消耗性能比较明显,如果我在仿真里同时放了虚拟示波器、OLED屏幕和多个信号源,程序运行速度会肉眼可见地下降。解决办法是降低闸门时间到0.2秒或者0.5秒,这样每次测量周期更短,屏幕刷新频率更高,仿真体验会好一些。同时可以适当关闭示波器,只在需要看波形时再打开,减少图形渲染的资源占用。

如果还是觉得卡,那就把虚拟终端和串口外设移除。频率计核心验证不需要OLED,你也可以在代码里用串口把频率值发给虚拟终端,这样代码改动小,仿真速度也快很多。

5. 这个项目还可以怎么继续折腾下去

如果你的目标是做毕业设计,或者想做一个真正能拿得出手的作品,仅仅测一个方波频率可能还不够。我建议在现在这个版本上做几个方向的扩展。

第一个方向是增加输入信号预处理。真实世界要测的信号千奇百怪,可能是正弦波、三角波甚至是不规则脉冲,需要先经过一个电压比较器或施密特触发器整形成TTL电平。这一块在Proteus里可以直接用NE5532或者LM393模型搭仿真电路,然后在A0引脚前串一个限流电阻,就能模拟出带阈值整形的效果。

第二个方向是提高测量上限。刚才的代码用的是外部时钟模式1,PA0引脚接收外部信号。你还可以用TIM2的编码器模式或者把外部信号接到多个引脚实现同步触发,进一步提高最大可测频率。甚至可以做成等精度测量,用TIM3产生基准时间闸门,用TIM2和TIM4同时分别测标准时钟和待测信号,然后在软件里算出准确频率。这个方案会让代码复杂度上升一个台阶,但测量精度也会明显提升。

第三个方向是改进显示端。当前项目如果用OLED显示,就只能显示数字。换成LCD1602或者串口屏之后,可以显示频率值和波形类型,配合按键还能切换不同量程。考虑到我们已经在Proteus里做了完整仿真,移植到真实硬件时只要调整引脚映射和触发电压即可。

我个人在实际调试过程中的体会是:频率计这个项目,经常会给人一种“原理听懂了,但动手就错”的感觉。定时器的外部时钟模式、中断优先级、溢出处理、闸门时间计算,每一个环节单独拎出来都不难,组合在一起却非常考验对整个时钟树和中断系统的理解。Proteus的好处是给了你一个放心试错的场地,代码烧进去出错了也不会烧板子,把逻辑改对再上实物,效率会高很多。建议拿到工程之后,先不要急着调高频,从1kHz开始,一步一步往上加,体会一下“计数正确→溢出处理→显示刷新”这三个阶段的差异。等你把这一套流程趟顺了,再去看工程级的频率计设计,会发现那些高级方案不过是在这个基础上叠了更多补偿和校准逻辑而已。

本文还有配套的精品资源,点击获取

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

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

立即咨询