06. SPI通信
一、SPI简介
1.1、SPI通信协议简介
SPI 是 Serial Peripheral interface 缩写,顾名思义就是串行外围设备接口。SPI 通信协议是 Motorola 公司首先在其 MC68HCXX 系列处理器上定义的。SPI 接口是一种高速的全双工同步的通信总线。

- SCK(Serial Clock)时钟信号,由主设备产生。
- MOSI(Master Out / Slave In)主设备数据输出,从设备数据输入。
- MISO(Master In / Slave Out)主设备数据输入,从设备数据输出。
- CS(Chip Select)从设备片选信号,由主设备产生。
SPI 总线具有三种传输方式:全双工、单工以及半双工传输方式。
1.2、SPI工作模式
SPI 通信协议就具备 4 种工作模式,在讲这 4 种工作模式前,首先先知道两个单词 CPOL 和 CPHA。
- CPOL,详称 Clock Polarity,就是 时钟极性,当主从机没有数据传输的时候 SCL 线的电平状态(即空闲状态)。假如空闲状态是高电平,CPOL=1;若空闲状态时低电平,那么 CPOL=0。
- CPHA,详称 Clock Phase,就是 时钟相位。 同步通信时,数据的变化和采样都是在时钟边沿上进行的,每一个时钟周期都会有上升沿和下降沿两个边沿,那么数据的变化和采样就分别安排在两个不同的边沿,由于数据在产生和到它稳定是需要一定的时间,那么假如我们在第 1 个边沿信号把数据输出了,从机只能从第 2 个边沿信号去采样这个数据。
CPHA 实质指的是数据的采样时刻,CPHA=0 的情况就表示数据的采样是从第 1 个边沿信号上即奇数边沿,具体是上升沿还是下降沿的问题,是由 CPOL 决定的。这里就存在一个问题:当开始传输第一个 bit 的时候,第 1 个时钟边沿就采集该数据了,那数据是什么时候输出来的呢?那么就有两种情况:一是 CS 使能的边沿,二是上一帧数据的最后一个时钟沿。
CPHA=1 的情况就是表示数据采样是从第 2 个边沿即偶数边沿,它的边沿极性要注意一点,不是和上面 CPHA=0 一样的边沿情况。前面的是奇数边沿采样数据,从 SCL 空闲状态的直接跳变,空闲状态是高电平,那么它就是下降沿,反之就是上升沿。由于 CPHA=1 是偶数边沿采样,所以需要根据偶数边沿判断,假如第一个边沿即奇数边沿是下降沿,那么偶数边沿的边沿极性就是上升沿。
由于 CPOL 和 CPHA 都有两种不同状态,所以 SPI 分成了 4 种模式。

#define SPI_SCK_GPIO_NUM GPIO_NUM_1
#define SPI_MOSI_GPIO_NUM GPIO_NUM_2
#define SPI_MISO_GPIO_NUM GPIO_NUM_3
#define SPI_SCK(x) gpio_set_level(SPI_SCK_GPIO_NUM, x)
#define SPI_MOSI(x) gpio_set_level(SPI_MOSI_GPIO_NUM, x)
#define SPI_MISO() gpio_get_level(SPI_MISO_GPIO_NUM)
#define SIMULATE_SPI_MODE0 0
#define SIMULATE_SPI_MODE1 1
#define SIMULATE_SPI_MODE2 2
#define SIMULATE_SPI_MODE3 3
#define SIMULATE_SPI_MODE SIMULATE_SPI_MODE0
/**
* @brief SPI初始化函数
*
*/
void bsp_simulate_spi_init(void)
{
gpio_config_t gpio_config_struct = {0};
gpio_config_struct.pin_bit_mask = (1ULL << SPI_SCK_GPIO_NUM) | (1ULL << SPI_MOSI_GPIO_NUM); // 设置引脚
gpio_config_struct.intr_type = GPIO_INTR_DISABLE; // 不使用中断
gpio_config_struct.mode = GPIO_MODE_OUTPUT; // 输出模式
gpio_config_struct.pull_up_en = GPIO_PULLUP_DISABLE; // 不使用上拉
gpio_config_struct.pull_down_en = GPIO_PULLUP_DISABLE; // 不使用下拉
gpio_config(&gpio_config_struct); // 配置GPIO
gpio_config_struct.pin_bit_mask = (1ULL << SPI_MISO_GPIO_NUM);
gpio_config_struct.mode = GPIO_MODE_INPUT;
gpio_config(&gpio_config_struct);
SPI_SCK(0); // SPI的SCK引脚默认为低电平,选择工作模式0或1
// SPI_SCK(1); // SPI的SCK引脚默认为高电平,选择工作模式2或3
}
/**
* @brief SPI交换一个字节函数
*
* @param data 待交换的数据
* @return uint8_t 交换后的数据
*/
uint8_t bsp_simulate_spi_swap_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
#if SIMULATE_SPI_MODE == SIMULATE_SPI_MODE1
// SCK上升沿
SPI_SCK(1);
#elif SIMULATE_SPI_MODE == SIMULATE_SPI_MODE3
// SCK下降沿
SPI_SCK(0);
#endif
// 移出数据
SPI_MOSI(data & 0x80);
data <<= 1;
#if SIMULATE_SPI_MODE == SIMULATE_SPI_MODE0 || SIMULATE_SPI_MODE == SIMULATE_SPI_MODE3
// SCK上升沿
SPI_SCK(1);
#elif SIMULATE_SPI_MODE == SIMULATE_SPI_MODE1 || SIMULATE_SPI_MODE == SIMULATE_SPI_MODE2
// SCK下降沿
SPI_SCK(0);
#endif
// 移入数据
if (SPI_MISO())
{
data |= 0x01;
}
#if SIMULATE_SPI_MODE == SIMULATE_SPI_MODE0
// SCK下降沿
SPI_SCK(0);
#elif SIMULATE_SPI_MODE == SIMULATE_SPI_MODE2
// SCK上升沿
SPI_SCK(1);
#endif
}
return data;
}
【1】、工作模式 0:串行时钟的奇数边沿上升沿采样

CPOL= 0 && CPHA= 0 的情形,由于配置了 CPOL= 0,可以看到当数据未发送或者发送完毕,SCK 的状态是 低电平,再者 CPHA = 0 即是 奇数边沿采集。所以传输的数据会在 奇数边沿上升沿 被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生变化。
/**
* @brief SPI交换一个字节函数
*
* @param data 待交换的数据
* @return uint8_t 交换后的数据
*/
uint8_t bsp_spi_simulate_swap_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 移出数据
SPI_MOSI(data & 0x80);
data <<= 1;
// SCK上升沿
SPI_SCK(1);
// 移入数据
if (SPI_MISO())
{
data |= 0x01;
}
// SCK下降沿
SPI_SCK(0);
}
return data;
}
【2】、工作模式 1:串行时钟的偶数边沿下降沿采样

