STM32F1 标准库 ADC正弦波采样+DMA+FFT+输出正弦波频率(超详细版)

您所在的位置:网站首页 stm32如何产生正弦波 STM32F1 标准库 ADC正弦波采样+DMA+FFT+输出正弦波频率(超详细版)

STM32F1 标准库 ADC正弦波采样+DMA+FFT+输出正弦波频率(超详细版)

2024-06-25 14:03:12| 来源: 网络整理| 查看: 265

大致思路

本文代码可以实现用单片机对一个未知频率的正弦波进行采样,经过运算后得到该频率。 实现方法为:通过定时器设置采样频率,DMA对采样到的数据进行传输,使用FFT对采样到的波形数据进行傅里叶变换,得到傅里叶序列,对其进行计算得到幅频特性序列,当幅值最大时对应的序列就是该未知波形的频率(序列需要乘采样频率再除采样点数)。

Created with Raphaël 2.3.0 单片机采样 采样数据传输 对采样数据FFT计算 对傅里叶序列进行计算 计算得到波形频率 代码实现 ADC相关配置

这里说一下如何配置采样频率,采样频率是完全由定时器来掌控的,通过改变下面这两个值

TIM_TimeBaseInitStructure.TIM_Period = 279; // 修改为满足采样频率的周期值 TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; // 修改为满足采样频率的预分频器值

预分频比较好理解,就是将自己单片机的时钟分频几次(除几)。例如我的单片机时钟为72MHz,如果我设置预分频器值为6,那么现在的时钟为72M÷6=12M。 而周期是在基础上再进行分割,例如我这里设置的周期为280,在计算时需要+1处理。72M÷281=256.2277KHz。

还有一个转换时间的问题,可以通过下面两行代码来更改。

RCC_ADCCLKConfig(RCC_PCLK2_Div4);//分频 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_7Cycles5); //最后一个参数为采样时间的长度,单位是ADC时钟周期

分频和刚才说的一样,就是将ADC的时钟分频几次(除几)。这里我ADC的时钟与单片机的一致,分频4,故时钟为72MHz÷4=18MHz。 最后一个参数是指采样的时间长度。

This parameter can be one of the following values: * @arg ADC_SampleTime_1Cycles5: Sample time equal to 1.5 cycles * @arg ADC_SampleTime_7Cycles5: Sample time equal to 7.5 cycles * @arg ADC_SampleTime_13Cycles5: Sample time equal to 13.5 cycles * @arg ADC_SampleTime_28Cycles5: Sample time equal to 28.5 cycles * @arg ADC_SampleTime_41Cycles5: Sample time equal to 41.5 cycles * @arg ADC_SampleTime_55Cycles5: Sample time equal to 55.5 cycles * @arg ADC_SampleTime_71Cycles5: Sample time equal to 71.5 cycles * @arg ADC_SampleTime_239Cycles5: Sample time equal to 239.5 cycles

这些是官方所给的参数,到时候可根据实际情况带进去算,选择最合适的。 如何带入合适的参数呢 第一个方面 首先要自己确定好采样频率,必须要适中(大了小了都不行)。先解释为什么不能太小,采样定理规定采样频率必须要大于所采波形频率的2倍,如果采样频率定的很小,那么所采的波形范围就会变小。 之后要确定一下采样点数,采样点数的话当然是越多越好(越精确嘛)但是采样到的数据是要存在单片机里的,所以太多了单片机也存不下。一般采样点数为64、256、1024(官方给的库里就是这三个参数)。 最后用采样频率÷采样点数就得到了分辨率。举个例子,我这个代码中采样频率为256KHz,采样点数为256,所以采样的分辨率就是1KHz。 这个公式也解释了为什么采样频率不能定的很大,这样的话会导致分辨率变得很大。 第二个方面 采样频率有了,求倒数可得采样周期,也就是每隔多长时间采一个点。但是单片机的ADC采样不可能一瞬间就能把采到的模拟量转换为数字量,需要一段转换时间,所以转换时间一定要小于采样周期,否则得到的采样数据就会有问题。 转换时间太短也不好,可能会导致采样的值不准确(最理想的情况就是转换时间与采样周期相等)。 转换时间=采样时间长度÷时钟周期

下面就是关于配置ADC的全部代码

