LCD屏幕上其实都是一个个的像素点组成的,每行每列都有若干个像素点。每个像素点所占据的数据位宽(或者说像素深度)对于不同类型的屏幕,是不一样的。比如真彩色屏幕,每个像素点占据24bit或者32bit的数据,也就是RGB888或ARGB8888(其中A表示透明度);而对于灰度屏(没错就是那种黑白电视)来说,1个像素点一般只占据8bit;而只有黑白两种颜色的黑白屏,只要1bit数据位即可表示。一般来说,占据的数据位越多,那么它能表示的颜色就越丰富,就越能真实呈现自然界中原本的颜色。屏幕像素点如下图所示:
对于LCD12864显示屏,则有128*64个像素点。我们要想在屏幕上显示出字符、文字甚至是图片等等,那么只要按照某种规律点亮相应的像素点就可以显示出相应的文字、字符或者图片,而这些按照某种规律点亮像素的数据就叫做点阵数据(对于图片的点阵数据一般叫做位图)。
扩展一下:其实对于点阵数据、图片的位图数据等,这一部分的数据处理是比较复杂的,比如数据矩阵转换、点阵数据反选等。另外,对于一幅图片的位图数据,数据量是非常庞大的。这里举个例子:对于分辨率是1920x1080,像素深度是24bit真彩色的一幅图片,它的大小就是:1920 x 1080 x 24bit = 5.93MB大小,也就是说一幅图片的大小就解决6MB的数据量了。那么如果是视频数据呢?那数据量可以大到惊人。但是我们生活中接触到的图片或者视频,其实都是经过压缩的,这里就使用了及其复杂的压缩算法,而要把图片或者视频呈现给我们时,就要按照压缩的规律进行解压缩,这又要涉及到解压缩算法。总之,对于这些数据的处理其实会涉及到很多的知识,感兴趣的小伙伴可以自己查找相关的资料学习。
下面我们使用一组8x16的字符A的点阵数据,解释一下这组数据怎么就能在屏幕上显示出’A’来。字符A点阵数据如下图所示:
其中,1代表点亮像素点,0代表不点亮。那么,我们把每个字节数据为1的位在8x16的像素格子上点亮,而为0的位不点亮。这样我们就可以在屏幕上呈现出一个字符’A’来。但是,有一个问题是,我们如何把这组点阵数据写到显示屏上(其实就是把数据写到显示屏里面的一块显存上)呢?
对于这一点,不同的屏幕,他们会使用不同的通信协议的,根据屏幕规定的通信协议要求,就可以往屏幕写入相应的点阵数据了,从而可以显示出自己想要显示的文字、图标、图片等等。因为本文驱动代码使用的显示屏是LCD12864,下面就简单介绍下LCD12864所使用的通信时序。
所用到的引脚
一般LCD12864都有20个引脚的,这个查阅手册都可以了解到。所有引脚如下图所示:
这里我所用到的是12864的串行通信方式,所以只用到一些与串行连接相关的引脚。LCD12864和我的STM32板子(我使用的型号是STM32F103VET6)的引脚连接如下:
CLK — PA5
SID — PA6
CS — PA7
PSB — GND
对于12864电源和背光引脚,根据自己的屏幕要求接板子上的电源即可。这里要注意的是PSB这个引脚,这个引脚是串并行控制引脚,如果代码不想控制,直接接地即可选择为串行。另外,CS片选引脚如果代码也不想控制的话,那直接接VCC(3.3V)一直保持高电平也可以。
LCD串行读写时序
写代码时,这幅时序图是非常重要的,根据这幅时序图就可以对LCD12864进行数据读写了。从上面的时序图可以看出,要发送1个字节的数据,实际上是一共需要发送3个字节的数据,首先是读写命令控制,然后是发送数据的高4位,然后再发送数据的低4位,这样的一个过程才完成了一次写过程。
关于LCD12864其他的一些数据缓冲区的地址位置,命令含义等这些内容,这里就不一一介绍了,这些其实都可以在手册查找到,我们还是直接上代码吧,从代码的注释中也可以了解到这些内容。
/** * @brief LCD12864使用到的引脚配置 * @param 无 * @retval 无 */ void lcd_gpio_config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 开启GPIOA时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* PA7(CS), PA5(CLK), PA6(SID)引脚配置 */ GPIO_InitStructure.GPIO_Pin = LCD_CS_GPIO_PIN | LCD_CLK_GPIO_PIN | LCD_SID_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); /* 控制引脚初始状态全部输出低电平 */ LCD_CS(0); LCD_CLK(0); LCD_SID(0); }
该延时函数只是简单的进行延时,不是非常精准。但是自己也是经过了测试的,自己对比过秒表,基本能对得上。
/** * @brief us延时函数,stm32f1系列,72MHz频率,自己使用秒表对比过误差不大 * @param us: us延时时间 * @retval 无 */ void delay_us(unsigned int us) { unsigned short i = 0; while(us--) { i = 8; while(i--); } } /** * @brief ms延时函数,stm32f1系列,72MHz频率,自己使用秒表对比过误差不大 * @param ms: ms延时时间 * @retval 无 */ void delay_ms(unsigned int ms) { unsigned short i = 0; while(ms--) { i = 8100; while(i--); } }
/** * @brief LCD12864串行发送一个字节数据 * @param byte: 一个字节数据 * @retval 无 */ static void lcd_send_byte(unsigned char byte) { unsigned char i; for(i=0; i<8; i++) { LCD_CLK(0); LCD_SID(byte & (0x80 >> i)); // 按位发送 delay_us(5); LCD_CLK(1); } }
LCD12864写命令或者写数据的过程,其实就是上面那一幅串行读写的时序图,时序图下面有详细的介绍,要发送一个字节的数据,实际上要发送3个字节的数据。第一个字节数是要告诉12864我要发送的是命令还是数据,接下来发送的两个字节数 ,是要把自己发送的命令/数据拆分为高4位和低4位发送(这个是时序图规定要这样发送的,不同规格的12864可能时序不一样,这里要注意一下)。
/** * @brief LCD12864写命令 * @param cmd: 要发送的命令 * @retval 无 */ static void lcd_write_cmd(unsigned char cmd) { LCD_CS(1); delay_ms(1); lcd_send_byte(0xf8); // 写命令 lcd_send_byte(0xf0 & cmd); // 写高4位指令 lcd_send_byte(cmd << 4); // 写低4位指令 LCD_CS(0); }
/** * @brief LCD12864写数据 * @param data: 要发送的数据 * @retval 无 */ static void lcd_write_data(unsigned char data) { LCD_CS(1); delay_ms(1); // 这里如果延时时间太长,在绘图模式下,会出现严重的闪屏现象。我测试发现在100us以下就基本没有闪屏 lcd_send_byte(0xfa); // 写数据 lcd_send_byte(0xf0 & data); // 写高4位数据 lcd_send_byte(data << 4); // 写低4位数据 LCD_CS(0); }
LCD12864的初始化,其实它的规格书有一个初始化的流程图的,按照手册的流程图依次对12864进行初始设置即可。
/** * @brief LCD12864初始化 * @param 无 * @retval 无 */ void lcd_init(void) { delay_ms(50); // 上电自检延时 lcd_write_cmd(0x30); // 选择基本指令集,8bit数据流 delay_ms(1); lcd_write_cmd(0x0c); // 开显示,关闭光标 delay_ms(1); lcd_write_cmd(0x01); // 清除显示 delay_ms(30); }
这里在LCD12864显示字符串的函数,使用的是12864内部自带的字库(包含了中文字库),而不是自己定义的点阵数据字库。内部自带的字库实际上就是DDRAM的地址,每个地址值所占据的像素点是16x16个像素点。自带字库的英文字符是8x16个像素点,中文字符是16x16个像素点,所以12864每行能显示的英文字符是16个,中文字符是8个,一共能显示4行。
/* LCD12864内部字库显示的DDRAM地址,每个地址占16*16个像素点 */ static unsigned char lcd_str_addr[4][8] = { {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} }; /** * @brief LCD12864显示字符串 * @param column: 行地址0~3,row: 列地址0~7 * @retval 无 */ void lcd_draw_str(unsigned char column, unsigned char row, const char *str) { /* 设置显示的行/列地址 */ lcd_write_cmd(lcd_str_addr[column][row]); /* 逐个字符写入 */ while(*str) { lcd_write_data(*str++); } }
LCD12864内部还提供了一片绘图显示的存储空间,我们通过写入不同的数据在这片空间中,就可以显示出我们想要的不同内容了,比如可以画点、画线、绘制图片等。要往这片存储空间中写入数据,我们首先得让12864切换到扩展指令模式。话不多说,我们直接上代码。
首先我们设置的坐标是以LCD12864点阵像素点为单位的,一个像素点就是某一个位置的坐标。我手上的这块LCD12864显示屏,对于这块绘图空间,是分为上下半屏的,上半屏x轴起始地址是0x80,下半屏x轴起始地址0x88。另外,对于x轴,每增加一个地址,实际上是占据了16个像素点的,而对于y轴每增加一个地址,只占据一个像素点。代码如下:
/** * @brief 设置LCD12864显示坐标 * @param x: x轴坐标0~127, y: y轴坐标0~63 * @retval 无 */ static void lcd_setXY(unsigned char x, unsigned char y) { if (y >= 32) { /* 下半屏 */ lcd_write_cmd(0x80 + (y - 32)); // y坐标 lcd_write_cmd(0x88 + (x>>4)); // x坐标 } else { /* 上半屏 */ lcd_write_cmd(0x80 + y); lcd_write_cmd(0x80 + (x>>4)); } }
就是y坐标大于等于32的时候,就处于下半屏了,因为上下半屏地址不一样,分类设置。另外x轴坐标是x>>4再增加的,这就是因为x轴一个地址就占据了16个像素点,所以需要右移4,实际上就是除以8。
清空屏幕实际上就是往那块绘图空间发送0x00的数据,全部熄灭像素点即可达到清空屏幕的目的。
/** * @brief 清空屏幕 * @param 无 * @retval 无 */ void lcd_clear(void) { unsigned char x, y; lcd_write_cmd(0x34); // 切换到扩展指令集 for(y=0; y<64; y++) { lcd_setXY(0, y); // 设置显示坐标 for(x=0; x<8; x++) { /* 因为x轴一个地址对应16个像素点,可以连续发送两个字节数据 */ lcd_write_data(0x00); lcd_write_data(0x00); } } lcd_write_cmd(0x36); // 打开绘图显示 lcd_write_cmd(0x30); // 回到基本指令集 }
这个函数实际上就是x轴循环8次,一次写入16字节的数据,所以在x轴就是每增加一次地址,就移动了16个像素点了(一定要注意这点,不然不好理解),所以只需要循环8次就够了。对于y轴每增加一次地址,只是移动到了下一个像素点,所以需要循环64次。
绘图函数其实和清空屏幕的函数原理是相似的,只不过绘图函数是往绘图存储空间写入的是图片的点阵数据(但是一定要注意所用的图片的点阵数据是符合这个显示屏的像素扫描方式的,上面也说过了,x轴一个地址对应16个像素点,y轴一个地址对应一个像素点)。代码如下:
/** * @brief 显示图片,注意显示起点坐标固定是(0, 0) * @param 无 * @retval 无 */ void lcd_draw_picture(const unsigned char *data) { unsigned char x, y; lcd_write_cmd(0x34); // 切换到扩展指令集 for(y=0; y<64; y++) { lcd_setXY(0, y); // 设置显示坐标 for(x=0; x<8; x++) { lcd_write_data(*data++); lcd_write_data(*data++); } } lcd_write_cmd(0x36); // 打开绘图显示 lcd_write_cmd(0x30); // 回到基本指令集 }
在绘图区域中,要绘制任意点,这需要一点点技巧。原因就是x轴一个地址对应16个像素点,我们需要在一次发送16个字节的数据中,根据x轴坐标点让这16个像素点的某一点像素点亮,而其他的像素点熄灭。代码如下:
/* x轴按位显示的位码表 */ static const unsigned char set_pix_bit[8] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01}; /* 显示缓冲区 */ static unsigned char disp_buff[8][64] = {0}; /** * @brief 在LCD显示范围内绘制任意点 x坐标范围是:0~127,y坐标范围是:0~63 * @param x: x轴坐标, y: y轴坐标, color: 颜色值,1:点亮像素,0:不点亮 * @retval 无 */ void lcd_draw_dots(unsigned char x, unsigned char y, unsigned char color) { lcd_write_cmd(0x34); // 切换到扩展指令集 /* 超出显示范围,退出函数 */ if ((x >= 128) || (y >= 64)) return; /* 设置x, y坐标 */ lcd_setXY(x, y); /* 填充显示缓冲区数据 */ if (color == 1) { /* 点亮某点像素 */ disp_buff[x>>3][y] |= 0x00; disp_buff[(x>>3)+1][y] |= set_pix_bit[x & 0x07]; } else { /* 熄灭某点像素 */ disp_buff[x>>3][y] |= 0x00; disp_buff[(x>>3)+1][y] &= ~set_pix_bit[x & 0x07]; } /* 输出数据到LCD显示 */ if ((x >> 3) % 2 != 0) // 判断x轴0~15个显示数据的奇偶性 { /* 奇数 */ lcd_write_data(disp_buff[x>>3][y]); lcd_write_data(disp_buff[(x>>3)+1][y]); } else { /* 偶数 */ lcd_write_data(disp_buff[(x>>3)+1][y]); lcd_write_data(disp_buff[x>>3][y]); } lcd_write_cmd(0x36); // 打开绘图显示 lcd_write_cmd(0x30); // 回到基本指令集 }
把绘制任意点的函数写出来后,画线的函数就容易多了。代码如下:
/** * @brief 绘制水平线 * @param x0: x轴起点, y0: y0轴起点, x1: x轴结束点, color: 颜色值,1点亮像素,0不点亮 * @retval 无 */ void lcd_draw_Hline(unsigned char x0, unsigned char y0, unsigned char x1, unsigned char color) { /* 超出显示范围,退出函数 */ if ((x0 >= 128) || (y0 >= 64) || (x1 >= 128)) return; for ( ; x1>=x0; x0++) lcd_draw_dots(x0, y0, color); }
/** * @brief 绘制垂直线 * @param x0: x轴起点, y0: y轴起点, y1: y轴结束点, color: 颜色值,1点亮像素,0不点亮 * @retval 无 */ void lcd_draw_Vline(unsigned char x0, unsigned char y0, unsigned char y1, unsigned char color) { /* 超出显示范围,退出函数 */ if ((x0 >= 128) || (y0 >= 64) || (y1 >= 64)) return; for ( ; y1>=y0; y0++) lcd_draw_dots(x0, y0, color); }
以上就是我实现的LCD12864各个函数的功能了,这些代码我都是经过了测试的,我自己测试没发现什么bug,都是可以正常显示字符串、图片、绘制任意点、绘制线条等。不过我发现,在绘图模式下时,绘制图片、或者线条等,如果发送数据/命令那里延时时间太长的话,会出现严重的闪屏现象,把延时时间调整到100us以下时,就基本看不到闪屏的现象了。
如果有小伙伴在复现代码的过程中,发现有bug,欢迎指出。如果得不到实验的结果,也可以和我一起讨论。