转:https://blog.csdn.net/yyx112358/article/details/78414594

想当年天天水论坛好为人师,现在已经全面转向计算机视觉方向了,颇为感慨。不过,自己的理性选择,个中得失早就意料之中。塞翁失马,焉知非福?

原文链接:http://www.openedv.com/forum.php?mod=viewthread&tid=82594&extra=


【教程】使用STM32测量频率和占空比的几种方法(申请置酷!)

这几天在论坛上面解答了好几个询问STM32测量频率的贴子,觉得这种需求还是存在的(示波器、电机控制等等)。而简单搜索了一下论坛,这方面的贴子有但是不全。正好今年参加比赛做过这方面的题目(最后是一等奖嘿嘿),所以把我们当时尝试过的各种方案都列出来,方便以后大家使用,也是作为一个长期在论坛的潜水党对论坛的回报。
PS:由于我们当时的题目除了测量频率之外,更麻烦的是测量占空比。而这两个的测量方法联系比较紧密,所以也一并把测量占空比的方法写出来。因为时间有限,所以并不能把所有思路都一一测试,只是写在下面作为参考,敬请谅解。
使用平台:官方STM32F429DISCOVERY开发板,180MHz的主频,定时器频率90MHz。
相关题目:
(1)测量脉冲信号频率f_O,频率范围为10Hz~2MHz,测量误差的绝对值不大于0.1%。(15分)
(2)测量脉冲信号占空比D,测量范围为10%~90%,测量误差的绝对值不大于2%。(15分)
思路一:外部中断
思路:这种方法是很容易想到的,而且对几乎所有MCU都适用(连51都可以)。方法也很简单,声明一个计数变量TIM_cnt,每次一个上升沿/下降沿就进入一次中断,对TIM_cnt++,然后定时统计即可。如果需要占空比,那么就另外用一个定时器统计上升沿、下降沿之间的时间即可。
缺点:缺陷显而易见,当频率提高,将会频繁进入中断,占用大量时间。而当频率超过100kHz时,中断程序时间甚至将超过脉冲周期,产生巨大误差。同时更重要的是,想要测量的占空比由于受到中断程序影响,误差将越来越大。
总结:我们当时第一时间就把这个方案PASS了,没有相关代码(这个代码也很简单)。不过,该方法在频率较低(10K以下)时,可以拿来测量频率。在频率更低的情况下,可以拿来测占空比。
思路二:PWM输入模式
思路:翻遍ST的参考手册,在定时器当中有这样一种模式:
总结:我们当时第一时间就把这个方案PASS了,没有相关代码(这个代码也很简单)。不过,该方法在频率较低(10K以下)时,可以拿来测量频率。在频率更低的情况下,可以拿来测占空比。
思路二:PWM输入模式
思路:翻遍ST的参考手册,在定时器当中有这样一种模式:
简而言之,理论上,通过这种模式,可以用硬件直接测量出频率和占空比。当时我们发现这一模式时欢欣鼓舞,以为可以一步解决这一问题,代码如下:
  1. void Tim2_PWMIC_Init(void)
  2. {
  3. GPIO_InitTypeDef GPIO_InitStructure;
  4. NVIC_InitTypeDef NVIC_InitStructure;
  5. TIM_ICInitTypeDef TIM_ICInitStructure;
  6. /* TIM4 clock enable */
  7. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
  8. /* GPIOB clock enable */
  9. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
  10. /* TIM4 chennel2 configuration : PB.07 */
  11. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
  12. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  13. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
  14. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  15. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP ;
  16. GPIO_Init(GPIOB, &GPIO_InitStructure);
  17. /* Connect TIM pin to AF2 */
  18. GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_TIM4);
  19. /* Enable the TIM4 global Interrupt */
  20. NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
  21. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  22. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  23. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  24. NVIC_Init(&NVIC_InitStructure);
  25. TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
  26. TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
  27. TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
  28. TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
  29. TIM_ICInitStructure.TIM_ICFilter = 0x0;
  30. TIM_PWMIConfig(TIM4, &TIM_ICInitStructure);
  31. /* Select the TIM4 Input Trigger: TI2FP2 */
  32. TIM_SelectInputTrigger(TIM4, TIM_TS_TI2FP2);
  33. /* Select the slave Mode: Reset Mode */
  34. TIM_SelectSlaveMode(TIM4, TIM_SlaveMode_Reset);
  35. TIM_SelectMasterSlaveMode(TIM4,TIM_MasterSlaveMode_Enable);
  36. /* TIM enable counter */
  37. TIM_Cmd(TIM4, ENABLE);
  38. /* Enable the CC2 Interrupt Request */
  39. TIM_ITConfig(TIM4, TIM_IT_CC2, ENABLE);
  40. }
  41. //中断程序:
  42. void TIM4_IRQHandler(void)
  43. {
  44. /* Clear TIM4 Capture compare interrupt pending bit */
  45. TIM_ClearITPendingBit(TIM4, TIM_IT_CC1|TIM_IT_CC2);
  46. /* Get the Input Capture value */
  47. IC2Value = TIM_GetCapture2(TIM4);//周期
  48. if (IC2Value != 0)
  49. {
  50. highval[filter_cnt]=TIM_GetCapture1(TIM4);//高电平周期
  51. waveval[filter_cnt]=IC2Value;
  52. filter_cnt++;
  53. if(filter_cnt>=FILTER_NUM)
  54. filter_cnt=0;
  55. }
  56. else
  57. {
  58. DutyCycle = 0;
  59. Frequency = 0;
  60. }
  61. }
  62. //主循环:
  63. while (1)
  64. {
  65. uint32_t highsum=0,wavesum=0,dutysum=0,freqsum=0;
  66. LCD_Clear(0);
  67. for(i=0;i<FILTER_NUM;i++)
  68. {
  69. highsum+=highval[i];
  70. wavesum+=waveval;
  71. }
  72. [/i] delay_ms(1);
  73. DutyCycle=highsum*1000/wavesum;
  74. Frequency=(SystemCoreClock/2*1000/wavesum);
  75. freq=Frequency*2.2118-47.05;//线性补偿
  76. sprintf(str,"DUTY:%3d\nFREQ:%.3f KHZ\n",DutyCycle,freq/1000);
  77. LCD_ShowString(0,200,str);
  78. delay_ms(100);
  79. }
