jianhong_wu 发表于 2014-9-17 14:38

第六十八节:单片机C语言的多文件编程技巧。 开场白:很多人也把多文件编程称作模块化编程,其实我觉得叫多文件编程会更加符合实际一些。多文件编程有两个最大的好处,一个是给我们的程序增加了目录,方便我们查找。另外一个好处是方便移植别人已经做好的功能程序模块,利用这个特点,特别适合团队一起做大型项目。很多初学者刚开始学多文件编程时,会经常遇到重复定义等问题,想知道怎么解决这些问题吗?只要按照以下鸿哥教的规则来做,这些问题就不存在了。第一个:每个文件保持成双成对出现。每个.c源文件必须有一个.h头文件跟它对应,每个.h头文件必须有一个.c源文件跟它对应。比如:main.c与main.h,delay.c与 delay.h。 第二个:.c源文件只负责函数的定义和变量的定义,但是不负责函数的声明和变量的声明。比如:unsigned char ucLedStep=0; //这个是全局变量的定义void led_flicker()   //这个是函数的定义{   //…里面是具体代码内容} 第三个:.h头文件只负责函数的声明和变量的声明,以及常量和IO口的宏定义,但是不负责函数的定义和变量的定义。比如:#define const_time_level 200//这个是常量的宏定义sbit led_dr=P3^5;    //这个是IO口的宏定义 void led_flicker();   //这个是函数的声明extern unsigned char ucLedStep;   //这个是全局变量的声明,不能赋初始值 第四个:每个.h头文件都必须固定以#ifndef,#define,#endif语句为模板,此模板是用来避免编译时由于重复包含头文件里面的内容而导致出错。其中标志变量_XXX_鸿哥建议用它本身的文件名称加前后下划线_。比如:#ifndef _LED_   //标志变量_LED_是用它本身的文件名称命名#define _LED_   //标志变量_LED_是用它本身的文件名称命名 #define const_time_level 200//这个是常量的宏定义sbit led_dr=P3^5;    //这个是IO口的宏定义 void led_flicker();   //这个是函数的声明extern unsigned char ucLedStep;   //这个是全局变量的声明,不能赋初始值#endif 第五个:每个.h头文件里都必须声明它对应的.c源文件里的所有定义函数和全局变量,注意:.c源文件里所有的全局变量都要在它所对应的.h头文件里声明一次,不仅仅是函数,这个地方很容易被人忽略。比如:在led.h头文件中:void led_flicker();   //这个是函数的声明,因为在这个函数在led.c文件里定义了。   extern unsigned char ucLedStep;   //这个是全局变量的声明,不许赋初值 第六个:每个.c源文件里都必须包含两个文件,一个是单片机的系统头文件REG52.H,另外一个是它自己本身的头文件比如initial.h.剩下其它的头文件看实际情况来决定是否调用,我们用到了哪些文件的函数,全局变量或者宏定义,就需要调用对应的头文件。比如:在initial.c源文件中:#include"REG52.H"//必须包含的单片机系统头文件#include"initial.h"//必须包含它本身的头文件/* 注释:   由于本源文件中用到了led_dr的语句,而led_dr是在led.h文件里宏定义的,所以必须把led.h也包含进来*/#include"led.h"//由于本源文件中用到了led_dr的语句,所以必须把led.h也包含进来void initial_myself()//这个是函数定义{led_dr=0;//led_dr是在led文件里定义和声明的} 第七个:声明一个全局变量必须加extern关键字,同时千万不能在声明全局变量的时候赋初始值,比如:extern unsigned char ucLedStep=0; //这样是绝对错误的。extern unsigned char ucLedStep; //这个是全局变量的声明,这个才是正确的 第八个:对于函数与全局变量的声明,编译器都不分配内存空间。对于函数与全局变量的定义,编译器都分配内存空间。函数与全局变量的定义只能在一个.c源文件中出现一次,而函数与全局变量的声明可以在多个.h文件中出现。
具体内容,请看源代码讲解,本程序例程是直接把前面第四节一个源文件更改成多文件编程方式。

(1)硬件平台:
    基于朱兆祺51单片机学习板。把前面第四节一个源文件更改成多文件编程方式。
(2)实现功能:跟前面第四节的功能一模一样,让一个LED闪烁。
   
(3)keil多文件编程的截图预览: (4)整个源代码讲解工程文件下载: (5)源代码讲解如下(注意,以下代码不能直接放到一个源文件里编译): /*以下是 main.h 的内容*/

/* 注释一:
每个头文件都是固定以#ifndef,#define,#endif
为模板,其中标志变量_XXX_我建议用它本身的文件名称加前后下划线_。
此标志变量名称是用来预防多次包含出错的,详细讲解请看注释二。
每个头文件只做函数的声明和变量的声明,以及常量和IO口的宏定义,不做
函数的定义与变量的定义。
*/
#ifndef _MAIN_   //标志变量_MAIN_是用它本身的文件名称命名
#define _MAIN_   //标志变量_MAIN_是用它本身的文件名称命名

void main();//这个是函数的声明

#endif

/* 注释二:
以上语句
#ifndef
#define
插入其它内容...
#endif

类似于把_MAIN_看成是一个标志变量
if(_MAIN_==0)// 相当于#ifndef _MAIN_
{
    _MAIN_=1;// 相当于#define _MAIN_
   插入其它内容...

}               //相当于#endif

目的是通过一个标志位变量的赋值,让编译器在编译的时候,只包含一次此头文件,避免多次包含出错
*/

/*------分割线--------------------------------------------------*/

/*以下是 main.c 的内容*/

/* 注释一:
每个源文件都必须包含两个文件,一个是单片机的系统头文件REG52.H,
另外一个是它自己本身的头文件main.h.剩下其它的头文件看实际情况来
决定是否调用,我们用到了哪些文件的函数,全局变量或者宏定义,就需要调用对应的头文件。
每个源文件只做函数的定义和变量的定义,不做函数的声明和变量的声明。
*/

#include "REG52.H"//必须包含的单片机系统头文件
#include "main.h"//必须包含它本身的头文件

/* 注释二:
   (1)由于本源文件中调用initial_myself()和initial_peripheral()函数,而这两个函数
      都是在initial文件里定义和声明的,所以必须把initial.h也包含进来。
   (2)由于本源文件中调用delay_long(100)函数,而这个函数
      是在delay文件里定义和声明的,所以必须把delay.h也包含进来。
   (2)由于本源文件中调用led_flicker()函数,而这个函数
      是在led文件里定义和声明的,所以必须把led.h也包含进来。
*/


#include "initial.h"//由于本源文件中用到了initial_myself()和initial_peripheral()函数,所以必须把initial.h也包含进来
#include "delay.h"//由于本源文件中用到了delay_long(100)函数,所以必须把delay.h也包含进来
#include "led.h"//由于本源文件中用到了led_flicker()函数,所以必须把led.h也包含进来

void main()//这个是函数的定义
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)   
   {
      led_flicker();   
   }

}

/*------分割线--------------------------------------------------*/

/*以下是 delay.h 的内容*/


#ifndef _DELAY_   //标志变量_DELAY_是用它本身的文件名称命名
#define _DELAY_   //标志变量_DELAY_是用它本身的文件名称命名

void delay_long(unsigned int uiDelaylong); //这个是函数的声明,每一个源文件里的函数都要在它的头文件里声明


#endif

/*------分割线--------------------------------------------------*/

/*以下是 delay.c 的内容*/


#include "REG52.H"//必须包含的单片机系统头文件
#include "delay.h"//必须包含它本身的头文件


void delay_long(unsigned int uiDelayLong)//这个是函数的定义
{
   unsigned int i;   //这个是局部变量的定义
   unsigned int j;   //这个是局部变量的定义
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)
          {
             ;
          }
   }
}
/*------分割线--------------------------------------------------*/
/*以下是 initial.h 的内容*/

#ifndef _INITIAL_   //标志变量_INITIAL_是用它本身的文件名称命名
#define _INITIAL_   //标志变量_INITIAL_是用它本身的文件名称命名

void initial_myself();    //这个是函数声明,每一个源文件里的函数都要在它的头文件里声明
void initial_peripheral(); //这个是函数声明,每一个源文件里的函数都要在它的头文件里声明

#endif

/*------分割线--------------------------------------------------*/
/*以下是 initial.c 的内容*/


#include "REG52.H"//必须包含的单片机系统头文件
#include "initial.h"//必须包含它本身的头文件

/* 注释一:
   由于本源文件中用到了led_dr的语句,而led_dr是在led文件里宏定义的,所以必须把led.h也包含进来
*/
#include "led.h"//由于本源文件中用到了led_dr的语句,所以必须把led.h也包含进来

void initial_myself()//这个是函数定义
{


TMOD=0x01;//以下能直接用TMOD,TH0,TL0,EA,ET0,TR0这些寄存器关键字,是因为包含了REG52.H头文件

TH0=0xf8;
TL0=0x2f;   

led_dr=0;//led_dr是在led文件里定义和声明的
}


void initial_peripheral() //这个是函数定义
{
EA=1;   
ET0=1;   
TR0=1;   

}

/*------分割线--------------------------------------------------*/
/*以下是 interrupt.h 的内容*/


#ifndef _INTERRUPT_   //标志变量_INTERRUPT_是用它本身的文件名称命名
#define _INTERRUPT_   //标志变量_INTERRUPT_是用它本身的文件名称命名

void T0_time();//这个是函数声明,每一个源文件里的函数都要在它的头文件里声明

/* 注释一:
声明一个外部全局变量必须加extern关键字,同时千万不能在声明全局变量的时候赋初始值,比如:
extern unsigned int uiTimeCnt=0; 这样是绝对错误的。
*/
extern unsigned int uiTimeCnt; //这个是全局变量的声明,不能赋初始值


#endif

/*------分割线--------------------------------------------------*/
/*以下是 interrupt.c 的内容*/


#include "REG52.H"//必须包含的单片机系统头文件
#include "interrupt.h"//必须包含它本身的头文件

unsigned int uiTimeCnt=0; //这个是全局变量的定义,可以赋初值


void T0_time() interrupt 1//这个是函数定义
{
TF0=0;   //以下能直接用TF0,TR0,TH0,TL0这些寄存器关键字,是因为包含了REG52.H头文件
TR0=0;

if(uiTimeCnt<0xffff)
{
      uiTimeCnt++;
}

TH0=0xf8;
TL0=0x2f;
TR0=1;
}


/*------分割线--------------------------------------------------*/
/*以下是 led.h 的内容*/


#ifndef _LED_   //标志变量_LED_是用它本身的文件名称命名
#define _LED_   //标志变量_LED_是用它本身的文件名称命名


#define const_time_level 200   //宏定义都放在头文件里

/* 注释一:
IO口的宏定义也放在头文件里,
如果是PIC单片机,以下IO口定义相当于宏定义 #defineled_dr LATBbits.LATB4等语句
*/
sbit led_dr=P3^5; //如果是PIC单片机,相当于宏定义 #defineled_dr LATBbits.LATB4等语句

void led_flicker();   //这个是函数的声明,每一个源文件里的函数都要在它的头文件里声明

/* 注释三:
声明一个全局变量必须加extern关键字,同时千万不能在声明全局变量的时候赋初始值,比如:
extern unsigned char ucLedStep=0; 这样是绝对错误的。
*/
extern unsigned char ucLedStep; //这个是全局变量的声明



#endif

/*------分割线--------------------------------------------------*/
/*以下是 led.c 的内容*/

#include "REG52.H"//必须包含的单片机系统头文件
#include "led.h"//必须包含它本身的头文件


/* 注释一:
   由于本源文件中用到了uiTimeCnt全局变量,而uiTimeCnt是在interrupt文件里声明和定义的,
   所以必须把interrupt.h也包含进来
*/
#include "interrupt.h"//必须包含它本身的头文件

unsigned char ucLedStep=0; //这个是全局变量的定义,可以赋初值

void led_flicker()   //这个是函数的定义
{
switch(ucLedStep)
{
   case 0:

         if(uiTimeCnt>=const_time_level)
         {

             ET0=0;//以下能直接用ET0寄存器关键字,是因为包含了REG52.H头文件
             uiTimeCnt=0; //uiTimeCnt此变量是在interrupt文件里声明和定义的,所以必须把interrupt.h也包含进来
             ET0=1;
             led_dr=1;//此IO口定义已经在led.h头文件中定义了
             ucLedStep=1; //切换到下一个步骤
         }
         break;
   case 1:
         if(uiTimeCnt>=const_time_level)
         {
             ET0=0;
             uiTimeCnt=0;
             ET0=1;
             led_dr=0;   
             ucLedStep=0; //返回到上一个步骤
         }
         break;

}

}


/*------分割线--------------------------------------------------*/

总结陈词: 下一节开始讲液晶屏显示方面的内容。欲知详情,请听下回分解----带字库12864液晶屏的常用点阵字体程序。 (未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-9-18 13:10

第六十九节:使用static关键字可以减少全局变量的使用。

开场白:
本来这一节打算开始讲液晶屏的,但是昨天经过网友“任军”的点拨,我发现了一个惊天秘密,原来static关键字是这么好的东西我却错过了那么多年。以前就有一些网友抱怨,鸿哥的程序好是好,就是全局变量满天飞,当时我觉得我也没招呀,C语言就全局变量和局部变量,单单靠局部变量肯定不行,局部变量每次进入函数内部数值都会被初始化改变,所以我在很多场合也只能靠全局变量了。但是自从昨天知道了static关键字的秘密后,我恍然大悟,很多场合只要在局部变量前面加上static关键字,就可以大大减少全局变量的使用了。
这一节要教会大家一个知识点:
大家都知道,普通的局部变量在每次程序执行到函数内部的时候,数值都会被重新初始化,数值会发生变化,不能保持之前的数值。但是在局部变量加上static关键字后,系统在刚上电的时候就已经把带static的局部变量赋初始值了,从此程序每次进入函数内部,都不会初始化带static关键字的局部变量,它会保持最近一次被程序执行更改的数值不变,像全局变量一样。跟全局变量唯一的差别是,带static关键字的局部变量的作用域仅仅在函数内部,而普通全局变量的作用域是整个工程。

本程序例程是直接在第八节程序上修改,大大减少了全局变量的使用。具体内容,请看源代码讲解。

(1)硬件平台:
   基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。

(2)实现功能:跟前面第八节的功能一模一样,有两个独立按键,每按一个独立按键,蜂鸣器发出“滴”的一声后就停。
   
(3)源代码讲解如下:

#include "REG52.H"

#define const_voice_short40

#define const_key_time120   
#define const_key_time220   

void initial_myself();   
void initial_peripheral();
void delay_long(unsigned int uiDelaylong);
void T0_time();
void key_service();
void key_scan();

sbit key_sr1=P0^0;
sbit key_sr2=P0^1;
sbit key_gnd_dr=P0^4;

sbit beep_dr=P2^7;

unsigned char ucKeySec=0;   //一些需要在不同函数之间使用的核心变量,只能用全局变量
unsigned intuiVoiceCnt=0;//一些需要在不同函数之间使用的核心变量,只能用全局变量

void main()
{
   initial_myself();
   delay_long(100);   
   initial_peripheral();
   while(1)
   {
       key_service();
   }

}

void key_scan()
{
/* 注释一:
   (1)大家都知道,普通的局部变量在每次程序执行到函数内部的时候,数值都会被重新初始化,
   数值会发生变化,不能保持之前的数值。
   (2)但是在局部变量加上static关键字后,系统在刚上电的时候就已经把带static的局部变量
   赋初始值了,从此程序每次进入函数内部,都不会初始化带static关键字的局部变量,它会保持
   最近一次被程序执行更改的数值不变,像全局变量一样。跟全局变量唯一的差别是,带static关键字
   的局部变量的作用域仅仅在函数内部,而普通全局变量的作用域是整个工程。
   (3)以下这些变量我原来在第八节是用普通全局变量的,现在改成用static的局部变量了,减少了全局变量
   的使用,让程序阅读起来更加简洁。大家也可以试试把以下变量的static去掉试试,结果会发现去掉了static后,
   按键就不会被触发了。
*/

static unsigned intuiKeyTimeCnt1=0; //带static的局部变量
static unsigned char ucKeyLock1=0;   
static unsigned intuiKeyTimeCnt2=0;
static unsigned char ucKeyLock2=0;


if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
         uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)
{
   ucKeyLock2=0;
         uiKeyTimeCnt2=0;
}
else if(ucKeyLock2==0)
{
   uiKeyTimeCnt2++;
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;
      ucKeySec=2;   
   }
}

}


void key_service()
{
switch(ucKeySec)
{
    case 1:

          uiVoiceCnt=const_voice_short;
          ucKeySec=0;
          break;      
    case 2:
          uiVoiceCnt=const_voice_short;
          ucKeySec=0;
          break;                  
}               
}



void T0_time() interrupt 1
{
TF0=0;
TR0=0;

key_scan();

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--;
   beep_dr=0;
}
else
{
   ;
   beep_dr=1;
}


TH0=0xf8;   
TL0=0x2f;
TR0=1;
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)
      {
         ;
      }
   }
}


void initial_myself()
{


key_gnd_dr=0;

beep_dr=1;

TMOD=0x01;

TH0=0xf8;   
TL0=0x2f;

}
void initial_peripheral()
{
EA=1;   
ET0=1;   
TR0=1;   
}
总结陈词:
下一节开始讲液晶屏显示方面的内容。欲知详情,请听下回分解----带字库12864液晶屏的常用点阵字体程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-9-24 14:37

第七十节:深入讲解液晶屏的构字过程。

开场白:
    液晶屏模块本身带控制芯片,驱动液晶屏的本质就是单片机通过串行或者并行方式,根据芯片资料指定的协议跟液晶芯片进行通讯的过程。这个详细的通讯协议驱动程序厂家都会免费提供的,也可以在网上找到大量的示范程序。那么我们最应该关注的核心是什么?我认为最核心的是要理清楚程序坐标与实际显示坐标之间的关系规律。本程序不使用模块自带的字库,而是使用自己构造的字库,目的就是为了让读者理解更底层的字模显示。
这一节要教会大家三个知识点:
第一个:对于驱动芯片是st7920的12864液晶屏,它的真实坐标体系的本质是256x32的点阵液晶屏。
第二个:鸿哥刻意在驱动显示函数里增加了大延时函数,目的是通过慢镜头回放,让大家观察到横向取模的字是如何一个字节一个字节构建而成的。
第三个:数组带const关键字,表示数据常量存放在ROM程序区,不占用RAM的变量。

具体内容,请看源代码讲解。

(1)硬件平台:
   基于朱兆祺51单片机学习板。

(2)实现功能:开机上电后,可以观察到0x01,0x02,0x03,0x04这4个显示数字在不同的排列方式下,出现在不同的液晶屏显示位置。也可以观察到“馒头”这两个字是如何一个字节一个字节构建而成的,加深理解字模数组跟显示现象的关系。
   
(3)源代码讲解如下:
#include "REG52.H"


sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount); //显示任意点阵函数
void display_clear(void); // 清屏

void delay_short(unsigned int uiDelayshort); //延时

/* 注释一:
* 数组带const关键字,表示数据常量存放在ROM程序区,不占用RAM的变量
*/
const unsigned char Hz1616_man[]= /*馒   横向取模16X16点阵 网上有很多免费的字模软件生成字模数组 */
{
0x21,0xF8,0x21,0x08,0x21,0xF8,0x3D,0x08,0x45,0xF8,0x48,0x00,0x83,0xFC,0x22,0x94,
0x23,0xFC,0x20,0x00,0x21,0xF8,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x00,0x00,
};

const unsigned char Hz1616_tou[]= /*头   横向取模16X16点阵 网上有很多免费的字模软件生成字模数组 */
{
0x00,0x80,0x10,0x80,0x0C,0x80,0x04,0x80,0x10,0x80,0x0C,0x80,0x08,0x80,0x00,0x80,
0xFF,0xFE,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x30,0x08,0x18,0x10,0x0C,0x20,0x08,
};

/* 注释二:
* 为了方便观察字模的数字与显示的关系,以下3个数组的本质是完全一样的,只是排列不一样而已。
*/
const unsigned char Byte_1[]=//4横,1列
{
0x01,0x02,0x03,0x04,
};

const unsigned char Byte_2[]= //2横,2列
{
0x01,0x02,
0x03,0x04,
};

const unsigned char Byte_3[]= //1横,4列
{
0x01,
0x02,
0x03,
0x04,
};


void main()
{
        LCDInit(); //初始化12864 内部包含液晶模块的复位

    display_clear(); // 清屏

        display_lattice(0,0,Byte_1,0,4,1);    //显示<4横,1列>的数组数字
        display_lattice(0,16,Byte_1,1,4,1);   //显示<4横,1列>的数组数字 反显

        display_lattice(7,0,Byte_2,0,2,2);   //显示<2横,2列>的数组数字
        display_lattice(7,16,Byte_2,1,2,2);//显示<2横,2列>的数组数字 反显

        display_lattice(8,0,Byte_3,0,1,4);//显示<1横,4列>的数组数字
        display_lattice(8,16,Byte_3,1,1,4); //显示<1横,4列>的数组数字 反显

        display_lattice(14,0,Hz1616_man,0,2,16);//显示<馒>字
        display_lattice(15,0,Hz1616_tou,0,2,16);//显示<头>字
        display_lattice(14,16,Hz1616_man,1,2,16); //显示<馒>字 反显
        display_lattice(15,16,Hz1616_tou,1,2,16); //显示<头>字 反显
    while(1)
    {
       ;
    }

}