void ADC_GPIO_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //管脚 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //GPIO组 } void ADC_TIM3_Configuration(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 时钟分频(预分频器值)和周期值设置为满足 256 kHz 采样频率的配置 TIM_TimeBaseInitStructure.TIM_Period = 280; // 修改为满足采样频率的周期值 TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; // 修改为满足采样频率的预分频器值 TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); } void ADC_DMA_NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); DMA_ClearITPendingBit(DMA1_IT_TC1); DMA_ITConfig(DMA1_Channel1,DMA1_IT_TC1,ENABLE); } void ADC_DMA_Configuration(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)ADC_SourceData; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = SAMPLS_NUM; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE);//使能DMA ADC_DMA_NVIC_Configuration(); } void ADC_Init_Configuration(void)//ADC配置函数 { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div4);//分频 ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_7Cycles5); //最后一个参数为 ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_ExternalTrigConvCmd(ADC1, ENABLE); //ADC_SoftwareStartConvCmd(ADC1, ENABLE); } void Adc_Init(void) { ADC_GPIO_Configuration(); ADC_TIM3_Configuration(); ADC_DMA_Configuration(); ADC_Init_Configuration(); } //ADC_DMA中断服务程序 void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC1) != RESET) { global.adc_finish_fg = true; DMA_ClearITPendingBit(DMA1_IT_TC1); } } FFT函数

stm32F1会有官方给的FFT函数,这里分享给大家。 链接:https://pan.baidu.com/s/18ydSdSU6_8MIIK1tqq5Tlw?pwd=c8t6 提取码:c8t6

void cr4_fft_64_stm32(void *pssOUT, void pssIN, u16 Nbin); / 256 points */ void cr4_fft_256_stm32(void *pssOUT, void pssIN, u16 Nbin); / 1024 points */ void cr4_fft_1024_stm32(void *pssOUT, void *pssIN, u16 Nbin);

官方给了3种,区别在于采样点数不同,采样点数越高,进行FFT计算后的精度也就越高。 这里要注意输入的数据是有要求的:输入的数据长度要与函数名给的一致,每个输入样本都应该是一个复数值,通常以结构体的形式表示,包含两个成员:实部和虚部。

void Get_FFT_Source_Data(EN_FFT_CHANNEL channel_idx) { u16 i; for(i=0; i Get_FFT_Source_Data(FFT_CHANNEL_1); cr4_fft_256_stm32(FFT_OutData, FFT_SourceData, SAMPLS_NUM); GetPowerMag(); // for(int i = 0; i < SAMPLS_NUM; i++) { printf("FFT_SourceData[%d]: %lu\n", i, FFT_SourceData[i]); // } // printf("FFT_Mag values:\n"); // for(int i = 0; i < SAMPLS_NUM / 2; i++) { printf("FFT_Mag[%d]: %lu\n", i, FFT_Mag[i]); // } } 得到幅频特性序列 void GetPowerMag(void) { signed short lX, lY; float X, Y, Mag; unsigned short i; unsigned long maxMag = 0; unsigned long secondMaxMag = 0; unsigned short maxIndex = 0; unsigned short secondMaxIndex = 0; for(i = 0; i secondMaxMag = maxMag; secondMaxIndex = maxIndex; maxMag = FFT_Mag[i]; maxIndex = i; } else if (FFT_Mag[i] > secondMaxMag) { secondMaxMag = FFT_Mag[i]; secondMaxIndex = i; } } // printf("Maximum FFT_Mag value: %lu at index %u\n", maxMag, maxIndex); printf("波形频率:%u\nKHz", secondMaxIndex); }

得到FFT_Mag[i]之后,就快要成功了,在FFT_Mag[i]中找到最大的幅值所对应的序号,用序号×分辨率就得到了该波形的频率了。(这是理想情况) 实际情况是单片机只能采集0-3.3v的电压,在把波形送入单片机时必须要进行偏置,也就为信号混入了直流,可能会导致最大的幅值所对应的序号为0,所以我找的是第二大的点。(或者还有什么解决办法,大家可以在评论区说说)

主函数 int main(void) { Adc_Init(); USARTx_Init(115200); // 初始化串口 delay_init(72); // 初始化延时函数 while(1) { if(global.adc_finish_fg) { global.adc_finish_fg = true; FFT_test(); // printf("找到的最大值索引: %u\n", maxMagIndex); // for (int i = 0; i < SAMPLS_NUM; i++) // { // printf("adc值[%d]:%d\n",i, ADC_SourceData[i]); // } // 将标志复位为false,等待下一次DMA传输完成触发FFT计算 global.adc_finish_fg = false; } } }

最后附上完整代码链接:https://pan.baidu.com/s/1l6y9Iwpz7smP1SFnZNjVGQ?pwd=c8t6 提取码:c8t6



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