首页 > 其他分享 >STM32H7 HAL库CubeMX 双重ADC模式同步采样详细配置+FFT计算相位差

STM32H7 HAL库CubeMX 双重ADC模式同步采样详细配置+FFT计算相位差

时间:2024-08-04 20:49:57浏览次数:10  
标签:采样 DMA HAL FFT STM32H7 GPIO ADC

前言

在电赛备赛期间琢磨了一下ADC同步采样的实现方式,本来是打算直接用AD7606来着,但是搞了半天也没把驱动整出来...考虑到AD7606本身采样率也拉不到太高,于是就花了几天时间把片上ADC配出来了。查资料的时候我发现关于STM32双重ADC模式的资料是真的少,用FFT算两路信号相位差的实例代码也半天没找到,于是干脆自己整理了一套。不过到了最后连ADC都没用上就是了,毕竟今年压根没出仪器仪表题(什么,你说B题?TI的垃圾板卡狗都不用)

一、片上同步采样的两种实现方案 对比

1. 软件同步

将两个ADC配置为独立模式(Independent mode),两个ADC的触发源设置为同一个定时器,这样一来不出意外的话两个ADC就能同步采样了,采样频率由定时器的频率决定。但是我在实际测试的时候发现,这样每一轮采样中总会有几个不连续的采样点,得到的波形都是间断的,原因我还没搞清楚。总之我最后没有采用这种方案。

2. 硬件同步

STM32H7的ADC1和ADC2可以配置为双重ADC模式(Dual ADC modes),在[RM0433_STM32H7x3和STM32H750单片机参考手册]的第983页可以看到详细介绍:
双重ADC模式
简单来说,ADC1和ADC2可以被配置成双ADC模式,在这种情况下ADC1处于主的地位,ADC2则处于从的地位。此时,两个ADC的转换的开始可以在硬件上配置为交替/同步进行。有四种基础模式:

  1. 注入同步模式(Injected simultaneous mode)
  2. 规则同步模式(Regular simultaneous mode)
  3. 交错模式(Interleaved mode)
  4. 交替触发模式(Alternate trigger mode)

这四种模式还能被结合成其他的模式,这里用不到,就不说了。比较有用的有两种模式:规则同步模式交错模式
先说规则同步模式,在该模式下,当主ADC(ADC1)的触发到来时,从ADC(ADC2)会收到一个同步的触发,于是两个ADC就会同步开始采样,整个过程是由硬件保证的。具体转换过程如下:
Dual
注意,在这张图中,两个ADC的转换时间(conversion段)未必是相等的。按照手册中的描述,对于序列的每次同时转换,从ADC的转换长度小于主ADC的转换长度
还有一个问题没有解决:ADC转换完成后的数据储存到哪里了?如果使用DMA的话,见下图的介绍:
DMA
这里给出了两种使用DMA读取数据的方式。第一种:分别为两个ADC配置DMA通道,然后从两个ADC各自的数据寄存器里读取数据。第二种:仅使用一个DMA通道(两个stream),使用一个32位公用数据寄存器ADCx_CDR。数据转换完成后,从ADC的数据将被存放在ADCx_CDR的高半字,主ADC的数据将被存放在ADCx_CDR的低半字,处理数据时只要位移16位把各自的数据取出来即可。
然后简单说一下交错模式。交错模式一般不用来同步采样,而是用来提高采样率。STM32ADC属于逐次逼近型ADC,每得到一个数据都要经过采样-转换这两个过程。交错模式使用了两个ADC,如果在一个ADC处于转换过程的间隙里启动另一个ADC对同一个通道采样,就相当于提高了时间的利用率,达到采样率翻倍的效果。具体过程如下图:
Dual2

二、双ADC规则同步模式配置

