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

news/2025/3/17 17:28:44/文章来源:https://www.cnblogs.com/Fisika/p/18340625

前言

在电赛备赛期间琢磨了一下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 allFs = 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];//实部记为ARRFFT_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;//换算为VADC_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%。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/777876.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python pymodbus类库使用学习总结

实践环境 Python 3.9.13 https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe pymodbus-3.6.8-py3-none-any.whl https://files.pythonhosted.org/packages/35/19/a9d16f74548d6750acf6604fa74c2cd165b5bc955fe021bf5e1fa04acf14/pymodbus-3.6.8-py3-none-any.w…

ThinkAdmin_v6两个简单漏洞(文件读取+信息泄露)

危险函数:file_get_contents()第二次遇到侵权声明 本文章中的所有内容(包括但不限于文字、图像和其他媒体)仅供教育和参考目的。如果在本文章中使用了任何受版权保护的材料,我们满怀敬意地承认该内容的版权归原作者所有。 如果您是版权持有人,并且认为您的作品被侵犯,请通…

ubuntu22.04容器安装ssh服务

除了特别说明外,否则以下命令均为ubuntu 22.04 容器内执行!!!安装 查看ubuntu22.04 有没有安装openssh-server,执行命令:sudo dpkg --list | grep ssh没有找到openssh-server的包,很显然,没有安装,那么就开始安装,执行:sudo apt install openssh-server安装完成后,…

3.Java基础语法

注释单行注释 //单行注释 //输入一个Hello World!多行注释:可以注释一段文字 /* 注释 */ //多行注释:可以注释一段文字 /* 注释 */ /* 多行注释 多行注释 多行注释 */文档注释:JavaDoc /** * @Description HelloWorld * @Author 爱吃麻辣烫的妹纸 */⭐️注意:书写注释是…

分享圣诞树+雪人+全屏动效

分享圣诞树+雪人+全屏动效 创建时间:2024年8月4号 分享之前学习老师发的几个小玩意 一、圣诞树 运行: 点击该exe即可出来一棵圣诞树。退出: 鼠标点击该圣诞树右键二、雪人 运行: 双击点开exe即可退出: 和圣诞树一样、 三、音乐:满满都是爱 运行:双击打开,该程序对鼠标的…

java:一键生成二维码工具类

前言:本工具选择了Zxing,他是一个开源的,使用java实现多种格式的1D/2D条码图像处理库, 1.pom添加依赖<!-- 二维码生成&识别组件 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>…

Luogu P10842 Piggy and Trees 题解 [ 绿 ] [ 拆边 ] [ 贡献思维 ]

Piggy and Trees:把路径拆成边的思维题。 思路 一看到这题的路径,就想到了 Luogu P3177 树上染色 这题化路径为边的贡献,分别计算的思维。 那么对于此题,先来观察题目里式子的意思:对于树上的每个无序点对,求出树上每个点 到这些点对之间的最短路径的 距离之和。枚举点对…

ZeRO:一种去除冗余的数据并行方案

ZeRO:一种去除冗余的数据并行方案 目前训练超大规模语言模型主要有两条技术路线:TPU + XLA + TensorFlow/JAX GPU + Pytorch + Megatron + DeepSpeed 前者由Google主导,由于TPU和自家云平台GCP深度绑定,对于非Googler来说并不友好 后者背后则有NVIDIA、Meta、MS等大厂加持,…

【攻防技术系列+权限维持】注册表运行键

在红队行动期间在网络中获得初步立足点是一项耗时的任务。因此,持久化是红队行动成功的关键,因为这将使团队能够专注于交战目标,而不会失去与指挥和控制服务器的通信。 创建将在 Windows 登录期间执行任意负载的注册表项是红队剧本中最古老的隐藏技巧之一。这种持久性技术需…

链表part02

今天是8月3日,学习了链表的第二部分。交换链表两个节点,考察对next的操作和tmp的灵活运用。 删除链表的倒数第N个节点,双指针减少遍历次数。 链表相交,移动链表尾对齐,其实就是动长链表的指针。 环形链表,记住方法。4. 24交换链表两个节点 题目:给你一个链表,两两交换其…

Qt-pyqt6与QTDesginers的相互使用技巧

1. 先在QT Designers Tools 设计器中画好框架,再保存为.ui文件导出 2. 再pycharm中创建一个main.py文件用来加载和使用这个.ui文件,通用的代码如下: main.py from PyQt6.QtGui import QIntValidator, QIcon from PyQt6.QtWidgets import QApplication, QLabel, QListWidgetI…

Go中使用Zap日志库与Lumberjack日志切割

Go中使用Zap日志库与Lumberjack日志切割Go中使用Zap日志库与Lumberjack日志切割 原创 何泽丰 ProgrammerHe2024年06月11日 20:15 广东 听全文Go中使用Zap日志库与Lumberjack日志切割 概述 在项目中使用日志记录有助于快速定位和修复问题,能帮助我们监控系统健康状态及时发现问…