但是,经过测量之后发现这种方法测试数据不稳定也不精确,数据不停跳动,且和实际值相差很大。ST的这些功能经常有这种问题,比如定时器的编码器模式,在0点处频繁正负跳变时有可能会卡死。这些方法虽然省事,稳定性却不是很好。
经过线性补偿可以一定程度上减少误差(参数在不同情况下不同):
freq=Frequency*2.2118-47.05;
这种方法无法实现要求。所以在这里我并不推荐这种方法。如果有谁能够有较好的程序,也欢迎发出来。
思路三:输入捕获
思路:一般来说,对STM32有一定了解的坛友们在测量频率的问题上往往都会想到利用输入捕获。首先设定为上升沿触发,当进入中断之后(rising)记录与上次中断(rising_last)之间的间隔(周期,其倒数就是频率)。再设定为下降沿,进入中断之后与上升沿时刻之差即为高电平时间(falling-rising_last),高电平时间除周期即为占空比
程序如下,注意由于为了减少程序复杂性使用了32位定时器5(计数周期如果是1us时可以计数4294s,否则如果是16位只能计数65ms),如果需要在F1上使用则需要自行处理:
  1. //定时器5通道1输入捕获配置
  2. //arr:自动重装值(TIM2,TIM5是32位的!!)
  3. //psc:时钟预分频数
  4. void TIM5_CH1_Cap_Init(u32 arr,u16 psc)
  5. {
  6. GPIO_InitTypeDef GPIO_InitStructure;
  7. TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  8. NVIC_InitTypeDef NVIC_InitStructure;
  9. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE); //TIM5时钟使能
  10. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); //使能PORTA时钟
  11. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIOA0
  12. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
  13. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz
  14. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
  15. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉
  16. GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA0
  17. GPIO_PinAFConfig(GPIOA,GPIO_PinSource0,GPIO_AF_TIM5); //PA0复用位定时器5
  18. TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频
  19. TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
  20. TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值
  21. TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
  22. TIM_TimeBaseInit(TIM5,&TIM_TimeBaseStructure);
  23. //初始化TIM5输入捕获参数
  24. TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 选择输入端 IC1映射到TI1上
  25. TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获
  26. TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
  27. TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
  28. TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波
  29. TIM_ICInit(TIM5, &TIM5_ICInitStructure);
  30. TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断
  31. TIM_Cmd(TIM5,ENABLE ); //使能定时器5
  32. NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn;
  33. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级
  34. NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级
  35. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
  36. NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
  37. }
  38. //捕获状态(对于32位定时器来说,1us计数器加1,溢出时间:4294秒)
  39. //定时器5中断服务程序
  40. void TIM5_IRQHandler(void)
  41. {
  42. if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
  43. {
  44. if(edge==RESET)//上升沿
  45. {
  46. rising=TIM5->CCR1-rising_last;
  47. rising_last=TIM5->CCR1;
  48. TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); //CC1P=0 设置为上升沿捕获
  49. edge=SET;
  50. }
  51. else
  52. {
  53. falling=TIM5->CCR1-rising_last;
  54. TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
  55. edge=RESET;
  56. }
  57. }
  58. TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位
  59. }
  60. 主程序:
  61. while (1)
  62. {
  63. uint32_t highsum=0,wavesum=0,dutysum=0,freqsum=0;
  64. LCD_Clear(0);
  65. delay_ms(1);
  66. sprintf(str,"rise:%3d\nfall:%d\nfall-rise:%d",rising,falling,falling-rising);
  67. LCD_ShowString(0,100,str);
  68. sprintf(str,"Freq:%.2f Hz\nDuty:%.3f\n",90000000.0/rising,(float)falling/(float)rising);//频率、占空比
  69. LCD_ShowString(0,200,str);
  70. delay_ms(100);
  71. }

