前言
在电赛备赛期间琢磨了一下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页可以看到详细介绍:
简单来说,ADC1和ADC2可以被配置成双ADC模式,在这种情况下ADC1处于主的地位,ADC2则处于从的地位。此时,两个ADC的转换的开始可以在硬件上配置为交替/同步进行。有四种基础模式:
- 注入同步模式(Injected simultaneous mode)
- 规则同步模式(Regular simultaneous mode)
- 交错模式(Interleaved mode)
- 交替触发模式(Alternate trigger mode)
这四种模式还能被结合成其他的模式,这里用不到,就不说了。比较有用的有两种模式:规则同步模式和交错模式。
先说规则同步模式,在该模式下,当主ADC(ADC1)的触发到来时,从ADC(ADC2)会收到一个同步的触发,于是两个ADC就会同步开始采样,整个过程是由硬件保证的。具体转换过程如下:
注意,在这张图中,两个ADC的转换时间(conversion段)未必是相等的。按照手册中的描述,对于序列的每次同时转换,从ADC的转换长度小于主ADC的转换长度。
还有一个问题没有解决:ADC转换完成后的数据储存到哪里了?如果使用DMA的话,见下图的介绍:
这里给出了两种使用DMA读取数据的方式。第一种:分别为两个ADC配置DMA通道,然后从两个ADC各自的数据寄存器里读取数据。第二种:仅使用一个DMA通道(两个stream),使用一个32位公用数据寄存器ADCx_CDR。数据转换完成后,从ADC的数据将被存放在ADCx_CDR的高半字,主ADC的数据将被存放在ADCx_CDR的低半字,处理数据时只要位移16位把各自的数据取出来即可。
然后简单说一下交错模式。交错模式一般不用来同步采样,而是用来提高采样率。STM32ADC属于逐次逼近型ADC,每得到一个数据都要经过采样-转换这两个过程。交错模式使用了两个ADC,如果在一个ADC处于转换过程的间隙里启动另一个ADC对同一个通道采样,就相当于提高了时间的利用率,达到采样率翻倍的效果。具体过程如下图:
二、双ADC规则同步模式配置
单片机型号:STM32H743VIT6 rev.V
CubeMX版本:6.9.2
关闭MPU和DCache,开启ICache
ADC1配置:
ADC1的DMA配置:注意将数据宽度改为word,因为之后要将数据放到公共寄存器里面
ADC2配置:
ADC2的DMA配置:
双ADC的外部触发源选为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));
}
}
}
最后采到的波形长这个样子:
通过这个方法,采样率就算取到1MSPS也没有任何问题,不会出现间断的波形。
3.2 FFT测相位差的实现
得到两路采样数据之后,要得到相位差,只需要对两组采样数据分别进行FFT计算即可。先用FFT计算出幅频,遍历找到基波对应的下标,然后分别计算这两个点上的相位,作差即可。下面用MATLAB仿真1MSPS采样率下对两路10kHz,相位差60度的正弦波的FFT结果:
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窗下,计算结果如下表所示:
相位差结果符合预期,十分接近零。而且,从表中可以看出加不加窗对相位差计算并没有太大的影响,虽然都说加窗可以减小频谱泄露,但是目前为止,除了能让频谱图好看一些以外,我暂时还没有体会到对FFT加窗有什么很显著的优点...事实上不加窗的频谱泄露也有对应的修正手段,详见这篇博客:浅谈信号处理加窗修正
为了验证相位计算的精度,我又测了几组数据,这次两路信号的配置如下:
CH1:2Vpp,10kHz,phase=90°
CH2:2Vpp,10kHz,phase=0°
采样率:1MSPS,加Hamming窗
将所有数据打到Excel里边:
需要注意的是,表中有两个数据相位差达到了-270°,这种情况下手动加上360°,不过误差也会大一些。除了这两个数据以外,相位测量误差基本稳定在0.2%。