最近在淘宝入手了一块ILI9341彩色屏幕,支持320x240分辨率。之前一直很好奇这类单片机驱动的彩色屏幕的原理,就打算自己写一个驱动,从电流层面操控ILI9341屏幕。话不多说,我们开始吧( ̄▽ ̄)~*
1.ILI9341芯片和ILI9341驱动板
首先这里要明确两个概念,ILI9341芯片和ILI9341驱动板。
ILI9341芯片是ilitek发布的液晶驱动芯片,是这个样子的:
而淘宝上的ILI9341驱动板是把ILI9341芯片、屏幕和针脚焊接在一起的电路板,它可能是这个样子的:
也可能是这个样子的:
还可能是这个样子的:
没错,不同的厂家可以制造不同形状,不同接口的ILI9341驱动板,但他们上面都有ILI9341芯片,所以我们可以用相同的方法操作它。
2.如何操作它呢?
这是ILI9341驱动板的背面,我在上面做了些标注,应该会方便理解些。我们只要控制这些针脚的通电与否(高电平与低电平),就能够获得操控这块屏幕的“完整权限”!那这些针脚的定义是什么呢?我们一个一个看:
左上角有五个最重要的针脚,分别是LCD_RST、LCD_CS、LCD_RS、LCD_WR和LCD_RD:(直接用文字写不能冒号对齐,没办法,开个代码框( ̄▽ ̄)~*)
LCD_RST : 即LCD Reset,用于在通电之后复位,初始化整个模块。 LCD_CS : 即LCD Chip Select,用于多个芯片之间的片选操作。由于这块驱动板只有一个可用的芯片,所以一般该针脚不通电。 LCD_RS : 又称D/CX信号线,用于切换写命令(Command)和写数据(Data),当对显示屏写命令(Command)时,应该让针脚不通电,当对显示屏写数据(Data)时,应该让针脚通电。 LCD_WR : 写使能。当LCD_WR不通电,并且LCD_RD通电时,数据传输方向为写入。 LCD_RD : 读使能。
左下角的针脚负责供电,不细讲。
右上角的LCD_D0到LCD_D7是数据脚,通过控制它的通电与否来传输8个比特,也就是8个0或1。这种方式可以传输一个最小值0,最大值255的数字,我们用它来传输所有命令和数据。
右下角的SD_SS,SD_DI,SD_D0,SD_SCK适用于控制SD卡读写的,不属于ILI9341的范畴,我们先不讨论。
那么,如何操作它呢?这张图能够很清楚的说明:(下面用拉低代表示不通电,拉高表示通电,这样术语会更加标准)
- 单片机开机,ILI9341驱动板接收到电流,开始进入待命状态
- 拉低LCD_CS片选信号,选择对ILI9341芯片发送命令
- 通过拉高拉低LCD_D0到LCD_D7数据脚,来表示二进制数据
- 拉低拉高LCD_RS针脚来告诉机器这是一个命令,还是一个数据
- 拉低LCD_WR,进行写使能(可以理解为按下回车键,把LCD_D0到D7的数据发送出去)
这就是发送一个命令或者数据的方法。二进制,十进制和十六进制的转换和表达先直接略过,如果要展开,那可能可以出一本书了( • ̀ω•́ )✧,关于LCD_D0到D7脚应该发送什么,ILITEK在设计ILI9341时就已经规定好了,中文文档在此:
接下来,就是,愉快的,编码时间啦( • ̀ω•́ )✧!!!
3.兼容性设定
不知刚才你有没有注意到数据脚是从LCD_D2开始的?那是因为Arduino Uno开发板的第0和1脚是USB针脚,不能被使用,只能从第2个针脚开始设计:
那我们在编程时要用到LCD_D0和LCD_D1时,就必须写成8和9。另外不同机器脚位也不一样,所以我用宏定义来简化程序:
#define LCD_D0 8 #define LCD_D1 9 #define LCD_D2 2 #define LCD_D3 3 #define LCD_D4 4 #define LCD_D5 5 #define LCD_D6 6 #define LCD_D7 7 #define LCD_RD A0 #define LCD_WR A1 #define LCD_RS A2 #define LCD_CS A3 #define LCD_RST A4
这样即解决了LCD_D0和LCD_D1脚的问题,还搞定的不同开发板的兼容性问题。由于#define是在预编译阶段生成的,所以不会影响代码运行的速度。
4.发送命令和数据
调用这块屏幕的方法很明确,就是写入2进制数字。通过设计厂商提供的命令表发送相应的2进制命令和数据,实现操控。这样做的好处是无论你使用的是什么机器,什么驱动板,只要实现了LcdWriteCommand()和LcdWriteData()两个函数,就可以实现对屏幕的完全控制。
你当然可以用最直接的办法去控制引脚,比如digitalWrite():
void LcdWriteCommand(unsigned char d){ //Write Command Mode On digitalWrite(LCD_RS,LOW); //Write Datas to LCD_D0 to LCD_D7 digitalWrite(LCD_D0,d%2); d = d >> 1; digitalWrite(LCD_D1,d%2); d = d >> 1; digitalWrite(LCD_D2,d%2); d = d >> 1; digitalWrite(LCD_D3,d%2); d = d >> 1; digitalWrite(LCD_D4,d%2); d = d >> 1; digitalWrite(LCD_D5,d%2); d = d >> 1; digitalWrite(LCD_D6,d%2); d = d >> 1; digitalWrite(LCD_D7,d%2); d = d >> 1; //Enable Datas digitalWrite(LCD_WR,LOW); digitalWrite(LCD_WR,HIGH); } void LcdWriteData(unsigned char d){ //Write Data Mode On digitalWrite(LCD_RS,HIGH); //Write Datas to LCD_D0 to LCD_D7 digitalWrite(LCD_D0,d%2); d = d >> 1; digitalWrite(LCD_D1,d%2); d = d >> 1; digitalWrite(LCD_D2,d%2); d = d >> 1; digitalWrite(LCD_D3,d%2); d = d >> 1; digitalWrite(LCD_D4,d%2); d = d >> 1; digitalWrite(LCD_D5,d%2); d = d >> 1; digitalWrite(LCD_D6,d%2); d = d >> 1; digitalWrite(LCD_D7,d%2); d = d >> 1; //Enable Datas digitalWrite(LCD_WR,LOW); digitalWrite(LCD_WR,HIGH); }
但是这样做的话,速度嘛。。。看看这个,你就不会想用digitalWrite了:
单片机中,速度为王,我们还是直接改Register吧:
void LcdWriteCommand(unsigned char d){ //Write Command Mode On fastDigitalWriteLOW(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); } void LcdWriteData(unsigned char d){ //Write Command Mode On fastDigitalWriteHIGH(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); }
这段代码中,我用了宏定义来实现fastDigitalWriteHIGH()和fastDigitalWriteLOW(),这两个定义能避免函数的栈调用。其实用内联函数来写也可以实现:
inline void fastDigitalWriteHIGH(int Pin){ *(portOutputRegister(digitalPinToPort(Pin)))|=digitalPinToBitMask(Pin); return; }
但我就是喜欢宏定义,而且宏定义行数少些。
另外你可能会疑惑:什么是PORTB和PORTD?
PORTB其实就是针脚8-13,PORTD其实就是针脚0-7:
假如你有一个这样的二进制数:
00000100
把他转换成十进制:
4
再把它赋值给PORTD
PORTD = 4;
你就会发现针脚2通电了(图中连接到左边第一个红色灯泡):
这就是PORTD的真正意义,它使用一个从0到255的数,记录针脚0到7的通电情况。
那我们为什么不用digitalWrite(),而是要用PORTB和PORTD呢?因为快啊( ̄▽ ̄)~*
5.Enjoy!
我们刚刚实现了LcdWriteCommand()和LcdWriteData()两个函数,现在,我们就可以实现对屏幕的完全控制了!
首先,先运行一段初始化命令:
//Initialize Data Pins pinMode(LCD_D0,OUTPUT); pinMode(LCD_D1,OUTPUT); pinMode(LCD_D2,OUTPUT); pinMode(LCD_D3,OUTPUT); pinMode(LCD_D4,OUTPUT); pinMode(LCD_D5,OUTPUT); pinMode(LCD_D6,OUTPUT); pinMode(LCD_D7,OUTPUT); //Initialize Command Pins pinMode(LCD_RD,OUTPUT); pinMode(LCD_WR,OUTPUT); pinMode(LCD_RS,OUTPUT); pinMode(LCD_CS,OUTPUT); pinMode(LCD_RST,OUTPUT); digitalWrite(LCD_CS,LOW); digitalWrite(LCD_RD,HIGH); digitalWrite(LCD_WR,LOW); //Reset digitalWrite(LCD_RST,HIGH); delay(5); digitalWrite(LCD_RST,LOW); delay(15); digitalWrite(LCD_RST,HIGH); delay(15); LcdWriteCommand(0xCB); LcdWriteData(0x39); LcdWriteData(0x2C); LcdWriteData(0x00); LcdWriteData(0x34); LcdWriteData(0x02); LcdWriteCommand(0xCF); LcdWriteData(0x00); LcdWriteData(0XC1); LcdWriteData(0X30); LcdWriteCommand(0xE8); LcdWriteData(0x85); LcdWriteData(0x00); LcdWriteData(0x78); LcdWriteCommand(0xEA); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteCommand(0xED); LcdWriteData(0x64); LcdWriteData(0x03); LcdWriteData(0X12); LcdWriteData(0X81); LcdWriteCommand(0xF7); LcdWriteData(0x20); LcdWriteCommand(0xC0); //Power control LcdWriteData(0x23); //VRH[5:0] LcdWriteCommand(0xC1); //Power control LcdWriteData(0x10); //SAP[2:0];BT[3:0] LcdWriteCommand(0xC5); //VCM control LcdWriteData(0x3e); //Contrast LcdWriteData(0x28); LcdWriteCommand(0xC7); //VCM control2 LcdWriteData(0x86); //-- LcdWriteCommand(0x36); // Memory Access Control LcdWriteData(0x48); LcdWriteCommand(0x3A); LcdWriteData(0x55); LcdWriteCommand(0xB1); LcdWriteData(0x00); LcdWriteData(0x18); LcdWriteCommand(0xB6); // Display Function Control LcdWriteData(0x08); LcdWriteData(0x82); LcdWriteData(0x27); LcdWriteCommand(0x11); //Exit Sleep delay(120); LcdWriteCommand(0x29); //Display on LcdWriteCommand(0x2c);
这么多!不要怕,原样复制过去运行就好。这段命令是按照ILITEK设计文档中的规则发送的,用于初始化屏幕。运行完这段命令之后,我们就可以开始发自己的命令了。
我们试着来清一下屏。清屏就是指定一块区域,然后给屏幕每一个像素点的颜色为白色,这样就好了。
首先定义我们要写入的区域,这里就是从(0,0)写入到(239,319):
int x1 = 0; int x2 = 239; int y1 = 0; int y2 = 319;
接着通知屏幕我们要写入的区域的X坐标的起始、终止位置(命令0x2a):
LcdWriteCommand(0x2a);
然后发送X坐标的起始位置(x1),和X坐标的终止位置(x2)。我们的机器一次只能发送八位数字,但八位数字最大只能表示255,所以我们要分两次发送,先发送前八位,再发送后八位:
LcdWriteData(x1>>8); LcdWriteData(x1); LcdWriteData(x2>>8); LcdWriteData(x2);
Y坐标也是一样,只是把通知命令改成0x2b:
LcdWriteCommand(0x2b); LcdWriteData(y1>>8); LcdWriteData(y1); LcdWriteData(y2>>8); LcdWriteData(y2);
接着,我们发送开始写入的命令(0x2c),告诉屏幕我要开始发送像素了:
LcdWriteCommand(0x2c);
最后,发送所有像素的颜色数据(Data)。里面的RGB()宏定义是我在上一篇文章实现的。另外,因为是数据,所以我们要使用LcdWriteData():
#define RGB(r,g,b) ((b&31)+((g&63)<<5)+((r&31)<<11)) for(int i=y1;i<=y2;i++){ for(int j=x1;j<=x2;j++){ LcdWriteData(RGB(31,63,31)>>8); LcdWriteData(RGB(31,63,31)); } }
保存,下载。
刷屏完整代码:
// Breakout/Arduino UNO pin usage: // LCD Data Bit : 7 6 5 4 3 2 1 0 // Uno dig. pin : 7 6 5 4 3 2 9 8 // Uno port/pin : PD7 PD6 PD5 PD4 PD3 PD2 PB1 PB0 // Mega dig. pin: 29 28 27 26 25 24 23 22 #define LCD_D0 8 #define LCD_D1 9 #define LCD_D2 2 #define LCD_D3 3 #define LCD_D4 4 #define LCD_D5 5 #define LCD_D6 6 #define LCD_D7 7 #define LCD_RD A0 #define LCD_WR A1 #define LCD_RS A2 #define LCD_CS A3 #define LCD_RST A4 #define fastDigitalWriteHIGH(Pin) *(portOutputRegister(digitalPinToPort(Pin)))|=digitalPinToBitMask(Pin) //Faster digitalWrite(Pin,HIGH); #define fastDigitalWriteLOW(Pin) *(portOutputRegister(digitalPinToPort(Pin)))&=~digitalPinToBitMask(Pin) //Faster digitalWrite(Pin,LOW); #define RGB(r,g,b) ((b&31)+((g&63)<<5)+((r&31)<<11)) void LcdWriteCommand(unsigned char d){ //Write Command Mode On fastDigitalWriteLOW(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); } void LcdWriteData(unsigned char d){ //Write Command Mode On fastDigitalWriteHIGH(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); } void setup(){ //Initialize Data Pins pinMode(LCD_D0,OUTPUT); pinMode(LCD_D1,OUTPUT); pinMode(LCD_D2,OUTPUT); pinMode(LCD_D3,OUTPUT); pinMode(LCD_D4,OUTPUT); pinMode(LCD_D5,OUTPUT); pinMode(LCD_D6,OUTPUT); pinMode(LCD_D7,OUTPUT); //Initialize Command Pins pinMode(LCD_RD,OUTPUT); pinMode(LCD_WR,OUTPUT); pinMode(LCD_RS,OUTPUT); pinMode(LCD_CS,OUTPUT); pinMode(LCD_RST,OUTPUT); digitalWrite(LCD_CS,LOW); digitalWrite(LCD_RD,HIGH); digitalWrite(LCD_WR,LOW); //Reset digitalWrite(LCD_RST,HIGH); delay(5); digitalWrite(LCD_RST,LOW); delay(15); digitalWrite(LCD_RST,HIGH); delay(15); LcdWriteCommand(0xCB); LcdWriteData(0x39); LcdWriteData(0x2C); LcdWriteData(0x00); LcdWriteData(0x34); LcdWriteData(0x02); LcdWriteCommand(0xCF); LcdWriteData(0x00); LcdWriteData(0XC1); LcdWriteData(0X30); LcdWriteCommand(0xE8); LcdWriteData(0x85); LcdWriteData(0x00); LcdWriteData(0x78); LcdWriteCommand(0xEA); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteCommand(0xED); LcdWriteData(0x64); LcdWriteData(0x03); LcdWriteData(0X12); LcdWriteData(0X81); LcdWriteCommand(0xF7); LcdWriteData(0x20); LcdWriteCommand(0xC0); //Power control LcdWriteData(0x23); //VRH[5:0] LcdWriteCommand(0xC1); //Power control LcdWriteData(0x10); //SAP[2:0];BT[3:0] LcdWriteCommand(0xC5); //VCM control LcdWriteData(0x3e); //Contrast LcdWriteData(0x28); LcdWriteCommand(0xC7); //VCM control2 LcdWriteData(0x86); //-- LcdWriteCommand(0x36); // Memory Access Control LcdWriteData(0x48); LcdWriteCommand(0x3A); LcdWriteData(0x55); LcdWriteCommand(0xB1); LcdWriteData(0x00); LcdWriteData(0x18); LcdWriteCommand(0xB6); // Display Function Control LcdWriteData(0x08); LcdWriteData(0x82); LcdWriteData(0x27); LcdWriteCommand(0x11); //Exit Sleep delay(120); LcdWriteCommand(0x29); //Display on LcdWriteCommand(0x2c); //Set Writing Area int x1 = 0; int x2 = 239; int y1 = 0; int y2 = 319; LcdWriteCommand(0x2a); LcdWriteData(x1>>8); LcdWriteData(x1); LcdWriteData(x2>>8); LcdWriteData(x2); LcdWriteCommand(0x2b); LcdWriteData(y1>>8); LcdWriteData(y1); LcdWriteData(y2>>8); LcdWriteData(y2); //Start Writing LcdWriteCommand(0x2c); for(int i=y1;i<=y2;i++){ for(int j=x1;j<=x2;j++){ LcdWriteData(RGB(31,63,31)>>8); LcdWriteData(RGB(31,63,31)); } } } void loop(){ }
接下来的路线就很简单了,把指定区域的命令(0x2a,0x2b,0x2c)分装成LcdOpenWindow()函数,再实现LcdFill()函数,一个完整的ILI9341驱动就完成了:
#define LCD_D0 8 #define LCD_D1 9 #define LCD_D2 2 #define LCD_D3 3 #define LCD_D4 4 #define LCD_D5 5 #define LCD_D6 6 #define LCD_D7 7 #define LCD_RD A0 #define LCD_WR A1 #define LCD_RS A2 #define LCD_CS A3 #define LCD_RST A4 #define fastDigitalWriteHIGH(Pin) *(portOutputRegister(digitalPinToPort(Pin)))|=digitalPinToBitMask(Pin) //Faster digitalWrite(Pin,HIGH); #define fastDigitalWriteLOW(Pin) *(portOutputRegister(digitalPinToPort(Pin)))&=~digitalPinToBitMask(Pin) //Faster digitalWrite(Pin,LOW); #define RGB(r,g,b) ((b&31)+((g&63)<<5)+((r&31)<<11)) void LcdWriteCommand(unsigned char d){ //Write Command Mode On fastDigitalWriteLOW(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); } void LcdWriteData(unsigned char d){ //Write Command Mode On fastDigitalWriteHIGH(LCD_RS); //Write Datas to LCD_D0 to LCD_D7 PORTD = (PORTD & B00000011) | ((d) & B11111100); PORTB = (PORTB & B11111100) | ((d) & B00000011); //Enable Datas fastDigitalWriteLOW(LCD_WR); fastDigitalWriteHIGH(LCD_WR); } void LcdInit(void){ //Initialize Data Pins pinMode(LCD_D0,OUTPUT); pinMode(LCD_D1,OUTPUT); pinMode(LCD_D2,OUTPUT); pinMode(LCD_D3,OUTPUT); pinMode(LCD_D4,OUTPUT); pinMode(LCD_D5,OUTPUT); pinMode(LCD_D6,OUTPUT); pinMode(LCD_D7,OUTPUT); //Initialize Command Pins pinMode(LCD_RD,OUTPUT); pinMode(LCD_WR,OUTPUT); pinMode(LCD_RS,OUTPUT); pinMode(LCD_CS,OUTPUT); pinMode(LCD_RST,OUTPUT); digitalWrite(LCD_CS,LOW); digitalWrite(LCD_RD,HIGH); digitalWrite(LCD_WR,LOW); //Reset digitalWrite(LCD_RST,HIGH); delay(5); digitalWrite(LCD_RST,LOW); delay(15); digitalWrite(LCD_RST,HIGH); delay(15); LcdWriteCommand(0xCB); LcdWriteData(0x39); LcdWriteData(0x2C); LcdWriteData(0x00); LcdWriteData(0x34); LcdWriteData(0x02); LcdWriteCommand(0xCF); LcdWriteData(0x00); LcdWriteData(0XC1); LcdWriteData(0X30); LcdWriteCommand(0xE8); LcdWriteData(0x85); LcdWriteData(0x00); LcdWriteData(0x78); LcdWriteCommand(0xEA); LcdWriteData(0x00); LcdWriteData(0x00); LcdWriteCommand(0xED); LcdWriteData(0x64); LcdWriteData(0x03); LcdWriteData(0X12); LcdWriteData(0X81); LcdWriteCommand(0xF7); LcdWriteData(0x20); LcdWriteCommand(0xC0); //Power control LcdWriteData(0x23); //VRH[5:0] LcdWriteCommand(0xC1); //Power control LcdWriteData(0x10); //SAP[2:0];BT[3:0] LcdWriteCommand(0xC5); //VCM control LcdWriteData(0x3e); //Contrast LcdWriteData(0x28); LcdWriteCommand(0xC7); //VCM control2 LcdWriteData(0x86); //-- LcdWriteCommand(0x36); // Memory Access Control LcdWriteData(0x48); LcdWriteCommand(0x3A); LcdWriteData(0x55); LcdWriteCommand(0xB1); LcdWriteData(0x00); LcdWriteData(0x18); LcdWriteCommand(0xB6); // Display Function Control LcdWriteData(0x08); LcdWriteData(0x82); LcdWriteData(0x27); LcdWriteCommand(0x11); //Exit Sleep delay(120); LcdWriteCommand(0x29); //Display on LcdWriteCommand(0x2c); } void LcdOpenWindow(unsigned int x1,unsigned int y1,unsigned int x2,unsigned int y2){ LcdWriteCommand(0x2a); LcdWriteData(x1>>8); LcdWriteData(x1); LcdWriteData(x2>>8); LcdWriteData(x2); LcdWriteCommand(0x2b); LcdWriteData(y1>>8); LcdWriteData(y1); LcdWriteData(y2>>8); LcdWriteData(y2); LcdWriteCommand(0x2c); } void LcdFill(int x,int y,int width,int height,unsigned int color) { LcdOpenWindow(x,y,x+width-1,y+height-1); for(int i=y;i<y+height;i++){ for(int j=x;j<x+width;j++){ LcdWriteData(color>>8); LcdWriteData(color); } } } void setup(){ LcdInit(); LcdFill(0,0,239,319,RGB(31,63,31)); LcdFill(10,10,100,100,RGB(31,0,0)); LcdFill(20,20,110,110,RGB(0,63,0)); LcdFill(30,30,120,120,RGB(0,0,31)); } void loop(){ }
都看到这了,还不点个赞吗?(✪ω✪)