第二十二章 ESP32S3 SPI_LCD 实验
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信协议,由摩托罗拉公司(Motorola)推出,广泛应用于微控制器与各种外围设备之间的短距离通信。SPI 是一种高速的全双工、同步、串行的通信总线,已经广泛应用在众多 MCU、存储芯片、 AD转换器和 LCD 之间。SPI 通信跟 IIC 通信一样,通信总线上允许挂载一个主设备和一个或者多
本章将学习 ESP32-S3 的硬件 SPI 接口,使用 SPI 接口去驱动 LCD 屏。在本章中,实现和 LCD 屏之间的通信,实现 ASCII 字符、彩色、图片和图形的显示。
本章分为如下几个小节:
22.1 SPI 与 LCD 简介
22.2 硬件设计
22.3 程序设计
22.4 下载验证
22.1 SPI 与 LCD 简介
22.1.1 SPI 介绍
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信协议,由摩托罗拉公司(Motorola)推出,广泛应用于微控制器与各种外围设备之间的短距离通信。SPI 是一种高速的全双工、同步、串行的通信总线,已经广泛应用在众多 MCU、存储芯片、 AD转换器和 LCD 之间。
SPI 通信跟 IIC 通信一样,通信总线上允许挂载一个主设备和一个或者多个从设备。为了跟从设备进行通信,一个主设备至少需要 4 跟数据线,分别为:
MOSI(Master Out / Slave In):主数据输出,从数据输入,用于主机向从机发送数据。
MISO(Master In / Slave Out):主数据输入,从数据输出,用于从机向主机发送数据。
SCLK(Serial Clock):时钟信号,由主设备产生,决定通信的速率。
CS(Chip Select):从设备片选信号,由主设备产生,低电平时选中从设备。
多从机 SPI 通信网络连接如下图所示。

图 22.1.1.1 多从机 SPI 通信网络图
从上图可以知道, MOSI、 MISO、 SCLK 引脚连接 SPI 总线上每一个设备,如果 CS 引脚为低电平,则从设备只侦听主机并与主机通信。 SPI主设备一次只能和一个从设备进行通信。如果主设备要和另外一个从设备通信,必须先终止和当前从设备通信,否则不能通信。
SPI 通信有 4 种不同的模式,不同的从机可能在出厂时就配置为某种模式,这是不能改变的。通信双方必须工作在同一模式下,才能正常进行通信,所以可以对主机的 SPI 模式进行配置。SPI 通信模式是通过配置 CPOL(时钟极性)和 CPHA(时钟相位)来选择的。
CPOL,详称 Clock Polarity,就是时钟极性,当主从机没有数据传输的时候即空闲状态,SCL 线的电平状态,假如空闲状态是高电平, CPOL=1;若空闲状态时低电平,那么 CPOL = 0。
CPHA,详称 Clock Phase,就是时钟相位,实质指的是数据的采样时刻。 CPHA = 0 表示数据的采样是从第 1 个边沿信号上即奇数边沿,具体是上升沿还是下降沿的问题,是由 CPOL 决定的。 CPHA=1 表示数据采样是从第 2 个边沿即偶数边沿。
SPI 的 4 种模式对比图,如下图所示。

图 22.1.1.2 SPI 的 4 种模式对比图
|
模式 |
CPOL |
CPHA |
时钟空闲状态 |
数据采样时刻 |
数据发送时刻 |
|---|---|---|---|---|---|
|
0 |
0 |
0 |
低电平 |
时钟第一个边沿(上升沿) |
时钟第二个边沿(下降沿) |
|
1 |
0 |
1 |
低电平 |
时钟第二个边沿(下降沿) |
时钟第一个边沿(上升沿) |
|
2 |
1 |
0 |
高电平 |
时钟第一个边沿(下降沿) |
时钟第二个边沿(上升沿) |
|
3 |
1 |
1 |
高电平 |
时钟第二个边沿(上升沿) |
时钟第一个边沿(下降沿) |
表 22.1.1.1 SPI 的 4 种模式对比
主设备和从设备必须在相同的SPI模式下才能正常通信。模式0和模式3较为常用。
22.1.2 SPI 控制器介绍
ESP32-S3 芯片集成了四个 SPI 控制器,分别为 SPI0、 SPI1、 SPI2 和 SPI3。 SPI0 和 SPI1 控制器主要供内部使用以访问外部 FLASH 和 PSRAM,所以只能使用 SPI2 和 SPI3。 SPI2 又称为HSPI,而 SPI3 又称为 VSPI,这两个属于 GP-SPI。
GP-SPI 特性:
(1)支持主机模式和从机模式;
(2)支持半双工通信和全双工通信;
(3)支持多种数据模式:
- SPI2: 1-bit SPI 模式、 2-bit Dual SPI 模式、 4-bit Quad SPI 模式、 QPI 模式、 8-bit Octal模式、 OPI 模式
- SPI3: 1-bit SPI 模式、 2-bit Dual SPI 模式、 4-bit Quad SPI 模式、 QPI 模式
下面用一个表格总结这些模式的核心区别:
|
模式名称 |
数据线数量 |
每时钟传输位数 |
通信方式 |
典型应用场景 |
特点 |
|---|---|---|---|---|---|
|
1-bit SPI (标准模式) |
2根 (MOSI, MISO) |
1位 |
全双工 |
各种常见外设(传感器、ADC、常规Flash) |
最基础、最广泛兼容的模式 |
|
2-bit Dual SPI |
2根 (SIO0, SIO1) |
2位 |
半双工 |
需要提速的SPI Flash、部分显示器 |
将传统SPI的全双工改为半双工,利用原有的MOSI和MISO作为双向数据线,实现一个时钟周期传输2位数据 |
|
4-bit Quad SPI (QSPI) |
4根 (IO0, IO1, IO2, IO3) |
4位 |
半双工 |
高速SPI Flash、存储器扩展、液晶屏初始化 |
使用4根双向数据线,吞吐量是标准SPI的4倍。支持内存映射,CPU可直接读取外部Flash |
|
QPI模式 |
4根 (IO0, IO1, IO2, IO3) |
4位 (命令、地址、数据) |
半双工 |
特定型号的SPI Flash |
是Quad SPI的增强版。命令、地址和数据阶段都使用4线传输,进一步减少了总线切换开销,时序更简单 |
|
8-bit Octal SPI |
8根 (IO0-IO7) |
8位 |
半双工 |
超高速数据存取、高性能计算 |
使用8根数据线,每个时钟周期传输1字节,提供极高的吞吐量 |
|
OPI模式 |
8根 (IO0-IO7) |
8位 (命令、地址、数据) |
半双工 |
最新一代超高速Flash存储器(如eMMC、UFS接口的替代) |
Octal SPI的增强版。命令、地址和数据全部通过8根数据线传输,是当前SPI协议家族中的顶级性能模式 |
表 22.1.2.1 SPI 的数据模式对比
所有这些模式都基于同一个核心思想:在同步时钟(SCLK)的控制下,通过并行化数据线来倍增每个时钟周期传输的数据量,从而在相同的时钟频率下实现更高的数据吞吐率。
(4)时钟频率可配置:
- 在主机模式下:时钟频率可达 80MHz
- 在从机模式下:时钟频率可达 60MHz
(5)数据位的读写顺序可配置;
(6)时钟极性和相位可配置;
(7)四种 SPI 时钟模式:模式 0 ~ 模式 3;
(8)在主机模式下,提供多条 CS 线
- SPI2: CS0 ~ CS5
- SPI3: CS0 ~ CS2
(9)支持访问 SPI 接口的传感器、显示屏控制器、 flash 或 RAM 芯片
SPI2 和 SPI3 接口相关信号线可以经过 GPIO 交换矩阵和 IO_MUX 实现与芯片引脚的映射,IO 使用起来非常灵活。
22.1.3 LCD 介绍
在介绍LCD之前,建议在B站上搜索看下LCD的工作原理,对下面学习有一定帮助。本例程支持两款屏幕,一款是原子的1.3 寸显示模块 ATK-MD0130,另一款是原子 2.4 寸显示模块 ATK-MD0240。这两款显示模块的 LCD 分辨率分别为 240*240 和 320*240,支持 16 位真彩色显示。模块采用 ST7789V 作为 LCD 的驱动芯片,该芯片自带 RAM,无需外加驱动器或存储器。使用外接的主控芯片时,仅需使用 SPI 接口就可以轻松地驱动这两个显示模块。
屏幕模块通过 2*4 的排针( 2.54m 间距)同外部相连接,该模块可直接与正点原子ESP32-S3 开发板的 WIRELESS 接口( SPI 接口)连接,而对于没有板载 WIRELESS 接口的开发板,可以通过杜邦线连接。
ATK-MD0240模块的外观,如下图所示。

图 22.1.3.1 ATK-MD0240 模块实物图
模块的原理图,如下图所示。

