LCD显示屏开发

LCD显示屏介绍

什么是LCD?

LCD(Liquid Crystal Display,液晶显示屏)是一种利用液晶材料的光电效应实现显示内容的平面显示设备。LCD广泛应用于嵌入式系统,如智能家居、工业控制、手持设备等。

LCD基本结构

LCD主要由以下几部分组成:

  • 液晶层:夹在两片玻璃基板之间,负责成像。
  • 偏光片:用于控制光线的通过与偏振。
  • 电极层:通过电信号控制液晶分子的排列。
  • 驱动电路:实现信号传递,控制显示内容。
  • 背光源:提供照明,使显示内容可见

LCD显示原理

LCD的显示原理是通过控制液晶分子的排列方式,改变光线的透射与反射,从而实现不同的显示效果。常见的显示方式有:

  • 静态驱动:每个像素单独控制,适用于小尺寸LCD。
  • 动态驱动:通过扫描方式控制行列,适用于大尺寸LCD。

常见屏幕型号

字符型LCD(Character LCD)

  • 特点:只能显示固定字符(如字母、数字),部分支持自定义简易符号。
  • 常见型号:1602(16列2行)、2004(20列4行)等。
  • 接口:并口(HD44780)、部分带I2C/SPI扩展板。
  • 用途:仪表、家电、简单菜单等。

段码屏(Segment LCD)

  • 特点:只能显示预设的数字符号图案(如电子秤、万年历的数字段)。
  • 常见型号:7段、14段、定制段码等。
  • 接口:并口(直接控制每个段)。
  • 用途时钟电子秤、遥控器、家电面板等。

图形点阵LCD(Graphic LCD)

  • 特点:支持显示任意点阵图形、汉字、图片等。
  • 常见型号:12864(128×64点)、NOKIA5110(84×48点)、ST7565等。
  • 接口:并口、SPI、I2C。
  • 用途:工业仪表、便携设备、复杂菜单、简易图形显示。

TFT彩色LCD(TFT LCD)

  • 特点:全彩显示,高分辨率,支持图片/动画/复杂UI。
  • 常见型号:ST7735(128×160)、ST7789(240×240)、ILI9341(240×320)等。
  • 接口:SPI、并口、RGB、MIPI等。
  • 用途:智能穿戴、手持终端、车载中控、消费电子。

IPS宽视角LCD(IPS LCD)

  • 特点:TFT的改进型,色彩更好,可视角更大,响应更快。
  • 常见型号:与TFT同,通常会注明“IPS”。
  • 用途:智能终端、高端显示、UI界面。

OLED显示屏(非LCD)

  • 特点:自发光,无需背光,厚度薄,对比度高,功耗低。
  • 常见型号:SSD1306(128×64)、SH1106等。
  • 接口:I2C、SPI。
  • 用途:智能手表、便携设备、低功耗场景。

主要区别总结

类型 显示内容 色彩 代表型号 典型接口 主要用途
字符型LCD 字符 单色 1602、2004 并口/I2C 仪表、菜单
段码屏 数字/符号 单色 7段、14段定制 并口 家电、计时器
点阵LCD 图形+文字 单色 12864、5110 并口/SPI/I2C 工业、定制图形
TFT LCD 图形/图片/动画 全彩 ILI9341等 SPI/并口 UI、动画、终端
IPS LCD 图形/图片/动画 全彩 IPS系列 SPI/并口等 高清显示
OLED(非LCD) 图形+文字 单/彩 SSD1306等 SPI/I2C 便携、低功耗

LCD开发手册解读

这里主要是记录我拿到一块屏幕该如何看手册,不至于一脸懵

还有就是LCD驱动时的一些思路,是为了学习开发驱动的过程和加快开发速度

厂家代码

在实操使用情况都是先跟屏幕厂家拿初始化时序,不同屏幕先拿到厂家提供时序代码,再来修改使用,一般厂家会提供,自己瞎试很费劲,如果时序不对,驱动起来屏幕会花屏

一般厂家会给我们初始化代码和驱动代码,我们一般不进行全部从零开始开发,这个涉及到底层驱动的实现,一般很麻烦,我们实际开发产品的时候都是对厂家提供的驱动例程等进行移植+修改,除非是芯片冷门没有驱动或有特殊性能优化需求的时候。

原因主要如下

驱动代码重复性很高,且容易出错

厂家/模块商往往会提供参考驱动、Demo、移植包,甚至HAL库兼容的驱动。

屏幕手册

我们这里使用P169H002-CTP触摸屏,首先先对一定是屏幕的手册进行了解,具体分为以下几个方面

总体描述

手册先看总体描述,了解屏幕开发时比较关键的参数:分辨率驱动芯片通信接口,其余的看看就即可

分辨率: 240×180

驱动芯片: ST7789V

通信接口:4-line SPI

  • 特性

    • 4 线 SPI 并行接口,方便与主控芯片连接。
  • 应用场景

    • MPOS 设备(移动支付终端)
    • 个人导航设备
    • 其他需要高质量显示的设备

image-20250726143949152

全介绍:

项目 规格 单位/说明 备注说明
屏幕尺寸 1.69 英寸(对角线) 常用于小型嵌入式设备
像素数量 240(RGB)(H) x 280(V) 像素 横向240,纵向280,RGB三色子像素
显示区域 27.97(H) x 32.63(V) 毫米 实际可视的显示区域
像素间距 0.11655(H) x 0.11655(V) 毫米 每个像素的物理间隔
外形尺寸 33.13 x 41.13 x 3.61 毫米 屏幕整体外壳尺寸
像素排列 RGB竖条 —— 每像素按红绿蓝竖条排列
显示模式 Normally Black —— 默认状态为黑色底
视角 ALL —— 全视角,任意方向都能清晰看到
灰度反转方向 —— 无特殊灰度反转
显示色彩 262K —— 18位色深,约26.2万种颜色
亮度 350 nit(cd/m²) 屏幕亮度,适合室内外使用
对比度 1000:1 —— 显示黑白对比效果
表面处理 —— —— 未特别说明
接口类型 4线SPI —— SPI串行接口,常见于嵌入式主控与LCD通信
背光类型 LED侧入式 —— LED灯从侧面照明
驱动芯片 ST7789V —— 负责与主控通信及驱动LCD显示
工作温度 -20~70 适用环境温度范围
存储温度 -30~80 存储时允许的温度范围
重量 —— g 未标注

引脚描述

然后是看引脚描述,一定要确定LCD各个引脚功能开发才能有大体思路,最后把哪些引脚属于哪部分相关的给分类

1.显示驱动芯片ST7789V相关(SPI接口)

D/C:数据/命令选择

CS: SPI片选信号

SCL: SPI时钟信号

SDA: SPI数据输入/输出引脚

RESET:外部复位信号

2.触摸芯片CST816T相关(I2C接口)

TP_SCL: 触摸I2C时钟信号

TP_SDA: 触摸I2C数据信号

TP_TRST: 触摸复位信号

TP_TINT: 触摸中断信号

屏幕英文手册:

image-20250719201136911

中文翻译:

image-20250719201927226

D/C引脚:Data/Command引脚,0代表是发送命令,1代表发送数据

电气特性

手册上这部分主要是进行硬件设计时需要关心的,软件暂时不用管

ST7789

ST7789介绍

ST7889是一款常用于小尺寸TFT-LCD屏的显示驱动芯片。它集成了显示RAM、控制器和驱动器,支持多种显示接口(通常为SPI或并口),为嵌入式设备如手表、智能穿戴、工业仪表等提供图像显示解决方案。

ST7789 通过 SPI 或并行接口与主控芯片通信,能够驱动 RGB 或 MCU 接口的 LCD 屏幕。

特性:

1.支持分辨率

ST7789 支持常见的 LCD 分辨率,如 240x320、240x240 等。

2.内置GRAM(显存)