单片机型号:STM32H743VIT6 rev.V
CubeMX版本:6.9.2
关闭MPU和DCache,开启ICache
ADC1配置:
ADC1
ADC1的DMA配置:注意将数据宽度改为word,因为之后要将数据放到公共寄存器里面
DMA1
ADC2配置:
ADC2
ADC2的DMA配置:
DMA2
双ADC的外部触发源选为TIM8,这样采样频率就由TIM8的定时频率决定。TIM8的配置如下:
TIM8
为了方便调试最好再开个串口。所有东西配置完毕之后就生成代码。

三、Keil代码

3.1 ADC同步采样

基本逻辑:双ADC模式下,两个ADC采样的数据存入同一个32位数组ADC_Raw_Data,其中低16位存储主ADC,高16位存储从ADC。每次采样完毕后,在ADC传输完成中断里将标志位置1,在while循环里判断标志位,清零标志位,将存储的数据取出进一步处理(单精度)。
串口重定向

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart6, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}

定义一些后面会用到的变量

uint8_t ADC_FLAG=0;//采样完成标志位
uint32_t ADC_Raw_Data[1024];//接收双ADC的数据
uint16_t ADC_1_Value_DMA[1024];//存放ADC1的采样值,点的个数与FFT的点数相同
uint16_t ADC_2_Value_DMA[1024];//存放ADC2的采样值,点的个数与FFT的点数相同

ADC初始化部分,校准、开启ADC2、开启Multi-DMA。

HAL_ADCEx_Calibration_Start(&hadc1,ADC_CALIB_OFFSET_LINEARITY, ADC_SINGLE_ENDED);
HAL_ADCEx_Calibration_Start(&hadc2,ADC_CALIB_OFFSET_LINEARITY, ADC_SINGLE_ENDED);
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t *)ADC_Raw_Data, 1024);

要开启ADC采样,只需开启TIM8即可:

HAL_TIM_Base_Start(&htim8);

ADC每次传输完成都会进入一次回调中断函数,在中断里边写太多东西的话可能会导致ADC采样出现故障。。。所以我只写了关闭定时器和给标志位置1的代码,复杂点的计算就放到while循环里

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	 HAL_TIM_Base_Stop(&htim8);//停止采样
	 DMA_FLAGG = 1;//标志位置1
}

在while循环里判断按钮是否按下和采样是否完成。按钮只是为了方便调试。采样完成后就进行移位操作,放到两个数组里,单位换算后再发给串口。

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET)
	  {
		  HAL_Delay(10);
		  if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET)
		  {
			  HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_3);
			  while(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET);
			  HAL_TIM_Base_Start(&htim8);
		  }
	  }
	  
	  if(DMA_FLAGG==1)//ADC传输完成后。。。。。。。。。
	  {
		  DMA_FLAGG=0;//清空标志位
		  for(i=0;i<1024;i++)//取出两个通道的采样值
		  {
			  ADC_1_Value_DMA[i]=ADC_Raw_Data[i] & 0xffff;
			  ADC_Raw_Data[i] >>= 16;
			  ADC_2_Value_DMA[i]=ADC_Raw_Data[i] & 0xffff;
		  }
		  for(i=0;i<1024;i++)//去掉直流偏置后将采样值发送给串口
		  {
			  printf("%f,%f\n",(double)(ADC_1_Value_DMA[i]-32768),(double)(ADC_2_Value_DMA[i]-32768));
		  }	  
	  }
      
  }

最后采到的波形长这个样子:
wave
通过这个方法,采样率就算取到1MSPS也没有任何问题,不会出现间断的波形。

3.2 FFT测相位差的实现

得到两路采样数据之后,要得到相位差,只需要对两组采样数据分别进行FFT计算即可。先用FFT计算出幅频,遍历找到基波对应的下标,然后分别计算这两个点上的相位,作差即可。下面用MATLAB仿真1MSPS采样率下对两路10kHz,相位差60度的正弦波的FFT结果:
matlab
MATLAB代码贴在下面:

clear 
close all

