嵌入式软件LCDLCD显示屏开发
THEDILCD显示屏介绍
什么是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
特性
应用场景
- MPOS 设备(移动支付终端)
- 个人导航设备
- 其他需要高质量显示的设备

全介绍:
项目 |
规格 |
单位/说明 |
备注说明 |
屏幕尺寸 |
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
: 触摸中断信号
屏幕英文手册:

中文翻译:

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.命令集丰富
提供寄存器配置命令,可自定义显示区域、色彩格式、背光控制等。
手册阅读
尽可能地只阅读我们需要的部分,减少耗时,下方的介绍顺序也是以后开发时推荐阅读手册的顺序
特征介绍
首先直接看一下特征介绍: 大致看一下分辨率
、通信接口
等等

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

通信接口
我们之前在手册第二章特征描述中看到了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接口 |
专用同步信号接口,用于显示刷新同步 |

然后就是找对我们关心的SPI串行接口通信相关部分: 我们可以看到第8章就对各个接口(8080、SPI)等功能进行详细描述,主要就是告诉我们该如何发送指令,数据等。
我们屏幕使用的是四线SPI,所以只关注四线SPI即可,主要看四线的引脚定义,如何写命令/数据等

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

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

所以我们就该去了解Serial Interface相关内容,主要是各个功能进行了解
ST7789的串行接口主要有四种引脚定义:三线串行接口Ⅰ/Ⅱ
、四线串行接口Ⅰ/Ⅱ
原文:

这部分说的是串行接口有3线/9位或4线/8位双向接口:
3线串行接口(3-line serial interfaceⅠ/Ⅱ)使用:CSX(芯片使能),SCL(串行时钟)和SDA(串行数据输入/输出)。
4线串行接口(4-line serial interfaceⅠ/Ⅱ)使用:CSX(芯片使能),SCL(串行时钟)和SDA(串行数据输入/输出),D/CX
(数据/命令标志)
串行时钟(SCL)仅用于与单片机的接口,因此它可以在不需要通信时停止。


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

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

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

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

该接口的写模式意味着微控制器会将指令和数据写入液晶驱动器.
3线串行数据包包含一个控制位D/CX和一个传输字节: 直接发送数据1bit(D/CX) + 8bit(Cmd 或 data)
然后ST7789启动IC直接对这个1bit+8bit进行解析,解析出来的1bit,决定8bit是Cmd还是data

在4线串行接口中,数据包仅包含传输字节,而控制位D/CX则通过D/CX引脚进行传输: 先设置D/CX引脚电平,然后发送8bit(Cmd 或 data)
然后ST7789驱动IC解析:如果D/CX引脚为低,则传输字节将被解释为命令字节。如果D/CX引脚为高,则该传输字节将被存储在显示数据RAM(内存写入命令)中,或者作为参数存储在命令寄存器中。

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

**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
:

3-line
:

读数据
这里只会写入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


驱动代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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
说明
:
RGB565(16bit)
:红色5bit表示,绿色6bit表示,蓝色5bit表示

白色数据(0xFFFF):代表红色数据为11111,绿色数据为111111,蓝色数据为11111,合起来就是0x1111111111111111(0xFFFF)
其他颜色同理
RGB888(24bit)
:红色8bit表示,绿色8bit表示,蓝色8bit表示

白色数据(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)