/* 注释三:真实坐标体系的本质。
* 从坐标体系的角度来看,本液晶屏表面上是128x64的液晶屏,实际上可以看做是256x32的液晶屏。
* 把256x32的液晶屏分左右两半,把左半屏128x32放在上面,把右半屏128x32放下面,就合并成了
* 一个128x64的液晶屏。由于液晶模块内部控制器的原因,虽然横向有256个点阵,但是我们的x轴
* 坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,因此256个点的x轴坐标范围是0至15。
* 而y轴的坐标可以精确到每个点为一行,所以32个点的y轴坐标范围是0至31.
*/

void display_clear(void) // 清屏
{   

        unsigned char x,y;
//WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x36); //这次为了观察每个数字在显示屏上的关系,所以把这个显示缓冲的命令提前打开,下一节放到本函数最后
        y=0;
        while(y<32)//y轴的范围0至31
    {
               WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(0x00);
             }
               y++;
    }


}


/* 注释四:本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 本函数后面故意增加一个长延时delay_short(30000),是为了方便读者观察横向取模的
* 字是如何一个字节一个字节构建而成的。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
//WriteCommand(0x34);   //关显示缓冲指令   
   WriteCommand(0x36);//这次为了观察每个数字在显示屏上的关系,所以把这个显示缓冲的命令提前打开,下一节放到本函数最后
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray;
             if(ucFbFlag==1)//反白显示
               {
            ucTemp=~ucTemp;
         }
             LCDWriteData(ucTemp);
               delay_short(30000);//本函数故意增加这个长延时,是为了方便读者观察横向取模的字是如何一个字节一个字节构建而成的。
      }
   }

}

/* 注释五:
* 以下是液晶屏模块的驱动程序,我觉得没有什么好讲的,因为我是直接在网上寻找现成的驱动时序修改而成。
* 它的本质就是单片机跟这个液晶模块芯片进行串行通信。
*/
void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
        unsigned char i;
        for ( i = 0; i < 8; i++ )
        {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
        }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
        SendByteToLcd( 0xf8 + (ucWRS << 1) );
        SendByteToLcd( ucWData & 0xf0 );
        SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

        LCDCS_dr = 0;
        LCDCS_dr = 1;
        SPIWrite(ucCommand, 0);
        delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
        LCDCS_dr = 0;
        LCDCS_dr = 1;
        SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
        LCDRST_dr = 1;//复位
        LCDRST_dr = 0;
        LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


总结陈词:
这节重点讲了液晶屏的构字过程,下节将会在本节的基础上,略作修改,显示常用的不同点阵字模。欲知详情,请听下回分解-----液晶屏的字符,16点阵,24点阵和32点阵的显示程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-9-25 11:55

第七十一节:液晶屏的字符,16点阵,24点阵和32点阵的显示程序。

开场白:
这一节要教会大家二个知识点:
第一个:如何利用任意点阵字体显示函数display_lattice来显示8x16的字符,16点阵汉字,24点阵汉字和32点阵汉字。
第二个:纠正上一节的一个小错误。C51编译器跟其它单片机的编译器有点不一样。想把常量数据保存在ROM程序存储区里并不是用const关键字,而是是用code关键字。

具体内容,请看源代码讲解。

(1)硬件平台:
   基于朱兆祺51单片机学习板。

(2)实现功能:开机上电后,可以看到液晶屏分别显示32点阵,24点阵和16点阵的“馒头”两个字,还有“V5”这两个8x16点阵的字符。
   
(3)源代码讲解如下:

#include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount); //显示任意点阵函数
void display_clear(void); // 清屏

void delay_short(unsigned int uiDelayshort); //延时

/* 注释一:
* 纠正上一节的一个小错误。C51编译器跟其它的编译器有点不一样。
* 存在ROM程序存储区里的常量数据并不是用const关键字,而是是用code关键字。
*/
code unsigned char Hz3232_man[]= /*馒   横向取模32x32点阵 */
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x07,0x03,0x00,0x0F,0x87,0xFF,0x80,
0x0F,0x07,0x03,0x80,0x0E,0x07,0x03,0x80,0x0E,0x37,0xFF,0x80,0x1C,0x7F,0x03,0x80,
0x1F,0xFF,0x03,0x80,0x18,0x77,0xFF,0x00,0x38,0xE0,0x00,0xC0,0x36,0xDF,0xFF,0xF0,
0x77,0x9C,0xCE,0xE0,0x67,0x1C,0xCE,0xE0,0xC7,0x1C,0xCE,0xE0,0x07,0x1C,0xCE,0xE0,
0x07,0x1F,0xFF,0xE0,0x07,0x18,0x00,0x00,0x07,0x00,0x03,0x80,0x07,0x0F,0xFF,0xC0,
0x07,0x71,0x8F,0x00,0x07,0xE0,0xDE,0x00,0x07,0xC0,0xFC,0x00,0x07,0x80,0x78,0x00,
0x0F,0x01,0xFE,0x00,0x07,0x03,0x8F,0xE0,0x00,0x1E,0x03,0xF0,0x00,0xF8,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};


code unsigned char Hz3232_tou[]= /*头   横向取模32x32点阵 */
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xC0,0x00,0x00,0x03,0xE0,0x00,
0x03,0xC3,0xC0,0x00,0x00,0xF3,0x80,0x00,0x00,0x7B,0x80,0x00,0x00,0x7B,0x80,0x00,
0x00,0x3B,0x80,0x00,0x0E,0x03,0x80,0x00,0x07,0x83,0x80,0x00,0x03,0xC3,0x80,0x00,
0x01,0xE3,0x80,0x00,0x01,0xE3,0x80,0x00,0x00,0xC3,0x80,0x00,0x00,0x03,0x81,0xE0,
0x7F,0xFF,0xFF,0xF0,0x00,0x07,0x80,0x30,0x00,0x07,0x00,0x00,0x00,0x07,0x80,0x00,
0x00,0x0E,0xE0,0x00,0x00,0x1E,0x7C,0x00,0x00,0x3C,0x1F,0x00,0x00,0x78,0x0F,0xC0,
0x00,0xF0,0x03,0xC0,0x03,0xC0,0x01,0xE0,0x0F,0x00,0x00,0xE0,0x78,0x00,0x00,0x00,
0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Hz2424_man[]= /*馒   横向取模24x24点阵 */
{
0x00,0x00,0x00,0x00,0x00,0x00,0x0C,0x18,0x30,0x1E,0x1F,0xF8,0x1C,0x1C,0x38,0x1C,
0x1F,0xF8,0x19,0xFC,0x38,0x3F,0xFF,0xF8,0x31,0x98,0x30,0x7B,0xE0,0x0E,0x6F,0x7F,
0xFE,0x6E,0x76,0xEE,0xCC,0x76,0xEE,0x0C,0x7F,0xFE,0x0C,0x70,0x0C,0x0C,0x00,0x38,
0x0C,0x3F,0xF8,0x0D,0xCE,0x70,0x0F,0x87,0xE0,0x0F,0x03,0x80,0x1E,0x07,0xE0,0x0C,
0x1C,0x7E,0x01,0xF0,0x1F,0x00,0x00,0x00,
};


code unsigned char Hz2424_tou[]= /*头   横向取模24x24点阵 */
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0E,0x00,0x06,0x0F,0x00,0x07,0x8E,0x00,0x01,
0xEE,0x00,0x00,0xEE,0x00,0x00,0xEC,0x00,0x1C,0x0C,0x00,0x0F,0x0C,0x00,0x07,0x9C,
0x00,0x03,0x9C,0x00,0x00,0x1C,0x0C,0x00,0x1C,0x1E,0x7F,0xFF,0xF6,0x00,0x1C,0x00,
0x00,0x3C,0x00,0x00,0x3F,0x80,0x00,0x71,0xE0,0x00,0xE0,0xF8,0x01,0xC0,0x3C,0x07,
0x00,0x1C,0x3C,0x00,0x0C,0x70,0x00,0x00,
};


code unsigned char Hz1616_man[]= /*馒   横向取模16X16点阵 */
{
0x21,0xF8,0x21,0x08,0x21,0xF8,0x3D,0x08,0x45,0xF8,0x48,0x00,0x83,0xFC,0x22,0x94,
0x23,0xFC,0x20,0x00,0x21,0xF8,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x00,0x00,
};

code unsigned char Hz1616_tou[]= /*头   横向取模16X16点阵 */
{
0x00,0x80,0x10,0x80,0x0C,0x80,0x04,0x80,0x10,0x80,0x0C,0x80,0x08,0x80,0x00,0x80,
0xFF,0xFE,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x30,0x08,0x18,0x10,0x0C,0x20,0x08,
};

code unsigned char Zf816_V[]= /*V   横向取模8x16点阵 */
{
0x00,0x00,0x00,0xE7,0x42,0x42,0x44,0x24,0x24,0x28,0x28,0x18,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_5[]= /*5   横向取模8x16点阵 */
{
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};



void main()
{
        LCDInit(); //初始化12864 内部包含液晶模块的复位

    display_clear(); // 清屏

        display_lattice(0,0,Hz3232_man,0,4,32);//显示32点阵的<馒>字
        display_lattice(2,0,Hz3232_tou,0,4,32);//显示32点阵的<头>字

        display_lattice(4,0,Hz2424_man,0,3,24);//显示24点阵的<馒>字
        display_lattice(6,0,Hz2424_tou,0,3,24);//显示24点阵的<头>字

        display_lattice(8,0,Hz1616_man,0,2,16);//显示16点阵的<馒>字
        display_lattice(9,0,Hz1616_tou,0,2,16);//显示16点阵的<头>字

        display_lattice(11,0,Zf816_V,0,1,16);//显示8x16点阵的<V>字符
        display_lattice(12,0,Zf816_5,0,1,16);//显示8x16点阵的<5>字符

    while(1)
    {
       ;
    }

}



void display_clear(void) // 清屏
{   

        unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
        y=0;
        while(y<32)//y轴的范围0至31
    {
               WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(0x00);
             }
               y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}


/* 注释二:本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   WriteCommand(0x34);//关显示缓冲指令            
   WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray;
             if(ucFbFlag==1)//反白显示
               {
            ucTemp=~ucTemp;
         }
             LCDWriteData(ucTemp);
          //       delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}


void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
        unsigned char i;
        for ( i = 0; i < 8; i++ )
        {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
        }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
        SendByteToLcd( 0xf8 + (ucWRS << 1) );
        SendByteToLcd( ucWData & 0xf0 );
        SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

        LCDCS_dr = 0;
        LCDCS_dr = 1;
        SPIWrite(ucCommand, 0);
        delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
        LCDCS_dr = 0;
        LCDCS_dr = 1;
        SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
        LCDRST_dr = 1;//复位
        LCDRST_dr = 0;
        LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}

总结陈词:
    我们现在讲的字体显示都是横向的,如果某个项目要把整个液晶屏顺时针旋转90度,要求像对联一样纵向显示一串字体的时候,该怎么办?我前两个月就遇到了这样的项目,当时我的做法就是把字体的字库数组通过算法旋转90度就达到了目的。这种算法程序是怎样编写的?欲知详情,请听下回分解-----把字体顺时针旋转90度显示的算法程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-10 13:19

第七十二节:在液晶屏中把字体顺时针旋转90度显示的算法程序。 开场白:我曾经遇到过这样的项目,客户由于外壳结果的原因,故意把液晶屏物理位置逆时针旋转了90度,在这种情况下,如果按之前的显示驱动就会发现字体也跟着倒了过来,影响了阅读。当时我的解决办法就是把字体的字库数组通过算法顺时针旋转90度就达到了目的。这一节把这个算法教给大家。 这个算法的本质是:请看以下附图1,附图2,附图3. 第一步:旋转90度的本质,就是把原来横向取模改成纵向去模。先把代表每一行16个点阵数的2个char型数据合并成1个int型数据。 第二步:再把每一列的16个点阵按2个字节分别取到一个数组里,就是纵向取模的过程了。 具体内容,请看源代码讲解。
(1)硬件平台:
    基于朱兆祺51单片机学习板。
(2)实现功能:把液晶屏物理位置逆时针旋转了90度,开机上电后,可以看到液晶屏像对联的显示顺序一样,从上往下分别显示“馒头V5”四个字。   (3)源代码讲解如下: #include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount); //显示任意点阵函数
void display_clear(void); // 清屏
void hz1616_s90(const unsigned char*p_ucHz,unsigned char *p_ucResult);//把16x16汉字字模顺时针旋转90度的转换函数
void hz816_s90(const unsigned char*p_ucHz,unsigned char *p_ucResult);//把8x16字符字模顺时针旋转90度的转换函数

void delay_short(unsigned int uiDelayshort); //延时

code unsigned char Hz1616_man[]= /*馒   横向取模16X16点阵 */
{
0x21,0xF8,0x21,0x08,0x21,0xF8,0x3D,0x08,0x45,0xF8,0x48,0x00,0x83,0xFC,0x22,0x94,
0x23,0xFC,0x20,0x00,0x21,0xF8,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x00,0x00,
};

code unsigned char Hz1616_tou[]= /*头   横向取模16X16点阵 */
{
0x00,0x80,0x10,0x80,0x0C,0x80,0x04,0x80,0x10,0x80,0x0C,0x80,0x08,0x80,0x00,0x80,
0xFF,0xFE,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x30,0x08,0x18,0x10,0x0C,0x20,0x08,
};


code unsigned char Zf816_V[]= /*V   横向取模8x16点阵 */
{
0x00,0x00,0x00,0xE7,0x42,0x42,0x44,0x24,0x24,0x28,0x28,0x18,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_5[]= /*5   横向取模8x16点阵 */
{
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};


unsigned char ucBufferResult; //用于临时存放转换结束后的字模数组

void main()
{
      LCDInit(); //初始化12864 内部包含液晶模块的复位

      display_clear(); // 清屏

/* 注释一:
* (1)把原来的液晶屏物理位置逆时针旋转90度后,从上往下阅读,类似对联的阅读习惯。所以请注意坐标体系参数的变化。
* (2)为了让字符居中显示,请注意在显示V和5两个字符时坐标体系的变化。
* (3)字符8x16经过旋转处理后,变成了16x8,在调用display_lattice函数时,要注意修改响应的参数。
*/

      hz1616_s90(Hz1616_man,ucBufferResult);//把<馒>字顺时针旋转90度放到ucBufferResult临时变量里。
      display_lattice(7,0,ucBufferResult,0,2,16);//显示旋转90度后的<馒>字

      hz1616_s90(Hz1616_tou,ucBufferResult);//把<头>字顺时针旋转90度放到ucBufferResult临时变量里。
      display_lattice(6,0,ucBufferResult,0,2,16);//显示旋转90度后的<头>字


      hz816_s90(Zf816_V,ucBufferResult);//把<V>字符顺时针旋转90度放到ucBufferResult临时变量里。
      display_lattice(5,4,ucBufferResult,0,2,8);//显示旋转90度后的<V>字符。注意在最后两个个参数,2表示每一行有2个字节,8表示8列。第二个坐标参数4是为了偏移居中显示。

      hz816_s90(Zf816_5,ucBufferResult);//把<5>字符顺时针旋转90度放到ucBufferResult临时变量里。
      display_lattice(4,4,ucBufferResult,0,2,8);//显示旋转90度后的<5>字符。注意在最后两个个参数,2表示每一行有2个字节,8表示8列。第二个坐标参数4是为了偏移居中显示。


    while(1)
    {
       ;
    }

}



void display_clear(void) // 清屏
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(0x00);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}


/* 注释二:
* 把16x16汉字字模顺时针旋转90度的步骤:请看附图1,附图2,附图3.
* 第一步:旋转90度的本质,就是把原来横向取模改成纵向去模。先把代表每一行16个点阵数的2个char型数据合并成1个int型数据。
* 第二步:再把每一列的16个点阵按2个字节分别取到一个数组里,就是纵向取模的过程了。以下程序int型数据每取8个数据的最高位,
* 就左移一次,本质就是纵向取模的过程。
*/
void hz1616_s90(const unsigned char*p_ucHz,unsigned char *p_ucResult)//把16x16汉字字模顺时针旋转90度的转换函数
{
       unsigned char a;
       unsigned char b;
       unsigned char c;
   unsigned int uiBuffer;//注意,是int类型数据,一个数据包含2个字节。
       
       for(a=0;a<16;a++) //把原来以字节为单位的字库每一行的2个字节合并成1个int型数据。放到一个包含16个int类型的数组里,为旋转90度算法处理做准备
       {
         uiBuffer=p_ucHz;
               uiBuffer=uiBuffer<<8;
               uiBuffer=uiBuffer+p_ucHz;
   }
       
       c=0;
       for(a=0;a<16;a++)//这里的16代表16列
       {
               for(b=0;b<8;b++)   //每一列中有16个点,有2个字节,这里的8代表第一个字节的8个位或点。
               {
                          p_ucResult=p_ucResult<<1;   
            p_ucResult=p_ucResult&0xfe;               
            if(uiBuffer>=0x8000)    //注意,int类型数据的判断是0x8000,char型的是0x80
                      {
               p_ucResult=p_ucResult+1;
            }
                      uiBuffer=uiBuffer<<1;
         }
               c++;
               
               for(b=0;b<8;b++) //每一列中有16个点,有2个字节,这里的8代表第二个字节的8个位或点。
               {
                          p_ucResult=p_ucResult<<1;
            p_ucResult=p_ucResult&0xfe;                                      
            if(uiBuffer>=0x8000)      
                      {
                   p_ucResult=p_ucResult+1;
            }
                             uiBuffer=uiBuffer<<1;
         }
               c++;
    }
       
}


/* 注释三:
* 把8x16字符字模顺时针旋转90度的步骤:
* 第一步:旋转90度的本质,就是把原来横向取模改成纵向去模。由于原来的字库存放在带code关键字的ROM区,只能读不能写,所以
* 先把原来的字模数组读取出来,放到一个变量缓冲区里。
* 第二步:再把每一列的16个点阵按2个字节分别取到一个数组里,就是纵向取模的过程了。以下程序int型数据每取8个数据的最高位,
   就左移一次,本质就是纵向取模的过程。
*/
void hz816_s90(const unsigned char*p_ucHz,unsigned char *p_ucResult)//把8x16字符字模顺时针旋转90度的转换函数
{
       unsigned char a;
       unsigned char b;
       unsigned char c;
   unsigned char uiBuffer; //注意,跟16x16点阵不一样,这里是char数据。因为横向的只有8个点
       
       for(a=0;a<16;a++) //把存放在ROM的字库放到一个16个char类型的数组里
       {
         uiBuffer=p_ucHz;
   }
       
       c=0;
       for(a=0;a<8;a++)//这里的8代表8列
       {
               for(b=0;b<8;b++)//每一列中有16个点,有2个字节,这里的8代表第一个字节的8个位或点。
               {
                          p_ucResult=p_ucResult<<1;
            p_ucResult=p_ucResult&0xfe;                                       
            if(uiBuffer>=0x80)   //注意,int类型数据的判断是0x8000,char型的是0x80
                        {
                  p_ucResult=p_ucResult+1;
            }
                      uiBuffer=uiBuffer<<1;
         }
             c++;
               
               for(b=0;b<8;b++)//每一列中有16个点,有2个字节,这里的8代表第二个字节的8个位或点。
               {
                          p_ucResult=p_ucResult<<1;
            p_ucResult=p_ucResult&0xfe;                                       
            if(uiBuffer>=0x80)   //注意,int类型数据的判断是0x8000,char型的是0x80
                          {
               p_ucResult=p_ucResult+1;
            }
                          uiBuffer=uiBuffer<<1;
         }
               c++;
   }
       
}



/* 注释四:本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   WriteCommand(0x34);//关显示缓冲指令            
   WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray;
             if(ucFbFlag==1)//反白显示
               {
            ucTemp=~ucTemp;
         }
             LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}


void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}

总结陈词:     有的项目会要求把字体或者图像进行镜像显示处理,这种算法程序是怎样编写的?欲知详情,请听下回分解-----在液晶屏中把字体镜像显示的算法程序。 (未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-13 11:07

第七十三节:在液晶屏中把字体镜像显示的算法程序。

开场白:
有的项目会要求把字体或者图像进行镜像显示处理,这一节把这个算法教给大家。
    这个算法的本质是:
16x16点阵的图像或者字体有16行,每行有2个字节,如果把这2个字节看成是一个16位int型数据,那么就是要这个数据从原来左边是高位,右边是低位的顺序颠倒过来。本程序没有把2个字节合并成一个int型数据,而是直接在一个字节数据内把高低位顺序颠倒过来,然后把第1字节数据跟第2字节数据交换。
8x16点阵的图像或者字体有16行,每行有1个字节,把这个数据从原来左边是高位,右边是低位的顺序颠倒过来。
具体内容,请看源代码讲解。

(1)硬件平台:
   基于朱兆祺51单片机学习板。

(2)实现功能:开机上电后,从上往下分别显示“馒头V5”四个字以及右边镜像后的“馒头V5”四个字。
   
(3)源代码讲解如下:
#include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount); //显示任意点阵函数
void display_clear(void); // 清屏
void hz1616_mirror(const unsigned char*p_ucHz,unsigned char *p_ucResult);//把16x16点阵字库镜像
void hz816_mirror(const unsigned char*p_ucHz,unsigned char *p_ucResult);//把8x16点阵字库镜像

void delay_short(unsigned int uiDelayshort); //延时

code unsigned char Hz1616_man[]= /*馒   横向取模16X16点阵 */
{
0x21,0xF8,0x21,0x08,0x21,0xF8,0x3D,0x08,0x45,0xF8,0x48,0x00,0x83,0xFC,0x22,0x94,
0x23,0xFC,0x20,0x00,0x21,0xF8,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x00,0x00,
};

code unsigned char Hz1616_tou[]= /*头   横向取模16X16点阵 */
{
0x00,0x80,0x10,0x80,0x0C,0x80,0x04,0x80,0x10,0x80,0x0C,0x80,0x08,0x80,0x00,0x80,
0xFF,0xFE,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x30,0x08,0x18,0x10,0x0C,0x20,0x08,
};


code unsigned char Zf816_V[]= /*V   横向取模8x16点阵 */
{
0x00,0x00,0x00,0xE7,0x42,0x42,0x44,0x24,0x24,0x28,0x28,0x18,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_5[]= /*5   横向取模8x16点阵 */
{
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};


unsigned char ucBufferResult; //用于临时存放转换结束后的字模数组

void main()
{
      LCDInit(); //初始化12864 内部包含液晶模块的复位

      display_clear(); // 清屏

      display_lattice(0,0,Hz1616_man,0,2,16);//显示镜像前的<馒>字
      hz1616_mirror(Hz1616_man,ucBufferResult);//把<馒>字镜像后放到ucBufferResult临时变量里。
      display_lattice(1,0,ucBufferResult,0,2,16);//显示镜像后的<馒>字


      display_lattice(0,16,Hz1616_tou,0,2,16);//显示镜像前的<头>字
      hz1616_mirror(Hz1616_tou,ucBufferResult);//把<头>字镜像后放到ucBufferResult临时变量里。
      display_lattice(1,16,ucBufferResult,0,2,16);//显示镜像后的<头>字

      display_lattice(8,0,Zf816_V,0,1,16);//显示镜像前的<V>字符
      hz816_mirror(Zf816_V,ucBufferResult);//把<V>字符镜像后放到ucBufferResult临时变量里。
      display_lattice(9,0,ucBufferResult,0,1,16);//显示镜像后的<V>字符

      display_lattice(8,16,Zf816_5,0,1,16);//显示镜像前的<5>字符
      hz816_mirror(Zf816_5,ucBufferResult);//把<5>字符镜像后放到ucBufferResult临时变量里。
      display_lattice(9,16,ucBufferResult,0,1,16);//显示镜像后的<5>字符

      while(1)
      {
             ;
      }

}



void display_clear(void) // 清屏
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(0x00);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释一:
* 16x16点阵镜像的本质:
* 16x16点阵有16行,每行有2个字节,如果把这2个字节看成是一个16位int型数据,
* 那么就是要这个数据从原来左边是高位,右边是低位的顺序颠倒过来。本程序没有把2个字节
* 合并成一个int型数据,而是直接在一个字节数据内把高低位顺序颠倒过来,然后把第1字节数据跟第2字节数据交换。
*/
void hz1616_mirror(const unsigned char*p_ucHz,unsigned char *p_ucResult)//把16x16点阵字库镜像的函数
{
       unsigned char a;
       unsigned char b;
       unsigned char c;
       unsigned char d;
       
       for(a=0;a<16;a++) //这里16代表有16行。每一行有2个字节。把每一个字节看做一列,这里先把第1列字节的数据从原来左边是高位,右边是低位的顺序颠倒过来,相当于镜像。
       {
          b=p_ucHz;//这里的2代表16x16点阵每行有2列字节,0代表从第1列开始。
          c=0;
          for(d=0;d<8;d++)//把一个字节调换顺序
                {
             c=c>>1;
         if((b&0x80)==0x80)
                   {
             c=c|0x80;
         }
                   b=b<<1;
      }               
      p_ucResult=c;   //注意,因为是镜像,所以要把颠倒顺序后的字节从原来是第1列的调换到第2列       
   }
       
       for(a=0;a<16;a++)//这里16代表有16行。每一行有2个字节。把每一个字节看做一列,这里先把第2列字节的数据从原来左边是高位,右边是低位的顺序颠倒过来,相当于镜像。
       {
          b=p_ucHz;   //这里的2代表16x16点阵每行有2列字节,1代表从第2列开始。
               
          c=0;
          for(d=0;d<8;d++)//把一个字节调换顺序
                {
                        c=c>>1;
            if((b&0x80)==0x80)
                        {
            c=c|0x80;
            }
                        b=b<<1;
         }

         p_ucResult=c;         //注意,因为是镜像,所以要把颠倒顺序后的字节从原来是第2列的调换到第1列        

               
   }
       

}



/* 注释二:
* 8x16点阵镜像的本质:
* 8x16点阵有16行,每行有1个字节,把这个数据从原来左边是高位,右边是低位的顺序颠倒过来。
*/
void hz816_mirror(const unsigned char*p_ucHz,unsigned char *p_ucResult)//把8x16点阵字库镜像的函数
{
       unsigned char a;
       unsigned char b;
       unsigned char c;
       unsigned char d;
       
       for(a=0;a<16;a++) //这里16代表有16行。每一行有1个字节。这里先把每一行字节的数据从原来左边是高位,右边是低位的顺序颠倒过来,相当于镜像。
       {
          b=p_ucHz;//这里的1代表8x16点阵每行有1列字节,0代表从第1列开始。
          c=0;
          for(d=0;d<8;d++)//把一个字节调换顺序
                {
             c=c>>1;
         if((b&0x80)==0x80)
                   {
             c=c|0x80;
         }
                   b=b<<1;
      }               
      p_ucResult=c;   //注意,因为每一行只有一列,所以不用像16x16点阵那样把第1列跟第2列对调交换。
   }
       
}



/* 注释三:本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   WriteCommand(0x34);//关显示缓冲指令            
   WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray;
             if(ucFbFlag==1)//反白显示
               {
            ucTemp=~ucTemp;
         }
             LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}


void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


总结陈词:
    细心的网友一定会发现,这种12864液晶屏普遍有个毛病,在坐标轴x,y方向上不能完全做到以一个点阵为单位进行随心所欲的显示,比如横向的至少是一个字节8个点阵为单位,而第1,2行跟第3,4行又做不到无缝对接显示,假如我要把汉字一半显示在第2行一半显示在第3行,行不行?当然可以。但是需要我们编写额外的算法程序。这种算法程序是怎样编写的?欲知详情,请听下回分解-----在液晶屏中让字体可以跨区域无缝对接显示的算法程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-16 15:14

第七十四节:在液晶屏中让字体可以跨区域无缝对接显示的算法程序。

开场白:
细心的网友会发现,这种12864液晶屏在显示自造字库时普遍有个毛病,在坐标轴x方向上是以每16个点阵为一个单位的,如果显示两个8x16字符”V”和”5”,虽然它们的x坐标轴是相邻的,但是实际显示的效果是中间隔了8个点阵。另外,这种12864液晶屏是由上半屏和下半屏组成的,软件上的坐标体系并没有做到跟物理的坐标体系一致,需要转换的。如果我们想把一个整体字符的一半显示在上半屏,另一半显示在下半屏,那怎么办?
这一节就要教给大家这个算法程序:
为了实现跨区域无缝显示,就先在某个区域显示一块画布,我们只要在这块画布数组中插入字模数组,就可以达到跨区域无缝显示的目的。

具体内容,请看源代码讲解。

(1)硬件平台:
   基于朱兆祺51单片机学习板。

(2)实现功能:开机上电后,看到液晶屏所有的点阵都显示。正中间露出一小方块空白的32x16点阵画布,从左到右分别显示“V5”两个字符。这两个字符是紧紧挨在一起的,中间并没有8个点阵的空格,同时这两个字符的上半部分显示在上半屏,下半部分显示在下半屏。实现了真正的跨区域无缝对接显示。

(3)源代码讲解如下:
#include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
void delay_short(unsigned int uiDelayshort); //延时

code unsigned char Zf816_V[]= /*V   横向取模8x16点阵 每一行只要1个字节,共16行 */
{
0x00,
0x00,
0x00,
0xE7,
0x42,
0x42,
0x44,
0x24,
0x24,
0x28,
0x28,
0x18,
0x10,
0x10,
0x00,
0x00,
};

code unsigned char Zf816_5[]= /*5   横向取模8x16点阵 每一行只要1个字节,共16行 */
{
0x00,
0x00,
0x00,
0x7E,
0x40,
0x40,
0x40,
0x58,
0x64,
0x02,
0x02,
0x42,
0x44,
0x38,
0x00,
0x00,
};


/* 注释一:
* 为了实现跨区域无缝显示,就先在某个区域显示一块画布,我们只要在这块画布数组中插入字模数组,
* 就可以达到跨区域无缝显示的目的。根据上几节的介绍,12864液晶屏由上下两半屏组成,以下这块画布
* 显示在上半屏和下半屏之间。横向4个字节,纵向16行。其中上半屏显示8行,下半屏显示8行。注意,这个数组
* 不带code关键字,是全局变量,这样可读可写。画布的横向x坐标范围是0至3,因为画布的横向只要4个字节。
* 画布的纵向y坐标范围是0至15,因为画布的纵向只有16行。
*/
unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};



void main()
{
      LCDInit(); //初始化12864 内部包含液晶模块的复位

      display_clear(0xff); // 清屏 全部显示空填充0x00   全部显示点阵用0xff

      insert_buffer_to_canvas(0,0,Zf816_V,0,1,16);//把<V>的字模插入画布
      insert_buffer_to_canvas(1,0,Zf816_5,0,1,16);//把<5>的字模插入画布

      display_lattice(3,24,ucCanvasBuffer,0,4,8,0);   //显示上半屏的画布,最后的参数0是偏移量
      display_lattice(11,0,ucCanvasBuffer,0,4,8,32);//显示下半屏的画布,最后的参数32是偏移量


      while(1)
      {
             ;
      }

}



void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释二:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }       

}

