后来经过很多种方法的比较发现, 最好的测量频率的方法是采用占空比的方法测量 , 必须用硬件进行测量. 要使用定时器的双通道测量法.

步骤参考下面的文章
https://blog.csdn.net/qq153471503/article/details/130363906

我下面的文章可以不用看了.

频率测量是个最基本的且常见的工业需求.
但是这种简单的需求却不是那么的好实现. 总体来看, 目前的单片机还是有很大的改进空间.
很少有频率测量能够覆盖所有的频率范围.
而使用 STM32F103 性能有限.
根据待测频率, 我分成低中高, 三个阶段. 分别对应着3种不同的测量方法.

低频 1hz- 200khz,

测量方法, STM32 时钟计数器的输入捕获中断函数测量法.
测量原理
输入捕获模式可以用来测量脉冲宽度或者测量频率。
在这里插入图片描述

STM32 的输入捕获,简单的说就是通过检测 TIMx_CHx 上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。

原理:假定定时器工作在向上计数模式,图中 t1~t2 时间,就是我们需要测量的高电平时间。同样的, 也可以用来测两个下降沿也就是一个周期的间隔时间 . 主要原理还是计算两个边沿之间的计数器的差.

测量方法如下:首先设置定时器通道 x 为 上升沿捕获,这样t1 时刻,STM32就会用硬件捕获到当前的 CNT 值到CCRx,产生中断,然后在程序中读取CCRx即可.

注意: 我曾经想在取得CCRx后, 立即设置CNT=0, 使得CNT从0开始重新计数.下次捕获t2的时候直接读取到的CCRx中就是时间差, 但是实验证明这里 不能立即清零 CNT, ,因为这里从进入中断 到判断完毕再到设置CNT为0的过程中,CNT已经计数很多了, 这个时候清0,无疑会丢失从进入中断到设置CNT为0,这部分的时间的计数值. 最后会导致数值不准确,时钟频率越高, 丢失的个数越大. 所以这里只能每次读取CCRx的数值 .

这样到 t2 时刻,又会发生捕获事件,得到此时的 CNT 值到CCRx2。这样,根据定时器的计数频率,我们就可以算出 t2 - t1 的时间,从而得到高电平脉宽。 在 t1 ~ t2 之间,可能产生 N 次定时器溢出,这就要求我们对定时器溢出,做处理,防止高电平太长,导致数据不准确。如图14.1.1所示,t1 ~ t2之间,CNT计数的次数等于:N*ARR+CCRx2,有了这个计数次数,再乘以 CNT 的计数周期,即可得到 t2-t1 的时间长度。

此种方法要注意的几个点是,

  1. TIM定时器的计数器CNT是16位的(有的是32位的). 超出65536 将会溢出.需要在溢出时在中断函数中增加一个软件计数即可. (当然也可以降低计数器的时钟, 但是不建议.这样会降低测量精度)
  2. 定时器在计数中, 最好不要更改CNT值, 只读取即可.
  3. 当被测的频率高到一定程度时, 每次跳变系统都会进入中断, 当中断函数里面的处理过长时, 将会导致系统罢工. 所以这里应该要在中断中增加一个额外的停止TIM时钟计数的功能. 防止系统频繁进入中断. 在while 循环中定时开启即可. 一般情况下, 不需要实时计算频率. 定时1秒计算一次频率即可.

中频测量

