前言
前段时间需要做一款频率采集设备,由于成本考虑,使用了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循环内,或者定时器的中断服务函数内。
总结
如果要求采样速度有降低,可以分频系数拉大,数据可以更精确。当前程序实际测试重复精度在十万分之五左右。可以看出程序和正点原子很像,感谢正点原子的教程与例程参考。