大家好,欢迎来到IT知识分享网。
上面是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 //数据选择
IT知识分享网
IT知识分享网//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的初始化函数,这个就不用细讲了,按数据手册照着写就对了。
IT知识分享网
///
//函数名: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
上班很无聊,有兴趣加我微信吹水呀!哈哈哈哈哈
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/10732.html