芯片内部集成显示缓冲区(GRAM),方便主控MCU刷新显示内容。

3.通信接口

支持SPI、并口(MCU接口),部分型号支持I2C。

4.显示模式

支持 16 位(RGB565)和 18 位(RGB666)颜色深度。,可配置显示方向(横屏/竖屏)。

5.电源管理

内部集成电源电路,支持低功耗模式,适合电池供电的设备。

6.命令集丰富

提供寄存器配置命令,可自定义显示区域、色彩格式、背光控制等。

手册阅读

尽可能地只阅读我们需要的部分,减少耗时,下方的介绍顺序也是以后开发时推荐阅读手册的顺序

特征介绍

首先直接看一下特征介绍: 大致看一下分辨率通信接口等等

image-20250726174412303

全引脚介绍

第六章是所有的引脚功能介绍,随便看看即可,是在要看只需要关注通信相关引脚、模式引脚等

image-20250726183552033

通信接口

我们之前在手册第二章特征描述中看到了ST7789支持的通信接口如下:

接口类型 说明
8080系列MCU接口(并口) 支持8位、9位、16位、18位数据位宽,适用于高速数据传输
RGB接口(6/16/18位) 包含VSYNC、HSYNC、DOTCLK、ENABLE、DB[17:0]等信号,适合高分辨率彩色显示
3/4线SPI串行接口 即Serial Peripheral Interface,便于少管脚连接及嵌入式应用
VSYNC接口 专用同步信号接口,用于显示刷新同步

image-20250726173653775

然后就是找对我们关心的SPI串行接口通信相关部分: 我们可以看到第8章就对各个接口(8080、SPI)等功能进行详细描述,主要就是告诉我们该如何发送指令,数据等。

我们屏幕使用的是四线SPI,所以只关注四线SPI即可,主要看四线的引脚定义,如何写命令/数据等

image-20250726160426692

通信引脚定义

ST7789通过IM3、IM2、IM1、IM0电平决定芯片属于哪种接口模式,其中圈起来的是Serial Interface(SPI),相关的模式

image-20250726182304530

对于该屏幕P169H002-CTP上说的是4线SPI模式,对应原理上ST7789的IM0、IM1、IM2、IM3连接如图:0 1 1 0,也对应4线SPI的模式

image-20250726182349760

所以我们就该去了解Serial Interface相关内容,主要是各个功能进行了解

ST7789的串行接口主要有四种引脚定义:三线串行接口Ⅰ/Ⅱ四线串行接口Ⅰ/Ⅱ

原文:

image-20250726161822078

这部分说的是串行接口有3线/9位或4线/8位双向接口:

3线串行接口(3-line serial interfaceⅠ/Ⅱ)使用:CSX(芯片使能),SCL(串行时钟)和SDA(串行数据输入/输出)。

4线串行接口(4-line serial interfaceⅠ/Ⅱ)使用:CSX(芯片使能),SCL(串行时钟)和SDA(串行数据输入/输出),D/CX(数据/命令标志)

串行时钟(SCL)仅用于与单片机的接口,因此它可以在不需要通信时停止。

image-20250726150654264

image-20250726150711904

P169H002-CTP:对于引脚的描述中,我们明显看到只有一个SDA,所以是4-line serial interface I模式

对于4-line serial interface I:

  • CSX:片选信号,低电平时选中LCD
  • WRX(D/CX):这个就是数据/命令选择引脚(D/CX),当WRX为低,数据被视为命令。当WRX为高,数据被视为数据/参数
    • 低电平:代表命令(如0x2A设置窗口)
    • 高电平:代表参数或像素数据
  • DCX(SCL):这个是时钟信号SCL
  • SDA:数据输入/输出线

在4-line serial interface Ⅱ模式下,SDA变为输入,新增SDO引脚

SDA: 数据输入

SDO:数据输出

RDX、WRX、DCX说明

在看手册的过程中,RDX、WRX、DCX这几个引脚会在写命令时出现,手册上经常混用,为了避免弄混,所以这里专门说明一下

手册描述(具体RDX和WRX的作用在对应的接口部分都有说明,以下做一个区分):

DCX: 在不同接口时作用不同,主要有两种情况:

  • 8080接口下:DCX就是用作是数据/命令选择引脚(D/CX),此时0/1代表命令和数据
  • 串行接口(SPI)下: DCX被复用为SCK时钟引脚,通常可以看到原理图上DCX被引出作为SCL/SCK

image-20250726181112908

RDX:只在MCU的8080接口(并口)通信时有用,代表读使能,对于其他情况下忽略即可

image-20250726181200455

WRX: 在以下几种接口的作用不同,主要情况如下

  • 8080接口(并口)下:WRX是写使能信号,LCD驱动芯片会在WRX上升沿(↑)采集内容(数据/命令)
  • 串行接口(SPI)下:
    • 3线串行接口SPI:只用CSX/SCL/SDA,WRX和RDX未使用(数据/命令选择是传输1bit数据位,不用控制引脚),我们直接不管WRX即可
    • 4线串行接口SPIWRX被复用为DCX引脚,是数据/命令选择的功能,通常可以看到原理图上WRX被引出作为RS/DCX
  • Second Data lane下:WRX用作第二数据线(SDA2), 此情况为特殊情况,大部分不用

image-20250726181214381

写命令

了解完引脚之后,我们就可以看如何发送命令,发送命令的格式、通信的时序等

数据格式

原文:

image-20250726162424293

该接口的写模式意味着微控制器会将指令和数据写入液晶驱动器.

3线串行数据包包含一个控制位D/CX和一个传输字节: 直接发送数据1bit(D/CX) + 8bit(Cmd 或 data)

然后ST7789启动IC直接对这个1bit+8bit进行解析,解析出来的1bit,决定8bit是Cmd还是data

image-20250726162624322


在4线串行接口中,数据包仅包含传输字节,而控制位D/CX则通过D/CX引脚进行传输: 先设置D/CX引脚电平,然后发送8bit(Cmd 或 data)

然后ST7789驱动IC解析:如果D/CX引脚为低,则传输字节将被解释为命令字节。如果D/CX引脚为高,则该传输字节将被存储在显示数据RAM(内存写入命令)中,或者作为参数存储在命令寄存器中。

image-20250726162759765

任何指令都可以以任意顺序发送给驱动器。最高有效位(MSB)首先传输。串行接口在CSX为高电平时被初始化。在此状态下,SCL
时钟脉冲或SDA数据均不起作用。CSX的下降沿使串行接口启用,并表示数据传输的开始。

时序

原文描述:

image-20250726164808220

  • **CSX为高时,SCL和SDA都被忽略,接口处于初始化/空闲状态。**也就是说,只有CSX拉低后,LCD才开始“收数据”。

  • **CSX下降沿触发传输开始,此时SCL可以是高或低。**你不需要关心SCL初始状态,只要CSX被拉低了,LCD就准备接收数据了。

  • **SDA(数据线)在 SCL(时钟线)每次上升沿被采样。**即每个字节的每一位数据都是在 SCL 上升沿被锁存。

  • **D/CX 的作用就是区分命令/数据(0=命令,1=参数/内存数据)。**需要根据发送内容切换 D/CX 的电平。

  • 采样D/CX的时机:

    • 3线串口: D/CX是在第一个 SCL 上升沿被采样(也就是说每个字节前携带一个 D/CX 位)。
    • 4线串口: D/CX是通过外部引脚传递,LCD会在每8个 SCL 上升沿(即每发送一个字节时)采样 D/CX 的电平。
      所以你发命令时 D/CX 拉低,发数据时 D/CX 拉高,LCD在每字节的第8个 SCL 上升沿采样当前 D/CX 状态。
  • 如果CSX保持低电平(没有结束传输),LCD会等待下一个字节的 D/CX 或数据位。

4-line

image-20250726163648023

3-line

image-20250726164204267

读数据

这里只会写入LCD,没有读取,到时候用的时候再看手册吧

