第十七章 SPI_LCD实验
本章,我们将学习ESP32-S3的硬件SPI接口,将会大家如何使用SPI接口去驱动LCD屏。在本章中,实现和LCD屏之间的通信,实现ASCII字符、彩色、图片和图形的显示。
本章分为如下几个小节:
17.1 SPI及LCD介绍
17.2 硬件设计
17.3 软件设计
17.4 下载验证
17.1 SPI及LCD介绍
17.1.1 SPI介绍
SPI,Serial Peripheral interface,顾名思义,就是串行外围设备接口,是由原摩托罗拉公司在其MC68HCXX系列处理器上定义的。SPI是一种高速的全双工、同步、串行的通信总线,已经广泛应用在众多MCU、存储芯片、AD转换器和LCD之间。
SPI通信跟IIC通信一样,通信总线上允许挂载一个主设备和一个或者多个从设备。为了跟从设备进行通信,一个主设备至少需要4跟数据线,分别为:
MOSI(Master Out / Slave In):主数据输出,从数据输入,用于主机向从机发送数据。
MISO(Master In / Slave Out):主数据输入,从数据输出,用于从机向主机发送数据。
SCLK(Serial Clock):时钟信号,由主设备产生,决定通信的速率。
CS(Chip Select):从设备片选信号,由主设备产生,低电平时选中从设备。
多从机SPI通信网络连接如下图所示。

图 1 7.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种模式对比图,如下图所示。

图17.1.1.2 SPI的4种模式对比图
1)模式0,CPOL=0,CPHA=0;空闲时,SCL处于低电平,数据采样在第1个边沿,即SCL由低电平到高电平的跳变,数据采样在上升沿,数据发送在下降沿。
2)模式1,CPOL=0,CPHA=1;空闲时,SCL处于低电平,数据采样在第2个边沿,即SCL由高电平到低电平的跳变,数据采样在下升沿,数据发送在上降沿。
3)模式2,CPOL=1,CPHA=0;空闲时,SCL处于高电平,数据采样在第1个边沿,即SCL由高电平到低电平的跳变,数据采样在下升沿,数据发送在上降沿。
4)模式3,CPOL=1,CPHA=1;空闲时,SCL处于高电平,数据采样在第2个边沿,即SCL由低电平到高电平的跳变,数据采样在上升沿,数据发送在下降沿。
17.1.2 SPI控制器介绍
ESP32-S3芯片集成了四个SPI控制器,分别为SPI0、SPI1、SPI2和SPI3。SPI0和SPI1控制器主要供内部使用以访问外部FLASH和PSRAM,所以只能使用SPI2和SPI3。SPI2又称为HSPI,而SPI3又称为VSPI,这两个属于GP-SPI。
GP-SPI特性:
支持主机模式和从机模式
支持半双工通信和全双工通信
支持多种数据模式:
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模式
时钟频率可配置:
在主机模式下:时钟频率可达80MHz
在从机模式下:时钟频率可达60MHz
数据位的读写顺序可配置
时钟极性和相位可配置
四种SPI时钟模式:模式0 ~ 模式3
在主机模式下,提供多条CS线
SPI2:CS0 ~ CS5
SPI3:CS0 ~ CS2
支持访问SPI接口的传感器、显示屏控制器、flash或RAM芯片
SPI2和SPI3接口相关信号线可以经过GPIO交换矩阵和IO_MUX实现与芯片引脚的映射,IO使用起来非常灵活。
17.1.3 LCD介绍
本例程仅支持两款屏幕,一款是正点原子的1.3寸显示模块ATK-MD0130,另一款是正点原子2.4寸显示模块ATK-MD0240。这两款显示模块的LCD分辨率分别为240*240和320*240,支持16位真彩色显示。模块采用ST7789V作为LCD的驱动芯片,该芯片自带RAM,无需外加驱动器或存储器。使用主控芯片的SPI接口就可以很轻松地驱动这两个显示模块。
两款显示模块的外观,如下图所示。

图17.1.3.1 显示模块实物图
模块的原理图,如下图所示。

图17.1.3.2 ATK-MD0130模块原理图
模块通过一个2*4的排针(2.54mm间距)同外部电路连接,各引脚的详细描述,如下表所示。