注意的是,中断程序当中的变量rising,last因为多次修改的缘故,与名称本身含义有所区别,示意如下:

该方法尤其是在中低频(<100kHz)之下精度不错。
缺点:稍有经验的朋友们应该都能看出来,该方法仍然会带来极高的中断频率。在高频之下,首先是CPU时间被完全占用,此外,更重要的是,中断程序时间过长往往导致会错过一次或多次中断信号,表现就是测量值在实际值、实际值×2、实际值×3等之间跳动。实测中,最高频率可以测到约400kHz。
总结:该方法在低频率(<100kHz)下有着很好的精度,在考虑到其它程序的情况下,建议在10kHz之下使用该方法。同时,可以参考以下的改进程序减少CPU负载。
改进:
前述问题,限制频率提高的主要因素是过长的中断时间(一般应用情景之下,还有其它程序部分的限制)。所以进行以下改进:
1.           使用2个通道,一个只测量上升沿,另一个只测量下降沿。这样可以减少切换触发边沿的延迟,缺点是多用了一个IO口。
2.           使用寄存器,简化程序
最终程序如下:
  1. /TIM2_CH1->PA5
  2. //TIM2_CH2->PB3
  3. void TIM2_CH1_Cap_Init(u32 arr,u16 psc)
  4. {
  5. GPIO_InitTypeDef GPIO_InitStructure;
  6. TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  7. NVIC_InitTypeDef NVIC_InitStructure;
  8. TIM_ICInitTypeDef TIM_ICInitStructure;
  9. TIM_DeInit(TIM2);
  10. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //TIM2时钟使能
  11. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOB, ENABLE); //使能PORTA时钟
  12. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //GPIOA0
  13. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
  14. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; //速度100MHz
  15. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
  16. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉
  17. GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA0
  18. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //GPIOA0
  19. GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PA0
  20. GPIO_PinAFConfig(GPIOA,GPIO_PinSource5,GPIO_AF_TIM2); //PA0复用位定时器5
  21. GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_TIM2); //PA0复用位定时器5
  22. TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频
  23. TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
  24. TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值
  25. TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
  26. TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
  27. //初始化TIM2输入捕获参数
  28. TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 选择输入端 IC1映射到TI1上
  29. TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获
  30. TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
  31. TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
  32. TIM_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波
  33. TIM_ICInit(TIM2, &TIM_ICInitStructure);
  34. TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //CC1S=01 选择输入端 IC1映射到TI1上
  35. TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; //上升沿捕获
  36. TIM_ICInit(TIM2, &TIM_ICInitStructure);
  37. TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC1|TIM_IT_CC2,ENABLE);//允许更新中断 ,允许CC1IE捕获中断
  38. // TIM2_CH1_Cap_DMAInit();
  39. TIM_Cmd(TIM2,ENABLE ); //使能定时器5
  40. NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
  41. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级3
  42. NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级3
  43. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
  44. NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
  45. }
  46. //定时器2中断服务程序(对于32位定时器来说,1us计数器加1,溢出时间:4294秒)
  47. void TIM2_IRQHandler(void)
  48. {
  49. if(TIM2->SR&TIM_FLAG_CC1)//TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
  50. {
  51. rising=TIM2->CCR1-rising_last;
  52. rising_last=TIM2->CCR1;
  53. return;
  54. }
  55. if(TIM2->SR&TIM_FLAG_CC2)//TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET)
  56. {
  57. falling=TIM2->CCR2-rising_last;
  58. return;
  59. }
  60. TIM2->SR=0;
  61. }
