赞
踩
前段时间需要做一款频率采集设备,由于成本考虑,使用了APM32F103作为主控,APM32F103和STM32F103基本完全通用,有极个别BUG。不影响本次试验。客户要求的频率信号为11KHz到23KHz,精度要求在任何频率范围误差不能大于当前频率的万分之一以上(排除温度影响),采集速度要100次每秒以上。占空比可能会有变化。这种要求其实也只能选择STM32的输入捕获功能,我使用TIM2时钟的外部输入捕获,定时器采用36MHz时钟,时钟两分频。硬件输入分频设置为不分频,因为外部有硬件滤波器和信号处理电路,在此则不加输入滤波器。(时钟跑72M,遇到定时器中断少进或多进的BUG,所以改为36M)
(注:2023/08/09修改了部分程序和文案修改)
其实采集频率的思路很简单,就是根据脉冲与脉冲直接的时间长短,得出周期时间。我采用的是全部上升沿捕获,而不是上升沿后改为下降沿。作用是防止占空比的变化影响采集精度。上升沿第一次进入中断后将TIM2计数器清空,将中间寄存器加一后退出,当中间寄存器大于等于10时,将TIM2中的计数值读取到采样寄存器中。当前采集到的就是80个输入脉冲周期的计数器值,根据要求,最小采样速度为100次每秒,11KHZ采集速度最快为137.5次每秒,也勉强满足要求。
定时器初始化代码:
void TIM2_signal_input_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能TIM2时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA,GPIO_Pin_0); //PA0 下拉 //定时器 TIM_TimeBaseStructure.TIM_Prescaler =1; //预分频 TIM_TimeBaseStructure.TIM_Period = 0XFFFF; //自动重装值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //输入捕获 TIM2_ICInitStructure.TIM_Channel = TIM_Channel_1;//该是那就映射到那 TIM2_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM2_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM2_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上 TIM2_ICInitStructure.TIM_ICFilter = 0x00;//不滤波 TIM_ICInit(TIM2, &TIM2_ICInitStructure); //中断 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2中断 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0级 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级2级 NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器 TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断 TIM_Cmd(TIM2,ENABLE ); //使能定时器2 }
中断代码:
//定时器2中断服务程序 void TIM2_IRQHandler(void) { static unsigned char H_C=0; if((TIM2CH1_CAPTURE_STA&0X80)==0)//捕获成功后等待数据处理完成后,再次捕获 { if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { if((TIM2CH1_CAPTURE_STA&0X3F)==0X3F) //频率太低了,不检测了 { TIM2CH1_CAPTURE_STA|=0X80; TIM2CH1_CAPTURE_VAL=0XFFFF; } else TIM2CH1_CAPTURE_STA++; } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//捕获1发生捕获事件 { if(TIM2CH1_CAPTURE_STA&0X40) {//捕获10个周期了 TIM2CH1_CAPTURE_VAL=TIM_GetCapture1(TIM2); TIM2CH1_CAPTURE_STA|=0X80; //出去处理数据,防止再次进入中断 }else {//第一次捕获上升沿 if(H_C==0) { TIM2CH1_CAPTURE_STA=0; //清空 TIM2CH1_CAPTURE_VAL=0; TIM_SetCounter(TIM2,0); } H_C++; if(H_C==divider) //divider :该变量表示将多少个频率周期整合在一起运算,以降低误差 {//捕获(divider-1)个周期了 H_C=0; TIM2CH1_CAPTURE_STA|=0X40; } } } } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位 }
转换为实际频率:
if(TIM2CH1_CAPTURE_STA&0X80)//成功捕获到八分频扭矩频率周期 { temp=TIM2CH1_CAPTURE_STA&0X3F; temp*=65536;//溢出时间总和 temp+=TIM2CH1_CAPTURE_VAL;//得到总的时间 frequent_input=(36000000.0*divider)/temp; //根据当前的分频系数divider计算实际值 frequent_input=frequent_input-(pow((frequent_input/100),2)*(0.00312/(divider)));//消除静差 if(frequent_input>1000) divider=frequent_input/1000; //根据当前频率大小修改下次采集的分频系数 else divider=1; ui_cst=(unsigned int)frequent_input; //备注:上面的系数0.00312可根据实际误差修改,采集到的频率数据偏大,则将该数据增加,反之减小。 //系数的目的为消除定时器在进入中断时,没有迅速读取CNT的数据,而多累加的值。 //可以自己加入数据输出函数 // // TIM2CH1_CAPTURE_STA=0; }
这是20KHZ,实际采样和输出对比。
这是10KHZ,实际采样和输出对比。
因为我现在的程序在一个较为大型程序中,剔除的话有很多变量和函数封装。而且上面的这段程序有很大的优化空间。谨慎移植。很多读者想了解移植办法,那我就简单说下移植办法。这三个程序段则为整个数据处理的底层。仅仅是部分全局变量未加入其中。
void TIM2_signal_input_Init(void) 这个函数放在主函数的初始化中。裸机时放在main函数的主while循环前。
void TIM2_IRQHandler(void) 这个函数就是中断服务函数,没强制要求放在固定位置。
if(TIM2CH1_CAPTURE_STA&0X80) 开头的函数为数据处理函数,如果使用RTOS,则最好使用消息队列。如果是裸机跑的话,建议放在main函数的主while循环内,或者定时器的中断服务函数内。
如果要求采样速度有降低,可以分频系数拉大,数据可以更精确。当前程序实际测试重复精度在十万分之五左右。可以看出程序和正点原子很像,感谢正点原子的教程与例程参考。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。