初始化代码中对应的部分:
1 2 3
| LCD_Write_Cmd(0x3A); LCD_Write_Data8(0x05);
|
我们设置像素格式为16bit后,之后我们对于每个像素点就用16bit数据代表颜色,对每个点写入数据时我们就可以使用提前定义颜色对应的宏定义16bit数据
然后在设置好行列地址后,将16bit数据写入某个像素点即可拥有对应颜色

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(); LCD_RST_LOW(); delay_ms(100); LCD_RST_HIGH(); delay_ms(100);
LCD_Write_Cmd(0x11); delay_ms(120);
LCD_Write_Cmd(0xB2); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x00); LCD_Write_Data8(0x33); LCD_Write_Data8(0x33);
LCD_Write_Cmd(0x35); LCD_Write_Data8(0x00);
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);
LCD_Write_Cmd(0x3A); LCD_Write_Data8(0x05);
LCD_Write_Cmd(0xB7); LCD_Write_Data8(0x35);
LCD_Write_Cmd(0xBB); LCD_Write_Data8(0x2D);
LCD_Write_Cmd(0xC0); LCD_Write_Data8(0x2C);
LCD_Write_Cmd(0xC2); LCD_Write_Data8(0x01);
LCD_Write_Cmd(0xC3); LCD_Write_Data8(0x15);
LCD_Write_Cmd(0xC4); LCD_Write_Data8(0x20);
LCD_Write_Cmd(0xC6); LCD_Write_Data8(0x0F);
LCD_Write_Cmd(0xD0); LCD_Write_Data8(0xA4); LCD_Write_Data8(0xA1);
LCD_Write_Cmd(0xD6); LCD_Write_Data8(0xA1);
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);
LCD_Write_Cmd(0x21);
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)
void LCD_Init(void);
void LCD_Write_Cmd(u8 data); void LCD_Write_Data8(u8 data); void LCD_Write_Data16(u16 data);
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);
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
| #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_RST_PORT GPIOB #define LCD_RST_PIN GPIO_PIN_7
#define LCD_DC_PORT GPIOB #define LCD_DC_PIN GPIO_PIN_9
#define LCD_CS_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_8
#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
| void LCD_Init(void);
void LCD_Write_Cmd(u8 data); void LCD_Write_Data8(u8 data); void LCD_Write_Data16(u16 data);
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2);
void LCD_ST7789_SleepOut(void); void LCD_ST7789_SleepIn(void);
void LCD_SPI_SetBit(u8 bit);void LCD_Init(void);
|
引脚初始化
这里使用硬件SPI,对应的SPI在其他地方配置了,所以只需要初始化软件的CS
、DC
、RST
引脚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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; 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
| static void LCD_Write_Byte(u8 data) { HAL_SPI_Transmit(&hspi1,&data,1,HAL_MAX_DELAY); }
static void LCD_Write_Byte_Soft(u8 data) { 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
| void LCD_Write_Cmd(u8 data) { LCD_DC_LOW(); LCD_Write_Byte(data); LCD_DC_HIGH(); }
void LCD_Write_Data8(u8 data) { LCD_DC_HIGH(); LCD_Write_Byte(data); }
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(); LCD_RST_LOW(); delay_ms(100); LCD_RST_HIGH(); delay_ms(100);
LCD_Write_Cmd(0x11); delay_ms(120);
LCD_Write_Cmd(0xB2); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x00); LCD_Write_Data8(0x33); LCD_Write_Data8(0x33);
LCD_Write_Cmd(0x35); LCD_Write_Data8(0x00);
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);
LCD_Write_Cmd(0x3A); LCD_Write_Data8(0x05);
LCD_Write_Cmd(0xB7); LCD_Write_Data8(0x35);
LCD_Write_Cmd(0xBB); LCD_Write_Data8(0x2D);
LCD_Write_Cmd(0xC0); LCD_Write_Data8(0x2C);
LCD_Write_Cmd(0xC2); LCD_Write_Data8(0x01);
LCD_Write_Cmd(0xC3); LCD_Write_Data8(0x15);
LCD_Write_Cmd(0xC4); LCD_Write_Data8(0x20);
LCD_Write_Cmd(0xC6); LCD_Write_Data8(0x0F);
LCD_Write_Cmd(0xD0); LCD_Write_Data8(0xA4); LCD_Write_Data8(0xA1);
LCD_Write_Cmd(0xD6); LCD_Write_Data8(0xA1);
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);
LCD_Write_Cmd(0x21);
LCD_Write_Cmd(0x29); }
|
LCD设置显示窗口(行列地址)
显示流程
想让LCD上的每个像素显示出来,我们首先得告诉LCD我们想要显示的区域是哪里:xy的起始和终止坐标
所以我们在写入像素点数据之前,需要先设置显示的行列地址
流程图
:

列地址
列地址设置命令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的值代表横竖屏两种的情况

四线SPI中不用管WRX
和RDX
(具体原因请见上方RDX、WRX、DCX)引脚的说明,只需要对D/CX
引脚进行控制,然后发送数据即可
整个流程就是:
拉低D/CX引脚,然后发送命令0x2A
发送起始水平坐标x的高8bit
发送起始水平坐标x的低8bit
发送结束水平坐标x的高8bit
发送结束水平坐标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的值代表横竖屏两种的情况

四线SPI中不用管WRX
和RDX
(具体原因请见上方RDX、WRX、DCX)引脚的说明,只需要对D/CX
引脚进行控制,然后发送数据即可
整个流程就是:
拉低D/CX引脚,然后发送命令0x2A
发送起始垂直坐标y的高8bit
发送起始垂直坐标y的低8bit
发送结束垂直坐标y的高8bit
发送结束垂直坐标y的低8bit
写显存
RAMWR(0x2C)
是写显存的命令,发送该命令后,芯片会等待接收显存数据了。
同时当这个命令被接受时,列寄存器和页寄存器被重置到设置的起始列/起始页位置。

对应代码
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
|
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2) { LCD_Write_Cmd(0x2A); LCD_Write_Data8(x1 >> 8); LCD_Write_Data8(x1 & 0xFF); LCD_Write_Data8(x2 >> 8); LCD_Write_Data8(x2 & 0xFF); LCD_Write_Cmd(0x2B); LCD_Write_Data8(y1 >> 8); LCD_Write_Data8(y1 & 0xFF); LCD_Write_Data8(y2 >> 8); LCD_Write_Data8(y2 & 0xFF); LCD_Write_Cmd(0x2C); }
|
低功耗相关
直接按照手册发送命令即可,分别对应进入睡眠
和退出睡眠
模式


