当前位置:   article > 正文

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

stm32 ad读取正弦波

大致思路

本文代码可以实现用单片机对一个未知频率的正弦波进行采样,经过运算后得到该频率。
实现方法为:通过定时器设置采样频率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	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这些是官方所给的参数,到时候可根据实际情况带进去算,选择最合适的。
如何带入合适的参数呢
第一个方面
首先要自己确定好采样频率,必须要适中(大了小了都不行)。先解释为什么不能太小,采样定理规定采样频率必须要大于所采波形频率的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);
	}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104

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<SAMPLS_NUM; i++)
	{
		FFT_SourceData[i] = ((signed short)ADC_SourceData[i]) << 16;
	}
}

void FFT_test(void)
{
    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]);
//    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

得到幅频特性序列

 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 < SAMPLS_NUM / 2; i++)
    {
        lX = (FFT_OutData[i] << 16) >> 16;
        lY = (FFT_OutData[i] >> 16);

        X = SAMPLS_NUM * ((float)lX) / 32768;
        Y = SAMPLS_NUM * ((float)lY) / 32768;

        Mag = sqrt(X * X + Y * Y) / SAMPLS_NUM;

        FFT_Mag[i] = (unsigned long)(Mag * 65536);

        if (FFT_Mag[i] > maxMag) {
            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);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

得到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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/967767
推荐阅读
相关标签
  

闽ICP备14008679号