之所以改用TIM2是因为TIM5的CH1(PA0)还是按键输入引脚。本来想来这应当也没什么,按键不按下不就是开路嘛。但是后来发现官方开发板上还有一个RC滤波……
所以,当使用别人的程序之前,请一定仔细查看电路图。

这样,最高频率能够达到约1.1MHz,是一个不小的进步。但是,其根本问题——中断太频繁——仍然存在。

解决思路也是存在的。本质上,我们实际上只需要读取CCR1和CCR2寄存器。而在内存复制过程中,面对大数据量的转移时,我们会想到什么?显然,我们很容易想到——利用DMA。所以,我们使用输入捕获事件触发DMA来搬运寄存器而非触发中断即可,然后将这些数据存放在一个数组当中并循环刷新。这样,我们可以随时来查看数据并计算出频率。

这一方法我曾经尝试过,没有调出来,因为,有一个更好的方法存在。但是理论上这是没有问题的,以供参考我列出如下。

【注意:这段程序无法工作,仅供参考!!!】

  1. //TIM2_CH1->DMA1_CHANNEL3_STREAM5
  2. u32 val[FILTER_NUM]={0};
  3. void TIM2_CH1_Cap_DMAInit(void)
  4. {
  5. NVIC_InitTypeDef NVIC_InitStructure;
  6. DMA_InitTypeDef DMA_InitStructure;
  7. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE);//DMA1时钟使能
  8. DMA_DeInit(DMA1_Stream5);
  9. while (DMA_GetCmdStatus(DMA1_Stream5) != DISABLE){}//等待DMA可配置
  10. /* 配置 DMA Stream */
  11. DMA_InitStructure.DMA_Channel = DMA_Channel_3; //通道选择
  12. DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(TIM5->CCR1);//DMA外设地址
  13. DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)val;//DMA 存储器0地址
  14. DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//存储器到外设模式
  15. DMA_InitStructure.DMA_BufferSize = FILTER_NUM;//数据传输量
  16. DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
  17. DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式
  18. DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;//外设数据长度:8位
  19. DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;//存储器数据长度:8位
  20. DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 使用普通模式
  21. DMA_InitStructure.DMA_Priority = DMA_Priority_High;//中等优先级
  22. DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  23. DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  24. DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输
  25. DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输
  26. DMA_Init(DMA1_Stream5, &DMA_InitStructure);//初始化DMA Stream
  27. TIM_DMAConfig(TIM5,TIM_DMABase_CCR1,TIM_DMABurstLength_16Bytes);
  28. TIM_DMACmd(TIM5,TIM_DMA_CC1,ENABLE);
  29. //如果需要DMA中断则如下面所示
  30. NVIC_InitStructure.NVIC_IRQChannel = DMA1_Stream5_IRQn; //使能TIM中断
  31. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级
  32. NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级
  33. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断
  34. NVIC_Init(&NVIC_InitStructure);
  35. DMA_ITConfig(DMA1_Stream5,DMA_IT_TC,ENABLE);
  36. //开启DMA传输
  37. DMA_Cmd(DMA1_Stream5, ENABLE);
  38. }
  39. void DMA1_Stream5_IRQHandler(void)
  40. {
  41. DMA_ClearITPendingBit(DMA1_Stream5,DMA_IT_TCIF5);
  42. }
