目录
前言
一、TFT LCD简介
二、液晶屏的信号线与8080 接口
三、TFTLCD 模块的使用流程
四、软件设计
4.1、常用的LCD函数
4.1.1、LCD地址结构体
4.1.2、清屏函数:void LCD_Clear(u16 color)
4.1.3、填充函数:void LCD_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 color)
4.1.4、填充指定的颜色函数:void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 *color)
4.1.5、画线函数:void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2)
4.1.6、画矩形函数:void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2)
4.1.7、画圆函数:void LCD_Draw_Circle(u16 x0, u16 y0, u8 r)
4.1.8、显示字符函数:void LCD_ShowChar(u16 x, u16 y, u8 num, u8 size, u8 mode)
4.1.9、写数字函数:void LCD_ShowNum(u16 x, u16 y, u32 num, u8 len, u8 size)
4.1.10、写数字函数:void LCD_ShowxNum(u16 x, u16 y, u32 num, u8 len, u8 size, u8 mode)
4.1.11、显示字符串的函数:void LCD_ShowString(u16 x, u16 y, u16 width, u16 height, u8 size, u8 *p)
4.2、主函数的调用
本篇文章,着重是对lcd.c的应用函数进行说明。
显示器属于计算机的 I/O 设备,即输入输出设备。它是一种将特定电子信息输出到屏幕上再反射到人眼的显示工具。常见的有 CRT 显示器、液晶显示器、LED 点阵显示器及 OLED 显示器。
液晶显示器,简称 LCD(Liquid Crystal Display),相对于上一代 CRT 显示器 (阴极射线管显示器), LCD 显示器具有功耗低、体积小、承载的信息量大及不伤眼的优点,因而它成为了现在的主流电子显示设备,其中包括电视、电脑显示器、手机屏幕及各种嵌入式设备的显示器。
本液晶屏内部包含有一个液晶控制芯片 ILI9341,它的内部结构非常复杂。
该芯片最主核心部分是位于中间的 GRAM(Graphics RAM),它就是显存。GRAM 中每个存储单元都对应着液晶面板的一个像素点。它右侧的各种模块共同作用把 GRAM 存储单元的数据转化成液晶面板的控制信号,使像素点呈现特定的颜色,而像素点组合起来则成为一幅完整的图像。
框图的左上角为 ILI9341 的主要控制信号线和配置引脚,根据其不同状态设置可以使芯片工作在不同的模式,如每个像素点的位数是 6、16 还是 18 位;可配置使用 SPI 接口、8080 接口还是 RGB 接口与 MCU 进行通讯。MCU 通过 SPI、8080 接口或 RGB 接口与 ILI9341 进行通讯,从而访问它的控制寄存器 (CR)、地址计数器 (AC)、及 GRAM。
在 GRAM 的左侧还有一个 LED 控制器 (LED Controller)。LCD 为非发光性的显示装置,它需要借助背光源才能达到显示功能,LED 控制器就是用来控制液晶屏中的 LED 背光源。
ILI9341 控制器根据自身的 IM[3:0] 信号线电平决定它与 MCU 的通讯方式,它本身支持 SPI 及 8080 通讯方式,本液晶屏的 ILI9341 控制器在出厂前就已经按固定配置好 (内部已连接硬件电路),它被配置为通过 8080 接口通讯,使用 16 根数据线的 RGB565 格式。
管脚序号 | 液晶屏管脚 | STM32管脚 | 管脚功能 |
---|---|---|---|
1 | FSMC_NE4 | PG12 | LCD片选信号(低电平有效) |
2 | FSMC_A10 | PG0 | 命令/数据控制信号,0:命令,1:数据 |
3 | FSMC_NWE | PD5 | 写使能信号(低电平有效) |
4 | FSMC_NOE | PD4 | 读使能信号(低电平有效) |
5 | RESET | NRST | 复位信号(低电平有效) |
6 | FSMC_D0 | PD14 | 双向数据总线D0 |
7 | FSMC_D1 | PD15 | 双向数据总线D1 |
8 | FSMC_D2 | PD0 | 双向数据总线D2 |
9 | FSMC_D3 | PD1 | 双向数据总线D3 |
10 | FSMC_D4 | PE7 | 双向数据总线D4 |
11 | FSMC_D5 | PE8 | 双向数据总线D5 |
12 | FSMC_D6 | PE9 | 双向数据总线D6 |
13 | FSMC_D7 | PE10 | 双向数据总线D7 |
14 | FSMC_D8 | PE11 | 双向数据总线D8 |
15 | FSMC_D9 | PE12 | 双向数据总线D9 |
16 | FSMC_D10 | PE13 | 双向数据总线D10 |
17 | FSMC_D11 | PE14 | 双向数据总线D11 |
18 | FSMC_D12 | PE15 | 双向数据总线D12 |
19 | FSMC_D13 | PD8 | 双向数据总线D13 |
20 | FSMC_D14 | PD9 | 双向数据总线D14 |
21 | FSMC_D15 | PD10 | 双向数据总线D15 |
22 | SD_CS | PG6 | SD卡片选线 |
23 | LCD_BL | PB0 | LCD背光控制(1:点亮,0:关闭) |
24 | 3.3V | 3.3V | 电源供电 |
25 | 3.3V | 3.3V | 电源供电 |
26 | GND | GND | 电源地 |
27 | GND | GND | 电源地 |
28 | 5V | 5V | 电源供电 |
29 | T_ M ISO | PB2 | 触摸屏SPI信号 |
30 | T_ MOSI | PF 9 | 触摸屏SPI信号 |
31 | T_PEN | PF10 | 触摸屏中断信号 |
32 | NC | NC | 空 |
33 | T_CS | PF11 | 触摸屏SPI片选信号 |
34 | T_SCK | PB1 | 触摸屏SPI时钟信号 |
ILI9341液晶控制器自带显存,其显存总大小为 172800(240*320*18/8),即 18 位模式(26 万色)下的显存量。在 16 位模式下,ILI9341 采用 RGB565 格式存储颜色数据,此时 ILI9341 的 18 位数据线与 MCU 的 16 位数据线以及 LCD GRAM 的对应关系,如图所示:
ILI9341 在 16 位模式下面,数据线有用的是:D17~D13 和 D11~D1,D0 和 D12 没有用到,实际上在LCD 模块里面,ILI9341 的 D0 和 D12 没有引出来,ILI9341 的 D17~D13 和 D11~D1 对应 MCU 的 D15~D0。
MCU 的 16 位数据,最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越大,表示该颜色越深。特别注意 ILI9341 所有的指令都是 8 位的(高 8 位无效),且参 除了读写 GRAM 的时候是 16 位,其他操作参数,都是 8 位的。要调用哪个颜色就令它对应的数据地址等于1就可以了。
(1)设置 STM32F1 与 TFTLCD 模块相连接的 IO 。这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。这里用到的是 FSMC,FSMC 在 《STM32学习笔记》专栏,已经详细介绍了。
(2)初始化 TFTLCD 模块。将 TFTLCD的 RST 同STM32F1的RESET连接在一起了,只要按下开发板的 RESET 键, 就会对 LCD 进行硬复位。
初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校准)这些初始化序列一般 LCD 供应商会提供给客户。在初始化之后,LCD 才可以正常使用。
(3)通过函数将字符和数字显示到 TFTLCD 模块上。即:设置坐标→写 GRAM 指令→写 GRAM来实现, 但是这个步骤,只是一个点的处理,要显示字符/数字,就必须要多次使用这个步骤,从而达到显示字符/数字的目的,需要设计一个函数来实现数字/字符的显示,之后调用该函数, 就可以实现数字/字符的显示了。
结构体如下:
//写寄存器
//LCD_Reg:寄存器地址
//LCD_RegValue:要写入的数据
void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue)
{
LCD->LCD_REG = LCD_Reg; //写入要写的寄存器序号
LCD->LCD_RAM = LCD_RegValue;//写入数据
}
写完REG寄存器地址后,RAM地址自增+1。
函数代码如下:
void LCD_Clear(u16 color)
{
u32 index = 0;
u32 totalpoint = lcddev.width;
totalpoint *= lcddev.height; //得到总点数
LCD_SetCursor(0x00, 0x0000); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
for (index = 0; index < totalpoint; index++)
{
LCD->LCD_RAM=color;
}
}
函数代码如下:
void LCD_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 color)
{
u16 i, j;
u16 xlen = 0;
xlen = ex - sx + 1;
for (i = sy; i <= ey; i++)
{
LCD_SetCursor(sx, i); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
for (j = 0; j < xlen; j++)
{
LCD->LCD_RAM=color; //设置光标位置
}
}
}
函数代码如下:
void LCD_Color_Fill(u16 sx, u16 sy, u16 ex, u16 ey, u16 *color)
{
u16 height, width;
u16 i, j;
width = ex - sx + 1; //得到填充的宽度
height = ey - sy + 1; //高度
for (i = 0; i < height; i++)
{
LCD_SetCursor(sx, sy + i); //设置光标位置
LCD_WriteRAM_Prepare(); //开始写入GRAM
for (j = 0; j < width; j++)
{
LCD->LCD_RAM=color[i * width + j]; //写入数据
}
}
}
函数代码如下:
void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2)
{
u16 t;
int xerr = 0, yerr = 0, delta_x, delta_y, distance;
int incx, incy, uRow, uCol;
delta_x = x2 - x1; //计算坐标增量
delta_y = y2 - y1;
uRow = x1;
uCol = y1;
if (delta_x > 0)incx = 1; //设置单步方向
else if (delta_x == 0)incx = 0; //垂直线
else
{
incx = -1;
delta_x = -delta_x;
}
if (delta_y > 0)incy = 1;
else if (delta_y == 0)incy = 0; //水平线
else
{
incy = -1;
delta_y = -delta_y;
}
if ( delta_x > delta_y)distance = delta_x; //选取基本增量坐标轴
else distance = delta_y;
for (t = 0; t <= distance + 1; t++ ) //画线输出
{
LCD_DrawPoint(uRow, uCol); //画点
xerr += delta_x ;
yerr += delta_y ;
if (xerr > distance)
{
xerr -= distance;
uRow += incx;
}
if (yerr > distance)
{
yerr -= distance;
uCol += incy;
}
}
}
函数代码如下:
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2)
{
LCD_DrawLine(x1, y1, x2, y1);
LCD_DrawLine(x1, y1, x1, y2);
LCD_DrawLine(x1, y2, x2, y2);
LCD_DrawLine(x2, y1, x2, y2);
}
函数代码如下:
void LCD_Draw_Circle(u16 x0, u16 y0, u8 r)
{
int a, b;
int di;
a = 0;
b = r;
di = 3 - (r << 1); //判断下个点位置的标志
while (a <= b)
{
LCD_DrawPoint(x0 + a, y0 - b); //5
LCD_DrawPoint(x0 + b, y0 - a); //0
LCD_DrawPoint(x0 + b, y0 + a); //4
LCD_DrawPoint(x0 + a, y0 + b); //6
LCD_DrawPoint(x0 - a, y0 + b); //1
LCD_DrawPoint(x0 - b, y0 + a);
LCD_DrawPoint(x0 - a, y0 - b); //2
LCD_DrawPoint(x0 - b, y0 - a); //7
a++;
//使用Bresenham算法画圆
if (di < 0)di += 4 * a + 6;
else
{
di += 10 + 4 * (a - b);
b--;
}
}
}
函数代码如下:
前两个参数(x,y)代表的是字符显示的起始坐标,num代表字符在字库中的位置,size代表了字节的大小,mode代表叠加方式。
u8 csize 表示一个字节所占的字节数,横向的点是纵向点的1/2。
接下来就是点开
for循环8位,一位一位的提取。先提取最高位(temp&0x80),然后通过画点LCD_Fast_DrawPoint(x, y, POINT_COLOR),画到显示屏中。
如果mode==0非叠加方式,我们可以在上面加入底色LCD_Fast_DrawPoint(x, y, BACK_COLOR);
画完点之后,纵坐标加一,连续取模。
void LCD_ShowChar(u16 x, u16 y, u8 num, u8 size, u8 mode)
{
u8 temp, t1, t;
u16 y0 = y;
u8 csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); //得到字体一个字符对应点阵集所占的字节数
num = num - ' '; //得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库)
for (t = 0; t < csize; t++)
{
if (size == 12)temp = asc2_1206[num][t]; //调用1206字体
else if (size == 16)temp = asc2_1608[num][t]; //调用1608字体
else if (size == 24)temp = asc2_2412[num][t]; //调用2412字体
else return; //没有的字库
for (t1 = 0; t1 < 8; t1++)
{
if (temp & 0x80)LCD_Fast_DrawPoint(x, y, POINT_COLOR);
else if (mode == 0)LCD_Fast_DrawPoint(x, y, BACK_COLOR);
temp <<= 1;
y++;
if (y >= lcddev.height)return; //超区域了
if ((y - y0) == size)
{
y = y0;
x++;
if (x >= lcddev.width)return; //超区域了
break;
}
}
}
}
函数代码如下:
该函数的优势是,不需要通过建模软件就可以直接写数字,通过num。
但是如果num前面有多余的0不会显示。
void LCD_ShowNum(u16 x, u16 y, u32 num, u8 len, u8 size)
{
u8 t, temp;
u8 enshow = 0;
for (t = 0; t < len; t++)
{
temp = (num / LCD_Pow(10, len - t - 1)) % 10;
if (enshow == 0 && t < (len - 1))
{
if (temp == 0)
{
LCD_ShowChar(x + (size / 2)*t, y, ' ', size, 0);
continue;
}
else enshow = 1;
}
LCD_ShowChar(x + (size / 2)*t, y, temp + '0', size, 0);
}
}
函数代码如下:
该函数num前面有0,依旧显示0。
void LCD_ShowxNum(u16 x, u16 y, u32 num, u8 len, u8 size, u8 mode)
{
u8 t, temp;
u8 enshow = 0;
for (t = 0; t < len; t++)
{
temp = (num / LCD_Pow(10, len - t - 1)) % 10;
if (enshow == 0 && t < (len - 1))
{
if (temp == 0)
{
if (mode & 0X80)LCD_ShowChar(x + (size / 2)*t, y, '0', size, mode & 0X01);
else LCD_ShowChar(x + (size / 2)*t, y, ' ', size, mode & 0X01);
continue;
}
else enshow = 1;
}
LCD_ShowChar(x + (size / 2)*t, y, temp + '0', size, mode & 0X01);
}
}
函数代码如下:
void LCD_ShowString(u16 x, u16 y, u16 width, u16 height, u8 size, u8 *p)
{
u8 x0 = x;
width += x;
height += y;
while ((*p <= '~') && (*p >= ' ')) //判断是不是非法字符!
{
if (x >= width)
{
x = x0;
y += size;
}
if (y >= height)break; //退出
LCD_ShowChar(x, y, *p, size, 0);
x += size / 2;
p++;
}
}
主函数main,如下所示:
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"
int main(void)
{
u8 x=0;
u8 lcd_id[12]; //存放LCD ID字符串
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
LCD_Init();
POINT_COLOR=RED;
sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id);//将LCD ID打印到lcd_id数组。
while(1)
{
switch(x)
{
case 0:LCD_Clear(WHITE);break;
case 1:LCD_Clear(BLACK);break;
case 2:LCD_Clear(BLUE);break;
case 3:LCD_Clear(RED);break;
case 4:LCD_Clear(MAGENTA);break;
case 5:LCD_Clear(GREEN);break;
case 6:LCD_Clear(CYAN);break;
case 7:LCD_Clear(YELLOW);break;
case 8:LCD_Clear(BRRED);break;
case 9:LCD_Clear(GRAY);break;
case 10:LCD_Clear(LGRAY);break;
case 11:LCD_Clear(BROWN);break;
}
POINT_COLOR=RED;
LCD_ShowString(30,40,210,24,24,"Follow me ^_^");
LCD_ShowString(30,70,200,16,16,"wlcome my space");
LCD_ShowString(30,90,200,12,12,"bitter tea seeds");
LCD_ShowString(30,110,200,16,16,lcd_id); //显示LCD ID
LCD_ShowString(30,130,200,12,12,"2022/7/17");
x++;
if(x==12)x=0;
LED0=!LED0;
delay_ms(1000);
}
}