示波器制作过程真的是困难重重,之前没学过嵌入式,就连51单片机也没学过,借鉴了许多大佬的思路才做出;
- 实现功能
(1)能够通过AIN1和AIN2测量外部的信号,通过定时器触发ADC实现采样率的精确设置,可测量DC-50khz的信号。显示 波形频率,峰峰值,最大值,最小值,平均值。通调节运放的同相输入端的电压,可以实现输入电压范围的 改变,并在屏幕上展示出来。通过调整采样率改变示波器的X轴拉伸.
通过改变SN74LVC1G3157,波形在y轴的拉伸
(2)可以进行fft运算,并在oled上展示频谱。
(3)在PB1输出方波作为测试信号
- 实现思路
(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数据,遍历数组找出最大最小值,做差得出峰峰值,对数组求和取平均值。
从左到右分别是频率 ,峰峰值,平均电压
求频率的原理是依据根据频谱求频率
从左往右数第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)//展示频谱
这里的频谱信息显示的不是很完整,原本我想再单独开一个该屏幕来展示频谱的.但没能弄出来
下来就是在外部中断里对按键进行判断给对应标志位赋值,确定用户的输入,这个写起来非常麻烦,是否切换屏幕标志,是否刷新屏幕标志,对应选项,对应通道标志等等。在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轴伸缩来回切换。重新进入菜单界面后只有按下旋转编码器才能重新进行菜单选择
50khz
测试信号使用580HZ的矩形波,直接配置定时器输出PWM波,
菜单界面说明
通道选择
通向参考电压选择:用于改变输入范围
采样率设置
fft显示开关
通道输入电压范围
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工具也不是很会用,感谢群里的老师和同学,我借鉴了很多有用的知识。