一、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 总线具有三种传输方式:全双工、单工以及半双工传输方式。
二、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)
/*** @brief SPI初始化函数* */
void bsp_spi_simulate_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); // 配置GPIOgpio_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
}
【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;
}
三、SPI控制器介绍
ESP32-S3 芯片集成了四个 SPI 控制器,分别为 SPI0、SPI1、SPI2 和 SPI3。SPI0 和 SPI1 控制器主要供内部使用以访问外部 FLASH 和 PSRAM,所以只能使用 SPI2 和 SPI3。SPI2 又称为 HSPI(高速 SPI),而 SPI3 又称为 VSPI(通用 SPI)。
四、SPI常用函数
ESP-IDF 提供了一套 API 来配置 SPI。要使用此功能,需要导入必要的头文件:
#include "driver/spi_master.h"
4.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, // SPI1SPI2_HOST=1, // SPI2
#if SOC_SPI_PERIPH_NUM > 2SPI3_HOST=2, // SPI3
#endifSPI_HOST_MAX, // 无效的主机值
} spi_host_device_t;
该函数使用 spi_bus_config_t
类型的结构体变量来配置 SPI 总线的SCLK、MISO、MOSI 等引脚以及其他参数。该结构体的定义如下所示:
typedef struct
{union {int mosi_io_num; // MISO引脚号int data0_io_num; // GPIO pin for spi data0 signal in quad/octal mode, or -1 if not used.};union{int miso_io_num; // MOSI引脚号int data1_io_num; // GPIO pin for spi data1 signal in quad/octal mode, or -1 if not used.};int sclk_io_num; // 时钟引脚号union {int quadwp_io_num; // 用于 Quad 模式的 WP 引脚号,未使用时设置为-1int data2_io_num; // GPIO pin for spi data2 signal in quad/octal mode, or -1 if not used.};union {int quadhd_io_num; // 用于 Quad 模式的 HD 引脚号,未使用时设置为-1int data3_io_num; // GPIO pin for spi data3 signal in quad/octal mode, or -1 if not used.};int data4_io_num; // GPIO pin for spi data4 signal in octal mode, or -1 if not used.int data5_io_num; // GPIO pin for spi data5 signal in octal mode, or -1 if not used.int data6_io_num; // GPIO pin for spi data6 signal in octal mode, or -1 if not used.int data7_io_num; // GPIO pin for spi data7 signal in octal mode, or -1 if not used.int max_transfer_sz; // 最大传输大小uint32_t flags; // Abilities of bus to be checked by the driver. Or-ed value of ``SPICOMMON_BUSFLAG_*`` flags.esp_intr_cpu_affinity_t isr_cpu_id; ///< Select cpu core to register SPI ISR.int intr_flags; // 中断标志的总线设置优先级
} spi_bus_config_t;
4.2、设备配置
我们需要使用 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_DEFAULTuint16_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_* flagsint queue_size; // 事务队列大小transaction_cb_t pre_cb; // 在传输开始之前调用的回调函数transaction_cb_t post_cb; // 在传输完成后调用的回调函数
} spi_device_interface_config_t;
4.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;
五、LCD简介
液晶显示器,即 Liquid Crystal Display,利用了液晶导电后透光性可变的特性,配合显示器光源、彩色滤光片和电压控制等工艺,最终可以在液晶阵列上显示彩色的图像。目前液晶显示技术以 TN、STN、TFT 三种技术为主,TFT-LCD 即采用了 TFT(Thin Film Transistor)技术的液晶显示器,也叫薄膜晶体管液晶显示器。
这里,LCD 使用 ST7735S 控制芯片。ST7735S 是一款用于 262K 色彩、图形类型 TFT-LCD 的单芯片控制器/驱动器。它包含 396 个源码线和 162 个网线驱动电路。该芯片可以直接连接到外部微处理器,并接受串行外围接口(SPI)、8 位 / 9 位 / 16 位 / 18 位并行接口。显示数据可以存储在 132 x 162 x 18 位的芯片内显示数据 RAM 中。 它可以在没有外部操作时钟的情况下执行显示数据 RAM 读写操作,以最小化功耗。此外,由于集成了驱动液晶所需的电源电路,可以使用更少的元件构建显示系统。
引脚 | 说明 |
---|---|
GND | 电源负极 |
VCC | 电源正极,3.3V ~ 5.0V |
SCL | SPI 时钟线,接 SPI SCLK 引脚 |
SDA | SPI 数据线,接 SPI MOSI 引脚 |
RST | 复位接口(低电平有效) |
DC | 数据/命令选择 |
CS | SPI 片选线 |
BLK | 背光控制(低电平关闭) |
六、实验例程
这里,我们在【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_mosi_io_num, gpio_num_t spi_miso_io_num);
void bsp_spi_device_interface_config(spi_host_device_t host_id, gpio_num_t cs_gpio_num, uint8_t mode, int clock_speed, spi_device_handle_t *handle);
void bsp_spi_send_one_byte(spi_device_handle_t handle, uint8_t data);
void bsp_spi_send_bytes(spi_device_handle_t handle, const uint8_t *data, uint16_t length);
uint8_t bsp_spi_transfer_one_byte(spi_device_handle_t 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_mosi_io_num, gpio_num_t spi_miso_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.mosi_io_num = spi_mosi_io_num; // SPI的MOSI引脚spi_bus_config.miso_io_num = spi_miso_io_num; // SPI的MISO引脚spi_bus_config.quadwp_io_num = -1; // SPI写保护信号引脚,该引脚未使能spi_bus_config.quadhd_io_num = -1; // SPI保持信号引脚,该引脚未使能spi_bus_config.max_transfer_sz = 1024 * 10; // 配置最大传输大小,以字节为单位spi_bus_initialize(host_id, &spi_bus_config, SPI_DMA_CH_AUTO);
}/*** @brief SPI设备接口配置函数* * @param host_id SPI总线的主机设备ID* @param cs_gpio_num SPI的片选引脚* @param mode SPI的工作模式* @param clock_speed SPI的时钟频率* @param handle SPI设备句柄*/
void bsp_spi_device_interface_config(spi_host_device_t host_id, gpio_num_t cs_gpio_num, uint8_t mode, int clock_speed, spi_device_handle_t *handle)
{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, handle);
}/*** @brief SPI发送一个字节数据* * @param handle SPI句柄* @param data 要发送的一个字节的数据*/
void bsp_spi_send_one_byte(spi_device_handle_t handle, uint8_t data)
{spi_transaction_t spi_transaction = {0};spi_transaction.length = 8; // 要传输的位数,一个字节8位spi_transaction.tx_buffer = &data; // 要传输的数据spi_device_polling_transmit(handle, &spi_transaction); // 发送数据
}/*** @brief SPI发送多个字节数据* * @param handle SPI句柄* @param data 要发送的多个字节的数据的缓冲区*/
void bsp_spi_send_bytes(spi_device_handle_t 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(handle, &spi_transaction); // 开始传输
}/*** @brief SPI传输一个字节数据* * @param handle SPI句柄* @param data 要传输的一个字节的数据* @return uint8_t 接收的数据*/
uint8_t bsp_spi_transfer_one_byte(spi_device_handle_t 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(handle, &spi_transaction);return spi_transaction.rx_data[0];
}
SPI0 和 SPI1 控制器主要供内部使用以访问外部 FLASH 和 PSRAM,所以只能使用 SPI2 和 SPI3。
我们在【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 160typedef 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_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);static void lcd_write_byte(uint8_t data, lcd_write_mode_t mode);
static void lcd_write_bytes(uint8_t *data, uint16_t length, lcd_write_mode_t mode);
static uint8_t lcd_read_one_byte(void);/*** @brief LCD初始化函数* * @param handle SPI设备地址*/
void lcd_init(void)
{lcd_gpio_init();lcd_reset(); // 复位LCDLCD_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); // 配置GPIOgpio_config_struct.pin_bit_mask = 1ULL << LCD_DC_GPIO_NUM; // 设置引脚gpio_config(&gpio_config_struct); // 配置GPIOgpio_config_struct.pin_bit_mask = 1ULL << LCD_RESET_GPIO_NUM; // 设置引脚gpio_config(&gpio_config_struct); // 配置GPIOgpio_config_struct.pin_bit_mask = 1ULL << LCD_BLK_GPIO_NUM; // 设置引脚gpio_config(&gpio_config_struct); // 配置GPIO
}/*** @brief 向寄存器写入一个字节数据函数* * @param data 一字节数据* @param mode LCD状态的枚举值*/
static 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状态的枚举值*/
static 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 读取的一字节数据*/
static 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 Ratelcd_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 Sequencelcd_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 Sequencelcd_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_dirslcd
)# 头文件路径
set(include_dirslcd# ${CMAKE_SOURCE_DIR}表示顶级 CMakeLists.txt 文件所在的目录的绝对路径${CMAKE_SOURCE_DIR}/components/peripheral/inc${CMAKE_SOURCE_DIR}/components/toolkit
)# 设置依赖库
set(requiresdriver
)# 注册组件到构建系统的函数
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)
修改【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_2, GPIO_NUM_3);bsp_spi_device_interface_config(SPI2_HOST, LCD_CS_GPIO_NUM, 0, 60000000, &g_lcd_spi_device_handle);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));}
}