/* 注释三:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   WriteCommand(0x34);//关显示缓冲指令            
   WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}


void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


总结陈词:
    经过这一节的算法处理后,字符终于可以在x轴上紧紧挨着显示了。也就是把原来x坐标是16个点阵为一个单位,改成了以8个点阵为一个单位。如果要求以1个点阵为单位显示,那该怎么办?这个还真有点难度,因为横向的最小显示单位就是一个字节8个点,不过鸿哥在下一节中照样有办法实现这个功能。欲知详情,请听下回分解-----在12864液晶屏中让字体以1个点阵为单位进行移动显示的算法程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-19 10:14

第七十五节:在12864液晶屏中让字体以1个点阵为单位进行移动显示的算法程序。

开场白:
    假设有一个固定的四方形透明窗口,在窗口里面放了一张画布,只要想办法让这个画布
往右边拖动,那么画布里面的内容就会跟着画布整体往右边移动,这个就是能以1个点阵为单位进行移动显示的本质。同理,这个画布有16行,每行有4个字节,我们只要把每行4个字节看作是一个首尾连接的二进制数据,把每一行的二进制数据每次整体往右边移动一位,就相当于移动一个点阵了。这一节就要把这个算法教给大家。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于朱兆祺51单片机学习板。

(2)实现功能:开机上电后,能看到正中间显示的两个字符“V5”整体以1个点阵为单位向右边慢慢移动。

(3)源代码讲解如下:
#include "REG52.H"

#define const_MoveTime 400//每移动一位后的延时时间

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
void delay_short(unsigned int uiDelayshort); //延时

void move_service(void); //整体画布移动的应用程序
void lcd_display_service(void); //应用层面的液晶屏显示程序
void move_canvas_to_one_bit(void);//把画布整体往右边移动一个点阵
void clear_all_canvas(void);//把画布全部清零

void T0_time(void);//定时中断函数

code unsigned char Zf816_V[]= /*V   横向取模8x16点阵 每一行只要1个字节,共16行 */
{
0x00,
0x00,
0x00,
0xE7,
0x42,
0x42,
0x44,
0x24,
0x24,
0x28,
0x28,
0x18,
0x10,
0x10,
0x00,
0x00,
};

code unsigned char Zf816_5[]= /*5   横向取模8x16点阵 每一行只要1个字节,共16行 */
{
0x00,
0x00,
0x00,
0x7E,
0x40,
0x40,
0x40,
0x58,
0x64,
0x02,
0x02,
0x42,
0x44,
0x38,
0x00,
0x00,
};


/* 注释一:
* 为了实现跨区域无缝显示,就先在某个区域显示一块画布,我们只要在这块画布数组中插入字模数组,
* 就可以达到跨区域无缝显示的目的。根据上几节的介绍,12864液晶屏由上下两半屏组成,以下这块画布
* 显示在上半屏和下半屏之间。横向4个字节,纵向16行。其中上半屏显示8行,下半屏显示8行。注意,这个数组
* 不带code关键字,是全局变量,这样可读可写。画布的横向x坐标范围是0至3,因为画布的横向只要4个字节。
* 画布的纵向y坐标范围是0至15,因为画布的纵向只有16行。
*/
unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};

unsigned char ucDisplayUpdate=1;//更新显示变量
unsigned char ucMoveStepReset=0;//这个变量是为了方便外部程序初始化应用程序内部后缀为step的步骤变量

unsigned char ucMoveTimeStart=0; //定时器的开关标志也相当于原子锁或互斥量的功能
unsigned int uiMoveTime=0;//定时器累计时间

void main()
{

      LCDInit(); //初始化12864 内部包含液晶模块的复位
      display_clear(0xff); // 清屏 全部显示空填充0x00   全部显示点阵用0xff


      TMOD=0x01;//设置定时器0为工作方式1
      TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
      TL0=0x2f;
      EA=1;   //开总中断
      ET0=1;    //允许定时中断
      TR0=1;    //启动定时中断


      while(1)
      {
         move_service(); //整体画布移动的应用程序
         lcd_display_service(); //应用层面的液晶屏显示程序
      }

}



void move_service(void) //整体画布移动的应用程序
{
   static unsigned char ucMoveStep=0; //运行步骤。前面加关键字static表示上电后这个变量只初始化一次,以后每次进出函数此变量不会重新初始化,保存之前的更改数值不变。
   static unsigned char ucMoveCnt=0; //统计当前已经往左边移动了多少位。关键字static表示此变量上电后只初始化一次,不会每次进入函数都初始化。

   if(ucMoveStepReset==1)//运行步骤的复位标志,此段代码结构方便外部程序初始化函数内部的步骤变量ucMoveStep
   {
      ucMoveStepReset=0; //及时把复位标志清零。避免一直处于复位的状态、

          ucMoveStep=0; //运行步骤变量被外部程序通过复位标志初始化。
   }

   switch(ucMoveStep)
   {
      case 0:
             clear_all_canvas();//把画布全部清零
         insert_buffer_to_canvas(0,0,Zf816_V,0,1,16);//把<V>的字模插入画布
         insert_buffer_to_canvas(1,0,Zf816_5,0,1,16);//把<5>的字模插入画布
         ucDisplayUpdate=1; //更新液晶屏显示
                  
                   uiMoveTime=0;//定时器清零
                   ucMoveTimeStart=1; //开定时器   也相当于原子锁或互斥量的功能
                   ucMoveCnt=0; //统计当前已经往左边移动了多少位
                   ucMoveStep=1; //切换到下一个运行步骤

             break;

      case 1:
             if(uiMoveTime>const_MoveTime)//延时一定的时间后
                   {
                              ucMoveTimeStart=0; //关定时器    也相当于原子锁或互斥量的功能
                     uiMoveTime=0;//定时器清零

                   if(ucMoveCnt<16)
                     {
                        ucMoveCnt++;
                  move_canvas_to_one_bit(); //把画布整体往左边移动一个点阵
                  ucDisplayUpdate=1; //更新液晶屏显示
                                  ucMoveTimeStart=1; //开定时器   也相当于原子锁或互斥量的功能

                     }
                     else
                     {
                                ucMoveStep=0; //移动了16个点阵后,返回上一个运行步骤,把字模重新插入画布
                     }



         }
             break;
   }

}


void lcd_display_service(void) //应用层面的液晶屏显示程序
{
    if(ucDisplayUpdate==1)//需要更新显示
    {
       ucDisplayUpdate=0;//及时把标志清零,避免一直处于不断更新的状态。


       display_lattice(3,24,ucCanvasBuffer,0,4,8,0);   //显示上半屏的画布,最后的参数0是偏移量
       display_lattice(11,0,ucCanvasBuffer,0,4,8,32);//显示下半屏的画布,最后的参数32是偏移量
    }
}

/* 注释二:
* 假设有一个固定的四方形透明窗口,在窗口里面放了一张画布,只要想办法让这个画布
* 往右边拖动,那么画布里面的内容就会跟着画布整体往右边移动,这个就是能以1个点阵为单位进行移动显示的本质。
* 同理,这个画布有16行,每行有4个字节,我们只要把每行4个字节看作是一个首尾连接的二进制数据,
* 把每一行的二进制数据每次整体往右边移动一位,就相当于移动一个点阵了。
*/

void move_canvas_to_one_bit(void)//把画布整体往右边移动一个点阵
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucBitH;//临时保存一个字节中的最高位
   unsigned char ucBitL;//临时保存一个字节中的最低位

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      ucBitH=0;   
          ucBitL=0;   
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  if((ucCanvasBuffer&0x01)==0x01)//临时保存一个字节中的最低位
                  {
                     ucBitL=1;
                  }
                  else
                  {
                     ucBitL=0;
                  }
                  ucCanvasBuffer=ucCanvasBuffer>>1;//一行中的一个字节右移一位

                  if(ucBitH==1)   //原来左边相邻的字节最低位移动到了当前字节的最高位
                  {
             ucCanvasBuffer=ucCanvasBuffer|0x80; //把最高位补上
                  }
          ucBitH=ucBitL;//把当前的最低位赋值给最高位,为下一个相邻字节做准备。
      }
   }         

}


void clear_all_canvas(void)//把画布全部清零
{
   unsigned int j=0;
   unsigned int i=0;

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  ucCanvasBuffer=0x00;
      }
   }         

}


void T0_time(void) interrupt 1//定时中断函数
{
TF0=0;//清除中断标志
TR0=0; //关中断

if(ucMoveTimeStart==1) //已经开了定时器也相当于原子锁或互斥量的功能
{
      uiMoveTime++; //定时器累加计时开始
}

TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
TL0=0x2f;
TR0=1;//开中断
}



void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释三:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }         

}

/* 注释四:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;

//注意,要把以下两行指令屏蔽,否则屏幕在更新显示时会整屏闪动
//WriteCommand(0x34);//关显示缓冲指令            
//WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}




void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}

总结陈词:
从下一节开始讲大家关注已久的液晶屏菜单程序。欲知详情,请听下回分解-----在1个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

(未完待续,下节更精彩,不要走开哦)

qiang7260 发表于 2014-10-19 15:03

辛苦了!楼主。:)

jianhong_wu 发表于 2014-10-23 11:52

第七十六节:如何把一个任意数值的变量显示在液晶屏上。

开场白:
本来这一节打算开始讲液晶屏的菜单程序,但是我担心跳跃太大,恐怕很多初学者跟不上,所以多插入这一节讲讲后面菜单程序中经常用到的基本功能,如何把一个任意数值的变量显示在液晶屏上。我们需要做一个变量转换成字模的函数,以后只要调用这个转换函数就可以了。这一节就要把这个转换函数教给大家。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于朱兆祺51单片机学习板。

(2)实现功能:我们定义一个char型的全局变量,把它默认初始化为218,开机上电后,能看到正中间恰好显示这个全局变量的数值218。大家也可以试着更改它的默认初始值,只要不超过char型最大数值255范围,我们就会看到它上电后显示的就是这个初始值。

(3)源代码讲解如下:
#include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
unsigned char *number_to_matrix(unsigned charucBitNumber); //把一位数字转换成字模首地址的函数
void delay_short(unsigned int uiDelayshort); //延时
void delay_long(unsigned int uiDelayLong);


void initial_myself();   
void initial_peripheral();


void lcd_display_service(void); //应用层面的液晶屏显示程序
void clear_all_canvas(void);//把画布全部清零

code unsigned char Zf816_0[]=
{
/*--文字:0--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};

code unsigned char Zf816_1[]=
{
/*--文字:1--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x10,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,
};

code unsigned char Zf816_2[]=
{
/*--文字:2--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x04,0x04,0x08,0x10,0x20,0x42,0x7E,0x00,0x00,
};

code unsigned char Zf816_3[]=
{
/*--文字:3--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x04,0x18,0x04,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_4[]=
{
/*--文字:4--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x04,0x0C,0x14,0x24,0x24,0x44,0x44,0x7E,0x04,0x04,0x1E,0x00,0x00,
};

code unsigned char Zf816_5[]=
{
/*--文字:5--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_6[]=
{
/*--文字:6--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x1C,0x24,0x40,0x40,0x58,0x64,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};


code unsigned char Zf816_7[]=
{
/*--文字:7--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x44,0x44,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_8[]=
{
/*--文字:8--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x24,0x18,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,
};

code unsigned char Zf816_9[]=
{
/*--文字:9--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x26,0x1A,0x02,0x02,0x24,0x38,0x00,0x00,
};


code unsigned char Zf816_nc[]=//空字模
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};


/* 注释一:
* 为了实现跨区域无缝显示,就先在某个区域显示一块画布,我们只要在这块画布数组中插入字模数组,
* 就可以达到跨区域无缝显示的目的。根据上几节的介绍,12864液晶屏由上下两半屏组成,以下这块画布
* 显示在上半屏和下半屏之间。横向4个字节,纵向16行。其中上半屏显示8行,下半屏显示8行。注意,这个数组
* 不带code关键字,是全局变量,这样可读可写。画布的横向x坐标范围是0至3,因为画布的横向只要4个字节。
* 画布的纵向y坐标范围是0至15,因为画布的纵向只有16行。
*/
unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};