表17.1.3.1显示模块引脚说明
显示模块采用ST7789V作为LCD驱动器,LCD的显存可直接存放在ST7789V的片上RAM中,ST7789V的片上RAM有240*320*3字节,并且ST7789V会在没有外部时钟的情况下,自动将其片上RAM的数据显示至LCD上,以最小化功耗。
ST7789V最高支持18位色深(262K色),但一般在显示模块上使用16位色深(65K色)的RGB565格式,这样可以在16位色深下达到最快的速度。在16位色深模式下,ST7789V采用RGB565格式传输、存储颜色数据,如下图所示。

图17.1.3.3 16位色深模式(RGB565)传输颜色数据
如上图所示,是一个传输像素数据的时序过程,D/CX线即前面提及的WR线需要拉高,表示传输的是数据。一个像素的颜色数据需要使用16比特来传输,这16比特数据中,高5比特用于表示红色,低5比特用于表示蓝色,中间的6比特用于表示绿色。数据的数值越大,对应表示的颜色就越深。
ST7789V支持连续读写RAM中存放的LCD上颜色对应的数据,并且连续读写的方向(LCD的扫描方向)是可以通过命令0x36进行配置的,如下图所示。

图17.1.3.4 命令0x36
从上图中可以看出,命令0x36可以配置6个参数,但对于配置LCD的扫描方向,仅需关心MY、MX和MV这三个参数,如下表所示。

表17.1.3.2 命令0x36配置LCD扫描方向
这样一来,就能够大大地提高ATK-MD0130和ATK-MD0240模块在刷屏时的效率,仅需设置一次坐标,然后连续地往ATK-MD0130和ATK-MD0240模块传输颜色数据即可。
在往ATK-MD0130和ATK-MD0240模块写入颜色数据前,还需要设置地址,以确定随后写入的颜色数据对应LCD上的哪一个像素,通过命令0x2A和命令0x2B可以分别设置ATK-MD0130和ATK-MD0240模块显示颜色数据的列地址和行地址,命令0x2A的描述,如下图所示。

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

图17.1.3.6 命令0x2B
以默认的LCD扫描方式(从左到右,从上到下)为例,命令0x2A的参数XS和XE和命令0x2B的参数YS和YE就在LCD上确定了一个区域,在连读读写颜色数据时,ST7789V就会按照从左到右,从上到下的扫描方式读写设个区域的颜色数据。
17.1.4 SPI接口函数介绍
本小节介绍到的函数可在以下文件中找到:
Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\SPI\src\SPI.cpp
在SPI.cpp中已经定义好了两个SPI对象HSPI和VSPI,对应的就是SPI2和SPI3,要想调用SPI库的函数前,必须先定义SPI对象实例选择某个SPI,格式如下:
SPIClass *spi_lcd = new SPIClass(HSPI);
接下来,我们介绍一下本章节所用到的SPI作为主机模式相关函数。
第一个函数:begin函数,该函数功能是初始化SPI接口。
void SPIClass::begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss);
参数sck为SPI总线的时钟线引脚;
参数miso为SPI总线的输入引脚;
参数mosi为SPI总线的输出引脚;
参数ss为SPI总线的片选引脚;
无返回值。
第二个函数:beginTransaction函数,该函数功能是按照settings设定的参数启动SPI通信。注意:采用该函数时,可以不用库中的setBitOrder、setFrequency和setDataMode函数去设置SPI总线的传送方式、传送的时钟频率和时钟的模式。
void SPIClass::beginTransaction(SPISettings settings);
参数settings为SPISettings对象,用来设置SPI通信参数,设置格式为SPISettings(clock, bitOrder, dataMode);
无返回值。
第三个函数:endTransaction函数,该函数功能是结束SPI通信。注意:跟beginTransaction函数是成对出现的。
void SPIClass::endTransaction();
无返回值。
第四个函数:transfer函数,该函数功能是发送一字节数据。
uint8_t SPIClass::transfer(uint8_t data);
参数data为要发送的字节数据;
返回值为接收到的字节数据。
第五个函数:transfer函数,该函数在SPI.cpp文件中有两个。
uint8_t SPIClass::transfer(uint8_t data);
该函数功能是发送一字节数据。
参数data为要发送的字节数据;
返回值为接收到的字节数据。
void SPIClass::transfer(void * data, uint32_t size);
该函数功能是发送指定长度数据。
参数data为发送缓冲区地址;
参数size为要发送的字节数;
无返回值。
第六个函数:transfer16函数,该函数功能是发送一个16位数据。前面提及到使用RGB565格式,所以一个像素点的数据大小就为16位,发送像素数据可以直接用该函数。
uint16_t SPIClass::transfer16(uint16_t data);
参数data为要发送的16位数据;
返回值为接收到的16位数据。
17.2 硬件设计
1. 例程功能
使用开发板的WIRELESS接口连接正点原子SPILCD模块(仅限SPI显示模块),实现SPILCD模块的显示。通过把LCD模块插入底板上的WIRELESS接口,按下复位之后,就可以看到SPILCD模块首先进行纯色刷屏测试,后面就显示LCD例程实验信息以及显示正点原子Logo以及一个3D立方体。
2. 硬件资源
1)USART0
U0TXD-IO43
U0RXD-IO44
2)XL9555
IIC_SDA-IO41
IIC_SCL-IO42
3)SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
3. 原理图
SPILCD原理图,如下图所示。