Fs = 1000000;              % 采样率取1MSPS
N  = 1024;             % 采样点数1024点
n  = 0:N-1;            % 采样序列
t  = 0:1/Fs:1-1/Fs;     % 时间序列
f = n * Fs / N;          %真实的频率
fs = 100*1000;         %待测信号频率

x1 = 1*cos(2*pi*(fs)*t);   
x2= 1*cos(2*pi*(fs)*t+pi/3);

y1 = fft(x1, N);    %对原始信号做FFT变换
Mag1 = abs(y1);    %求FFT转换结果的模值
subplot(4,1,1);
plot(f, Mag1);       %绘制幅频相应曲线
title('幅频相应1');
xlabel('频率/Hz');
ylabel('幅度');

subplot(4,1,2);
plot(f,  angle(y1)*180/pi.*(Mag1 > 300)); %绘制相频响应曲线,注意这将弧度转换成了角度
title('相频响应1');
xlabel('频率/Hz');
ylabel('相角');

y2 = fft(x2, N);    %对原始信号做FFT变换
Mag2 = abs(y2);    %求FFT转换结果的模值
subplot(4,1,3);
plot(f, Mag1);       %绘制幅频相应曲线
title('幅频相应2');
xlabel('频率/Hz');
ylabel('幅度');

subplot(4,1,4);
plot(f,  angle(y2)*180/pi.*(Mag2 > 300)); %绘制相频响应曲线,注意这将弧度转换成了角度
title('相频响应2');
xlabel('频率/Hz');
ylabel('相角');

算出幅频特性之后,只取幅值大于300的点(其实就是基波)计算相位。两路信号的相位差为132.019-71.8157=60.2033,没有问题。
下面基于ARM DSP库的FFT函数在STM32上实现这一过程。

uint8_t ifftFlag=0;
uint8_t doBitReverse=1;
float DOUBLE[FFT_LENGTH];     //采样数据经过单位换算,加窗操作后存放在这里
arm_cfft_radix4_instance_f32 scfft;//定义scfft结构体
float FFT_InputBuf[FFT_LENGTH*2];	//FFT输入数组,大小为点数的两倍
float FFT_OutputBuf[FFT_LENGTH];	//FFT输出数组,大小等于点数

上面定义了几个基本变量。为了方便求出相位,我又写了两个函数,只需要输入要处理的数组地址就能返回基波的相位:

/*
* 函数名:Find_nMax
* 功能说明:求出幅频中的极大值
* 形参: ARR:要遍历的数组
*        N  FFT点数
*返 回 值: 极大值的下标
*/
uint32_t Find_nMax(float *ARR,uint32_t N)
{
	uint32_t i;
	float aMax=0;
	uint32_t nMax=0;
	for ( i = 1; i < N/2; i++)//i必须是1,是0的话,会把直流分量加进去!!!!
        {
           if (ARR[i]>aMax)
           {
            aMax = ARR[i];
            nMax=i;
           }
        }
	return nMax;
}
/*
* 函 数 名: Find_PhaseAngle
* 功能说明: 求出该数组中幅值最大的位置对应的相位角
* 形 参: ARR  ADC采样数组
*        N    遍历的总数量的一半
* 返 回 值: 相位角,单位:度
*/
float32_t Find_PhaseAngle(float32_t *ARR,float32_t N)
{
     uint16_t n; 
	/*按实部、虚部的顺序存储数据*/
	for(n=0;n<LENGTH_SAMPLES;n++)
	{
		FFT_InputBuf[2*n]=ARR[n];//实部记为ARR
		FFT_InputBuf[2*n+1]=0;   //虚部记为0
	}
	/*FFT变换*/     /***记得更换对应点数****/
	arm_cfft_f32(&arm_cfft_sR_f32_len1024,FFT_InputBuf,ifftFlag,doBitReverse);
	/*求解模值,求解模值的结果储存在FFT_OutputBuf[i]中*/
	arm_cmplx_mag_f32(FFT_InputBuf,FFT_OutputBuf,LENGTH_SAMPLES);
	n_Max_Temp = Find_nMax(FFT_OutputBuf,N);//找出幅值的最大值下标
	float32_t phase_TEMP=atan2f(FFT_InputBuf[2*n_Max_Temp+1],FFT_InputBuf[2*n_Max_Temp])* 180.0f/3.1415926f;//计算相位角
	return phase_TEMP;
}

