电磁兼容(EMC)设计与整改
直播中

陈洁

7年用户 179经验值
私信 关注
[问答]

怎么实现基于LCD12864显示器的数字示波器设计

数字示波器原理是什么?
图形液晶LCD12864绘图驱动设计基础
怎么提高图形液晶LCD12864绘图驱动设计?

回帖(1)

梁艳

2021-5-10 10:27:29
  本文针对LCD12864 特性,完成了数字示波器显示必须的绘图驱动程序设计,这个教程定位给初学者使用,立足从简单到复杂一步一步介绍设计过程,甚至是调试的过程,还包括一些经验总结,特别是提供了完整的keil 工程附件。希望读者立足示波器项目,学到更多软硬件设计经验技巧。
  一、简易数字示波器原理
  数字示波器基本原理可以简单理解为:数据采集+ 图形显示,该过程循环进行,如图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(unsigned char right)。
  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);
  }
举报

更多回帖

发帖
×
20
完善资料,
赚取积分