LCD12864串口高级操作

在这里插入图片描述
上面是LCD12864的串口通信时序图。其中RW是方向位,RS是命令数据选择位,SID为数据线,SCLK为时钟线,CS为使能端。

其中CS为1时使能时序操作,由图可以看出,数据线在时钟线为低电平的时候变化,在时钟线为高电平时锁存。一次完整的通信由3个字节组成,第一个字节是引导码,由固定的5为高电平,1位方向位,1位命令数据选择位,1位低电平组成,第二个字节是数据或命令的高4位+4个低电平,第三个字节是数据或命令的低4位+4个低电平。

接下来我们实现基本时序。
为了程序方便移植,我在这里创建了一个GPIO管理结构体

//LCD12864.H文件
//GPIO管理控件
typedef struct gpio_set
{
	GPIO_TypeDef*  Port;
	uint16_t Pin;
}gpio_set_t;

//IO操作宏定义
#define LCD_IO_SET(Type,Val)  GPIO_WriteBit(LCD_setGPIO[Type].Port, LCD_setGPIO[Type].Pin,(BitAction)(Val))

#define LCD_DELAY_TIME   20  //LCD通讯脉宽调整宏定义
#define LCD_HIGH    0   //LCD接口处的电平状态值
#define LCD_LOW     1   //LCD接口处的电平状态值
#define LCD_CMD			1		//命令选择
#define LCD_DAT			0		//数据选择
//LCD12864.c文件
gpio_set_t LCD_setGPIO[3] = {
	{GPIOD,GPIO_Pin_14},
	{GPIOD,GPIO_Pin_13},
	{GPIOD,GPIO_Pin_12}
};

#define LCDCLK 0 //E
#define LCDSTD 1 //RW
#define LCDCS  2 //RS

通过上面提供的代码片段,我们可以很方便的操作IO口,以及在各平台移植。
继续。

///
//函数名:LCD_SendByte			    ///
//功  能:串口发送1byte字节           ///
//参  数:Data:要发送的字节           ///
///
static void LCD_SendByte(u8 Data)
{
	u8 i = 0;
	LCD_IO_SET(LCDCS,LCD_HIGH);//使能
	LCD_IO_SET(LCDCLK,LCD_LOW);//时钟线拉低
	delay_us(LCD_DELAY_TIME);//延时
	for(i = 0;i<8;i++)
	{
		LCD_IO_SET(LCDCLK,LCD_LOW);//时钟线拉低
		
		LCD_IO_SET(LCDSTD,((Data<<i) & 0x80)?LCD_HIGH:LCD_LOW);//电平变化
		delay_us(LCD_DELAY_TIME);//延时
		
		LCD_IO_SET(LCDCLK,LCD_HIGH);//时钟线拉高
		delay_us(LCD_DELAY_TIME);//延时
		
		LCD_IO_SET(LCDCLK,LCD_LOW);//时钟线拉低
	}
	
	LCD_IO_SET(LCDCLK,LCD_LOW);//时钟线拉低
	LCD_IO_SET(LCDCS,LCD_LOW);//失能
}
	
/
//函数名:LCD_Write							                             //
//功  能:给LCD写入一字节命令或数据                          //
//参  数:CmdSelect:1--Cmd,0--Data dat:要发送的字节          //
/
void LCD_Write(u8 CmdSelect,u8 dat)
{
	LCD_SendByte(CmdSelect? 0xf8 : 0xfa);//RW位为0:写。RS位:1--数据,0--命令
	LCD_SendByte( dat & 0xf0);//高4位
	LCD_SendByte((dat & 0x0f)<<4);//低4位
}	

OK,我们已经实现了基本的底层串口通信。我们只需要微调LCD_DELAY_TIME 宏定义来适当让通信速率处于一个合适的频率即可。

接下来是初始化IO口和LCD的初始化函数,这个就不用细讲了,按数据手册照着写就对了。


