为了实现使用STM32f103制作一个简易数值示波器,大体流程如下:
1 - 模拟信号(输入)
2- ADC(模数转换)( 适当采样率和分辨率配置)
3 - DMA(直接存储器访问)(DMA搬运,减少CPU负担)
4 - 内存缓冲区(数据转入缓冲区)
5 - FFT处理(频域数据)(FFT处理(可选,用于频域显示))
6 - 显示处理(像素转换)(数据映射为像素)
7 - TFT显示屏 ( 实时更新显示内容)
下面分步骤给大家讲解一下:
一:采样频率的选择
触发ADC采样的方式常见的是使用定时器触发和直接使用软件触发,可在ADC初始化中自行选择,定时器触发可以更加方便的修改采样频率,所以我选择使用定时器触发采样举例。
了解了采样频率后再介绍一下ADC采样周期,他是触发ADC采样以后采集一组数据需要的时间,可进行选择,一般选采样时间长一点采集数据更加准确,但是不能大于采样频率的时间间隔。
根据奈奎斯特采样定理,采样率必须至少是信号最高频率分量的两倍,才能准确重构该信号。例如,如果你要采样的信号最高频率是10kHz,那么采样率至少应该是20kHz。在实际应用中,通常会选择比奈奎斯特频率更高的采样率,以获得更好的信号质量和抗混叠能力。例如,对于10kHz的信号,可能选择40kHz或更高的采样率。
二:信号采集ADC的配置
以下是初始化ADC的代码示例:
这段代码的主要功能是配置STM32的ADC1,以采集连接在GPIOA第6个引脚上的模拟信号,并使用DMA高效地将转换后的数据传送到内存缓冲区。具体步骤如下:
1,使能时钟:使能GPIOA和ADC1的时钟。
2,设置ADC时钟:将ADC时钟分频至12 MHz。
3,配置GPIO引脚:将GPIOA的第6个引脚配置为模拟输入。
4,复位ADC:复位ADC1。
5,配置ADC参数:设置ADC工作模式(独立模式)、连续模式(禁止)、触发源(定时器2通道2)、数据对齐(右对齐)、通道数(1)。
6,启用ADC和DMA:使能ADC1和其DMA功能。
7,校准ADC:复位校准寄存器并等待校准完成。
8,配置ADC通道:设置ADC通道6的采样时间。
9,启动ADC转换:开启ADC的软件转换启动功能。
这样配置完成后,可以通过DMA将采集到的数据自动搬运到内存缓冲区,方便后续处理和显示。
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE ); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PA6 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1); //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //模数转换工作在非连续转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2; //转换由定时器2的通道2触发(只有在上升沿时可以触发)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_DMACmd(ADC1, ENABLE); //ADC的DMA功能使能
ADC_ResetCalibration(ADC1); //使能复位校准
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 1, ADC_SampleTime_7Cycles5 );//ADC1通道6,采样时间为239.5周期
ADC_ResetCalibration(ADC1);//复位较准寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
需要注意的是,我们的单片机并不能采集负电压,常见的方法是对包含小于0的信号加上一个直流偏置,保证波形的采样完整性。
三:DMA的配置
下面的代码的功能是配置并初始化DMA,以便从外设(例如ADC)传输数据到内存。其中涉及几个关键步骤:
1,使能DMA时钟:使能DMA1的时钟。
2,复位DMA通道:将指定DMA通道的寄存器恢复到默认设置。
3,配置DMA通道参数:包括外设基地址、内存基地址、传输方向、缓存大小、地址增量模式、数据宽度、工作模式、优先级等。
4,使能DMA中断:启用DMA传输完成中断。
5,配置中断优先级:设置NVIC中断的优先级和使能中断。
6,启动DMA传输:使能DMA通道,开始数据传输。
通过这种配置,可以将ADC采集到的数据高效地传输到内存,实现高性能的数据采集和处理。
代码和注释如下
/******************************************************************
函数名称:MYDMA1_Config()
函数功能:DMA1初始化配置
参数说明:DMA_CHx:DMA通道选择
cpar:DMA外设ADC基地址
cmar:DMA内存基地址
cndtrDMA通道的DMA缓存的大小
备 注:
*******************************************************************/
void MYDMA1_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
{
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设ADC基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存//
DMA_InitStructure.DMA_BufferSize = cndtr; //DMA通道的DMA缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //DMA通道 x拥有高优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA_CHx, &DMA_InitStructure); //ADC1匹配DMA通道1
DMA_ITConfig(DMA1_Channel1,DMA1_IT_TC1,ENABLE); //使能DMA传输中断
//配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0 ;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
DMA_Cmd(DMA1_Channel1,ENABLE);//使能DMA通道
}
这里我们选择1024个采样点进行数据处理或FFT分析有多个原因,主要有以下:
1,使用1024个样点简化了很多数学运算,尤其是在频域的处理和分析。
2,使用1024个样点形成的数据块大小适中,适合传输和存储。每次传输固定长度的数据块便于同步和管理。
四:对ADC采集的数据进行处理
我们需要写一段代码用于处理从ADC采集到的数据,并进行一些预处理操作,包括查找数据的最大值和最小值以及为FFT(快速傅里叶变换)准备输入数据
代码如下:
adcmax=adcx[1];
adcmin=adcx[1];
for(i=0;i<NPT;i++)
{
fftin[i] = 0;
fftin[i] = adcx[i] << 16;
if(adcx[i] >= adcmax)
{
adcmax = adcx[i];
}
if(adcx[i] <= adcmin)
{
adcmin = adcx[i];
}
}
代码的逻辑和总结如下:
1,初始化最大值和最小值:将最大值adcmax和最小值adcmin初始化为数组adcx的第二个元素。
2,找到最大和最小值:遍历adcx数组,找到其中的最大值和最小值。
3,准备FFT输入数据:将adcx中的每个值左移16位并存储到fftin数组中,为后续的FFT处理做准备。这样可以将定点格式数据转换为浮点格式,便于FFT运算。
这种处理方法在信号处理中非常常见,比如数字示波器或者频谱分析仪应用中。这段代码为后续的频域分析(使用FFT)做好了准备,对信号的最大值和最小值进行了统计,这样可以了解信号的动态范围。
/
五:FFT运算
这里我写了一个进行快速傅里叶变换(FFT)的函数,通过将fftin数组的数据进行处理后转运到fftout中
/******************************************************************
函数名称:GetPowerMag()
函数功能:计算各次谐波幅值
参数说明:
备 注:先将lBufOutArray分解成实部(X)和虚部(Y),然后计算幅值(sqrt(X*X+Y*Y)
*******************************************************************/
float GetPowerMag(void)
{
u32 temp = 0; // 幅值最大的频率成分
float X = 0.0, Y = 0.0, Mag = 0.0, magmax = 0.0; // 实部,虚部,各频率幅值,最大幅值
// 调用自 cr4_fft_1024_stm32
cr4_fft_1024_stm32(fftout, fftin, NPT);
for (i = 0; i < NPT / 2; i++)
{
X = (fftout[i] << 16) >> 16; // 低16位存实部
Y = (fftout[i] >> 16); // 高16位存虚部
Mag = sqrt(X * X + Y * Y); // 计算模值
FFT_Mag[i] = Mag; // 存入缓存,用于输出查验
}
代码执行以下几个步骤:
1,定义并初始化临时变量,用于存储计算过程中需要的实部、虚部、幅值和最大幅值等信息。
2,调用FFT函数‘cr4_fft_1024_stm32,对输入数据fftin进行1024点FFT变换,结果存储在fftout中。
3,遍历频谱的一半,提取频率分量的实部和虚部,并计算其幅值。
这段代码的典型应用场景包括频谱分析、信号处理等。
六:显示波形/频谱
如果是显示波形,下面我们只需要把ADC采集转换后的数组显示在屏幕上,然后每进行一次循环进行更新一次就能够显示对应的波形了
代码如下:
u16 zoom_factor = 1; // 放大倍数,可以根据需要调整
void clear_point(u16 mode)
{
u16 x, i, past_vol, pre_vol;
static u16 h;
POINT_COLOR = BLUE;
fre = 72000000 / T / pre; // 更新采样频率
// LCD_ShowNum(685,326,fre,5,16); // 更新采样率显示
for (x = 0; x < 599 / zoom_factor; x++) // 从x=0开始,调整x的范围
{
POINT_COLOR = BLACK; // 按列清除
for (int z = 0; z < zoom_factor; z++)
{
u16 x_pos = x * zoom_factor + z;
if (x_pos != 309) // y轴不进行列清除
lcd_huaxian(x_pos, 24, x_pos, 456);
}
// 绘制坐标
POINT_COLOR = WHITE;
for (int z = 0; z < zoom_factor; z++)
{
u16 x_pos = x * zoom_factor + z;
lcd_huadian(x_pos, 240, WHITE); // 绘制水平中线
if (x_pos == table[h])
{
lcd_huaxian(x_pos, 241, x_pos, 243); // 绘制坐标刻度
h++;
if (h >= 58) h = 0;
}
if (x_pos == 310)
{
lcd_huaxian(x_pos, 20, x_pos, 460); // 绘制y轴
for (i = 30; i < 460; i += 10)
{
lcd_huaxian(306, i, 309, i); // 绘制y轴刻度
}
}
}
pre_vol = 70 + adcx[x] / 4096.0 * 350;
// 波形更新
if (mode == 1)
{
POINT_COLOR = YELLOW;
for (int z = 0; z < zoom_factor; z++)
{
u16 x_pos = x * zoom_factor + z;
if (x_pos > 0 && x_pos < 599 && x_pos != 310)
{
if (x_pos > 0)
{
lcd_huaxian(x_pos - 1, past_vol, x_pos, pre_vol);
}
else
{
lcd_huadian(x_pos, pre_vol, YELLOW);
}
}
}
}
else
{
for (int z = 0; z < zoom_factor; z++)
{
u16 x_pos = x * zoom_factor + z;
lcd_huadian(x_pos, pre_vol, YELLOW);
}
}
past_vol = pre_vol;
}
}
其中的u16 zoom_factor 是放大倍数,我是加了一个外中断进行设置,past_vol 和 pre_vol存储前一个和当前采样点的y轴值。
其中的 pre_vol = 70 + adcx[x] / 4096.0 * 350;
pre_vol:计算电压点位置。70 为纵向偏置,350 为缩放因子,adcx[x] 为当前ADC样本值。
如果需要显示频谱波形,只需要改成pre_vol = 70 + FFT_Mag[x]就OK了。
通过不断的清除和绘制自己设定的背景和坐标系,便能够实现在屏幕上绘制波形啦