图 22.1.3.2 ATK-MD0130 模块原理图
模块通过一个 2*4 的排针( 2.54mm 间距)同外部电路连接,各引脚的详细描述,如下表所示。
| 序号 | 名称 | 说明 |
|---|---|---|
| 1 | 3V3 | 3.3V 电源供电 |
| 2 | CS | SPI 通讯片选信号(低电平有效) |
| 3 | SDA | SPI 通讯 MOSI 信号线 |
| 4 | RST | 硬件复位引脚(低电平有效) |
| 5 | WR | 写命令/数据信号线(低电平:写命令;高电平:写数据) |
| 6 | SCK | SPI 通讯 SCK 信号线 |
| 7 | PWR | LCD 背光控制引脚(低电平:关闭;高电平:开启) |
| 8 | GND | 电源地 |
表 22.1.3.1 ATK-MD0130 和 ATK-MD0240 模块引脚说明
22.1.4 模块 SPI 时序介绍
ATK-MD0130 和 ATK-MD0240 模块在四线 SPI 通讯模式下,最少仅需四根信号线(CS、SCK、 SDA、 WR(DC))就能够完成与这两个显示模块的通讯,四线 SPI 接口时序如下图所示。

图 22.1.4.1 四线 SPI 接口时序图
上图中各个时间参数,如下图所示。

图 22.1.4.2 四线 SPI 接口时序时间参数
从上图中可以看出, ATK-MD0130 和 ATK-MD0240 模块四线 SPI 的写周期是非常快的(TSCYCW = 66ns),而读周期就相对慢了很多(TSCYCR = 150ns)。更详细的时序介绍,可以参考 ST7789V 的数据手册《ST7789V_SPEC_V1.4.pdf》。
22.1.5 模块驱动说明
ATK-MD0130 和 ATK-MD0240 模块采用 ST7789V 作为 LCD 驱动器, LCD 的显存可直接存放在 ST7789V 的片上 RAM 中, ST7789V 的片上 RAM 有 240*320*3 字节,并且 ST7789V 会在没有外部时钟的情况下,自动将其片上 RAM 的数据显示至 LCD 上,以最小化功耗。
在每次初始化显示模块之前,必须先通过 RST 引脚对显示模块进行硬件复位,硬件复位要求 RST 至少被拉低 10 微秒,拉高 RST 结束硬件复位后,须延时 120 毫秒等待复位完成后,才能够往显示模块传输数据。
PWR 引脚用于控制显示模块的 LCD 背光,该引脚自带下拉电阻,当 PWR 引脚被拉低或悬空时, ATK-MD0130 模块的 LCD 背光都处于关闭状态,当 PWR 引脚被拉高时,显示模块的LCD 背光才会点亮。
ST7789V最高支持 18位色深(262K色),但一般使用 16位色深(65K色)的 RGB565 格式,这样可以在 16 位色深下达到最快的速度。在 16 位色深模式下, ST7789V 采用 RGB565 格式传输、存储颜色数据,如下图所示。

图 22.1.4.3 位色深模式(RGB565)传输颜色数据
如上图所示,一个像素的颜色数据需要使用 16 比特来传输,这 16 比特数据中,高 5 比特用于表示红色,低 5 比特用于表示蓝色,中间的 6 比特用于表示绿色。数据的数值越大,对应表示的颜色就越深。
22.1.6 ST7789V驱动芯片介绍
22.1.6.1 核心特性
ST7789V 是一款由 Sitronix(矽创)公司生产的单芯片 TFT LCD 控制器/驱动器。它以其高集成度、丰富的接口支持和优异的显示性能,广泛应用于各种中小尺寸彩色显示设备中。
核心特性:
|
特性维度 |
说明 |
|---|---|
|
最大分辨率 |
240 RGB × 320 (即 240×320 像素),能驱动此分辨率及以下的各种尺寸屏幕 (如 1.3寸, 1.54寸, 2.0寸等常见模块) |
|
色彩深度 |
最高支持 262K色 (18位,RGB 6-6-6),通常配置为65K色 (16位,RGB 5-6-5) 以平衡性能和带宽 |
|
接口类型 |
SPI (3线/4线,最常用) |
|
内置内存 |
帧缓存 (Frame Memory),大小为 240 x 320 x 18-bit (约1.38Mb),可存储一整屏图像数据 |
|
电源电压 |
I/O 电压 (VDDI):1.65V ~ 3.3V |
|
关键特性 |
内置时序控制器、伽马校正电路、电源管理单元(支持睡眠/待机等低功耗模式)、可编程帧率、部分显示功能 |
|
典型应用 |
智能手表、物联网设备显示屏、便携式医疗设备、工业控制面板、智能家居显示终端等 |
表 22.1.6.1.1 ST7789V芯片核心特性
22.1.6.2 工作原理与流程
ST7789V 作为驱动芯片,其核心工作是接收来自主控制器(如 MCU)的指令和数据,并将其转换为控制 TFT LCD 面板的正确时序和电压信号。其基本工作流程,特别是常用的 SPI 模式,可以概括如下:

