目录
正点原子探索者开发板上使用4.3寸NT35510控制的。其他的芯片驱动也是差不多,不同的芯片驱动的程序不一样,填充函数等是数学算法,写法并没有固定的公式
1.LCD
1.1基本概念
1.1.1LCD的简介
- 液晶显示器,简称 LCD(Liquid Crystal Display)相对于上一代 CRT 显示器 (阴极射线管显示器),LCD显示器具有功耗低、体积小、承载的信息量大及不伤眼的优点,因而它成为了现在的主流电子显示设备
- 液晶是一种介于固体和液体之间的特殊物质,它是一种有机化合物,常态下呈液态,但是它的分 子排列却和固体晶体一样非常规则,因此取名液晶。如果给液晶施加电场,会改变它的分子排列,从而改变光线的传播方向,配合偏振光片,它就具有控制光线透过率的作用,再配合彩色滤光片,改变加给液晶电压大小,就能改变某一颜色透光量的多少。
-
注意液晶本身是不发光的,所以需要有一个背光灯提供光源 ,光线经过一系列处理过程才到输出,所以输出的光线强度是要比光源的强度低很多的
-
显示器的基本参数
(1)像素
像素是组成图像的最基本单元要素,显示器的像素指它成像最小的点,把三种显示结构组成一个显示单位,通过控制红绿蓝的强度,可以使该单位混合输出不同的色彩,这样的一个显示单位被称为像素
(2)分辨率
一些嵌入式设备的显示器常常以“行像素值x列像素值”表示屏幕的分辨率。如分辨率800x480 表示该显示器的每一行有800个像素点,每一列有 480 个像素点,也可理解为有800 列,480行。
(3)显示器尺寸
显示器的大小一般以英寸表示,如5英寸、21 英寸、24 英寸等,这个长度是指屏幕对角线的长度,通过显示器的对角线长度及长宽比可确定显示器的实际长宽尺寸。
- 基本色原理:无法通过其他颜色混合得到的颜色,称之为基本色;通过三基色混合,可以得到自然界中绝大部分颜色
-
色彩深度:色彩深度指显示器的每个像素点能表示多少种颜色,一般用“位”(bit) 来表示。如单色屏的每个像素点能表示亮或灭两种状态 (即实际上能显示2种颜色),用1个数据位就可以表示像素点的所有状态,所以它的色彩深度为1bit,其它常见的显示屏色深为16bit、24bit
-
这样MCU的16位数据,最低5位代表蓝色,中间6位为绿色,最高5位为红色。数值越大,表示该颜色越深。ILI9341在16位模式下面,数据线有用的是:D17~D13和D11~D1,D0和D12没有用到。
1.1.2ILI9341液晶控制器简介
- 通讯引脚
-
RGB信号线
-
同步时钟信号CLK
-
水平同步信号HSYNC
-
垂直同步信号VSYNC
-
数据使能信号 DE
- 显存
(1)液晶屏中的每个像素点都是数据,在实际应用中需要把每个像素点的数据缓存起来,再传输给液晶屏,一般会使用SRAM或SDRAM性质的存储器,而这些专门用于存储显示数据的存储器,则被称为显存。显存一般至少要能存储液晶屏的一帧显示数据,如分辨率为 800x480 的液晶屏,使用RGB888格式显示,它的一帧显示数据大小为:3x800x480=1152000 字节;若使用 RGB565格式显示,一帧显示数据大小为:2x800x480=768000 字节。
- LED控制
(1) 在GRAM的左侧还有一个 LED控制器 (LED Controller)。LCD为非发光性的显示装置,它需要借助背光源才能达到显示功能,LED 控制器就是用来控制液晶屏中的LED背光源
1.1.3LCD的时序图
-
液晶屏有一个显示指针, 它指向将要显示的像素。显示指针的扫描方向方向从左到右、从上到下,一个像素点一个像素点地描绘图形。这些像素点的数据通过RGB数据线传输至液晶屏,它们在同步时钟CLK 的驱动下一个一个地传输到液晶屏中,交给显示指针,传输完成一行时,水平同步信号 HSYNC电平跳变一次,而传输完一帧时VSYNC电平跳变一次
1.1.48080时序的模拟
- 若向0x60000000地址写入数据如0xABCD,FSMC会自动在各信号线上产生相应的电平信号,写入数据。FSMC会控制片选信号NE1选择相应的NOR芯片,然后使用地址线A[25:0]输出0x60000000,在NWE写使能信号线上发出低电平的写使能信号,而要写入的数据信号 0xABCD则从数据线D[15:0] 输出,然后数据就被保存到NOR FLASH中了
-
由图可知,写命令时序由片选信号CSX拉低开始,对数据/命令选择信号线D/CX也置低电平表示写入的是命令地址 (可理解为命令编码,如软件复位命令:0x01),以写信号WRX为低,读信号RDX为高表示数据传输方向为写入,同时,在数据线D[17:0](或 D[15:0]) 输出命令地址,在第二个传输阶段传送的是命令的参数,所以D/CX要置高电平,表示写入的是命令数据,命令数据是某些指令带有的参数,如复位指令编码为0x01,它后面可以带一个参数,该参数表示多少秒后复位
-
对于FSMC和8080 接口,前四种信号线都是完全一样的,仅仅是FSMC的地址信号线A[25:0]与8080 的数据/命令选择线 D/CX 有区别。 而对于 D/CX 线,它为高电平的时候表示数值,为低电平的时候表示命令 ,如果能使用 FSMC 的 A 地址线根据不同的情况产生对应的电平,那么就完全可以使用 FSMC 来产生 8080 接口需要的时序了。
-
为了模拟出8080时序我们可以把FSMC的A0地址线 (也可以使用其它 A1/A2 等地址线) 与ILI9341 芯片 8080 接口的D/CX信号线连接,那么当A0为高电平时 (即 D/CX 为高电平),数据线D[15:0] 的信号会被ILI9341理解为数值,若A0为低电平时 (即 D/CX 为低电平),传输的信号则会被理解为命令