1 2 3 4 5 6 7 8 9 10 11 12 13
| void LCD_ST7789_SleepOut(void) { LCD_Write_Cmd(0x10); delay_ms(100); }
void LCD_ST7789_SleepIn(void) { LCD_Write_Cmd(0x11); delay_ms(100); }
|
其他补充
还可以放一些其他于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"
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; 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; } }
static void LCD_Write_Byte(u8 data) { HAL_SPI_Transmit(&hspi1,&data,1,HAL_MAX_DELAY); }
static void LCD_Write_Byte_Soft(u8 data) { 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(); } }
void LCD_Write_Cmd(u8 data) { LCD_DC_LOW(); LCD_Write_Byte(data); LCD_DC_HIGH(); }
void LCD_Write_Data8(u8 data) { LCD_DC_HIGH(); LCD_Write_Byte(data); }
void LCD_Write_Data16(u16 data) { LCD_DC_HIGH(); LCD_Write_Byte(data >> 8); LCD_Write_Byte(data & 0xFF); }
void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2) { LCD_Write_Cmd(0x2A); LCD_Write_Data8(x1 >> 8); LCD_Write_Data8(x1 & 0xFF); LCD_Write_Data8(x2 >> 8); LCD_Write_Data8(x2 & 0xFF); LCD_Write_Cmd(0x2B); LCD_Write_Data8(y1 >> 8); LCD_Write_Data8(y1 & 0xFF); LCD_Write_Data8(y2 >> 8); LCD_Write_Data8(y2 & 0xFF); LCD_Write_Cmd(0x2C); }
void LCD_ST7789_SleepOut(void) { LCD_Write_Cmd(0x10); delay_ms(100); }
void LCD_ST7789_SleepIn(void) { LCD_Write_Cmd(0x11); delay_ms(100); }
void LCD_Init() { LCD_GPIO_Init(); LCD_CS_LOW(); LCD_RST_LOW(); delay_ms(100); LCD_RST_HIGH(); delay_ms(100);
LCD_Write_Cmd(0x11); delay_ms(120);
LCD_Write_Cmd(0xB2); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x0C); LCD_Write_Data8(0x00); LCD_Write_Data8(0x33); LCD_Write_Data8(0x33);
LCD_Write_Cmd(0x35); LCD_Write_Data8(0x00);
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);
LCD_Write_Cmd(0x3A); LCD_Write_Data8(0x05);
LCD_Write_Cmd(0xB7); LCD_Write_Data8(0x35);
LCD_Write_Cmd(0xBB); LCD_Write_Data8(0x2D);
LCD_Write_Cmd(0xC0); LCD_Write_Data8(0x2C);
LCD_Write_Cmd(0xC2); LCD_Write_Data8(0x01);
LCD_Write_Cmd(0xC3); LCD_Write_Data8(0x15);
LCD_Write_Cmd(0xC4); LCD_Write_Data8(0x20);
LCD_Write_Cmd(0xC6); LCD_Write_Data8(0x0F);
LCD_Write_Cmd(0xD0); LCD_Write_Data8(0xA4); LCD_Write_Data8(0xA1);
LCD_Write_Cmd(0xD6); LCD_Write_Data8(0xA1);
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);
LCD_Write_Cmd(0x21);
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"
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();
#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 #define LGRAYBLUE 0XA651 #define LBBLUE 0X2B12
#endif
|
下面分别介绍头文件内容:
函数声明
: 主要对我们进行实际LCD显示的一些API,供应用层使用,包括区域填充,画图形,显示汉字/字符/数字等。
还有涉及到背光
的需要我们开启背光LCD才能显示
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); 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
| #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 #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
| 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); }
|
LCD显示开发
填充区域
写好LCD底层后,我们想要对应区域显示我们指定的颜色,需要以下步骤:
- 设置显示行列地址
- 写入像素数据
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; LCD_Address_Set(xsta,ysta,xend-1,yend-1); 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); }
|
画图形
显示字符/数字等
显示图像
源文件
最终得到的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 #define OFFSET_X 0
extern lv_display_t * disp;
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); }
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); }
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi == &hspi1) { LCD_SPI_SetBit(8); lv_display_flush_ready(disp); } }
|
驱动测试代码编写
编写完驱动之后,我们通常会写一些参考示例测试我们的驱动,同时后续以供别人使用
填充测试
这个是最基本的测试,可以测试我们驱动是否有问题
驱动编写完成之后我们就可以开始测试了:
1.初始化LCD,开启LCD背光
2.LCD填充

最终看屏幕有没有正常显示颜色,正常显示代表成功。
否则可能是通讯协议
、数据格式
、偏移量
的问题,建议依次排查
其他测试
然后可以进行一些其他的点、线、图片测试等
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-240
,y:0-280
,我们需要自己添加偏移量找到显示位置
最终尝试后发现需要在y轴添加20的偏移量后屏幕显示正常,所以该屏幕的实际显示坐标为 x: 0-240
,y: 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); } } }
|