@xkwy大神在回复中提出了几个改进意见,列出如下:
1.可以设定仅有通道2进行下降沿捕获并触发中断,而通道1捕获上升沿不触发中断。在中断函数当中,一次读取CCR1和CCR2。这样可以节省大量时间。
2.可以先进行一次测量,根据测量值改变预分频值PSC,从而提高精度
3.间隔采样。例如每100ms采样10ms.
这样的改进应当能够将最高采样频率增加到2M.但是频率的进一步提高仍然不可能。因为这时的主要矛盾是中断函数时间过长,导致CPU还在处理中断的时候这一次周期就结束了,使得最终测量到的频率为真实频率的整数倍左右。示意图如下:

因此,高频时仍然推荐以下方法。

思路四:使用外部时钟计数器

这种方法是我这几天回答问题时推荐的方法。思路是配置两个定时器,定时器a设置为外部时钟计数器模式,定时器b设置为定时器(比如50ms溢出一次,也可以用软件定时器),然后定时器b中断函数中统计定时器a在这段时间内的增量,简单计算即可。

代码:

  1. //TIM7->100ms
  2. //TIM2_CH2->PB3
  3. void TIM_Cnt_Init(void)
  4. {
  5. GPIO_InitTypeDef GPIO_InitStructure;
  6. TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
  7. NVIC_InitTypeDef NVIC_InitStructure;
  8. TIM_DeInit(TIM2);
  9. TIM_DeInit(TIM7);
  10. RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2|RCC_APB1Periph_TIM7,ENABLE); //TIM2时钟使能
  11. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //使能PORTA时钟
  12. //IO
  13. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //GPIOA0
  14. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
  15. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_25MHz; //速度100MHz
  16. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
  17. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //下拉
  18. GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PA0
  19. GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_TIM2); //PA0复用位定时器5
  20. //TIM2配置
  21. TIM_TimeBaseStructure.TIM_Prescaler=0; //定时器分频
  22. TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
  23. TIM_TimeBaseStructure.TIM_Period=0xFFFFFFFF; //自动重装载值
  24. TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
  25. TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
  26. TIM_TIxExternalClockConfig(TIM2,TIM_TIxExternalCLK1Source_TI2,TIM_ICPolarity_Rising,0);//外部时钟源
  27. //TIM7 100ms
  28. TIM_TimeBaseStructure.TIM_Prescaler=18000-1; //定时器分频
  29. TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
  30. TIM_TimeBaseStructure.TIM_Period=1000-1; //自动重装载值
  31. TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
  32. TIM_TimeBaseInit(TIM7,&TIM_TimeBaseStructure);
  33. //中断
  34. NVIC_InitStructure.NVIC_IRQChannel = TIM7_IRQn;
  35. NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级3
  36. NVIC_InitStructure.NVIC_IRQChannelSubPriority =0; //子优先级3
  37. NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
  38. NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
  39. TIM_ITConfig(TIM7,TIM_IT_Update,ENABLE);//允许更新中断 ,允许CC1IE捕获中断
  40. TIM_Cmd(TIM7,ENABLE ); //使能定时器5
  41. TIM_Cmd(TIM2,ENABLE ); //使能定时器5
  42. }
  43. u32 TIM7_LastCnt;
  44. //频率为TIM_ExtCntFreq
  45. void TIM7_IRQHandler(void)
  46. {
  47. char str[32];
  48. TIM_ExtCntFreq=(TIM2->CNT-TIM7_LastCnt)*(1/SAMPLE_PERIOD);// SAMPLE_PERIOD为采样周期0.1s
  49. sprintf(str,"%3.3f",TIM_ExtCntFreq/1000.0);//必须加这一句,莫明其妙
  50. TIM7_LastCnt=TIM2->CNT;
  51. TIM_ClearITPendingBit(TIM7,TIM_IT_Update);
  52. }
