常规驱动12864有以下几种实现:
1. IO模拟80并口。
2. FSMC总线驱动
3. IO模拟串行驱动
4. 硬件SPI驱动
暂时选用的IO模拟80并口方式,这种办法较为简单,易于实现。下面详述此方案:
受限于板子的IO分配,液晶和MCU引脚连接如下:
/*************************************************************************************
LCD12864
LCD_RS PB1
LCD_RW PE3
LCD_EN PE4
LCD_PSB PE5
LCD_RST 接到板子RST上
LCD_D0 PE6
LCD_D1 PF6
LCD_D2 PF11
LCD_D3 PG6
LCD_D4 PG7
LCD_D5 PG8
LCD_D6 PG12
LCD_D7 PG15
LCD_BL PB0
************************************************************************************/
由于分配了不连续的IO,因此读写数据函数的实现比往常困难一点,不过只要熟悉C语言的位运算,这都不叫事~~。
//数据格式
//D7 D6 D5 D4 D3 D2 D1 D0
uint8_t ReadByteFromLCD(void)
{
uint8_t res=0;
res=(LCD_D0_IN<<0)|(LCD_D1_IN<<1)
|(LCD_D2_IN<<2)|(LCD_D3_IN<<3)
|(LCD_D4_IN<<4)|(LCD_D5_IN<<5)
|(LCD_D6_IN<<6)|(LCD_D7_IN<<7);
returnres;
}
void WriteByteToLCD(uint8_t byte)
{
LCD_D0_OUT=(byte&0x01)>>0;
LCD_D1_OUT=(byte&0x02)>>1;
LCD_D2_OUT=(byte&0x04)>>2;
LCD_D3_OUT=(byte&0x08)>>3;
LCD_D4_OUT=(byte&0x10)>>4;
LCD_D5_OUT=(byte&0x20)>>5;
LCD_D6_OUT=(byte&0x40)>>6;
LCD_D7_OUT=(byte&0x80)>>7;
}
以上就是读写函数的实现,不过不要忘了在读数据之前将IO口设为输入,写数据之前将IO口设为输出。
/*************************************************************************************
LCD判忙函数
1:忙
0:不忙
************************************************************************************/
uint8_t LCD_Busy(void)
{
uint8_t res=0;
LCD_RS = 0;
LCD_RW = 1;
LCD_EN = 1;
//数据线IO方向设定
GPIO_Set(GPIOE,PIN6,GPIO_MODE_IN,0,0,GPIO_PUPD_PU);
GPIO_Set(GPIOF,PIN6|PIN11,GPIO_MODE_IN,0,0,GPIO_PUPD_PU);
GPIO_Set(GPIOG,PIN6|PIN7|PIN8|PIN12|PIN15,GPIO_MODE_IN,0,0,GPIO_PUPD_PU);
delay_us(5);
//读数据
res=(ReadByteFromLCD()&0x80);
LCD_EN = 0;
return res;
}
12864液晶和高速MCU通信的时候需要注意检测忙信号,以免导致错误的发生。接下来就需要实现写数据/指令到LCD的函数了。 //写指令/数据到LCD
void WriteToLCD(uint8_t mode,uint8_t byte)
{
if(mode==LCD_CMD)//写指令
{
while(LCD_Busy());
LCD_RS = 0;//指令
LCD_RW = 0;//写
LCD_EN = 0;
delay_us(5);
GPIO_Set(GPIOE,PIN6,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
GPIO_Set(GPIOF,PIN6|PIN11,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
GPIO_Set(GPIOG,PIN6|PIN7|PIN8|PIN12|PIN15,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
WriteByteToLCD(byte);//写
delay_us(5);
LCD_EN = 1;
delay_us(5);
LCD_EN = 0;
}else //写数据
{
while(LCD_Busy());
LCD_RS = 1;//数据
LCD_RW = 0;
LCD_EN = 0;
delay_us(5);
GPIO_Set(GPIOE,PIN6,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
GPIO_Set(GPIOF,PIN6|PIN11,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
GPIO_Set(GPIOG,PIN6|PIN7|PIN8|PIN12|PIN15,GPIO_MODE_OUT,GPIO_OTYPE_OD,GPIO_SPEED_100M,GPIO_PUPD_PU);
WriteByteToLCD(byte);//写
delay_us(5);
LCD_EN = 1;
delay_us(5);
LCD_EN = 0;
}
}
按照时序完成这个,基本上不会有太大问题了。剩下的就是初始化之类的,完整显示字符串和汉字实现见附件。
到这,基本的驱动算是完成了,但我想用液晶做人机界面及交互,必然会涉及到GUI,因此,底层必须实现画点函数。基于此,我完成了这个画点函数,下面就说一下我在实现这个函数过程中碰到的问题和解决办法。首先,由于LCD12864并没有画点指令,但好在还有一个256*64的DGRAM缓冲区,缓冲区和液晶位置映射图如下:
下图即为GDRAM图
我初步的想法是,直接写入相关位置实现,但是这样做存在一个弊端,那就是在写入某行多个点的时候后边的点会覆盖前边的点,为了避免这种情况,我们需要在写入某点之前,将当前的位址的所有点读出,然后将数据或上当前某点,然后再写入该位址。即所谓的读->改->写。具体实现如下:
//画点函数
/*
GDRAM分布
256(16*16) X
|-----------------------------|-----------------------------------|
|0 | |
| 上半屏128*32 | 下半屏128*32 |
Y|32 | |
|-----------------------------|-----------------------------------|
水平x坐标每隔16个点才有一个位址,因此改变某个点需要知道该点位于这16个点的哪个位置
垂直y坐标在大于31时处于下半屏,小于31处于上半屏
*/
voidLCD_SetPoint(uint8_t x,uint8_t y)
{
uint16_t volatile readdata=0; //判断处于哪行哪列
uint8_t x_pos,x_bit;//x_pos用来判断处于位址,x_bit用来判断处于位址中的位置
uint8_t y_pos,y_bit;//y_pos用来判断处于上半屏还是下半屏 y_bit用来判断位于哪行
y_pos=y/32; //0:上半屏;1:下半屏
y_bit=y%32; //得到具体行位置
x_pos=x/16;
x_bit=x%16;
WriteToLCD(LCD_CMD,LCD_EXTERN_SET);//扩展指令集
WriteToLCD(LCD_CMD,LCD_DRAW_OFF);//关掉绘图显示
WriteToLCD(LCD_CMD,0x80+y_bit);
WriteToLCD(LCD_CMD,0x80+8*y_pos+x_pos);
//数据线IO方向设定
GPIO_Set(GPIOE,PIN6,GPIO_MODE_IN,0,0,GPIO_PUPD_PD);
GPIO_Set(GPIOF,PIN6|PIN11,GPIO_MODE_IN,0,0,GPIO_PUPD_PD);
GPIO_Set(GPIOG,PIN6|PIN7|PIN8|PIN12|PIN15,GPIO_MODE_IN,0,0,GPIO_PUPD_PD);
delay_ms(1);
ReadByteFromLCD();//空读一次
readdata=ReadByteFromLCD();
readdata<<=8;
readdata|=ReadByteFromLCD();
WriteToLCD(LCD_CMD,0x80+y_bit);
WriteToLCD(LCD_CMD,0x80+8*y_pos+x_pos);
if(x_bit<8)
{
WriteToLCD(LCD_DATA,((uint8_t)(readdata>>8))|(0x01<<(7-x_bit)));
WriteToLCD(LCD_DATA,(uint8_t)readdata);
}else
{
WriteToLCD(LCD_DATA,(uint8_t)(readdata>>8));
WriteToLCD(LCD_DATA,(uint8_t)readdata|(0x01<<(15-x_bit)));
}
WriteToLCD(LCD_CMD,LCD_DRAW_ON);//开绘图显示
WriteToLCD(LCD_CMD,LCD_BASIC_SET);//回到基本指令集
}
voidLCD_Refresh_GRAM(void)
{
uint8_t i;uint16_t j;
WriteToLCD(LCD_CMD,LCD_EXTERN_SET);//扩展指令集
WriteToLCD(LCD_CMD,LCD_DRAW_OFF);//关掉绘图显示
for(i=0;i<32;i++)//遍历0-31行
{
WriteToLCD(LCD_CMD,0x80+i);//写入行地址
WriteToLCD(LCD_CMD,0x80); //写入列地址
for(j=0;j<32;j++)
{
WriteToLCD(LCD_DATA,LCD_GRAM[j]);
}
}
WriteToLCD(LCD_CMD,LCD_DRAW_ON);//开启绘图显示
WriteToLCD(LCD_CMD,LCD_BASIC_SET);//回到基本指令集
}
这样做理论上没有问题,但在实际操作的时候出现了一些错误,在读取位址的时候,将IO口设置为上拉输入,读取到的数据都是0xff,设置为下拉输入,则读到的数据都是0x00。
因此,该实现就卡死在了这里。后来也曾试过将IO口设置为开漏输出,加上拉。在读取数据前,向端口位写1,然后读回数据的方法,但是也不能解决问题。
经过一番的查找思路的过程,最终想到了用MCU内部缓冲区的办法。这种方案需要MCU内存不能太紧张,不过相对STM32F407ZGT6那192K的内存,这点开销毛毛雨了。于是乎在内存中开辟了一块缓冲区: 实现一个[32][32]RAM缓冲区
格式如下
//[0]0 1 2 3 ...31(字节)
//[1]0 1 2 3 ...31(字节)
//[2]0 1 2 3 ...31(字节)
//[3]0 1 2 3 ...31(字节)
[0..31]代表0-31行,0..31代表某行的所有字节(16*16/8=32)。
画点函数实现纯粹是操作uint8_t LCD_GRAM[32][32];
这个二维数组,在操作完毕后,更新缓冲区数据到GDRAM中即可。
//x:0-127
//y:0-63
//mode:1,画点
//mode:0,清空
void LCD_DrawPoint(uint8_t x,uint8_t y,uint8_t mode)
{
//判断处于哪行哪列
uint8_tx_pos,x_bit;//x_pos用来判断处于位址,x_bit用来判断处于位址中的位置
uint8_t y_pos,y_bit;//y_pos用来判断处于上半屏还是下半屏 y_bit用来判断位于哪行
// uint8_tx_pos_temp;
if((x>127)||(y>63)||(x<0)||(y<0))return;//去掉不合理参数
y_pos=y/32; //0:上半屏;1:下半屏
y_bit=y%32; //得到具体行位置
x_pos=x/16;
x_bit=x%16;
if(y_pos>0)//下半屏
{
if(mode)
{
if(x_bit<8)
{
LCD_GRAM[y_bit][x_pos*2+16]|=(1<<(7-x_bit));
LCD_GRAM[y_bit][x_pos*2+1+16]|=0x00;
}else
{
LCD_GRAM[y_bit][x_pos*2+16]|=0x00;
LCD_GRAM[y_bit][x_pos*2+1+16]|=(1<<(15-x_bit));
}
}
else
{
if(x_bit<8)
{
LCD_GRAM[y_bit][x_pos*2+16]&=~(1<<(7-x_bit));
LCD_GRAM[y_bit][x_pos*2+1+16]&=~0x00;
}else
{
LCD_GRAM[y_bit][x_pos*2+16]&=~0x00;
LCD_GRAM[y_bit][x_pos*2+1+16]&=~(1<<(15-x_bit));
}
}
}else//上半屏
{
if(mode)
{
if(x_bit<8)
{
LCD_GRAM[y_bit][x_pos*2]|=(1<<(7-x_bit));
LCD_GRAM[y_bit][x_pos*2+1]|=0x00;
}else
{
LCD_GRAM[y_bit][x_pos*2]|=0x00;
LCD_GRAM[y_bit][x_pos*2+1]|=(1<<(15-x_bit));
}
}
else
{
if(x_bit<8)
{
LCD_GRAM[y_bit][x_pos*2]&=~(1<<(7-x_bit));
LCD_GRAM[y_bit][x_pos*2+1]&=~0x00;
}else
{
LCD_GRAM[y_bit][x_pos*2]&=~0x00;
LCD_GRAM[y_bit][x_pos*2+1]&=~(1<<(15-x_bit));
}
}
}
// LCD_Refresh_GRAM();
}
这个函数是为最终的画点函数实现。调用此函数完毕后,再调用刷新缓冲区数据函数即可完成画点。
void LCD_Refresh_GRAM(void)
{
uint8_t i;uint16_t j;
WriteToLCD(LCD_CMD,LCD_EXTERN_SET);//扩展指令集
WriteToLCD(LCD_CMD,LCD_DRAW_OFF);//关掉绘图显示
for(i=0;i<32;i++)//遍历0-31行
{
WriteToLCD(LCD_CMD,0x80+i);//写入行地址
WriteToLCD(LCD_CMD,0x80); //写入列地址
for(j=0;j<32;j++)
{
WriteToLCD(LCD_DATA,LCD_GRAM[j]);
}
}
WriteToLCD(LCD_CMD,LCD_DRAW_ON);//开启绘图显示
WriteToLCD(LCD_CMD,LCD_BASIC_SET);//回到基本指令集
}
具体的代码请参看附件我的工程,下面附两张调试完成的照片,仅作观赏之用。