LCD显示开发时相关知识

LCD背光源(Back Light)

背光层介绍

OLED:屏幕采用了有机发光材料,每个像素都可以发光,也就是可以自发光不需要LCD屏幕那样的背光层、液晶层,也能点亮。因此可以做的更加轻薄,实现屏下指纹、柔性屏等特殊的功能。

LCD:屏幕的发光原理主要依靠背光层,背光层发出白光,背光层上有一层有颜色的薄膜,透过薄膜之后就能显示出彩色,在背光层和颜色薄膜之间液晶层,调整红蓝绿的比例。

所以在嵌入式开发中,很多LCD屏幕会有一个单独的背光引脚(常见标识为LEDK、BLK等)用于控制亮度

而OLED屏幕则通常没有专门的背光引脚。

背光引脚

背光引脚是液晶屏(LCD)专门用于连接背光源(通常是一排或多排LED灯)的引脚,通常是背光LED灯的负极(以“LEDK”命名,K代表Cathode阴极)。

工作原理

  • 液晶层本身不发光,只能改变光的通过与阻挡。
  • 背光LED点亮后,光通过液晶层,显示内容才能被人眼看到;关闭背光,屏幕就看不到内容。

我们可通过该引脚控制背光LED的亮灭,为液晶面板提供照明,从而使液晶层的内容显示或关闭

同时我们还可以通过背光引脚控制背光源亮度,实现屏幕亮度、功耗的修改

我们在使用LCD屏幕时只有开启了背光源,屏幕才能显示内容,这主要就是通过控制我们的背光引脚LEDK来实现的

背光引脚的使用

硬件PWM调光

调光介绍

​ PWM是一种通过控制信号的高低电平持续时间比例(占空比)来调节输出功率的方法。用在背光引脚时,就是让背光LED在极高频率下“快速开关”,人眼看到的是亮度的变化。

我们可以对背光引脚输出PWM波,通过控制PWM波的占空比来控制背光LED的亮度,进而控制屏幕的亮度。

占空比越高,LCD亮度越高

占空比越低,LCD亮度越高

使用PWM注意事项

PWM频率选择:

  • 频率过低
    • 低于人眼视觉融合阈值(约100Hz~200Hz),会有频闪现象,导致屏幕肉眼可见闪烁,影响观感和舒适度,长时间观看可能造成眼睛疲劳甚至头痛。
  • 频率过高
    • 频率过高(如几十kHz以上),可能导致驱动器件(如三极管/MOS管)切换损耗增加,发热变大,能耗上升,甚至影响电磁兼容(EMC),造成干扰。
    • 某些LED驱动电路在高频下可能无法完全响应,导致调光曲线不线性或亮度不稳定。

根据比较权威的IEEE Std1789-2015国际标准和国内标准的报告,频闪在400Hz以上时人眼就难以察觉,但无法保证无危害性,而在PWM频率在1250Hz以上时,频闪危害基本处于低风险水平,最高的无显著影响标准则是3125Hz

PWM调光时,PWM波频率设置在1kHz~20kHz都可以。最好设置在3125Hz以上

背光引脚代码

引脚配置

主要是对我们的GPIO配置复用模式,然后配置我们的TIM输出对应的PWM波,直接使用CubeMX配置TIM即可

我们这里的主频为100MHz,所以PSC配置为320,ARR配置为100, 使我们的PWM波频率为3125Hz

image-20250728163239160

image-20250728163248478

驱动代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 设置亮度,设置CCR的值控制占空比
void LCD_Set_Light(uint8_t duty)
{
if(duty >=5 && duty <=100)
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,duty*320/100);
}
// 启动背光源
void LCD_Open_BackLight()
{
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);
}
//关闭背光源
void LCD_Close_BackLight()
{
HAL_TIM_PWM_Stop(&htim3,TIM_CHANNEL_3);
}

注意在设置占空比的时候不要太低duty<5%,低于5%很多LED模块根本不亮或者闪烁,影响实际效果

LCD像素格式

像素格式介绍

像素格式:指图像数据存储所用的格式,描述了像素在内存中的编码方式。像素格式定义了每个像素所使用的总位数以及用于存储像素色彩的红、绿、蓝和 alpha 分量的位数。常见的有12/16/18/24 bit/pixel

说明

  • bit数越高,颜色越丰富、显示越细腻,但每个像素需要的数据越多,刷新速度越慢,对MCU要求越高。

  • 绝大多数STM32/单片机项目都用16位色(RGB565),因为速度和显示效果兼顾。18/24位色适合高端应用,对MCU和总线速率要求很高。

RGB565(16bit):红色5bit表示,绿色6bit表示,蓝色5bit表示

image-20250728182259774

白色数据(0xFFFF):代表红色数据为11111,绿色数据为111111,蓝色数据为11111,合起来就是0x1111111111111111(0xFFFF)

其他颜色同理

RGB888(24bit):红色8bit表示,绿色8bit表示,蓝色8bit表示

image-20250728182509918

白色数据(0xFFFFFF):代表红色数据为11111111,绿色数据为11111111,蓝色数据为11111111,合起来就是0x111111111111111111111111(0xFFFFFF)

其他颜色同理

LCD使用

我们在LCD初始化时,常常会对LCD的像素格式进行设置,如ST7789就有一个命令COLMOD(0x3A):

对应参数D7~D0:

  • D7: 设置为0
  • D6~D4: RGB接口格式,我们使用的是SPI接口,全部设置为0即可
  • D3: 设置为0
  • D2~D0: MCU控制接口像素格式, 我们主要配置这里,可以设置为12bit、16bit、18bit、16M

通常我们在MCU中使用16bit的像素格式,对应写入命令参数为:0x05(0000 0101)

image-20250728181323380

初始化代码中对应的部分:

1
2
3
// 7. 设置像素格式(COLMOD),0x05表示16位色(RGB565)
LCD_Write_Cmd(0x3A);
LCD_Write_Data8(0x05); // 0 000 0 101 16bit像素格式

我们设置像素格式为16bit后,之后我们对于每个像素点就用16bit数据代表颜色,对每个点写入数据时我们就可以使用提前定义颜色对应的宏定义16bit数据

然后在设置好行列地址后,将16bit数据写入某个像素点即可拥有对应颜色

image-20250728184456328

1
2
LCD_Write_Data16(WHITE); //该点写入白色
LCD_Write_Data16(BLACK); //该点写入黑色

有关不同RGB颜色对应的16bit数据可以在网上去转换后自己宏定义即可,或者可以手动转换

LCD初始化代码

为什么要有这个LCD初始化代码?

在我们购买屏幕的时候,厂家一般会给你一段初始化代码,这个代码有什么作用呢?

我们通常会看到有很多形状的屏幕,他们使用的可能是相同的屏幕驱动芯片(ST7789), 通过初始化代码设置(告诉)驱动芯片,我们要驱动的屏幕尺寸是多少、显示范围是多少、显示频率、一些电压等等是多少。这样就可以使用同一种芯片驱动不同尺寸的屏幕了

初始化代码示例

对于一块屏幕的初始化代码,厂家一般都会提供,我们直接将其移植过来用即可,一般不用了解内容,但是了解一下初始化代码做了什么事情对于我理解LCD有好处

下面就介绍一下这个初始化代码一般干什么

这是我买的P169H002-CTP触摸屏(使用ST7789)厂家给的初始化代码,主要对以下几个方面进行配置:

  • Sleep Out(0x11):退出睡眠

  • MADCTL(0x36): 内存访问控制,主要是屏幕显示的方向、翻转、旋转和颜色顺序

  • COLMOD(0x3A): LCD像素格式配置,颜色 bit 大小直接影响每个像素需要的数据量和屏幕显示效果。MCU常设置为16bit/pixel(RGB565),因为它色彩丰富、速度合适。bit数越高,色彩越多,但传输数据也越大。

  • 电压/电源相关命令这些命令用来调节 LCD 屏幕的驱动电压、对比度、色彩和显示稳定性。合理设置能适配不同屏幕、优化显示效果、防止异常。一般用厂家推荐值,特殊需要时可微调

  • INVON (0x21): 开启像素反相显示,让液晶每个像素点的电压周期性反转(像素反相),开启反相后,颜色会更饱满,亮度更均匀

  • DISPON(0x29): 开启LCD显示