CPOL= 0 && CPHA= 1 的情形,由于配置了 CPOL= 0,可以看到当数据未发送或者发送完毕,SCK 的状态是 低电平,再者 CPHA = 1 即是 偶数边沿采集。所以传输的数据会在 偶数边沿下降沿 被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 偶数边沿保持稳定且被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生变化。
/**
* @brief SPI交换一个字节函数
*
* @param data 待交换的数据
* @return uint8_t 交换后的数据
*/
uint8_t bsp_spi_simulate_swap_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// SCK上升沿
SPI_SCK(1);
// 移出数据
SPI_MOSI(data & 0x80);
data <<= 1;
// SCK下降沿
SPI_SCK(0);
// 移入数据
if (SPI_MISO())
{
data |= 0x01;
}
}
return data;
}
【3】、工作模式 2:串行时钟的奇数边沿下降沿采样

CPOL= 1 && CPHA= 0 的情形,由于配置了 CPOL= 1,可以看到当数据未发送或者发送完毕,SCK 的状态是 高电平,再者 CPHA = 0 即是 奇数边沿采集。所以传输的数据会在 奇数边沿下升沿 被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 奇数边沿保持稳定且被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生变化。
/**
* @brief SPI交换一个字节函数
*
* @param data 待交换的数据
* @return uint8_t 交换后的数据
*/
uint8_t bsp_spi_simulate_swap_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// 移出数据
SPI_MOSI(data & 0x80);
data <<= 1;
// SCK下降沿
SPI_SCK(0);
// 移入数据
if (SPI_MISO())
{
data |= 0x01;
}
// SCK上升沿
SPI_SCK(1);
}
return data;
}
【4】、工作模式 3:串行时钟的偶数边沿上升沿采样

CPOL= 1 && CPHA= 1 的情形,由于配置了 CPOL= 1,可以看到当数据未发送或者发送完毕,SCK 的状态是 高电平,再者 CPHA = 1 即是 偶数边沿采集。所以传输的数据会在 偶数边沿上升沿 被采集,MOSI 和 MISO 数据的有效信号需要在 SCK 偶数边沿保持稳定且被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生变化。
/**
* @brief SPI交换一个字节函数
*
* @param data 待交换的数据
* @return uint8_t 交换后的数据
*/
uint8_t bsp_spi_simulate_swap_one_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++)
{
// SCK下降沿
SPI_SCK(0);
// 移出数据
SPI_MOSI(data & 0x80);
data <<= 1;
// SCK上升沿
SPI_SCK(1);
// 移入数据
if (SPI_MISO())
{
data |= 0x01;
}
}
return data;
}
二、ESP32的SPI控制器
ESP32 S3 芯片集成了四个 SPI 控制器,分别为 SPI0、SPI1、SPI2 和 SPI3。SPI0 和 SPI1 控制器主要供内部使用以访问外部 FLASH 和 PSRAM,所以只能使用 SPI2 和 SPI3。SPI2 又称为 HSPI(高速 SPI),而 SPI3 又称为 VSPI(通用 SPI),这两个属于 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 芯片。
三、SPI常用函数
ESP-IDF 提供了一套 API 来配置 SPI。要使用此功能,需要导入必要的头文件:
#include "driver/spi_master.h"
3.1、初始化SPI总线
我们需要使用 spi_bus_initialize() 函数用于 初始化 SPI 总线,并配置其 GPIO 引脚和主模式下的时钟等参数,该函数原型如下所示:
/**
* @brief 初始化SPI总线
*
* @param host_id 指定SPI总线的主机设备ID
* @param bus_config 用于配置SPI总线的引脚
* @param dma_chan 指定使用哪个DMA通道
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
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);
该函数使用 spi_host_device_t 类型的结构体变量来 指定 SPI 总线的主机设备 ID。该结构体的定义如下所示:
// 带有三个 SPI 外围设备的枚举,这些外围设备可通过软件访问
typedef enum
{
SPI1_HOST=0, // SPI1
SPI2_HOST=1, // SPI2
#if SOC_SPI_PERIPH_NUM > 2
SPI3_HOST=2, // SPI3
#endif
SPI_HOST_MAX, // 无效的主机值
} spi_host_device_t;
该函数使用 spi_bus_config_t 类型的结构体变量来 配置 SPI 总线的SCLK、MISO、MOSI 等引脚以及其它参数。该结构体的定义如下所示:
typedef struct
{
union
{
int mosi_io_num; // SPI总线的主机输出从机输入引脚(MOSI引脚号)
int data0_io_num; // QSPI总线的数据引脚〇
};
union
{
int miso_io_num; // SPI总线的主机输入从机输出引脚(MISO引脚号)
int data1_io_num; // QSPI总线的数据引脚
};
int sclk_io_num; // SPI总线的时钟引脚
union
{
int quadwp_io_num; // 写保护引脚
int data2_io_num; // QSPI总线的数据引脚2
};
union
{
int quadhd_io_num; // 暂停通信引脚
int data3_io_num; // QSPI总线的数据引脚3
};
int data4_io_num; // QSPI总线的数据引脚4
int data5_io_num; // QSPI总线的数据引脚5
int data6_io_num; // QSPI 总线的数据引脚6
int data7_io_num; // QSPI总线的数据引脚7
int max_transfer_sz; // 最大传输长度,字节为单位(启用DMA,默认4092)
uint32_t flags; // 驱动检查总线的标志
esp_intr_cpu_affinity_t isr_cpu_id; // 选择中断挂载的内核
int intr_flags; // 中断标志为总线设置优先级
} spi_bus_config_t;
3.2、SPI总线添加设备
我们需要使用 spi_bus_add_device() 函数用于 在 SPI 总线上分配设备,函数原型如下所示:
/**
* @brief 在SPI总线上分配设备
*
* @param host_id 指定SPI总线的主机设备ID
* @param dev_config 配置SPI设备的通信参数
* @param handle 保存创建的设备句柄
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
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);
该函数使用 spi_host_device_t 类型以及 spi_device_interface_config_t 类型的结构体变量 传入 SPI 外围设备的配置参数,该结构体的定义如下所示:
typedef struct
{
uint8_t command_bits; // 命令阶段的位数
uint8_t address_bits; // 址阶段的位数
uint8_t dummy_bits; // 虚拟阶段的位数
uint8_t mode; // SPI模式
spi_clock_source_t clock_source; // SPI的时钟源,默认是SPI_CLK_SRC_DEFAULT
uint16_t duty_cycle_pos; // 有效时钟的占空比
uint16_t cs_ena_pretrans; // cs在传输前应该被激活SPI位周期的数量(0-16)
uint8_t cs_ena_posttrans; // cs在传输后应该保持活跃SPI位周期的数量(0-16)
int clock_speed_hz; // 时钟速率
int input_delay_ns; // 从机数据最大有效时间
int spics_io_num; // CS引脚号
uint32_t flags; // Bitwise OR of SPI_DEVICE_* flags
int queue_size; // 事务队列大小
transaction_cb_t pre_cb; // 在传输开始之前调用的回调函数
transaction_cb_t post_cb; // 在传输完成后调用的回调函数
} spi_device_interface_config_t;
3.3、数据传输
/**
* @brief 发送一个SPI事务,等待它完成,并返回结果
*
* @param handle 设备的句柄
* @param trans_desc 描述了要发送的事务详情
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t SPI_MASTER_ATTR spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
/**
* @brief 该函数用于发送一个轮询事务,等待它完成,并返回结果
*
* @param handle 设备的句柄
* @param trans_desc 描述了要发送的事务详情
* @return esp_err_t ESP_OK配置成功,其它配置失败
*/
esp_err_t SPI_MASTER_ISR_ATTR spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t* trans_desc);
形参 trans_desc 是 spi_transaction_t 结构体类型的指针,它描述了要发送的事务详情,它的定义如下:
struct spi_transaction_t {
uint32_t flags; // SPI传输标志
uint16_t cmd; // 命令数据,其长度在spi_device_interface_config_t结构体中配置
uint64_t addr; // 地址数据,其长度在spi_device_interface_config_t结构体中配置
size_t length; // 数据长度
size_t rxlength; // 接收的数据长度
void *user; // 用户数据
union {
const void *tx_buffer; // 传输的数据缓冲区
uint8_t tx_data[4]; // 如果设置了SPI_TRANS_USE_TXDATA,则这里的数据集将直接从该变量发送
};
union {
void *rx_buffer; // 接收的数据缓冲区
uint8_t rx_data[4]; // 如果设置了SPI_TRANS_USE_RXDATA,则直接将数据接收到该变量
};
};
typedef struct spi_transaction_t spi_transaction_t;
四、SPI LCD液晶显示器
4.1、LCD简介
液晶显示器,即 Liquid Crystal Display,利用了液晶导电后透光性可变的特性,配合显示器光源、彩色滤光片和电压控制等工艺,最终可以在液晶阵列上显示彩色的图像。目前液晶显示技术以 TN、STN、TFT 三种技术为主,TFT-LCD 即采用了 TFT(Thin Film Transistor)技术的液晶显示器,也叫薄膜晶体管液晶显示器。

