本帖最后由 人民币的幻想 于 2015-4-16 09:17 编辑
今天的进度是把12864液晶显示字符,汉字,画点函数搞定了。 常规驱动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缓冲区,缓冲区和液晶位置映射图如下:
file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg 下图即为GDRAM图 file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image004.jpg 我初步的想法是,直接写入相关位置实现,但是这样做存在一个弊端,那就是在写入某行多个点的时候后边的点会覆盖前边的点,为了避免这种情况,我们需要在写入某点之前,将当前的位址的所有点读出,然后将数据或上当前某点,然后再写入该位址。即所谓的读->改->写。具体实现如下: //画点函数 /* 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);//回到基本指令集 } 具体的代码请参看附件我的工程,下面附上一张调试完成的照片,仅作观赏之用。 file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image006.jpg |