基于STM32G031设计一个带频谱分析功能的双通道示波器
基本实现波形与频谱展示,可通过按键和编码器对示波器的参数进行设置,对波形幅度和时间伸缩,测试信号使用方波信号。
标签
嵌入式系统
STM32G031
简易示波器
FFT
2022寒假在家练
频谱展示
x鑫鑫
更新2022-03-03
1509

内容介绍

示波器制作过程真的是困难重重,之前没学过嵌入式,就连51单片机也没学过,借鉴了许多大佬的思路才做出;

Fg0wzfKPT_NNGjs7gYwlx6VFcoB3

  1. 实现功能

    (1)能够通过AIN1和AIN2测量外部的信号,通过定时器触发ADC实现采样率的精确设置,可测量DC-50khz的信号。显示 波形频率,峰峰值,最大值,最小值,平均值。通调节运放的同相输入端的电压,可以实现输入电压范围的 改变,并在屏幕上展示出来。通过调整采样率改变示波器的X轴拉伸.

    通过改变SN74LVC1G3157,波形在y轴的拉伸

             (2)可以进行fft运算,并在oled上展示频谱。

             (3)在PB1输出方波作为测试信号

  2. 实现思路
    (1)ADC双通道采集

         使用cubemx 使能ADC的两个通道并且使能连续扫描和DMA continu REQUEST 选项 ADC触发方式选择定时器输出事件触发,在定时器的pwm上升沿和下降沿触发AD采样。通道优先级通道一高于通道二。在相应的定时器中使能相关的输出事件,在查阅硬禾学堂和csdn上的资料后,指出此时定时器要使能通道3,选择产生PWM但不输出模式,使能更新事件。

这是因为g031的定时器1通道3对应的引脚是被OLED使用的。

//ADC初始化    
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;//连续转换
    hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
    hadc1.Init.LowPowerAutoWait = DISABLE;
    hadc1.Init.LowPowerAutoPowerOff = DISABLE;
    hadc1.Init.ContinuousConvMode = DISABLE;
    hadc1.Init.NbrOfConversion = 2;//通道数
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO2;
    hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISINGFALLING;
    hadc1.Init.DMAContinuousRequests = ENABLE;//DMADMAContinuousRequests 使能

   //定时器输出事件及输出模式配置
    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
    sMasterConfig.MasterOutputTrigger2 = TIM_TRGO2_UPDATE;
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
    if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
    {
        Error_Handler();
    }
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
    sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
    if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
    {
        Error_Handler();
    }

定时器设置为不分频,以64MHZ运行,

通过设置输出比较寄存器和重装载寄存器 来确定pwm波周期,进而确定ADC 采样周期

/* USER CODE BEGIN 1 */
//设置采样率 这里参考的是氢化脱氯次氯酸同学的方法
void set_sample_rate(uint32_t sample_rate)
{
    __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3, 32000000 / sample_rate);
    __HAL_TIM_SET_AUTORELOAD(&htim1, 64000000 / sample_rate - 1);
    TIM1->EGR = TIM_EGR_UG;
}

(2)显示峰峰值,平均值,最大值,最小值。就是对采集的ADC数据,遍历数组找出最大最小值,做差得出峰峰值,对数组求和取平均值。

FsAcKdzlzRvsuUxT3wT41r6_YoDI

从左到右分别是频率 ,峰峰值,平均电压

        求频率的原理是依据根据频谱求频率

从左往右数第n根谱线对应的模拟频率是:n * fs /N
其中n表示第n根谱线,取值从0开始到N-1,显然左侧第0根对应的就是直流分量.
fs表示采样频率
N表示FFT的点数.

 

//求平均电压
static void get_dc_value(uint16_t *ADCValue, uint16_t *a_dc_value)//求均值
{
    uint16_t i;
    uint32_t a_sum = 0;
    for (i = 0; i < 256; i++)
        a_sum += ADCValue[i];
    *a_dc_value = a_sum / 256;

}
#define FFT_BIN(num, fs, size)                                                 \
  ((num) *                                                                       \
   ((fs) / (size))) ///< return the center frequency of FFT bin 'num'
///< based on the sample rate and FFT stize
//找到频谱最大值
uint8_t spectrum_max(uint16_t *FFTValue, uint8_t ignore_dc)
{
    uint8_t i;
    uint8_t temp_max_index = ignore_dc ? 2 : 0;

    for (i = ignore_dc ? 3 : 1; i <= 256 / 2; i++)
        if (FFTValue[i] > FFTValue[temp_max_index])
            temp_max_index = i;

    return temp_max_index;
}