【1】、背光源:
背光源位于 LCD 面板的背面,负责提供均匀的光源。背光是 LCD 显示图像的基础,没有背光,LCD屏幕无法显示图像。所以在驱动 LCD屏幕,首先确保背光有没有被点亮。
【2】、扩散板和导光板:
扩散板和导光板位于背光源和液晶层之间,导光板负责将背光源发出的光线均匀分布在整个屏幕,而扩散板主要让光扩散的面积变大,让光均匀柔和。
可以把背光源、导光板和扩散板归纳为背光部分。背光部分还包含未被体现出来的棱镜膜,其作用是让不符合角度的光线再次被利用。
【3】、偏光片
偏光片又称偏振板,起到光线过滤作用。LCD 的两侧各贴有一片偏振板,他们的方向通常是互相垂直的。光线进入 LCD 时,只有特定方向的光(垂直偏光片 A)才能通过偏光片 A,其他方向的光会被吸收,而偏光片 B 与偏光片 A 方向是垂直的,这时候光是不能透过偏光片 B,屏幕显示出来就是黑色。想要屏幕显示白色,必定需要光透过偏光片 B,这时就需要在两个偏光片之间增加一个液晶层。液晶层的液晶分子的排列变化会影响光线通过偏光片 B 的能力,从而实现明暗的变化。所以光通过的路径是:通过偏光片 A,然后穿过液晶层,最后通过偏光片 B。
【4】、液晶层
液晶层中有液晶分子和配向膜。液晶分子在自然状态下,有规律、整齐排列,而液晶层的配向膜把液晶夹在中间,液晶分子就会沿着配向膜的细槽方向排列。最终导致透过偏光片 A 的光,经过液晶层时,会沿着液晶分子的扭曲产生变化,这样光变成水平方向,就可以透光偏光片 B,显示出白色,有彩色滤光片,所以显示出颜色。液晶分子在没有电场作用时具有一定的排列顺序,而在电场的影响下,其排列方式会发生变化。所以在配向膜两端加上电极层,只需要改变电极层的电压,即可控制光是否透过偏光片 B,进而显示颜色。这个电极层就是 TFT 的内容。
【5】、彩色滤光片
彩色滤光片位于一个玻璃基板之上,覆盖有红、绿、蓝三原色的滤色条纹阵列,对应形成屏幕上的每一个像素点。换句话来说,每个像素点都有三个子像素,分别对应的是红色子像素、绿色子像素和蓝色子像素。通过改变红绿蓝的明暗程度,即可混合出各种颜色,为显示色彩提供基础。
【6】、TFT 层
TFT 层由电极、玻璃层和 TFT 组成。TFT 即薄膜晶体管,每个子像素都有一个晶体管,也就是说一个像素点会有三个晶体管。这个晶体管就是每个子像素的亮度调节开关,可以调节子像素范围内的电压,这个子像素范围内的液晶分子自然也会受到电压大小的影响而发生不同程度扭曲。
【7】、玻璃基板
LCD 由两片很薄的玻璃基板组成,分别作为顶层和底层,这是显示屏的主体结构。这些基板通常是由钙钠玻璃经过精细抛光和镀膜处理制成,以保证透光性和耐用性。
这里,我们使用的液晶模块采用的是 SPI 通信,它的引脚接线如下:
| 引脚 | 说明 |
|---|---|
| GND | 电源负极 |
| VCC | 电源正极,3.3V ~ 5.0V |
| SCL | SPI 时钟线,接 SPI SCLK 引脚 |
| SDA | SPI 数据线,接 SPI MOSI 引脚 |
| RST | 复位接口(低电平有效) |
| DC | 数据/命令选择 |
| CS | SPI 片选线 |
| BLK | 背光控制(低电平关闭) |
4.2、ST7735S IC驱动芯片
这里,LCD 使用 ST7735S 控制芯片。ST7735S 是一款用于 262K 色彩、图形类型 TFT LCD 的单芯片控制器/驱动器。它包含 396 个源码线和 162 个网线驱动电路。该芯片可以直接连接到外部微处理器,并接受串行外围接口(SPI)、8 位 / 9 位 / 16 位 / 18 位并行接口。显示数据可以存储在 132 x 162 x 18 位的芯片内显示数据 RAM 中。 它可以在没有外部操作时钟的情况下执行显示数据 RAM 读写操作,以最小化功耗。此外,由于集成了驱动液晶所需的电源电路,可以使用更少的元件构建显示系统。
驱动 IC 通过特定的接口对外通信,并控制输出电压让液晶扭转,使其发生色阶及明暗的变化。它通常包含控制电路和驱动电路两部分。控制电路负责接收来自主控芯片的信号,以及图像信号的转换与处理,驱动电路负责输出图像信号并显示到面板上。
ST7735S 最高支持 128 x 160的分辨率,最高支持 16 位色深。这里,我们采用 RGB565 格式传输、存储颜色数据,如下图所示。