///                     
//函数名:LCD_Init							                           //                     
//功  能:初始化函数,包括初始化IO口和LCD指令               //
//参  数:																		             //                     
///                     
void LCD_Init(void)
{
	
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
	
	GPIO_InitStructure.GPIO_Pin  =  LCD_setGPIO[LCDCLK].Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(LCD_setGPIO[LCDCLK].Port, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin  =  LCD_setGPIO[LCDSTD].Pin;
	GPIO_Init(LCD_setGPIO[LCDSTD].Port, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin  =  LCD_setGPIO[LCDCS].Pin;
	GPIO_Init(LCD_setGPIO[LCDCS].Port, &GPIO_InitStructure);
	
	
	LCD_Write(LCD_CMD,0x38);	  delay_ms(5);
	LCD_Write(LCD_CMD,0x38);	  delay_ms(5);
	LCD_Write(LCD_CMD,0x38);	  delay_ms(5);
	LCD_Write(LCD_CMD,0x06);	  delay_ms(5);
	LCD_Write(LCD_CMD,0x01);	  delay_ms(5);
	LCD_Write(LCD_CMD,0x0c);	  delay_ms(5);	
}

接着提供最基本的LCD显示字符串。值得注意的是一个汉字占2个字节,它不能以奇字节开头。比如LCD_StringPlay(0,0,“0好好学习”);这样操作是会乱码的。
正确的操作是LCD_StringPlay(0,0," 0好好学习");增加一个空格,让汉字对齐在偶数字节即可。

///
//函数名:LCD_StringPlay							             //
//功  能:指定地址显示字符串                       //
//参  数:x,y:地址。CorpInf:字符串指针             //
//		坐标定义:                                  //
//								x(0,7)                       //
//		*------------>                             //
//		|                                          //
//		|                                          //
//		|                                          //
//		| y(0,3)                                  //
//		v                                          //
///
void LCD_StringPlay(u8 x,u8 y,const char *CorpInf)
{
	u8 address; 
	
//	if(y==0){Address=0x80+x;}     
//	if(y==1){Address=0x90+x;}        
//	if(y==2){Address=0x88+x;}    
//	if(y==3){Address=0x98+x;}  
	
	//此处纯粹是位运算炫技,大概会比上面用法快那么一丁丁丁点。我编译了一下,也就
	//省10来个字节的ROM。有兴趣的可以对照上面逐句分析。
	
	address = 0x80 + ((y&0x01)<<4) + ((y&0x02)<<2) + x;
	
	LCD_Write(LCD_CMD,address);
	delay_ms(5);
	while(*CorpInf)
	{
		LCD_Write(LCD_DAT,*CorpInf++);
		delay_ms(1);
	}
	delay_ms(1);
}

如果是在STM32这种平台上,用上面那种低级的字符串操作函数,显得太抠了。
接下来通过C标准库实现printf打印函数,为了方便移植到51等资源较少的单片机上,我加了个宏定义来裁剪该功能。

该函数稍微改一下也可以移植到串口上。

#if (LCD_FMT_EN == 1)

#include "stdio.h"
#include "string.h"
#include "stdarg.h"

void LCD_Printf(u8 x,u8 y,const char *fmt,...)
{
	va_list ap;
	char string[64];
	va_start(ap,fmt);
	vsnprintf(string,64,fmt,ap);
	LCD_StringPlay(x,y,string);
	va_end(ap);
	
	//sprintf(str,"或者用以下替换%d",a);
	//LCD_StringPlay(x,y,string);
}
#endif 

基本的LCD12864操作就是上面这些,下面我们来搞一下绘图区。
LCD12864有两个显示区,一个用来显示字库中的图形,一个用来显示自己写的数据。通过指令0x30和0x36切换。具体见数据手册。

/
//函数名:LCD_RamPlay							                           //
//功  能:播放显示一整页数据                                 //
//			 可用来显示图片																		 //
//			 取模方式为从左到右逐行写
//参  数:*str:数据指针,播放该指针后面的1024字节数据(128*64)  //
/
void LCD_PagePlay(u8 *str)
{
	u8 i,j,k;
	
	LCD_Write(LCD_CMD,0x34);      
  LCD_Write(LCD_CMD,0x36);//扩展指令集
	i = 0x80;            
	for(j = 0;j < 32;j++)//上半屏
	{
      LCD_Write(LCD_CMD,i++);
      LCD_Write(LCD_CMD,0x80);
  		for(k = 0;k < 16;k++)
  		{
  		  LCD_Write(LCD_DAT,*str++);
  		}
	}
	i = 0x80;
 	for(j = 0;j < 32;j++)//下半屏
	{
 	    LCD_Write(LCD_CMD,i++);
      LCD_Write(LCD_CMD,0x88);	   
  		for(k = 0;k < 16;k++)
  		{
   		  LCD_Write(LCD_DAT,*str++);
   		} 
	}       
	LCD_Write(LCD_CMD,0x30);//基本指令集        
}

现在问题来了,如果我们想画圆,画直线,画表格、矩形等怎么操作呢?取一个模?然后调用上面的函数吗?这样肯定不行。
正常的操作是打点,根据算法打点打印出各种数学图形。

但是LCD12864的绘图区由于地址的原因,我们一次最少只能操作2个字节,做不到按位操作。如果我们像在任意地方打点,那我们必须知道该点所在的2个字节的其它bit的情况,不然我们这个地方打了点就会影响到其它地方的点位数据。

所以我们需要知道其它地方的点位数据情况,再通过位运算组成一整个半字,再写入LCD中。
实现打点函数的思路有2种:
1:读出LCD12864该半字地址数据,操作该数据,重新写入该数据。
2 :创建一个1024字节大小的显存,改变该地址对应位置的显存数据,重新写入该位置的显存数据。

方案一,读LCD12864太慢了,基本没什么实用价值。
方案二,费内存,需要开辟1k的RAM。资源太少的单片机实现不了。

我用的平台是STM32F103ZET6,1k字节是小意思。

下面是打点函数的代码:

//LCD显存
static u16 LCD_RAM[512];


//函数名:void LCD_PointPlay(u8 x,u8 y,u8 bitEn)   //
//功  能:打点                                     //
//参  数:x,y:打点坐标,bitEn:1--打点,0--消点      //
//                                                //
//		坐标定义:                                   //
//								x(0,127)                        //
//		*------------>                              //
//		|                                           //
//		|                                           //
//		|                                           //
//		| y(0,63)                                   //
//		v                                           //
									
void LCD_PointPlay(u8 x,u8 y,u8 bitEn)
{
	u16 point = ((u16)y<<7) + x;//x*128+y :当前点位序号
	u16 Index = point>>4;				//point/16:当前点位所在显存数组下标
	
	if(bitEn)
		LCD_RAM[Index] |=   0x8000>>(point & 0xf);	//point % 16 == 当前点位所在显存半字中的bit位
	else
		LCD_RAM[Index] &=  ~(0x8000>>(point & 0xf));
	LCD_Write(LCD_CMD,0x34);      
  LCD_Write(LCD_CMD,0x36);//扩展指令集
	//写入垂直地址
	LCD_Write(LCD_CMD,0x80 + (y & 0x1f));			
	//写入水平地址   
	//第一行0x80-0x87
	//第二行0x90-0x97
	//第三行0x88-0x8f
	//第四行0x98-0x9f
	LCD_Write(LCD_CMD,0x80 + (y >= 32 ? 0x08 :0) + (x>>4));
	//写入半字数据
	LCD_Write(LCD_DAT,(LCD_RAM[Index]&0xff00)>>8);
	LCD_Write(LCD_DAT,(LCD_RAM[Index]&0x00ff));
	LCD_Write(LCD_CMD,0x30);//基本指令集       
}

我实现打点函数其实不是为了画数学图形,我是为了反白某一行。LCD12864有个反白指令,但这是一条鸡肋指令,它要么反白第一第三行,要么反白第二第四行。着实让人难受…

先来两个清显存函数,一个清显存不显示,一个清显存并显示。


//
//函数名:LCD_RamInit							            //
//功  能:LCD_RAM全局赋值                     //
//参  数:Dat:赋给RAM的值                     //
//
void LCD_PointRamInit(u16 Dat)
{
	u16 x;
	for(x=0;x<512;x++)LCD_RAM[x] = Dat;
}

///
//函数名:LCD_PointPageFill							   //
//功  能:整页填充指定数据                 //
//参  数:Dat:显示的值,0即清屏            //
///
void LCD_PointPageFill(u16 Dat)
{
	u16 x;
	LCD_PointRamInit(0x0000);
	for(x = 0;x< 512 ;x++)
	{
		LCD_RAM[x] = Dat;
	}
	LCD_PagePlay((u8*)LCD_RAM);
}

前面说了LCD12864有两个显示区,我好像忘了说,真实的显示其实是这两个显示区数据的异或值。
异或是什么意思呢,就是如果这个点位有显示表示这个点为1,如果另外一个区这里也有显示表示这个点也为1,它们的异或值就是0;如果另外一个区这里没有显示,即为0,它们的异或值即为1。
明白了吗?我们只要在绘图区给某一行全部写1,即可反白该行。

///
//函数名:LCD_LineInvert							 //
//功  能:LCD指定行反白                //
//参  数:Line(0-3)                   //
//note	:同时只能有一行反白			     //
//			 用来做菜单时的指示					 //
///
void LCD_PointLineInvert(u8 Line)
{
	u16 x;
	LCD_PointRamInit(0x0000);//全部清0
	for(x = (Line<<7);x< (Line+1)<<7;x++)//反白一行
	{
		LCD_RAM[x] = 0Xffff;
	}
	LCD_PagePlay((u8*)LCD_RAM);//显示整个显存
}

打点相关的功能基本就以上,当然你还可以在打点函数的基础上实现画数学图形,以及任意连线等等等…

接下来我们继续讲LCD12864的字符串高级显示。
12864可以显示64个英文字符数字等或者32个汉字。
二话不说,先建立一个64字节大小的显存,用来存储对应位置的字节数据。

static u8 LCD_CharRAM[64];      //字符显存
static u8 chIndex     = 0;			//字符显存实时索引	

我们把整屏的显存先初始化为空格。

///
//函数名:LCD_CharInit							           //
//功  能:显存全部初始化为空格字符,索引清零     //
//参  数:无                                   //
///
void LCD_CharInit(void)
{
	u8 i = 0;
	for(i = 0;i<64;i++){
		LCD_CharRAM[i] = ' ';
	}
	chIndex = 0;
}

接着是光标操作

///
//函数名:LCD_CharCursor							             //
//功  能:光标定位指定地址                         //
//参  数:x,y:光标指定地址。									     //
//		坐标定义:                                  //
//								x(0,15)                        //
//		*------------>                             //
//		|                                          //
//		|                                          //
//		|                                          //
//		| y(0,3)                                   //
//		v                                          //
///
void LCD_CharCursor(u8 x,u8 y)
{
	//第一行0x80-0x87--0
	//第二行0x90-0x97--1
	//第三行0x88-0x8f--2
	//第四行0x98-0x9f--3
	u8 address = 0x80 + ((0x01&y)<<4) + ((0x02&y)<<2) + (x>>1);
	LCD_Write(LCD_CMD,address);	
	chIndex = ((y<<4)+x);
	if(x&0x01){
		LCD_Write(LCD_DAT,(LCD_CharRAM[chIndex-1]));
	}
}

不知道大家有没有看出来,这个函数有很大一部分心思是用在奇列地址定位上。
它的x坐标支持0-15。不像之前那个LCD_StringPlay,它的x坐标只能支持0-7。
当然,汉字仍然不能在奇列位置开始显示。

在上面这个光标定位的基础上,我们可以实现下面这个函数

///
//函数名:LCD_CharOut							          									 //
//功  能:将字符保存到当前显存中,并将当前显存位置的字符输出     //
//参  数:ch:要输出的字符																		 //
///
void LCD_CharOut(u8 ch)
{
	u8 address;
	LCD_CharRAM[chIndex] = ch;
	LCD_Write(LCD_DAT,LCD_CharRAM[chIndex++]);
	chIndex &= 0x3f;
	//第一行0x80-0x87--0x00-0x0f
	//第二行0x90-0x97--0x10-0x1f
	//第三行0x88-0x8f--0x20-0x2f
	//第四行0x98-0x9f--0x30-0x3f
	if(!(chIndex & 0x0f)){
		address = 0x80 + (chIndex & 0x10) + ((chIndex & 0x20)>>2);
		LCD_Write(LCD_CMD,address);
	}
}

这个函数就了不起了…它可以自动换行,并且同时把数据放到显存中…
自动换行,意味着我只需要在刚开始定位一次光标,接着一个字符一个字符的输出,它会挨着逐行顺序打印,每一行满了,接着下一个就会自动从下一行开始输出。

封装成字符串操作。

/
//函数名:LCD_CharString							          						     //
//功  能:打印字符串,每行最多16个英文字符,8个汉字字符。          //
//			 其中汉字字符不能再奇列地址。调用之前需至少定位一次光标, //
//			 之后将在初始光标位置按序打印,支持自动换行。            //
//参  数:无																									 	 //						 
/

void LCD_CharString(const char *str)
{
	while(*str)
	{
		LCD_CharOut(*str++);
	}
}

在上面的基础上实现printf格式化输出


#if (LCD_CHAR_FMT_EN == 1)

#include "stdio.h"
#include "string.h"
#include "stdarg.h"

/
//函数名:LCD_CharPrintf							          						     //
//功  能:LCD_CharString的格式化输出														 //       
//参  数:可变参数,同Printf																		 //						 
/
void LCD_CharPrintf(const char *fmt,...)
{
	va_list ap;
	char string[64];
	va_start(ap,fmt);
	vsnprintf(string,64,fmt,ap);
	LCD_CharString(string);
	va_end(ap);
	
	//sprintf(str,"或者用以下替换%d",a);
	//LCD_CharString(string);
}
#endif 

这样我们用LCD_CharPrintf函数操作就方便舒服多了。
比如无聊显示例子…

LCD_CharCursor(0,0);//光标定位
LCD_CharPrintf("%s二三四五六七八%s二三四五六七八%s二三四五六七八%s二三四五六七八","一","二","三","四");

显示:
一二三四五六七八
二二三四五六七八
三二三四五六七八
四二三四五六七八

/
我们再实现最后一个功能。这个功能我调试了半天时间。
它叫做滚屏,或者叫卷屏。

首先,一个全局静态变量,一个函数操作接口。

static u8 RollSumLine = 0;      //参与滚屏行数设置,0-4:note:及时设置为0也至少有一行参与滚屏


//函数名:LCD_CharScrollingSetline							          			//					 
//功  能:设置参与滚屏的行数 																		//
//参  数:line:参与滚屏的行数,Note:设置为0,也有一行参与滚屏  //																 
///

//函数名:LCD_CharScrollingSetline							          			//					 
//功  能:设置参与滚屏的行数 																		//
//参  数:line:参与滚屏的行数,Note:设置为0,也有一行参与滚屏  //																 
///
void LCD_CharScrollingSetline(u8 line)
{
	if(line>4){line = 4;}
	RollSumLine = line;
//	RollConUp   = 4 - RollSumLine;
//	RollConDown = RollSumLine;
}
///
//函数名:LCD_CharScrolling  						          						 //
//功  能:滚屏函数,打印字符串之前调用一次该函数,即可滚一行		 //
//参  数:derection:1--向上滚屏,0--向下滚屏									 //											 
///
void LCD_CharScrolling(u8 derection)
{
	u8 i = 0;
	
	if(derection){//向上滚屏
//		if(RollConUp < 4){
//			LCD_CharCursor(0,RollConUp++);
//		}
//		else{

		//每调用一次,就将显存中的数据往上移动一行,并将光标定位在最后一行
			for(i = ((4-RollSumLine)<<4);i<0x30;i++)
			{
				if((i&0x0f) == 0){
					LCD_CharCursor(0,i>>4);
				}
				LCD_CharOut(LCD_CharRAM[i+0x10]);
			}
			LCD_CharCursor(0,3);
			LCD_CharString("                ");
			LCD_CharCursor(0,3);
//		}
	}
	else{
//		if(RollConDown > 0){
//			LCD_CharCursor(0,--RollConDown);
//		}
//		else{
		//每调用一次,就将显存中的数据往下移动一行,并将光标定位在参与滚屏的首行
			for(i = (((RollSumLine)<<4)-1);i>=0x10;i--)
			{
				LCD_CharCursor((i&0x0f),i>>4);
				LCD_CharOut(LCD_CharRAM[i-0x10]);
			}
			LCD_CharCursor(0,4-RollSumLine);
			LCD_CharString("                ");
			LCD_CharCursor(0,4-RollSumLine);
//		}
	}
}

一些位运算的注释

异或运算:
	口诀:相同为0,相异为1。任何数跟0异或都是它本身,任何数跟1异或是它的取反
	用途 :  常用来指定位取反。比如取反byte的最高位:byte ^= 0x80;
位与运算:
	口诀:两个数同时为1才为1,否则为0
	用途一 :  常用来清零指定位,比如清零byte的最高位:byte &= 0x7f;
	用途二 :  可用来代替2的整数幂求余运算,比如 byte %= 8;可替换为 byte &= 0x07;
	```

位或运算:
口诀:两个数有1则为1,否则为0
用途一 :  常用来置位指定位,比如将byte的最高位置1:byte |= 0x80;
用途二 :  可用来代替2的整数幂求余运算,比如 byte %= 8;可替换为 byte &= 0x07;
```
移位运算:
	用途一 :  可用来替代2的整数幂的乘除法。比如byte *= 2,替换为 byte <<= 1。除法同理。
	```

源码链接
https://download.csdn.net/download/weixin_42992743/11596933

  • 41
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值