上面这些命令我们按照对应ST7789手册命令部分的介绍进行了解和修改即可,通常这些是在后续我们需要对屏幕进行优化的时候进行修改,厂家给的可以直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
void LCD_Init()
{
LCD_GPIO_Init();
LCD_CS_LOW(); //这是我们手动添加的,使用SPI通信需要拉低CS引脚

LCD_RST_LOW(); //外部引脚硬件复位芯片
delay_ms(100);
LCD_RST_HIGH();
delay_ms(100);

/*开始初始化序列*/
//************* Start Initial Sequence **********//

// 3. 退出睡眠模式,唤醒LCD
LCD_Write_Cmd(0x11); // Sleep Out(唤醒命令)
delay_ms(120); // 等待120ms,确保芯片完全唤醒(手册要求)

// 4. 设置 Porch 参数(显示驱动时的帧间隔、同步等,影响稳定性)
LCD_Write_Cmd(0xB2); // Porch Setting
LCD_Write_Data8(0x0C); // 前 Porch
LCD_Write_Data8(0x0C); // 后 Porch
LCD_Write_Data8(0x00); // 间隔时间
LCD_Write_Data8(0x33); // 帧间隔/时序
LCD_Write_Data8(0x33); // 帧间隔/时序

// 5. 开启撕裂信号(Tearing Effect Line),通常用于同步显示,0表示关闭
LCD_Write_Cmd(0x35);
LCD_Write_Data8(0x00);

// 6. 设置内存访问控制(MADCTL),决定屏幕的扫描方向和RGB排列
LCD_Write_Cmd(0x36);
if (USE_HORIZONTAL == 0)
LCD_Write_Data8(0x00); // 竖屏,正常方向
else if (USE_HORIZONTAL == 1)
LCD_Write_Data8(0xC0); // 横屏,水平翻转
else if (USE_HORIZONTAL == 2)
LCD_Write_Data8(0x70); // 竖屏,垂直翻转
else
LCD_Write_Data8(0xA0); // 横屏,其他方向

// 7. 设置像素格式(COLMOD),0x05表示16位色(RGB565)
LCD_Write_Cmd(0x3A);
LCD_Write_Data8(0x05);

// 8. 设置门极驱动控制(Gate Control),影响扫描驱动电压
LCD_Write_Cmd(0xB7);
LCD_Write_Data8(0x35);

// 9. 设置VCOM电压,影响对比度和显示质量
LCD_Write_Cmd(0xBB);
LCD_Write_Data8(0x2D);

// 10. 设置电源控制(Power Control 1),调节显示驱动电压
LCD_Write_Cmd(0xC0);
LCD_Write_Data8(0x2C);

// 11. 设置电源控制(Power Control 2),调节内部偏置
LCD_Write_Cmd(0xC2);
LCD_Write_Data8(0x01);

// 12. 设置电源控制(Power Control 3),进一步调节电压参数
LCD_Write_Cmd(0xC3);
LCD_Write_Data8(0x15);

// 13. 设置电源控制(Power Control 4),进一步调节电压参数
LCD_Write_Cmd(0xC4);
LCD_Write_Data8(0x20);

// 14. 设置驱动控制(VDV and VRH Command Enable),影响帧速率和电压
LCD_Write_Cmd(0xC6);
LCD_Write_Data8(0x0F);

// 15. 设置正向电压控制(Positive Voltage Gamma Control)
LCD_Write_Cmd(0xD0);
LCD_Write_Data8(0xA4);
LCD_Write_Data8(0xA1);

// 16. 设置负向电压控制(Negative Voltage Gamma Control)
LCD_Write_Cmd(0xD6);
LCD_Write_Data8(0xA1);

// 17. 设置Gamma校准参数(影响色彩表现和灰阶显示)
LCD_Write_Cmd(0xE0);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x05);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x0B);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x27);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x44);
LCD_Write_Data8(0x47);
LCD_Write_Data8(0x37);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x29);
LCD_Write_Data8(0x2F);

LCD_Write_Cmd(0xE1);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x07);
LCD_Write_Data8(0x0C);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x04);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x33);
LCD_Write_Data8(0x46);
LCD_Write_Data8(0x18);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x2B);
LCD_Write_Data8(0x2D);

// 18. 显示反相(Display Inversion On),让颜色更鲜亮,防止残影
LCD_Write_Cmd(0x21);

// 19. 开启显示(Display On)
LCD_Write_Cmd(0x29);
}

从零开始开发显示驱动

如何入手开发LCD

我们拿到一块LCD屏幕的时候,知道了他用的哪块芯片,但是整个屏幕点亮到显示图像的流程是怎样的呢,如何驱动屏幕呢,我自己总结一下LCD开发的全流程

第一步:阅读手册

1.阅读屏幕手册,了解使用的是什么驱动芯片,驱动芯片使用的什么模式

2.然后去阅读驱动芯片的手册,详细了解对应的模式的通信流程、如何发送命令等

P169H002-CTP 使用的就是四线SPI,ST7789驱动,所以我们看ST7789手册时,就关注四线SPI即可

第二步:底层通讯协议的开发

最开始肯定是对我们底层通讯协议(I2C、SPI等)接收、发送数据的开发,主要是写命令写数据(1byte/2byte)

第三步:

常用命令

设置行/列地址:0x2B/0x2A

LCD底层开发

前言

这里主要是先对通讯协议读写数据驱动IC ST7789写命令/数据LCD初始化LCD设置行列地址进行开发,主要是对驱动IC ST7789的通讯相关的,开发好后供后续LCD显示对应图像等提供接口

以我们的P169H002-CTP这款屏幕为例,使用的驱动IC是ST7789V,四线SPI驱动。

头文件

LCD显示我们一般会先从头文件写起,主要包括:引脚宏定义通讯函数宏定义函数声明

便于后续实现函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#ifndef __LCD_ST7789_H
#define __LCD_ST7789_H

#include "sys.h"
#include "spi.h"
#include "delay.h"
#include "tim.h"

#define USE_HORIZONTAL 0

/* 引脚宏定义 */
#define LCD_SCLK_PORT GPIOB
#define LCD_SCLK_PIN GPIO_PIN_3

#define LCD_MOSI_PORT GPIOB
#define LCD_MOSI_PIN GPIO_PIN_5

#define LCD_CS_PORT GPIOB
#define LCD_CS_PIN GPIO_PIN_8

#define LCD_RST_PORT GPIOB
#define LCD_RST_PIN GPIO_PIN_7

#define LCD_DC_PORT GPIOB
#define LCD_DC_PIN GPIO_PIN_9

#define LCD_BLK_PORT GPIOB
#define LCD_BLK_PIN GPIO_PIN_0

/* 宏函数 */
#define LCD_SCLK_LOW() HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_RESET)
#define LCD_SCLK_HIGH() HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_SET)

#define LCD_MOSI_LOW() HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_RESET)
#define LCD_MOSI_HIGH() HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_SET)

#define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET)
#define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET)

#define LCD_DC_LOW() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET)
#define LCD_DC_HIGH() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET)

#define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET)
#define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET)

#define LCD_BLK_LOW() HAL_GPIO_WritePin(LCD_BLK_PORT, LCD_BLK_PIN, GPIO_PIN_RESET)
#define LCD_BLK_HIGH() HAL_GPIO_WritePin(LCD_BLK_PORT, LCD_BLK_PIN, GPIO_PIN_SET)

/* LCD初始化 */
void LCD_Init(void);