计算输入电压,外部输入电压经过的实际上是一个双端输入的运算放大电路,由同相端的电压和输出点,反馈系数共同计算出反向输入端的电压

 

//计算原始的输入电压
//vrf参考电压的值
//反馈电阻的状态 sw的取值
int com_v(int vrf,uint8_t sw,int uo)
{

    if(sw==1)
        vrf=(vrf)*11;
        uo=10*uo;
        int sum=(double)(vrf-uo);
        return sum ;
    } else
    {
        vrf=(vrf)*3;
        uo=2*uo;
        int sum=(double)(vrf-uo);
        return sum ;
    }

}

波形显示函数,这里并没有使用往显示芯片里写入gram的方法,而使用每次写入要画当前点的像素和之前点的像素。使用的是B站UP尔等小众的示波方法,

在具体使用要给从ADC获取的值乘以一个比例系数以确保图像在屏幕适合的位置

这里选取的是把示波区域在下方的函数

#define accur 18*3.3/4096  //0.015295//accur=18*3.3/4096(3.3/4096就是ADC采样精度,18是为了让波形转化一下能够显示在适当位子)

uint8_t Bef2[3];//保存前一个数据的几个参数1.要写在第几页2.0x01要移动几位3.写什么数据
uint8_t Cur2[3];//当前前一个数据1.要写在第几页2.0x01要移动几位3.写什么数据
void Bef2ore_State_Update2(uint8_t y)//根据y的值,求出前一个数据的有关参数
{
	Bef2[0]=7-y/8+10;//移动图像上下位置
	Bef2[1]=7-y%8;
	Bef2[2]=1<<Bef2[1];
}
void Cur2rent_State_Update2(uint8_t y)//根据Y值,求出当前数据的有关参数
{
	Cur2[0]=7-y/8+10;//移动图像上下位置 //数据写在第几页 23 7- 2=5; 54 7- 6=1
	Cur2[1]=7-y%8;//0x01要移动的位数  23 7-7=0 ;54 7-6=1
	Cur2[2]=1<<Cur2[1];//要写什么数据 1<<0
}
void OLED_DrawWave2(uint8_t x,uint8_t y)
{
//printf("adc1 %d\r\n",y);
	int8_t page_sub;
	uint8_t page_buff,i,j;
	Cur2rent_State_Update2(y);//根据Y值,求出当前数据的有关参数
	page_sub=Bef2[0]-Cur2[0];//当前值与前一个值的页数相比较
	//确定当前列,每一页应该写什么数据
	if(page_sub>0)
	{
		page_buff=Bef2[0];
		OLED_SetPos(page_buff,x);
		OLED_WR_Byte(Bef2[2]-0x01,OLED_DATA);
		page_buff--;
		for(i=0;i<page_sub-1;i++)
		{
			OLED_SetPos(page_buff,x);
			OLED_WR_Byte(0xff,OLED_DATA);
			page_buff--;
		}
		OLED_SetPos(page_buff,x);
		OLED_WR_Byte(0xff<<Cur2[1],OLED_DATA);
	}
	else if(page_sub==0)
	{
		if(Cur2[1]==Bef2[1])
		{
			OLED_SetPos(Cur2[0],x);
			OLED_WR_Byte(Cur2[2],OLED_DATA);
		}
		else if(Cur2[1]>Bef2[1])
		{
			OLED_SetPos(Cur2[0],x);
			WriteDat((Cur2[2]-Bef2[2])|Cur2[2]);
		}
		else if(Cur2[1]<Bef2[1])
		{
			OLED_SetPos(Cur2[0],x);
			WriteDat(Bef2[2]-Cur2[2]);
		}
	}
	else if(page_sub<0)
	{
		page_buff=Cur2[0];
		OLED_SetPos(page_buff,x);
		WriteDat((Cur2[2]<<1)-0x01);
		page_buff--;
		for(i=0;i<0-page_sub-1;i++)
		{
			OLED_SetPos(page_buff,x);
			WriteDat(0xff);
			page_buff--;
		}
		OLED_SetPos(page_buff,x);
		WriteDat(0xff<<(Bef2[1]+1));
	}
	Bef2ore_State_Update2(y);
	//把下一列,每一页的数据清除掉
	for(i=10;i<16;i++)//确定波形显示的位置的下限单位 页数
	{
		OLED_SetPos(i, x+1) ;
		for(j=0;j<1;j++)
			WriteDat(0x00);
	}
}

绘图界面展示函数,都会去调用波形展示函数 把缓冲区的一个一个点展示出来