缺点:
1.无法测量占空比,高频的占空比测量方法见下文。
2.在频率较低的情况下,测量精度不如思路3(因为测量周期为100ms,此时如果脉冲周期是200ms……)。
3.输入幅值必须超过3V。如果不够或者超出,需要加入前置放大器。
总结:这种方法精度很高,实测在2MHz之下误差为30Hz也就是0.0015%(由中断服务程序引发,可以使用线性补偿修正),在25MHz之下也是误差30Hz左右(没法达到更高的原因是波形发生器的最大输出频率是25MHz^_^)。同时,从根本上解决了中断频率过高的问题。而由于低频的问题,建议:在低频时,或者加大采样间隔(更改TIM7的周期),或者采用思路3的输入捕获。
此外,还有一个莫名其妙的问题就是,中断当中如果不加入sprintf(str,"%3.3f",TIM_ExtCntFreq/1000.0)这一句,TIM_ExtCntFreq就始终为0。我猜测是优化的问题,但是加入volatile也没有用,时间不够就没有理睬了。
思路五:ADC采样测量(概率测量法)
一般的高端示波器,测量频率即是这种方法。简而言之,高速采样一系列数据,然后通过频谱分析(例如快速傅里叶变换FFT),获得频率。F4有着FPU和DSP指令,计算速度上可以接受。但是ADC的采样频率远远达不到。官方手册上声明,在三通道交替采样+DMA之下,最高可以达到8.4M的采样率。然而,根据香农采样定理,采样频率至少要达到信号的2倍。2M信号和8.4M的采样率,即使能够计算,误差也无法接受。所以,ADC采样是无法测量频率特别是高频频率的。
但是,无法测量频率,却可以测量占空比,乃至超调量和上升时间(信号从10%幅值上升到90%的时间)!原理也很简单,大学概率课上都说过这个概率基本原理:

当采样数n趋于无穷时,事件A的概率即趋近于统计的频率。所以,当采样数越大,则采样到的高电平占样本总数的频率即趋近于概率——占空比!

因此,基本思路即是等间隔(速度无所谓,但必须是保证等概率采样)采样,并将这些数据存入一个数组,反复刷新。这样,可以在任意时间对数组中数据进行统计,获得占空比数据。
以下是代码,使用了三通道8位ADC+DMA。理论上,采用查询法也是可以的。
  1. //ADC1-CH13-PC3
  2. //DMA2-CH0-STREAM0
  3. #define ADCx ADC1
  4. #define ADC_CHANNEL ADC_Channel_13
  5. #define ADCx_CLK RCC_APB2Periph_ADC1
  6. #define ADCx_CHANNEL_GPIO_CLK RCC_AHB1Periph_GPIOC
  7. #define GPIO_PIN GPIO_Pin_3
  8. #define GPIO_PORT GPIOC
  9. #define DMA_CHANNELx DMA_Channel_0
  10. #define DMA_STREAMx DMA2_Stream0
  11. #define ADCx_DR_ADDRESS ((uint32_t)&(ADCx->DR))//((uint32_t)0x4001224C)
  12. void ADC_DMAInit(void)
  13. {
  14. ADC_InitTypeDef ADC_InitStructure;
  15. ADC_CommonInitTypeDef ADC_CommonInitStructure;
  16. DMA_InitTypeDef DMA_InitStructure;
  17. GPIO_InitTypeDef GPIO_InitStructure;
  18. /* Enable ADCx, DMA and GPIO clocks ****************************************/
  19. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
  20. RCC_AHB1PeriphClockCmd(ADCx_CHANNEL_GPIO_CLK, ENABLE);
  21. RCC_APB2PeriphClockCmd(ADCx_CLK, ENABLE);
  22. /* DMA2 Stream0 channel2 configuration **************************************/
  23. DMA_InitStructure.DMA_Channel = DMA_CHANNELx;
  24. DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADCx_DR_ADDRESS;
  25. DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)&(ADC_DATAPOOL[0]);
  26. DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
  27. DMA_InitStructure.DMA_BufferSize = ADC_POOLSIZE;
  28. DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  29. DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  30. DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
  31. DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
  32. DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  33. DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  34. DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  35. DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  36. DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  37. DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  38. DMA_Init(DMA_STREAMx, &DMA_InitStructure);
  39. DMA_Cmd(DMA_STREAMx, ENABLE);
  40. /* Configure ADC3 Channel7 pin as analog input ******************************/
  41. GPIO_InitStructure.GPIO_Pin = GPIO_PIN;
  42. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;
  43. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;
  44. GPIO_Init(GPIO_PORT, &GPIO_InitStructure);
  45. /* ADC Common Init **********************************************************/
  46. ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
  47. ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2;
  48. ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
  49. ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;
  50. ADC_CommonInit(&ADC_CommonInitStructure);
  51. /* ADC3 Init ****************************************************************/
  52. ADC_InitStructure.ADC_Resolution = ADC_Resolution_8b;
  53. ADC_InitStructure.ADC_ScanConvMode = DISABLE;
  54. ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
  55. ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  56. ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  57. ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  58. ADC_InitStructure.ADC_NbrOfConversion = 1;
  59. ADC_Init(ADCx, &ADC_InitStructure);
  60. /* ADC3 regular channel7 configuration **************************************/
  61. ADC_RegularChannelConfig(ADCx, ADC_CHANNEL, 1, ADC_SampleTime_480Cycles);
  62. /* Enable DMA request after last transfer (Single-ADC mode) */
  63. ADC_DMARequestAfterLastTransferCmd(ADCx, ENABLE);
  64. /* Enable ADC3 DMA */
  65. ADC_DMACmd(ADCx, ENABLE);
  66. /* Enable ADC3 */
  67. ADC_Cmd(ADCx, ENABLE);
  68. }
  69. 主程序:
  70. for(j=0;j<ADC_POOLSIZE;j++)
  71. {
  72. if(ADC_DATAPOOL[j]>0x01)
  73. posicnt++;
  74. }
  75. duty=100*posicnt/(float)(ADC_POOLSIZE)+0.1f;//线性补偿