有了上边这个函数,在原来的while循环里添加几行代码,将两路信号的相位计算出来,并发给串口:

while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET)
	  {
		  HAL_Delay(10);
		  if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET)
		  {
			  HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_3);
			  while(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_1)==GPIO_PIN_RESET);
			  HAL_TIM_Base_Start(&htim8);
		  }
	  }
	  
	  if(DMA_FLAGG==1)//ADC传输完成后。。。。。。。。。
	  {
		  DMA_FLAGG=0;
		  
		  for(i=0;i<1024;i++)//取出两个通道的采样值
		  {
			  ADC_1_Value_DMA[i]=ADC_Raw_Data[i] & 0xffff;
			  ADC_Raw_Data[i] >>= 16;
			  ADC_2_Value_DMA[i]=ADC_Raw_Data[i] & 0xffff;
		  }
		  for(i=0;i<1024;i++)//单位换算,顺便加窗
		  {
			  ADC_1_Real_Value[i] = (ADC_1_Value_DMA[i]-32768)*Reference_Voltage/65536;//换算为V
			  ADC_2_Real_Value[i] = (ADC_2_Value_DMA[i]-32768)*Reference_Voltage/65536;//换算为V
		  }
		  
		  float32_t phase1 = Find_PhaseAngle(ADC_1_Real_Value,1024);//计算ADC1采样的相位
		  printf("n1:%f\n",(double)(n_Max_Temp*1000000/1024));//发送频率值
		  float32_t phase2 = Find_PhaseAngle(ADC_2_Real_Value,1024);//计算ADC2采样的相位
		  printf("n2:%f\n",(double)(n_Max_Temp*1000000/1024));//发送频率值
		  printf("phase1:%f\n",phase1);//发送相位
		  printf("phase2:%f\n",phase2);//发送相位
	  }
  }

四、相位计算实例

使用函数发生器生成两路完全相同的正弦信号:
CH1:2Vpp,10kHz,phase=0°
CH2:2Vpp,10kHz,phase=0°
**采样率:1MSPS **
在矩形窗和Hamming窗下,计算结果如下表所示:
Phase1
相位差结果符合预期,十分接近零。而且,从表中可以看出加不加窗对相位差计算并没有太大的影响,虽然都说加窗可以减小频谱泄露,但是目前为止,除了能让频谱图好看一些以外,我暂时还没有体会到对FFT加窗有什么很显著的优点...事实上不加窗的频谱泄露也有对应的修正手段,详见这篇博客:浅谈信号处理加窗修正
为了验证相位计算的精度,我又测了几组数据,这次两路信号的配置如下:
CH1:2Vpp,10kHz,phase=90°
CH2:2Vpp,10kHz,phase=0°
采样率:1MSPS,加Hamming窗
p1
将所有数据打到Excel里边:
excel
需要注意的是,表中有两个数据相位差达到了-270°,这种情况下手动加上360°,不过误差也会大一些。除了这两个数据以外,相位测量误差基本稳定在0.2%。

标签:采样,DMA,HAL,FFT,STM32H7,GPIO,ADC
From: https://www.cnblogs.com/Fisika/p/18340625