函数里对通道1、2分别展示

 
void show_wave(uint16_t *channl1,uint16_t *channl2 )//展示波形
void FFTwave(uint16_t *fft)//展示频谱

Fh9-71ZmHWibwjQ87BIZWb508CAp

这里的频谱信息显示的不是很完整,原本我想再单独开一个该屏幕来展示频谱的.但没能弄出来

下来就是在外部中断里对按键进行判断给对应标志位赋值,确定用户的输入,这个写起来非常麻烦,是否切换屏幕标志,是否刷新屏幕标志,对应选项,对应通道标志等等。在ui显示和按键回调中反复使用。

void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin) {
    HAL_Delay(5);
    if(GPIO_Pin==KEY1_Pin) {
        if(key2==1) {
//            printf("%d..............\r\n",1);
           key1=0;
        }else{
			 key1++;
		}
       
        if(key1==6) {
            key1=0;
        }
        LED(ON);
    }
    if(GPIO_Pin==KEY2_Pin) {
        LED(OFF);

		key3++;
				if(key3==3)
		{
		key3=0;key1=0;
	
		}
		if(key3==0)
		{
		
			l=1;
		}
//		if(key3==2)
//		{
//cot.swictch=0;
//		key3=1;
//		}
		if(key3==1)
		{
		l=2;
		}
    }
    if(GPIO_Pin==KEY4_Pin) {
        if(GPIO_Pin==KEY4_Pin) {
            LED_TR;
			
			key2=!key2;
			
        }
    }
    if(GPIO_Pin==KEY3_Pin ) {
        //printf("----------\r\n");
        uint8_t kt;
        kt=HAL_GPIO_ReadPin(KEY5_GPIO_Port,KEY5_Pin );
        //把旋钮另一端电平状态记录
        HAL_Delay(10);
        //延时
        if(!HAL_GPIO_ReadPin(KEY3_GPIO_Port,KEY3_Pin)) {
            if(kt==0) {
                a=1;
                //右转
                printf("%d反转----------------------------\r\n",b++);
                if(key1==1) {
                    if(cot.swictch==3) {
                        cot.swictch=0;
//						l=255;
                    }
                    cot.swictch++;
                }
                if(key1==2) {
                    if(cot.swictch==1) {
                        ain.vrf1=ain.vrf1-100;
                        if(ain.vrf1==65536-100) {
                            ain.vrf1=3300;
                        }
                    }
                    if(cot.swictch==2) {
                        ain.vrf2=ain.vrf2-100;
                        if(ain.vrf2==65536-100) {
                            ain.vrf2=3300;
                        }
                    }
                }
				if(key1==3)
				{
				if(cot.swictch==1)
				{ain.sw1=!ain.sw1;}
				
				
				if(cot.swictch==2)
				{ain.sw2=!ain.sw2;}
				}
				if(key1==4)
				{
				if(ain.rate>=5000000)
				{
				ain.rate=5000000;
				
				
				}
				ain.rate=ain.rate/10;
				
				
				}

				if(key1==5)
				{
				if(cot.swictch==1)
				{
					fftshow1=!fftshow1;
				
				}
								if(cot.swictch==2)
				{
					fftshow2=!fftshow2;
				
				}
				}


            } else {
                a=2;
                //左转
                if(key1==1) {
                    cot.swictch--;
                    if(cot.swictch==256-1) {
                        cot.swictch=2;
                    }
                }
                if(key1==2) {
                    if(cot.swictch==1) {
                        ain.vrf1=ain.vrf1+100;
                        if(ain.vrf1==3400) {
                            ain.vrf1=0;
                        }
                    }
                    if(cot.swictch==2) {
                        ain.vrf2=ain.vrf2+100;
                        if(ain.vrf2==3400) {
                            ain.vrf2=0;
                        }
                    } 
				}
				if(key1==3)
				{
				if(cot.swictch==1)
				{ain.sw1=!ain.sw1;}
				
				
				if(cot.swictch==2)
				{ain.sw2=!ain.sw2;}
				}
								if(key1==4)
				{
				if(ain.rate<=50)
				{
				ain.rate=50;
				}
				ain.rate=ain.rate*10;
				
				}
				if(key1==5)
				{
				if(cot.swictch==1)
				{
					fftshow1=!fftshow1;
				
				}
								if(cot.swictch==2)
				{
					fftshow2=!fftshow2;
				
				}
				}
            }
        }

    }
}

波形Y轴的拉伸与收缩,主要是依靠控制可变电阻改变运放的增益实现的,可变电阻值的会影响运放的反馈电路阻值,进而影响运放的增益,这里是模电的知识。

