使用ESP32的硬件SPI驱动中景园1.3寸Lcd
接上次我们使用IO模拟的方式驱动LCD屏幕,由于IO模拟刷屏速度太慢,本次我们使用ESP32的硬件SPI来驱动LCD,在测试过程中遇到了很多坑,所以会详细的讲一下ESP32的SPI外设。
ESP32的SPI外设
ESP32-S2 系列芯片共有4 个SPI(SPI0,SPI1,SPI2 和SPI3)。SPI0 和SPI1 只可以配置成SPI 存储器模式, SPI2 既可以配置成SPI 存储器模式又可以配置成通用SPI 模式;SPI3 只可以配置成通用SPI 模式。 ? SPI 存储器(SPI Memory) 模式 SPI 存储器模式(SPI0, SPI1 和SPI2)用于连接SPI 接口的外部存储器。SPI 存储器模式下数据传输长度 以字节为单位,最高支持 8 线 STR/DDR 读写操作。时钟频率可配置, STR 模式下支持的最高时钟频率为 80 MHz,DDR 模式下支持的最高时钟频率为40 MHz。 ? SPI2 通用SPI (GP-SPI) 模式 SPI2 作为通用 SPI 时,既可以配置成主机模式,又可以配置成从机模式。主机模式支持 2 线全双工和 1/2/4/8 线半双工通信;从机模式支持 2 线全双工和 1/2/4 线半双工通信。通用 SPI 的主机时钟频率可配 置;数据传输长度以字节为单位;时钟极性(CPOL) 和相位(CPHA) 可配置;可连接DMA 通道。 – 在 2 线全双工通信模式下, 主机的时钟最高频率为 80 MHz,从机的时钟最高频率为 40 MHz。支持 SPI 传输的4 种时钟模式。 – 在主机1/2/4/8 线半双工通信模式下,时钟频率最高为80 MHz,支持SPI 传输的4 种时钟模式。 – 在从机1/2/4 线半双工通信模式下,时钟频率最高为40 MHz,也支持SPI 传输的4 种时钟模式。 ? SPI3 通用SPI (GP-SPI) 模式 SPI3 只能作为通用SPI,既可以配置成主机模式,又可以配置成从机模式,具有2 线全双工和1 线半双工 通信功能。通用SPI 的主机时钟频率可配置;数据传输长度以字节为单位;时钟极性(CPOL) 和相位(CPHA) 可配置;可连接DMA 通道。 – 在 2 线全双工通信模式下, 主机的时钟频率最高为 80 MHz,从机的时钟频率最高为 40 MHz。支持 SPI 传输的4 种时钟模式。 – 在1 线半双工通信模式下,主机的时钟频率最高为80 MHz,支持SPI 传输的4 种时钟模式;从机的 时钟频率最高为40 MHz,也支持SPI 传输的4 种时钟模式。
在频率这一部分我查阅了很多资料,对于ESP32我看到的大多数描述是如果使用IO_MUX,那么最高频率能到80MHz,如果使用GPIO矩阵,那么最高频率只能到26.6MHz,否则ESP_LOG将会报错;我手上的是ESP32-S2,翻阅了乐鑫官方的数据手册,并没有看到描述必须使用IO_MUX才能将频率配置到80MHz,同时ESP32-S2的IO_MUX列表中我并没有找到SPI3的标注,之有SPI和FSPI。所以对于能不呢配置成80MHz,我们只需要直接配置成80MHz然后打开串口终端,观察是否打印错误信息即可。但是对于实际的频率,并不能确定是80MHz。
一、SPI-Master
我们用SPI主机模式来驱动LCD
ESP32共有4个SPI,与我们熟悉的STM32来说,并不是叫做SPI1,SPI2,SPI3,在ESP32的SPI中,SPI0和SPI1是不提供给用户使用的,在模组中,SPI0/1直接连接外挂的FLASH,当然有一部分ESP32芯片也是带有片上FLASH的,但同样不能将SPI0/1用来驱动其他模块,提供的功能也仅是与外挂的FLASH进行连接。剩余的两个SPI也不叫做SPI2/3,而是叫做FSPI和HSPI,对于这个命名我一开始认为是Fast SPI和High-Speed SPI的缩写,但实际上并不能这样理解,暂且认为它和SPI2/3相同。
二、使用SPI-Master驱动
1.首先配置两个结构体:
通过调用 spi_bus_initialize 初始化 SPI 总线. 确保在 bus_config 结构中设置正确的 IO 引脚. 注意将不需要的信号设置为 -1. 通过调用 spi_bus_add_device 告诉驱动程序连接到总线的 SPI 从设备. 确保在 dev_config 结构中配置设备具有的任何时序要求. 您现在应该拥有该设备的句柄,以便在发送事务时使用. 要与设备交互,请使用您需要的任何事务参数填充一个或多个 spi_transaction_t 结构. 然后以轮询方式或中断方式发送它们:
- 中断方式:通过调用 spi_device_queue_trans 将事务添加到队列中,之后使用
spi_device_get_trans_result 查询结果,或者通过将它们提供给 spi_device_transmit 来处理所有请求. - 轮询方式:调用 spi_device_polling_transmit 发送轮询事务。或者,如果要在它们之间插入内容,可以通过
spi_device_polling_start 和 spi_device_polling_end 发送轮询事务。
可选:要对设备执行事务,请在事务之前调用 spi_device_acquire_bus,并在事务之后调用 spi_device_release_bus。 可选:要卸载设备的驱动程序,请以设备句柄作为参数调用 spi_bus_remove_device 可选:要删除总线的驱动程序,请确保没有连接更多驱动程序并调用 spi_bus_free.
下面我们先看看LCD的原理图:
这里的SCL引脚对应SPI的SCLK引脚,SDA引脚对应SPI的MOSI引脚,这里并不需要使用MISO,所以我们将其配置为-1,RES是LCD的复位引脚,在初始化时先要将LCD复位,CS引脚是模块的片选引脚,对于SPI外设,最多可以有三个CS来选择是给所连接的三个外设中的哪一个进行数据传输,CS低电平有效。这里LCD直接连接到了地,所以我们并不需要使用ESP32的硬件SPI的CS来控制。DC是写寄存器/写数据的选择引脚,这里我们直接用软件控制的方式,BLK是LCD的背光选择引脚,记得打开背光,LCD才能正常显示。
下面我们直接来进行配置:
首先我么使用宏来定义使用的管脚:
#define LCD_SCLK_PIN 3
#define LCD_MOSI_PIN 4
#define LCD_MISO_PIN -1
#define LCD_CS_PIN -1
#define LCD_HOST FSPI_HOST
#define DMA_CHAN SPI_DMA_CH_AUTO
这里的DMA_CHAN 在官方例程中,是直接配置为LCD_HOST,也就是说我们的LCD_HOST是配置的FSPI_HOST的宏,FSPI_HOST是2,那么DMA通道也就配置成了2; 但是对于ESP32-S2略有不同;我们先看看SDK中的预编译内容:
typedef enum {
SPI_DMA_DISABLED = 0,
#if CONFIG_IDF_TARGET_ESP32
SPI_DMA_CH1 = 1,
SPI_DMA_CH2 = 2,
#endif
SPI_DMA_CH_AUTO = 3,
} spi_common_dma_t;
这里对DMA通道进行了枚举,我们使用的是ESP32-S2所以将DMA通道配置为SPI_DMA_CH_AUTO 对于最大传输的Size我们直接配置为默认的最大4094即可; 记得首先要创建SPI句柄:
static spi_device_handle_t spi;
spi_bus_config_t buscfg = {
.miso_io_num = LCD_MISO_PIN,
.mosi_io_num = LCD_MOSI_PIN,
.sclk_io_num = LCD_SCLK_PIN,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 4094};
spi_device_interface_config_t devcfg = {
.clock_speed_hz = SPI_MASTER_FREQ_80M,
.mode = 2,
.spics_io_num = LCD_CS_PIN,
.queue_size = 7
};
然后我们使用刚刚声明的两个结构体变量和spi句柄进行初始化和安装spi驱动:
void Lcd_SpiInit(void)
{
spi_bus_initialize(LCD_HOST, &buscfg, DMA_CHAN);
spi_bus_add_device(LCD_HOST, &devcfg, &spi);
}
2.改写LCD发送数据的函数
void Lcd_Cmd(const uint8_t cmd)
{
esp_err_t ret;
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.length = 8;
t.tx_buffer = &cmd;
t.user = (void *)0;
ret = spi_device_polling_transmit(spi, &t);
assert(ret == ESP_OK);
}
这里是通过spi_transaction_t这个结构体来进行传输数据的写入和发送; 对于小于8bit的数据,我们需要使用rx_data这个结构体成员进行数据写入,并且要配置flag。我们这里使用的最小数据为8bit,直接使用tx_buffer成员即可。
我们再编写一个写任意长度数据的函数,将其用于刷屏的函数,用于提高速度:
IRAM_ATTR void Lcd_Datax(uint16_t *dat, uint32_t len)
{
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.length = len;
t.tx_buffer = dat;
t.user = (void *)0;
esp_err_t ret = spi_device_polling_transmit(spi, &t);
assert(ret == ESP_OK);
}
然后我们将LCD写8位数据和写16位数据的函数中IO模拟发送数据的部分改位硬件SPI发送数据:
void Lcd_WriteByte(uint8_t dat)
{
Lcd_SetDCLevel(1);
Lcd_Cmd(dat);
}
void Lcd_WriteData(uint16_t dat)
{
Lcd_SetDCLevel(1);
Lcd_Cmd(dat >> 8);
Lcd_Cmd(dat);
}
改写写命令函数:
void Lcd_WriteReg(uint8_t cmd)
{
Lcd_SetDCLevel(0);
Lcd_Cmd(cmd);
}
改写清屏函数:
void Lcd_Clear(uint16_t color)
{
Lcd_SetAddress(0, 0, LCD_Width - 1, LCD_Height - 1);
Lcd_SetDCLevel(1);
uint16_t color_temp[240 * 2];
memset(color_temp, color, sizeof(color_temp));
for (uint16_t i = 0; i < LCD_Width / 2; i++)
{
Lcd_Datax(color_temp, 240 * 16 * 2);
}
}
最后记得在初始化中删除掉初始化MOSI引脚和SCLK引脚的部分,初始化SPI后就不需要把它们设置为普通IO了,记得在LCD初始化中调用SPI初始化函数:
Lcd_SpiInit();
gpio_pad_select_gpio(LCD_RES_PIN);
gpio_pad_select_gpio(LCD_DC_PIN);
gpio_pad_select_gpio(LCD_BLK_PIN);
gpio_set_direction(LCD_RES_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction(LCD_DC_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction(LCD_BLK_PIN, GPIO_MODE_OUTPUT);
刷屏测试我们还是使用上次创建的任务:
void Lcd_RefreshTask(void *arg)
{
Lcd_Init();
while (1)
{
BACK_COLOR = WHITE;
Lcd_Clear(WHITE);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(GREEN);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(RED);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(YELLOW);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(BLUE);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(GRAY);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(BROWN);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(MAGENTA);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(CYAN);
vTaskDelay(200 / portTICK_PERIOD_MS);
Lcd_Clear(WHITE);
Lcd_ShowString(0, 0, (uint8_t *)"Test", 12, BLACK);
Lcd_ShowNum(0, 12, 999, 3, 12, BLACK);
Lcd_ShowfloatNum(0, 24, 99.9, 4, 12, BLACK);
vTaskDelay(100 / portTICK_PERIOD_MS);
Lcd_ShowString(40, 40, (uint8_t *)"Test", 16, BLUE);
Lcd_ShowNum(40, 56, 999, 3, 16, BLUE);
Lcd_ShowfloatNum(40, 72, 99.9, 4, 16, BLUE);
vTaskDelay(100 / portTICK_PERIOD_MS);
Lcd_ShowString(80, 80, (uint8_t *)"Test", 24, GREEN);
Lcd_ShowNum(80, 104, 999, 3, 24, GREEN);
Lcd_ShowfloatNum(80, 128, 99.9, 4, 24, GREEN);
vTaskDelay(100 / portTICK_PERIOD_MS);
Lcd_ShowString(140, 120, (uint8_t *)"Test", 32, RED);
Lcd_ShowNum(140, 152, 999, 3, 32, RED);
Lcd_ShowfloatNum(140, 184, 99.9, 4, 32, RED);
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
ESP_LOGI(TAG, "APP Start......");
ESP_ERROR_CHECK(nvs_flash_init());
xTaskCreate(Lcd_RefreshTask, "Lcd_Refresh", 2048, NULL, 4, NULL);
}
这里使用延时是为了看清刷屏的内容,实际硬件SPI非常快;
烧录测试
将编译好的代码烧录进开发板中,观察刷屏显示内容是否和代码中设置相同: 完整工程代码.
|