unsigned char ucDisplayUpdate=1;//更新显示变量


/* 注释二:
* 以下变量就是本程序的任意变量,网友可以自己更改它的大小来测试本程序,不要超过255.
*/
unsigned char ucAnyNumber=218;//任意变量默认初始化为218。


void main()
{
      initial_myself();      //第一区,上电后马上初始化
      delay_long(100);       //一线,延时线。延时一段时间
      initial_peripheral();//第二区,上电后延时一段时间再初始化

      while(1)   //第三区
      {
            lcd_display_service(); //应用层面的液晶屏显示程序
      }

}


void initial_myself()//第一区 上电后马上初始化
{
    ;
}
void initial_peripheral() //第二区 上电后延时一段时间再初始化
{
    LCDInit(); //初始化12864 内部包含液晶模块的复位
    display_clear(0xff); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
}



/* 注释三:
* 本程序的核心转换函数。
* 是可以把一位任意数字变量的函数转换成对应的字模,由于字模是数组,所以返回的是指针,代表字模数组的首地址。
*/
unsigned char *number_to_matrix(unsigned charucBitNumber)
{
    unsigned char *p_ucAnyNumber;//此指针根据ucBitNumber数值的大小,分别调用不同的字库。

        switch(ucBitNumber)//根据ucBitNumber数值的大小,分别调用不同的字库。
        {
          case 0:
             p_ucAnyNumber=Zf816_0;
                     break;
          case 1:
             p_ucAnyNumber=Zf816_1;
                     break;
          case 2:
             p_ucAnyNumber=Zf816_2;
                     break;
          case 3:
             p_ucAnyNumber=Zf816_3;
                     break;
          case 4:
             p_ucAnyNumber=Zf816_4;
                     break;
          case 5:
             p_ucAnyNumber=Zf816_5;
                     break;
          case 6:
             p_ucAnyNumber=Zf816_6;
                     break;
          case 7:
             p_ucAnyNumber=Zf816_7;
                     break;
          case 8:
             p_ucAnyNumber=Zf816_8;
                     break;
          case 9:
             p_ucAnyNumber=Zf816_9;
                     break;
          case 10:
             p_ucAnyNumber=Zf816_nc;
                     break;
                default:   //如果上面的条件都不符合,那么默认指向空字模
             p_ucAnyNumber=Zf816_nc;
                     break;
        }

    return p_ucAnyNumber;//返回转换结束后的指针
}


void lcd_display_service(void) //应用层面的液晶屏显示程序
{
    static unsigned char ucAnyNumber_1; //分解变量的个位
    static unsigned char ucAnyNumber_10; //分解变量的十位
    static unsigned char ucAnyNumber_100; //分解变量的百位

    static unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    static unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址
    static unsigned char *p_ucAnyNumber_100; //经过数字转换成字模后,分解变量的百位字模首地址

    if(ucDisplayUpdate==1)//需要更新显示
    {
       ucDisplayUpdate=0;//及时把标志清零,避免一直处于不断更新的状态。

           if(ucAnyNumber>=100) //有3位数以上
           {
         ucAnyNumber_100=ucAnyNumber/100; //百位
       }
           else //否则显示空
           {
             ucAnyNumber_100=10;//在下面的转换函数中,代码10表示空字模
           }

           if(ucAnyNumber>=10) //有2位数以上
           {
         ucAnyNumber_10=ucAnyNumber%100/10;//十位
       }
           else //否则显示空
           {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
           }

       ucAnyNumber_1=ucAnyNumber%10/1;//个位

           p_ucAnyNumber_100=number_to_matrix(ucAnyNumber_100); //把数字转换成字模首地址      
           p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
           p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址

       clear_all_canvas();//把画布全部清零
       insert_buffer_to_canvas(0,0,p_ucAnyNumber_100,0,1,16);//把百位的字模插入画布
       insert_buffer_to_canvas(1,0,p_ucAnyNumber_10,0,1,16);//把十的字模插入画布
       insert_buffer_to_canvas(2,0,p_ucAnyNumber_1,0,1,16);//把个的字模插入画布

       display_lattice(3,24,ucCanvasBuffer,0,4,8,0);   //显示上半屏的画布,最后的参数0是偏移量
       display_lattice(11,0,ucCanvasBuffer,0,4,8,32);//显示下半屏的画布,最后的参数32是偏移量
    }
}



void clear_all_canvas(void)//把画布全部清零
{
   unsigned int j=0;
   unsigned int i=0;

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  ucCanvasBuffer=0x00;
      }
   }         

}





void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释四:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }         

}

/* 注释五:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;

//注意,要把以下两行指令屏蔽,否则屏幕在更新显示时会整屏闪动
//WriteCommand(0x34);//关显示缓冲指令            
//WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}




void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

总结陈词:
有了这一节的基础,我们继续循序渐进,下一节将会讲到液晶屏的菜单程序。欲知详情,请听下回分解-----在1个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-26 10:01

第七十七节:在1个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

开场白:
    这一节要教会大家两个知识点:
第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
   通过按键设置4个不同的参数。
    有1个窗口。每个窗口显示4个参数。每个参数的范围是从0到99。
   有4个按键:
(a)        一个是设置参数S13按键,按下此按键,液晶屏的第一行会出现反显的光标,表示进入设置参数模式,再次按下此按键,反显光标会消失,表示退出设置参数模式。
(b)        一个是移动光标S9按键,在进入设置参数的模式下,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。

(3)源代码讲解如下:
#include "REG52.H"


#define const_voice_short40   //蜂鸣器短叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
unsigned char *number_to_matrix(unsigned charucBitNumber); //把一位数字转换成字模首地址的函数
void delay_short(unsigned int uiDelayshort); //延时
void delay_long(unsigned int uiDelayLong);

void T0_time(); //定时中断函数
void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void initial_myself();   
void initial_peripheral();


void lcd_display_service(void); //应用层面的液晶屏显示程序
void clear_all_canvas(void);//把画布全部清零

code unsigned char Zf816_0[]=
{
/*--文字:0--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};

code unsigned char Zf816_1[]=
{
/*--文字:1--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x10,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,
};

code unsigned char Zf816_2[]=
{
/*--文字:2--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x04,0x04,0x08,0x10,0x20,0x42,0x7E,0x00,0x00,
};

code unsigned char Zf816_3[]=
{
/*--文字:3--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x04,0x18,0x04,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_4[]=
{
/*--文字:4--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x04,0x0C,0x14,0x24,0x24,0x44,0x44,0x7E,0x04,0x04,0x1E,0x00,0x00,
};

code unsigned char Zf816_5[]=
{
/*--文字:5--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_6[]=
{
/*--文字:6--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x1C,0x24,0x40,0x40,0x58,0x64,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};


code unsigned char Zf816_7[]=
{
/*--文字:7--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x44,0x44,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_8[]=
{
/*--文字:8--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x24,0x18,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,
};

code unsigned char Zf816_9[]=
{
/*--文字:9--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x26,0x1A,0x02,0x02,0x24,0x38,0x00,0x00,
};


code unsigned char Zf816_nc[]=//空字模
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Zf816_mao_hao[]=//冒号
{
/*--文字::--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,
};

code unsigned char Hz1616_yi[]=
{
/*--文字:一--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x7F,0xFE,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_er[]=
{
/*--文字:二--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x10,0x3F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x7F,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_san[]=
{
/*--文字:三--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x7F,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0xF8,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7F,0xFE,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_si[]=
{
/*--文字:四--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x7F,0xFC,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,
0x48,0x84,0x48,0x7C,0x50,0x04,0x60,0x04,0x40,0x04,0x7F,0xFC,0x40,0x04,0x00,0x00,
};

code unsigned char Hz1616_chuang[]=
{
/*--文字:窗--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x01,0x00,0x00,0x80,0x7F,0xFE,0x40,0x22,0x09,0x18,0x12,0x06,0x7F,0xF8,0x11,0x08,
0x13,0xE8,0x14,0x48,0x1A,0x88,0x11,0x08,0x12,0x88,0x14,0x08,0x1F,0xF8,0x10,0x08,
};

code unsigned char Hz1616_kou[]=
{
/*--文字:口--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x3F,0xF8,0x20,0x08,0x20,0x08,0x20,0x08,0x20,0x08,0x20,0x08,
0x20,0x08,0x20,0x08,0x20,0x08,0x3F,0xF8,0x20,0x08,0x20,0x08,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_hang[]=
{
/*--文字:行--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x08,0x00,0x1C,0x00,0x31,0xFC,0x40,0x00,0x88,0x00,0x0C,0x00,0x1B,0xFE,0x30,0x20,
0x50,0x20,0x90,0x20,0x10,0x20,0x10,0x20,0x10,0x20,0x10,0x20,0x10,0xA0,0x10,0x40,
};


unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};



unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器


unsigned char ucWd=1; //窗口变量
unsigned char ucPart=0; //局部变量 0代表没有选中任何一行,其它数值1到4代表选中某一行


unsigned char ucWd1Update=1; //窗口1的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part1Update=0; //窗口1的第1行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part2Update=0; //窗口1的第2行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part3Update=0; //窗口1的第3行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part4Update=0; //窗口1的第4行局部更新显示变量1代表更新显示,响应函数内部会清零



unsigned char ucData_1_1=8;//第1个窗口第1行的被设置数据
unsigned char ucData_1_2=9;//第1个窗口第2行的被设置数据
unsigned char ucData_1_3=10;//第1个窗口第3行的被设置数据
unsigned char ucData_1_4=11;//第1个窗口第4行的被设置数据

void main()
{
      initial_myself();      //第一区,上电后马上初始化
      delay_long(100);       //一线,延时线。延时一段时间
      initial_peripheral();//第二区,上电后延时一段时间再初始化

      while(1)   //第三区
      {
                  key_service(); //按键服务的应用程序
            lcd_display_service(); //应用层面的液晶屏显示程序
      }

}


void initial_myself()//第一区 上电后马上初始化
{
/* 注释一:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
   beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

   TMOD=0x01;//设置定时器0为工作方式1

   TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
   TL0=0x2f;
}
void initial_peripheral() //第二区 上电后延时一段时间再初始化
{
    LCDInit(); //初始化12864 内部包含液晶模块的复位


    EA=1;   //开总中断
    ET0=1;    //允许定时中断
    TR0=1;    //启动定时中断

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
         beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
         beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
TL0=0x2f;
TR0=1;//开中断
}



void key_scan(void)//按键扫描函数 放在定时中断里
{


static unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
static unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
static unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
static unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
static unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}

}


void key_service(void) //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_1_1++;
                                                                if(ucData_1_1>99)
                                                                {
                                                                   ucData_1_1=99;
                                                                }
                              ucWd1Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_1_2++;
                                                                if(ucData_1_2>99)
                                                                {
                                                                   ucData_1_2=99;
                                                                }
                              ucWd1Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_1_3++;
                                                                if(ucData_1_3>99)
                                                                {
                                                                   ucData_1_3=99;
                                                                }
                              ucWd1Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_1_4++;
                                                                if(ucData_1_4>99)
                                                                {
                                                                   ucData_1_4=99;
                                                                }
                              ucWd1Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;
         
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_1_1--;
                                                                if(ucData_1_1>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                                                                {
                                                                   ucData_1_1=0;
                                                                }
                              ucWd1Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_1_2--;
                                                                if(ucData_1_2>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                                                                {
                                                                   ucData_1_2=0;
                                                                }
                              ucWd1Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_1_3--;
                                                                if(ucData_1_3>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                                                                {
                                                                   ucData_1_3=0;
                                                                }
                              ucWd1Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_1_4--;
                                                                if(ucData_1_4>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                                                                {
                                                                   ucData_1_4=0;
                                                                }
                              ucWd1Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;
         
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 切换"光标"移动按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucPart=2; //光标切换到下一行
                              ucWd1Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //设置第2行参数
                              ucPart=3; //光标切换到下一行
                              ucWd1Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part3Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 3:   //设置第3行参数
                              ucPart=4; //光标切换到下一行
                              ucWd1Part3Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part4Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 4:   //设置第4行参数
                              ucPart=1; //光标返回到最上面第一行
                              ucWd1Part4Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part1Update=1; //更新显示最上面第一行,    目的是更新反显光标的状态
                              break;


                   }
                   break;
         
          }         
      
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
   
    case 4: // 设置按键对应朱兆祺学习板的S13键,按一次进入设置状态,出现反显光标。再按一次推出设置状态,消除反显光标
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1:
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {

                        case 0:   //无光标显示的状态
                              ucPart=1; //光标显示第一行,进入设置模式
                              ucWd1Part1Update=1; //更新显示
                              break;
                        case 1:   //设置第1行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part1Update=1; //更新显示
                              break;
                        case 2:   //设置第2行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part2Update=1; //更新显示
                              break;
                        case 3:   //设置第3行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part3Update=1; //更新显示
                              break;
                        case 4:   //设置第4行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part4Update=1; //更新显示
                              break;


                   }
                   break;
         
          }   

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         

}               
}


unsigned char *number_to_matrix(unsigned charucBitNumber)
{
    unsigned char *p_ucAnyNumber;//此指针根据ucBitNumber数值的大小,分别调用不同的字库。

      switch(ucBitNumber)//根据ucBitNumber数值的大小,分别调用不同的字库。
      {
            case 0:
             p_ucAnyNumber=Zf816_0;
                     break;
            case 1:
             p_ucAnyNumber=Zf816_1;
                     break;
            case 2:
             p_ucAnyNumber=Zf816_2;
                     break;
            case 3:
             p_ucAnyNumber=Zf816_3;
                     break;
            case 4:
             p_ucAnyNumber=Zf816_4;
                     break;
            case 5:
             p_ucAnyNumber=Zf816_5;
                     break;
            case 6:
             p_ucAnyNumber=Zf816_6;
                     break;
            case 7:
             p_ucAnyNumber=Zf816_7;
                     break;
            case 8:
             p_ucAnyNumber=Zf816_8;
                     break;
            case 9:
             p_ucAnyNumber=Zf816_9;
                     break;
            case 10:
             p_ucAnyNumber=Zf816_nc;
                     break;
                default:   //如果上面的条件都不符合,那么默认指向空字模
             p_ucAnyNumber=Zf816_nc;
                     break;
      }

    return p_ucAnyNumber;//返回转换结束后的指针
}



void lcd_display_service(void) //应用层面的液晶屏显示程序
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

        unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

    switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
    {
      case 1:   //显示窗口1的数据

/* 注释二:
* 把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候
* 才需要更新,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要
* 刷新显示的内容,这种内容放在局部更新显示的括号里。
*/
                      if(ucWd1Update==1)//窗口1整屏更新,里面只放那些不用经常刷新显示的内容
                          {
                             ucWd1Update=0;//及时清零,避免一直更新

               ucWd1Part1Update=1; //激活窗口1的第1行局部更新显示变量,这里在前面数码管显示框架上有所改进
               ucWd1Part2Update=1; //激活窗口1的第2行局部更新显示变量,这里在前面数码管显示框架上有所改进
               ucWd1Part3Update=1; //激活窗口1的第3行局部更新显示变量,这里在前面数码管显示框架上有所改进
               ucWd1Part4Update=1; //激活窗口1的第4行局部更新显示变量,这里在前面数码管显示框架上有所改进

               display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
               clear_all_canvas();//把画布全部清零
               insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布

               display_lattice(0,0,Hz1616_yi,0,2,16,0);    //一窗口一行,这些内容不用经常更新,只有在切换窗口的时候才更新显示
               display_lattice(1,0,Hz1616_chuang,0,2,16,0);   
               display_lattice(2,0,Hz1616_kou,0,2,16,0);   
               display_lattice(3,0,Hz1616_yi,0,2,16,0);
               display_lattice(4,0,Hz1616_hang,0,2,16,0);

               display_lattice(0,16,Hz1616_yi,0,2,16,0);    //一窗口二行
               display_lattice(1,16,Hz1616_chuang,0,2,16,0);   
               display_lattice(2,16,Hz1616_kou,0,2,16,0);   
               display_lattice(3,16,Hz1616_er,0,2,16,0);
               display_lattice(4,16,Hz1616_hang,0,2,16,0);

               display_lattice(8,0,Hz1616_yi,0,2,16,0);    //一窗口三行
               display_lattice(9,0,Hz1616_chuang,0,2,16,0);   
               display_lattice(10,0,Hz1616_kou,0,2,16,0);   
               display_lattice(11,0,Hz1616_san,0,2,16,0);
               display_lattice(12,0,Hz1616_hang,0,2,16,0);

               display_lattice(8,16,Hz1616_yi,0,2,16,0);    //一窗口四行
               display_lattice(9,16,Hz1616_chuang,0,2,16,0);   
               display_lattice(10,16,Hz1616_kou,0,2,16,0);   
               display_lattice(11,16,Hz1616_si,0,2,16,0);
               display_lattice(12,16,Hz1616_hang,0,2,16,0);

                          }

/* 注释三:
* 注意!我前面讲数码管显示的时候有一句话讲错了,我那时说<局部更新应该写在整屏更新之前>,这是不对的。
* 按照现在的显示程序框架<即整屏显示更新括号里包含了所有局部变量的激活>,应该是<整屏更新应该写在局部更新之前>
* 这样才对。
*/
                          if(ucWd1Part1Update==1) //窗口1的第1行局部更新显示变量,里面放一些经常需要刷新显示的内容
                          {
                             ucWd1Part1Update=0; //及时清零,避免一直更新

                   if(ucPart==1) //被选中
                               {
                                  ucCursorFlag=1; //反显 显示
                               }
                   else //没被选中
                               {
                                  ucCursorFlag=0; //正常 显示
                               }

               if(ucData_1_1>=10) //有2位数以上
               {
                  ucAnyNumber_10=ucData_1_1/10;//十位
               }
               else //否则显示空
               {
                  ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
               }

               ucAnyNumber_1=ucData_1_1%10/1;//个位

   
               p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
               p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


               insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
               insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

               display_lattice(5,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                          
                          }

                          if(ucWd1Part2Update==1) //窗口1的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
                          {
                             ucWd1Part2Update=0; //及时清零,避免一直更新

                   if(ucPart==2) //被选中
                               {
                                  ucCursorFlag=1; //反显 显示
                               }
                   else //没被选中
                               {
                                  ucCursorFlag=0; //正常 显示
                               }

               if(ucData_1_2>=10) //有2位数以上
               {
                  ucAnyNumber_10=ucData_1_2/10;//十位
               }
               else //否则显示空
               {
                  ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
               }

               ucAnyNumber_1=ucData_1_2%10/1;//个位

   
               p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
               p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


               insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
               insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

               display_lattice(5,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                          
                          }

                          if(ucWd1Part3Update==1) //窗口1的第3行局部更新显示变量,里面放一些经常需要刷新显示的内容
                          {
                             ucWd1Part3Update=0; //及时清零,避免一直更新

                   if(ucPart==3) //被选中
                               {
                                  ucCursorFlag=1; //反显 显示
                               }
                   else //没被选中
                               {
                                  ucCursorFlag=0; //正常 显示
                               }

               if(ucData_1_3>=10) //有2位数以上
               {
                  ucAnyNumber_10=ucData_1_3/10;//十位
               }
               else //否则显示空
               {
                  ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
               }

               ucAnyNumber_1=ucData_1_3%10/1;//个位

   
               p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
               p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


               insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
               insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

               display_lattice(13,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                          
                          }

                          if(ucWd1Part4Update==1) //窗口1的第4行局部更新显示变量,里面放一些经常需要刷新显示的内容
                          {
                             ucWd1Part4Update=0; //及时清零,避免一直更新

                   if(ucPart==4) //被选中
                               {
                                  ucCursorFlag=1; //反显 显示
                               }
                   else //没被选中
                               {
                                  ucCursorFlag=0; //正常 显示
                               }

               if(ucData_1_4>=10) //有2位数以上
               {
                  ucAnyNumber_10=ucData_1_4/10;//十位
               }
               else //否则显示空
               {
                  ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
               }

               ucAnyNumber_1=ucData_1_4%10/1;//个位

   
               p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
               p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


               insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
               insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

               display_lattice(13,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量                          
                          }

            break;
      //本程序只有1个窗口,所以只有一个case 1,如果要增加窗口,就直接增加 case 2, case 3...       
    }


}



void clear_all_canvas(void)//把画布全部清零
{
   unsigned int j=0;
   unsigned int i=0;

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  ucCanvasBuffer=0x00;
      }
   }         

}





void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释四:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }         

}

/* 注释五:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;

//注意,要把以下两行指令屏蔽,否则屏幕在更新显示时会整屏闪动
//WriteCommand(0x34);//关显示缓冲指令            
//WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}




void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

总结陈词:
这一节讲了在一个窗口里设置不同的参数,如果有几个窗口的情况下,该如何编程?欲知详情,请听下回分解-----在多个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-10-28 15:29

第七十八节:在多个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

开场白:
上一节讲了1个窗口下如何设置参数的菜单程序,这一节多增加1个窗口变成2个窗口,看看它们两个窗口之间是如何通过按键程序进行切换的。继续巩固上一节教给大家的两个知识点:
   第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
    第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
   通过按键设置8个不同的参数。
    有2个窗口。每个窗口显示4个参数。每个参数的范围是从0到99。
    有4个按键:
(a)        一个是设置参数S13按键,按下此按键,液晶屏的第1个窗口第一行会出现反显的光标,表示进入设置参数模式,再次按下此按键,反显光标会消失,并且强行切换到第1个窗口,表示退出设置参数模式。
(b)        一个是移动光标S9按键,在进入设置参数的模式下,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。当移动到每个窗口最下边那一行时,再按下此按键会进行切换窗口的操作。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。
(3)源代码讲解如下:
#include "REG52.H"

/* 注释一:
* 本程序用到的变量比较多,所以在keil编译模式里要设置一下编译模式memory model,
* 否则编译会出错.右键单击Target选择“Options for Target'Target1'”就会出来一个框
* 在memory model中选择compact:variables in pdata 就可以了。
*/

