1.项目介绍
本项目是基于硬禾学堂“寒假一起练——基于stm32的双通道示波器学习平台”的板卡硬件实现的带有双通道,电平触发,FFT变换,峰峰值、直流、频率自动测量的时基幅值可调的示波器。最高可以测量峰峰值为33V的信号。
2.设计思路(附框图)
3.硬件介绍
本项目基于硬禾学堂寒假一起练活动的“基于STM32的简易示波器/频谱仪/信号发生器学习平台”配套板卡,其中使用的部分包括前级衰减电路,通过数据选择器实现的数字电阻选择电路,以及OLED显示电路,ADC采样和PWM输出通过STM32编程内部硬件实现。
4.实现的功能及图片展示
·使用说明:左按键用于选择需要调节的参数,右按键用于参数切换,旋钮用于放大或缩小参数
·实现双通道切换波形显示
CH1正弦波显示
CH2三角波显示
·实现电平触发功能
具体展示请观看视频,视频中波形触发稳定,每次显示的位置基本不变,可以看出电平触发已经完全实现
·实现时基调整和幅度调整功能
具体观看视屏动态展示
·实现FFT计算和频谱显示功能
显示正弦波频谱
显示方波频谱
·实现频谱幅度调整功能
具体看视频演示
·实现通过PWM产生测试方波
5.主要代码片段及说明
1)外设使能
单片机配置完成后,要对外设使能,启动PWM,ADC,DMA。其中ADC设置为双通道,扫描模式,PWM外部触发,DMA传输式采集,每次采集两个数据,同时将采集的数据放在一个数组里(长度为2),通过选择将位置0的数据或1的数据压入队列来实现选择对CH1还是CH2的数据展示和处理。
OLED_Init();//显示屏初始化
Set_Switch();//设置放大器倍数
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)ADC_Data,2);//开始ADC转换DMA传输
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_2);//PWN输出方波
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);//PWM触发ADC采样
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1);//PWM输出直流偏置
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_4);//PWM输出直流偏置
HAL_TIM_Base_Start_IT(&htim17);//定时器17触发按键扫描
2)DMA传输完毕中断函数
每次采集数据时,先根据选择的通道将数据压入队列中,然后当采集1000个数据时,对数据进行触发扫描。若此时设置的模式是时域波形显示,则将触发扫描后的100个点展示出来,若此时设置的是频谱显示,则将触发扫描后的128的点,通过FFT算法计算出频谱,之后再展示。
void DMA1_Channel2_3_IRQHandler(void)
{
static uint16_t count=0;//用来记录采样了多少数据
uint8_t i=0;
HAL_DMA_IRQHandler(&hdma_adc1);
FIFO_in(ADC_Data[OS_Channel]);//通道选择,将对应通道的ADC值入队列
if(count==1000)
{
HAL_ADC_Stop_DMA(&hadc1);
Update_Key();
Trigger_Scan();
if(OS_Mode==FFT)
{
for(i=0;i<FFT_LEN;++i)
{
fft_arry[Rank_128[i]].Re=Get_vol(FIFO_out());
fft_arry[Rank_128[i]].Im=(float)0;
}
FFT_process();
}
display();
FIFO_clear();
count=0;
Set_TIM2_Period(TIM2_Period);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)ADC_Data,2);
}
++count;
}
3)按键扫描
按键扫描由独立的定时器中断触发,共扫描两个按键,一个旋转编码器,对按键和编码器的操作进行识别,一但识别成功,就将操作压入按键命令队列中,通过ADC采集中断实现对操作的异步处理,简化了程序结构。
void Key_Scan(void)
{
uint8_t i=0;
static uint16_t counter[5]={0};
key_mes_typedef key_mes;
for(i=0;i<2;++i)
{
if(HAL_GPIO_ReadPin(keyP[i].key_port,keyP[i].key_pin)==GPIO_PIN_RESET)//按键扫描
{
if(counter[i]<=KEY_DOWN_TIME+1)//按键延时确认
++counter[i];
if(counter[i]<KEY_DOWN_TIME)
continue;
else if(counter[i]==KEY_DOWN_TIME)
{
key_mes.key=i;
key_mes.key_status=KEY_DOWN;
Key_FIFO_IN(key_mes);
}
}
else
counter[i]=0;
}
if(HAL_GPIO_ReadPin(keyP[2].key_port,keyP[2].key_pin)==GPIO_PIN_RESET && fall_flag==0)//编码器扫描
{
if(counter[2]<50)
++counter[2];
if(counter[2]==20)//下降沿延迟确认
{
fall_flag=1;
counter[2]=0;
}
}
else if(HAL_GPIO_ReadPin(keyP[2].key_port,keyP[2].key_pin)==GPIO_PIN_SET && fall_flag==1)//上升沿时读取数据
{
if(HAL_GPIO_ReadPin(keyP[3].key_port,keyP[3].key_pin)==GPIO_PIN_SET)
{
key_mes.key=coder1;
key_mes.key_status=CODER_DOWN;
Key_FIFO_IN(key_mes);
fall_flag=0;
}
else if(HAL_GPIO_ReadPin(keyP[3].key_port,keyP[3].key_pin)==GPIO_PIN_RESET)
{
key_mes.key=coder1;
key_mes.key_status=CODER_UP;
Key_FIFO_IN(key_mes);
fall_flag=0;
}
}
}
4)数据展示函数
当触发扫描结束之后,处理器将采集的数据显示出来,通过以下一系列操作实现
void display(void)
{
voltage_per_pix=voltage_per_div/10;//设置单位像素的电压值
TIM2_Period=time_per_div*64/5;//计算计时器的计数值
OLED_Clear();//屏幕清屏
Update_UI();//更新UI显示
DC_vol=0;//重置直流值
max_vol=none_val;//重置最大值和最小值
min_vol=none_val;
Draw_Axis_Point();//画坐标点
Draw_Axis();//画坐标轴
Draw_Curve(100,128);//绘制曲线
OLED_Refresh();//屏幕刷新
Set_Switch();//设置放大器参数
}
其中曲线绘制函数如下,根据时域显示和频域显示两种模式分别实现曲线绘制函数,当系统处于时域显示模式时,将数据队列中的100个数据映射到OLED坐标点,当系统处于频域显示模式时,将计算完成的FFT数据映射到显示屏中,二者的映射函数不同,因此要区别编写。
void Draw_Curve(uint8_t data_length,uint8_t fft_length)
{
uint8_t i=0;
uint8_t y1=0;
uint8_t y2=0;
if(OS_Mode==OS)//当模式为时域时绘制100个点
{
Set_Display_Para();//设置展示参数
for(i=0;i<data_length-1;++i)
{
if(i==0)
y1=Get_y(FIFO_out());
if(i!=0)
y1=y2;
y2=Get_y(FIFO_out());
OLED_DrawLine(i,y1,i+1,y2,1);
}
}
else if(OS_Mode==FFT)//当模式为频域时绘制128个点
{
for(i=0;i<fft_length-1;++i)
{
//printf("fft%d:%f\r\n",i,fft_arry[i].Re);
OLED_DrawLine(i,fft_Get_y(fft_arry[i].Re),i+1,fft_Get_y(fft_arry[i+1].Re),1);
}
}
}
5)FFT计算算法实现
由于FFT全部是由复数运算实现的,因此首先要构造复数结构,同时要实现复数加法,减法,乘法,共轭运算,相反数运算,指数运算等所有涉及的基本运算作为基础,之后才能实现FFT算法。一下为FFT算法的核心部分,即蝶形运算,其是在复数运算实现的基础上完成的。该算法选用的是时间抽取基2算法,其中时间抽取部分为了节省空间,已经在实数转换为复数时已经实现了,这里只展示了蝶形运算的相关算法。
void FFT_process(void)//fft计算函数
{
uint16_t i=0;
uint16_t j=0;
uint16_t k=0;
uint8_t length=0;
uint8_t dN=0;
com_f temp1;
com_f temp2;
for(i=0;i<log2(FFT_LEN);++i)//蝶形运算
{
length=pow(2,i+1);
for(j=0;j<FFT_LEN/length;++j)
{
dN=FFT_LEN/length;
for(k=0;k<length/2;++k)
{
fft_arry[j*length+length/2+k]=com_Mul_com(W_m_N((float)k*dN,(float)FFT_LEN),fft_arry[j*length+length/2+k]);
temp1=com_Add(fft_arry[j*length+k],fft_arry[j*length+length/2+k]);
temp2=com_Add(fft_arry[j*length+k],com_inv(fft_arry[j*length+length/2+k]));
fft_arry[j*length+k]=temp1;
fft_arry[j*length+length/2+k]=temp2;
}
}
}
for(i=0;i<FFT_LEN;++i)
{
fft_arry[i].Re=com_abs(fft_arry[i]);//将计算结果取绝对值并放在数组的实部中
}
}
6.遇到的主要难题及解决方法
1)FFT算法软件实现
由于许多芯片都含有硬件FFT功能,因此FFT算法的软件实现在网络上资源较少,本次实验中的FFT计算算法完全是本人依照《数字信号处理》教材中的基2时间抽取FFT计算过程一步步实现的。其中出现了时间抽取时空间复杂度过大导致内存溢出的问题,后通过对代码的优化解决,并正确计算出来频谱。
2)编码器驱动
编码器驱动编写时出现了抖动问题,即由于编码器在旋转过程中出现许多毛刺,影响系统对编码器操作的判断。通过不断尝试,本人采用了下降沿延迟确认,上升沿立即确认的方案,很好的解决了抖动毛刺产生的影响。
3)PWM触发ADC采样DMA传输
本次实验中本人第一次接触等间隔采样问题,并不了解通过PWM触发ADC采样的过程,经过不断地查找资料和仔细阅读技术手册,最终成功实现PWM触发ADC采样DMA传输同时可以通过计算改变计数器的值实现采样间隔的更改。
4)波形电压与OLED坐标之间的映射
波形电压与OLED坐标之间的映射也是本次实验的一大难点,因为一个电压映射点的坐标与坐标中心值,每DIV的电压值,放大器的倍数以及OLED的尺寸都有关,一个细节错误都有可能导致bug的产生导致显示不正确。
7.未来的计划或建议
目前此项目的实现的示波器采样还不能达到很高,当采样律过高时系统会因为来不及处理而导致系统卡死,因此选用更快的AD芯片和处理器以及对算法时间复杂度的优化可以提升系统的带宽和性能。
同时,本项目的最高采样周期为60us,若继续减小采样时间,会导致系统卡死。这也是之后需要改进的问题,通过进一遍缩小采样周期可以增大系统带宽。
程序源代码以及工程项目:
链接: https://pan.baidu.com/s/1LEAP34cDpG8_UjSsnwHZ0Q?pwd=tbh4 提取码: tbh4