一、简易数字示波器原理
数字示波器基本原理可以简单理解为:数据采集+ 图形显示,该过程循环进行,如图1 所示。
图1 简易数字示波器流程图
LCD 图形显示需要根据LCD 特性设计,不同LCD驱动程序不同,本篇将结合不带字库的LCD12864 设计显示程序。
二、图形液晶LCD12864绘图驱动设计基础
关于LCD 的硬件接口电路,在其他教程中有详细介绍,涉及单片机总线知识和CPLD 内部电路,需要认真学习,这里借助现成的驱动函数,重点讲解LCD绘图程序设计。
LCD12864 的电路接口在头文件中定义:
#define LCD_LCW XBYTE[0xf4ea]
// 左屏命令写入
#define LCD_LDW XBYTE[0xf5ea]
// 左屏数据写入
#define LCD_LCR XBYTE[0xf6ea]
// 左屏命令读出
#define LCD_LDR XBYTE[0xf7ea]
// 左屏数据读出
#define LCD_RCW XBYTE[0xf8ea]
// 右屏命令写入
#define LCD_RDW XBYTE[0xf9ea]
// 右屏数据写入
#define LCD_RCR XBYTE[0xfaea]
// 右屏命令读出
#define LCD_RDR XBYTE[0xfbea]
// 右屏数据读出
后面所有对LCD 的编程操作都是基于以上接口定义进行的各种读写操作。
首先来看LCD12864 的点阵结构图,如图2 所示。
图2 LCD点阵分布结构图
此LCD 屏由水平128 列,垂直64 行组成。水平128 列分左右各64 列两个半屏构成。垂直64 行又分8 页,每页8 行(1 列8 点刚好1 字节)。程序每次对LCD 的绘图操作就是以最小单位1 字节进行操作的。
理解这点至关重要。也就是每次只能针对8 点进行操作,而不是1 点进行操作。左右屏由单独地址线控制(前面的接口定义就是分左右屏定义的)。实际打点只需往指定“位置”写入数据,“1”亮,“0”暗。
LCD 驱动忙检测函数void loop_lcd12864_is_busy(unsignedcharright)。
void loop_lcd12864_is_busy(unsigned char right)
{
unsigned char tmp,counter=0;
do {
if(right) tmp = LCD_RCR;
else tmp = LCD_LCR;
if(counter++>50) break; // 超时跳出
}
while ((tmp|0x7f)==0xff); //bit7 为1 则表示LCD 内部执行命令,处于“忙”状态
}
对LCD 进行读写操作时,需要进行“忙”检测,LCD 内部也是由控制器来完成一系列刷屏操作的,执行各种操作都是需要一定的时间,也就是说不是任何时候外部控制器都可以对LCD 发操作指令的,只有LCD为空闲状态时才可以操作,忙检测就是循环读取LCD状态标志位,判断是否空闲,关于命令的细节请参考数据手册。
命令写入函数void lcd_cmd_wr(unsigned char cmd,right)。
void lcd_cmd_wr(unsigned char cmd, right)
{
loop_lcd12864_is_busy(right); // 忙检测
if(right) LCD_RCW = cmd; // 右屏命令写入
else LCD_LCW = cmd; // 左屏命令写入
}
数据写入函数void lcd_dat_wr(unsigned char data,right)。
void lcd_dat_wr(unsigned char data,right)
{
loop_lcd12864_is_busy(right);
if(right) LCD_RDW = data;
else LCD_LDW = data;
}
lcd_cmd_wr() 和lcd_dat_wr() 两个函数分别是给LCD 写命令和写数据函数,通过写命令函数设定地址。每个函数都分左右屏,“right”参数选择,“0”选左屏,“非0”选右屏。
读数据函数unsigned char lcd_dat_rd(unsigned char right)。
unsigned char lcd_dat_rd(unsigned char right)
{
loop_lcd12864_is_busy(right);
if(right) return(LCD_RDR);
else retuen(LCD_LDR);
}
该函数可以读出LCD 当前显示的数据,首次操作需要读2次才有效。
LCD 清屏函数void lcd12864_clr(void)。
void lcd12864_clr(void)
{
unsigned char i,j;
for(i=0;i<8;i++) { // 从0 到7 共8 页
lcd_cmd_wr(ORGX,0); // 分页设定左屏0 点地址
lcd_cmd_wr(ORGY+i,0);
lcd_cmd_wr(ORGX,1); // 分页设定右屏0 点地址
lcd_cmd_wr(ORGY+i,1);
for(j=0;j<64;j++) {
lcd_data_wr(0,0);
lcd_data_wr(0,1);
}
}
}
该函数对LCD 所有点阵写0,完成一次清屏操作。这里的ORGY,PRGX 是设定光标的命令,光标指向(0,0)字节,是一个固定值。实际在执行数据写入的时,x 坐标范围从0 到63,在连续写入过程中能够实现自动加1,y 轴页地址范围从0 到7,需要逐页设定。
LCD 初始化函数void lcd12864_init(void)。
void lcd12864_init(void)
{
lcd_cmd_wr(DISPON,0); // 显示开启
lcd_cmd_wr(DISPFIRST,0); // 设定显示首行地址,修改首行地址可以实现屏幕滚动显示效果
lcd_cmd_wr(ORGY,0); // 设定初始光标
lcd_cmd_wr(ORGX,0);
lcd_cmd_wr(DISPON,1); // 初始另外一半
lcd_cmd_wr(DISPFIRST,1);
lcd_cmd_wr(ORGY,1);
lcd_cmd_wr(ORG,1);
lcd12864_clr(); // 执行清屏,非必须操作
}
该函数用来初始化LCD,设置显示模式,光标位置等,在对LCD 绘图时,最多的命令就是设定当前光标位置,通过光标位置来指定将要操作的LCD 显示点。
在对LCD 编程操作以前,一定要执行此函数对LCD 进行初始化操作。
从驱动函数可见,一次对LCD 写入数据是以字节为单位,通过写命令设定坐标,y 坐标从0 页到7 页,x 坐标从0 列到63 列,分左右屏,左上角为坐标(0,0)点,这和我们习惯的左下角为(0,0)坐标轴是不一样的。
因为每次操作LCD 是一个字节为单位,对应8 点,如果我们希望以任意点为坐标显示,还得另外寻找别的办法编程实现真正“点”显示。
如图3 所示,在屏幕上指定位置画点,水平轴就是x,与LCD 坐标一致,垂直轴需要将点坐标变成字节为单位的坐标,我们先按习惯将y 轴64 点从下至上编号0 到63,其中0 到7 点为字节0,8 到15 点为字节1,依此类推对应8 字节。
图3 LCD“点”显示示例
第一点y 轴为30,应该对应垂直哪个字节的哪个比特呢?
实际30 点应该在第4 字节(24 到31)的Bit 6 上,拿30/8 取整为3,刚好是应该跳过的前3 字节(对应0 到23),那么30%8(30 除8 取余数)呢,余数是6,不是刚好是Bit 位吗?所以可以这样将y 值映射到某字节的某点上,如果y 轴64 点对应8 字节变量Da[n],n从0 到7,则:
da[y/8] = 1 《 (y%8);或da[y》3]=0x01《(y&0x07);后一种算法更优。
通过总结规律,用以上算法可以将任意0 到63 之间的数据作为坐标描点到对应的8 个字节中,然后将8个字节全部写入LCD,则通过刚才算法就会有一点与所给坐标一致。
第一点:da[30/8] = 1 《 30%8; 即da[3] = 0x40;
第二点:da[10/8] = 1 《10%8; 即da[1] = 0x04;
首先给出列显示子函数,在任意列显示y 值对应点。
{
unsigned char j;
if(x<64) { // 根据列坐标选择左右半屏
for(j=0;j<8;j++) { // 写左半屏
lcd_cmd_wr(ORGY+j,0);
lcd_cmd_wr(ORGX+x,0);
lcd_data_wr(da[j],0);
}
}
else {
x-=64;// 坐标调整
for(j=0;j<8;j++) { // 写右半屏
lcd_cmd_wr(ORGY+j,1);
lcd_cmd_wr(ORGX+x,1);
lcd_data_wr(da[j],1);
}
}
}
有了列显示函数,LCD 任何坐标位置上描点绘图函数为:
void lcd_disp(unsigned char x,y) //x 水平坐标,y 垂直坐标
{
unsigned char dat[8];
unsigned char j;
y=63-y;// 使xy 坐标符合习惯
for(j=0;j<8;j++) dat[j] = 0x0;
dat[y/8] |= 0x01《(y&0x07);
lcd_row_wr(x,dat);
}
以上函数能够在指定坐标(x,y)上描点,下面尝试将ADC 的值采集后送LCD 显示。再按时间轴x 轴顺序将不同时刻采集到的y 值顺序写入LCD,这是我们就可以在LCD 上看到随时间变化的电压曲线了。主程序为:
void main()
{
unsigned char i;
lcd12864_init();
for(i=0;i<128;i++) {
lcd_disp(i,read_adc(0)/16);// 从10bit 映射到6bit,要除16
}
while(1);
}
调整输入给ADC 的信号频率,可以得到满意的波形图了,效果如图4 所示。
图4 LCD实时显示ADC图
如果你成功做到了这一步,可喜可贺,已经掌握了绘图基础了,不过程序还要继续完善。
三、图形液晶LCD12864绘图驱动设计提高
如何在现实波形显示的基础上,同时将定标网格也显示出来呢?
首先我们看一种C 语法“A = 0x05; A |= 0x50;”运行以上指令后,A = 0x55 ;也可以说第二个数据0x50 是叠加到第一个数据上的,我们可以用这种算法把需要显示的亮点(也就是“1”)按一定的算法叠加在一起,送LCD 显示,就出现了我们希望的在波形上增加背景网格的效果。
因为网格与水平x 轴是严格关联的,所以我们可以对x 轴数据进行判断,有规律的将边框和背景格点加入。
改进带背景格的lcd_disp() 函数。
void lcd_disp(unsigned char x,unsigned char y)
{
unsigned char da[8];
unsigned char j;
y = 63-y;
for(j=1;j<7;j++) da[j] = 0x0;
da[0]=0x01;
da[7]=0x80;
if(x%5==0) {
da[21/8] |= 0x01《(21%8);
da[42/8] |= 0x01《(42%8);
}
if((x==0)||(x==127)) {// 加两端边框
for(j=0;j<8;j++) da[j] = 0xff;
}
da[y/8] |= 0x01《(y%8);
lcd_row_wr(x,da);
}
效果如图5 所示。
图5 带网格的LCD显示图
四、图形液晶LCD12864移动游标线绘图驱动设计
声明控制水平线的变量“unsigned charpointY=0;” 范围0 到63, 声明控制垂直线的变量“unsigned char pointX=0;”范围0 到127。
void lcd_disp(unsigned char x,unsigned char y)
{
unsigned char da[8];
unsigned char j;
y = 63-y;
for(j=1;j<7;j++) da[j] = 0x0;
{// 绘制边框
da[0]=0x01;
da[7]=0x80;
if((x==0)||(x==127)) {
for(j=0;j<8;j++) da[j] = 0xff;
}
}
if(x%5==0) da[pointY>>3] |= 0x01 《 (pointY&0x07);
// 绘制由变量pointY 控制的水平游标线
if(x==pointX) // 绘制由变量pointX 控制的垂直游标线
for(j=0;j<64;j++)
if(j%5==0) da[j>>3] |= 0x01 《(j&0x07);
da[y/8] |= 0x01《(y%8); // 绘制信号波形
lcd_row_wr(x,da);
}
运行效果如图6 所示。
图6 水平垂直移动游标线示例
五、图形液晶LCD12864数字符号显示
图形点阵LCD 显示数字,原理是把数字以点阵的形式取模,再把点阵模写入特定的LCD 空间即可,首先来看数字取模,如图7 所示,对数字“0”按8×5点取模。
图7 数字取模示例图
纵向看,8 点一列,从上至下对应bit0 到bit7,我们用1 表示“亮”,0 表示“暗”,从左至右,依次确定为0111 1100,即0x7c ;1000 0010,即0x82 ;10000010, 即0x82 ;1000 0010, 即0x82 ;0111 1100,即0x7c ;如果我们依次将这5 个字节写入LCD 某页连续5个地址空间,LCD 上就会显示“0”。
下面我们把数字变量在LCD 上动态显示,就是数值变了,显示跟着变。
字符显示LCD 驱动函数,实现8×n 点阵字符写入函数。
void lcd_put_xyns(unsigned char x,y,n,unsigned char *s)
{
unsigned char i;
for(i=0;i<n;i++) {
if((x+i)>63) {
lcd_cmd_wr(ORGY+y,1);
lcd_cmd_wr(ORGX+x+i-64,1);
lcd_dat_wr(s[i],1);
}
else {
lcd_cmd_wr(ORGY+y,0);
lcd_cmd_wr(ORGX+x+i,0);
lcd_dat_wr(s[i],0);
}
}
}
参数:“x, y”是坐标,这里y 是页坐标,取值从0 到7,“n”是点阵模字节数,“*s”是点阵模起始地址。
将字模生成字模表:
unsigned char codenumber[]={
0x7C,0x82,0x82,0x7C,0x84,0xFE,0x80,0x00,0xCC,0xA2,0x
92,0x8C,0x44,0x92,0x92,0x6C,
0x38,0x24,0xFE,0x20,0x9E,0x92,0x92,0x62,0x7C,0x92,0x9
2,0x64,0x06,0xF2,0x0E,0x02,
0x6C,0x92,0x92,0x6C,0x4C,0x92,0x92,0x7C,0x80,
};
lcd_put_xyns(0,0,4,number+0*4); // 直接显示字符0
lcd_put_xyns(4,0,4,number+1*4); // 直接显示字符1
……
tmp=3421; // 以下代码显示变量tmp
lcd_put_xyns(80,2,4,number+(tmp/1000)*4);
lcd_put_xyns(84,2,4,number+(tmp/100%10)*4);
lcd_put_xyns(88,2,4,number+(tmp/10%10)*4);
lcd_put_xyns(92,2,4,number+(tmp%10)*4);
运行效果如图8 所示。
图8 LCD显示数字图
基于以上原理,我们还可以将其他字符取模显示。
最终完成的效果如图9 所示。
图9 完整示波器效果图
六、数字示波器几个参数
1. 数据采样率与时间轴定标
数据采样率也就是每秒钟连续采集到的数据个数,或者说两个有效数据之间的时间间隔。为了在LCD 上还原波形图,时间轴与采样率之间要有严格的换算关系。
为了还原波形图,采样率与被测量信号频率之间有一定的关联,不同的采样率,对应不同频段的信号,高采样率适合高频信号,低采样率适合变化缓慢的低频信号。
从显示效果来看,为了在屏幕上再现信号的频率特征,根据Nyquist 频率特征定理,一般要保证信号的每个周期内至少有2 点,用LCD 再现,2 点效果已经很差了。最高采样率一般由ADC 器件决定,这里用的TLC1549最快只能做到88μs 采集一点,按2 点每周期计算的话,可以做到对5.6K(1000000/88/2) 信号采样,实际效果已经很差了,1K 以内效果最好。
实际在设计采样率档位时,最快档按100μs 每点,屏幕每5 点(500μs)一格,第一档就是0.5ms/p,这一档位需要连续采样,用少量延时控制采样率为10k,第二档就是1ms/p,第三档2ms/p,第四档5ms/p,第五档10ms/p,后面四档分别用定时器控制完成。
2. 电压轴分档定标
由于这版没做模拟电路,没有信号电调理电路,所谓电压档定标是纯软件算法实现,屏幕同样取5 点一格,共10 格,当输入信号从0 到5V 变化时,LCD 刚好满屏显示,每格0.5V,另外还设计了0.1v/p,0.2v/p,1v/p,2v/p,5v/p。共6 档。
3. 交流直流切换
功能完善的示波器应该从硬件上实现交直流切换,本版先从软件上实现交流直流显示,设计思想是根据采样得到的数据的最大值和最小值确定交流信号幅度和中值,把直流部分减掉,显示只显示交流部分,这就是交流档,直流档就是不做去直流处理。
4. 运行停止切换
该功能实现起来很容易,所谓停止,就是停止新的数据采集,重复显示同一帧数据,显示的效果就是波形稳定无抖动,便于对信号电压和频率进行测量。
5. 电压测量
根据当前电压档位(每格表示多少伏),计算出两测量线之间的电压值,在LCD 上显示。
6. 频率测量
根据当前时间轴档位(每格表示多长时间),计算出两测量线之间的时间,假定两测量线之间刚好是一个周期,转换成频率值在LCD 上显示。
7. 波形平移
用变量控制波形在LCD 上显示的相对位置,实现波形上下左右平移。
8. 帧同步
我们使用的数字示波器,对有规律的周期信号能够稳定显示,要想实现稳定显示,需要在起始点显示信号的不同周期的同一点,如何做到这点呢?
如图10 所示,我们对比两帧数据,波形起点不在同一点,我们没办法保证每次数据采样刚好在同一点(硬件触发的除外),我需要按一定的规律,在LCD 上绘图时,总是从同一点开始,例如过零点,这里我们首先用统计的办法找出信号的最大值和最小值,计算出信号中交流部分的中点,也就是过零点,然后逐点比较,搜寻过零点。
图10 不同帧数据起点不同
程序设计思想是逐点比较过滤,我们取中值,如图7-1 中第5 点所示,信号从小于中值到大于中值的上升沿为起点,不同帧信号可能从1、2、3、4 任何一点开始,这就要用程序判断把设定的起点5 以前的数据丢掉。
程序是这样的:
while(da_buffer[i] > dam) if(++i > (DATA_SIZE/2)) break;
while(da_buffer[i] <= dam) if(++i > (DATA_SIZE/2)) break;
while(da_buffer[i] > dam) if(++i > (DATA_SIZE/2)) break;
这里的dam 就是信号交流部分的中值。
程序用几个while 语句,看上去很呆板,实际运行效果很好喔。第一句把比设定值大的数据过滤,如图10 中的1、2、3 点,如果数据是从4 开始的,第一句会直接跳过;第二句把比设定值小的数据跳过,找出过零点的上升沿5 点。
9. 矢量绘图
前面我们介绍的LCD 描点绘图不是矢量绘图,图形由一系列的“点”组成,在波形绘图区,每列只有1个点,拿方波绘图来看,描点绘图图形如图12 所示,矢量绘图在点与点之间填充“直线”,方波绘图效果如图11 所示。对比两种绘图效果,最好选择矢量绘图,LCD 波形显示效果较点绘图好很多。
图11 点绘图
图12 矢量绘图
矢量绘图需要按时间轴连续两点同时考虑,根据两点之间的差值补点,这就是绘图程序disp(x, y, l)中参数l 的作用。
七、数字示波器程序流程图设计分析
表达程序设计思想的关键就是程序流程图,下面将重点分析本程序设计的几个关键流程图。
主程序流程图如图13 所示,初始化完成系统初始化设置,包括全局变量初始化,红外按键中断、定时器中断初始化,在主循环程序中,处理外部按键输入,由于数据采集是在定时中断中完成的,主循环中需要等待一帧数据采集完成后,才能对数据进行同步处理,包括直流滤波,数据到LCD 映射,调用LCD 显示函数完成波形绘图,一帧数据处理完成后,重新开中断,等待下一帧数据采集。
图13 主程序流程图图
LCD 波形显示流程图如图14 所示,LCD 波形显示区需要根据新采集到的数据不断刷新,这里以LCD列为单位,一次绘图一个(x, y)坐标点,这里的编程思想是定义7 字节内存变量,与LCD 的第x 列对应,首先对参数进行检查,例如y 参数就不能超过LCD 显示去最大值,超过需要修正,确保点到点映射不出错,需要显示的内容除了波形信号外,还有窗口边框,背景格,游标线等,都要同时考虑。这里的l 参数是两点之间的间隔,采用矢量显示,两点之间用直线矢量绘图代替描点,显示效果要好很多。
图14 LCD波形显示
数据采集流程图如图15 所示,数据采集需要严格等时,而且根据不同频率的信号,从低频到高频需要用不同的采样率,所以这里用定时器控制采样时间间隔。
图15 数据采集流程图
由于定时器中断任务调用需要额外时间,对于高速中断是有一定时间间隔限制的,通过计算,100μs 周期采样用定时器实现不了,所以这里定时数据采样分高速和低速两档,高速档一次一帧,低速档一次一点,一帧数据采集完成后,回到主程序对数据进行处理。
按键任务流程图如图16 所示,由于示波器控制需要很多按键,涉及电压档位,采样时间档位,游标线等,这些档位都由一个全局变量控制,在按键任务模块,就是根据有效按键,对这些档位全局变量进行加减调整,变量调整后,有些效果会在波形显示刷新中体现出来,还有的需要单独刷新显示。
图16 按键任务流程图
八、经验总结与心得体会
STC89C52RC内部扩展RAM 只有256 字节,不像数据手册上介绍的512 字节,外部RAM 超量使用会导致意想不到的后果,经常莫名其妙的死机。
窗口波形显示有两种模式,逐点显示(采集一个数据马上刷新显示)和逐帧显示(采集完成一帧数据后刷新显示),经过测试论证,低频逐点视觉效果好,高频没区别,但是逐点没办法同步。
由于版面限制,这里发表的只是一个压缩版,详细资料见“大学生电子实验室论坛” http://www.ceet.hbnu.edu.cn/bbs/。
初学者在学习他人程序时,不要仅仅对程序代码下功夫看懂,其实真正学习的捷径是看懂程序流程图,学习他人的程序设计思想,在吸收他人程序设计思想的前提下,具体编程实现特定的功能,程序可以有多种“写法”。
作为终端输出显示装置,并不需要很高的刷新速度,因为它是给“人”看的,每秒超过10 次的变化,对人眼睛产生不了有用的效果。
我写这篇教程,特别注重解决问题方法的总结,我认为掌握一种解决问题的方法比掌握一种具体的知识要重要得多。
参考文献:
[1].CPLD datasheethttp://www.dzsc.com/datasheet/CPLD+_1136600.html.
[2].TLC1549 datasheethttp://www.dzsc.com/datasheet/TLC1549+_1095397.html.
[3].STC89C52RC datasheethttp://www.dzsc.com/datasheet/STC89C52RC+_1054862.html.