#define const_voice_short40   //蜂鸣器短叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
unsigned char *number_to_matrix(unsigned charucBitNumber); //把一位数字转换成字模首地址的函数
void delay_short(unsigned int uiDelayshort); //延时
void delay_long(unsigned int uiDelayLong);

void T0_time(); //定时中断函数
void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void initial_myself();   
void initial_peripheral();


void lcd_display_service(void); //应用层面的液晶屏显示程序
void clear_all_canvas(void);//把画布全部清零

void wd1(void);//窗口1显示的内容
void wd2(void);//窗口2显示的内容

code unsigned char Zf816_0[]=
{
/*--文字:0--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};

code unsigned char Zf816_1[]=
{
/*--文字:1--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x10,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,
};

code unsigned char Zf816_2[]=
{
/*--文字:2--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x04,0x04,0x08,0x10,0x20,0x42,0x7E,0x00,0x00,
};

code unsigned char Zf816_3[]=
{
/*--文字:3--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x04,0x18,0x04,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_4[]=
{
/*--文字:4--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x04,0x0C,0x14,0x24,0x24,0x44,0x44,0x7E,0x04,0x04,0x1E,0x00,0x00,
};

code unsigned char Zf816_5[]=
{
/*--文字:5--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_6[]=
{
/*--文字:6--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x1C,0x24,0x40,0x40,0x58,0x64,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};


code unsigned char Zf816_7[]=
{
/*--文字:7--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x44,0x44,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_8[]=
{
/*--文字:8--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x24,0x18,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,
};

code unsigned char Zf816_9[]=
{
/*--文字:9--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x26,0x1A,0x02,0x02,0x24,0x38,0x00,0x00,
};


code unsigned char Zf816_nc[]=//空字模
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Zf816_mao_hao[]=//冒号
{
/*--文字::--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,
};

code unsigned char Hz1616_yi[]=
{
/*--文字:一--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x7F,0xFE,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_er[]=
{
/*--文字:二--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x10,0x3F,0xF8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x7F,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_san[]=
{
/*--文字:三--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x7F,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3F,0xF8,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7F,0xFE,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_si[]=
{
/*--文字:四--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x7F,0xFC,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,0x44,0x84,
0x48,0x84,0x48,0x7C,0x50,0x04,0x60,0x04,0x40,0x04,0x7F,0xFC,0x40,0x04,0x00,0x00,
};

code unsigned char Hz1616_chuang[]=
{
/*--文字:窗--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x01,0x00,0x00,0x80,0x7F,0xFE,0x40,0x22,0x09,0x18,0x12,0x06,0x7F,0xF8,0x11,0x08,
0x13,0xE8,0x14,0x48,0x1A,0x88,0x11,0x08,0x12,0x88,0x14,0x08,0x1F,0xF8,0x10,0x08,
};

code unsigned char Hz1616_kou[]=
{
/*--文字:口--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x00,0x00,0x3F,0xF8,0x20,0x08,0x20,0x08,0x20,0x08,0x20,0x08,0x20,0x08,
0x20,0x08,0x20,0x08,0x20,0x08,0x3F,0xF8,0x20,0x08,0x20,0x08,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_hang[]=
{
/*--文字:行--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x08,0x00,0x1C,0x00,0x31,0xFC,0x40,0x00,0x88,0x00,0x0C,0x00,0x1B,0xFE,0x30,0x20,
0x50,0x20,0x90,0x20,0x10,0x20,0x10,0x20,0x10,0x20,0x10,0x20,0x10,0xA0,0x10,0x40,
};


unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};



unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器


unsigned char ucWd=1; //窗口变量
unsigned char ucPart=0; //局部变量 0代表没有选中任何一行,其它数值1到4代表选中某一行


unsigned char ucWd1Update=1; //窗口1的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part1Update=0; //窗口1的第1行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part2Update=0; //窗口1的第2行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part3Update=0; //窗口1的第3行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part4Update=0; //窗口1的第4行局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucWd2Update=0; //窗口2的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part1Update=0; //窗口2的第1行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part2Update=0; //窗口2的第2行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part3Update=0; //窗口2的第3行局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part4Update=0; //窗口2的第4行局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucData_1_1=8;//第1个窗口第1行的被设置数据
unsigned char ucData_1_2=9;//第1个窗口第2行的被设置数据
unsigned char ucData_1_3=10;//第1个窗口第3行的被设置数据
unsigned char ucData_1_4=11;//第1个窗口第4行的被设置数据

unsigned char ucData_2_1=12;//第2个窗口第1行的被设置数据
unsigned char ucData_2_2=13;//第2个窗口第2行的被设置数据
unsigned char ucData_2_3=14;//第2个窗口第3行的被设置数据
unsigned char ucData_2_4=15;//第2个窗口第4行的被设置数据

void main()
{
      initial_myself();      //第一区,上电后马上初始化
      delay_long(100);       //一线,延时线。延时一段时间
      initial_peripheral();//第二区,上电后延时一段时间再初始化

      while(1)   //第三区
      {
                  key_service(); //按键服务的应用程序
            lcd_display_service(); //应用层面的液晶屏显示程序
      }

}


void initial_myself()//第一区 上电后马上初始化
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
   beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

   TMOD=0x01;//设置定时器0为工作方式1

   TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
   TL0=0x2f;
}
void initial_peripheral() //第二区 上电后延时一段时间再初始化
{
    LCDInit(); //初始化12864 内部包含液晶模块的复位


    EA=1;   //开总中断
    ET0=1;    //允许定时中断
    TR0=1;    //启动定时中断

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
         beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
         beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
TL0=0x2f;
TR0=1;//开中断
}



void key_scan(void)//按键扫描函数 放在定时中断里
{


static unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
static unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
static unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
static unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
static unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}

}


void key_service(void) //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://窗口1
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_1_1++;
                              if(ucData_1_1>99)
                              {
                                 ucData_1_1=99;
                              }
                              ucWd1Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_1_2++;
                              if(ucData_1_2>99)
                              {
                                 ucData_1_2=99;
                              }
                              ucWd1Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_1_3++;
                              if(ucData_1_3>99)
                              {
                                 ucData_1_3=99;
                              }
                              ucWd1Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_1_4++;
                              if(ucData_1_4>99)
                              {
                                 ucData_1_4=99;
                              }
                              ucWd1Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;
            case 2://窗口2
                   switch(ucPart)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_2_1++;
                              if(ucData_2_1>99)
                              {
                                 ucData_2_1=99;
                              }
                              ucWd2Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_2_2++;
                              if(ucData_2_2>99)
                              {
                                 ucData_2_2=99;
                              }
                              ucWd2Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_2_3++;
                              if(ucData_2_3>99)
                              {
                                 ucData_2_3=99;
                              }
                              ucWd2Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_2_4++;
                              if(ucData_2_4>99)
                              {
                                 ucData_2_4=99;
                              }
                              ucWd2Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;         
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://窗口1
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_1_1--;
                              if(ucData_1_1>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_1_1=0;
                              }
                              ucWd1Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_1_2--;
                              if(ucData_1_2>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_1_2=0;
                              }
                              ucWd1Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_1_3--;
                              if(ucData_1_3>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_1_3=0;
                              }
                              ucWd1Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_1_4--;
                              if(ucData_1_4>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_1_4=0;
                              }
                              ucWd1Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;
            case 2://窗口2
                   switch(ucPart)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucData_2_1--;
                              if(ucData_2_1>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_2_1=0;
                              }
                              ucWd2Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置第2行参数
                              ucData_2_2--;
                              if(ucData_2_2>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_2_2=0;
                              }
                              ucWd2Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 3:   //设置第3行参数
                              ucData_2_3--;
                              if(ucData_2_3>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_2_3=0;
                              }
                              ucWd2Part3Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 4:   //设置第4行参数
                              ucData_2_4--;
                              if(ucData_2_4>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucData_2_4=0;
                              }
                              ucWd2Part4Update=1; //1代表更新显示,响应函数内部会清零
                              break;


                   }
                   break;         
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 切换"光标"移动按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1: //窗口1
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucPart=2; //光标切换到下一行
                              ucWd1Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //设置第2行参数
                              ucPart=3; //光标切换到下一行
                              ucWd1Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part3Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 3:   //设置第3行参数
                              ucPart=4; //光标切换到下一行
                              ucWd1Part3Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part4Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 4:   //设置第4行参数
                                                        ucWd=2;//切换到第2个窗口
                              ucPart=1; //光标返回到最上面第一行
                                                                ucWd2Update=1; //窗口2整屏更新
                              break;


                   }
                   break;
            case 2: //窗口2
                   switch(ucPart)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {
                        case 0:   //无光标显示的状态 此处的case 0可以省略

                              break;
                        case 1:   //设置第1行参数
                              ucPart=2; //光标切换到下一行
                              ucWd2Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd2Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //设置第2行参数
                              ucPart=3; //光标切换到下一行
                              ucWd2Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd2Part3Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 3:   //设置第3行参数
                              ucPart=4; //光标切换到下一行
                              ucWd2Part3Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd2Part4Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 4:   //设置第4行参数
                                                        ucWd=1;//切换到第1个窗口
                              ucPart=1; //光标返回到最上面第一行
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;


                   }
                   break;         
          }         
      
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
   
    case 4: // 设置按键对应朱兆祺学习板的S13键,按一次进入设置状态,出现反显光标。再按一次推出设置状态,消除反显光标,并且强行切换到第1个窗口
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://窗口1
                   switch(ucPart)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {

                        case 0:   //无光标显示的状态
                              ucPart=1; //光标显示第一行,进入设置模式
                              ucWd1Part1Update=1; //更新显示
                              break;
                        case 1:   //设置第1行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part1Update=1; //更新显示
                              break;
                        case 2:   //设置第2行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part2Update=1; //更新显示
                              break;
                        case 3:   //设置第3行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part3Update=1; //更新显示
                              break;
                        case 4:   //设置第4行参数
                              ucPart=0; //无光标显示,退出设置模式
                              ucWd1Part4Update=1; //更新显示
                              break;


                   }
                   break;
            case 2://窗口2
                   switch(ucPart)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {

                        case 0:   //无光标显示的状态
                                                        ucWd=1; //强行切换到第1个窗口
                              ucPart=1; //光标显示第一行,进入设置模式
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;
                        case 1:   //设置第1行参数
                                                                  ucWd=1; //强行切换到第1个窗口
                              ucPart=0; //无光标显示,退出设置模式
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;
                        case 2:   //设置第2行参数
                                                                  ucWd=1; //强行切换到第1个窗口
                              ucPart=0; //无光标显示,退出设置模式
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;
                        case 3:   //设置第3行参数
                                                            ucWd=1; //强行切换到第1个窗口
                              ucPart=0; //无光标显示,退出设置模式
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;
                        case 4:   //设置第4行参数
                                                                  ucWd=1; //强行切换到第1个窗口
                              ucPart=0; //无光标显示,退出设置模式
                                                                ucWd1Update=1; //窗口1整屏更新
                              break;


                   }
                   break;         
          }   

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         

}               
}


unsigned char *number_to_matrix(unsigned charucBitNumber)
{
    unsigned char *p_ucAnyNumber;//此指针根据ucBitNumber数值的大小,分别调用不同的字库。

      switch(ucBitNumber)//根据ucBitNumber数值的大小,分别调用不同的字库。
      {
            case 0:
             p_ucAnyNumber=Zf816_0;
                     break;
            case 1:
             p_ucAnyNumber=Zf816_1;
                     break;
            case 2:
             p_ucAnyNumber=Zf816_2;
                     break;
            case 3:
             p_ucAnyNumber=Zf816_3;
                     break;
            case 4:
             p_ucAnyNumber=Zf816_4;
                     break;
            case 5:
             p_ucAnyNumber=Zf816_5;
                     break;
            case 6:
             p_ucAnyNumber=Zf816_6;
                     break;
            case 7:
             p_ucAnyNumber=Zf816_7;
                     break;
            case 8:
             p_ucAnyNumber=Zf816_8;
                     break;
            case 9:
             p_ucAnyNumber=Zf816_9;
                     break;
            case 10:
             p_ucAnyNumber=Zf816_nc;
                     break;
                default:   //如果上面的条件都不符合,那么默认指向空字模
             p_ucAnyNumber=Zf816_nc;
                     break;
      }

    return p_ucAnyNumber;//返回转换结束后的指针
}



void lcd_display_service(void) //应用层面的液晶屏显示程序
{

    switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
    {
      case 1:
            wd1();//窗口1显示的内容
            break;
      case 2:
            wd2();//窗口2显示的内容
            break;
      //本程序只有2个窗口,所以只有一个case 1,如果要增加窗口,就直接增加 case 2, case 3...      
    }

}


void wd1(void)//窗口1显示的内容
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

/* 注释三:
* 把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候
* 才需要更新,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要
* 刷新显示的内容,这种内容放在局部更新显示的括号里。
*/
    if(ucWd1Update==1)//窗口1整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd1Update=0;//及时清零,避免一直更新

      ucWd1Part1Update=1; //激活窗口1的第1行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd1Part2Update=1; //激活窗口1的第2行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd1Part3Update=1; //激活窗口1的第3行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd1Part4Update=1; //激活窗口1的第4行局部更新显示变量,这里在前面数码管显示框架上有所改进

      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布

      display_lattice(0,0,Hz1616_yi,0,2,16,0);    //一窗口一行,这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(1,0,Hz1616_chuang,0,2,16,0);   
      display_lattice(2,0,Hz1616_kou,0,2,16,0);   
      display_lattice(3,0,Hz1616_yi,0,2,16,0);
      display_lattice(4,0,Hz1616_hang,0,2,16,0);

      display_lattice(0,16,Hz1616_yi,0,2,16,0);    //一窗口二行
      display_lattice(1,16,Hz1616_chuang,0,2,16,0);   
      display_lattice(2,16,Hz1616_kou,0,2,16,0);   
      display_lattice(3,16,Hz1616_er,0,2,16,0);
      display_lattice(4,16,Hz1616_hang,0,2,16,0);

      display_lattice(8,0,Hz1616_yi,0,2,16,0);    //一窗口三行
      display_lattice(9,0,Hz1616_chuang,0,2,16,0);   
      display_lattice(10,0,Hz1616_kou,0,2,16,0);   
      display_lattice(11,0,Hz1616_san,0,2,16,0);
      display_lattice(12,0,Hz1616_hang,0,2,16,0);

      display_lattice(8,16,Hz1616_yi,0,2,16,0);    //一窗口四行
      display_lattice(9,16,Hz1616_chuang,0,2,16,0);   
      display_lattice(10,16,Hz1616_kou,0,2,16,0);   
      display_lattice(11,16,Hz1616_si,0,2,16,0);
      display_lattice(12,16,Hz1616_hang,0,2,16,0);

    }