缺点:
1.精度低:实测2MHz下误差约1.3%,低频时无法统计(比如,频率10Hz,而ADC采样时间50ms。这时如果采样时间中刚好全是高电平,占空比为1……)。
2.内存占用大:数据池大小为65536,占用了64KB内存。
3.有响应延迟:测量出来的是“平均占空比”而非“瞬时占空比”。由于我测试时使用的是波形发生器,输出波形相当稳定(1W+的价格毕竟是有它的道理的……),实际应用当中一般不能够达到这样的水平,势必带来响应延迟(准确说应该是采样系统积分惯性越大)。
4.幅值过低(0.3V)无法测量,过高则超过ADC允许最大值。所以必须视情况使用不同的前置放大器。
实际上使用时如何取舍,就需要看实际情况了。毕竟,这只是低成本下的解决方案而已。
综上,对这几种方法做一个总结:
外部中断:编写容易,通用性强。缺点是中断进入频繁,误差大。
PWM输入:全硬件完成,CPU负载小,编写容易。缺点是不稳定,误差大。
输入捕获:可达到约400kHz。低频精度高,10Hz可达到0.01%以下,400kHz也有3%。缺点是中断频繁,无法测量高频,幅值必须在3.3~5V之间。
外部时钟计数器(首选):可达到非常高的频率(理论上应当是90MHz)和非常低的误差(2MHz下为0.0015%且可线性补偿)。缺点是低频精度较低,同样幅值必须在3.3~5V之间。
ADC采样频率测量法:难以测量频率,高频下对占空比、上升时间有可以接受的测量精度(2MHz下约1.3%),低频下无法测量。幅值0.3~3.3V,加入前置放大则幅值随意。
ADC采样频谱分析:高端示波器专用,STM32弃疗。
我采用的方法是:首先ADC测量幅值并据此改变前置放大器放大倍数,调整幅值为3.3V,同时测量得到参考占空比。而后使用外部时钟计数器测量得到频率,如果较高(>10000)则确认为频率数据,同时ADC测量占空比确认为占空比数据。否则再使用输入捕获方法测量得到频率、占空比数据。
对于各个方法存在的线性误差,使用了线性补偿来提高精度。一般情况下,使用存储在ROM中的数据作为参数,当需要校正时,采用如下校正思路:
波形发生器生成一些预设参数波形(例如10Hz,10%;100K,50%;2M,90%……),在不同区间内多次测量得到数据,随后以原始数据为x,真实数据为y,去除异常数据之后,做y=f(x)的线性回归,并取相关系数最高的作为新的参数,同时存储在ROM当中。
我认为,我的这篇文章,应当是很全面了。当然,限于水平,存在着未完善和不正确的地方,也欢迎指正。