输入捕获+DMA
使用DMA, 自动读取N个 TIM的CNT到数组中, 然后定时1秒去计算一次CNT之间的差, 取个平均值. 这样不用频繁的进入系统中断. DMA的速度比软件中断要稍微快那么一点, CPU的开销也小了很多.
理论上只要DMA搬运的速度跟得上新数据产生的速度, 就能测到频率.
此种方法注意事项:
1.因为用DMA搬运CNT数值, 所以无法处理CNT溢出问题.需要在软件中处理.溢出的数值. 例如抛弃掉异常值.
2.软件计算时应当停止DMA搬运, 否则可能覆盖正在计算的数值. 最好将DMA设置为一轮,不要循环搬运. 计算完毕再开启DMA搬运.爱心小提示.开启和停止之间要有时间间隙.给DMA搬运的时间.
3.并非所有的TIM的所有通道都支持DMA搬运输入捕获值, 据我所知,STM32F103 只有TIM1的CH1通道是支持DMA搬运输入捕获值的. 用时需要先用STM32CubeIDE 查看支持DMA搬运输入捕获值的定时器通道有哪些…

高频测量

对于更高频率的频率测量.

1. ADC + 软件测量
2. FFT

时间有限, 有空再写.

遇到的bug和处理方式

经过不断的改进和试错, 最后还是选用了第一种方法**.输入捕获中断**
bug1
htm1.Instance->DIER 的寻址时间比 TIM1->DIER 要慢, 导致捕获的数据CNT值不准.
修改方法, 将所有的htm1.Instance 替换成 TIM1

bug2
STM32CubeIDE生成的代码不是很好用. 后来还是查寄存器手册, 自己手动设置寄存器比较好使…

bug2
main函数主循环中的代码有可能跟中断函数中的代码是乱序执行的.如果代码中引入了状态机, state 的状态有可能是不同的. 虽然只有一个变量, 但是执行的时机不同导致了. 一些莫名其妙的情况… 解决方法, 将state 增加一个状态, 避免开始状态和结束状态混用一个状态的情况.

stm32 HAL库的代码封装的不好

主循环中尽量少用HAL库的代码, 我一般用它生成的代码进行初始化和配置工作. 可以减少大部分的配置工作.
而主循环中的控制逻辑例如:使能,取值, 失能, 禁止更新事件等. 我一般都是自己直接操作寄存器. 这样可以发挥两者的优势. 一般主循环和主逻辑中我大部分会使用 __HAL 开头的函数, 因为这些__HAL 开头的函数 基本上都是直接操作寄存器的. 当然也可以用我下面的函数来实现寄存器的操作. 我个人觉得挺好用的

//1、对某位置1,即赋值为1
// a  |=  (1<<5);//把a 的第6位(bit5)置一,其他位不变
#define SetBitVal1(var,pos)  var  |=  (1<<pos);//把a 的第6位(bit5)置一,其他位不变

//2、对某位清0,其他位不变
//a  &= !(1<<5);//括号内 1左移5位:0010 0000,按位取反:1101 1111,即把a 的第6位(bit5)清0,其他位不变
#define SetBitVal0(var,pos)  var &=  ~(1<<pos);//把a 的第6位(bit5)置一,其他位不变

//3、将变量的第6位(bit5)取反,其他位不变
//a ^= (1<<5); //把第七位(bit5)取反,其他位不变
#define SetBitValToggle (var,pos)  var ^=  (1<<pos);//把a 的第6位(bit5)置一,其他位不变

使用方法如下,

SetBitVal1(TIM1->DIER, TIM_DIER_UIE_Pos);//允许更新中断,TIM1_UP_IRQHandler会触发
SetBitVal0(TIM1->DIER, TIM_DIER_UIE_Pos);//禁止更新中断,TIM1_UP_IRQHandler不会触发 

要善于使用 STM32库中提供的宏, 例如 TIM_DIER_UIE_Pos 这种基础定义都在一个文件中. “你的项目目录\Drivers\CMSIS\Device\ST\STM32F1xx\Include\stm32f103xb.h”
例如下面的代码, 就不那么好理解也不容易看懂. 虽然功能一样 . 看上去还是上面的更容易懂一些.

SetBitVal1(TIM1->DIER,  (0U));//允许更新中断,TIM1_UP_IRQHandler会触发
SetBitVal0(TIM1->DIER,  (0U));//禁止更新中断,TIM1_UP_IRQHandler不会触发 
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