目录
出于产品需求,需要在产品中集成示波器功能,满足显示实时电压的需求。
这篇文章就总结和讨论一下示波器的方案设计(低成本)。
其中,实现方案主要是在3、4章节,1、2是概念部分和环境介绍,不需要的可以跳过。
如下是我设计时的主要参考资料。
总体而言,能够提供的硬件模块2个:MCU的ADC接口,LCD屏幕,别的就没了;
放大电路、滤波功能统统没有,demo板子直接用MCU的ADC接口采集外部电压,将处理后的数据送到屏幕去刷新。
同时为了模拟测量数据,还使用DAC+定时器来输出正弦波、方波、三角波等波形。
概念1:波形是指相应物理量在时间和空间上的分布情况的图形抽象。
概念2:正弦波是在时域中定义的,其它任何非正弦波形都可以看作是正弦波的叠加。

如下图所示。
基本观察参数:最大值、最小值和峰峰值(不是错别字,就是峰峰值,最差);
其它参数还有:顶端值、底端值和幅值,这三者都是在稳定值中计算出来的;
时域参数:上升时间,下降时间,正脉宽,负脉宽,这四者都是测量值,还有频率、占空比,是计算出来的值。
一般我们需要显示的,就是①最大值、最小值和峰峰值,②频率、占空比,③X、Y轴单位大小

采样率是指一秒内能采集到多少个样本值。
理论上说,采样率最大要超过被测信号最大频率的5倍以上,才能较为完整的采集到数据规律。(为什么?被测信号频率除以采样频率,其实就是在被测信号一个周期内能采集到的点数,若小于5个点,则很难具体描绘出数据规律)
STM32单片机的采样率,等于采样时钟除以采样周期,以STM32F4系列芯片为例,ADC最大时钟为36MHz,ADC最小采样周期为3+12个周期,故采样率最大为36/15=2.4MHz。
实际应用中:
1、根据主频时钟,ADC实际分频得到的时钟计算,比如同时使用USB接口时,主频最大168MHz,ADC最大时钟21MHz;
2、可以通过双路或三路ADC同时使用,提高ADC采样率;
带宽的定义:在频幅特性曲线中,当信号衰减至-3db(70.7%)时,此时的频点定义为示波器的带宽。
如何计算?我还没闹明白,留到后面仔细研究。
但是可以明白的是,带宽要大于被测信号的5倍以上(普遍认为的)才能不失真。
采样率低和带宽小对波形带来的的几种影响:


刷新率,即刷新一帧波形图像的速率。
假设刷新一张波形(大约2万个像素点)需要15ms,则刷新率就等于66帧每秒。
刷新率主要受限于MCU主频,以及数据处理效率,和屏幕刷新效率。
数据处理和屏幕刷新的提高有这么几种思路:
1、裸机运行
减少任务切换带来的时间消耗(微秒级)
2、利用缓存
以空间换时间:一种是将函数封装写入存储设备,提高调用效率;一种是开辟缓存(全局变量)存储数据,便于处理波形数据和LCD像素点数据。
3、优化算法
一是优化代码,二是提高编译器优化等级。
对于前期的开发工作,都可以先抛开,先上RTOS,再实现基本的波形实时输出功能,并先在功能上进行优化。优化完毕之后,再在刷新时间上进行尝试优化(可以逐步降级,但不能逐步升级)。
前提 {
芯片平台:STM32F429ZGT6
ADC:PA3
DAC:PA4
}
如下以PA3为例,①选择定时器3触发,②选择最小采样周期三,③选择DMA循环模式或Normal模式(半字大小)

如下,选择定时器触发事件为更新事件。

由此我们就用两种手段判断ADC数据转换是否结束:
1、读取ADC的DMAbuf的count值
注意ADC的DMA Count读取函数,读取到的数目,而不是实际长度;比如BUF长度为300,每个单元2byte大小,当DMA满时读取到的数值时300(而不是像串口中断那样,读到的是0)。
2、在DMA完成中断的回调函数中判断
触发完成中断时,肯定意味着1次转换结束,可以直接处理,也可以只关闭不处理(等待线程去处理)。
ADC数据的第一层处理,就是要根据自己的硬件电路设计,将ADC数值转换成真实电压值。
我这里是MCU直连,所以转换关系就是 vol = adc_value * 3300 / 4096 .
参考资料中,DS201中硬件电路使用了输入放大器和挡位切换电路,需要由硬件工程师计算电阻分压和放大关系,最终得到计算公式。
有了这一层数据,才方便后续进行算法消抖、归一化处理等等。
由于测量内部电压的波形基本是条直线,外部环境输入电压又容易超过3.3V导致MCU烧坏。
所以利用内部DAC输出,用ADC测量DAC,还少了跟地线。
或者更巧妙一点,就只用一个IO,DAC输出的同时用ADC进行测量。
血的教训!
单片机ADC采集的基准电压与芯片供电电压都是3.3V,又没有分压电阻,
所以最大只能测量3.3V。
但是,我接入外部输入时,没控制好,怼了一个12V过去,正点原子380块的STM32H743 MCU直接烧了。
这里DAC的配置,采用的是DMA+DMA+定时器。
记得配置为DMA循环模式

还是定时器“更新事件”