/* 注释四:
* 注意!我前面讲数码管显示的时候有一句话讲错了,我那时说<局部更新应该写在整屏更新之前>,这是不对的。
* 按照现在的显示程序框架<即整屏显示更新括号里包含了所有局部变量的激活>,应该是<整屏更新应该写在局部更新之前>
* 这样才对。
*/
    if(ucWd1Part1Update==1) //窗口1的第1行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd1Part1Update=0; //及时清零,避免一直更新

      if(ucPart==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

      if(ucData_1_1>=10) //有2位数以上
      {
             ucAnyNumber_10=ucData_1_1/10;//十位
      }
      else //否则显示空
      {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucData_1_1%10/1;//个位

   
      p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
      p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


      insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
      insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

      display_lattice(5,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                        
    }

    if(ucWd1Part2Update==1) //窗口1的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd1Part2Update=0; //及时清零,避免一直更新

         if(ucPart==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_1_2>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_1_2/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_1_2%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(5,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   if(ucWd1Part3Update==1) //窗口1的第3行局部更新显示变量,里面放一些经常需要刷新显示的内容
   {
         ucWd1Part3Update=0; //及时清零,避免一直更新

         if(ucPart==3) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_1_3>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_1_3/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_1_3%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(13,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   if(ucWd1Part4Update==1) //窗口1的第4行局部更新显示变量,里面放一些经常需要刷新显示的内容
   {
         ucWd1Part4Update=0; //及时清零,避免一直更新

         if(ucPart==4) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_1_4>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_1_4/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_1_4%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(13,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量                        
   }


}


void wd2(void)//窗口2显示的内容
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

    if(ucWd2Update==1)//窗口2整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd2Update=0;//及时清零,避免一直更新

      ucWd2Part1Update=1; //激活窗口2的第1行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd2Part2Update=1; //激活窗口2的第2行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd2Part3Update=1; //激活窗口2的第3行局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd2Part4Update=1; //激活窗口2的第4行局部更新显示变量,这里在前面数码管显示框架上有所改进

      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布

      display_lattice(0,0,Hz1616_er,0,2,16,0);    //二窗口一行,这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(1,0,Hz1616_chuang,0,2,16,0);   
      display_lattice(2,0,Hz1616_kou,0,2,16,0);   
      display_lattice(3,0,Hz1616_yi,0,2,16,0);
      display_lattice(4,0,Hz1616_hang,0,2,16,0);

      display_lattice(0,16,Hz1616_er,0,2,16,0);    //二窗口二行
      display_lattice(1,16,Hz1616_chuang,0,2,16,0);   
      display_lattice(2,16,Hz1616_kou,0,2,16,0);   
      display_lattice(3,16,Hz1616_er,0,2,16,0);
      display_lattice(4,16,Hz1616_hang,0,2,16,0);

      display_lattice(8,0,Hz1616_er,0,2,16,0);    //二窗口三行
      display_lattice(9,0,Hz1616_chuang,0,2,16,0);   
      display_lattice(10,0,Hz1616_kou,0,2,16,0);   
      display_lattice(11,0,Hz1616_san,0,2,16,0);
      display_lattice(12,0,Hz1616_hang,0,2,16,0);

      display_lattice(8,16,Hz1616_er,0,2,16,0);    //二窗口四行
      display_lattice(9,16,Hz1616_chuang,0,2,16,0);   
      display_lattice(10,16,Hz1616_kou,0,2,16,0);   
      display_lattice(11,16,Hz1616_si,0,2,16,0);
      display_lattice(12,16,Hz1616_hang,0,2,16,0);

    }

    if(ucWd2Part1Update==1) //窗口2的第1行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd2Part1Update=0; //及时清零,避免一直更新

      if(ucPart==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

      if(ucData_2_1>=10) //有2位数以上
      {
             ucAnyNumber_10=ucData_2_1/10;//十位
      }
      else //否则显示空
      {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucData_2_1%10/1;//个位

   
      p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
      p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


      insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
      insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

      display_lattice(5,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                        
    }

    if(ucWd2Part2Update==1) //窗口2的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd2Part2Update=0; //及时清零,避免一直更新

         if(ucPart==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_2_2>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_2_2/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_2_2%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(5,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   if(ucWd2Part3Update==1) //窗口2的第3行局部更新显示变量,里面放一些经常需要刷新显示的内容
   {
         ucWd2Part3Update=0; //及时清零,避免一直更新

         if(ucPart==3) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_2_3>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_2_3/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_2_3%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(13,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   if(ucWd2Part4Update==1) //窗口2的第4行局部更新显示变量,里面放一些经常需要刷新显示的内容
   {
         ucWd2Part4Update=0; //及时清零,避免一直更新

         if(ucPart==4) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucData_2_4>=10) //有2位数以上
         {
             ucAnyNumber_10=ucData_2_4/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucData_2_4%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(13,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量                        
   }


}


void clear_all_canvas(void)//把画布全部清零
{
   unsigned int j=0;
   unsigned int i=0;

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  ucCanvasBuffer=0x00;
      }
   }         

}





void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释五:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }         

}

/* 注释六:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;

//注意,要把以下两行指令屏蔽,否则屏幕在更新显示时会整屏闪动
//WriteCommand(0x34);//关显示缓冲指令            
//WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}




void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

总结陈词:
这一节讲了在多个窗口里设置不同的参数。还有一种常见的项目,要求把第1窗口是用来作为主菜单,主菜单里面有3个子菜单,可以通过移动光标进入不同的子菜单窗口进行参数设置,这类项目该如何编程?欲知详情,请听下回分解-----通过主菜单移动光标来进入子菜单窗口的液晶屏程序。

(未完待续,下节更精彩,不要走开哦)

kuier1992 发表于 2014-10-29 10:14

谢谢你的分享好好学习,也称大牛哈哈

飞翔荷兰人号 发表于 2014-10-30 00:31

赞一个

jianhong_wu 发表于 2014-10-30 15:57

第七十九节:通过主菜单移动光标来进入子菜单窗口的液晶屏程序。

开场白:
    其实主菜单窗口与子菜单窗口本质都是多窗口菜单程序,只不过我在按键服务程序里面建立起来了一条主窗口与子窗口的关系链。这个关系链还是用switch语句搭建起来的,在某个窗口某个局部显示上,操作某个按键就会切换到不同的窗口显示。
继续巩固上一节教给大家的两个知识点:
   第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucWdxPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
    第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
   通过按键设置6个不同的参数。
    有4个窗口。第1个窗口是主菜单界面,通过光标切换可以进去设置不同参数的子菜单界面。第2个窗口是设置时间范围界面。第3个窗口是设置速度范围界面。第4个窗口是设置频率范围界面。每个设置界面显示2个参数。每个参数的范围是从0到99。
    有4个按键:
(a)        一个是进入和退出S13按键,按一次进入选中的子菜单。再按一次退出子菜单。
(b)        一个是移动光标S9按键,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。当移动到每个窗口最下边那一行时,再按下此按键会把光标移动到第一个参数。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。

(3)源代码讲解如下:
#include "REG52.H"

/* 注释一:
* 本程序用到的变量比较多,所以在keil编译模式里要设置一下编译模式memory model,
* 否则编译会出错.右键单击Target选择“Options for Target'Target1'”就会出来一个框
* 在memory model中选择compact:variables in pdata 就可以了。
*/

#define const_voice_short40   //蜂鸣器短叫的持续时间

#define const_key_time120    //按键去抖动延时的时间
#define const_key_time220    //按键去抖动延时的时间
#define const_key_time320    //按键去抖动延时的时间
#define const_key_time420    //按键去抖动延时的时间


sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键
sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键
sbit key_sr3=P0^2; //对应朱兆祺学习板的S9键
sbit key_sr4=P0^3; //对应朱兆祺学习板的S13键

sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(unsigned char ucFillDate); // 清屏 全部显示空填充0x00   全部显示点阵用0xff
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount);//把字模插入画布.
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr); //显示任意点阵函数
unsigned char *number_to_matrix(unsigned charucBitNumber); //把一位数字转换成字模首地址的函数
void delay_short(unsigned int uiDelayshort); //延时
void delay_long(unsigned int uiDelayLong);

void T0_time(); //定时中断函数
void key_service(void); //按键服务的应用程序
void key_scan(void);//按键扫描函数 放在定时中断里

void initial_myself();   
void initial_peripheral();


void lcd_display_service(void); //应用层面的液晶屏显示程序
void clear_all_canvas(void);//把画布全部清零

void wd1(void);//窗口1主菜单
void wd2(void);//窗口2设置时间
void wd3(void);//窗口3设置速度
void wd4(void);//窗口4设置频率

code unsigned char Zf816_0[]=
{
/*--文字:0--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};

code unsigned char Zf816_1[]=
{
/*--文字:1--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x10,0x70,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x10,0x7C,0x00,0x00,
};

code unsigned char Zf816_2[]=
{
/*--文字:2--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x04,0x04,0x08,0x10,0x20,0x42,0x7E,0x00,0x00,
};

code unsigned char Zf816_3[]=
{
/*--文字:3--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x04,0x18,0x04,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_4[]=
{
/*--文字:4--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x04,0x0C,0x14,0x24,0x24,0x44,0x44,0x7E,0x04,0x04,0x1E,0x00,0x00,
};

code unsigned char Zf816_5[]=
{
/*--文字:5--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x40,0x40,0x40,0x58,0x64,0x02,0x02,0x42,0x44,0x38,0x00,0x00,
};

code unsigned char Zf816_6[]=
{
/*--文字:6--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x1C,0x24,0x40,0x40,0x58,0x64,0x42,0x42,0x42,0x24,0x18,0x00,0x00,
};


code unsigned char Zf816_7[]=
{
/*--文字:7--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x7E,0x44,0x44,0x08,0x08,0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x00,
};

code unsigned char Zf816_8[]=
{
/*--文字:8--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x3C,0x42,0x42,0x42,0x24,0x18,0x24,0x42,0x42,0x42,0x3C,0x00,0x00,
};

code unsigned char Zf816_9[]=
{
/*--文字:9--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x18,0x24,0x42,0x42,0x42,0x26,0x1A,0x02,0x02,0x24,0x38,0x00,0x00,
};


code unsigned char Zf816_nc[]=//空字模
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

code unsigned char Zf816_mao_hao[]=//冒号
{
/*--文字::--*/
/*--宋体12;此字体下对应的点阵为:宽x高=8x16   --*/
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,0x00,0x00,0x18,0x18,0x00,0x00,
};


code unsigned char Hz1616_zhu[]=
{
/*--文字:主--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x02,0x00,0x01,0x80,0x01,0x00,0x00,0x08,0x3F,0xFC,0x01,0x00,0x01,0x00,0x01,0x08,
0x3F,0xFC,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x04,0x7F,0xFE,0x00,0x00,0x00,0x00,
};

code unsigned char Hz1616_cai[]=
{
/*--文字:菜--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x04,0x40,0xFF,0xFE,0x04,0x40,0x04,0x40,0x3F,0xF8,0x22,0x08,0x11,0x10,0x08,0x20,
0x01,0x00,0x7F,0xFE,0x03,0x80,0x05,0x40,0x09,0x30,0x11,0x1C,0x61,0x08,0x01,0x00,
};

code unsigned char Hz1616_dan[]=
{
/*--文字:单--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x08,0x20,0x06,0x30,0x04,0x40,0x3F,0xF8,0x21,0x08,0x3F,0xF8,0x21,0x08,0x21,0x08,
0x3F,0xF8,0x21,0x08,0x01,0x00,0xFF,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,
};

code unsigned char Hz1616_she[]=
{
/*--文字:设--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x40,0x00,0x21,0xF0,0x31,0x10,0x21,0x10,0x01,0x10,0x01,0x10,0xE2,0x0E,0x25,0xF8,
0x21,0x08,0x21,0x08,0x20,0x90,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x0C,0x04,
};

code unsigned char Hz1616_zhi[]=
{
/*--文字:置--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x3F,0xF8,0x24,0x48,0x24,0x48,0x3F,0xF8,0x01,0x00,0x7F,0xFC,0x02,0x00,0x1F,0xF0,
0x10,0x10,0x1F,0xF0,0x10,0x10,0x1F,0xF0,0x10,0x10,0x1F,0xF0,0x10,0x10,0xFF,0xFE,
};

code unsigned char Hz1616_su[]=
{
/*--文字:速--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x80,0x40,0x80,0x2F,0xFC,0x20,0x80,0x00,0x80,0x07,0xF8,0xE4,0x88,0x24,0x88,
0x27,0xF8,0x21,0xA0,0x22,0x98,0x2C,0x88,0x20,0x80,0x50,0x80,0x8F,0xFE,0x00,0x00,
};

code unsigned char Hz1616_du[]=
{
/*--文字:度--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x01,0x00,0x00,0x80,0x3F,0xFE,0x22,0x20,0x22,0x20,0x2F,0xFC,0x22,0x20,0x23,0xE0,
0x20,0x00,0x27,0xF8,0x22,0x10,0x21,0x20,0x20,0xC0,0x41,0x30,0x46,0x0E,0x98,0x04,
};

code unsigned char Hz1616_shi[]=
{
/*--文字:时--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x10,0x00,0x10,0x7C,0x10,0x44,0x10,0x47,0xFE,0x44,0x10,0x7C,0x10,0x45,0x10,
0x44,0x90,0x44,0x90,0x7C,0x10,0x00,0x10,0x00,0x10,0x00,0x10,0x00,0x50,0x00,0x20,
};

code unsigned char Hz1616_jian[]=
{
/*--文字:间--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x20,0x00,0x13,0xFC,0x10,0x04,0x40,0x04,0x47,0xE4,0x44,0x24,0x44,0x24,0x47,0xE4,
0x44,0x24,0x44,0x24,0x47,0xE4,0x40,0x04,0x40,0x04,0x40,0x04,0x40,0x14,0x40,0x08,
};

code unsigned char Hz1616_pin[]=
{
/*--文字:频--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x08,0x00,0x08,0xFE,0x4E,0x20,0x48,0x40,0x48,0xFC,0xFE,0x84,0x00,0xA4,0x08,0xA4,
0x4A,0xA4,0x4A,0xA4,0x84,0xA4,0x08,0x50,0x10,0x48,0x20,0x86,0xC3,0x02,0x00,0x00,
};

code unsigned char Hz1616_lv[]=
{
/*--文字:率--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x02,0x00,0x01,0x00,0x7F,0xFE,0x41,0x00,0x22,0x28,0x17,0xD0,0x04,0x80,0x11,0x10,
0x22,0x48,0x47,0xC4,0x01,0x20,0xFF,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,
};

code unsigned char Hz1616_fan[]=
{
/*--文字:范--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x04,0x20,0x04,0x20,0xFF,0xFE,0x04,0x60,0x40,0x00,0x31,0xF8,0x91,0x08,0x61,0x08,
0x49,0x08,0x09,0x38,0x11,0x10,0xE1,0x00,0x21,0x04,0x21,0x04,0x20,0xFC,0x20,0x00,
};

code unsigned char Hz1616_wei[]=
{
/*--文字:围--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x7F,0xFC,0x42,0x04,0x42,0x04,0x5F,0xF4,0x42,0x04,0x4F,0xE4,0x42,0x04,0x5F,0xE4,
0x42,0x24,0x42,0x24,0x42,0x24,0x42,0xA4,0x42,0x44,0x40,0x04,0x7F,0xFC,0x40,0x04,
};

code unsigned char Hz1616_shang[]=
{
/*--文字:上--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0xF8,0x01,0x00,
0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x04,0x7F,0xFE,0x00,0x00,
};

code unsigned char Hz1616_xia[]=
{
/*--文字:下--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x04,0x7F,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0xC0,0x01,0x60,0x01,0x30,
0x01,0x20,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x00,0x00,
};

code unsigned char Hz1616_xian[]=
{
/*--文字:限--*/
/*--宋体12;此字体下对应的点阵为:宽x高=16x16   --*/
0x00,0x00,0xFB,0xF8,0x92,0x08,0x93,0xF8,0xA2,0x08,0xA2,0x08,0x93,0xF8,0x8A,0x80,
0x8A,0x48,0xAA,0x50,0x92,0x20,0x82,0x20,0x82,0x10,0x82,0x8E,0x83,0x04,0x82,0x00,
};


unsigned char ucCanvasBuffer[]= //画布显示数组。注意,这里没有code关键字,是全局变量。初始化全部填充0x00
{
0x00,0x00,0x00,0x00,//上半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,

//------------上半屏和下半屏的分割线-----------

0x00,0x00,0x00,0x00,//下半屏
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
};


unsigned char ucKeySec=0;   //被触发的按键编号
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucWd=1; //窗口变量

unsigned char ucWd1Part=1;//窗口1的局部变量,代表选中某一行。
unsigned char ucWd1Update=1; //窗口1的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part1Update=0; //窗口1的第1个局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part2Update=0; //窗口1的第2个局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part3Update=0; //窗口1的第3个局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucWd2Part=1;//窗口2的局部变量,代表选中某一行。
unsigned char ucWd2Update=0; //窗口2的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part1Update=0; //窗口2的第1个局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd2Part2Update=0; //窗口2的第2个局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucWd3Part=1;//窗口3的局部变量,代表选中某一行。
unsigned char ucWd3Update=0; //窗口3的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd3Part1Update=0; //窗口3的第1个局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd3Part2Update=0; //窗口3的第2个局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucWd4Part=1;//窗口4的局部变量,代表选中某一行。
unsigned char ucWd4Update=0; //窗口4的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd4Part1Update=0; //窗口4的第1个局部更新显示变量1代表更新显示,响应函数内部会清零
unsigned char ucWd4Part2Update=0; //窗口4的第2个局部更新显示变量1代表更新显示,响应函数内部会清零


unsigned char ucTimeH=2;//设置时间的上限数据
unsigned char ucTimeL=1;//设置时间的下限数据

unsigned char ucSpeedH=4;//设置速度的上限数据
unsigned char ucSpeedL=3;//设置速度的下限数据

unsigned char ucFreqH=6;//设置频率的上限数据
unsigned char ucFreqL=5;//设置频率的下限数据

void main()
{
      initial_myself();      //第一区,上电后马上初始化
      delay_long(100);       //一线,延时线。延时一段时间
      initial_peripheral();//第二区,上电后延时一段时间再初始化

      while(1)   //第三区
      {
                  key_service(); //按键服务的应用程序
            lcd_display_service(); //应用层面的液晶屏显示程序
      }

}


void initial_myself()//第一区 上电后马上初始化
{
/* 注释二:
* 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平,
* 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。
* 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。
*/
   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平
   beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

   TMOD=0x01;//设置定时器0为工作方式1

   TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
   TL0=0x2f;
}
void initial_peripheral() //第二区 上电后延时一段时间再初始化
{
    LCDInit(); //初始化12864 内部包含液晶模块的复位


    EA=1;   //开总中断
    ET0=1;    //允许定时中断
    TR0=1;    //启动定时中断

}


void T0_time() interrupt 1
{
TF0=0;//清除中断标志
TR0=0; //关中断

key_scan(); //按键扫描函数

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
         beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
         beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f
TL0=0x2f;
TR0=1;//开中断
}



void key_scan(void)//按键扫描函数 放在定时中断里
{


static unsigned intuiKeyTimeCnt1=0; //按键去抖动延时计数器
static unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt2=0; //按键去抖动延时计数器
static unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt3=0; //按键去抖动延时计数器
static unsigned char ucKeyLock3=0; //按键触发后自锁的变量标志

static unsigned intuiKeyTimeCnt4=0; //按键去抖动延时计数器
static unsigned char ucKeyLock4=0; //按键触发后自锁的变量标志

if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock1=0; //按键自锁标志清零
   uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt1++; //累加定时中断次数
   if(uiKeyTimeCnt1>const_key_time1)
   {
      uiKeyTimeCnt1=0;
      ucKeyLock1=1;//自锁按键置位,避免一直触发
      ucKeySec=1;    //触发1号键
   }
}

if(key_sr2==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock2=0; //按键自锁标志清零
   uiKeyTimeCnt2=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock2==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt2++; //累加定时中断次数
   if(uiKeyTimeCnt2>const_key_time2)
   {
      uiKeyTimeCnt2=0;
      ucKeyLock2=1;//自锁按键置位,避免一直触发
      ucKeySec=2;    //触发2号键
   }
}

if(key_sr3==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock3=0; //按键自锁标志清零
   uiKeyTimeCnt3=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock3==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt3++; //累加定时中断次数
   if(uiKeyTimeCnt3>const_key_time3)
   {
      uiKeyTimeCnt3=0;
      ucKeyLock3=1;//自锁按键置位,避免一直触发
      ucKeySec=3;    //触发3号键
   }
}

if(key_sr4==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
   ucKeyLock4=0; //按键自锁标志清零
   uiKeyTimeCnt4=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。      
}
else if(ucKeyLock4==0)//有按键按下,且是第一次被按下
{
   uiKeyTimeCnt4++; //累加定时中断次数
   if(uiKeyTimeCnt4>const_key_time4)
   {
      uiKeyTimeCnt4=0;
      ucKeyLock4=1;//自锁按键置位,避免一直触发
      ucKeySec=4;    //触发4号键
   }
}

}


void key_service(void) //按键服务的应用程序
{
switch(ucKeySec) //按键服务状态切换
{
    case 1:// 加按键 对应朱兆祺学习板的S1键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 2://窗口2设置时间
                   switch(ucWd2Part)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //设置时间上限
                              ucTimeH++;
                              if(ucTimeH>99)
                              {
                                 ucTimeH=99;
                              }
                              ucWd2Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置时间下限
                              ucTimeL++;
                              if(ucTimeL>99)
                              {
                                 ucTimeL=99;
                              }
                              ucWd2Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        
                   }
                   break;         
            case 3://窗口3设置速度
                   switch(ucWd3Part)//在窗口3下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //设置速度上限
                              ucSpeedH++;
                              if(ucSpeedH>99)
                              {
                                 ucSpeedH=99;
                              }
                              ucWd3Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置速度下限
                              ucSpeedL++;
                              if(ucSpeedL>99)
                              {
                                 ucSpeedL=99;
                              }
                              ucWd3Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        
                   }
                   break;   
            case 4://窗口4设置速度
                   switch(ucWd4Part)//在窗口4下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //设置频率上限
                              ucFreqH++;
                              if(ucFreqH>99)
                              {
                                 ucFreqH=99;
                              }
                              ucWd4Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置频率下限
                              ucFreqL++;
                              if(ucFreqL>99)
                              {
                                 ucFreqL=99;
                              }
                              ucWd4Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        
                   }
                   break;   
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;   
   
    case 2:// 减按键 对应朱兆祺学习板的S5键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {         
            case 2://窗口2 设置时间
                   switch(ucWd2Part)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {
                        case 1:   //设置时间上限
                              ucTimeH--;
                              if(ucTimeH>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucTimeH=0;
                              }
                              ucWd2Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置时间下限
                              ucTimeL--;
                              if(ucTimeL>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucTimeL=0;
                              }
                              ucWd2Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                   }
                   break;         
            case 3://窗口3设置速度
                   switch(ucWd3Part)//在窗口3下,根据不同的局部变量来设置不同的参数
                   {
                        case 1:   //设置速度上限
                              ucSpeedH--;
                              if(ucSpeedH>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucSpeedH=0;
                              }
                              ucWd3Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置速度下限
                              ucSpeedL--;
                              if(ucSpeedL>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucSpeedL=0;
                              }
                              ucWd3Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                   }
                   break;      
            case 4://窗口4设置频率
                   switch(ucWd4Part)//在窗口4下,根据不同的局部变量来设置不同的参数
                   {
                        case 1:   //设置频率上限
                              ucFreqH--;
                              if(ucFreqH>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucFreqH=0;
                              }
                              ucWd4Part1Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                        case 2:   //设置频率下限
                              ucFreqL--;
                              if(ucFreqL>99) //一直减到最后,单片机C语言编译器有一个特征,0减去1会溢出变成255(0xff)
                              {
                                 ucFreqL=0;
                              }
                              ucWd4Part2Update=1; //1代表更新显示,响应函数内部会清零
                              break;
                   }
                   break;   
          }   
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;

    case 3:// 切换"光标"移动按键 对应朱兆祺学习板的S9键
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1: //窗口1 主菜单
                   switch(ucWd1Part)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //设置时间
                              ucWd1Part=2; //光标切换到下一行
                              ucWd1Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //设置速度
                              ucWd1Part=3; //光标切换到下一行
                              ucWd1Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part3Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 3:   //设置第3行参数
                              ucWd1Part=1; //光标返回到第一行
                              ucWd1Part3Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd1Part1Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;


                   }
                   break;
            case 2: //窗口2 设置时间
                   switch(ucWd2Part)//在窗口2下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //时间上限
                              ucWd2Part=2; //光标切换到下一行
                              ucWd2Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd2Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //时间下限
                              ucWd2Part=1; //光标返回到第一行
                              ucWd2Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd2Part1Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;

                   }
                   break;      
            case 3: //窗口3 设置速度
                   switch(ucWd3Part)//在窗口3下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //速度上限
                              ucWd3Part=2; //光标切换到下一行
                              ucWd3Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd3Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //速度下限
                              ucWd3Part=1; //光标返回到第一行
                              ucWd3Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd3Part1Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;

                   }
                   break;      
            case 4: //窗口4 设置频率
                   switch(ucWd4Part)//在窗口4下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //频率上限
                              ucWd4Part=2; //光标切换到下一行
                              ucWd4Part1Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd4Part2Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;
                        case 2:   //频率下限
                              ucWd4Part=1; //光标返回到第一行
                              ucWd4Part2Update=1; //更新显示原来那一行,目的是更新反显光标的状态
                              ucWd4Part1Update=1; //更新显示下一行,    目的是更新反显光标的状态
                              break;

                   }
                   break;   
          }         
      
          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;      
   
    case 4: // 进入和退出按键对应朱兆祺学习板的S13键,按一次进入选中的子菜单。再按一次退出子菜单。
          switch(ucWd)//在不同的窗口下,设置不同的参数
          {
            case 1://窗口1                             
                   switch(ucWd1Part)//在窗口1下,根据不同的局部变量来设置不同的参数
                   {

                        case 1:   //设置时间
                                                        ucWd=2; //进入设置时间的窗口2
                                                                ucWd2Update=1; //窗口2整屏更新
                              break;
                        case 2:   //设置速度
                                                        ucWd=3; //进入设置速度的窗口3
                                                                ucWd3Update=1; //窗口3整屏更新
                              break;
                        case 3:   //设置频率
                                                        ucWd=4; //进入设置频率的窗口4
                                                                ucWd4Update=1; //窗口4整屏更新
                              break;


                   }
                   break;
            case 2://窗口2
                                   ucWd=1;      //返回主菜单窗口1
                                   ucWd1Update=1; //窗口1整屏更新
                   break;      
            case 3://窗口3
                                   ucWd=1;      //返回主菜单窗口1
                                   ucWd1Update=1; //窗口1整屏更新
                   break;   
               case 4://窗口4
                                   ucWd=1;      //返回主菜单窗口1
                                   ucWd1Update=1; //窗口1整屏更新
                   break;   

          }   

          uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。
          ucKeySec=0;//响应按键服务处理程序后,按键编号清零,避免一致触发
          break;         

}               
}


unsigned char *number_to_matrix(unsigned charucBitNumber)
{
    unsigned char *p_ucAnyNumber;//此指针根据ucBitNumber数值的大小,分别调用不同的字库。

      switch(ucBitNumber)//根据ucBitNumber数值的大小,分别调用不同的字库。
      {
            case 0:
             p_ucAnyNumber=Zf816_0;
                     break;
            case 1:
             p_ucAnyNumber=Zf816_1;
                     break;
            case 2:
             p_ucAnyNumber=Zf816_2;
                     break;
            case 3:
             p_ucAnyNumber=Zf816_3;
                     break;
            case 4:
             p_ucAnyNumber=Zf816_4;
                     break;
            case 5:
             p_ucAnyNumber=Zf816_5;
                     break;
            case 6:
             p_ucAnyNumber=Zf816_6;
                     break;
            case 7:
             p_ucAnyNumber=Zf816_7;
                     break;
            case 8:
             p_ucAnyNumber=Zf816_8;
                     break;
            case 9:
             p_ucAnyNumber=Zf816_9;
                     break;
            case 10:
             p_ucAnyNumber=Zf816_nc;
                     break;
                default:   //如果上面的条件都不符合,那么默认指向空字模
             p_ucAnyNumber=Zf816_nc;
                     break;
      }

    return p_ucAnyNumber;//返回转换结束后的指针
}



