数字示波器完整设计过程.docx
《数字示波器完整设计过程.docx》由会员分享,可在线阅读,更多相关《数字示波器完整设计过程.docx(51页珍藏版)》请在冰点文库上搜索。
![数字示波器完整设计过程.docx](https://file1.bingdoc.com/fileroot1/2023-5/9/550cee2b-427e-4a54-993d-a905550773be/550cee2b-427e-4a54-993d-a905550773be1.gif)
数字示波器完整设计过程
基于Mini51板的数字示波器设计
基于Mini51板硬件资源,构思数字示波器的方案已经思考很久了,总是没有集中的时间,一个稍微复杂的设计完成创作需要集中的时间才能完成,这次利用学期结束的一段集中时间,完成了基于LCD12864显示的数字示波器程序设计,现在将文档写出来供大家交流学习用。
在此声明,这个教程是写给初学者看的,我会从简单到复杂一步一步详细介绍设计过程,甚至是调试的过程,还包括一些经验总结,特别是提供了完整的keil工程附件。
希望读者立足示波器项目,学到更多关于软硬件开发的一些经验技巧。
1简易数字示波器原理
数字示波器基本原理可以简单理解为:
数据采集+图形显示,该过程循环进行,如图1-1所示。
首先是数据采集,这一版我们直接用Mini51板上的ADC“TLC1549”。
(如果你没有ADC,也可能没有信号发生器,后面会介绍一种正弦表调试方法。
)
TLC1549驱动函数unsignedintread_adc(void)。
图1-1简易数字示波器流程图
unsignedintread_adc(void)
{
unsignedchari;
unsignedinttemp=0;
ADC_CS=0;//开启控制电路,使能DA和CKIO引脚
for(i=0;i<10;i++){//采集10次,即10bit
ADC_CK=0;
temp<<=1;
if(ADC_DA)temp++;
ADC_CK=1;
}
ADC_CS=1;
return(temp);
}
注:
带背景色的源码都是直接从演示程序中拷贝的。
以上是是驱动TLC1549的函数,如果你还想彻底弄清TLC1549的各种参数,请参考数据手册TLC1549.pdf,使用该函数需要注意的是,两次调用该函数之间的间隔要超过21us,AD转换是要一段时间的,在高速系统中时间控制尤其关键。
Mini51板单片机在22.1184M晶振时钟频率下运行,连续两次AD采集数据并将数据写入外部扩展RAM变量缓冲区,之间的时间间隔实测略小于21us的,需要适当延时。
这在高速档数据采集时增加了一定延时等待就是这个原因。
图形显示有很多种,LCD显示稍难,ADC得到的结果如何在LCD上描点,这确实是一个难点,涉及LCD驱动问题,需要花费很大篇幅才能完成。
最初调试我们可以选用串口来做,借助他人现成的工具软件。
下面介绍基于串口和上位机工具软件的波形显示程序设计。
串口初始化函数rs232_port_init(void)。
voidrs232_port_init(void)//串口初始化
{
SCON=0x50;//串口工作在方式1,异步模式
PCON=0x80;//波特率翻倍
TMOD=0x20;//定时器1工作在方式2
TH1=0xff;//波特率115200bps,单片机时钟晶振为22.1184MHz
TL1=0xff;
TR1=1;//开启时钟
RI=0;//清空接受标志位
TI=0;//清空发送标志位
}
往串口写1字节函数voiduart_put_uchar(unsignedcharc)。
voiduart_put_uchar(unsignedcharc)//往串口写1字节无符号数据
{
SBUF=c;
while(!
TI);
TI=0;
}
从串口读1字节函数unsignedcharuart_get_uchar()。
unsignedcharuart_get_uchar()//从串口读1字节无符号数据
{
while(!
RI);
RI=0;
returnSBUF;
}
以上这几个函数是学单片机的人一定要掌握的,能够随手拿来就用,通过串口调试程序,很方便。
有了以上4个函数,再建一个keil工程,添加一个主函数,就可以演示了。
#include"mini51b.h"//所有与硬件相关的接口函数定义
#include"uart.h"
voidmain()
{
rs232_port_init();//串口初始化
read_adc();//首个ADC数据丢失
while
(1){
if(uart_get_uchar()==0x55)uart_put_uchar(read_adc()/4);//10bit/4变8bit
}
}
在主程序循环中,接收到上位机下发的数据0x55后,读取ADC数据并发送一次,在串口调试助手(例如SSCOM)里,设置相关端口和波特率后,发送0x55,注意HEX(十六进制格式)选项,就可以看到ADC的结果,如图1-2所示。
图1-2串口调试ADC
调试ADC还有一种更方便的方法,结果直接在Mini51板上的数码管上显示出来,不管你对数码管硬件熟悉不熟悉,只要使用模板程序提供的数码管驱动函数led_disp(uintnumber)即可。
Mini51板数码管驱动函数led_disp(unsignedint)。
voidled_disp(uintnumber)//Mini51板数码管显示函数,传入整数0~9999
{
unsignedcharcodetab1[20]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,};
unsignedchartemp,flag=0;
if(number<10000){
temp=number/1000%10;//千位数码管
if(temp){
SEG_Q=tab1[temp];
flag=1;
}
else{
SEG_Q=0xff;//数码管熄灭
flag=0;
}
temp=number/100%10;//百位数码管
if(flag|temp){
SEG_B=tab1[temp];
flag=1;
}
else{
SEG_B=0xff;//数码管熄灭
flag=0;
}
temp=number/10%10;//十位数码管
if(flag|temp)SEG_S=tab1[temp];
elseSEG_S=0xff;//数码管熄灭
temp=number%10;//个位数码管
SEG_G=tab1[temp];
}
else{
SEG_Q=0xbf;//"-"
SEG_B=0xbf;
SEG_S=0xbf;
SEG_G=0xbf;
}
}
以上演示源程序keil工程请参考附件【串口调试1】
这里我再介绍两款串口绘图软件【MyOsc】和【ComCalWave】,可以直接把串口接收到的数据按X-Y轴绘图,显示结果更直观。
主程序这样改:
#include"mini51b.n"//所有与硬件相关的接口函数定义
#include"uart.h"
voidmain()
{
rs232_port_init();
read_adc();
delay_ms
(1);
while
(1){
uart_put_uchar(read_adc()/4);
//seg7_disp(read_adc());
delay_ms
(1);//这里的延时起到调节采样率的作用
}
}
运行【MyOsc】,设置串口和波特率后“OPEN”,适当调节输入信号频率后可以看到如图1-3所示的图案。
图1-3串口绘图软件示例
运行【ComCalWave】,选择正确的串口号和波特率,“OpenCOM”,再设置“WaveShow”,看到了什么?
图形!
在图形窗口尝试用鼠标右键操作,还可选择特定范围显示,如图1-4所示。
图1-4串口绘图软件示例2
要看到以上漂亮的波形,还有一些硬件连接要做,需要将信号发生器和Mini51板上ADCin接口连接,注意,TLC1549只能进行0到5V之间的信号转换,你还需要调整信号发生器,产生满足条件的信号才行。
以上演示源程序keil工程请参考附件【串口调试2】
实际在调试程序中,缺少必要硬件设备的,还可以用正弦表代替实际ADC,这里再介绍一款正弦表生成器软件【正弦表发生器】,软件界面如图1-5所示。
图1-5正弦表发生器
【量化阶数】就是ADC位数,例如tlc1549是10阶,ADC0809是8阶;
【采样点数】就是在一个正弦周期内,均匀分布多少个采样点,例如在128点的lcd上显示2个以上周期的话,采样点数要小于64点,这里选用30点数来举例,源程序如下。
#include"mini51b.h"//所有与硬件相关的接口函数定义
#include"uart.h"
unsignedcharcodedot[30]={//正弦表,注意数据类型是“code”,存放在rom当中
0x80,0x9a,0xb4,0xcb,0xdf,0xee,0xf9,0xff,0xff,0xf9,
0xee,0xdf,0xcb,0xb4,0x9a,0x80,0x65,0x4c,0x34,0x21,
0x11,0x6,0x0,0x0,0x6,0x10,0x20,0x34,0x4b,0x65,
};
voidmain()
{
unsignedchari;
rs232_port_init();
delay_ms
(1);
while
(1){
for(i=0;i<128;i++){
uart_put_uchar(dot[i%30]);
delay_ms
(1);//此处延时当于调节了采样率
}
}
}
用以上调试软件同样可以看到漂亮的正弦信号图形。
以上调试成功后,是不是感觉很棒,如果你是第一次亲自完成ADC将数据采集,再用软件绘图显示还原信号波形图,一定是一件特别令人激动的事情。
2图形液晶LCD12864绘图驱动设计基础
下面我们学习如何在LCD12864上显示同样的正弦波形。
关于LCD的硬件接口电路,在前面的教程中有详细介绍,涉及单片机总线知识和CPLD内部电路,需要专门学习,这里我们借助现成的驱动函数,重点讲解LCD绘图程序设计。
LCD12864的电路接口在【mini51b.h】头文件中定义。
#defineLCD_LCWXBYTE[0xf4ea]//左屏命令写入
#defineLCD_LDWXBYTE[0xf5ea]//左屏数据写入
#defineLCD_LCRXBYTE[0xf6ea]//左屏命令读出
#defineLCD_LDRXBYTE[0xf7ea]//左屏数据读出
#defineLCD_RCWXBYTE[0xf8ea]//右屏命令写入
#defineLCD_RDWXBYTE[0xf9ea]//右屏数据写入
#defineLCD_RCRXBYTE[0xfaea]//右屏命令读出
#defineLCD_RDRXBYTE[0xfbea]//右屏数据读出
后面所有对LCD的编程操作都是基于以上接口定义(总线编址)进行的读写操作。
首先来看LCD点阵结构图,这里以不带字库的LCD12864来讲解,如图2-1所示。
图2-1LCD点阵分布结构图
此LCD屏由水平128列,垂直64行组成。
水平128列分左右各64列两个半屏构成。
垂直64行又分8页,每页8行(1列8点刚好1字节)。
程序每次对LCD的绘图操作就是以最小单位1字节进行操作的。
理解这点至关重要。
也就是每次只能针对8点进行操作,而不是1点进行操作。
左右屏由单独地址线控制(前面的接口定义就是分左右屏定义的)。
实际打点只需往指定“位置”写入数据,“1”亮,“0”暗。
LCD驱动函数:
忙检测函数voidloop_lcd12864_is_busy(unsignedcharright)。
voidloop_lcd12864_is_busy(unsignedcharright)
{
unsignedchartmp,counter=0;
do{
if(right)tmp=LCD_RCR;
elsetmp=LCD_LCR;
if(counter++>50)break;//超时跳出
}
while((tmp|0x7f)==0xff);//bit7为1则表示LCD内部执行命令,处于“忙”状态
}
对LCD进行读写操作时,需要进行“忙”检测,LCD内部也是由控制器来完成一些列LCD屏显示操作的,执行各种操作都是需要一定的时间的,也就是说不是任何时候外部控制器都可以操作LCD的,只有LCD为空闲状态时才可以操作,忙检测就是循环读取LCD状态标志位,判断是否空闲,关于命令的细节请参考数据手册。
命令写入函数voidlcd_cmd_wr(unsignedcharcmd,right)。
voidlcd_cmd_wr(unsignedcharcmd,right)
{
loop_lcd12864_is_busy(right);//忙检测
if(right)LCD_RCW=cmd;//右屏命令写入
elseLCD_LCW=cmd;//左屏命令写入
}
数据写入函数voidlcd_dat_wr(unsignedchardata,right)。
voidlcd_dat_wr(unsignedchardata,right)
{
loop_lcd12864_is_busy(right);
if(right)LCD_RDW=data;
elseLCD_LDW=data;
}
lcd_cmd_wr()和lcd_dat_wr()两个函数分别是给LCD写命令和写数据函数,通过写命令函数设定地址。
每个函数都分左右屏,用0,非0选择。
读数据函数unsignedcharlcd_dat_rd(unsignedcharright)。
unsignedcharlcd_dat_rd(unsignedcharright)
{
loop_lcd12864_is_busy(right);
if(right)return(LCD_RDR);
elseretuen(LCD_LDR);
}
该函数完成对LCD已显示的数据读出,首次操作需要读2次数据才有效。
LCD清屏函数voidlcd12864_clr(void)。
voidlcd12864_clr(void)
{
unsignedchari,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初始化函数voidlcd12864_init(void)。
voidlcd12864_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进行初始化操作。
例如给左半屏(0,0)首字节写入数据0x55,应该执行以下程序。
lcd12864_init();//LCD初始化
lcd_cmd_wr(ORGX,0);//设定水平轴X地址
lcd_cmd_wr(ORGY,0);//设定垂直轴Y地址
lcd_dat_wr(0x55,0);//写入数据,从下至上为01010101
lcd_dat_wr(0xaa,0);//写入数据,从下至上为10101010,连续写入,X地址自动加1
这样在如图0字节和1字节就将交替有4点亮,4点暗,构成如图2-2所示图案。
图2-2LCD显示示例图2-3LCD显示示例
又如执行程序:
lcd12864_init();
lcd_cmd_wr(ORGX,1);
lcd_cmd_wr(ORGY,1);
lcd_dat_wr(0x55,1);
显示效果如图2-3所示。
如果执行以下程序:
lcd12864_init();
lcd_cmd_wr(ORGX+1,0);
lcd_cmd_wr(ORGY+1,0);
lcd_data_wr(0x55,0);
显示效果如图2-4所示。
图2-4LCD显示示例图2-5LCD显示示例
从驱动函数可见,一次对LCD写入数据是以字节为单位,通过写命令确定坐标,Y坐标从0页到7页,X坐标从0列到63列,分左右屏,左上角为坐标(0,0)点,这和我们习惯的左下角为(0,0)坐标轴是不一样的。
因为每次操作LCD是一个字节为单位,对应8点,如果我们希望以任意点为坐标显示,还得另外寻找别的办法编程实现真正“点”显示。
如图2-5所示,在屏幕上指定位置画点,水平轴就是x,与lcd坐标一致,y轴需要将点坐标变成字节为单位的坐标,我们先按习惯将y轴64点从下至上编号0到63,其中0到7点为字节0,8到15点为字节1,依此类推对应8字节。
第一点Y轴为30,应该是哪字节哪bit呢?
30/8取整为3,前3字节(0,1,2三字节)对应0到23点跳过,实际30点应该在第4字节(24到31)的bit6上,这就要用算法计算出字节数和bit位,前面30/8=3,这里的3似乎就是要找的字节数,那么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;
下面我们来设计的函数。
总结以上算法,编写出在LCD任何坐标位置上描点绘图函数voidlcd_disp(unsignedcharx,y)。
列更新子函数lcd_row_wr(unsignedcharx,unsignedchar*da)。
voidlcd_row_wr(unsignedcharx,unsignedchar*da)//x是列坐标,*da是8字节列数据
{
unsignedcharj;
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);
}
}
}
voidlcd_disp(unsignedcharx,y)//x水平坐标,y垂直坐标
{
unsignedchardat[8];
unsignedcharj;
for(j=0;j<8;j++)dat[j]=0x0;
dat[y/8]|=0x01<<(y&0x07);
lcd_row_wr(x,dat);
}
以上函数能够在指定坐标(x,y)上描点,例如执行程序:
lcd_init();
lcd_disp(0,0);
lcd_disp(1,1);
lcd_disp(2,2);
lcd_disp(3,3);
lcd_disp(4,4);
lcd_disp(5,5);
lcd_disp(6,6);
lcd_disp(7,7);
lcd_disp(70,10);
lcd_disp(71,20);
lcd_disp(72,30);
lcd_disp(73,40);
效果如图2-6所示。
图2-6LCD显示示例图2-7LCD显示示例
以上演示源程序keil工程请参考附件【LCD驱动exa1】。
如图2-6所示效果,Y轴“反了”,是的,这很好办,在lcd_disp()函数中增加一句:
y=63-y;就可以实现。
在下面的例子中将做到与我们习惯的坐标一致。
voidmain()
{
unsignedchari;
lcd_init();
for(i=0;i<128;i++){
lcd_disp(i,i/2);
}
while
(1);
}
效果如图2-7所示。
以上演示源程序keil工程请参考附件【LCD驱动exa2】。
如果将正弦表的数据送LCD显示,程序:
voidmain()
{
unsignedchari;
lcd_init();
for(i=0;i<128;i++){lcd_disp(i,dot[i%30]/4);//除以4是变8bit为6bit(0-63)进行LCD映射
}
while
(1);
}
效果如图2-8所示。
图2-8LCD显示示例图2-9矢量绘图示例
以上演示源程序keil工程请参考附件【LCD驱动exa3】。
以上是逐点绘图,其实矢量绘图效果会更好,矢量绘图