图 22.1.6.2.1 ST7789V工作原理与流程图
接口模式详解:
ST7789V 支持多种接口模式,可通过硬件引脚(如 IM0, IM1, IM2, IM3)进行配置:
(1)SPI (串行外设接口):最常用,尤其适合引脚资源紧张的 MCU;
- 3线 SPI:使用 SCL(时钟)、SDA(数据线,双向)和 CS(片选)引脚。通过数据线的一位(DCX/D-C)来区分命令和数据。
- 4线 SPI:使用 SCL、SDA(主出从入 MOSI)、DC(数据/命令选择)和 CS 引脚。这是最常见的形式,通信时序由主设备(MCU)控制。
(2)并行接口 (如 8080 系列):传输速度快,适合需要高速刷新或高分辨率的场景。需要较多的数据线(D0-D17)和控制线(如 RD, WR, CS, DC);
(3)RGB 接口:直接接收 RGB 颜色数据、行场同步信号(HSYNC, VSYNC)和像素时钟(DOTCLK)。通常用于直接连接带有 LCD 控制器的更强大处理器(如 MPU),ST7789V 此时主要作为驱动器。
开发使用要点:
(1)初始化序列 (Initialization Sequence):上电后,必须通过一系列特定的命令(Command)和数据(Data) 对 ST7789V 进行配置,才能正常显示。包括设置扫描方向、颜色模式、伽马校正、打开显示等。通常供应商会提供参考初始化代码。
(2)内存与寻址:ST7789V 内置了帧缓存。当你向它发送像素数据时,数据会存储在其内部内存中。你可以通过命令设置列地址(CASET) 和 行地址(RASET) 来定义一个要更新的“窗口”,然后连续写入该区域的数据,提高效率。
(3)常用命令示例:
- 0x36:存储器访问控制(MX, MY, RGB mode等)
- 0x3A:颜色模式设置(如 0x55为 16位/pixel,0x66为 18位/pixel)
- 0x2A:列地址设置 (CASET)
- 0x2B:行地址设置 (RASET)
- 0x2C:存储器写入 (RAMWR) - 紧接着发送像素数据
- 0x11:睡眠模式退出 (SLPOUT)
- 0x29:打开显示 (DISPON)
更详细命令如下表:
建议用到的命令详细说明参考 ST7789V 的数据手册《ST7789V_SPEC_V1.4.pdf》的第9章节。
|
命令 (名称) |
命令码 (Hex) |
主要功能说明 |
参数示例/备注 |
|---|---|---|---|
|
SWRESET (软件复位) |
0x01 |
软件复位,让芯片恢复初始状态。 |
无参数。通常需要延迟一段时间等待复位完成。 |
|
SLPOUT (睡眠模式关闭) |
0x11 |
让芯片从睡眠模式唤醒,进入正常工作模式。 |
无参数。发送此命令后通常需要等待 120ms 左右 (如 |
|
DISPON (显示开启) |
0x29 |
开启显示,使得显存内容能够呈现在屏幕上。 |
无参数。 |
|
DISPOFF (显示关闭) |
0x28 |
关闭显示,屏幕不再显示内容,但驱动电路可能仍在工作。 |
无参数。 |
|
CASET (列地址设置) |
0x2A |
设置要在哪个水平(X方向)区域写入像素数据。 |
4 个参数:起始列地址高8位、低8位,结束列地址高8位、低8位。例如要设置 X 从 0 到 239: |
|
RASET (行地址设置) |
0x2B |
设置要在哪个垂直(Y方向)区域写入像素数据。 |
4 个参数:起始行地址高8位、低8位,结束行地址高8位、低8位。例如要设置 Y 从 0 到 319: |
|
RAMWR (内存写入) |
0x2C |
向由 CASET 和 RASET 设定的区域连续写入像素数据。 |
其后跟随大量的像素数据。数据格式由 COLMOD 命令设置。 |
|
MADCTL (内存访问控制) |
0x36 |
控制显示方向(旋转、镜像)、颜色顺序(RGB/BGR)和 扫描方式。这是一个非常常用的命令。 |
1 个参数,各位含义丰富: |
|
• MY: 行地址顺序 (上下镜像) |
|||
|
• MX: 列地址顺序 (左右镜像) |
|||
|
• MV: 行列交换 (横屏/竖屏) |
|||
|
• ML: 垂直刷新顺序 |
|||
|
• RGB: RGB/BGR 顺序 |
|||
|
• MH: 水平刷新顺序 |
|||
|
COLMOD (接口像素格式) |
0x3A |
设置像素数据的颜色深度和格式,即每个像素点用多少位来表示。 |
1 个参数。常用模式: |
|
INVON (反色显示开启) |
0x21 |
开启显示颜色反转。 |
无参数。 |
|
INVOFF (反色显示关闭) |
0x20 |
关闭显示颜色反转。 |
无参数。 |
|
PORCTRL (Porch设置) |
0xB2 |
控制水平同步信号前沿和后沿的延迟时间,以及垂直同步信号前沿和后沿的延迟时间,影响显示时序和稳定性。 |
5个参数。 |
|
GCTRL (Gate控制) |
0xB7 |
调整像素电压的开关时间,以控制液晶的亮度和对比度。 |
1个参数。 |
|
VCOMS (VCOM设置) |
0xBB |
设置液晶面板的电压,以调整图像的亮度和对比度。 |
1个参数。 |
|
VDVVRHEN (VDV和VRH命令启用) |
0xC2 |
启用VDV(VCOM Deselect voltage)和VRH(Voltage range High)命令,以使VCOM电压和电压范围高度可调。 |
1个参数。 |
|
VRHS (VRH设置) |
0xC3 |
设置VRH的值,以控制VCOM电压的范围。 |
1个参数。 |
|
VDVS (VDV设置) |
0xC4 |
设置VDV的值,以调整VCOM电压的偏移量。 |
1个参数。 |
|
PWCTRL1 (电源控制1) |
0xD0 |
控制液晶面板的电源,以调整电压和电流的输出。 |
2个参数。 |
|
PVGAMCTRL (正电压伽马控制) |
0xE0 |
控制正电压伽马校正,以调整图像亮度和对比度。用于色彩校准。 |
15个参数。通常使用厂家提供的预设值。 |
|
NVGAMCTRL (负电压伽马控制) |
0xE1 |
控制负电压伽马校正,以调整图像亮度和对比度。用于色彩校准。 |
15个参数。通常使用厂家提供的预设值。 |
表 22.1.6.2.1 ST7789V芯片命令
针对几个常用命名再做说明:
ST7789V 支持连续读写 RAM 中存放的 LCD 上颜色对应的数据,并且连续读写的方向(LCD 的扫描方向)是可以通过命令 0x36 进行配置的,如下图所示。

图 22.1.6.2.12 命令 0x36
从上图中可以看出,命令 0x36 可以配置 6 个参数,但对于配置 LCD 的扫描方向,仅需关心 MY、 MX 和 MV 这三个参数,如下表所示。
| 参数 | LCDLCD 扫描方向(RAM 自增方向) | ||
| MY | MX | MV | |
| 0 | 0 | 0 | 从左到右,从上到下 |
| 1 | 0 | 0 | 从左到右,从下到上 |
| 0 | 1 | 0 | 从右到左,从上到下 |
| 1 | 1 | 0 | 从右到左,从下到上 |
| 0 | 0 | 1 | 从上到下,从左到右 |
| 1 | 0 | 1 | 从上到下,从右到左 |
| 0 | 1 | 1 | 从下到上,从左到右 |
| 1 | 1 | 1 | 从下到上,从右到左 |
表 22.1.6.2.2 命令 0x36 配置 LCD 扫描方向
仅需设置一次坐标,然后连续地往 ATK-MD0130 和 ATK-MD0240 模块传输颜色数据即可。大大地提高 ATK-MD0130 和 ATK-MD0240 模块在刷屏时的效率,
在往 ATK-MD0130 和 ATK-MD0240 模块写入颜色数据前,还需要设置地址,以确定随后写入的颜色数据对应 LCD 上的哪一个像素,通过命令 0x2A 和命令 0x2B 可以分别设置 ATK-MD0130 和 ATK-MD0240 模块显示颜色数据的列地址和行地址,命令 0x2A 的描述,如下图所示。

图 22.1.6.2.3 命令 0x2A
命令 0x2B 的描述,如下图所示。

图 22.1.6.2.4 命令 0x2B
以默认的 LCD 扫描方式(从左到右,从上到下)为例,命令 0x2A 的参数 XS 和 XE 和命令0x2B 的参数 YS 和 YE 就在 LCD 上确定了一个区域,在连读读写颜色数据时, ST7789V 就会按照从左到右,从上到下的扫描方式读写设个区域的颜色数据。
22.2 硬件设计
22.2.1 例程功能
本章实验功能简介:使用开发板的 SPI 接口连接正点原子 SPI LCD 模块(仅限 SPI 显示模块),实现 SPI LCD 模块的显示。通过把 LCD 模块插入底板上的 WIRELESS 接口(SPI 接口),按下复位之后,就可以看到 SPI LCD 模块不停的显示一些信息并不断切换底色。 LED 闪烁用于提示程序正在运行。
22.2.2 硬件资源
1. LED
LED - IO1
2. 2.4 寸 SPI LCD 模块
22.2.3 原理图
本章实验使用2.4 寸的 SPI LCD 模块,该模块与板载的 WIRELESS 接口进行连接,该接口与板载 MCU 的连接原理图,如下图所示:

图 22.2.3.1 SPILCD 模块与 MCU 的连接原理图
对应驱动管脚:
| 管脚标识 | 真实管脚 | 说明 |
|---|---|---|
| SLCD_PWR | IO1_3 | 使用XL9555的扩展管脚 |
| SLCD_CS | IO21 | |
| SPI_SCK | IO12 | |
| SPI_MOSI | IO11 | |
| IO_SEL | IO40 | 通过跳线帽连接LCD_DC->IO40 |
| SLCD_RST | IO1_2 | 使用XL9555的扩展管脚 |
表 22.2.3.1 MCU驱动SPILCD 模块对应管脚
22.3 程序设计
22.3.1 程序流程图
程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:

图 22.3.1.1 SPI_LCD 实验程序流程图
22.3.2 SPI_LCD 函数解析
ESP-IDF 提供了一套 API 来配置 SPI。要使用此功能,需要导入必要的头文件:
#include "driver/spi_master.h"
接下来,介绍一些常用的 ESP32-S3中的 SPI函数,以及 IO扩展芯片中用到的函数,这些函数的描述及其作用如下:
(1) 初始化和配置
该函数用于初始化 SPI 总线,并配置其 GPIO引脚和主模式下的时钟等参数,该函数原型如下所示:
esp_err_t spi_bus_initialize(spi_host_device_t host_id,
const spi_bus_config_t *bus_config,
spi_dma_chan_t dma_chan);
该函数的形参描述,如下表所示:
|
参数名 |
类型 |
说明 |
备注/常用值 |
|---|---|---|---|
|
host_id |
|
指定要初始化的 SPI 主机控制器编号 |
|
|
bus_config |
|
指向 SPI 总线配置结构体 的指针,该结构体定义了 GPIO 引脚、最大传输大小等参数 |
详见下方 |
|
dma_chan |
|
指定 DMA 通道 |
|
表 22.3.2.1 spi_bus_initialize()函数形参描述
spi_bus_config_t结构体说明:
typedef struct {
int mosi_io_num; /*!< MOSI 引脚 GPIO 编号 */
int miso_io_num; /*!< MISO 引脚 GPIO 编号 */
int sclk_io_num; /*!< SCLK (时钟) 引脚 GPIO 编号 */
int quadwp_io_num; /*!< 四线 SPI 模式下的 WP (写保护) 引脚 GPIO 编号,不使用则设为 -1 */
int quadhd_io_num; /*!< 四线 SPI 模式下的 HD (保持) 引脚 GPIO 编号,不使用则设为 -1 */
int max_transfer_sz; /*!< 最大传输大小(以字节为单位) */
int intr_flags; /*!< 中断标志位,通常设置为 0 */
} spi_bus_config_t;
函数使用流程:
(1)配置总线参数:填充 spi_bus_config_t结构体,指定 MOSI、MISO、SCLK 等引脚。不使用的功能(如 Quad SPI)其引脚号设为 -1;
(2)调用函数初始化:使用 spi_bus_initialize()并指定 SPI 主机(如 SPI2_HOST)、上述配置结构体和 DMA 通道(通常用 SPI_DMA_CH_AUTO);
(3)检查返回值:函数返回 ESP_OK表示初始化成功,否则需根据错误码排查问题。
注意事项:
(1)SPI 控制器选择:ESP32 有多个 SPI 控制器。SPI0 和 SPI1 通常预留给内部连接 Flash 和 PSRAM,严禁在应用代码中初始化它们,否则可能导致系统崩溃;
(2)用户应用应使用 SPI2 (HSPI) 或 SPI3 (VSPI)
GPIO 引脚映射:SPI2 和 SPI3 的信号线可以通过 GPIO 交换矩阵 映射到几乎所有空闲的GPIO 引脚,这提供了极大的灵活性。但需注意:
- 使用 GPIO 矩阵时,SCLK 的最高频率会受限(通常降至 40 MHz);
- 为了追求更高的速度和稳定性,建议优先使用芯片数据手册推荐的 IO_MUX 专用引脚。
(3)DMA 通道:对于大数据量传输(如操作显示屏、读写大容量 Flash),强烈建议启用 DMA(如 SPI_DMA_CH_AUTO),这可以显著减轻 CPU 负担并提高效率,对于小数据量或简单设备,可以禁用 DMA (SPI_DMA_DISABLED);
(4)最大传输大小:max_transfer_sz参数限制了单次 SPI 事务能传输的最大数据量(字节)。请根据实际应用需求设置此值,特别是启用 DMA 时,ESP32 的 DMA 对单次传输数据量有约束;
(5)后续步骤:spi_bus_initialize()仅初始化了总线本身。总线初始化成功后,还需要使用 spi_bus_add_device()来添加具体的 SPI 从设备,然后才能使用 spi_device_transmit()等函数进行数据传输;
(6)错误处理:务必检查函数的返回值 (esp_err_t),确保初始化成功后再进行后续操作。
(2)设备配置
该函数用于在 SPI 总线上分配设备,函数原型如下所示:
esp_err_t spi_bus_add_device(spi_host_device_t host_id,
const spi_device_interface_config_t *dev_config,
spi_device_handle_t *handle);
该函数的形参描述,如下表所示:
|
参数名 |
类型 |
说明 |
|---|---|---|
|
host_id |
|
指定要添加设备的 SPI 主机控制器编号,需与 |
|
dev_config |
|
指向 SPI 设备接口配置结构体 的指针,该结构体定义了设备的通信参数。 |
|
handle |
|
输出参数,用于返回新添加的 SPI 设备的句柄。后续对该设备的所有操作(如数据传输)都需使用此句柄。 |
表 22.3.2.1 spi_bus_add_device()函数形参描述
spi_device_interface_config_t 结构体:
此结构体用于配置 SPI 从设备的通信参数,需根据从设备的数据手册进行设置。
typedef struct {
uint8_t command_bits; /*!< 命令阶段的位数 (0-16),若无需命令阶段则设为0 */
uint8_t address_bits; /*!< 地址阶段的位数 (0-64),若无需地址阶段则设为0 */
uint8_t dummy_bits; /*!< 在地址和数据阶段之间插入的虚拟位数,用于满足时序要求 */
uint8_t mode; /*!< SPI 模式 (0-3),由 CPOL 和 CPHA 决定 */
spi_clock_source_t clock_source; /*!< SPI 时钟源,通常使用默认值 `SPI_CLK_SRC_DEFAULT` */
uint16_t duty_cycle_pos; /*!< 时钟正占空比,单位为 1/256,128 表示 50% */
uint16_t cs_ena_pretrans; /*!< 传输开始前 CS 信号需要提前激活的时钟周期数 (0-16),用于半双工 */
uint16_t cs_ena_posttrans; /*!< 传输结束后 CS 信号需要保持激活的时钟周期数 (0-16),用于半双工 */
int clock_speed_hz; /*!< SPI 时钟频率,单位 Hz */
int input_delay_ns; /*!< 从设备最大输入延迟(MISO 有效时间),用于时序调整 */
int spics_io_num; /*!< 该设备专用的片选 (CS) 引脚 GPIO 编号 */
uint32_t flags; /*!< 设备特性标志位(位掩码),如字节序、双工模式等 */
int queue_size; /*!< 事务队列大小,决定可挂起的待处理事务数量 */
transaction_cb_t pre_cb; /*!< 传输开始前的回调函数(在中断中调用) */
transaction_cb_t post_cb; /*!< 传输完成后的回调函数(在中断中调用) */
} spi_device_interface_config_t;
关键配置字段详解
(1)mode(SPI模式):必须与从设备要求的模式一致,有4种选择:
- 0: CPOL=0,CPHA=0(空闲时SCLK低电平,数据在第一个边沿采样)
- 1: CPOL=0,CPHA=1(空闲时SCLK低电平,数据在第二个边沿采样)
- 2: CPOL=1,CPHA=0(空闲时SCLK高电平,数据在第一个边沿采样)
- 3:CPOL=1,CPHA=1(空闲时SCLK高电平,数据在第二个边沿采样)
(2)clock_speed_hz(时钟频率):设置通信速率。注意,当使用GPIO矩阵(非专用IO_MUX引脚)时,最高频率可能受限(如40MHz);
(3)spics_io_num(片选引脚):指定控制该设备的片选信号线GPIO。同一总线上不同设备必须使用不同的片选引脚;
(4)flags(标志位):用于设置一些特殊设备属性,常用的有:
- SPI_DEVICE_TXBIT_LSBFIRST:发送数据时低位在先(LSB)
- SPI_DEVICE_RXBIT_LSBFIRST:接收数据时低位在先(LSB)
- SPI_DEVICE_3WIRE:使用三线制SPI(双向数据线)
- SPI_DEVICE_HALFDUPLEX:设备工作在半双工模式
- SPI_DEVICE_POSITIVE_CS:片选信号高电平有效(默认为低电平有效)
(5)queue_size(队列大小):设置挂起事务队列的长度。如果使用spi_device_queue_trans进行异步传输,此值应大于1。
注意事项:
(1)线程安全:SPI驱动本身是线程安全的,但对同一设备的操作若来自不同任务,则需用户自行添加互斥锁(如FreeRTOS的互斥量)进行保护;
(2)句柄管理:成功添加设备后获得的spi_device_handle_t句柄必须妥善保存,后续所有的数据传输(如spi_device_transmit)以及最后移除设备时都需要使用它;
(3)参数验证:部分配置参数(如clock_speed_hz)有物理限制(如IO_MUX引脚最高80MHz,GPIO矩阵引脚最高40MHz),设置时需注意;
(4)专用引脚:为了获得更好的性能和更高的时钟速率,建议优先使用ESP32-S3的IO_MUX专用引脚作为SPI信号线;
(5)错误处理:务必检查函数的返回值(esp_err_t),确保设备添加成功后再进行后续操作;
(6)资源释放:当不再需要与某设备通信时,应使用spi_bus_remove_device(handle)将其从总线上移除,并在所有设备都移除后可使用spi_bus_free(host_id)释放总线资源。
(3)数据传输
根据函数功能,以下2个函数可以归为一类进行讲解,如下表所示。
| 函数 | 描述 |
|---|---|
| spi_device_transmit() | 该函数用于发送一个 SPI 事务,等待它完成,并返回结果。 handle: 设备的句柄。 trans_desc: 指向 spi_transaction_t 结构体的指针,描述了要发送的事务详情。 |
| spi_device_polling_transmit() | 该函数用于发送一个轮询事务,等待它完成,并返回结果。 handle: 设备的句柄。 trans_desc: 指向 spi_transaction_t 结构体的指针,描述了要发送的事务详情。 |
表 22.3.2.2 数据传输函数介绍
spi_device_transmit()函数:
该函数用于发送一个 SPI 事务,等待它完成,并返回结果。函数原型如下所示:
esp_err_t spi_device_transmit(spi_device_handle_t handle,
spi_transaction_t *trans_desc);
该函数的形参描述,如下表所示:
|
参数名 |
类型 |
说明 |
|---|---|---|
|
handle |
spi_device_handle_t |
SPI 设备句柄,通过 |
|
|
spi_transaction_t * |
指向 |
表 22.3.2.3 spi_device_transmit()函数形参描述
spi_transaction_t 结构体:
|
字段 |
类型 |
说明 |
注意事项 |
|---|---|---|---|
|
flags |
|
传输标志位,用于控制特殊传输模式。例如: |
根据需要设置,默认为 0。 |
|
cmd |
|
命令值。如果配置 SPI 设备时设置了 |
通常用于发送一些控制指令。 |
|
addr |
|
地址值。如果配置 SPI 设备时设置了 |
通常用于指定从设备的寄存器地址或存储地址。 |
|
length |
|
数据传输的总长度(单位:位)。 |
注意是位(bit)数,而不是字节数。例如,要发送 3 个字节,则 |
|
rxlength |
|
接收数据的长度(单位:位)。如果设置为 0,则接收数据长度由 |
通常用于发送和接收长度不等的场景。 |
|
tx_buffer |
|
指向发送数据缓冲区的指针。如果使用 |
发送大量数据时使用此字段。如果不需要发送数据,设置为 NULL。 |
|
rx_buffer |
|
指向接收数据缓冲区的指针。驱动程序接收到的数据将填充到此缓冲区。如果使用 |
如果需要接收数据,必须指向一个足够大的有效缓冲区。如果不需要接收数据,设置为 NULL。 |
|
user |
|
用户自定义数据。此变量不会被驱动程序使用,但会在回调函数中传递回用户。 |
常用于传递上下文信息,例如片选引脚号。 |
|
tx_data |
|
内嵌的发送数据数组。当数据长度 ≤ 4 字节且设置了 |
方便短数据发送,避免额外定义缓冲区。 |
|
rx_data |
|
内嵌的接收数据数组。当数据长度 ≤ 4 字节且设置了 |
方便短数据接收。 |
表 22.3.2.4 spi_transaction_t结构体描述
函数使用流程:
(1)初始化 SPI 总线:使用 spi_bus_initialize()。
(2)添加 SPI 设备:使用 spi_bus_add_device(),获得设备句柄 handle。
(3)配置事务结构体:填充 spi_transaction_t trans_desc,根据传输需求设置字段。
(4)执行传输:调用 spi_device_transmit(handle, &trans_desc)。
(5)检查返回值并处理数据。
(6)(可选) 移除设备和释放总线:当不再需要时,调用 spi_bus_remove_device()和 spi_bus_free()。
同步 vs. 异步传输
spi_device_transmit()是同步的:它会阻塞当前任务,直到 SPI 事务完全完成后才返回。简单易用,但效率较低。
队列传输是异步的:使用 spi_device_queue_trans()将事务放入队列后函数立即返回,后续通过 spi_device_get_trans_result()获取结果。效率更高,适合复杂应用或需要并发处理多个事务的场景。
spi_device_polling_transmit()函数:
该函数用于以轮询(阻塞)的方式同步执行一个 SPI 传输事务。它会启动 SPI 传输,并阻塞当前任务,直到整个传输完成后才返回。
|
特性维度 |
说明 |
|---|---|
|
函数原型 |
|
|
核心功能 |
启动一个 SPI 传输,并阻塞当前任务,等待传输完成。 |
|
传输模式 |
轮询(Polling)模式。CPU 会不断检查 SPI 外设的状态位,直到传输完成。 |
|
阻塞行为 |
阻塞当前任务,直到 SPI 事务完全完成。在此期间,CPU 不能执行其他任务。 |
|
返回值 |
|
|
性能特点 |
延迟低(节省了中断处理、队列管理和上下文切换的开销),但CPU 利用率高(传输期间CPU被独占)。 |
|
适用场景 |
对时序要求严格、数据量小或在关键代码段中需要简单可靠通信的场景。 |
|
线程安全 |
对同一 SPI 设备的访问不是线程安全的。如果多个任务可能访问同一设备,必须使用互斥锁(如 FreeRTOS 的互斥量)进行保护。 |
表 22.3.2.5 spi_device_polling_transmit()函数说明
函数入参解释同spi_device_transmit()入参,使用流程也相同,不再重复介绍;
spi_device_polling_transmit()函数会阻塞调用它的任务,并通过轮询 SPI 主控制器的状态寄存器来检查传输是否完成,而不是依赖中断来通知完成事件。这意味着在 SPI 传输进行期间,CPU 无法处理其他任务,会一直等待直到整个 SPI 事务操作完毕。这种方式的优点是减少了中断处理和任务调度的开销,使得通信延迟更小,时序更可控;缺点则是传输期间 CPU 被完全占用,效率较低。
spi_device_polling_transmit()函数优点与缺点:
优点:
- 简单可靠:代码逻辑简单,不易受中断延迟或任务调度的影响;
- 低延迟:免去了中断处理、任务队列管理和上下文切换的开销,传输的延迟更小,时序非常精确和可控。
缺点:
- CPU 利用率高:在传输期间,CPU 被完全占用,无法处理其他任务,效率较低;
- 可能影响系统实时性:长时间的 SPI 传输会阻塞整个任务,可能影响其他关键事件的响应。
重要提示:
- 通信协议:ST7789 通常使用 SPI(串行外设接口)进行控制。在发送命令时,需要将 DC(数据/命令选择)引脚拉低;在发送数据或参数时,则需要将 DC 引脚拉高。
- 复位时序:硬件复位(RST引脚)时,低电平脉冲持续时间至少需要 10μs,并且在拉高后需等待最多 120ms 才能进行软件访问。
- 精细调整:PORCTRL, GCTRL, VCOMS, 伽马设置等命令主要用于调整屏幕的显示效果、亮度和对比度,通常直接使用屏幕厂商提供的默认值即可,除非有特殊的调试需求。
- 数据格式:使用 16位色 (RGB565) 时,每个像素由两个字节组成,例如 0xF800表示红色,0x07E0表示绿色,0x001F表示蓝色。发送时注意字节顺序(大端序)。
22.3.3 SPI_LCD 驱动解析
看这个代码前为了更好理解代码,最好先看下第二十一章 OLED部分实现。在 IDF 版的 12_spilcd 例程中,在 12_spilcd \components\BSP 路径下新增了一个 SPI 文件夹和一个 LCD 文件夹,分别用于存放 spi.c、 spi.h 和 lcd.c 以及 lcd.h 这四个文件。其中,spi.h和 lcd.h 文件负责声明 SPI 以及 LCD 相关的函数和变量,而 spi.c 和 lcd.c 文件则实现了 SPI 以及LCD 的驱动代码。下面将详细解析这四个文件的实现内容。
(1)spi.h 文件
/* 引脚定义 */
#define SPI_MOSI_GPIO_PIN GPIO_NUM_11 /* SPI2_MOSI */
#define SPI_CLK_GPIO_PIN GPIO_NUM_12 /* SPI2_CLK */
#define SPI_MISO_GPIO_PIN GPIO_NUM_13 /* SPI2_MISO */
该文件下定义了 SPI 的时钟引脚与通讯引脚。
(2)spi.c 文件
/**
* @brief 初始化SPI
* @param 无
* @retval 无
*/
void spi2_init(void)
{
esp_err_t ret = 0;
spi_bus_config_t spi_bus_conf = {0};
/* SPI总线配置 */
spi_bus_conf.miso_io_num = SPI_MISO_GPIO_PIN; /* SPI_MISO引脚 */
spi_bus_conf.mosi_io_num = SPI_MOSI_GPIO_PIN; /* SPI_MOSI引脚 */
spi_bus_conf.sclk_io_num = SPI_CLK_GPIO_PIN; /* SPI_SCLK引脚 */
spi_bus_conf.quadwp_io_num = -1; /* SPI写保护信号引脚,该引脚未使能 */
spi_bus_conf.quadhd_io_num = -1; /* SPI保持信号引脚,该引脚未使能 */
spi_bus_conf.max_transfer_sz = 320 * 240 * 2; /* 配置最大传输大小,以字节为单位 */
/* 初始化SPI总线 */
ret = spi_bus_initialize(SPI2_HOST, &spi_bus_conf, SPI_DMA_CH_AUTO); /* SPI总线初始化 */
ESP_ERROR_CHECK(ret); /* 校验参数值 */
}
/**
* @brief SPI发送命令
* @param handle : SPI句柄
* @param cmd : 要发送命令
* @retval 无
*/
void spi2_write_cmd(spi_device_handle_t handle, uint8_t cmd)
{
esp_err_t ret;
spi_transaction_t t = {0};
t.length = 8; /* 要传输的位数 一个字节 8位 */
t.tx_buffer = &cmd; /* 将命令填充进去 */
ret = spi_device_polling_transmit(handle, &t); /* 开始传输 */
ESP_ERROR_CHECK(ret); /* 一般不会有问题 */
}
/**
* @brief SPI发送数据
* @param handle : SPI句柄
* @param data : 要发送的数据
* @param len : 要发送的数据长度
* @retval 无
*/
void spi2_write_data(spi_device_handle_t handle, const uint8_t *data, int len)
{
esp_err_t ret;
spi_transaction_t t = {0};
if (len == 0)
{
return; /* 长度为0 没有数据要传输 */
}
t.length = len * 8; /* 要传输的位数 一个字节 8位 */
t.tx_buffer = data; /* 将命令填充进去 */
ret = spi_device_polling_transmit(handle, &t); /* 开始传输 */
ESP_ERROR_CHECK(ret); /* 一般不会有问题 */
}
/**
* @brief SPI处理数据
* @param handle : SPI句柄
* @param data : 要发送的数据
* @retval t.rx_data[0] : 接收到的数据
*/
uint8_t spi2_transfer_byte(spi_device_handle_t handle, uint8_t data)
{
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA;
t.length = 8;
t.tx_data[0] = data;
spi_device_transmit(handle, &t);
return t.rx_data[0];
}
在 spi2_init()函数中主要工作就是对于 SPI 参数的配置,如 SPI 管脚配置和数据传输大小以及 SPI 总线配置等,通过该函数就可以完成 SPI 初始化。
SPI 驱动中对 SPI 的各种操作,请读者结合 SPI 的时序规定查看本实验的配套实验源码。
(3)lcd.h 文件
lcd.c 和 lcd.h 文件是驱动函数和引脚接口宏定义以及函数声明等。 lcdfont.h 头文件存放了 4种字体大小不一样的 ASCII 字符集(12*12、 16*16、 24*24 和 32*32)。这个跟 oledfont.h 头文件一样的,只是这里多了 32*32 的 ASCII 字符集,制作方法请回顾 OLED 实验 。下面我们还是先
介绍 lcd.h 文件,首先是 LCD 的引脚定义:
/* 引脚定义 */
#define LCD_NUM_WR GPIO_NUM_40
#define LCD_NUM_CS GPIO_NUM_21
/* IO操作 */
#define LCD_WR(x) do{ x ? \
(gpio_set_level(LCD_NUM_WR, 1)): \
(gpio_set_level(LCD_NUM_WR, 0)); \
}while(0)
#define LCD_CS(x) do{ x ? \
(gpio_set_level(LCD_NUM_CS, 1)): \
(gpio_set_level(LCD_NUM_CS, 0)); \
}while(0)
#define LCD_PWR(x) do{ x ? \
(xl9555_pin_write(SLCD_PWR_IO, 1)): \
(xl9555_pin_write(SLCD_PWR_IO, 0)); \
}while(0)
#define LCD_RST(x) do{ x ? \
(xl9555_pin_write(SLCD_RST_IO, 1)): \
(xl9555_pin_write(SLCD_RST_IO, 0)); \
}while(0)
/* 常用颜色值 */
#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 */
/* 非常用颜色 */
#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 /* 浅棕蓝色(选择条目的反色) */
/* 扫描方向定义 */
#define L2R_U2D 0 /* 从左到右,从上到下 */
#define L2R_D2U 1 /* 从左到右,从下到上 */
#define R2L_U2D 2 /* 从右到左,从上到下 */
#define R2L_D2U 3 /* 从右到左,从下到上 */
#define U2D_L2R 4 /* 从上到下,从左到右 */
#define U2D_R2L 5 /* 从上到下,从右到左 */
#define D2U_L2R 6 /* 从下到上,从左到右 */
#define D2U_R2L 7 /* 从下到上,从右到左 */
#define DFT_SCAN_DIR L2R_U2D /* 默认的扫描方向 */
/* 屏幕选择 */
#define LCD_320X240 0
#define LCD_240X240 1
/* LCD信息结构体 */
typedef struct _lcd_obj_t
{
uint16_t width; /* 宽度 */
uint16_t height; /* 高度 */
uint8_t dir; /* 横屏还是竖屏控制:0,竖屏;1,横屏。 */
uint16_t wramcmd; /* 开始写gram指令 */
uint16_t setxcmd; /* 设置x坐标指令 */
uint16_t setycmd; /* 设置y坐标指令 */
uint16_t wr; /* 命令/数据IO */
uint16_t cs; /* 片选IO */
} lcd_obj_t;
/* LCD缓存大小设置,修改此值时请注意!!!!修改这两个值时可能会影响以下函数 lcd_clear/lcd_fill/lcd_draw_line */
#define LCD_TOTAL_BUF_SIZE (320 * 240 * 2)
#define LCD_BUF_SIZE 15360
/* 导出相关变量 */
extern lcd_obj_t lcd_self;
extern uint8_t lcd_buf[LCD_TOTAL_BUF_SIZE];
第一部分的宏定义是对 WR/CS 引脚的定义,第二部分宏定义是 LCD_WR/CS/PWR/RST 引脚操作的定义,接下来的部分是对一些常用颜色的 RGB 数值以及 LCD 信息结构体的定义。
(4)lcd.c 文件
#define SPI_LCD_TYPE 1 /* SPI接口屏幕类型(1:2.4寸SPILCD 0:1.3寸SPILCD) */
spi_device_handle_t MY_LCD_Handle;
uint8_t lcd_buf[LCD_TOTAL_BUF_SIZE];
lcd_obj_t lcd_self;
/* LCD需要初始化一组命令/参数值。它们存储在此结构中 */
typedef struct
{
uint8_t cmd;
uint8_t data[16];
uint8_t databytes; /* 数据中没有数据;比特7=设置后的延迟;0xFF=cmds结束 */
} lcd_init_cmd_t;
/**
* @brief 发送命令到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param cmd 传输的8位命令数据
* @retval 无
*/
void lcd_write_cmd(const uint8_t cmd)
{
LCD_WR(0);
spi2_write_cmd(MY_LCD_Handle, cmd);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的8位数据
* @retval 无
*/
void lcd_write_data(const uint8_t *data, int len)
{
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, data, len);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的16位数据
* @retval 无
*/
void lcd_write_data16(uint16_t data)
{
uint8_t dataBuf[2] = {0,0};
dataBuf[0] = data >> 8;
dataBuf[1] = data & 0xFF;
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, dataBuf,2);
}
/**
* @brief 设置窗口大小
* @param xstar:左上角x轴
* @param ystar:左上角y轴
* @param xend:右下角x轴
* @param yend:右下角y轴
* @retval 无
*/
void lcd_set_window(uint16_t xstar, uint16_t ystar,uint16_t xend,uint16_t yend)
{
uint8_t databuf[4] = {0,0,0,0};
databuf[0] = xstar >> 8;
databuf[1] = 0xFF & xstar;
databuf[2] = xend >> 8;
databuf[3] = 0xFF & xend;
lcd_write_cmd(lcd_self.setxcmd);
lcd_write_data(databuf, 4);
databuf[0] = ystar >> 8;
databuf[1] = 0xFF & ystar;
databuf[2] = yend >> 8;
databuf[3] = 0xFF & yend;
lcd_write_cmd(lcd_self.setycmd);
lcd_write_data(databuf, 4);
lcd_write_cmd(lcd_self.wramcmd); /* 开始写入GRAM */
}
/**
* @brief 以一种颜色清空LCD屏
* @param color 清屏颜色
* @retval 无
*/
void lcd_clear(uint16_t color)
{
uint16_t i, j;
uint8_t data[2] = {0}; // 准备一个2字节的数组
// 将16位的color拆分成两个8位数据(高位和低位)
data[0] = color >> 8; // 获取颜色值的高8位
data[1] = color; // 获取颜色值的低8位 (发生截断,实际取低8位)
// 设置LCD的写入窗口为整个屏幕
// 从(0, 0)到(lcd_self.width-1, lcd_self.height-1),即左上角到右下角
lcd_set_window(0, 0, lcd_self.width - 1, lcd_self.height - 1);
// 填充缓冲区 lcd_buf(重点理解循环次数)
// LCD_BUF_SIZE / 2:因为我们要填充的是16位的颜色,但操作是以8位为单位。
// 所以缓冲区大小15360字节,对应 15360 / 2 = 7680 个16位颜色值。
for(j = 0; j < LCD_BUF_SIZE / 2; j++)
{
// 将缓冲区中每一对字节都设置为目标颜色的高8位和低8位
lcd_buf[j * 2] = data[0]; // 偶数索引位置放颜色高位
lcd_buf[j * 2 + 1] = data[1]; // 奇数索引位置放颜色低位
}
// 此时,lcd_buf这个数组已经被完全填充为重复的color值了。
// 分批次将缓冲区数据写入LCD
// LCD_TOTAL_BUF_SIZE / LCD_BUF_SIZE = 153600 / 15360 = 10
// 所以这个循环会执行10次
for(i = 0; i < (LCD_TOTAL_BUF_SIZE / LCD_BUF_SIZE); i++)
{
// 每次写入LCD_BUF_SIZE(15360)字节的数据。
// 由于之前设置了窗口,数据会连续地填充到屏幕GRAM中。
lcd_write_data(lcd_buf, LCD_BUF_SIZE);
}
}
/**
* @brief 在指定区域内填充单个颜色
* @param (sx,sy),(ex,ey):填充矩形对角坐标,区域大小为:(ex - sx + 1) * (ey - sy + 1)
* @param color:要填充的颜色(32位颜色,方便兼容LTDC)
* @retval 无
*/
void lcd_fill(uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey, uint16_t color)
{
uint16_t i;
uint16_t j;
uint16_t width;
uint16_t height;
width = ex - sx + 1;
height = ey - sy + 1;
lcd_set_window(sx, sy, ex, ey);
for (i = 0; i < height; i++)
{
for (j = 0; j < width; j++)
{
lcd_write_data16(color);
}
}
lcd_set_window(sx, sy, ex, ey);
}
/**
* @brief 设置光标的位置
* @param Xpos:左上角x轴
* @param Ypos:左上角y轴
* @retval 无
*/
void lcd_set_cursor(uint16_t xpos, uint16_t ypos)
{
lcd_set_window(xpos,ypos,xpos,ypos);
}
/**
* @brief 设置LCD的自动扫描方向(对RGB屏无效)
* @param dir:0~7,代表8个方向(具体定义见lcd.h)
* @retval 无
*/
void lcd_scan_dir(uint8_t dir)
{
uint8_t regval = 0;
uint8_t dirreg = 0;
uint16_t temp;
/* 横屏时,对1963不改变扫描方向, 其他IC改变扫描方向!竖屏时1963改变方向, 其他IC不改变扫描方向 */
if (lcd_self.dir == 1)
{
dir = 5;
}
/* 根据扫描方式 设置 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; /* 存储器访问控制命令,对绝大部分驱动IC, 由0X36寄存器控制 */
uint8_t date_send[1] = {regval};
lcd_write_cmd(dirreg);
lcd_write_data(date_send, 1);
if (regval & 0x20)
{
if (lcd_self.width < lcd_self.height) /* 交换X,Y */
{
temp = lcd_self.width;
lcd_self.width = lcd_self.height;
lcd_self.height = temp;
}
}
else
{
if (lcd_self.width > lcd_self.height) /* 交换X,Y */
{
temp = lcd_self.width;
lcd_self.width = lcd_self.height;
lcd_self.height = temp;
}
}
lcd_set_window(0, 0, lcd_self.width,lcd_self.height);
}
/**
* @brief 设置LCD显示方向
* @param dir:0,竖屏; 1,横屏
* @retval 无
*/
void lcd_display_dir(uint8_t dir)
{
lcd_self.dir = dir;
if (lcd_self.dir == 0) /* 竖屏 */
{
lcd_self.width = 240;
lcd_self.height = 320;
lcd_self.wramcmd = 0X2C; /* 向由 列地址(CASET) 和 行地址(RASET) 设定的区域连续写入像素数据 */
lcd_self.setxcmd = 0X2A; /* 设置要在哪个水平(X方向)区域写入像素数据 */
lcd_self.setycmd = 0X2B; /* 设置要在哪个垂直(Y方向)区域写入像素数据 */
}
else /* 横屏 */
{
lcd_self.width = 320; /* 默认宽度 */
lcd_self.height = 240; /* 默认高度 */
lcd_self.wramcmd = 0X2C; /* 向由 列地址(CASET) 和 行地址(RASET) 设定的区域连续写入像素数据 */
lcd_self.setxcmd = 0X2A; /* 设置要在哪个水平(X方向)区域写入像素数据 */
lcd_self.setycmd = 0X2B; /* 设置要在哪个垂直(Y方向)区域写入像素数据 */
}
lcd_scan_dir(DFT_SCAN_DIR); /* 默认扫描方向 */
}
/**
* @brief 硬件复位
* @param self_in:LCD结构体
* @retval 无
*/
void lcd_hard_reset(void)
{
/* 复位显示屏 */
LCD_RST(0);
vTaskDelay(100);
LCD_RST(1);
vTaskDelay(100);
}
/**
* @brief 绘画一个像素点
* @param self_in:LCD结构体
* @param x:x轴坐标
* @param y:y轴坐标
* @param color:颜色值
* @retval 无
*/
void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color)
{
lcd_set_cursor(x, y);
lcd_write_data16(color);
}
/**
* @brief 画线函数(直线、斜线)
* @param x1,y1 起点坐标
* @param x2,y2 终点坐标
* @param color 填充颜色
* @retval 无
*/
void lcd_draw_line(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
uint16_t 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_draw_pixel(urow,ucol,color); /* 画点 */
xerr += delta_x ;
yerr += delta_y ;
if(xerr>distance)
{
xerr -= distance;
urow += incx;
}
if (yerr > distance)
{
yerr -= distance;
ucol += incy;
}
}
}
/**
* @brief 画水平线
* @param x0,y0: 起点坐标
* @param len : 线长度
* @param color: 矩形的颜色
* @retval 无
*/
void lcd_draw_hline(uint16_t x, uint16_t y, uint16_t len, uint16_t color)
{
if ((len == 0) || (x > lcd_self.width) || (y > lcd_self.height))return;
lcd_fill(x, y, x + len - 1, y, color);
}
/**
* @brief 画一个矩形
* @param x1,y1 起点坐标
* @param x2,y2 终点坐标
* @param color 填充颜色
* @retval 无
*/
void lcd_draw_rectangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1,uint16_t color)
{
lcd_draw_line(x0, y0, x1, y0,color);
lcd_draw_line(x0, y0, x0, y1,color);
lcd_draw_line(x0, y1, x1, y1,color);
lcd_draw_line(x1, y0, x1, y1,color);
}
/**
* @brief 画一个圆
* @param x0,y0 圆心坐标
* @param r 圆半径
* @param color 填充颜色
* @retval 无
*/
void lcd_draw_circle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color)
{
int a, b;
int di;
a = 0;
b = r;
di = 3 - (r << 1);
while (a <= b)
{
lcd_draw_pixel(x0 - b, y0 - a, color);
lcd_draw_pixel(x0 + b, y0 - a, color);
lcd_draw_pixel(x0 - a, y0 + b, color);
lcd_draw_pixel(x0 - b, y0 - a, color);
lcd_draw_pixel(x0 - a, y0 - b, color);
lcd_draw_pixel(x0 + b, y0 + a, color);
lcd_draw_pixel(x0 + a, y0 - b, color);
lcd_draw_pixel(x0 + a, y0 + b, color);
lcd_draw_pixel(x0 - b, y0 + a, color);
a++;
if (di < 0)
{
di += 4 * a + 6;
}
else
{
di += 10 + 4 * (a - b);
b--;
}
lcd_draw_pixel(x0 + a, y0 + b, color);
}
}
/**
* @brief 在指定位置显示一个字符
* @param x,y : 坐标
* @param chr : 要显示的字符:" "--->"~"
* @param size : 字体大小 12/16/24/32
* @param mode : 叠加方式(1); 非叠加方式(0);
* @param color : 字符的颜色;
* @retval 无
*/
void lcd_show_char(uint16_t x, uint16_t y, uint8_t chr, uint8_t size, uint8_t mode, uint16_t color)
{
uint8_t temp = 0, t1 = 0, t = 0;
uint8_t *pfont = 0;
uint8_t csize = 0; /* 得到字体一个字符对应点阵集所占的字节数 */
uint16_t colortemp = 0;
uint8_t sta = 0;
csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2); /* 得到字体一个字符对应点阵集所占的字节数 */
chr = chr - ' '; /* 得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库) */
if ((x > (lcd_self.width - size / 2)) || (y > (lcd_self.height - size)))
{
return;
}
lcd_set_window(x, y, x + size / 2 - 1, y + size - 1); /* (x,y,x+8-1,y+16-1) */
switch (size)
{
case 12:
pfont = (uint8_t *)asc2_1206[chr]; /* 调用1206字体 */
sta = 6;
break;
case 16:
pfont = (uint8_t *)asc2_1608[chr]; /* 调用1608字体 */
sta = 8;
break;
case 24:
pfont = (uint8_t *)asc2_2412[chr]; /* 调用2412字体 */
break;
case 32:
pfont = (uint8_t *)asc2_3216[chr]; /* 调用3216字体 */
sta = 8;
break;
default:
return ;
}
if (size != 24)
{
csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);
for (t = 0; t < csize; t++)
{
temp = pfont[t]; /* 获取字符的点阵数据 */
for (t1 = 0; t1 < sta; t1++)
{
if (temp & 0x80)
{
colortemp = color;
}
else if (mode == 0) /* 无效点,不显示 */
{
colortemp = 0xFFFF;
}
lcd_write_data16(colortemp);
temp <<= 1;
}
}
}
else
{
csize = (size * 16) / 8;
for (t = 0; t < csize; t++)
{
temp = asc2_2412[chr][t];
if (t % 2 == 0)
{
sta = 8;
}
else
{
sta = 4;
}
for (t1 = 0; t1 < sta; t1++)
{
if(temp & 0x80)
{
colortemp = color;
}
else if (mode == 0) /* 无效点,不显示 */
{
colortemp = 0xFFFF;
}
lcd_write_data16(colortemp);
temp <<= 1;
}
}
}
}
/**
* @brief m^n函数
* @param m,n 输入参数
* @retval m^n次方
*/
uint32_t lcd_pow(uint8_t m, uint8_t n)
{
uint32_t result = 1;
while(n--)result *= m;
return result;
}
/**
* @brief 显示len个数字
* @param x,y : 起始坐标
* @param num : 数值(0 ~ 2^32)
* @param len : 显示数字的位数
* @param size: 选择字体 12/16/24/32
* @retval 无
*/
void lcd_show_num(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint16_t color)
{
uint8_t t, temp;
uint8_t 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_show_char(x + (size / 2)*t, y, ' ', size, 0, color); /* 显示空格,占位 */
continue; /* 继续下个一位 */
}
else
{
enshow = 1; /* 使能显示 */
}
}
lcd_show_char(x + (size / 2)*t, y, temp + '0', size, 0, color); /* 显示字符 */
}
}
/**
* @brief 扩展显示len个数字(高位是0也显示)
* @param x,y : 起始坐标
* @param num : 数值(0 ~ 2^32)
* @param len : 显示数字的位数
* @param size: 选择字体 12/16/24/32
* @param mode: 显示模式
* [7]:0,不填充;1,填充0.
* [6:1]:保留
* [0]:0,非叠加显示;1,叠加显示.
* @param color : 数字的颜色;
* @retval 无
*/
void lcd_show_xnum(uint16_t x, uint16_t y, uint32_t num, uint8_t len, uint8_t size, uint8_t mode, uint16_t color)
{
uint8_t t, temp;
uint8_t 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) /* 高位需要填充0 */
{
lcd_show_char(x + (size / 2)*t, y, '0', size, mode & 0X01, color); /* 用0占位 */
}
else
{
lcd_show_char(x + (size / 2)*t, y, ' ', size, mode & 0X01, color); /* 用空格占位 */
}
continue;
}
else
{
enshow = 1; /* 使能显示 */
}
}
lcd_show_char(x + (size / 2)*t, y, temp + '0', size, mode & 0X01, color);
}
}
/**
* @brief 显示字符串
* @param x,y : 起始坐标
* @param width,height: 区域大小
* @param size : 选择字体 12/16/24/32
* @param p : 字符串首地址
* @retval 无
*/
void lcd_show_string(uint16_t x, uint16_t y, uint16_t width, uint16_t height, uint8_t size, char *p, uint16_t color)
{
uint8_t x0 = x;
width += x;
height += y;
while ((*p <= '~') && (*p >= ' ')) /* 判断是不是非法字符! */
{
if (x >= width)
{
x = x0;
y += size;
}
if (y >= height) break; /* 退出 */
lcd_show_char(x, y, *p, size, 0, color);
x += size / 2;
p++;
}
}
/**
* @brief 打开LCD
* @param self_in:SPI控制块
* @retval mp_const_none:初始化成功
*/
void lcd_on(void)
{
LCD_PWR(1);
vTaskDelay(10);
}
/**
* @brief 关闭LCD
* @param self_in:SPI控制块
* @retval mp_const_none:初始化成功
*/
void lcd_off(void)
{
LCD_PWR(0);
vTaskDelay(10);
}
/**
* @brief LCD初始化
* @param 无
* @retval 无
*/
void lcd_init(void)
{
int cmd = 0;
esp_err_t ret = 0;
lcd_self.dir = 0; /* 0 -> 竖屏 */
lcd_self.wr = LCD_NUM_WR; /* 配置WR引脚 */
lcd_self.cs = LCD_NUM_CS; /* 配置CS引脚 */
gpio_config_t gpio_init_struct;
/* SPI驱动接口配置 */
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 60 * 1000 * 1000, /* SPI时钟 */
.mode = 0, /* SPI模式0 */
.spics_io_num = lcd_self.cs, /* SPI设备引脚 */
.queue_size = 7, /* 事务队列尺寸 7个 */
};
/* 添加SPI总线设备 */
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &MY_LCD_Handle); /* 配置SPI总线设备 */
ESP_ERROR_CHECK(ret);
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */
gpio_init_struct.mode = GPIO_MODE_OUTPUT; /* 配置输出模式 */
gpio_init_struct.pin_bit_mask = 1ull << lcd_self.wr; /* 配置引脚位掩码 */
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能下拉 */
gpio_config(&gpio_init_struct); /* 引脚配置 */
lcd_hard_reset(); /* LCD硬件复位 */
/* 初始化代码 */
#if SPI_LCD_TYPE /* 对2.4寸LCD寄存器进行设置 */
lcd_init_cmd_t ili_init_cmds[] =
{
{0x11, {0}, 0x80}, /* 0x11让芯片从睡眠模式唤醒,进入正常工作模式,芯片手册需要等待120ms */
{0x36, {0x00}, 1}, /* 0x36 控制显示方向(旋转、镜像)、颜色顺序(RGB/BGR)和扫描方式 */
{0x3A, {0x65}, 1}, /* 0x3A 设置像素数据的颜色深度和格式,即每个像素点用多少位来表示,0x65的6(110)为262K,5表示16bit表示一个像素 */
{0X21, {0}, 0x80}, /* 开启显示颜色反转。*/
{0x29, {0}, 0x80}, /*开启显示,使得显存内容能够呈现在屏幕上*/
{0, {0}, 0xff},
};
#else /* 不为0则视为使用1.3寸SPILCD屏,那么屏幕将不会反显 */
lcd_init_cmd_t ili_init_cmds[] =
{
{0x11, {0}, 0x80},
{0x36, {0x00}, 1},
{0x3A, {0x65}, 1},
{0xB2, {0x0C, 0x0C, 0x00, 0x33,0x33}, 5},
{0xB7, {0x75}, 1},
{0xBB, {0x1C}, 1},
{0xC0, {0x2c}, 1},
{0xC2, {0x01}, 1},
{0xC3, {0x0F}, 1},
{0xC4, {0x20}, 1},
{0xC6, {0X01}, 1},
{0xD0, {0xA4,0xA1}, 2},
{0xE0, {0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23}, 14},
{0xE1, {0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23}, 14},
{0X21, {0}, 0x80},
{0x29, {0}, 0x80},
{0, {0}, 0xff},
};
#endif
/* 循环发送设置所有寄存器 */
while (ili_init_cmds[cmd].databytes != 0xff)
{
lcd_write_cmd(ili_init_cmds[cmd].cmd);
lcd_write_data(ili_init_cmds[cmd].data, ili_init_cmds[cmd].databytes & 0x1F);
if (ili_init_cmds[cmd].databytes & 0x80)
{
vTaskDelay(120);
}
cmd++;
}
lcd_display_dir(1); /* 设置屏幕方向 */
LCD_PWR(1);
lcd_clear(WHITE); /* 清屏 */
}
从上的代码中可以看出,本章实验的 SPILCD 驱动是兼容了 1.3 寸与 2.4 寸SPILCD 模块的,因此在加载完 SPI 设备后,会与 SPILCD 进行通讯,确定 SPILCD 的型号,然后根据型号针对性地对 SPILCD 模块进行配置。
SPILCD 驱动中与 SPILCD 模块通讯的函数,如下所示:
/**
* @brief 发送命令到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param cmd 传输的8位命令数据
* @retval 无
*/
void lcd_write_cmd(const uint8_t cmd)
{
LCD_WR(0);
spi2_write_cmd(MY_LCD_Handle, cmd);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的8位数据
* @retval 无
*/
void lcd_write_data(const uint8_t *data, int len)
{
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, data, len);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的16位数据
* @retval 无
*/
void lcd_write_data16(uint16_t data)
{
uint8_t dataBuf[2] = {0,0};
dataBuf[0] = data >> 8;
dataBuf[1] = data & 0xFF;
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, dataBuf,2);
}
在上述代码中, lcd_write_cmd()和 lcd_write_data()在调用 spi 的驱动函数前,按照 LCD 时序图,前者需要先将 WR 引脚电平信号置 0,后者则需要置 1。
通过上面介绍的驱动函数就能够与 SPILCD模块进行通讯了,而在 SPILCD模块的显示屏上显示出特定的图案或字符或设置 SPILCD模块的显示方向等等的操作都是能够通过 SPILCD模块规定的特定命令来完成的。
下面再理解下函数void lcd_clear(uint16_t color),具体已在函数中做了详细说明,下面讨论为什么这么实现?
(1)为何要分批次写入?
微控制器的可用内存(RAM)可能有限,一次性分配153600字节的缓冲区(对于320x240的16位色屏需要150KB)可能不现实。而分配一个15KB的缓冲区(LCD_BUF_SIZE),然后循环使用10次,大大降低了对MCU RAM的需求。
许多LCD驱动芯片(如ST7789, ILI9341等)支持连续写入(lcd_set_window设置了窗口后,后续写入的数据会自动填充该窗口)。所以只需要分批发送数据即可,无需重复设置地址。
(2)为什么先填充缓冲区?
如果直接在循环里调用 lcd_write_data(&color, 2)来逐个像素写入,虽然简单,但效率极低,因为每次写入都有函数调用和通信开销(如SPI或8080并行的时序控制)。
现在的方式是:先用一个紧凑的循环在内存中准备好一大块数据(填充缓冲区),然后一次性发送这一大块数据。这显著减少了函数调用次数和数据传输的次数,大大提高清屏速度。
22.3.4 CMakeLists.txt 文件
打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示
set(src_dirs
IIC
LCD
LED
SPI
XL9555)
set(include_dirs
IIC
LCD
LED
SPI
XL9555)
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
22.3.5 实验应用代码
打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
uint8_t x = 0;
esp_err_t ret;
ret = nvs_flash_init(); /* 初始化NVS */
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
led_init(); /* 初始化LED */
i2c0_master = iic_init(I2C_NUM_0); /* 初始化IIC0 */
spi2_init(); /* 初始化SPI2 */
xl9555_init(i2c0_master); /* IO扩展芯片初始化 */
lcd_init(); /* 初始化LCD */
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;
}
}
lcd_show_string(10, 40, 240, 32, 32, "ESP32", RED);
lcd_show_string(10, 80, 240, 24, 24, "SPILCD TEST", RED);
lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(10, 150, 240, 12, 12, "ATOM@ALIENTEK", RED);
x++;
if (x == 12)
{
x = 0;
}
LED_TOGGLE();
vTaskDelay(500);
}
}
从上面的代码中可以看出,在初始化完LCD后,便在LCD上显示一些本实验的相关信息,随后便每间隔 500 毫秒就更换一次 LCD 屏幕显示的背景色。
22.4 下载验证
在完成编译和烧录操作后,可以看到 SPI LCD 上不断变换着不同的颜色, LED 灯闪烁。

图 22.4.1 SPI LCD 显示效果图
更多推荐



所有评论(0)