- 由于FSMC会自动产生地址信号,当使用FSMC向0x6xxx xxx1、0x6xxx xxx3、0x6xxx xxx5…这些奇数地址写入数据时,地址最低位的值均为 1,所以它会控制地址线 A0(D/CX) 输出高电平,那么这时通过数据线传输的信号会被理解为数值;若向 0x6xxx xxx0 、0x6xxx xxx2、0x6xxx xxx4…这些偶数地址写入数据时,地址最低位的值均为 0,所以它会控制地址线 A0(D/CX) 输出低电平, 因此这时通过数据线传输的信号会被理解为命令
1.2代码讲解(正点原子和LCD9341芯片)
1.2.1读和写操作代码
/* FSMC相关参数 定义
* 注意: 我们默认是通过FSMC块1来连接LCD, 块1有4个片选: FSMC_NE1~4
*
* 修改LCD_FSMC_NEX, 对应的LCD_CS_GPIO相关设置也得改
* 修改LCD_FSMC_AX , 对应的LCD_RS_GPIO相关设置也得改
*/
#define LCD_FSMC_NEX 4 /* 使用FSMC_NE4接LCD_CS,取值范围只能是: 1~4 */
#define LCD_FSMC_AX 6 /* 使用FSMC_A6接LCD_RS,取值范围是: 0 ~ 25 */
/* LCD_BASE的详细解算方法:
* 我们一般使用FSMC的块1(BANK1)来驱动TFTLCD液晶屏(MCU屏), 块1地址范围总大小为256MB,均分成4块:
* 存储块1(FSMC_NE1)地址范围: 0x6000 0000 ~ 0x63FF FFFF
* 存储块2(FSMC_NE2)地址范围: 0x6400 0000 ~ 0x67FF FFFF
* 存储块3(FSMC_NE3)地址范围: 0x6800 0000 ~ 0x6BFF FFFF
* 存储块4(FSMC_NE4)地址范围: 0x6C00 0000 ~ 0x6FFF FFFF
*
* 我们需要根据硬件连接方式选择合适的片选(连接LCD_CS)和地址线(连接LCD_RS)
* 探索者F407开发板使用FSMC_NE4连接LCD_CS, FSMC_A6连接LCD_RS ,16位数据线,计算方法如下:
* 首先FSMC_NE4的基地址为: 0x6C00 0000; NEX的基址为(x=1/2/3/4): 0x6000 0000 + (0x400
* 0000 * (x - 1))
* FSMC_A6对应地址值: 2^6 * 2 = 0x80; FSMC_Ay对应的地址为(y = 0 ~ 25): 2^y * 2
*
* LCD->LCD_REG,对应LCD_RS = 0(LCD寄存器); LCD->LCD_RAM,对应LCD_RS = 1(LCD数据)
* 则 LCD->LCD_RAM的地址为: 0x6C00 0000 + 2^6 * 2 = 0x6C00 0080
* LCD->LCD_REG的地址可以为 LCD->LCD_RAM之外的任意地址.
* 由于我们使用结构体管理LCD_REG 和 LCD_RAM(REG在前,RAM在后,均为16位数据宽度)
* 因此 结构体的基地址(LCD_BASE) = LCD_RAM - 2 = 0x6C00 0080 -2
*
* 更加通用的计算公式为((片选脚FSMC_NEX)X=1/2/3/4, (RS接地址线FSMC_Ay)y=0~25):
* LCD_BASE = (0x6000 0000 + (0x400 0000 * (x - 1))) | (2^y * 2 -2)
* 等效于(使用移位操作)
* LCD_BASE = (0x6000 0000 + (0x400 0000 * (x - 1))) | ((1 << y) * 2 -2)
*/
#define LCD_BASE (uint32_t)((0x60000000+(0x4000000*(LCD_FSMC_NEX-1)))|(((1<< LCD_FSMC_AX)*2)-2))
#define LCD ((LCD_TypeDef *) LCD_BASE)
/**
* @brief 写数据的时候由基地址选择Bank1的NE4,由A6选择数据位则向左移动7位,此时为高电平。为传送
* 数据位。写数据其实就是传送像素点
* @param data: 要写入的数据
* @retval 无
*/
void lcd_wr_data(volatile uint16_t data)
{
data = data; /* 使用-O2优化的时候,必须插入的延时 */
LCD->LCD_RAM = data;
}
/**
* @brief 写命令的时候由基地址选择Bank1的NE4,由A6选择数据位则向左移动7位,此时为低电平。 为传
* 送命令。
* @param regno: 寄存器编号/地址
* @retval 无
*/
void lcd_wr_regno(volatile uint16_t regno)
{
regno = regno; /* 使用-O2优化的时候,必须插入的延时 */
LCD->LCD_REG = regno; /* 写入要写的寄存器序号 */
}
/**
* @brief LCD延时函数,仅用于部分在mdk -O1时间优化时需要设置的地方
* @param t:延时的数值
* @retval 无
*/
static void lcd_opt_delay(uint32_t i)
{
while (i--); /* 使用AC6时空循环可能被优化,可使用while(1) __asm volatile(""); */
}
/**
* @brief 读操作就是将寄存器的值赋值给定义的变量读出来
* @param
* @retval
*/
static uint16_t lcd_rd_data(void)
{
volatile uint16_t ram; /* 防止被优化 */
lcd_opt_delay(2);
ram = LCD->LCD_RAM; /* 数据位寄存器的数据赋值给变量*/
return ram; /* 返回变量*/
}
1.2.2LCD的开启显示代码
-
接下来看指令:0X2C,该指令是写 GRAM 指令,在发送该指令之后,我们便可以往 LCD的 GRAM 里面写入颜色数据了,该指令支持连续写
/**
* @brief 设置LCD显示方向
* @retval 无
*/
void lcd_display_dir(uint8_t dir)
{
lcddev.wramcmd = 0x2C; //开启内存写操作
lcddev.setxcmd = 0x2A; //初始化行命令
lcddev.setycmd = 0x2B; //初始化列命令
}
/**
* @brief LCD开启显示
* @param 无
* @retval 无
*/
void lcd_display_on(void)
{
lcd_wr_regno(0x29); /* 开启显示 */
}
/**
* @brief LCD关闭显示
* @param 无
* @retval 无
*/
void lcd_display_off(void)
{
lcd_wr_regno(0x28); /* 关闭显示 */
}
/**
* @brief 设置光标位置
* @retval 无
*/
void lcd_set_cursor(uint16_t x, uint16_t y)
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0xFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0xFF);
}
1.2.3LCD的扫描代码
/**
* @brief 设置LCD的自动扫描方向(对RGB屏无效)
* @note
* 9341/5310/5510/1963/7789/7796/9806等IC已经实际测试
* 注意:其他函数可能会受到此函数设置的影响(尤其是9341),
* 所以,一般设置为L2R_U2D即可,如果设置为其他扫描方式,可能导致显示不正常.
*
* @param dir:0~7,代表8个方向(具体定义见lcd.h)
* @retval 无
*/
void lcd_scan_dir(uint8_t dir)
{
/* 根据扫描方式 设置 0x36/0x3600 寄存器 bit 5,6,7 位的值 */
switch (dir)
{
case L2R_U2D: /* 从左到右,从上到下 */
regval |= (0 << 7) | (0 << 6) | (0 << 5);
break;
case L2R_D2U: /* 从左到右,从下到上 */
regval |= (1 << 7) | (0 << 6) | (0 << 5);
break;
case R2L_U2D: /* 从右到左,从上到下 */
regval |= (0 << 7) | (1 << 6) | (0 << 5);
break;
case R2L_D2U: /* 从右到左,从下到上 */
regval |= (1 << 7) | (1 << 6) | (0 << 5);
break;
case U2D_L2R: /* 从上到下,从左到右 */
regval |= (0 << 7) | (0 << 6) | (1 << 5);
break;
case U2D_R2L: /* 从上到下,从右到左 */
regval |= (0 << 7) | (1 << 6) | (1 << 5);
break;
case D2U_L2R: /* 从下到上,从左到右 */
regval |= (1 << 7) | (0 << 6) | (1 << 5);
break;
case D2U_R2L: /* 从下到上,从右到左 */
regval |= (1 << 7) | (1 << 6) | (1 << 5);
break;
}
dirreg = 0x36; /* 0x36的高三位可以设置GRAM的扫描方向 */
/* 9341要设置BGR位 */
if (lcddev.id == 0x9341)
{
regval |= 0x08; //BGR位要设置1
}
lcd_wr_regno(lcddev.setxcmd); //发送2A的行命令
lcd_wr_data(0); //x起始坐标高8位
lcd_wr_data(0); //x起始坐标低8位
lcd_wr_data((lcddev.width - 1) >> 8); //x结束坐标高8位
lcd_wr_data((lcddev.width - 1) & 0xFF);//x 结束坐标低8位
lcd_wr_regno(lcddev.setycmd); //发送2B列命令
lcd_wr_data(0);
lcd_wr_data(0);
lcd_wr_data((lcddev.height - 1) >> 8);
lcd_wr_data((lcddev.height - 1) & 0xFF);
}
1.2.4LCD的像素的显示
- 在16位模式下,ILI9341采用RGB565 格式存储颜色数据,此时 ILI9341 的18位显存与 MCU 的16位数据线以及RGB565的对应关系如图
-
从图中可以看出,ILI9341 在16位模式下面,18 位显存的 B0 和 B12 并没有用到,对外的数据线使用 DB0-DB15 连接 MCU 的 D0-D15 实现 16 位颜色的传输
-
这样MCU的6位数据,最低5位代表蓝色,中间6位为绿色,最高5位为红色。数值越大,表示该颜色越深。另外,特别注意 ILI9341 所有的指令都是 8 位的(高 8 位无效),且参数 除了读写 GRAM 的时候是16位,其他操作参数,都是8位的
#define WHITE 0xFFFF /* 白色 */
#define BLACK 0x0000 /* 黑色 */
#define RED 0xF800 /* 红色 */
#define GREEN 0x07E0 /* 绿色 */
#define BLUE 0x001F /* 蓝色 */
#define MAGENTA 0xF81F /* 品红色/紫红色 = BLUE + RED */
#define YELLOW 0xFFE0 /* 黄色 = GREEN + RED */
#define CYAN 0x07FF /* 青色 = GREEN + BLUE */
/**
* @brief 设置光标位置(对RGB屏无效)
* @param x,y: 坐标
* @retval 无
*/
void lcd_set_cursor(uint16_t x, uint16_t y)
{
lcd_wr_regno(lcddev.setxcmd);
lcd_wr_data(x >> 8);
lcd_wr_data(x & 0xFF);
lcd_wr_regno(lcddev.setycmd);
lcd_wr_data(y >> 8);
lcd_wr_data(y & 0xFF);
}
void lcd_write_ram_prepare(void)
{
LCD->LCD_REG = lcddev.wramcmd; //wramcmd=0x2c,准备写像素的命令
}
/**
* @brief 画点
* @param x,y: 坐标
* @param color: 点的颜色(32位颜色,方便兼容LTDC)
* @retval 无
*/
void lcd_draw_point(uint16_t x, uint16_t y, uint32_t color)
{
lcd_set_cursor(x, y); /* 设置光标位置 */
lcd_write_ram_prepare(); /* 开始写入GRAM */
LCD->LCD_RAM = color; /* 向一个像素点填充颜色/
}
1.2.5LCD的初始化代码
/**
* @brief 初始化LCD
* @note 该初始化函数可以初始化各种型号的LCD(详见本.c文件最前面的描述)
*
* @param 无
* @retval 无
*/
void lcd_init(void)
{
/* 尝试9341 ID的读取 */
lcd_wr_regno(0xD3);
lcddev.id = lcd_rd_data(); /* dummy read */
lcddev.id = lcd_rd_data(); /* 读到0x00 */
lcddev.id = lcd_rd_data(); /* 读取93 */
lcddev.id <<= 8;
lcddev.id |= lcd_rd_data(); /* 读取41 */
lcd_ex_ili9341_reginit(); /* 执行ILI9341初始化 */
}
/**
* @brief ILI9341寄存器初始化代码
* @param 无
* @retval 无
*/
void lcd_ex_ili9341_reginit(void)
{
lcd_wr_regno(0xCF);
lcd_wr_data(0x00);
lcd_wr_data(0xC1);
lcd_wr_data(0x30);
lcd_wr_regno(0xED);
lcd_wr_data(0x64);
lcd_wr_data(0x03);
lcd_wr_data(0x12);
lcd_wr_data(0x81);
lcd_wr_regno(0xE8);
lcd_wr_data(0x85);
lcd_wr_data(0x10);
lcd_wr_data(0x7A);
lcd_wr_regno(0xCB);
lcd_wr_data(0x39);
lcd_wr_data(0x2C);
lcd_wr_data(0x00);
lcd_wr_data(0x34);
lcd_wr_data(0x02);
lcd_wr_regno(0xF7);
lcd_wr_data(0x20);
lcd_wr_regno(0xEA);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_regno(0xC0); /* Power control */
lcd_wr_data(0x1B); /* VRH[5:0] */
lcd_wr_regno(0xC1); /* Power control */
lcd_wr_data(0x01); /* SAP[2:0];BT[3:0] */
lcd_wr_regno(0xC5); /* VCM control */
lcd_wr_data(0x30); /* 3F */
lcd_wr_data(0x30); /* 3C */
lcd_wr_regno(0xC7); /* VCM control2 */
lcd_wr_data(0xB7);
lcd_wr_regno(0x36); /* Memory Access Control */
lcd_wr_data(0x48);
lcd_wr_regno(0x3A);
lcd_wr_data(0x55);
lcd_wr_regno(0xB1);
lcd_wr_data(0x00);
lcd_wr_data(0x1A);
lcd_wr_regno(0xB6); /* Display Function Control */
lcd_wr_data(0x0A);
lcd_wr_data(0xA2);
lcd_wr_regno(0xF2); /* 3Gamma Function Disable */
lcd_wr_data(0x00);
lcd_wr_regno(0x26); /* Gamma curve selected */
lcd_wr_data(0x01);
lcd_wr_regno(0xE0); /* Set Gamma */
lcd_wr_data(0x0F);
lcd_wr_data(0x2A);
lcd_wr_data(0x28);
lcd_wr_data(0x08);
lcd_wr_data(0x0E);
lcd_wr_data(0x08);
lcd_wr_data(0x54);
lcd_wr_data(0xA9);
lcd_wr_data(0x43);
lcd_wr_data(0x0A);
lcd_wr_data(0x0F);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_regno(0xE1); /* Set Gamma */
lcd_wr_data(0x00);
lcd_wr_data(0x15);
lcd_wr_data(0x17);
lcd_wr_data(0x07);
lcd_wr_data(0x11);
lcd_wr_data(0x06);
lcd_wr_data(0x2B);
lcd_wr_data(0x56);
lcd_wr_data(0x3C);
lcd_wr_data(0x05);
lcd_wr_data(0x10);
lcd_wr_data(0x0F);
lcd_wr_data(0x3F);
lcd_wr_data(0x3F);
lcd_wr_data(0x0F);
lcd_wr_regno(0x2B);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_data(0x01);
lcd_wr_data(0x3f);
lcd_wr_regno(0x2A);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_data(0x00);
lcd_wr_data(0xef);
lcd_wr_regno(0x11); /* Exit Sleep */
HAL_Delay(120);
lcd_wr_regno(0x29); /* display on */
}
1.2.6其他
正点原子探索者开发板上使用4.3寸NT35510控制的。其他的芯片驱动也是差不多,不同的芯片驱动的程序不一样,填充函数等是数学算法,写法并没有固定的公式
1.3FSMC的配置显示
1.3.1FSMC的配置
- 在STM32CubeMX中可以设置读写时序的地址建立时间和数据建立时间,单位为一个HCLK时钟周期,当HCLK时钟频率为STM32F407最高频率168MHz时,一个周期大约为6us
- 地址建立时间,0-0xF个HCLK周期,地址建立时间,0-0xF个HCLK周期,在cubmx中写模式下的数据建立时间不包含后面的加1。读地址建立时间>10ns(2个HCLK时钟周期),写地址建立时间>0us,数据建立/保持时间>15+10ns(5个HCLK时钟周期),读写地址保持时间>2ns,,另外总线翻转时间设置为0,访问模式选择A即可。
-
Extended mode:本成员用于设置是否使用扩展模式 在非扩展模式下,对存储器读写的时序都只使用 FSMC_BCR 寄存器中的配置, 在扩展模式下,对存储器的读写时序可以分开配置 ,读时序使用 FSMC_BCR寄存器,写时序使用FSMC_BWTR寄存器的配置,
1.3.2实验效果
lcd_init();
lcd_show_string(30,40,210,24,24,"2024/9/5",RED);