目录
前言
出于产品需求,需要在产品中集成示波器功能,满足显示实时电压的需求。
这篇文章就总结和讨论一下示波器的方案设计(低成本)。
其中,实现方案主要是在3、4章节,1、2是概念部分和环境介绍,不需要的可以跳过。
如下是我设计时的主要参考资料。
1、硬件模块
总体而言,能够提供的硬件模块2个:MCU的ADC接口,LCD屏幕,别的就没了;
放大电路、滤波功能统统没有,demo板子直接用MCU的ADC接口采集外部电压,将处理后的数据送到屏幕去刷新。
同时为了模拟测量数据,还使用DAC+定时器来输出正弦波、方波、三角波等波形。
2、示波器基础知识
2.1 当头一棒就是,波形的概念
概念1:波形是指相应物理量在时间和空间上的分布情况的图形抽象。
概念2:正弦波是在时域中定义的,其它任何非正弦波形都可以看作是正弦波的叠加。
2 .2 第二就是需要观察的波形参数
如下图所示。
基本观察参数:最大值、最小值和峰峰值(不是错别字,就是峰峰值,最差);
其它参数还有:顶端值、底端值和幅值,这三者都是在稳定值中计算出来的;
时域参数:上升时间,下降时间,正脉宽,负脉宽,这四者都是测量值,还有频率、占空比,是计算出来的值。
一般我们需要显示的,就是①最大值、最小值和峰峰值,②频率、占空比,③X、Y轴单位大小
2.3 第三就是示波器参数
2.3.1 采样率
采样率是指一秒内能采集到多少个样本值。
理论上说,采样率最大要超过被测信号最大频率的5倍以上,才能较为完整的采集到数据规律。(为什么?被测信号频率除以采样频率,其实就是在被测信号一个周期内能采集到的点数,若小于5个点,则很难具体描绘出数据规律)
STM32单片机的采样率,等于采样时钟除以采样周期,以STM32F4系列芯片为例,ADC最大时钟为36MHz,ADC最小采样周期为3+12个周期,故采样率最大为36/15=2.4MHz。
实际应用中:
1、根据主频时钟,ADC实际分频得到的时钟计算,比如同时使用USB接口时,主频最大168MHz,ADC最大时钟21MHz;
2、可以通过双路或三路ADC同时使用,提高ADC采样率;
2.3.2 带宽
带宽的定义:在频幅特性曲线中,当信号衰减至-3db(70.7%)时,此时的频点定义为示波器的带宽。
如何计算?我还没闹明白,留到后面仔细研究。
但是可以明白的是,带宽要大于被测信号的5倍以上(普遍认为的)才能不失真。
采样率低和带宽小对波形带来的的几种影响:
2.3.4 刷新率
刷新率,即刷新一帧波形图像的速率。
假设刷新一张波形(大约2万个像素点)需要15ms,则刷新率就等于66帧每秒。
刷新率主要受限于MCU主频,以及数据处理效率,和屏幕刷新效率。
数据处理和屏幕刷新的提高有这么几种思路:
1、裸机运行
减少任务切换带来的时间消耗(微秒级)
2、利用缓存
以空间换时间:一种是将函数封装写入存储设备,提高调用效率;一种是开辟缓存(全局变量)存储数据,便于处理波形数据和LCD像素点数据。
3、优化算法
一是优化代码,二是提高编译器优化等级。
对于前期的开发工作,都可以先抛开,先上RTOS,再实现基本的波形实时输出功能,并先在功能上进行优化。优化完毕之后,再在刷新时间上进行尝试优化(可以逐步降级,但不能逐步升级)。
3、ADC采集和DAC输出
前提 {
芯片平台:STM32F429ZGT6
ADC:PA3
DAC:PA4
}
3.1 ADC 采集实现
3.1.1 配置ADC采集为定时触发DMA采集模式
如下以PA3为例,①选择定时器3触发,②选择最小采样周期三,③选择DMA循环模式或Normal模式(半字大小)
3.1.2 配置ADC关联的定时器
如下,选择定时器触发事件为更新事件。
由此我们就用两种手段判断ADC数据转换是否结束:
1、读取ADC的DMAbuf的count值
注意ADC的DMA Count读取函数,读取到的数目,而不是实际长度;比如BUF长度为300,每个单元2byte大小,当DMA满时读取到的数值时300(而不是像串口中断那样,读到的是0)。
2、在DMA完成中断的回调函数中判断
触发完成中断时,肯定意味着1次转换结束,可以直接处理,也可以只关闭不处理(等待线程去处理)。
3.1.3 转换成有效值
ADC数据的第一层处理,就是要根据自己的硬件电路设计,将ADC数值转换成真实电压值。
我这里是MCU直连,所以转换关系就是 vol = adc_value * 3300 / 4096 .
参考资料中,DS201中硬件电路使用了输入放大器和挡位切换电路,需要由硬件工程师计算电阻分压和放大关系,最终得到计算公式。
有了这一层数据,才方便后续进行算法消抖、归一化处理等等。
3.2 DAC波形输出
由于测量内部电压的波形基本是条直线,外部环境输入电压又容易超过3.3V导致MCU烧坏。
所以利用内部DAC输出,用ADC测量DAC,还少了跟地线。
或者更巧妙一点,就只用一个IO,DAC输出的同时用ADC进行测量。
血的教训!
单片机ADC采集的基准电压与芯片供电电压都是3.3V,又没有分压电阻,
所以最大只能测量3.3V。
但是,我接入外部输入时,没控制好,怼了一个12V过去,正点原子380块的STM32H743 MCU直接烧了。
这里DAC的配置,采用的是DMA+DMA+定时器。
3.2.1 选择定时器触发
记得配置为DMA循环模式
3.2.2 配置定时器
还是定时器“更新事件”
3.2.3 生成波形数据
根据自己的测试需求,生成正弦波和方波数据。其本质就是一个uint16_t类型的数组,其中的数据按照一定的周期进行演进。
配置好之后,启动定时器并配置DMAbuf进行输出。
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, pData, Length, DAC_ALIGN_12B_R);
4、波形刷新方案
前提 {
屏幕尺寸:320*240
波形窗口:300*200
LCD API函数(初始化、清屏、画点、画线、显示字符串)
}
4.1 初始化流程
1)初始化DAC模块,启动DAC模块数据传输
2)初始化ADC模块,开始采集ADC数据
3)while(1)循环进行如下判断:死等ADC采集完成,完成之后先处理数据,处理之后,先启动下一次ADC传输,再在ADC传输没有结束之前刷新、刷完波形。
4.2 生成波形数据
在”ADC+DMA中断接收“,我们已经收到了真实电压数据集。
假如此时垂直扫描电压为5V(即Y轴最大电压为5V,最小电压为0V),那么y轴又只有200个像素点,每一个点的数值单元为5000/200=25,所以还要对数据集除以25得到对应的Y轴坐标点。
这里有几个问题:
1、能否直接除以25?
假如数据除以25之后,出现与真实值+1、-1的锯齿特性时,会导致波形有毛刺。
这时就需要设计优化算法,比如最简单的四舍五入。
2、X轴坐标点如何确定?
我目前的处理方式时,按照当前水平扫描时钟(时基挡位)扫描1次,比如当前时基为200us,那就200us扫描一次,扫描够300个坐标点结束,开始处理-刷新。
这里也有很多优化方式:比如扫描次数设置为300的倍数,时基缩小对应的倍数或不变,从中筛选若干个范围并取平均得到300个点。
不论如何,最终送给屏幕刷新的就是一个长度为300的数组,对应300个坐标值。这300个坐标值,就叫做波形数据。
4.3 波形刷新
比较简单的方法是,画线+缓存上一次波形数据并消隐;
画线:顾名思义,就是对300个坐标点,每两个点之间画线;
1、先对上一次的波形数据消隐,消隐即将线的颜色化成背景色(擦除上一帧波形);
2、再绘制当前波形数据的波形;
3、缓存当前波形数据。
//绘制波形
//1\擦除原有波形再绘制新波形
//2\输入数组长度为OSC_WIDTH:300
//X0\X1\Y0\Y1分别波形窗口的角落坐标值
void OscDrawWave(uint8_t WaveBuf[])
{
static uint16_t X = {0};
static uint8_t WaveBufPre[OSC_WIDTH] = {0};
if(WaveBufPre[0])
{
LCD_SetColor(List_c[OSC_M_WAVE].Back, List_c[OSC_M_WAVE].Back);
for(X = 0; X < X1 - X0 - 1; X++)
{
LCD_DrawLine(
X0 + X, Y1 - WaveBufPre[X],
X0 + X + 1, Y1 - WaveBufPre[X+1]
);
}
}
LCD_SetColor(List_c[OSC_M_WAVE].Point, List_c[OSC_M_WAVE].Back);
for(X = 0; X < X1 - X0 - 1; X++)
{
LCD_DrawLine(
X0 + X, Y1 - WaveBuf[X],
X0 + X+ 1, Y1 - WaveBuf[X+1]
);
}
for(X = 0; X < X1 - X0; X++){
WaveBufPre[X] = WaveBuf[X];
}
}
这样做的好处就是能减少闪烁频度。
进一步的,从显示原理上,还可以通过以下几种方法提高刷新速度:
方案1:消隐一条线,画当前线,而不是消隐完再画线
如上代码中,即使是做了消隐也会出现闪烁,改成擦除一条线再画一条线,感官上就能好很多。
void OscDrawWave(uint8_t WaveBuf[])
{
static uint16_t X = {0};
static uint8_t WaveBufPre[OSC_WIDTH] = {0};
for(X = 0; X < X1 - X0 - 1; X++)
{
LCD_SetColor(List_c[OSC_M_WAVE].Back, List_c[OSC_M_WAVE].Back);
LCD_DrawLine(
X0 + X, Y1 - WaveBufPre[X],
X0 + X + 1, Y1 - WaveBufPre[X+1]
);
LCD_SetColor(List_c[OSC_M_WAVE].Point, List_c[OSC_M_WAVE].Back);
LCD_DrawLine(
X0 + X, Y1 - WaveBuf[X],
X0 + X+ 1, Y1 - WaveBuf[X+1]
);
for(X = 0; X < X1 - X0; X++){
WaveBufPre[X] = WaveBuf[X];
}
}
方案2:特殊处理网格点
对于示波器要刷新的不知有波形,还有网格线,与波形参数。
波形参数一般要变动的是字符串类型的数值,优化空间不大(像素点大小固定)。
而且波形中,往往有大部分像素点是与网格点重合的。
所以对于网格点,就有三种状态,两种判断:
1、消隐时,网格点要绘画成网格颜色,不能是背景色;
2、绘制时,网格点要绘画成波形颜色,不能是网格颜色;
如果不小心将网格点搞成背景色,就会导致波形刷着刷着,网格没了……
方案3:计算像素点,整屏刷新
这种方案是想,计算所有需要变更状态的像素点,送到屏幕进行一次性刷新,或者退后一步逐个刷新像素点。前者“一次性刷新”可以想办法利用类似DMA2D的方式提高刷新速度,后者则可以减少像素点重复刷新次数,进而提高速度。
目前还没有做出来很好的实现方法,后面再研究并分享一下。
5、触发模式
以上讨论的都是,无触发模式的波形处理。
我们当然知道,示波器还有触发功能:自动触发、单次触发,其中触发方式最常用的就是边沿触发,我们假定为垂直扫描电压值的一半+上升沿作为触发条件。
这种条件的实现方式,想到的有一下几种:
1、硬件触发
由硬件决定将数据送进来,第一个信号就是满足条件的起始信号。(我们不用)
2、软件触发
扩大数据采样的样本数量
2.1、软件判断数据的第一个上升沿位置;
2.2、利用外部中断EXTI,选择上升沿触发,触发时记录样本位置;
找到触发位置时,将其后的300个样本值作为坐标点数据
如果没有找到触发位置,则取最后面的300个坐标点强制显示。
6、存储深度
以波形窗口宽度300为例,假设时基最小单位是200us,最大为200ms,相差1000倍,如果想在暂停时极限放大信号状态,就需要3000个坐标点,若还需要能够前后移动一般大小,则总共需要6000个样本点,即6K大小。
存储深度如何实现,略……