/* LCD写数据/命令*/
void LCD_Write_Cmd(u8 data);
void LCD_Write_Data8(u8 data);
void LCD_Write_Data16(u16 data);

/* LCD行列地址设置 */
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);

/* LCD低功耗相关 */
void LCD_ST7789_SleepOut(void);
void LCD_ST7789_SleepIn(void);

/* 补充函数 */
void LCD_SPI_SetBit(u8 bit);

#endif

接下来我们就分别来说明头文件内容:

引脚宏定义:为了便于移植,我们通常就会先将对应的LCD相关引脚进行宏定义,后续不用动上层的代码,只需要改底层的引脚,感觉手册和原理图定义即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* SPI SCLK引脚*/
#define LCD_SCLK_PORT GPIOB
#define LCD_SCLK_PIN GPIO_PIN_3

/* SPI MOSI引脚*/
#define LCD_MOSI_PORT GPIOB
#define LCD_MOSI_PIN GPIO_PIN_5

/* SPI RST引脚*/
#define LCD_RST_PORT GPIOB
#define LCD_RST_PIN GPIO_PIN_7

/* ST7789数据/命令选择引脚*/
#define LCD_DC_PORT GPIOB
#define LCD_DC_PIN GPIO_PIN_9

/* 片选引脚 */
#define LCD_CS_PORT GPIOB
#define LCD_CS_PIN GPIO_PIN_8

/* LCD背光控制引脚 */
#define LCD_BLK_PORT GPIOB
#define LCD_BLK_PIN GPIO_PIN_0

通讯函数宏定义:然后是对通讯过程中这些引脚需要拉低拉高的一个宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define LCD_SCLK_LOW()      HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_RESET)
#define LCD_SCLK_HIGH() HAL_GPIO_WritePin(LCD_SCLK_PORT, LCD_SCLK_PIN, GPIO_PIN_SET)

#define LCD_MOSI_LOW() HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_RESET)
#define LCD_MOSI_HIGH() HAL_GPIO_WritePin(LCD_MOSI_PORT, LCD_MOSI_PIN, GPIO_PIN_SET)

#define LCD_RST_LOW() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET)
#define LCD_RST_HIGH() HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET)

#define LCD_DC_LOW() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET)
#define LCD_DC_HIGH() HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET)

#define LCD_CS_LOW() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET)
#define LCD_CS_HIGH() HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET)

#define LCD_BLK_LOW() HAL_GPIO_WritePin(LCD_BLK_PORT, LCD_BLK_PIN, GPIO_PIN_RESET)
#define LCD_BLK_HIGH() HAL_GPIO_WritePin(LCD_BLK_PORT, LCD_BLK_PIN, GPIO_PIN_SET)

函数声明: 主要对我们底层LCD通讯的一些函数进行声明,写数据、写命令、设置行列地址等,这里一般是边写边添加函数声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* LCD初始化 */
void LCD_Init(void);

/* LCD写数据/命令*/
void LCD_Write_Cmd(u8 data);
void LCD_Write_Data8(u8 data);
void LCD_Write_Data16(u16 data);

/* LCD行列地址设置 */
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);

/* LCD低功耗相关 */
void LCD_ST7789_SleepOut(void);
void LCD_ST7789_SleepIn(void);

/* 补充函数 */
void LCD_SPI_SetBit(u8 bit);void LCD_Init(void);

引脚初始化

这里使用硬件SPI,对应的SPI在其他地方配置了,所以只需要初始化软件的CSDCRST引脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 我们这里使用硬件SPI,其他的CS/DC/RST引脚使用软件模拟*/
static void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};

__HAL_RCC_GPIOB_CLK_ENABLE();

GPIO_InitStructure.Pin = LCD_CS_PIN | LCD_DC_PIN | LCD_RST_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH; // 50MHz即可
HAL_GPIO_Init(GPIOB,&GPIO_InitStructure);

HAL_GPIO_WritePin(GPIOB, LCD_CS_PIN | LCD_DC_PIN | LCD_RST_PIN, GPIO_PIN_SET); //默认拉高
}

通讯协议写数据

然后就应该实现我们通讯协议的发送字节,这里我们使用硬件SPI,进行封装HAL层的SPI发送函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 硬件SPI向ST7789发送一个字节*/
static void LCD_Write_Byte(u8 data)
{
//hardware spi
HAL_SPI_Transmit(&hspi1,&data,1,HAL_MAX_DELAY);
}

/* 软件spi*/
static void LCD_Write_Byte_Soft(u8 data)
{
//software spi
for(uint8_t i=0;i<8;i++)
{
LCD_SCLK_LOW();
if( data & (0x80 >> i))
LCD_MOSI_HIGH();
else
LCD_MOSI_LOW();

LCD_SCLK_HIGH();
}
}

芯片写命令/数据

然后就是封装我们对应向ST7789芯片发送命令/数据的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* LCD向ST7789写命令*/
void LCD_Write_Cmd(u8 data)
{
LCD_DC_LOW(); // 切换为写命令
LCD_Write_Byte(data);
LCD_DC_HIGH();
}

/*LCD向ST7789写8bit数据*/
void LCD_Write_Data8(u8 data)
{
LCD_DC_HIGH(); // 切换为写数据
LCD_Write_Byte(data);
}

/*LCD向ST7789写16bit数据*/
void LCD_Write_Data16(u16 data)
{
LCD_DC_HIGH();
LCD_Write_Byte(data >> 8);
LCD_Write_Byte(data & 0xFF);
}

LCD初始化

这里的LCD初始化实际上是我们的ST7789芯片的初始化,对应写入一些参数配置。

这部分代码通常由厂家提供,我们直接移植即可,后续需要优化自己查手册对应参数修改即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
void LCD_Init()
{
LCD_GPIO_Init();
LCD_CS_LOW(); //chip select

LCD_RST_LOW(); //复位芯片
delay_ms(100);
LCD_RST_HIGH();
delay_ms(100);

/*开始初始化序列*/
//************* Start Initial Sequence **********//

// 3. 退出睡眠模式,唤醒LCD
LCD_Write_Cmd(0x11); // Sleep Out(唤醒命令)
delay_ms(120); // 等待120ms,确保芯片完全唤醒(手册要求)

// 4. 设置 Porch 参数(显示驱动时的帧间隔、同步等,影响稳定性)
LCD_Write_Cmd(0xB2); // Porch Setting
LCD_Write_Data8(0x0C); // 前 Porch
LCD_Write_Data8(0x0C); // 后 Porch
LCD_Write_Data8(0x00); // 间隔时间
LCD_Write_Data8(0x33); // 帧间隔/时序
LCD_Write_Data8(0x33); // 帧间隔/时序

// 5. 开启撕裂信号(Tearing Effect Line),通常用于同步显示,0表示关闭
LCD_Write_Cmd(0x35);
LCD_Write_Data8(0x00);

// 6. 设置内存访问控制(MADCTL),决定屏幕的扫描方向和RGB排列
LCD_Write_Cmd(0x36);
if (USE_HORIZONTAL == 0)
LCD_Write_Data8(0x00); // 竖屏,正常方向
else if (USE_HORIZONTAL == 1)
LCD_Write_Data8(0xC0); // 横屏,水平翻转
else if (USE_HORIZONTAL == 2)
LCD_Write_Data8(0x70); // 竖屏,垂直翻转
else
LCD_Write_Data8(0xA0); // 横屏,其他方向

// 7. 设置像素格式(COLMOD),0x05表示16位色(RGB565)
LCD_Write_Cmd(0x3A);
LCD_Write_Data8(0x05);

// 8. 设置门极驱动控制(Gate Control),影响扫描驱动电压
LCD_Write_Cmd(0xB7);
LCD_Write_Data8(0x35);

// 9. 设置VCOM电压,影响对比度和显示质量
LCD_Write_Cmd(0xBB);
LCD_Write_Data8(0x2D);

// 10. 设置电源控制(Power Control 1),调节显示驱动电压
LCD_Write_Cmd(0xC0);
LCD_Write_Data8(0x2C);

// 11. 设置电源控制(Power Control 2),调节内部偏置
LCD_Write_Cmd(0xC2);
LCD_Write_Data8(0x01);

// 12. 设置电源控制(Power Control 3),进一步调节电压参数
LCD_Write_Cmd(0xC3);
LCD_Write_Data8(0x15);

// 13. 设置电源控制(Power Control 4),进一步调节电压参数
LCD_Write_Cmd(0xC4);
LCD_Write_Data8(0x20);

// 14. 设置驱动控制(VDV and VRH Command Enable),影响帧速率和电压
LCD_Write_Cmd(0xC6);
LCD_Write_Data8(0x0F);

// 15. 设置正向电压控制(Positive Voltage Gamma Control)
LCD_Write_Cmd(0xD0);
LCD_Write_Data8(0xA4);
LCD_Write_Data8(0xA1);

// 16. 设置负向电压控制(Negative Voltage Gamma Control)
LCD_Write_Cmd(0xD6);
LCD_Write_Data8(0xA1);

// 17. 设置Gamma校准参数(影响色彩表现和灰阶显示)
LCD_Write_Cmd(0xE0);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x05);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x0B);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x27);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x44);
LCD_Write_Data8(0x47);
LCD_Write_Data8(0x37);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x29);
LCD_Write_Data8(0x2F);

