Bootstrap

从零开始制作一个简易数字示波器,超详细看完必懂 举例基于stm32f103标准库

为了实现使用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了。

通过不断的清除和绘制自己设定的背景和坐标系,便能够实现在屏幕上绘制波形啦

;