void lcd_display_service(void) //应用层面的液晶屏显示程序
{

    switch(ucWd)//本程序的核心变量,窗口显示变量。类似于一级菜单的变量。代表显示不同的窗口。
    {
      case 1:
            wd1();//主菜单
            break;
      case 2:
            wd2();//设置时间
            break;
      case 3:
            wd3();//设置速度
            break;
      case 4:
            wd4();//设置频率
            break;

      //本程序只有4个窗口,所以只有4个case ,如果要增加窗口,就直接增加 case 5, case 6...      
    }

}


void wd1(void)//窗口1主菜单
{

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

/* 注释三:
* 把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候
* 才需要更新,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要
* 刷新显示的内容,这种内容放在局部更新显示的括号里。
*/
    if(ucWd1Update==1)//窗口1整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd1Update=0;//及时清零,避免一直更新

      ucWd1Part1Update=1; //激活窗口1的第1个局部更新显示变量
      ucWd1Part2Update=1; //激活窗口1的第2个局部更新显示变量
      ucWd1Part3Update=1; //激活窗口1的第3个局部更新显示变量


      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布

      display_lattice(2,0,Hz1616_zhu,0,2,16,0);    //主菜单。这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(3,0,Hz1616_cai,0,2,16,0);
      display_lattice(4,0,Hz1616_dan,0,2,16,0);


    }

/* 注释四:
* 注意!我前面讲数码管显示的时候有一句话讲错了,我那时说<局部更新应该写在整屏更新之前>,这是不对的。
* 按照现在的显示程序框架<即整屏显示更新括号里包含了所有局部变量的激活>,应该是<整屏更新应该写在局部更新之前>
* 这样才对。
*/
    if(ucWd1Part1Update==1) //窗口1的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd1Part1Update=0; //及时清零,避免一直更新

      if(ucWd1Part==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

                display_lattice(0,16,Hz1616_she,ucCursorFlag,2,16,0);    //设置时间范围
      display_lattice(1,16,Hz1616_zhi,ucCursorFlag,2,16,0);   
      display_lattice(2,16,Hz1616_shi,ucCursorFlag,2,16,0);   
      display_lattice(3,16,Hz1616_jian,ucCursorFlag,2,16,0);
      display_lattice(4,16,Hz1616_fan,ucCursorFlag,2,16,0);   
      display_lattice(5,16,Hz1616_wei,ucCursorFlag,2,16,0);


                        
    }

    if(ucWd1Part2Update==1) //窗口1的第2个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd1Part2Update=0; //及时清零,避免一直更新

         if(ucWd1Part==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         display_lattice(8,0,Hz1616_she,ucCursorFlag,2,16,0);      //设置速度范围
         display_lattice(9,0,Hz1616_zhi,ucCursorFlag,2,16,0);   
         display_lattice(10,0,Hz1616_su,ucCursorFlag,2,16,0);   
         display_lattice(11,0,Hz1616_du,ucCursorFlag,2,16,0);
         display_lattice(12,0,Hz1616_fan,ucCursorFlag,2,16,0);   
         display_lattice(13,0,Hz1616_wei,ucCursorFlag,2,16,0);
                        
   }

   if(ucWd1Part3Update==1) //窗口1的第3行局部更新显示变量,里面放一些经常需要刷新显示的内容
   {
         ucWd1Part3Update=0; //及时清零,避免一直更新

         if(ucWd1Part==3) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         display_lattice(8,16,Hz1616_she,ucCursorFlag,2,16,0);    //设置频率范围
         display_lattice(9,16,Hz1616_zhi,ucCursorFlag,2,16,0);   
         display_lattice(10,16,Hz1616_pin,ucCursorFlag,2,16,0);   
         display_lattice(11,16,Hz1616_lv,ucCursorFlag,2,16,0);
         display_lattice(12,16,Hz1616_fan,ucCursorFlag,2,16,0);   
         display_lattice(13,16,Hz1616_wei,ucCursorFlag,2,16,0);                           
   }


}


void wd2(void)//窗口2 设置时间
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

    if(ucWd2Update==1)//窗口2整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd2Update=0;//及时清零,避免一直更新

      ucWd2Part1Update=1; //激活窗口2的第1个局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd2Part2Update=1; //激活窗口2的第2个局部更新显示变量,这里在前面数码管显示框架上有所改进

      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布


      display_lattice(2,0,Hz1616_she,0,2,16,0);    //设置时间。这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(3,0,Hz1616_zhi,0,2,16,0);
      display_lattice(4,0,Hz1616_shi,0,2,16,0);
      display_lattice(5,0,Hz1616_jian,0,2,16,0);


                display_lattice(0,16,Hz1616_shi,0,2,16,0);    //时间上限
      display_lattice(1,16,Hz1616_jian,0,2,16,0);   
      display_lattice(2,16,Hz1616_shang,0,2,16,0);   
      display_lattice(3,16,Hz1616_xian,0,2,16,0);

      display_lattice(8,0,Hz1616_shi,0,2,16,0);//时间下限
      display_lattice(9,0,Hz1616_jian,0,2,16,0);   
      display_lattice(10,0,Hz1616_xia,0,2,16,0);   
      display_lattice(11,0,Hz1616_xian,0,2,16,0);

    }

    if(ucWd2Part1Update==1) //窗口2的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd2Part1Update=0; //及时清零,避免一直更新

      if(ucWd2Part==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

      if(ucTimeH>=10) //有2位数以上
      {
             ucAnyNumber_10=ucTimeH/10;//十位
      }
      else //否则显示空
      {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucTimeH%10/1;//个位

   
      p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
      p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


      insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
      insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

      display_lattice(4,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                        
    }

    if(ucWd2Part2Update==1) //窗口2的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd2Part2Update=0; //及时清零,避免一直更新

         if(ucWd2Part==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucTimeL>=10) //有2位数以上
         {
             ucAnyNumber_10=ucTimeL/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucTimeL%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(12,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   
}



void wd3(void)//窗口3 设置速度
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

    if(ucWd3Update==1)//窗口3整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd3Update=0;//及时清零,避免一直更新

      ucWd3Part1Update=1; //激活窗口3的第1个局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd3Part2Update=1; //激活窗口3的第2个局部更新显示变量,这里在前面数码管显示框架上有所改进

      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布


      display_lattice(2,0,Hz1616_she,0,2,16,0);    //设置速度。这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(3,0,Hz1616_zhi,0,2,16,0);
      display_lattice(4,0,Hz1616_su,0,2,16,0);
      display_lattice(5,0,Hz1616_du,0,2,16,0);


                display_lattice(0,16,Hz1616_su,0,2,16,0);    //速度上限
      display_lattice(1,16,Hz1616_du,0,2,16,0);   
      display_lattice(2,16,Hz1616_shang,0,2,16,0);   
      display_lattice(3,16,Hz1616_xian,0,2,16,0);

      display_lattice(8,0,Hz1616_su,0,2,16,0);//速度下限
      display_lattice(9,0,Hz1616_du,0,2,16,0);   
      display_lattice(10,0,Hz1616_xia,0,2,16,0);   
      display_lattice(11,0,Hz1616_xian,0,2,16,0);

    }

    if(ucWd3Part1Update==1) //窗口3的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd3Part1Update=0; //及时清零,避免一直更新

      if(ucWd3Part==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

      if(ucSpeedH>=10) //有2位数以上
      {
             ucAnyNumber_10=ucSpeedH/10;//十位
      }
      else //否则显示空
      {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucSpeedH%10/1;//个位

   
      p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
      p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


      insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
      insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

      display_lattice(4,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                        
    }

    if(ucWd3Part2Update==1) //窗口3的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd3Part2Update=0; //及时清零,避免一直更新

         if(ucWd3Part==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucSpeedL>=10) //有2位数以上
         {
             ucAnyNumber_10=ucSpeedL/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucSpeedL%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(12,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   
}



void wd4(void)//窗口4 设置频率
{
    unsigned char ucAnyNumber_1; //分解变量的个位
    unsigned char ucAnyNumber_10; //分解变量的十位


    unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址

    unsigned char ucCursorFlag;//光标标志,也就是反显的标志,它是根据局部变量ucPart来定的

    if(ucWd4Update==1)//窗口4整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd4Update=0;//及时清零,避免一直更新

      ucWd4Part1Update=1; //激活窗口4的第1个局部更新显示变量,这里在前面数码管显示框架上有所改进
      ucWd4Part2Update=1; //激活窗口4的第2个局部更新显示变量,这里在前面数码管显示框架上有所改进

      display_clear(0x00); // 清屏操作, 全部显示空填充0x00,全部显示点阵用0xff。
      clear_all_canvas();//把画布全部清零
      insert_buffer_to_canvas(0,0,Zf816_mao_hao,0,1,16);//把冒号的字模插入画布


      display_lattice(2,0,Hz1616_she,0,2,16,0);    //设置频率。这些内容不用经常更新,只有在切换窗口的时候才更新显示
      display_lattice(3,0,Hz1616_zhi,0,2,16,0);
      display_lattice(4,0,Hz1616_pin,0,2,16,0);
      display_lattice(5,0,Hz1616_lv,0,2,16,0);


                display_lattice(0,16,Hz1616_pin,0,2,16,0);    //频率上限
      display_lattice(1,16,Hz1616_lv,0,2,16,0);   
      display_lattice(2,16,Hz1616_shang,0,2,16,0);   
      display_lattice(3,16,Hz1616_xian,0,2,16,0);

      display_lattice(8,0,Hz1616_pin,0,2,16,0);//频率下限
      display_lattice(9,0,Hz1616_lv,0,2,16,0);   
      display_lattice(10,0,Hz1616_xia,0,2,16,0);   
      display_lattice(11,0,Hz1616_xian,0,2,16,0);

    }

    if(ucWd4Part1Update==1) //窗口4的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd4Part1Update=0; //及时清零,避免一直更新

      if(ucWd4Part==1) //被选中
      {
             ucCursorFlag=1; //反显 显示
      }
      else //没被选中
      {
             ucCursorFlag=0; //正常 显示
      }

      if(ucFreqH>=10) //有2位数以上
      {
             ucAnyNumber_10=ucFreqH/10;//十位
      }
      else //否则显示空
      {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucFreqH%10/1;//个位

   
      p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
      p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


      insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
      insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

      display_lattice(4,16,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量

                        
    }

    if(ucWd4Part2Update==1) //窗口4的第2行局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
         ucWd4Part2Update=0; //及时清零,避免一直更新

         if(ucWd4Part==2) //被选中
         {
             ucCursorFlag=1; //反显 显示
         }
         else //没被选中
         {
             ucCursorFlag=0; //正常 显示
         }

         if(ucFreqL>=10) //有2位数以上
         {
             ucAnyNumber_10=ucFreqL/10;//十位
         }
         else //否则显示空
         {
             ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
         }

         ucAnyNumber_1=ucFreqL%10/1;//个位

   
         p_ucAnyNumber_10=number_to_matrix(ucAnyNumber_10); //把数字转换成字模首地址
         p_ucAnyNumber_1=number_to_matrix(ucAnyNumber_1); //把数字转换成字模首地址


         insert_buffer_to_canvas(2,0,p_ucAnyNumber_10,ucCursorFlag,1,16);//把十的字模插入画布
         insert_buffer_to_canvas(3,0,p_ucAnyNumber_1,ucCursorFlag,1,16);//把个的字模插入画布

         display_lattice(12,0,ucCanvasBuffer,0,4,16,0);   //显示整屏的画布,最后的参数0是偏移量
                        
   }

   
}


void clear_all_canvas(void)//把画布全部清零
{
   unsigned int j=0;
   unsigned int i=0;

   for(j=0;j<16;j++)//这里的16表示画布有16行
   {
      for(i=0;i<4;i++) //这里的4表示画布每行有4个字节
      {
                  ucCanvasBuffer=0x00;
      }
   }         

}





void display_clear(unsigned char ucFillDate) // 清屏全部显示空填充0x00   全部显示点阵用0xff
{   

    unsigned char x,y;
    WriteCommand(0x34);//关显示缓冲指令            
    WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
    y=0;
    while(y<32)//y轴的范围0至31
    {
         WriteCommand(y+0x80);      //垂直地址
         WriteCommand(0x80);          //水平地址
         for(x=0;x<32;x++)//256个横向点,有32个字节
         {
            LCDWriteData(ucFillDate);
         }
         y++;
    }
    WriteCommand(0x36); //开显示缓冲指令

}

/* 注释五:
* 把字模插入画布的函数.
* 这是本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是在画布中的坐标体系。
* x的范围是0至3,因为画布的横向只要4个字节。y的范围是0至15,因为画布的纵向只有16行。
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
*/
void insert_buffer_to_canvas(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;
   for(j=0;j<y_amount;j++)
   {
      for(i=0;i<x_amount;i++)
      {
                   ucTemp=ucArray;
                   if(ucFbFlag==0)
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=ucTemp; //这里的4代表画布每一行只有4个字节
                   }
                   else
                   {
            ucCanvasBuffer[(y+j)*4+x+i]=~ucTemp; //这里的4代表画布每一行只有4个字节
                   }
      }
   }         

}

/* 注释六:
* 显示任意点阵函数.
* 注意,本函数在前几节的基础上多增加了第7个参数uiOffSetAddr,它是偏移地址。
* 对于这个函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 第7个参数uiOffSetAddr是偏移地址,代表字模数组的从第几个数据开始显示。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount,unsigned int uiOffSetAddr)
{
   unsigned int j=0;
   unsigned int i=0;
   unsigned char ucTemp;

//注意,要把以下两行指令屏蔽,否则屏幕在更新显示时会整屏闪动
//WriteCommand(0x34);//关显示缓冲指令            
//WriteCommand(0x34);//关显示缓冲指令故意写2次,怕1次关不了 这个是因为我参考到某厂家的驱动程序也是这样写的
   for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
   {
       WriteCommand(y+j+0x80);      //垂直地址
       WriteCommand(x+0x80);          //水平地址
       for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
       {
         ucTemp=ucArray; //uiOffSetAddr是字模数组的偏移地址
         if(ucFbFlag==1)//反白显示
         {
               ucTemp=~ucTemp;
         }
         LCDWriteData(ucTemp);
          //         delay_short(30000);//把上一节这个延时函数去掉,加快刷屏速度
      }
   }
   WriteCommand(0x36); //开显示缓冲指令
}




void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}


void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}

总结陈词:
    我前面几节液晶屏程序的字模都是通过外围工具软件生成的,其实这款12864液晶模块本身就是自带字库,编程的时候只要在源代码里直接写入所需要的汉字或者字符,就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字就可以调用到这个汉字的字库?在这个过程中,C51编译器到底还干了哪些鲜为人知的好事?欲知详情,请听下回分解-----液晶屏自带字库跟汉字机内码的关系。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-11-3 15:37

第八十节:调用液晶屏内部字库来显示汉字或字符的坐标体系和本质。

开场白:
前面章节讲的内容全部都是用自构字库的,相当于使用液晶屏的图像模式。其实这个款12864液晶屏的驱动芯片是st7920,它内部是自带16x16字库的,可以显示16x16的汉字或者8x16的字符。这一节开始就跟大家讲讲这方面的内容。要教会大家四个知识点:
第一个:内部字库的真实坐标体系的本质。当我们用内部字库的时候,它的坐标体系跟前面讲的自造字库坐标不一样,不再是256x32的液晶屏。它还原成为128x64的液晶屏,横坐标x轴坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,因此128个点的x轴坐标范围是0至8。而y轴的坐标也是以16个点(2个字节)为一个单位,因此64个点的x轴坐标范围是0至3。把12864液晶屏分成4行8列,每个数代表一个坐标点。
第二个:在使用内部字库时,C51编译器暗地里干了啥?如果使用液晶屏内部自带字库,编程的时候只要在源代码里直接写入所需要的汉字或者字符,就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字就可以调用到这个汉字的字库?其实,表面上我们写下具体的某个汉字或者字符,但是C51编译器会自动对数组内的汉字翻译成 机内码(2字节),会自动对数组内的字符翻译成 ASCII码(1字节)。
第三个:12864的控制芯片st7920内部有两套驱动显示指令方式,一种是前面章节讲的自构字库模式,也是图像模式。另外一种就是本节讲的用内部字库模式。在切换模式的时候,发送命令字0x0c表示用内部字库模式,发送命令字0x36表示用自构字库模式。
第四个:12864整屏有4行8列,一共32个坐标点,每个坐标点可以显示一个16x16的汉字,但是在显示8x16字符时候,必须一次显示2个字符筹够16x16的点阵。例如,只想达到显示一个字符的时候,应该在另外一个空位置上显示空字符来填充。

具体内容,请看源代码讲解。


(1)硬件平台:基于朱兆祺51单片机学习板。

(2)实现功能:
   开机上电后,液晶屏第一行调用直接汉字书写方式的数组来显示(馒头V5)的内容。第四行调用机内码和ASCII码的数组来显示(馒头V5)的内容。

(3)源代码讲解如下:
#include "REG52.H"

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(void); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。

void display_hz1616(unsigned int x,unsigned int y,const unsigned char*ucArray);
void display_double_zf816(unsigned int x,unsigned int y,const unsigned char*ucArray1,const unsigned char*ucArray2);

void delay_short(unsigned int uiDelayshort); //延时

/* 注释一:内部字库的真实坐标体系的本质。
* 当我们用内部字库的时候,它的坐标体系跟前面讲的自造字库坐标不一样,不再是256x32的液晶屏。
* 它还原成为128x64的液晶屏,横坐标x轴坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,
* 因此128个点的x轴坐标范围是0至8。而y轴的坐标也是以16个点(2个字节)为一个单位,因此64个点的x轴
* 坐标范围是0至3。以下是坐标地址的位置编码。把12864液晶屏分成4行8列,每个数代表一个坐标点,
* 用深究具体含义,液晶驱动芯片ST7920的手册上有提到。
*/
code unsigned charucAddrTable[]=//调用内部字库时,液晶屏的坐标体系,位置编码,是驱动内容,读者可以不用深究它的含义。
{   
0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,
0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,
0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
};


/* 注释二:在使用内部字库时,C51编译器暗地里干了啥?
* 如果使用液晶屏内部自带字库,以下编程的时候只要在源代码里直接写入所需要的汉字或者字符,
* 就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字
* 就可以调用到这个汉字的字库?其实,表面上我们写下具体的某个汉字或者字符,但是C51编译器
* 会自动对数组内的汉字翻译成 机内码(2字节),会自动对数组内的字符翻译成 ASCII码(1字节)。
* 本节程序会做这个实验来验证它。以下两种书写方式不一样,但本质是一样的。
*/

code unsigned char Hz1616_man[]="馒"; //对于数组内的汉字,编译会自动翻译成 机内码(2字节)
code unsigned char JN1616_man[]=//机内码馒网上有很多把汉字或者字符转换成相关编码的工具软件
{
0xC2,
0xF8,
};

code unsigned char Hz1616_tou[]="头"; //对于数组内的汉字,编译会自动翻译成 机内码(2字节)
code unsigned char JN1616_tou[]=//机内码头网上有很多把汉字或者字符转换成相关编码的工具软件
{
0xCD,
0xB7,
};

code unsigned char Zf816_V[]="V";   //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_V[]= //ASCII码V网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x56,
};

code unsigned char Zf816_5[]="5";   //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_5[]= //ASCII码5网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x35,
};


code unsigned char Zf816_nc[]=" ";   //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_nc[]= //ASCII码空字符网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x20,
};


void main()
{
      LCDInit(); //初始化12864 内部包含液晶模块的复位

/* 注释三:
* 12864的控制芯片st7920内部有两套驱动显示指令方式,一种是前面章节讲的自构字库模式,也是图像模式。
* 另外一种就是本节讲的用内部字库模式。以下是切换模式的命令,命令字0x0c表示用内部字库模式。
* 命令字0x36表示用自构字库模式。
*/
      WriteCommand(0x0C); //命令字0x0c表示用内部字库模式。命令字0x36表示用自构字库模式。

      display_clear(); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。


      display_hz1616(0,0,Hz1616_man);//第一行,调用直接汉字书写方式的数组来显示(馒头V5),
      display_hz1616(1,0,Hz1616_tou);
      display_double_zf816(2,0,Zf816_V,Zf816_5);

      display_hz1616(0,3,JN1616_man);//第四行,调用机内码和ASCII码的数组来显示(馒头V5),
      display_hz1616(1,3,JN1616_tou);
      display_double_zf816(2,3,ASCII816_V,Zf816_5);


      while(1)
      {
         ;
      }

}


/* 注释四:在一个坐标点显示1个内部字库汉字的函数
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数*ucArray是汉字机内码,是有2个字节的数组。
*/
void display_hz1616(unsigned int x,unsigned int y,const unsigned char*ucArray)
{
    WriteCommand(0x30);   //基本指令集
        WriteCommand(ucAddrTable);        //起始位置
        LCDWriteData(ucArray);
        LCDWriteData(ucArray);
}