LCD_Write_Cmd(0xE1);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x07);
LCD_Write_Data8(0x0C);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x04);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x33);
LCD_Write_Data8(0x46);
LCD_Write_Data8(0x18);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x2B);
LCD_Write_Data8(0x2D);

// 18. 显示反相(Display Inversion On),让颜色更鲜亮,防止残影
LCD_Write_Cmd(0x21);

// 19. 开启显示(Display On)
LCD_Write_Cmd(0x29);
}

LCD设置显示窗口(行列地址)

显示流程

想让LCD上的每个像素显示出来,我们首先得告诉LCD我们想要显示的区域是哪里:xy的起始和终止坐标

所以我们在写入像素点数据之前,需要先设置显示的行列地址

流程图:

image-20250726171606593

列地址

列地址设置命令CASET(0x2A),用于告诉LCD接下来要在哪些列范围写入数据

XS[15:0]: 16bit的起始水平坐标x

XE[15:0]: 16bit的结束水平坐标x

取值范围:0 < XS[15:0] < XE[15:0] < 239(00EF) MV = “0”

取值范围:0 < XS[15:0] < XE[15:0] < 319(013F) MV = “1”

MV的值代表横竖屏两种的情况

image-20250726135425056

四线SPI中不用管WRXRDX(具体原因请见上方RDX、WRX、DCX)引脚的说明,只需要对D/CX引脚进行控制,然后发送数据即可

整个流程就是:

  1. 拉低D/CX引脚,然后发送命令0x2A

  2. 发送起始水平坐标x的高8bit

  3. 发送起始水平坐标x的低8bit

  4. 发送结束水平坐标x的高8bit

  5. 发送结束水平坐标x的低8bit

行地址

列地址设置命令RASET(0x2B),用于告诉LCD接下来要在哪些行范围写入数据

YS[15:0]: 16bit的起始垂直坐标y

YE[15:0]: 16bit的结束垂直坐标y

取值范围:0 < YS[15:0] < YE[15:0] < 239(00EF) MV = “0”

取值范围:0 < YS[15:0] < YE[15:0] < 319(013F) MV = “1”

MV的值代表横竖屏两种的情况

image-20250727140752463

四线SPI中不用管WRXRDX(具体原因请见上方RDX、WRX、DCX)引脚的说明,只需要对D/CX引脚进行控制,然后发送数据即可

整个流程就是:

  1. 拉低D/CX引脚,然后发送命令0x2A

  2. 发送起始垂直坐标y的高8bit

  3. 发送起始垂直坐标y的低8bit

  4. 发送结束垂直坐标y的高8bit

  5. 发送结束垂直坐标y的低8bit

写显存

RAMWR(0x2C)是写显存的命令,发送该命令后,芯片会等待接收显存数据了。

同时当这个命令被接受时,列寄存器和页寄存器被重置到设置的起始列/起始页位置。

image-20250727144443040

对应代码

  • 设置行列地址+写显存命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @brief 设置LCD的显示窗口(起始和结束地址)
* @param x1 显示区域起始列地址
* @param y1 显示区域起始行地址
* @param x2 显示区域结束列地址
* @param y2 显示区域结束行地址
*
* @note 本函数通过向LCD控制芯片发送命令和参数,设置后续像素数据写入的目标区域。
* 只有设置好显示窗口后,才能高效地在指定区域进行图像、文字等内容的刷新操作。
* 常用于绘制图形、局部刷新等场景,提升LCD操作效率。
*/
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2)
{

LCD_Write_Cmd(0x2A); // CASET命令
LCD_Write_Data8(x1 >> 8); // XS高字节
LCD_Write_Data8(x1 & 0xFF); // XS低字节
LCD_Write_Data8(x2 >> 8); // XE高字节
LCD_Write_Data8(x2 & 0xFF); // XE低字节

LCD_Write_Cmd(0x2B); // RASET命令
LCD_Write_Data8(y1 >> 8); // YS高字节
LCD_Write_Data8(y1 & 0xFF); // YS低字节
LCD_Write_Data8(y2 >> 8); // YE高字节
LCD_Write_Data8(y2 & 0xFF); // YE低字节

LCD_Write_Cmd(0x2C); // 发送该命令,LCD开始等待接收显存数据
}

低功耗相关

直接按照手册发送命令即可,分别对应进入睡眠退出睡眠模式

image-20250808190608876

image-20250808190628808

1
2
3
4
5
6
7
8
9
10
11
12
13
/* LCD退出睡眠  */
void LCD_ST7789_SleepOut(void)
{
LCD_Write_Cmd(0x10);
delay_ms(100); //手册要求至少延时5ms
}

/* LCD进入睡眠 */
void LCD_ST7789_SleepIn(void)
{
LCD_Write_Cmd(0x11);
delay_ms(100); //手册要求至少延时5ms
}

其他补充

还可以放一些其他于LCD相关的,比如我的项目中使用了SPI切换数据宽度,所以就放在这里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void LCD_SPI_SetBit(u8 bit)
{

if(bit == 8)
{
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Instance->CR1&=~SPI_CR1_DFF;
}
else if(bit == 16)
{
hspi1.Init.DataSize = SPI_DATASIZE_16BIT;
hspi1.Instance->CR1|=SPI_CR1_DFF;
}
}

源文件

总结以上实现得到的源文件lcd_st7789.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
#include "sys.h"
#include "lcd_st7789.h"


/* 我们这里使用硬件SPI,其他的CS/DC/RST引脚使用软件模拟*/
static void LCD_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};

__HAL_RCC_GPIOB_CLK_ENABLE();

GPIO_InitStructure.Pin = LCD_CS_PIN | LCD_DC_PIN | LCD_RST_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH; // 50MHz即可
HAL_GPIO_Init(GPIOB,&GPIO_InitStructure);

HAL_GPIO_WritePin(GPIOB, LCD_CS_PIN | LCD_DC_PIN | LCD_RST_PIN, GPIO_PIN_SET); //默认拉高
}


void LCD_SPI_SetBit(u8 bit)
{

if(bit == 8)
{
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Instance->CR1&=~SPI_CR1_DFF;
}
else if(bit == 16)
{
hspi1.Init.DataSize = SPI_DATASIZE_16BIT;
hspi1.Instance->CR1|=SPI_CR1_DFF;
}
}