波形的横向拉伸是用改变采样率来实现的

在示波器界面按左边的黑色键进入波形变换模式,此时按键将循环在y轴伸缩,和x轴伸缩来回切换。重新进入菜单界面后只有按下旋转编码器才能重新进行菜单选择

FtQTqzkdrTLgQFGd9SGnlmTKLeVF

Fn2hm_1GxYyQ7K8wCgz6N3rQqnqT

50khz

FitZuW29HZ0ix0FlZmE_9azGGu4y

测试信号使用580HZ的矩形波,直接配置定时器输出PWM波,

菜单界面说明

通道选择

通向参考电压选择:用于改变输入范围

采样率设置

fft显示开关

通道输入电压范围

FuvYrVNKrOwjrFlvCwVw4A_8LB9D

         3.存在的不足与改进办法

            1.fft频谱展示不够清晰,频谱的X,Y轴缺少相关信息。

            2.GUI制作困难,菜单界面制作简陋,有些情况下,该刷新没刷新。有的时候一             直刷新,影响观感。有些数值没有单位,有些数值溢出。我的做法是为每一个选项单独做一个显示帧。引出来一个问题,就是重复内容很多但必须分离在不同选项中,代码写起来很臃肿,观感很不好。

            3.程序设计过于混乱,经常被各种标志位绕来绕去。并且内存占用过多,为了方便波形和频谱的展示及运算。我使用了很多的数组来缓存。程序运行的很多时间都在各种数组之间赋值。

            4.测试信号不是正弦波,在用DAC产生正弦波时,我就知道输出基本波形必须先生成相应的点。我是通过matlab生成的,之后把pwm配置成重装载寄存器的值和点数最大值一致。matlab的点存放在数组里,通过DMA写入的比较寄存器里改变占空比,产生不同电压,进而生成波形。由于示波器也是用了DMA传输,测试信号也使用DMA传输,最开始他们的DMA传输优先级相同,导致程序卡住,示波器无法输出波形,测试信号也不能输出。我修改优先级后示波器能够工作但测试信号不能输出。我认为是ADC传输的数据长期占据DMA总线导致。所以我退而求其次使用矩形波作为测试信号。

            5.波形显示也不好。和之前同学显示波形的方式对比。他们的使用oled里的画线函数和画点函数需要往显示芯片里写入gram,我使用的示波函数是一个点一个画的,类似与野火的示波方式。所以我移植一套函数过去给上面的示波函数使用,并作了一定程度修改。虽然波形显示了,但如果此时给示波区域加入画线函数。由于oled显示方式,每次写入一页进入一页中只有一行是有效像素其他是无效的。因此会覆盖之前的波形,所以在加入网格后波形就消失了。因此我去掉了网格在示波区域。只加了X,Y轴及显示最大值,最小值。

            6.采样率无法自动设定,必须认为调节到输入信号频率的2倍以上才能显示出正常波形

            不能微调,我觉旋转编码器微调太慢,所以采样率都是10倍10倍的增加、减少。

            7.波形触发没有实现,主要是没有精力了

           改进办法:

               1.使用嵌入式GUI来制作菜单界面,我了解到玲珑GUI对硬件资源要求较低可以尝试用它来做。

               2.单独展示频谱和示波器,使用状态机或者操作系统来进行任务调度。给各种操作进行抽象使代码层次化。

               3。产生测试信号,受硬件资源限制这次只能用pwm模拟dac产生测试信号,我在之前单独测试时用G031产生了三角波信号。这个信号的频率不稳定切毛刺过多。我准备使用硬件DAC来产生所需要的信号,分时复用DMA传输所需的dac值。

               4.自适应采样,我的想法是单独接一路输入信号到定时器,用定时器算出测试信号的大致频率 再根据采样定理进行采样率的设定,这样频谱算出来频率也更精确。使用上位机对示波器的参数进行精确设置。

              

           4. 收获与总结

                  感谢硬禾学堂提供硬件平台和硬件资料。我从中学到很多有用的知识,比从前只会点灯的应用要实用很多。同时我也学习到了很多嵌入式,信号处理 相关的东西,对曾经学过的理论值有了跟深刻体会。

               说实话我在最开始时HAL库也不怎懂,CUBEMX工具也不是很会用,感谢群里的老师和同学,我借鉴了很多有用的知识。

 

 

附件下载

app_2.zip
源码
two_2.hex
hex文件

团队介绍

信阳学院 物联网工程 尚鑫
团队成员
x鑫鑫

评论

0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号