/* 注释五:在一个坐标点显示2个内部字库字符的函数
* 注意,由于一个坐标点是16x16点阵,而一个字符是8x16点阵的,所以务必要显示2个字符筹够1个坐标点。
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数*ucArray1是左边第1个字符ASCII码,是有1个字节的数组。
* 第4个参数*ucArray2是右边第2个字符ASCII码,是有1个字节的数组。
*/
void display_double_zf816(unsigned int x,unsigned int y,const unsigned char *ucArray1,const unsigned char*ucArray2)
{
    WriteCommand(0x30);   //基本指令集
        WriteCommand(ucAddrTable);        //起始位置
        LCDWriteData(ucArray1);
        LCDWriteData(ucArray2);
}


void display_clear(void) // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
{   

    unsigned int i,j;
        for(i=0;i<4;i++)
        {
                for(j=0;j<8;j++)
                {
                   display_double_zf816(j,i,Zf816_nc,ASCII816_nc);//Zf816_nc与ASCII816_nc本质是一样的,只是书写方式不一样。
                }
        }


}

void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



void delay_short(unsigned int uiDelayShort) //延时函数
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}
总结陈词:
    通过本节的实验,我们发现汉字的识别本质是机内码,字符的识别本质是ASCII码。不管是机内码还是ASCII码,这些都是16进制的数字,也就是我们手机平时接收和发送的信息本质都是这些数字编码,但是机内码是2个字节,ASCII码是1个字节,如果在一串随机的信息中,同时包含汉字和字符两种数字信息,我们的程序又该如何能筛选和识别它们,会不会把机内码和ASCII码搞混乱了?不会的。其实这两种编码都是有规律可以筛选识别的,欲知详情,请听下回分解-----液晶屏显示串口发送过来的任意汉字和字符。

(未完待续,下节更精彩,不要走开哦)

missuu 发表于 2014-11-6 00:23

嗯嗯很好

jianhong_wu 发表于 2014-11-6 16:48

第八十一节:液晶屏显示串口发送过来的任意汉字和字符。

开场白:
通过上一节的学习,我们发现汉字的识别本质是机内码,字符的识别本质是ASCII码。不管是机内码还是ASCII码,这些都是16进制的数字,也就是我们手机平时接收和发送的信息本质都是这些数字编码,但是机内码是2个字节,ASCII码是1个字节,如果在一串随机的信息中,同时包含汉字和字符两种数字信息,我们的程序又该如何能筛选和识别它们,会不会把机内码和ASCII码搞混乱了?这一节要教大家三个知识点:
第一个:ASCII码与汉字机内码不一样的规律是,ASCII码都是小于128(0x80)的,根据这个特点可以编程序把它们区分开来。
第二个:当任意一串信息中既包含汉字机内码,又包含字符ASCII码时,并且当ASCII码左右相邻个数是以奇数存在的时候,如何巧妙地插入填充空格字符0x20使它们能够符合一个坐标点显示2个字符的要求。
第三个:本节程序串口部分是在第39节内容基础上移植修改而成,本节程序中多添加了如何通过结束标志0x0D 0x0A来提取有效数据的内容,读者可以学习一下其中的框架。

具体内容,请看源代码讲解。


(1)硬件平台:基于朱兆祺51单片机学习板。

(2)实现功能:
   开机上电后,液晶屏第1行显示“请发送信息”。 任意时刻,从电脑“串口调试助手”根据以下协议要求,发送一串不超过24个汉字或者字符的信息,液晶屏就实时把这些信息显示在第2,3,4行。并且蜂鸣器会鸣叫一声表示数据接收正确。

波特率是:9600 。
通讯协议:EB 00 55XX XX XX XX …XX XX 0D 0A
最前面3个字节EB 00 55 表示数据头。
最后面2个字节0D 0A表示信息的结束标志。
中间的XX是机内码和ASCII码信息。比如:要发送“曹健1人学习51单片机”的信息,它们对应的指令是:
EB 00 55 B2 DC BD A1 31 C8 CB D1 A7 CF B0 35 31 B5 A5 C6 AC BB FA 0D 0A

(3)源代码讲解如下:
#include "REG52.H"


/* 注释一:
* 本程序的串口那部分内容是从《第三十九节:判断数据头来接收一串数据的串口通用程序框架。》
* 移植过来的,但是以下要把接收缓冲区的数据从10改成60.同时,协议后面多增加了数据结束标志0x0d 0x0a。
*/

#define const_rc_size60//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

#define const_voice_short40   //蜂鸣器短叫的持续时间

sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);
void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里

void display_service(void); //显示服务程序,在main函数里
void empty_diaplay_buffer(void); //把显示缓冲区全部填充空格字符0x20
void diaplay_all_buffer(void); //显示第2,3,4行全部缓冲区的内容

void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(void); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned charucArray2); //在一个坐标点显示1个汉字或者2个字符的函数
void delay_short(unsigned int uiDelayshort); //延时


code unsigned charucAddrTable[]=//调用内部字库时,液晶屏的坐标体系,位置编码,是驱动内容,读者可以不用深究它的含义。
{   
0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,
0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,
0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
};

code unsigned char JN1616_qing[]=//机内码请
{
0xC7,0xEB, //请
};

code unsigned char JN1616_fa[]=//机内码发
{
0xB7,0xA2,
};

code unsigned char JN1616_song[]=//机内码送
{
0xCB,0xCD,
};

code unsigned char JN1616_xin[]=//机内码信
{
0xD0,0xC5,
};

code unsigned char JN1616_xi[]=//机内码息
{
0xCF,0xA2,
};

unsigned intuiSendCnt=0;   //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量

unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器

unsigned char ucWd1Update=1; //窗口1的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part1Update=0; //窗口1的第1个局部更新显示变量1代表更新显示,响应函数内部会清零

unsigned char ucDispplayBuffer; //第2,3,4行显示内容的缓冲区

void main()
{
      initial_myself();
      delay_long(100);   
      initial_peripheral();

      while(1)
      {
            usart_service();//串口服务程序
                        display_service(); //显示服务程序
      }

}



/* 注释二:在一个坐标点显示1个汉字或者2个字符的函数
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数ucArray1是第1个汉字机内码或者ASCII码。
* 第4个参数ucArray2是第2个汉字机内码或者ASCII码。
*/
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned charucArray2)
{
    WriteCommand(0x30);   //基本指令集
    WriteCommand(ucAddrTable);      //起始位置
    LCDWriteData(ucArray1);
    LCDWriteData(ucArray2);
}


void display_clear(void) // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
{   

    unsigned int i,j;
      for(i=0;i<4;i++)
      {
                for(j=0;j<8;j++)
                {
                   display_double_code(j,i,0x20,0x20);//0x20是空格的ASCII码
                }
      }


}

void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}


void empty_diaplay_buffer(void) //把显示缓冲区全部填充空格字符0x20
{
   unsigned int i;

   for(i=0;i<48;i++)
   {
      ucDispplayBuffer=0x20; //第2,3,4行显示内容的缓冲区全部填充0x20空格字符
   }

}

void diaplay_all_buffer(void) //显示第2,3,4行全部缓冲区的内容
{
   unsigned int i,j;

   for(i=0;i<3;i++) //i代表行数
   {
      for(j=0;j<8;j++) //j代表某行的某个坐标在第几列
      {
         display_double_code(j,i+1,ucDispplayBuffer,ucDispplayBuffer); //这里的16代表一行可以显示16个字符
      }
   }

}


void display_service(void) //显示服务程序,在main函数里
{
if(ucWd1Update==1)//窗口1整屏更新,里面只放那些不用经常刷新显示的内容
    {
      ucWd1Update=0;//及时清零,避免一直更新

      ucWd1Part1Update=1; //激活窗口1的第1个局部更新显示变量

      display_clear(); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。

                //显示第一行固定的内容:请发送信息
      display_double_code(1,0,JN1616_qing,JN1616_qing);      //请
      display_double_code(2,0,JN1616_fa,JN1616_fa);          //发
      display_double_code(3,0,JN1616_song,JN1616_song);      //送
      display_double_code(4,0,JN1616_xin,JN1616_xin);      //信
      display_double_code(5,0,JN1616_xi,JN1616_xi);          //息

    }

    if(ucWd1Part1Update==1) //窗口1的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd1Part1Update=0; //及时清零,避免一直更新

      diaplay_all_buffer(); //显示第2,3,4行全部缓冲区的内容
        }
}


/* 注释三:
* 以下有效信息截取和如何判断机内码与ASCII码是本程序的核心,请仔细看讲解。
* 凡是ASCII码都是小于0x80(128)的,根据这个特点可以把ASCII码和机内码分离出来,
* 同时,由于液晶屏的1个坐标必须显示2个编码,对于单个存在的ASCII码,我们要在
* 它的右边多插入一个空格字符0x20。至于如何插入空格0x20字符,请看以下代码。
*/
void usart_service(void)//串口服务程序,在main函数里
{
   unsigned int i;
       unsigned int uiCodeCnt; //统计接收的有效编码数量
       unsigned int uiCodeYu;//对uiCodeCnt求2的余数,方便识别是否是1个ASCII码相邻
   if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
   {


            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动这个变量是用来抗干扰处理的

            while(uiRcregTotal>=6&&uiRcMoveIndex<=(uiRcregTotal-6)) //这里的6表示有3个字节的数据头,至少1个有效数据,2个数据结束标志0x0d 0x0a
            {
               if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
               {

                              empty_diaplay_buffer(); //把显示缓冲区全部填充空格字符0x20
                                  uiCodeCnt=0; //统计接收的有效编码数量清零
                  for(i=0;i<(uiRcregTotal-uiRcMoveIndex-3)&&i<48;i++)//这里的3表示有3个字节的数据头。48表示最大只能接收24个汉字,一共48个字节的机内码.
                                  {
                      if(ucRcregBuf==0x0d&&ucRcregBuf==0x0a)//结束标志0x0d 0x0a的判断
                      {
                           uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,表示数据接收正确完毕
                                                   ucWd1Part1Update=1; //及时更新显示第2,3,4行内容的信息
                                                   break; //退出for循环
                      }       
                                       else//收集有效信息编码进入显示缓冲区
                                          {
                                              uiCodeYu=uiCodeCnt%2; //对2求余数,用来识别相信的2个是否是机内码,否则要进行插入填充0x20处理
                                                  if(uiCodeYu==1)
                                                  {
                                                     if(ucRcregBuf>=0x80&&ucRcregBuf<0x80) //如果当前的是机内码,而上一个不是机内码
                                                       {
                                                               ucDispplayBuffer=0x20; //当前的先填充插入空格字符0x20
                                                                uiCodeCnt++;   //统计接收的有效编码数量
                                                       }
                                                  }
                                              ucDispplayBuffer=ucRcregBuf; //收集有效信息编码进入显示缓冲区
                                              uiCodeCnt++;   //统计接收的有效编码数量
                                          }
                                  }
                  break;   //退出while循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
         }
                                       
         uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据

   }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断


if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
{
          uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;   //开自锁标志
}

if(uiVoiceCnt!=0)
{
   uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
   beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

}
else
{
   ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
   beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}


TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}


void usart_receive(void) interrupt 4               //串口接收数据中断      
{      

   if(RI==1)
   {
      RI = 0;

            ++uiRcregTotal;
      if(uiRcregTotal>const_rc_size)//超过缓冲区
      {
         uiRcregTotal=const_rc_size;
      }
      ucRcregBuf=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
      uiSendCnt=0;//及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
   
   }
   else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
      TI = 0;
   }
                                                         
}                              


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{

beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;


//配置串口
SCON=0x50;
TMOD=0X21;
IP =0x10;//把串口中断设置为最高优先级,必须的。
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;   //开总中断
   ES=1;   //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断


   LCDInit(); //初始化12864 内部包含液晶模块的复位
   WriteCommand(0x0C); //命令字0x0c表示用内部字库模式。命令字0x36表示用自构字库模式。
   empty_diaplay_buffer(); //把显示缓冲区全部填充空格字符0x20
}





总结陈词:
我们现在是调用液晶屏内部字库来显示内容,如果要某行内容反显或者光标闪烁改怎么编程?欲知详情,请听下回分解-----如何在调用液晶屏内部字库时让某行内容反显或者光标闪烁。

(未完待续,下节更精彩,不要走开哦)

jianhong_wu 发表于 2014-11-14 12:32

第八十二节:如何通过调用液晶屏内部字库把一个任意数值的变量显示出来。

开场白:
本来这一节打算开始讲调用液晶屏内部字库时的反显程序,但是我担心跳跃太大,恐怕很多初学者跟不上,所以多插入这一节讲讲后面菜单程序中经常用到的基本功能,在调用内部字库的情况下,如何把一个任意数值的变量显示在液晶屏上。这一节的功能需求跟前面第76节是一模一样的,只不过前面的不是用自带字库,现在的是用自带字库而已。我们还是需要做一个变量转换成ASCII码的函数,以后只要调用这个转换函数就可以了。这一节就要把这个转换函数和框架思路教给大家。

具体内容,请看源代码讲解。

(1)硬件平台:
    基于朱兆祺51单片机学习板。

(2)实现功能:我们定义一个char型的全局变量,把它默认初始化为218,开机上电后,能看到正中间恰好显示这个全局变量的数值218。大家也可以试着更改它的默认初始值,只要不超过char型最大数值255范围,我们就会看到它上电后显示的就是这个初始值。

(3)源代码讲解如下:
#include "REG52.H"


sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);

unsigned char *number_to_ASCII(unsigned charucBitNumber);
void display_service(void); //显示服务程序,在main函数里


void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData);   //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_clear(void); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned charucArray2); //在一个坐标点显示1个汉字或者2个字符的函数
void delay_short(unsigned int uiDelayshort); //延时


code unsigned charucAddrTable[]=//调用内部字库时,液晶屏的坐标体系,位置编码,是驱动内容,读者可以不用深究它的含义。
{   
0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,
0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,
0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
};

code unsigned char ASCII816_0[]="0";   //0对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_1[]="1";   //1
code unsigned char ASCII816_2[]="2";   //2
code unsigned char ASCII816_3[]="3";   //3
code unsigned char ASCII816_4[]="4";   //4
code unsigned char ASCII816_5[]="5";   //5
code unsigned char ASCII816_6[]="6";   //6
code unsigned char ASCII816_7[]="7";   //7
code unsigned char ASCII816_8[]="8";   //8
code unsigned char ASCII816_9[]="9";   //9
code unsigned char ASCII816_nc[]=" ";//空格

/* 注释一:
* 以下变量就是本程序的任意变量,网友可以自己更改它的大小来测试本程序,不要超过255.
*/
unsigned char ucAnyNumber=218;//任意变量默认初始化为218。
unsigned char ucWd1Part1Update=1; //窗口1的第1个局部更新显示变量1代表更新显示,响应函数内部会清零


void main()
{
      initial_myself();
      delay_long(100);   
      initial_peripheral();

      while(1)
      {
         display_service(); //显示服务程序
      }

}



/* 注释二:在一个坐标点显示1个汉字或者2个字符的函数
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数ucArray1是第1个汉字机内码或者ASCII码。
* 第4个参数ucArray2是第2个汉字机内码或者ASCII码。
*/
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned charucArray2)
{
    WriteCommand(0x30);   //基本指令集
    WriteCommand(ucAddrTable);      //起始位置
    LCDWriteData(ucArray1);
    LCDWriteData(ucArray2);
}


void display_clear(void) // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
{   

    unsigned int i,j;
      for(i=0;i<4;i++)
      {
                for(j=0;j<8;j++)
                {
                   display_double_code(j,i,0x20,0x20);//0x20是空格的ASCII码
                }
      }


}

void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
      unsigned char i;
      for ( i = 0; i < 8; i++ )
      {
                if ( (ucData << i) & 0x80 )
                {
                        LCDSID_dr = 1;
                }
                else
                {
                        LCDSID_dr = 0;
                }
                LCDCLK_dr = 0;
                LCDCLK_dr = 1;
      }
}

void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
      SendByteToLcd( 0xf8 + (ucWRS << 1) );
      SendByteToLcd( ucWData & 0xf0 );
      SendByteToLcd( (ucWData << 4) & 0xf0);
}


void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{

      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucCommand, 0);
      delay_short(90);
}

void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
      LCDCS_dr = 0;
      LCDCS_dr = 1;
      SPIWrite(ucData, 1);
}

void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
      LCDRST_dr = 1;//复位
      LCDRST_dr = 0;
      LCDRST_dr = 1;
}



/* 注释三:
* 本程序的核心转换函数。
* 是可以把一位任意数字变量的函数转换成对应的ASCII码,由于ASCII码放在数组里,所以返回的是指针,代表数组的首地址。
*/
unsigned char *number_to_ASCII(unsigned charucBitNumber)
{
      unsigned char *p_ucAnyNumber;//此指针根据ucBitNumber数值的大小,分别调用不同的ASCII码。

      switch(ucBitNumber)//根据ucBitNumber数值的大小,分别调用不同的ASCII码。
      {
            case 0:
                  p_ucAnyNumber=ASCII816_0;
                  break;
            case 1:
                  p_ucAnyNumber=ASCII816_1;
                  break;
            case 2:
                  p_ucAnyNumber=ASCII816_2;
                  break;
            case 3:
                  p_ucAnyNumber=ASCII816_3;
                  break;
            case 4:
                  p_ucAnyNumber=ASCII816_4;
                  break;
            case 5:
                  p_ucAnyNumber=ASCII816_5;
                  break;
            case 6:
                  p_ucAnyNumber=ASCII816_6;
                  break;
            case 7:
                  p_ucAnyNumber=ASCII816_7;
                  break;
            case 8:
                  p_ucAnyNumber=ASCII816_8;
                  break;
            case 9:
                  p_ucAnyNumber=ASCII816_9;
                  break;
            case 10:
                  p_ucAnyNumber=ASCII816_nc;
                  break;
            default:   //如果上面的条件都不符合,那么默认指向空格ASCII码
                  p_ucAnyNumber=ASCII816_nc;
                  break;
      }

      return p_ucAnyNumber;//返回转换结束后的指针
}


void display_service(void) //显示服务程序,在main函数里
{
/* 注释四:
* 这里的局部变量用static关键词修饰,是因为这个函数一直在主函数while(1)里循环扫描,我不希望它每次进来这个函数
* 都多花几条指令去初始化这些局部变量,这样会多耗掉几个指令,所以我就用static关键字避免了这种情况,让这些局部变量
* 只在上电那一刻就初始化了,以后每次进来这个函数不用再初始化这些变量。
*/
    static unsigned char ucAnyNumber_1; //分解变量的个位
    static unsigned char ucAnyNumber_10; //分解变量的十位
    static unsigned char ucAnyNumber_100; //分解变量的百位

    static unsigned char *p_ucAnyNumber_1; //经过数字转换成字模后,分解变量的个位字模首地址
    static unsigned char *p_ucAnyNumber_10; //经过数字转换成字模后,分解变量的十位字模首地址
    static unsigned char *p_ucAnyNumber_100; //经过数字转换成字模后,分解变量的百位字模首地址


    if(ucWd1Part1Update==1) //窗口1的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
      ucWd1Part1Update=0; //及时清零,避免一直更新

      if(ucAnyNumber>=100) //有3位数以上
      {
         ucAnyNumber_100=ucAnyNumber/100; //百位
      }
      else //否则显示空
      {
         ucAnyNumber_100=10;//在下面的转换函数中,代码10表示空字模
      }

      if(ucAnyNumber>=10) //有2位数以上
      {
         ucAnyNumber_10=ucAnyNumber%100/10;//十位
      }
      else //否则显示空
      {
         ucAnyNumber_10=10;//在下面的转换函数中,代码10表示空字模
      }

      ucAnyNumber_1=ucAnyNumber%10/1;//个位

      p_ucAnyNumber_100=number_to_ASCII(ucAnyNumber_100); //把数字转换成字符ASCII码      
      p_ucAnyNumber_10=number_to_ASCII(ucAnyNumber_10);   //把数字转换成字符ASCII码
      p_ucAnyNumber_1=number_to_ASCII(ucAnyNumber_1);   //把数字转换成字符ASCII码

      display_double_code(2,1,ASCII816_nc,p_ucAnyNumber_100);//液晶屏的显示驱动函数这里的ASCII816_nc代表填充显示一个空格字符
      display_double_code(3,1,p_ucAnyNumber_10,p_ucAnyNumber_1);//液晶屏的显示驱动函数

    }
}


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;
   for(i=0;i<uiDelayShort;i++)
   {
   ;
   }
}

void delay_long(unsigned int uiDelayLong)
{
   unsigned int i;
   unsigned int j;
   for(i=0;i<uiDelayLong;i++)
   {
      for(j=0;j<500;j++)//内嵌循环的空指令数量
          {
             ; //一个分号相当于执行一条空语句
          }
   }
}


void initial_myself(void)//第一区 初始化单片机
{

beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。



}

void initial_peripheral(void) //第二区 初始化外围
{

   LCDInit(); //初始化12864 内部包含液晶模块的复位
   WriteCommand(0x0C); //命令字0x0c表示用内部字库模式。命令字0x36表示用自构字库模式。
   display_clear(); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。

}




总结陈词:
在液晶屏程序里,经常要用到反显的功能来表示选中某一项菜单。在调用内部字库时,这样的驱动程序又该怎么写?欲知详情,请听下回分解-----如何在调用液晶屏内部字库时让某行内容反显。

(未完待续,下节更精彩,不要走开哦)

KODA 发表于 2014-11-20 12:16

学习单片机一直很迷茫感谢楼主的分享
页: 1 2 3 4 5 6 [7] 8 9 10
查看完整版本: 从业将近十年!手把手教你单片机程序框架(连载)