/* 硬件SPI向ST7789发送一个字节*/
static void LCD_Write_Byte(u8 data)
{
//hardware spi
HAL_SPI_Transmit(&hspi1,&data,1,HAL_MAX_DELAY);
}

/* 软件spi*/
static void LCD_Write_Byte_Soft(u8 data)
{
//software spi
for(uint8_t i=0;i<8;i++)
{
LCD_SCLK_LOW();
if( data & (0x80 >> i))
LCD_MOSI_HIGH();
else
LCD_MOSI_LOW();

LCD_SCLK_HIGH();
}
}

/* LCD向ST7789写命令*/
void LCD_Write_Cmd(u8 data)
{
LCD_DC_LOW(); // 切换为写命令
LCD_Write_Byte(data);
LCD_DC_HIGH();
}

/*LCD向ST7789写8bit数据*/
void LCD_Write_Data8(u8 data)
{
LCD_DC_HIGH(); // 切换为写数据
LCD_Write_Byte(data);
}

/*LCD向ST7789写16bit数据*/
void LCD_Write_Data16(u16 data)
{
LCD_DC_HIGH();
LCD_Write_Byte(data >> 8);
LCD_Write_Byte(data & 0xFF);
}


/**
* @brief 设置LCD的显示窗口(起始和结束地址)
* @param x1 显示区域起始列地址
* @param y1 显示区域起始行地址
* @param x2 显示区域结束列地址
* @param y2 显示区域结束行地址
*
* @note 本函数通过向LCD控制芯片发送命令和参数,设置后续像素数据写入的目标区域。
* 只有设置好显示窗口后,才能高效地在指定区域进行图像、文字等内容的刷新操作。
* 常用于绘制图形、局部刷新等场景,提升LCD操作效率。
*/
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2)
{

LCD_Write_Cmd(0x2A); // CASET命令
LCD_Write_Data8(x1 >> 8); // XS高字节
LCD_Write_Data8(x1 & 0xFF); // XS低字节
LCD_Write_Data8(x2 >> 8); // XE高字节
LCD_Write_Data8(x2 & 0xFF); // XE低字节

LCD_Write_Cmd(0x2B); // RASET命令
LCD_Write_Data8(y1 >> 8); // YS高字节
LCD_Write_Data8(y1 & 0xFF); // YS低字节
LCD_Write_Data8(y2 >> 8); // YE高字节
LCD_Write_Data8(y2 & 0xFF); // YE低字节

LCD_Write_Cmd(0x2C); // 发送该命令,LCD开始等待接收显存数据
}


/* LCD退出睡眠 */
void LCD_ST7789_SleepOut(void)
{
LCD_Write_Cmd(0x10);
delay_ms(100); //手册要求至少延时5ms
}

/* LCD进入睡眠 */
void LCD_ST7789_SleepIn(void)
{
LCD_Write_Cmd(0x11);
delay_ms(100); //手册要求至少延时5ms
}

void LCD_Init()
{
LCD_GPIO_Init();
LCD_CS_LOW(); //chip select

LCD_RST_LOW(); //复位芯片
delay_ms(100);
LCD_RST_HIGH();
delay_ms(100);

/*开始初始化序列*/
//************* Start Initial Sequence **********//

// 3. 退出睡眠模式,唤醒LCD
LCD_Write_Cmd(0x11); // Sleep Out(唤醒命令)
delay_ms(120); // 等待120ms,确保芯片完全唤醒(手册要求)

// 4. 设置 Porch 参数(显示驱动时的帧间隔、同步等,影响稳定性)
LCD_Write_Cmd(0xB2); // Porch Setting
LCD_Write_Data8(0x0C); // 前 Porch
LCD_Write_Data8(0x0C); // 后 Porch
LCD_Write_Data8(0x00); // 间隔时间
LCD_Write_Data8(0x33); // 帧间隔/时序
LCD_Write_Data8(0x33); // 帧间隔/时序

// 5. 开启撕裂信号(Tearing Effect Line),通常用于同步显示,0表示关闭
LCD_Write_Cmd(0x35);
LCD_Write_Data8(0x00);

// 6. 设置内存访问控制(MADCTL),决定屏幕的扫描方向和RGB排列
LCD_Write_Cmd(0x36);
if (USE_HORIZONTAL == 0)
LCD_Write_Data8(0x00); // 竖屏,正常方向
else if (USE_HORIZONTAL == 1)
LCD_Write_Data8(0xC0); // 横屏,水平翻转
else if (USE_HORIZONTAL == 2)
LCD_Write_Data8(0x70); // 竖屏,垂直翻转
else
LCD_Write_Data8(0xA0); // 横屏,其他方向

// 7. 设置像素格式(COLMOD),0x05表示16位色(RGB565)
LCD_Write_Cmd(0x3A);
LCD_Write_Data8(0x05);

// 8. 设置门极驱动控制(Gate Control),影响扫描驱动电压
LCD_Write_Cmd(0xB7);
LCD_Write_Data8(0x35);

// 9. 设置VCOM电压,影响对比度和显示质量
LCD_Write_Cmd(0xBB);
LCD_Write_Data8(0x2D);

// 10. 设置电源控制(Power Control 1),调节显示驱动电压
LCD_Write_Cmd(0xC0);
LCD_Write_Data8(0x2C);

// 11. 设置电源控制(Power Control 2),调节内部偏置
LCD_Write_Cmd(0xC2);
LCD_Write_Data8(0x01);

// 12. 设置电源控制(Power Control 3),进一步调节电压参数
LCD_Write_Cmd(0xC3);
LCD_Write_Data8(0x15);

// 13. 设置电源控制(Power Control 4),进一步调节电压参数
LCD_Write_Cmd(0xC4);
LCD_Write_Data8(0x20);

// 14. 设置驱动控制(VDV and VRH Command Enable),影响帧速率和电压
LCD_Write_Cmd(0xC6);
LCD_Write_Data8(0x0F);

// 15. 设置正向电压控制(Positive Voltage Gamma Control)
LCD_Write_Cmd(0xD0);
LCD_Write_Data8(0xA4);
LCD_Write_Data8(0xA1);

// 16. 设置负向电压控制(Negative Voltage Gamma Control)
LCD_Write_Cmd(0xD6);
LCD_Write_Data8(0xA1);

// 17. 设置Gamma校准参数(影响色彩表现和灰阶显示)
LCD_Write_Cmd(0xE0);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x05);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x0B);
LCD_Write_Data8(0x0A);
LCD_Write_Data8(0x27);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x44);
LCD_Write_Data8(0x47);
LCD_Write_Data8(0x37);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x14);
LCD_Write_Data8(0x29);
LCD_Write_Data8(0x2F);

LCD_Write_Cmd(0xE1);
LCD_Write_Data8(0x70);
LCD_Write_Data8(0x07);
LCD_Write_Data8(0x0C);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x08);
LCD_Write_Data8(0x04);
LCD_Write_Data8(0x2F);
LCD_Write_Data8(0x33);
LCD_Write_Data8(0x46);
LCD_Write_Data8(0x18);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x15);
LCD_Write_Data8(0x2B);
LCD_Write_Data8(0x2D);

// 18. 显示反相(Display Inversion On),让颜色更鲜亮,防止残影
LCD_Write_Cmd(0x21);

// 19. 开启显示(Display On)
LCD_Write_Cmd(0x29);
}

LCD显示开发

前言

我们底层已经写好了,这里就可以进行编写一些LCD显示相关的函数了

头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#ifndef __LCD_H_
#define __LCD_H_

/*底层驱动文件*/
#include "lcd_st7789.h"

/*LCD绘制相关*/
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color);
void LCD_Fill_DMA(u16 x_start,u16 y_start,u16 x_end,u16 y_end,u16* color);

// ..... 包括画图形(点、线、图形)、显示汉字/字符/整数、图片等定义,参考厂家代码设计


/* 背光相关设置 */
void LCD_Set_Light(uint8_t duty);
void LCD_Open_BackLight();
void LCD_Close_BackLight();


