
简介面向嵌入式开发者与电子类专业学生的基于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主频下定时器最大计数速度能跑到36MHzAPB1时钟为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对应的引脚一般是PA0TIM2的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.50.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.3VBOOT0接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引脚前串一个限流电阻就能模拟出带阈值整形的效果。第二个方向是提高测量上限。刚才的代码用的是外部时钟模式1PA0引脚接收外部信号。你还可以用TIM2的编码器模式或者把外部信号接到多个引脚实现同步触发进一步提高最大可测频率。甚至可以做成等精度测量用TIM3产生基准时间闸门用TIM2和TIM4同时分别测标准时钟和待测信号然后在软件里算出准确频率。这个方案会让代码复杂度上升一个台阶但测量精度也会明显提升。第三个方向是改进显示端。当前项目如果用OLED显示就只能显示数字。换成LCD1602或者串口屏之后可以显示频率值和波形类型配合按键还能切换不同量程。考虑到我们已经在Proteus里做了完整仿真移植到真实硬件时只要调整引脚映射和触发电压即可。我个人在实际调试过程中的体会是频率计这个项目经常会给人一种“原理听懂了但动手就错”的感觉。定时器的外部时钟模式、中断优先级、溢出处理、闸门时间计算每一个环节单独拎出来都不难组合在一起却非常考验对整个时钟树和中断系统的理解。Proteus的好处是给了你一个放心试错的场地代码烧进去出错了也不会烧板子把逻辑改对再上实物效率会高很多。建议拿到工程之后先不要急着调高频从1kHz开始一步一步往上加体会一下“计数正确→溢出处理→显示刷新”这三个阶段的差异。等你把这一套流程趟顺了再去看工程级的频率计设计会发现那些高级方案不过是在这个基础上叠了更多补偿和校准逻辑而已。本文还有配套的精品资源点击获取