4. 使用MSI001解调8027发出的已知单音信号
4.1 输出24Mhz和验证SPI接口
-
硬件连接
本例中我们添加MSI001相关的引脚也连接到STM32H750开发板。程序中操作的管脚如下描述:
2. RCC时钟输出24MHz驱动Msi001
MSI001芯片需要输入24MHz的时钟作为参考信号,在这里使用专门的时钟产生单元RCC产生24M的方波,提供给MSI001作为输入参考信号。
使能Master clock output1后,配置PLL1Q输出为48M,MCO1选择时钟源为PLL1Q,经过2分频后,得到24M时钟。
RCC产生24Mhz时钟单元STM32CUBE配置如下:
3. 硬件SPI接口配置
芯片的控制接口是SPI协议,要使芯片正常工作,首先SPI接口的操作要正常。这里向MSI001芯片配置频率为98.5Mhz,观察配置前MSI001和配置后差分输出管脚的波形变化。如果发生变化,说明SPI操作正常,芯片可以被控。这样进行后续调试才有初步把握。
需要配置STM32H750的硬件SPI,然后发出控制字操作MSI001芯片,确认板卡和芯片正常工作。SPI工作速度设为3.75Mhz.
4. 编写代码
在main中使能RCC输出,和写MSI001寄存器
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_I2C2_Init();
MX_DAC1_Init();
MX_TIM6_Init();
MX_SPI4_Init();
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);//tim2开启pwm,输出24Mhz
for(i=0;i< SIN_ROM_LENGTH;i++)//生成sin表
{
sin_25_rom[i] = (uint16_t)(sin(2*3.14*i/(SIN_ROM_LENGTH))*1000 +2047);
}
HAL_TIM_Base_Start_IT(&htim6);//tim6开启
HAL_DAC_Start(&hdac1,DAC_CHANNEL_1);//dac1的通道1开启
Qn8027_Init(); //qn8027初始化
Msi001_Init();//msi初始化
while (1)
{
}
}
添加MSI001驱动代码
#include "msi001/msi001.h"
SPI_HandleTypeDef *msi001_spi = &hspi4; ///
uint32_t g_msi001_reg[7]={0};//msi寄存器配置
//msi001的spi发送三个字节,
HAL_StatusTypeDef Msi001_SPI_Transmit(uint32_t Data)
{
HAL_StatusTypeDef errorcode = HAL_OK;
uint8_t pData[4];
pData[0] = (Data >>16)&0xFF;
pData[1] = (Data >>8)&0xFF;
pData[2] = (Data)&0xFF;
errorcode = HAL_SPI_Transmit(msi001_spi,pData,3,10);
return errorcode;
}
//msi001初始化,初始化成98.5M,改变寄存器参数配置不同频率
HAL_StatusTypeDef Msi001_Init(void)
{
uint32_t i=0;
HAL_StatusTypeDef errorcode = HAL_OK;
//labview上位机配置为98.5m
g_msi001_reg[0] = 0x043420;
g_msi001_reg[1] = 0x00C141;
g_msi001_reg[2] = 0x20BA12;
g_msi001_reg[3] = 0x00FFF3;
g_msi001_reg[4] = 0x000004;
g_msi001_reg[5] = 0x28DF55;
g_msi001_reg[6] = 0x200016;
for(i=0;i< 6;i++)
{
errorcode = Msi001_SPI_Transmit(g_msi001_reg[i]);
}
return errorcode;
}
5. 测试MCO输出的24MHz时钟
如果方便,可以使用示波器测试stm32开发板的PA8(RCC_MCO_1)管脚,观测有无24M的波形输出。
6. MSI001写测试
在前面程序中配置QN8027输出的单音FM信号在98.5M上,下面我们把MSI001的接收频点也配在98.5M,通过示波器查看MSI001芯片的IQ输出的波形。
在main.c中,我们调用了SPI.c中的程序对SPI4进行初始化,配置SPI的时钟,相位等;
在MSI001.c中,我们尝试写寄存器,使用示波器观察MSI001的反应:
- 在keil中用debug单步调试,复位后,打断点运行到初始化MSI001芯片前。
- 运行到下一行,配置寄存器0为0x143420后,示波器的表现如下:
- 再运行一行,配置寄存器0为0x243420后,示波器的表现如下:
- 再运行一行,配置寄存器0为0x043420后,示波器的表现如下:
如果IQ输出能够跟随我们写入的寄存器动作,这说明SPI时序正确,硬件也是好的,这时我们就可以进行下一步操作了。
注意,SPI时序写入这一步看上去虽然简单,却也是最经常出问题的步骤。如果遇到MSI001没有反应,建议用如下方法排查:
- 电源测试:MSI001供电是否正常;
- IO通断测试:使用IO输出高低电平,通过测量确定PCB焊接正确,且插对了孔位;
- SPI时序测试:使用示波器或逻辑分析仪捕获发出的SPI时序,判断是否SPI配置寄存器有错误;FPGA写的SPI程序,则要特别留意是否有代码bug。
- 如果管脚上的SPI时序正确,但MSI001如果没有应答,观察是否有虚焊等情况(开发板发货前经过测试,基本上可以排除电源和8027的焊接问题)
- 为减少MSI001死掉的几率,使能STM32或FPGA管脚内部的下拉或上拉电阻,SPI时序正常的情况下没有反应,可以全板掉电重启试试。
如果没有示波器,可以使用STM32内部ADC采集后通过UART传到上位机观察波形,请查看下一节内容。
4.2 ADC采集和UARTPlot
1. 硬件连接
本例中我们使用CMSIS-DAP上自带的UART2USB功能,把ADC采集到的数据发到电脑,通过UARTPlot软件观察采集的波形。程序中操作的管脚如下描述:
2. 配置ADC1/2同步差分输入DMA采集
STM32处理数据流的能力远不如FPGA,要实现实时信号处理,在STM32中我们需要使用双缓冲的方法,即准备两个数据段,采集A段数据的时候处理B数据段,采集B数据段的时候处理A数据段,这样才能确保波形的连续实时处理。
本例中我们要实现的功能是定时器TIM1输出TRG信号触发ADC1和ADC2,实现500KSPS采样率的DMA双缓冲采集,采集完后通过UART发送IQ数据给上位机,通过上位机软件观察波形。
实现思路如下:将DMA采集输出长度设为2000个点,采集完前半部分1000个点后,进入半回调函数HAL_ADC_ConvHalfCpltCallback中,标志位置1;后半部分1000个点采集完成后,进入HAL_ADC_ConvCpltCallback,标志位置2,停止DMA采集;在主程序中如果标志位等于2,就将IQ数据发送到PC上位机显示,再开启DMA进行下一帧数据采集。这样确保我们看到的波形正确后,就可以进入下一节,进行后面的FM解调处理。
实现思路框图如下:
在while循环中,一直进行标志位判断,标志位是2时,发送数据。回调函数和半回调函数都是通过DMA1_Stream0_IRQHandler中断进入。
具体ADC同步DMA采集介绍,可以回顾基础实验 “实验二十四 ADC定时器触发配合DMA双缓冲实现实时采集”
在程序中,添加了串口打印函数printf用于发送数据到上位机,可以回顾基础实验 “实验十六 串口通信”
3. 程序解读
while (1)
{
if(g_adc1_dma_complete_flag == 2) //采集数据完成
{
for(i=0;i< ADC_DATA_LENGTH;i++)
{
adc1_I_voltage[i] = ((float)3.3*((g_adc1_dma_data1[i])&0x0000ffff))/65536; //转换码值为电压值
adc2_Q_voltage[i] = ((float)3.3*((g_adc1_dma_data1[i] >>16)&0x0000ffff))/65536; //转换码值为电压值
}
//Send I Channel Data to PC
for(i=0;i< ADC_DATA_LENGTH;i++)
{
printf("%f\\r\\n", adc1_I_voltage[i]);
}
HAL_Delay(100);//Delay
//Send Q Channel Data to PC
for(i=0;i< ADC_DATA_LENGTH;i++)
{
printf("%f\\r\\n",adc2_Q_voltage[i]);
}
HAL_Delay(100);//Delay
//Restart DMA
g_adc1_dma_complete_flag = 0;
memset(&g_adc1_dma_data1[0],0,ADC_DATA_LENGTH); HAL_ADCEx_MultiModeStart_DMA(&hadc1,g_adc1_dma_data1,ADC_DATA_LENGTH);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
4. 使用UART将波形数据发送给UARTPlot (pyserial_display) 软件
使用串口数据接收软件“pyserial_display.exe”的步骤:
- 选择CMSIS-DAP对应的串口号
- 设定串口波特率115200
- 数据位8,校验位N,停止位1
- 波形长度2000,浮点类型,双通道模式
- CH1是I通道波形,CH2是Q通道波形。
- 点击开始采集,等待下位机数据。
UartPlot的使用注意事项:
- 确保上位机设置的UART参数(波特率、数据位、校验位、停止位)与大拇指开发板中的程序设定一致。
- 检查波形长度,通道数,显示数据格式是否和大拇指开发板中程序一致;
- 由于本软件没有使用帧头等传输协议,在使用软件时, 先在UARTPlot 上位机界面上点击开始采集,然后在大拇指开发板上启动数据传输 ,确保上位机软件捕获到数组的起始点,如果没有遵循上述启动流程,会出现波形截断现象。停止上位机并重复上述流程即可修复。
- H750例程使用的是板载DAP调试器的UART2USB功能,波特率设定为115200,上位机界面选择USB串行设备。注意:DAP调试器在Debug模式下同时使用UART2USB功能传输数据可能导致调试器死机(死机后表现为DAP连不上芯片,Keil报No Debug Device Found),此时按住H750板上的BOOT0按钮不放,重新拔插USB后下载已知可运行程序可以解决。
一帧数据显示
按住鼠标左键,可以上下左右移动波形,按住鼠标右键,可以放大或者缩小X轴或Y轴:
点击CH1或者CH2可以关闭或打开指定通道的波形显示
如果鼠标不能用,在波形显示界面点击鼠标右键,可以选择是否在X轴或Y轴启用鼠标:
在Plot Options里,可以对波形做FFT等处理
在Export里可以导出波形数据为JPG,或EXCEL文件
4.3 FM解调算法
1. FM解调算法回顾
求解频率,FM解调
在利用相位差分计算瞬时频率f(n)时,由于计算相位要用到除法和反正切运算,这对于非专用数字处理器来说是较复杂的,在用软件实现时,也可用下面的方法来计算瞬时频率f(n)
**对于FM信号,其振幅近似恒定,可以设定 **,则
这就是利用XI(n)和XQ(n)计算f(n)的近似公式。这种方法只有乘法和减法,计算简便。也是开发板例程中用到的方法。
2. 编写代码
if(g_adc1_dma_complete_flag == 2)
{
for(i=0;i< ADC_DATA_LENGTH;i++)
{
adc1_I_voltage[i] = ((float)3.3*((g_adc1_dma_data1[i])&0x0000ffff))/65536;
adc2_Q_voltage[i] = ((float)3.3*((g_adc1_dma_data1[i] >>16)&0x0000ffff))/65536;
}
// Send I Channel Data to PC
for(i=0;i< ADC_DATA_LENGTH;i++)
{
printf("%f\\r\\n",adc1_I_voltage[i]);
}
HAL_Delay(200);//Delay
// FM demodulate
for(i=0;i< ADC_DATA_LENGTH;i++)
{
if (i==ADC_DATA_LENGTH-1)
{
adc12_fm_out[i] = adc12_fm_out[i-1];
}
else
{
adc12_fm_out[i] = (adc1_I_voltage[i]*adc2_Q_voltage[i+1] - adc1_I_voltage[i+1]*adc2_Q_voltage[i])*1;
}
}
// Send demodulated data to PC
for(i=0;i< ADC_DATA_LENGTH;i++)
{
printf("%f\\r\\n",adc12_fm_out[i]);
}
HAL_Delay(200);//Delay
//Restart DMA
g_adc1_dma_complete_flag = 0;
memset(&g_adc1_dma_data1[0],0,ADC_DATA_LENGTH); HAL_ADCEx_MultiModeStart_DMA(&hadc1,g_adc1_dma_data1,ADC_DATA_LENGTH);
3. FM解调测试
CH1是I通道波形,CH2是解调后的波形。
解调后波形(红色)可以看出1KHz的成分,需要后续进行滤波处理。
4.3 实时信号抽取和DAC输出
1. 硬件连接
程序中操作的管脚如下描述:
2. 实时信号采集和500K信号抽取
STM32和FPGA不同的地方在于,MCU处理数据流的能力较弱,对于500KSPS的连续FIR滤波软件开销较大,因此在STM32程序中我们先抽取降速后再进行FIR滤波(下一节介绍),降低系统的运算量。
本例中我们要实现的功能为:定时器TIM1输出TRG信号触发500KSPS采样率的ADC1和ADC2,实现DMA双缓冲采集,进行实时采集->解调->每20个数据平均为1个数据->通过TIM6触发的刷新率为25KSPS的DAC发出。最后通过示波器观察DAC管脚波形。
定时器触发ADC做DMA双缓冲数据传输的实现思路是:
- 将DMA采集输出长度设为2000个点,DMA配置为循环模式一直进行自动采集;
- 采集完成前1000个点,调用半回调函数HAL_ADC_ConvHalfCpltCallback,进行解调,20次平均,如果平均后数据达到12500个后,标志位置1,while(1)中进行前半段12500个数据处理(DAC幅度变换,更新DAC数组);
- 采集完成后1000个点,调用回调函数HAL_ADC_ConvCpltCallback,进行解调,20次平均,平均后数据如果达到25000个后,标志位置2,在While(1)中进行后半段12500个数据处理(DAC幅度变换,更新DAC数组)。
实现思路框图如下:
具体ADC同步DMA采集介绍,可以回顾基础实验 “实验二十四 ADC定时器触发配合DMA双缓冲实现实时采集”:
3. While循环处理
在while循环中,一直进行标志位判断,标志位是1时,处理前半部分12500个数据处理,标志位是2时,处理后半部分12500个数据处理。完成回调函数和半完成回调函数都是通过DMA1_Stream0_IRQHandler中断进入。
4. 编写代码
在main中初始化接口,配置芯片,while循环中处理数据
int main(void)
{
uint32_t i=0;
float iq_temp=0;//临时存储
uint32_t dac2_start_flag=0;//第一次dac开启标志 HAL_Init();
SystemClock_Config();
PeriphCommonClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_I2C2_Init();
MX_DAC1_Init();
MX_TIM6_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_ADC2_Init();
MX_TIM1_Init();
MX_SPI4_Init();
MX_UART4_Init();
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);//tim2开启pwm,输出24Mhz
for(i=0;i< SIN_ROM_LENGTH;i++)//生成sin表
{
sin_25_rom[i] = (uint16_t)(sin(2*3.14*i/(SIN_ROM_LENGTH))*1000 +2047);
}
HAL_TIM_Base_Start_IT(&htim6);//tim6开启
HAL_DAC_Start(&hdac1,DAC_CHANNEL_1);//dac1的通道1开启
Qn8027_Init(); //qn8027初始化
Msi001_Init();//msi初始化
HAL_Delay(100);
HAL_TIM_Base_Start_IT(&htim1);//tim1开启
HAL_ADCEx_MultiModeStart_DMA(&hadc1,g_adc1_dma_data1,ADC_DATA_LENGTH);//ADC的dma开始采集
while (1)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(g_adc1_dma_complete_flag==1)//采集完前12500个数据后,进入这个部分
{
for(i=0;i< ADC_FIR_DATA_LENGTH/2;i++)//将fir滤波器输出值幅度缩小范围,再将直流偏置调整到1.65v,再计算出DAC对应的码值
{
iq_fir_out[i] = iq_fir_in[i]*0.9;
iq_temp = iq_fir_out[i]+1.65;
iq_temp = iq_temp/3.3;
iq_temp = iq_temp * 4095;
audio_out_dac[i] = ((uint16_t)iq_temp)&0x0fff;
}
if(dac2_start_flag ==0) //第一次进入dac开启,只需第一次开启
{
HAL_DAC_Start(&hdac1,DAC_CHANNEL_2);//dac1的通道2开启
dac_phase=0;
dac2_start_flag=1;
}
g_adc1_dma_complete_flag=0;
}
//////////////////////////////////////////////