/* 每个16bit像素对应的颜色数据*/
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0XF81F
#define GRED 0XFFE0
#define GBLUE 0X07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0XBC40 //棕色
#define BRRED 0XFC07 //棕红色
#define GRAY 0X8430 //灰色
#define DARKBLUE 0X01CF //深蓝色
#define LIGHTBLUE 0X7D7C //浅蓝色
#define GRAYBLUE 0X5458 //灰蓝色
#define LIGHTGREEN 0X841F //浅绿色
#define LGRAY 0XC618 //浅灰色(PANNEL),窗体背景色
#define LGRAYBLUE 0XA651 //浅灰蓝色(中间层颜色)
#define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色)

#endif

下面分别介绍头文件内容:

函数声明: 主要对我们进行实际LCD显示的一些API,供应用层使用,包括区域填充,画图形,显示汉字/字符/数字等。

还有涉及到背光的需要我们开启背光LCD才能显示

1
2
3
4
5
6
7
8
9
10
11
12
/*LCD绘制相关*/
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color);
void LCD_Fill_DMA(u16 x_start,u16 y_start,u16 x_end,u16 y_end,u16* color);

// ..... 包括画图形(点、线、图形)、显示汉字/字符/整数、图片等定义,参考厂家代码设计
void LCD_DrawPoint(u16 x,u16 y,u16 color);//在指定位置画一个点
//....

/* 背光相关设置 */
void LCD_Set_Light(uint8_t duty);
void LCD_Open_BackLight();
void LCD_Close_BackLight();

宏定义:我们底层使用的是RGB565的格式,每个像素16bit显示不同颜色,我们需要提前定义好对应颜色的数据,以便于应用层可以直接指定想要填充的颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 每个16bit像素对应的颜色数据*/
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0XF81F
#define GRED 0XFFE0
#define GBLUE 0X07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0XBC40 //棕色
#define BRRED 0XFC07 //棕红色
#define GRAY 0X8430 //灰色
#define DARKBLUE 0X01CF //深蓝色
#define LIGHTBLUE 0X7D7C //浅蓝色
#define GRAYBLUE 0X5458 //灰蓝色
#define LIGHTGREEN 0X841F //浅绿色
#define LGRAY 0XC618 //浅灰色(PANNEL),窗体背景色
#define LGRAYBLUE 0XA651 //浅灰蓝色(中间层颜色)
#define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色)

背光

这里我们使用PWM对背光引脚进行输出,可以控制屏幕的亮度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*设置LCD背光源亮度*/
void LCD_Set_Light(uint8_t duty)
{
if(duty >=5 && duty <=100)
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,duty*320/100);
}

/*开启LCD背光*/
void LCD_Open_BackLight()
{
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);
}

/*关闭LCD背光*/
void LCD_Close_BackLight()
{
HAL_TIM_PWM_Stop(&htim3,TIM_CHANNEL_3);
}

LCD显示开发

填充区域

写好LCD底层后,我们想要对应区域显示我们指定的颜色,需要以下步骤:

  1. 设置显示行列地址
  2. 写入像素数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color)
{
u16 i,j;
/* 1. 设置显示范围*/
LCD_Address_Set(xsta,ysta,xend-1,yend-1);

/* 2. 写入对应的像素数据RGB565 */
for(i=ysta;i<yend;i++)
{
for(j=xsta;j<xend;j++)
{
LCD_Write_Data16(color);
}
}
}

上面就是最基本的显示方法了,但是我们通常都是使用DMA的方式批量写入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void LCD_Fill_DMA(u16 x_start,u16 y_start,u16 x_end,u16 y_end,u16* color)
{

u16 width,height;
u32 size;
width = x_end-x_start+1;
height = y_end-y_start+1;
size = width * height; // 区域总像素个数

LCD_Address_Set(x_start,y_start+OFFSET_Y,x_end,y_end+OFFSET_Y);
LCD_SPI_SetBit(16);

HAL_SPI_Transmit_DMA(&hspi1,(uint8_t*)color,size); // 每个像素16bit
}

画图形

显示字符/数字等

显示图像

源文件

最终得到的lcd.c文件如下:

唯一注意的就是,我们需要根据实际情况,实际测试时应该需要校准:添加一个偏移量到我们的xy坐标,时显示区域覆盖完我们的LCD屏幕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "lcd.h"
#include "lv_port_disp.h"


#define OFFSET_Y 20 //显示区域为x:0~239 y:20~299
#define OFFSET_X 0

extern lv_display_t * disp; // lvgl显示对象


void LCD_Fill(u16 x_start,u16 y_start,u16 x_end,u16 y_end,u16 color)
{
u16 i,j;
LCD_Address_Set(x_start,y_start+OFFSET_Y,x_end-1,y_end+OFFSET_Y-1);//设置显示范围
for(i=y_start;i<y_end;i++)
{
for(j=x_start;j<x_end;j++)
{
LCD_Write_Data16(color);
}
}
}

void LCD_Fill_DMA(u16 x_start,u16 y_start,u16 x_end,u16 y_end,u16* color)
{

u16 width,height;
u32 size;
width = x_end-x_start+1;
height = y_end-y_start+1;
size = width * height; // 区域总像素个数

LCD_Address_Set(x_start,y_start+OFFSET_Y,x_end,y_end+OFFSET_Y);
LCD_SPI_SetBit(16);

HAL_SPI_Transmit_DMA(&hspi1,(uint8_t*)color,size); // 每个像素16bit
}

/*设置LCD背光源亮度*/
void LCD_Set_Light(uint8_t duty)
{
if(duty >=5 && duty <=100)
__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,duty*320/100);
}

/*开启LCD背光*/
void LCD_Open_BackLight()
{
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);
}

/*关闭LCD背光*/
void LCD_Close_BackLight()
{
HAL_TIM_PWM_Stop(&htim3,TIM_CHANNEL_3);
}


void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == &hspi1)
{
LCD_SPI_SetBit(8);
lv_display_flush_ready(disp);
}
}

/* 其余部分是厂家给的画点、画线等等LCD的驱动,我这里不使用就不移植了*/

驱动测试代码编写

编写完驱动之后,我们通常会写一些参考示例测试我们的驱动,同时后续以供别人使用

填充测试

这个是最基本的测试,可以测试我们驱动是否有问题

驱动编写完成之后我们就可以开始测试了:

1.初始化LCD,开启LCD背光

2.LCD填充

image-20250808182830530

最终看屏幕有没有正常显示颜色,正常显示代表成功。

否则可能是通讯协议数据格式偏移量的问题,建议依次排查

其他测试

然后可以进行一些其他的点、线、图片测试等

LCD开发时可能的问题

花屏

问题1:调用填充LCD函数LCD_Fill(0,20,240, 280, 0xFFFF)时,部分行花屏

1
2
3
4
5
6
7
8
9
10
11
12
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color)
{
u16 i,j;
LCD_Address_Set(xsta,ysta,xend-1,yend-1);//设置显示范围
for(i=ysta;i<yend;i++)
{
for(j=xsta;j<xend;j++)
{
LCD_WR_DATA(color);
}
}
}

原因:使用ST7789V可操纵240×320的分辨率,而买到的P169H002-CTP屏幕是240×280,显示坐标不一定是从0开始x:0-240y:0-280,我们需要自己添加偏移量找到显示位置

最终尝试后发现需要在y轴添加20的偏移量后屏幕显示正常,所以该屏幕的实际显示坐标为 x: 0-240y: 20-300,对应的分辨率也满足240×280

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define OFFSET_Y 20

void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color)
{
u16 i,j;
LCD_Address_Set(xsta,ysta+OFFSET_Y,xend-1,yend-1+OFFSET_Y);//设置显示范围
for(i=ysta;i<yend;i++)
{
for(j=xsta;j<xend;j++)
{
LCD_WR_DATA(color);
}
}
}