《ESP32S3 Arduino开发指南 V1.0 》第十七章 SPI_LCD实验

第十七章 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显示效果图


请使用浏览器的分享功能分享到微信等