图17.2.1 SPILCD原理图
17.3 软件设计
17.3.1 程序流程图
下面看看本实验的程序流程图:

图17.3.1.1 程序流程图
17.3.2 程序解析
1. lcd驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SPILCD驱动源码包括三个文件:spilcd.cpp、spilcd.h和font.h。
font.h存放的是ASCII字符点阵数据,有4个字体大小,12号、16号、24号和3号。
下面我们先解析spilcd.h的程序。对LCD相关引脚做了相关定义。
#define SLCD_CS_PIN 21 #define SLCD_SDA_PIN 11 #define SLCD_SCK_PIN 12 #define SLCD_SDI_PIN -1 #define SLCD_WR_PIN 40
我们选择使用IO21作为LCD的片选信号线,IO11作为SPI的MOSI线,IO12作为SPI的SCL线,而SPI的MISO线没有用到即为-1,IO40作为LCD的命令数据选择线。另外还有两个引脚PWR和RST,它们是连接在XL9555器件上的,需要在lcd_init函数中初始化后才能使用。
便于对引脚操作,所以定义了一些宏函数,用于控制引脚输出高低电平。
#define LCD_PWR(x) xl9555_pin_set(SLCD_PWR, x ? IO_SET_HIGH : IO_SET_LOW) #define LCD_RST(x) xl9555_pin_set(SLCD_RST, x ? IO_SET_HIGH : IO_SET_LOW) #define LCD_WR(x) digitalWrite(SLCD_WR_PIN, x) #define LCD_CS(x) digitalWrite(SLCD_CS_PIN, x)
在spilcd.cpp文件中,为了兼容两种分辨率屏幕也定义了一些宏,代码如下:
#define SPI_LCD_TYPE 1 /* SPI接口屏幕类型(1:2.4寸SPILCD 0:1.3寸SPILCD) */ /* LCD的宽和高定义 */ #if SPI_LCD_TYPE /* 2.4寸SPI_LCD屏幕 */ uint16_t spilcd_width = 240; /* 屏幕的宽度 240(竖屏) */ uint16_t spilcd_height = 320; /* 屏幕的宽度 320(竖屏) */ #else uint16_t spilcd_width = 240; /* 屏幕的宽度 240(竖屏) */ uint16_t spilcd_height = 240 /* 屏幕的宽度 240(竖屏) */ #endif /* 1.3寸SPI_LCD屏幕 */
下面,首先先来看一下初始化函数lcd_init,代码如下:
/**
* @brief 初始化spilcd
* @param 无
* @retval 无
*/
void lcd_init(void)
{
/* 初始化LCD屏需要用到的引脚 */
xl9555_io_config(SLCD_PWR, IO_SET_OUTPUT);
xl9555_io_config(SLCD_RST, IO_SET_OUTPUT);
pinMode(SLCD_WR_PIN, OUTPUT);
/* LCD屏需要用到的引脚默认状态为高电平 */
xl9555_pin_set(SLCD_PWR, IO_SET_HIGH);
xl9555_pin_set(SLCD_RST, IO_SET_HIGH);
digitalWrite(SLCD_WR_PIN, HIGH);
/* 对SPI进行配置 */
spi_lcd = new SPIClass(HSPI);
/* 创建SPIClass实例时选择SPI总线(HSPI) */
spi_lcd->begin(SLCD_SCK_PIN, SLCD_SDI_PIN, SLCD_SDA_PIN, SLCD_CS_PIN);
/* 设置SPI的通信线 */
pinMode(SLCD_CS_PIN, OUTPUT); /* 设置CS引脚为输出模式 */
/* 硬件复位 */
LCD_RST(1);
delay(10);
LCD_RST(0);
delay(10);
LCD_RST(1);
delay(120);
spi_lcd->beginTransaction(SPISettings(SPICLK, MSBFIRST, SPI_MODE3));
/* 使用在SPISettings中自定义的配置进行SPI总线初始化 */
LCD_CS(0); /* 拉低片选线,选中设备 */
/* 对LCD的寄存器进行配置 */
#if SPI_LCD_TYPE /* 对2.4寸LCD寄存器进行设置 */
lcd_write_cmd(0x11); /* Sleep Out */
delay(120); /* wait for power stability */
lcd_write_cmd(0x3A); /* 65k mode */
lcd_write_data(0x05);
lcd_write_cmd(0xC5); /* VCOM */
lcd_write_data(0x1A);
lcd_write_cmd(0x36); /* 屏幕显示方向设置 */
lcd_write_data(0x00);
/*-------------ST7789V Frame rate setting-----------*/
lcd_write_cmd(0xB2); /* Porch Setting */
lcd_write_data(0x05);
lcd_write_data(0x05);
lcd_write_data(0x00);
lcd_write_data(0x33);
lcd_write_data(0x33);
lcd_write_cmd(0xB7); /* Gate Control */
lcd_write_data(0x05); /* 12.2v -10.43v */
/*--------------ST7789V Power setting---------------*/
lcd_write_cmd(0xBB); /* VCOM */
lcd_write_data(0x3F);
lcd_write_cmd(0xC0); /* Power control */
lcd_write_data(0x2c);
lcd_write_cmd(0xC2); /* VDV and VRH Command Enable */
lcd_write_data(0x01);
lcd_write_cmd(0xC3); /* VRH Set */
lcd_write_data(0x0F); /* 4.3+( vcom+vcom offset+vdv) */
lcd_write_cmd(0xC4); /* VDV Set */
lcd_write_data(0x20); /* 0v */
lcd_write_cmd(0xC6); /* Frame Rate Control in Normal Mode */
lcd_write_data(0X01); /* 111Hz */
lcd_write_cmd(0xD0); /* Power Control 1 */
lcd_write_data(0xA4);
lcd_write_data(0xA1);
lcd_write_cmd(0xE8); /* Power Control 1 */
lcd_write_data(0x03);
lcd_write_cmd(0xE9); /* Equalize time control */
lcd_write_data(0x09);
lcd_write_data(0x09);
lcd_write_data(0x08);
/*---------------ST7789V gamma setting-------------*/
lcd_write_cmd(0xE0); /* Set Gamma */
lcd_write_data(0xD0);
lcd_write_data(0x05);
lcd_write_data(0x09);
lcd_write_data(0x09);
lcd_write_data(0x08);
lcd_write_data(0x14);
lcd_write_data(0x28);
lcd_write_data(0x33);
lcd_write_data(0x3F);
lcd_write_data(0x07);
lcd_write_data(0x13);
lcd_write_data(0x14);
lcd_write_data(0x28);
lcd_write_data(0x30);
lcd_write_cmd(0XE1); /* Set Gamma */
lcd_write_data(0xD0);
lcd_write_data(0x05);
lcd_write_data(0x09);
lcd_write_data(0x09);
lcd_write_data(0x08);
lcd_write_data(0x03);
lcd_write_data(0x24);
lcd_write_data(0x32);
lcd_write_data(0x32);
lcd_write_data(0x3B);
lcd_write_data(0x14);
lcd_write_data(0x13);
lcd_write_data(0x28);
lcd_write_data(0x2F);
lcd_write_cmd(0x20); /* 反显 */
lcd_write_cmd(0x29); /* 开启显示 */
#else /* 对1.3寸LCD寄存器进行设置 */
lcd_write_cmd(0x11); /* Sleep Out */
delay(120); /* wait for power stability */
lcd_write_cmd(0x36); /* Memory Data Access Control */
lcd_write_data(0x00);
lcd_write_cmd(0x3A); /* RGB 5-6-5-bit */
lcd_write_data(0x65);
lcd_write_cmd(0xB2); /* Porch Setting */
lcd_write_data(0x0C);
lcd_write_data(0x0C);
lcd_write_data(0x00);
lcd_write_data(0x33);
lcd_write_data(0x33);
lcd_write_cmd(0xB7); /* Gate Control */
lcd_write_data(0x72);
lcd_write_cmd(0xBB); /* VCOM Setting */
lcd_write_data(0x3D);
lcd_write_cmd(0xC0); /* LCM Control */
lcd_write_data(0x2C);
lcd_write_cmd(0xC2); /* VDV and VRH Command Enable */
lcd_write_data(0x01);
lcd_write_cmd(0xC3); /* VRH Set */
lcd_write_data(0x19);
lcd_write_cmd(0xC4); /* VDV Set */
lcd_write_data(0x20);
lcd_write_cmd(0xC6); /* Frame Rate Control in Normal Mode */
lcd_write_data(0x0F);
lcd_write_cmd(0xD0); /* Power Control 1 */
lcd_write_data(0xA4);
lcd_write_data(0xA1);
lcd_write_cmd(0xE0); /* Positive Voltage Gamma Control */
lcd_write_data(0xD0);
lcd_write_data(0x04);
lcd_write_data(0x0D);
lcd_write_data(0x11);
lcd_write_data(0x13);
lcd_write_data(0x2B);
lcd_write_data(0x3F);
lcd_write_data(0x54);
lcd_write_data(0x4C);
lcd_write_data(0x18);
lcd_write_data(0x0D);
lcd_write_data(0x0B);
lcd_write_data(0x1F);
lcd_write_data(0x23);
lcd_write_cmd(0xE1); /* Negative Voltage Gamma Control */
lcd_write_data(0xD0);
lcd_write_data(0x04);
lcd_write_data(0x0C);
lcd_write_data(0x11);
lcd_write_data(0x13);
lcd_write_data(0x2C);
lcd_write_data(0x3F);
lcd_write_data(0x44);
lcd_write_data(0x51);
lcd_write_data(0x2F);
lcd_write_data(0x1F);
lcd_write_data(0x1F);
lcd_write_data(0x20);
lcd_write_data(0x23);
lcd_write_cmd(0x21); /* Display Inversion On */
lcd_write_cmd(0x29);
#endif
LCD_CS(1); /* 拉高片选线,取消选中 */
spi_lcd->endTransaction(); /* 结束SPI传输 */
lcd_display_dir(1); /* 默认为横屏 */
lcd_display_on(); /* 开启LCD背光 */
lcd_clear(WHITE); /* 清屏 */
}
在lcd_init初始化函数中,先对与LCD连接相关IO进行初始化,之后就是对SPI进行配置,根据SPI_LCD_TYPE宏执行2.4寸LCD的初始化代码还是执行1.3寸 LCD的初始化代码,最后设置横屏显示,开启背光,清屏。
注意:通过SPI发送数据步骤如下:
spi_lcd->beginTransaction(SPISettings(SPICLK, MSBFIRST, SPI_MODE3)); LCD_CS(0); /* 拉低片选线,选中设备 */ ……(SPI可以发送数据或命令,WR高电平表示发送数据,WR低电平表示发送命令) LCD_CS(1); /* 拉高片选线,取消选中 */ spi_lcd->endTransaction(); /* 结束SPI传输 */
SPI发送数据函数针对场景不同,定义了四个,代码如下:
/**
* @brief 往LCD写命令
* @param cmd:命令
* @retval 无
*/
static void lcd_write_cmd(uint8_t cmd)
{
LCD_WR(0);
spi_lcd->transfer(cmd);
}
/**
* @brief 往LCD写数据
* @param data:数据
* @retval 无
*/
static void lcd_write_data(uint8_t data)
{
LCD_WR(1);
spi_lcd->transfer(data);
}
/**
* @brief 往LCD写指定数量的数据
* @param data:数据的起始地址
* @param size:发送数据大小
* @return void
*/
static void lcd_write_bytes(uint8_t *data, uint32_t size)
{
LCD_WR(1);
spi_lcd->transfer(data, size);
}
/**
* @brief 往LCD写像素数据
* @param data:像素数据
* @retval 无
*/
static void lcd_write_pixeldata(uint16_t data)
{
LCD_WR(1);
spi_lcd->transfer16(data);
}
lcd_write_cmd函数主要是用于向LCD驱动IC发送命令;lcd_write_data函数主要是用于向LCD驱动IC发送数据;lcd_write_bytes函数也是向LCD驱动IC发送数据,只不过是可以批量发送数据,用于图像数据的发送;lcd_write_pixeldata函数也是向LCD驱动IC发送数据,只不过是发送16位数据,用作像素数据发送。
接下来,介绍一下设置显示区域大小函数,代码如下:
/**
* @brief 设置LCD行列地址
* @param xs: 列起始地址
* ys: 行起始地址
* xe: 列结束地址
* ye: 行结束地址
* @retval 无
*/
void lcd_set_address(uint16_t xs, uint16_t ys, uint16_t xe, uint16_t ye)
{
lcd_write_cmd(0x2A);
lcd_write_data((uint8_t)(xs >> 8) & 0xFF);
lcd_write_data((uint8_t)xs & 0xFF);
lcd_write_data((uint8_t)(xe >> 8) & 0xFF);
lcd_write_data((uint8_t)xe & 0xFF);
lcd_write_cmd(0x2B);
lcd_write_data((uint8_t)(ys >> 8) & 0xFF);
lcd_write_data((uint8_t)ys & 0xFF);
lcd_write_data((uint8_t)(ye >> 8) & 0xFF);
lcd_write_data((uint8_t)ye & 0xFF);
lcd_write_cmd(0x2C);
}
该函数是对LCD显示区域进行设置,调用该函数时,LCD的当前操作点设置到指定坐标(xs, ys)即光标,并可以准备写GRAM,也就是填充像素数据。注意:当我们要画像素点时,需要先调该函数。
接下来,介绍一下画点函数,其定义如下:
/**
* @brief LCD画点
* @param x : 待画点的X坐标
* y : 待画点的Y坐标
* color: 待画点的颜色
* @retval 无
*/
void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color)
{
spi_lcd->beginTransaction(SPISettings(SPICLK, MSBFIRST, SPI_MODE3));
LCD_CS(0);
lcd_set_address(x, y, x, y);
lcd_write_pixeldata(color);
LCD_CS(1);
spi_lcd->endTransaction();
}
该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。lcd_draw_point函数虽然简单,但是至关重要,其他几乎所有上层函数,都是通过调用这个函数实现的。
接下来要介绍的是字符显示函数lcd_show_char,该函数实现代码如下:
/**
* @brief LCD显示1个字符
* @param x : 待显示字符的X坐标
* y : 待显示字符的Y坐标
* ch : 待显示字符
* font : 待显示字符的字体
* mode : 叠加方式(1); 非叠加方式(0)
* color: 待显示字符的颜色
* @retval 无
*/
void lcd_show_char(uint16_t x, uint16_t y, char ch, lcd_font_t font, uint8_t mode, uint16_t color)
{
const uint8_t *ch_code;
uint8_t ch_width;
uint8_t ch_height;
uint8_t ch_size;
uint8_t ch_offset;
uint8_t byte_index;
uint8_t byte_code;
uint8_t bit_index;
uint8_t width_index = 0;
uint8_t height_index = 0;
ch_offset = ch - ' ';
/* 得到偏移后的值(ASCII字库是从空格开始取模,所以-' '就是对应字符的字库) */
switch (font) /* 获取字体的高度以及宽度 */
{
#if (FONT_12 != 0)
case LCD_FONT_12:
{
ch_code = font_1206[ch_offset];
ch_width = FONT_12_CHAR_WIDTH;
ch_height = FONT_12_CHAR_HEIGHT;
ch_size = FONT_12_CHAR_SIZE;
break;
}
#endif
#if (FONT_16 != 0)
case LCD_FONT_16:
{
ch_code = font_1608[ch_offset];
ch_width = FONT_16_CHAR_WIDTH;
ch_height = FONT_16_CHAR_HEIGHT;
ch_size = FONT_16_CHAR_SIZE;
break;
}
#endif
#if (FONT_24 != 0)
case LCD_FONT_24:
{
ch_code = font_2412[ch_offset];
ch_width = FONT_24_CHAR_WIDTH;
ch_height = FONT_24_CHAR_HEIGHT;
ch_size = FONT_24_CHAR_SIZE;
break;
}
#endif
#if (FONT_32 != 0)
case LCD_FONT_32:
{
ch_code = font_3216[ch_offset];
ch_width = FONT_32_CHAR_WIDTH;
ch_height = FONT_32_CHAR_HEIGHT;
ch_size = FONT_32_CHAR_SIZE;
break;
}
#endif
default:
{
return;
}
}
if ((x + ch_width > spilcd_width) || (y + ch_height > spilcd_height))
{
return;
}
for (byte_index = 0; byte_index < ch_size; byte_index++)
{
byte_code = ch_code[byte_index]; /* 获取字符的点阵数据 */
for (bit_index = 0; bit_index < 8; bit_index++) /* 一个字节8个点 */
{
if ((byte_code & 0x80) != 0) /* 有效点,需要显示 */
{
lcd_draw_point(x + width_index, y + height_index, color);
/* 画点出来,要显示这个点 */
}
else if (mode == 0)
{
lcd_draw_point(x + width_index, y + height_index, g_back_color);
/* 画背景色,相当于这个点不显示(注意背景色由全局变量控制) */
}
width_index++;
if (width_index == ch_width) /* 显示完一列了? */
{
width_index = 0; /* y坐标复位 */
height_index++; /* x坐标递增 */
break;
}
byte_code <<= 1; /* 移位, 以便获取下一个位的状态 */
}
}
}
在lcd_show_char函数里面,我们用到了四个字符集点阵数据数组asc2_1206、asc2_1608、asc2_2412和asc2_3216,通过参数font决定。此外该函数增加以叠加方式显示,或者以非叠加方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。
spilcd.cpp的函数比较多,其余函数请大家自行查看源码,都有详细的解释。
2. 11_spi_lcd.ino代码
在11_spi_lcd.ino里面编写如下代码:
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include "alientek_logo.h"
#include "demo_show.h"
/**
* @brief 当程序开始执行时,将调用setup()函数,通常用来初始化变量、函数等
* @param 无
* @retval 无
*/
void setup()
{
uart_init(0, 115200); /* 串口0初始化 */
xl9555_init(); /* IO扩展芯片初始化 */
lcd_init(); /* LCD初始化 */
/* 刷屏测试 */
lcd_clear(BLACK);
delay(500);
lcd_clear(RED);
delay(500);
lcd_clear(GREEN);
delay(500);
lcd_clear(BLUE);
delay(500);
lcd_clear(YELLOW);
delay(500);
lcd_clear(WHITE);
delay(500);
lcd_show_pic(0, 0, 240, 82, ALIENTEK_LOGO);
/* LCD显示ALIENTEK图片 */
lcd_show_string(10, 100, 200, 32, LCD_FONT_32, "ESP32-S3", RED);
/* LCD显示32号字体ESP32S3 */
lcd_show_string(10, 132, 200, 24, LCD_FONT_24, "TFTLCD TEST", RED);
/* LCD显示32号字体TFTLCD TEST */
lcd_show_string(10, 156, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
/* LCD显示32号字体ATOM@ALIENTEK */
delay(500);
}
/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
demo_show_cube(); /* 演示立方体3D旋转 */
}
在setup函数中,调用uart_init函数完成串口初始化,调用xl9555_init函数完成XL9555初始化,然后调用lcd_init函数去完成LCD初始化,接下来就是调用lcd_clear进行刷屏测试,调用lcd_show_pic函数显示正点原子logo,调用lcd_show_string函数显示实验信息。
在loop函数中,调用demo_show.cpp文件里面的demo_show_cube函数展示一个立方体3D旋转,这里只是为了展示,大家没有必要去研究这里的实现。
17.4 下载验证
下载代码后,可以看到SPILCD模块首先进行纯色刷屏测试,后面就显示LCD例程实验信息以及显示正点原子Logo以及一个3D立方体。

图17.4.1 LCD显示效果图