测量频率和占空比的几种方法相关推荐

  1. 使用 STM32 测量频率和占空比的几种方法

    以前在本科时写的教程文章,主要是把自己当时参赛的方法拿出来做了个总结. 想当年天天水论坛好为人师,现在已经全面转向计算机视觉方向了,颇为感慨.不过,自己的理性选择,个中得失早就意料之中.塞翁失马,焉知 ...

  2. Arduino产生PWM的3种方法

                                                  Arduino产生PWM的3种方法! PWM是个啥? 有人翻译成:脉冲宽度调制 PWM 是用占空比不同的方波 ...

  3. arduino利用三种方法产生pwm波使用l298n驱动12v小车电机(附电路连接图和pwm详解)

    前段时间参加硕士复试,有一道题是pwm是什么,如何产生pwm,录取成功后接下来在这里写一下吧! 1.脉冲宽度调制(PWM)是一种对模拟信号电平进行数字编码的方法,由于计算机不能输出模拟电压,只能输出0 ...

  4. spring boot项目 中止运行 最常用的几种方法

    spring boot项目 中止运行 最常用的几种方法: 1. 调用接口,停止应用上下文 @RestController public class ShutdownController impleme ...

  5. 设置select下拉框不可修改的→“四”←种方法

    设置select下拉框为不可修改的几种方法: 因为select的特殊性,导致它不能像input表单一样简单地设置一个readonly来限制修改,所以,我们需要进行别的操作! 1.为下拉框添加样式,可以 ...

  6. 用python下载文件的若干种方法汇总

    压缩文件可以直接放到下载器里面下载的 you-get 连接 下载任意文件 重点 用python下载文件的若干种方法汇总 写文章 用python下载文件的若干种方法汇总 zhangqibot发表于Met ...

  7. Python两个字典键同值相加的几种方法

    版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/Jerry_1126/article/d ...

  8. VS中C#读取app.config数据库配置字符串的三种方法(转)

    关于VS2008或VS2005中数据库配置字符串的三种取法 VS2008建立Form程序时,如果添加数据源会在配置文件 app.config中自动写入连接字符串,这个字符串将会在你利用DataSet, ...

  9. 在PHP中使用全局变量的几种方法

    简介 即使开发一个新的大型PHP程序,你也不可避免的要使用到全局数据,因为有些数据是需要用到你的代码的不同部分的.一些常见的全局数据有:程序设定类.数据库连接类.用户资料等等.有很多方法能够使这些数据 ...

最新文章

  1. 微信小程序直播开启公测了,与平台直播有何不同?小程序直播如何搭建
  2. Oracle 11g Dataguard 物理备库配置(一)之Duplicate配置
  3. 分布式文件系统—HDFS—核心设计
  4. AJPFX关于构造器的总结
  5. 【C++基础】异常匹配与内建异常类
  6. Spring+SpringMVC+Mybatis 多数据源整合
  7. 企业微信怎么删除好友 企业微信如何删除成员
  8. MySQL ALTER命令
  9. kali linux虚拟机密码,[Linux]安装kali虚拟机后忘记root密码
  10. 概要设计说明书任务分配
  11. mysql中XtraBackup备份工作机制
  12. UE5 预览版载具模板工程车不能移动的问题
  13. 【路径规划】基于改进粒子群实现机器人栅格地图路径规划
  14. 2 简单使用原形工具Axure RP
  15. 【剑指 Offe】剑指 Offer 17. 打印从1到最大的n位数
  16. 计算机知识动画小学,电脑动画
  17. 整车nvh培训入门 仿真 ,基于Hyperworks 掌握思路 细节
  18. Liquibase中利用changelog增加表字段
  19. 总线(四)Modbus总线 协议
  20. Python 布尔类型 bool

热门文章

  1. 触发器(四、执行顺序控制)
  2. 计算机考研和就业pk,考研PK就业:提高自身竞争力比文凭更重要
  3. 3dvary灯光材质为什么不亮_3dmax灯光教程灯光打出来太假?不真实?杂点?曝光?原因都在这儿呢...
  4. 2018年天津大学夏令营机试第一题
  5. php开发俄罗斯方块,HTML5+JS实现俄罗斯方块原理及具体步骤_html5教程技巧
  6. Redis 内存分析神器
  7. (unity)新手接入Facebook登录,分享以及google登录,Android,IOS,OC接入篇
  8. 【Avro二】Avro RPC框架
  9. iOS WKWebView的使用以及遇到的问题
  10. 【Javaweb】TCP原理(三次握手四次挥手)