如上图所示,是一个传输像素数据的时序过程,D/CX 线需要拉高,表示传输的是数据。一个像素的颜色数据需要使用 16 bit 来传输,这 16 bit 数据中,高 5 bit 用于表示红色,低 5 bit 用于表示蓝色,中间的 6 bit 用于表示绿色。数据的数值越大,对应表示的颜色就越深。
ST7735S 用于控制 LCD 的各种显示功能和效果,整体功能比较复杂。一般我们只需要用以下几条指令就可以完成对 LCD 的基本使用。
| 寄存器 | 功能简介 | 使用说明 | 注意 |
|---|---|---|---|
| 0x11 | 唤醒显示屏,从睡眠模式恢复到正常工作模式 | 指令 | 至少延时 120ms |
| 0x36 | 控制数据访问方式,RGB/BGR 选择,行列读写方向与水平垂直刷新等 | 指令 + 1 个 8 位数据 | 可设定 MY MX MV ML RGB/BGR MH |
| 0x3A | RGB 图像数据格式 | 指令 + 1 个 8 位数据 | 8 位的后三位有效,如 3: 12bit; 5: 1bit; 6:18bit; 7: 未使用; |
| 0xB1 ~ 0xB3 | 多种模式帧率控制,正常模式/全彩/空闲模式/8色 /部分模式等 | 指令 + 3 个 8 位数据 | |
| 0xB4 | 显示反转控制 | 指令 + 1 个 8 位数据 | 后三位位有效,分别对应不同模式 |
| 0xC0 ~ 0xC4 | 特定颜色模式下的电压参数,调整显示屏的亮度、对比度等显示效果 | 指令 + 3 个 8 位数据 | |
| 0xC5 | 设置显示屏的 VCOM 电压,即显示屏公共电极的电压,影响整体显示效果,调整亮度均匀,减少色彩失真 | 指令 + 1 个 8 位数据 | |
| 0xE0 ~ 0xE1 | 伽马极性校正设置,屏幕亮度更符合人眼的感知特性,减少视觉疲劳,优化色彩准确性,增强暗部细节 | 指令 + 16 个 8 位数据 | |
| 0x20 | 从颜色反转中恢复 | 指令 | 不反色无需设置 |
| 0x21 | 打开颜色反转功能 | 指令 | 若屏幕反色,可能是打开了该指令 |
| 0x28 | 关闭屏幕 | 指令 | 与 0x29 相反 |
| 0x29 | 打开屏幕 | 指令 | 初始化驱动时,加上该指令 |
| 0x2A | 设置列地址范围,与 0x2B 共同作用选定屏幕点亮区域 | 指令 + 4 个 8 位数据 | 前 2 个 8 位数据:起始列坐标 x_start,后 2 个 8 位数据表示 X_end |
| 0x2B | 设置行地址范围,0x2A 0x2B 使用后需要用 0x2C 确定,否则无效 | 指令 + 4 个 8 位数据 | 前 2 个 8 位数据:起始行坐标 Y_start,后 2 个 8 位数据表示 Y_end |
| 0x2C | 写入配置的显示参数,简单理解就相当关于一个确认操作 | 指令 | 部分寄存器修改后,必须加上 0x2C 指令进行确认写入 |
ST7735S 支持连续读写 RAM 中存放的 LCD 上颜色对应的数据,并且连续读写的方向(LCD 的扫描方向)是可以通过命令 0x36 进行配置的,如下图所示。

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

在往 LCD 模块写入颜色数据前,还需要设置地址,以确定随后写入的颜色数据对应 LCD 上的哪一个像素,通过命令 0x2A 和命令 0x2B 可以分别设置 LCD 模块显示颜色数据的 列地址 和 行地址。
命令 0x2A 的描述,如下图所示:

命令 0x2B 的描述,如下图所示:

五、实验例程
5.1、SPI相关的函数
这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_spi.h 文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_spi.c 文件。
#ifndef __BSP_SPI_H__
#define __BSP_SPI_H__
#include "driver/spi_master.h"
#include "driver/gpio.h"
void bsp_spi_init(spi_host_device_t host_id, gpio_num_t spi_sclk_io_num, gpio_num_t spi_miso_io_num, gpio_num_t spi_mosi_io_num);
void bsp_spi_bus_add_device(spi_device_handle_t *device_handle, spi_host_device_t host_id, gpio_num_t cs_gpio_num, uint8_t mode, int clock_speed);
void bsp_spi_send_one_byte(spi_device_handle_t device_handle, uint8_t data);
void bsp_spi_send_bytes(spi_device_handle_t device_handle, const uint8_t *data, uint16_t length);
uint8_t bsp_spi_transfer_one_byte(spi_device_handle_t device_handle, uint8_t data);
#endif // !__BSP_SPI_H__
#include "bsp_spi.h"
/**
* @brief 初始化SPI
*
* @param host_id SPI总线的主机设备ID
* @param sclk_io_num SPI的SCLK引脚
* @param miso_io_num SPI的MISO引脚
* @param mosi_io_num SPI的MOSI引脚
*/
void bsp_spi_init(spi_host_device_t host_id, gpio_num_t spi_sclk_io_num, gpio_num_t spi_miso_io_num, gpio_num_t spi_mosi_io_num)
{
spi_bus_config_t spi_bus_config = {0};
// SPI总线配置
spi_bus_config.sclk_io_num = spi_sclk_io_num; // SPI的SCLK引脚
spi_bus_config.miso_io_num = spi_miso_io_num; // SPI的MISO引脚
spi_bus_config.mosi_io_num = spi_mosi_io_num; // SPI的MOSI引脚
spi_bus_config.quadwp_io_num = -1; // SPI写保护信号引脚,该引脚未使能
spi_bus_config.quadhd_io_num = -1; // SPI保持信号引脚,该引脚未使能
spi_bus_config.max_transfer_sz = 1024 * 1; // 配置最大传输大小,以字节为单位
spi_bus_initialize(host_id, &spi_bus_config, SPI_DMA_CH_AUTO);
}
/**
* @brief SPI设备接口配置函数
*
* @param device_handle SPI设备句柄
* @param host_id SPI总线的主机设备ID
* @param cs_gpio_num SPI的片选引脚
* @param mode SPI的工作模式
* @param clock_speed SPI的时钟频率
*/
void bsp_spi_bus_add_device(spi_device_handle_t *device_handle, spi_host_device_t host_id, gpio_num_t cs_gpio_num, uint8_t mode, int clock_speed)
{
spi_device_interface_config_t spi_device_interface_config = {0};
spi_device_interface_config.spics_io_num = cs_gpio_num; // SPI的片选引脚
spi_device_interface_config.mode = mode; // SPI的工作模式
spi_device_interface_config.clock_speed_hz = clock_speed; // SPI的时钟频率
spi_device_interface_config.queue_size = 8; // 事务队列大小
spi_bus_add_device(host_id, &spi_device_interface_config, device_handle); // 添加SPI设备
}
/**
* @brief SPI发送一个字节数据
*
* @param device_handle SPI设备句柄
* @param data 要发送的一个字节的数据
*/
void bsp_spi_send_one_byte(spi_device_handle_t device_handle, uint8_t data)
{
spi_transaction_t spi_transaction = {0};
spi_transaction.length = 8; // 要传输的位数,一个字节8位
spi_transaction.tx_buffer = &data; // 要传输的数据
spi_device_polling_transmit(device_handle, &spi_transaction); // 发送数据
}
/**
* @brief SPI发送多个字节数据
*
* @param device_handle SPI设备句柄
* @param data 要发送的多个字节的数据的缓冲区
*/
void bsp_spi_send_bytes(spi_device_handle_t device_handle, const uint8_t *data, uint16_t length)
{
spi_transaction_t spi_transaction = {0};
spi_transaction.length = length * 8; // 要传输的位数,一个字节8位
spi_transaction.tx_buffer = data; // 将命令填充进去
spi_device_polling_transmit(device_handle, &spi_transaction); // 开始传输
}
/**
* @brief SPI传输一个字节数据
*
* @param device_handle SPI设备句柄
* @param data 要传输的一个字节的数据
* @return uint8_t 接收的数据
*/
uint8_t bsp_spi_transfer_one_byte(spi_device_handle_t device_handle, uint8_t data)
{
spi_transaction_t spi_transaction = {0};
spi_transaction.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA;
spi_transaction.length = 8;
spi_transaction.tx_data[0] = data;
spi_device_transmit(device_handle, &spi_transaction);
return spi_transaction.rx_data[0];
}
SPI0 和 SPI1 控制器主要供内部使用以访问外部 FLASH 和 PSRAM,所以只能使用 SPI2 和 SPI3。
5.2、LCD相关的函数
我们在【components】文件夹下的【device】文件夹中新增了一个 【lcd】 文件夹,用于存放 lcd.c 和 lcd.h 这两个文件。
#ifndef __LCD_H__
#define __LCD_H__
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include "bsp_spi.h"
#include "screen/color.h"
#include "screen/font.h"
#include "screen/image.h"
#define LCD_CS_GPIO_NUM GPIO_NUM_4
#define LCD_CS(x) do{ x ? \
gpio_set_level(LCD_CS_GPIO_NUM, 1) : \
gpio_set_level(LCD_CS_GPIO_NUM, 0); \
}while(0)
#define LCD_DC_GPIO_NUM GPIO_NUM_5
#define LCD_DC(x) do{ x ? \
gpio_set_level(LCD_DC_GPIO_NUM, 1) : \
gpio_set_level(LCD_DC_GPIO_NUM, 0); \
}while(0)
#define LCD_RESET_GPIO_NUM GPIO_NUM_6
#define LCD_RESET(x) do{ x ? \
gpio_set_level(LCD_RESET_GPIO_NUM, 1) : \
gpio_set_level(LCD_RESET_GPIO_NUM, 0); \
}while(0)
#define LCD_BLK_GPIO_NUM GPIO_NUM_7
#define LCD_BLK(x) do{ x ? \
gpio_set_level(LCD_BLK_GPIO_NUM, 1) : \
gpio_set_level(LCD_BLK_GPIO_NUM, 0); \
}while(0)
#define LCD_WIDTH 128
#define LCD_HEIGHT 160
typedef enum LCD_Write_Mode_t
{
LCD_MODE_CMD = 0,
LCD_MODE_DATA = 1
} lcd_write_mode_t;
typedef enum LCD_Display_Mode_t
{
LCD_DISPLAY_NORMAL = 0,
LCD_DISPLAY_OVERLAPPING = 1
} lcd_display_mode_t;
extern spi_device_handle_t g_lcd_spi_device_handle;
void lcd_init(void);
void lcd_reset(void);
void lcd_write_byte(uint8_t data, lcd_write_mode_t mode);
void lcd_write_bytes(uint8_t *data, uint16_t length, lcd_write_mode_t mode);
uint8_t lcd_read_one_byte(void);
void lcd_set_cursor(uint8_t x, uint8_t y);
void lcd_set_cursor_area(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2);
void lcd_display_direction(uint8_t mode);
void lcd_clear(uint16_t color);
void lcd_clear_area(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint16_t color);
void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color);
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode);
void lcd_show_string(uint16_t x, uint16_t y, char *str, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode);
void lcd_show_chinese(uint16_t x, uint16_t y, char *chinese, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode);
void lcd_show_picture(uint8_t image[], uint16_t x, uint16_t y, uint16_t width, uint16_t height);
#endif // !__LCD_H__
#include "lcd.h"
spi_device_handle_t g_lcd_spi_device_handle;
static void lcd_gpio_init(void);
static void lcd_st7735_init(void);
/**
* @brief LCD初始化函数
*
*/
void lcd_init(void)
{
lcd_gpio_init();
lcd_reset(); // 复位LCD
LCD_BLK(1); // 开启背光
LCD_CS(1);
lcd_st7735_init();
lcd_clear(MAGENTA);
}
/**
* @brief LCD底层初始化函数
*
*/
static void lcd_gpio_init(void)
{
gpio_config_t gpio_config_struct = {0};
gpio_config_struct.pin_bit_mask = 1ULL << LCD_CS_GPIO_NUM; // 设置引脚
gpio_config_struct.intr_type = GPIO_INTR_DISABLE; // 不使用中断
gpio_config_struct.mode = GPIO_MODE_OUTPUT; // 输出模式
gpio_config_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; // 不使用下拉
gpio_config_struct.pull_up_en = GPIO_PULLUP_DISABLE; // 不使用上拉
gpio_config(&gpio_config_struct); // 配置GPIO
gpio_config_struct.pin_bit_mask = 1ULL << LCD_DC_GPIO_NUM; // 设置引脚
gpio_config(&gpio_config_struct); // 配置GPIO
gpio_config_struct.pin_bit_mask = 1ULL << LCD_RESET_GPIO_NUM; // 设置引脚
gpio_config(&gpio_config_struct); // 配置GPIO
gpio_config_struct.pin_bit_mask = 1ULL << LCD_BLK_GPIO_NUM; // 设置引脚
gpio_config(&gpio_config_struct); // 配置GPIO
}
/**
* @brief 向寄存器写入一个字节数据函数
*
* @param data 一字节数据
* @param mode LCD状态的枚举值
*/
void lcd_write_byte(uint8_t data, lcd_write_mode_t mode)
{
LCD_CS(0); // 片选
LCD_DC(mode); // 数据/命令选择
bsp_spi_send_one_byte(g_lcd_spi_device_handle, data); // 发送数据
LCD_CS(1); // 取消片选
}
/**
* @brief 向寄存器写入多个字节数据函数
*
* @param data 指向要写入的数据的指针
* @param length 要写入的数据长度
* @param mode LCD状态的枚举值
*/
void lcd_write_bytes(uint8_t *data, uint16_t length, lcd_write_mode_t mode)
{
LCD_CS(0); // 片选
LCD_DC(mode); // 数据/命令选择
bsp_spi_send_bytes(g_lcd_spi_device_handle, data, length); // 发送数据
LCD_CS(1); // 取消片选
}
/**
* @brief 从寄存器读取一个字节数据函数
*
* @param mode LCD状态的枚举值
* @return uint8_t 读取的一字节数据
*/
uint8_t lcd_read_one_byte(void)
{
uint8_t data = 0;
LCD_CS(0); // 片选
LCD_DC(LCD_MODE_DATA); // 数据/命令选择
data = bsp_spi_transfer_one_byte(g_lcd_spi_device_handle, 0); // 读取数据
LCD_CS(1); // 取消片选
return data;
}
/**
* @brief ST7735初始化函数
*
*/
static void lcd_st7735_init(void)
{
// LCD Init For 1.44Inch LCD Panel with ST7735R.
lcd_write_byte(0x11, LCD_MODE_CMD); // Sleep exit
vTaskDelay(120);
// ST7735R Frame Rate
lcd_write_byte(0xB1, LCD_MODE_CMD);
lcd_write_byte(0x01, LCD_MODE_DATA);
lcd_write_byte(0x2C, LCD_MODE_DATA);
lcd_write_byte(0x2D, LCD_MODE_DATA);
lcd_write_byte(0xB2, LCD_MODE_CMD);
lcd_write_byte(0x01, LCD_MODE_DATA);
lcd_write_byte(0x2C, LCD_MODE_DATA);
lcd_write_byte(0x2D, LCD_MODE_DATA);
lcd_write_byte(0xB3, LCD_MODE_CMD);
lcd_write_byte(0x01, LCD_MODE_DATA);
lcd_write_byte(0x2C, LCD_MODE_DATA);
lcd_write_byte(0x2D, LCD_MODE_DATA);
lcd_write_byte(0x01, LCD_MODE_DATA);
lcd_write_byte(0x2C, LCD_MODE_DATA);
lcd_write_byte(0x2D, LCD_MODE_DATA);
lcd_write_byte(0xB4, LCD_MODE_CMD); // Column inversion
lcd_write_byte(0x07, LCD_MODE_DATA);
//ST7735R Power Sequence
lcd_write_byte(0xC0, LCD_MODE_CMD);
lcd_write_byte(0xA2, LCD_MODE_DATA);
lcd_write_byte(0x02, LCD_MODE_DATA);
lcd_write_byte(0x84, LCD_MODE_DATA);
lcd_write_byte(0xC1, LCD_MODE_CMD);
lcd_write_byte(0xC5, LCD_MODE_DATA);
lcd_write_byte(0xC2, LCD_MODE_CMD);
lcd_write_byte(0x0A, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0xC3, LCD_MODE_CMD);
lcd_write_byte(0x8A, LCD_MODE_DATA);
lcd_write_byte(0x2A, LCD_MODE_DATA);
lcd_write_byte(0xC4, LCD_MODE_CMD);
lcd_write_byte(0x8A, LCD_MODE_DATA);
lcd_write_byte(0xEE, LCD_MODE_DATA);
lcd_write_byte(0xC5, LCD_MODE_CMD); // VCOM
lcd_write_byte(0x0E, LCD_MODE_DATA);
lcd_write_byte(0x36, LCD_MODE_CMD); // MX, MY, RGB mode
lcd_write_byte(0xC0, LCD_MODE_DATA);
// ST7735R Gamma Sequence
lcd_write_byte(0xe0, LCD_MODE_CMD);
lcd_write_byte(0x0F, LCD_MODE_DATA);
lcd_write_byte(0x1A, LCD_MODE_DATA);
lcd_write_byte(0x0F, LCD_MODE_DATA);
lcd_write_byte(0x18, LCD_MODE_DATA);
lcd_write_byte(0x2F, LCD_MODE_DATA);
lcd_write_byte(0x28, LCD_MODE_DATA);
lcd_write_byte(0x20, LCD_MODE_DATA);
lcd_write_byte(0x22, LCD_MODE_DATA);
lcd_write_byte(0x1F, LCD_MODE_DATA);
lcd_write_byte(0x1B, LCD_MODE_DATA);
lcd_write_byte(0x23, LCD_MODE_DATA);
lcd_write_byte(0x37, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x07, LCD_MODE_DATA);
lcd_write_byte(0x02, LCD_MODE_DATA);
lcd_write_byte(0x10, LCD_MODE_DATA);
lcd_write_byte(0xE1, LCD_MODE_CMD);
lcd_write_byte(0x0F, LCD_MODE_DATA);
lcd_write_byte(0x1B, LCD_MODE_DATA);
lcd_write_byte(0x0F, LCD_MODE_DATA);
lcd_write_byte(0x17, LCD_MODE_DATA);
lcd_write_byte(0x33, LCD_MODE_DATA);
lcd_write_byte(0x2C, LCD_MODE_DATA);
lcd_write_byte(0x29, LCD_MODE_DATA);
lcd_write_byte(0x2E, LCD_MODE_DATA);
lcd_write_byte(0x30, LCD_MODE_DATA);
lcd_write_byte(0x30, LCD_MODE_DATA);
lcd_write_byte(0x39, LCD_MODE_DATA);
lcd_write_byte(0x3F, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x07, LCD_MODE_DATA);
lcd_write_byte(0x03, LCD_MODE_DATA);
lcd_write_byte(0x10, LCD_MODE_DATA);
lcd_write_byte(0x2A, LCD_MODE_CMD);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x7F, LCD_MODE_DATA);
lcd_write_byte(0x2B, LCD_MODE_CMD);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x9F, LCD_MODE_DATA);
lcd_write_byte(0xF0, LCD_MODE_CMD); // Enable test command
lcd_write_byte(0x01, LCD_MODE_DATA);
lcd_write_byte(0xF6, LCD_MODE_CMD); // Disable ram power save mode
lcd_write_byte(0x00, LCD_MODE_DATA);
lcd_write_byte(0x3A, LCD_MODE_CMD); // 65k mode
lcd_write_byte(0x05, LCD_MODE_DATA);
lcd_write_byte(0x29, LCD_MODE_CMD); // Display on
}
/**
* @brief LCD复位函数
*
*/
void lcd_reset(void)
{
LCD_RESET(0);
vTaskDelay(1);
LCD_RESET(1);
vTaskDelay(120);
}
/**
* @brief OLED设置坐标函数
*
* @param x 坐标所在的列,范围: 0 ~ 127
* @param y 坐标所在的行: 0 ~ 159
*/
void lcd_set_cursor(uint8_t x, uint8_t y)
{
lcd_write_byte(0x2A, LCD_MODE_CMD); // 设置列地址
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送列地址的起始地址高8位
lcd_write_byte(x, LCD_MODE_DATA); // 发送列地址的起始地址低8位
lcd_write_byte(0x2B, LCD_MODE_CMD); // 发送页地址
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送页地址的起始地址高8位
lcd_write_byte(y, LCD_MODE_DATA);
}
/**
* @brief 设置LCD的光标范围
*
* @param x1 光标的起始位置的列
* @param y1 光标的起始位置的行
* @param x2 光标的结束位置的列
* @param y2 光标的结束位置的行
*/
void lcd_set_cursor_area(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2)
{
lcd_write_byte(0x2A, LCD_MODE_CMD); // 设置列地址
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送列地址的起始地址高8位
lcd_write_byte(x1, LCD_MODE_DATA); // 发送列地址的起始地址低8位
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送列地址的起始地址高8位
lcd_write_byte(x2, LCD_MODE_DATA); // 发送列地址的起始地址低8位
lcd_write_byte(0x2B, LCD_MODE_CMD); // 发送页地址
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送页地址的起始地址高8位
lcd_write_byte(y1, LCD_MODE_DATA); // 发送页地址的起始地址低8位
lcd_write_byte(0x00, LCD_MODE_DATA); // 发送页地址的起始地址高8位
lcd_write_byte(y2, LCD_MODE_DATA); // 发送页地址的起始地址低8位
}
/**
* @brief LCD设置显示方向函数
*
* @param direction 0:从左到右,从上到下
* 1:从上到下,从左到右
* 2:从右到左,从上到下
* 3:从上到下,从右到左
* 4:从左到右,从下到上
* 5:从下到上,从左到右
* 6:从右到左,从下到上
* 7:从下到上,从右到左
*/
void lcd_display_direction(uint8_t mode)
{
lcd_write_byte(0x36, LCD_MODE_CMD); //设置彩屏显示方向的寄存器
lcd_write_byte(0x00 | (mode << 5), LCD_MODE_DATA);
switch (mode)
{
case 0:
case 2:
case 4:
case 6:
lcd_set_cursor_area(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1);
break;
case 1:
case 3:
case 5:
case 7:
lcd_set_cursor_area(0, 0, LCD_HEIGHT - 1, LCD_WIDTH - 1);
default:
break;
}
}
/**
* @brief LCD清屏函数
*
* @param color 颜色
*/
void lcd_clear(uint16_t color)
{
uint16_t total_point = LCD_WIDTH * LCD_HEIGHT; // 得到总点数
uint8_t data[2] = {color >> 8, color & 0xFF};
lcd_set_cursor_area(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); // 设置光标位置
lcd_write_byte(0x2C, LCD_MODE_CMD); // 发送写GRAM指令
for (uint16_t index = 0; index < total_point; index++)
{
lcd_write_bytes(data, 2, LCD_MODE_DATA); // 发送颜色数据
}
}
/**
* @brief LCD局部清屏函数
*
* @param x 要清空的区域的左上角的列坐标
* @param y 要清空的区域的左上角的行坐标
* @param width 要清空的区域的宽度
* @param height 要清空的区域的高度
* @param color 要清空的区域的颜色
*/
void lcd_clear_area(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint16_t color)
{
for (uint8_t i = y; i < y + height; i++)
{
lcd_set_cursor(x, i); // 设置光标位置
lcd_write_byte(0x2C, LCD_MODE_CMD); // 发送写GRAM指令
for (uint8_t j = x; j < x + width; j++)
{
uint8_t data[2] = {color >> 8, color & 0xFF}; // 颜色数据
lcd_write_bytes(data, 2, LCD_MODE_DATA); // 发送颜色数据
}
}
}
/**
* @brief LCD画点函数
*
* @param x 列
* @param y 行
* @param color 颜色
*/
void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color)
{
uint8_t data[2] = {color >> 8, color & 0xFF}; // 16位颜色
lcd_set_cursor(x, y); // 设置坐标
lcd_write_byte(0x2C, LCD_MODE_CMD); // 发送写GRAM指令
lcd_write_bytes(data, 2, LCD_MODE_DATA); // 写入颜色值
}
/**
* @brief LCD读点函数
*
* @param x 列数
* @param y 行数
* @return uint16_t
*/
uint16_t lcd_read_point(uint16_t x, uint16_t y)
{
uint16_t r = 0, g = 0, b = 0;
lcd_set_cursor(x, y); // 设置坐标
lcd_write_byte(0x2E, LCD_MODE_CMD); // 读GRAM数据指令
lcd_read_one_byte(); // 假读
r = lcd_read_one_byte(); // 读取R通道和G通道的值
b = lcd_read_one_byte(); // 读取B通道的值
g = r & 0xFF; // 获取G通道的值
return (((r >> 11) << 11) | ((g >> 2) << 5) | (b >> 11));
}
/**
* @brief LCD显示字符函数
*
* @param x 列
* @param y 行
* @param chr 显示的字符
* @param size 字体大小,这里字符的高度等于字重,字符的宽度等于字重的一半
* @param forecolor 字符的颜色
* @param backcolor 背景色
* @param display_mode 显示模式的枚举值
*/
void lcd_show_char(uint16_t x, uint16_t y, char chr, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode)
{
uint8_t *pfont = NULL;
uint8_t temp = 0;
uint8_t high = size / 8 + ((size % 8) ? 1 : 0); // 得到一个字符对应的字节数
switch (size)
{
case 12:
pfont = (uint8_t *)ascii_06x12[chr - ' ']; // 调用06x12字体
break;
case 16:
pfont = (uint8_t *)ascii_08x16[chr - ' ']; // 调用08x16字体
break;
case 24:
pfont = (uint8_t *)ascii_12x24[chr - ' ']; // 调用12x24字体
break;
case 32:
pfont = (uint8_t *)ascii_16x32[chr - ' ']; // 调用16x32字体
break;
default:
return ;
}
for (uint8_t h = 0; h < high; h++) // 遍历字符的高度
{
for (uint8_t w = 0; w < size / 2; w++) // 遍历字符的宽度
{
temp = pfont[h * size / 2 + w]; // 获取字符对应的字节数据
for (uint8_t k = 0; k < 8; k++) // 一个字节8个像素点
{
if (temp & 0x01) // 绘制字符
{
lcd_draw_point(x + w, y + k + 8 * h , forecolor);
}
else
{
if (display_mode == LCD_DISPLAY_NORMAL) // 是否绘制背景
{
lcd_draw_point(x + w, y + k + 8 * h , backcolor);
}
}
temp >>= 1;
}
}
}
}
/**
* @brief LCD显示字符串函数
*
* @param x 列
* @param y 行
* @param str 显示的字符串
* @param size 字体大小,这里字符的高度等于字重,字符的宽度等于字重的一半
* @param forecolor 字符串的颜色
* @param backcolor 背景色
* @param display_mode 显示模式的枚举值
*/
void lcd_show_string(uint16_t x, uint16_t y, char *str, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode)
{
uint16_t x0 = x;
for (uint16_t i = 0; str[i] != '\0'; i++)
{
if (str[i] == '\n')
{
x = x0;
y += size;
continue;
}
lcd_show_char(x, y, str[i], size, forecolor, backcolor, display_mode);
x += (size / 2);
}
}
/**
* @brief LCD显示汉字函数
*
* @param x 列
* @param y 行
* @param chinese 要显示的汉字
* @param size 要显示的汉字大小
* @param forecolor 汉字的颜色
* @param backcolor 背景色
* @param display_mode 显示模式的枚举值
*/
void lcd_show_chinese(uint16_t x, uint16_t y, char *chinese, uint16_t size, uint16_t forecolor, uint16_t backcolor, lcd_display_mode_t display_mode)
{
char sigle_chinese[4] = {0}; // 存储单个汉字
uint16_t index = 0; // 汉字索引
uint16_t high = size / 8 + ((size % 8) ? 1 : 0); // 得到一个字符对应的字节数
uint16_t temp = 0;
uint16_t j = 0;
switch (size)
{
case 32:
for (uint16_t i = 0; chinese[i] != '\0'; i++) // 遍历汉字字符串
{
// 获取单个汉字,一般UTF-8编码使用3个字节存储汉字,GBK编码使用2个字节存储汉字
sigle_chinese[index] = chinese[i];
index++;
index = index % 3;
if (index == 0) // 汉字索引为0,说明已经获取了一个汉字
{
for (j = 0; strcmp(chinese_32x32[j].index, "") != 0; j++) // 遍历汉字数组
{
if (strcmp(chinese_32x32[j].index, sigle_chinese) == 0) // 找到汉字
{
break;
}
}
for (uint16_t h = 0; h < high; h++) // 遍历字符的高度
{
for (uint16_t w = 0; w < size; w++) // 遍历字符的宽度
{
temp = chinese_32x32[j].data[h * size + w]; // 获取字符对应的字节数据
for (uint16_t k = 0; k < 8; k++) // 一个字节8个像素点
{
if (temp & 0x01) // 绘制字体
{
// ((i + 1) / 3)定位到第几个汉字
lcd_draw_point(x + w + ((i + 1) / 3 - 1) * size, y + k + 8 * h , forecolor);
}
else
{
if (display_mode == LCD_DISPLAY_NORMAL) // 是否绘制背景
{
lcd_draw_point(x + w + ((i + 1) / 3 - 1) * size, y + k + 8 * h , backcolor);
}
}
temp >>= 1;
}
}
}
}
}
break;
default:
break;
}
}
/**
* @brief LCD显示图片函数
*
* @param image 图片数据
* @param x 列
* @param y 行
* @param width 图片的宽度
* @param height 图片的高度
*/
void lcd_show_picture(uint8_t image[], uint16_t x, uint16_t y, uint16_t width, uint16_t height)
{
uint8_t color[2] = {0};
uint32_t i = 0, j = 0, k = 0;
for (i = 0; i < height; i++)
{
lcd_set_cursor(x, y + i); // 设置光标位置
lcd_write_byte(0x2C, LCD_MODE_CMD); // 发送写GRAM指令
for (j = 0; j < width; j++)
{
color[0] = image[k + 1]; // 获取图片数据
color[1] = image[k]; // 获取图片数据
k += 2;
lcd_write_bytes(color, 2, LCD_MODE_DATA); // 写入颜色值
}
}
}
然后,我们修改【components】文件夹下的【device】文件夹下的 CMakeLists.txt 文件。
# 源文件路径
set(src_dirs
lcd
)
# 头文件路径
set(include_dirs
lcd
# ${CMAKE_SOURCE_DIR}表示顶级 CMakeLists.txt 文件所在的目录的绝对路径
${CMAKE_SOURCE_DIR}/components/peripheral/inc
${CMAKE_SOURCE_DIR}/components/toolkit
)
# 设置依赖库
set(requires
driver
)
# 注册组件到构建系统的函数
idf_component_register(
# 源文件路径
SRC_DIRS ${src_dirs}
# 自定义头文件的路径
INCLUDE_DIRS ${include_dirs}
# 依赖库的路径
REQUIRES ${requires}
)
# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
5.3、main()函数
修改【main】文件夹下的 main.c 文件。
#include "freertos/FreeRTOS.h"
#include "lcd.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
bsp_spi_init(SPI2_HOST, GPIO_NUM_1, GPIO_NUM_NC, GPIO_NUM_2);
bsp_spi_bus_add_device(&g_lcd_spi_device_handle, SPI2_HOST, LCD_CS_GPIO_NUM, 0, 60000000);
lcd_init();
lcd_show_char(0, 0, 'A', 12, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
lcd_show_char(10, 0, 'A', 16, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
lcd_show_string(10, 20, "Hello Shana!", 32, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
lcd_show_chinese(80, 60, "小樱", 32, BLUE, RED, LCD_DISPLAY_OVERLAPPING);
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(10));
}
}

浙公网安备 33010602011771号