根据自己的测试需求,生成正弦波和方波数据。其本质就是一个uint16_t类型的数组,其中的数据按照一定的周期进行演进。
配置好之后,启动定时器并配置DMAbuf进行输出。
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, pData, Length, DAC_ALIGN_12B_R);
前提 {
屏幕尺寸:320*240
波形窗口:300*200
LCD API函数(初始化、清屏、画点、画线、显示字符串)
}
1)初始化DAC模块,启动DAC模块数据传输
2)初始化ADC模块,开始采集ADC数据
3)while(1)循环进行如下判断:死等ADC采集完成,完成之后先处理数据,处理之后,先启动下一次ADC传输,再在ADC传输没有结束之前刷新、刷完波形。

在”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个坐标值,就叫做波形数据。
比较简单的方法是,画线+缓存上一次波形数据并消隐;
画线:顾名思义,就是对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];
}
}
这样做的好处就是能减少闪烁频度。
进一步的,从显示原理上,还可以通过以下几种方法提高刷新速度:
如上代码中,即使是做了消隐也会出现闪烁,改成擦除一条线再画一条线,感官上就能好很多。
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];
}
}
对于示波器要刷新的不知有波形,还有网格线,与波形参数。
波形参数一般要变动的是字符串类型的数值,优化空间不大(像素点大小固定)。
而且波形中,往往有大部分像素点是与网格点重合的。
所以对于网格点,就有三种状态,两种判断:
1、消隐时,网格点要绘画成网格颜色,不能是背景色;
2、绘制时,网格点要绘画成波形颜色,不能是网格颜色;
如果不小心将网格点搞成背景色,就会导致波形刷着刷着,网格没了……
这种方案是想,计算所有需要变更状态的像素点,送到屏幕进行一次性刷新,或者退后一步逐个刷新像素点。前者“一次性刷新”可以想办法利用类似DMA2D的方式提高刷新速度,后者则可以减少像素点重复刷新次数,进而提高速度。
目前还没有做出来很好的实现方法,后面再研究并分享一下。
以上讨论的都是,无触发模式的波形处理。
我们当然知道,示波器还有触发功能:自动触发、单次触发,其中触发方式最常用的就是边沿触发,我们假定为垂直扫描电压值的一半+上升沿作为触发条件。
这种条件的实现方式,想到的有一下几种:
1、硬件触发
由硬件决定将数据送进来,第一个信号就是满足条件的起始信号。(我们不用)
2、软件触发
扩大数据采样的样本数量
2.1、软件判断数据的第一个上升沿位置;
2.2、利用外部中断EXTI,选择上升沿触发,触发时记录样本位置;
找到触发位置时,将其后的300个样本值作为坐标点数据
如果没有找到触发位置,则取最后面的300个坐标点强制显示。
以波形窗口宽度300为例,假设时基最小单位是200us,最大为200ms,相差1000倍,如果想在暂停时极限放大信号状态,就需要3000个坐标点,若还需要能够前后移动一般大小,则总共需要6000个样本点,即6K大小。
存储深度如何实现,略……
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
我在我的项目中有一个用户和一个管理员角色。我使用Devise创建了身份验证。在我的管理员角色中,我没有任何确认。在我的用户模型中,我有以下内容:devise:database_authenticatable,:confirmable,:recoverable,:rememberable,:trackable,:validatable,:timeoutable,:registerable#Setupaccessible(orprotected)attributesforyourmodelattr_accessible:email,:username,:prename,:surname,:
我正在尝试创建密码规则来设计可恢复的密码更改。我通过passwords_controller.rb做了一个父类(superclass),但我需要在应用规则之前检查用户角色,但我所拥有的只是reset_password_token。 最佳答案 假设您的模型是用户:User.with_reset_password_token(your_token_here)Source 关于ruby-on-rails-设计通过reset_password_token获取用户,我们在StackOverflow
我已经使用Apartment设置了一个Rails5应用程序(1.2.0)和Devise(4.2.0)。由于某些DDNS问题,应用只能在app.myapp.com下访问(请注意子域app)。myapp.com重定向到app.myapp.com。我的用例是每个注册该应用的用户(租户)都应该通过他们的子域(例如tenant.myapp.com)访问他们的特定数据。用户不应限定在其子域内。基本上应该可以从任何子域登录。重定向到租户的正确子域由ApplicationController处理。根据Devise标准,登录页面位于app.myapp.com/users/sign_in。这就是问题开始的
我在关注RyanbatesRailsCast的devise和omniauth(第235集-devise-and-omniauth-revised)。当我尝试使用Twitter登录时,标题中不断出现错误。defself.new_with_session(params,session)ifsession["devise.user_attributes"]new(session["devise.user_attributes"],without_protection:true)do|user|user.attributes=paramsuser.valid?end完整跟踪:C:/Ruby20
LL库和HAL库简介LL:Low-Layer,底层库HAL:HardwareAbstractionLayer,硬件抽象层库LL库和hal库对比,很精简,这实际上是一个精简的库。LL库的配置选择如下:在STM32CUBEMX中,点击菜单的“ProjectManager”–>“AdvancedSettings”,在下面的界面中选择“AdvancedSettings”,然后在每个模块后面选择使用的库总结:1、如果使用的MCU是小容量的,那么STM32CubeLL将是最佳选择;2、如果结合可移植性和优化,使用STM32CubeHAL并使用特定的优化实现替换一些调用,可保持最大的可移植性。另外HAL和L