相关文章

  • 基于大预言模型的AI逻辑推理#AI夏令营 #Datawhale #夏令营
    这是基于第二届世界科学智能大赛逻辑推理赛道:复杂推理能力评估(点击跳转赛事官网)的一篇从0基础入门AI的实践课,适合于零基础小白,本篇文章是我自己的学习笔记,供大家参考1.大语言模型介绍大语言模型的概念大语言模型(英文:LargeLanguageModel,缩写LLM),是一种人工智能模型,旨在理......
  • Datawhale AI 暑期夏令营 第四期Task3
    Transformer架构Transformer是一种用于自然语言处理(NLP)和其他序列到序列(sequence-to-sequence)任务的深度学习模型架构,它在2017年由Vaswani等人首次提出。Transformer架构引入了自注意力机制(self-attentionmechanism),这是一个关键的创新,使其在处理序列数据时表现出色。以下是Tran......
  • datawhale AI夏令营AI+经济task3
    Task3:进阶上分方向讨论特征工程详细解析特征工程是机器学习预处理过程的核心部分,关注从原始数据中提取和构建新特征以优化模型的表现。其核心目的在于通过精心选择和创新性构建特征,从而让模型更有效地解析数据并准确预测目标变量。时间序列特征构造详解时间序列数据的特征构造......
  • Floyd-Warshall
    Floyd-WarshallFloyd-Warshall算法的核心思想是利用动态规划的思想,通过逐步更新距离矩阵来求解所有节点对之间的最短路径。它适用于解决图中节点对数量较小且权重可以为负的情况。时间复杂度为O(n^3)。java代码模板staticfinalintINF=Integer.MAX_VALUE;//graph中......
  • Datawhale AI夏令营(笔记③)
    #DatawhaleAI夏令营##AI##夏令营#    今天的主要内容是学习如何用Transformer建模SMILES进行反应产率预测。Task2(即笔记②)中,我们学习使用RNN建模SMILES,且发现了RNN在处理这类问题的局限性。那么今天,我们就跟着本节内容,看看Transformer是如何解决RNN的缺陷的。一......
  • Task3 逻辑推理方向 #datawhale#LoRA
    写在前面本次博客主要是2024年第三期的第三次任务,涉及了大模型微调等相关内容。经过先前的两次任务,我们已经学会了如何去调用大模型,但是对于如何提升大模型,我们可能只能靠更加精准的提问,这种靠更加精准的提问来获得更好的效果的方式我们称为提示词工程(promptengineering)关......
  • Task2 baseline01 精读 #datawhale夏令营
    写在前面这个是datawhale夏令营2024年第三期的第二次Task的笔记,由于Task2要求的是精读代码,而我在第一篇文章中已经精读的差不多了,这篇文章我就总结升华一下上篇文章的内容,并且补充说明一下上一篇文章说的不太完善的__main__函数后面的部分内容。好了,我们开始今天的探索之旅......
  • Datawhale AI夏令营(第三期)
    Task4上分加油站提分思路如何使用大语言模型实现top方案呢?一是训练&微调模型提升解决逻辑推理问题的能力。二是使用各种prompt工程、agent系统方案,达到模型解决逻辑推理问题的能力边界。从这两个方面向大家介绍如何提分,有哪些方案可以选择,可以参考哪些资料。模型训练&......
  • 【Android驱动07】Sensor传感器框架以及驱动移植和调试方法(Hal层部分)
    一,Androidsensor系统架构Hal就是对Linux内核驱动程序的封装,向上提供接口,屏蔽低层的实现细节。也就是说,把对硬件的支持分成了两层,一层放在用户空间(UserSpace),一层放在内核空间(KernelSpace),其中,硬件抽象层运行在用户空间,而Linux内核驱动程序运行在内核空间。二,HAL层Sen......
  • Datawhale AI夏令营(AI+生命科学)深度学习-Task3直播笔记
    机器学习lgm上分思路    1、引入新特征(1)对于Task2特征的再刻画        GC含量是siRNA效率中的一个重要且基本的参数,可以作为模型预测的特征。这是因为低GC含量会导致非特异性和较弱的结合,而高GC含量可能会阻碍siRNA双链在解旋酶和RISC复合体作用下的解旋。......