图22.3.1.1 SPI_LCD实验程序流程图
22.3.2 SPI_LCD函数解析
ESP-IDF提供了一套API来配置SPI。要使用此功能,需要导入必要的头文件:
#include "driver/spi_master.h"
接下来,作者将介绍一些常用的ESP32-S3中的SPI函数,以及IO扩展芯片中用到的函数,这些函数的描述及其作用如下:
1,初始化和配置
该函数用于初始化SPI总线,并配置其GPIO引脚和主模式下的时钟等参数,该函数原型如下所示:
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总线的主机设备ID |
| 指向spi_bus_config_t结构体的指针,用于配置SPI总线的SCLK、MISO、MOSI等引脚以及其他参数 |
| 指定使用哪个DMA通道。 有效值为: SPI_DMA_CH_AUTO, SPI_DMA_DISABLED或1至2之间的数字 |
表22.3.2.1 spi_bus_initialize()函数形参描述
返回值:ESP_OK配置成功。其他配置失败。
该函数使用spi_bus_config_t类型的结构体变量传入,笔者此处列举了我们需要用到的结构体,该结构体的定义如下所示:
typedef struct {
int miso_io_num; /* MISO引脚号 */
int mosi_io_num; /* MOSI引脚号 */
int sclk_io_num; /* 时钟引脚号 */
int quadwp_io_num; /* 用于Quad模式的WP引脚号,未使用时设置为-1 */
int quadhd_io_num; /* 用于Quad模式的HD引脚号,未使用时设置为-1 */
int max_transfer_sz; /* 最大传输大小 */
… /* 其他特定的配置参数 */
} spi_bus_config_t;
完成上述结构体参数配置之后,可以将结构传递给 spi_bus_initialize () 函数,用以实例化SPI。
2,设备配置
该函数用于在SPI总线上分配设备,函数原型如下所示:
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_device_interface_config_t结构体的指针,用于配置SPI设备的通信参数,如时钟速率、SPI模式等。 |
| |
表22.3.2.2 函数spi_bus_add_device()形参描述
返回值:ESP_OK配置成功。其他配置失败。
该函数使用spi_host_device_t类型以及spi_device_interface_config_t类型的结构体变量传入SPI外围设备的配置参数,该结构体的定义如下所示:
/**
* @Brief 带有三个SPI外围设备的枚举,这些外围设备可通过软件访问 */
typedef enum {
/* SPI1只能在ESP32上用作GPSPI */
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
typedef struct {
uint32_t command_bits; /* 命令阶段的位数 */
uint32_t address_bits; /* 地址阶段的位数 */
uint32_t dummy_bits; /* 虚拟阶段的位数 */
int clock_speed_hz; /* 时钟速率 */
uint32_t mode; /* SPI模式(0-3) */
int spics_io_num; /* CS引脚号 */
... /* 其他设备特定的配置参数 */
} spi_device_interface_config_t;
3,数据传输
根据函数功能,以下函数可以归为一类进行讲解,下面将以表格的形式逐个介绍这些函数的作用与参数。
| |
| 该函数用于发送一个SPI事务,等待它完成,并返回结果。 handle:设备的句柄。 trans_desc:指向spi_transaction_t结构体的指针,描述了要发送的事务详情。 |
spi_device_polling_transmit() | 该函数用于发送一个轮询事务,等待它完成,并返回结果。 handle:设备的句柄。 trans_desc:指向spi_transaction_t结构体的指针,描述了要发送的事务详情。 |
表22.3.2.3 SPI数据传输函数描述
22.3.3 SPI_LCD驱动解析
在IDF版的12_spilcd例程中,作者在12_spilcd \components\BSP路径下新增了一个SPI文件夹和一个LCD文件夹,分别用于存放spi.c、spi.h和lcd.c以及lcd.h这四个文件。其中,spi.h和lcd.h文件负责声明SPI以及LCD相关的函数和变量,而spi.c和lcd.c文件则实现了SPI以及LCD的驱动代码。下面,我们将详细解析这四个文件的实现内容。
1,spi.h文件
/* 引脚定义 */
#define SPI_MOSI_GPIO_PIN GPIO_NUM_11 /* SPI2_MOSI */
#define SPI_CLK_GPIO_PIN GPIO_NUM_12 /* SPI2_CLK */
#define SPI_MISO_GPIO_PIN GPIO_NUM_13 /* SPI2_MISO */
该文件下定义了SPI的时钟引脚与通讯引脚。
2,spi.c文件
/**
* @brief 初始化SPI
* @retval 无
*/
void spi2_init(void)
{
esp_err_t ret = 0;
spi_bus_config_t spi_bus_conf = {0};
/* SPI总线配置 */
spi_bus_conf.miso_io_num = SPI_MISO_GPIO_PIN; /* SPI_MISO引脚 */
spi_bus_conf.mosi_io_num = SPI_MOSI_GPIO_PIN; /* SPI_MOSI引脚 */
spi_bus_conf.sclk_io_num = SPI_CLK_GPIO_PIN; /* SPI_SCLK引脚 */
spi_bus_conf.quadwp_io_num = -1; /* SPI写保护信号引脚,该引脚未使能 */
spi_bus_conf.quadhd_io_num = -1; /* SPI保持信号引脚,该引脚未使能 */
spi_bus_conf.max_transfer_sz = 320 * 240 * 2;/* 配置最大传输大小,以字节为单位 */
/* 初始化SPI总线 */
ret = spi_bus_initialize(SPI2_HOST, &spi_bus_conf, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret); /* 校验参数值 */
}
/**
* @brief SPI发送命令
* @param handle : SPI句柄
* @param cmd : 要发送命令
* @retval 无
*/
void spi2_write_cmd(spi_device_handle_t handle, uint8_t cmd)
{
esp_err_t ret;
spi_transaction_t t = {0};
t.length = 8; /* 要传输的位数 一个字节 8位 */
t.tx_buffer = &cmd; /* 将命令填充进去 */
ret = spi_device_polling_transmit(handle, &t); /* 开始传输 */
ESP_ERROR_CHECK(ret); /* 一般不会有问题 */
}
/**
* @brief SPI发送数据
* @param handle : SPI句柄
* @param data : 要发送的数据
* @param len : 要发送的数据长度
* @retval 无
*/
void spi2_write_data(spi_device_handle_t handle, const uint8_t *data, int len)
{
esp_err_t ret;
spi_transaction_t t = {0};
if (len == 0)
{
return; /* 长度为0 没有数据要传输 */
}
t.length = len * 8; /* 要传输的位数 一个字节 8位 */
t.tx_buffer = data; /* 将命令填充进去 */
ret = spi_device_polling_transmit(handle, &t); /* 开始传输 */
ESP_ERROR_CHECK(ret); /* 一般不会有问题 */
}
/**
* @brief SPI处理数据
* @param handle : SPI句柄
* @param data : 要发送的数据
* @retval t.rx_data[0] : 接收到的数据
*/
uint8_t spi2_transfer_byte(spi_device_handle_t handle, uint8_t data)
{
spi_transaction_t t;
memset(&t, 0, sizeof(t));
t.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA;
t.length = 8;
t.tx_data[0] = data;
spi_device_transmit(handle, &t);
return t.rx_data[0];
}
在spi2_init()函数中主要工作就是对于SPI参数的配置,如SPI管脚配置和数据传输大小以及SPI总线配置等,通过该函数就可以完成SPI初始化。
SPI驱动中对SPI的各种操作,请读者结合SPI的时序规定查看本实验的配套实验源码。
3,lcd.h文件
lcd.c和lcd.h文件是驱动函数和引脚接口宏定义以及函数声明等。lcdfont.h头文件存放了4种字体大小不一样的ASCII字符集(12*12、16*16、24*24和32*32)。这个跟oledfont.h头文件一样的,只是这里多了32*32的ASCII字符集,制作方法请回顾OLED实验。下面我们还是先介绍lcd.h文件,首先是LCD的引脚定义:
/* 引脚定义 */
#define LCD_NUM_WR GPIO_NUM_40
#define LCD_NUM_CS GPIO_NUM_21
/* IO操作 */
#define LCD_WR(x) do{ x ? \
(gpio_set_level(LCD_NUM_WR, 1)): \
(gpio_set_level(LCD_NUM_WR, 0)); \
}while(0)
#define LCD_CS(x) do{ x ? \
(gpio_set_level(LCD_NUM_CS, 1)): \
(gpio_set_level(LCD_NUM_CS, 0)); \
}while(0)
#define LCD_PWR(x) do{ x ? \
(xl9555_pin_write(SLCD_PWR_IO, 1)): \
(xl9555_pin_write(SLCD_PWR_IO, 0)); \
}while(0)
#define LCD_RST(x) do{ x ? \
(xl9555_pin_write(SLCD_RST_IO, 1)): \
(xl9555_pin_write(SLCD_RST_IO, 0)); \
}while(0)
/* 常用颜色值 */
#define WHITE 0xFFFF /* 白色 */
#define BLACK 0x0000 /* 黑色 */
#define RED 0xF800 /* 红色 */
#define GREEN 0x07E0 /* 绿色 */
#define BLUE 0x001F /* 蓝色 */
#define MAGENTA 0XF81F /* 品红色/紫红色 = BLUE + RED */
#define YELLOW 0XFFE0 /* 黄色 = GREEN + RED */
#define CYAN 0X07FF /* 青色 = GREEN + BLUE */
/* 非常用颜色 */
#define BROWN 0XBC40 /* 棕色 */
#define BRRED 0XFC07 /* 棕红色 */
#define GRAY 0X8430 /* 灰色 */
#define DARKBLUE 0X01CF /* 深蓝色 */
#define LIGHTBLUE 0X7D7C /* 浅蓝色 */
#define GRAYBLUE 0X5458 /* 灰蓝色 */
#define LIGHTGREEN 0X841F /* 浅绿色 */
#define LGRAY 0XC618 /* 浅灰色(PANNEL),窗体背景色 */
#define LGRAYBLUE 0XA651 /* 浅灰蓝色(中间层颜色) */
#define LBBLUE 0X2B12 /* 浅棕蓝色(选择条目的反色) */
/* 扫描方向定义 */
#define L2R_U2D 0 /* 从左到右,从上到下 */
#define L2R_D2U 1 /* 从左到右,从下到上 */
#define R2L_U2D 2 /* 从右到左,从上到下 */
#define R2L_D2U 3 /* 从右到左,从下到上 */
#define U2D_L2R 4 /* 从上到下,从左到右 */
#define U2D_R2L 5 /* 从上到下,从右到左 */
#define D2U_L2R 6 /* 从下到上,从左到右 */
#define D2U_R2L 7 /* 从下到上,从右到左 */
#define DFT_SCAN_DIR L2R_U2D /* 默认的扫描方向 */
/* 屏幕选择 */
#define LCD_320X240 0
#define LCD_240X240 1
/* LCD信息结构体 */
typedef struct _lcd_obj_t
{
uint16_t width; /* 宽度 */
uint16_t height; /* 高度 */
uint8_t dir; /* 横屏还是竖屏控制:0,竖屏;1,横屏。 */
uint16_t wramcmd; /* 开始写gram指令 */
uint16_t setxcmd; /* 设置x坐标指令 */
uint16_t setycmd; /* 设置y坐标指令 */
uint16_t wr; /* 命令/数据IO */
uint16_t cs; /* 片选IO */
} lcd_obj_t;
/* LCD缓存大小设置,修改此值时请注意!!!!修改这两个值时可能会影响以下函数 lcd_clear/lcd_fill/lcd_draw_line */
#define LCD_TOTAL_BUF_SIZE (320 * 240 * 2)
#define LCD_BUF_SIZE 15360
/* 导出相关变量 */
extern lcd_obj_t lcd_self;
extern uint8_t lcd_buf[LCD_TOTAL_BUF_SIZE];
第一部分的宏定义是对WR/CS引脚的定义,第二部分宏定义是LCD_WR/CS/PWR/RST 引脚操作的定义,接下来的部分是对一些常用颜色的RGB数值以及LCD信息结构体的定义。
4,lcd.c文件
/**
* @brief LCD初始化
* @param 无
* @retval 无
*/
void lcd_init(void)
{
int cmd = 0;
esp_err_t ret = 0;
lcd_self.dir = 0;
lcd_self.wr = LCD_NUM_WR; /* 配置WR引脚 */
lcd_self.cs = LCD_NUM_CS; /* 配置CS引脚 */
gpio_config_t gpio_init_struct;
/* SPI驱动接口配置 */
spi_device_interface_config_t devcfg = {
.clock_speed_hz = 60 * 1000 * 1000, /* SPI时钟 */
.mode = 0, /* SPI模式0 */
.spics_io_num = lcd_self.cs, /* SPI设备引脚 */
.queue_size = 7, /* 事务队列尺寸 7个 */
};
/* 添加SPI总线设备 */
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &MY_LCD_Handle);
ESP_ERROR_CHECK(ret);
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */
gpio_init_struct.mode = GPIO_MODE_OUTPUT; /* 配置输出模式 */
gpio_init_struct.pin_bit_mask = 1ull << lcd_self.wr; /* 配置引脚位掩码 */
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能上拉 */
gpio_config(&gpio_init_struct); /* 引脚配置 */
lcd_hard_reset(); /* LCD硬件复位 */
/* 初始化代码,对2.4寸LCD寄存器进行设置 */
#if SPI_LCD_TYPE
lcd_init_cmd_t ili_init_cmds[] =
{
(……此处省略初始化代码……)
};
#else /* 不为0则视为使用1.3寸SPILCD屏,那么屏幕将不会反显 */
lcd_init_cmd_t ili_init_cmds[] =
{
(……此处省略初始化代码……) };
#endif
/* 循环发送设置所有寄存器 */
while (ili_init_cmds[cmd].databytes != 0xff)
{
lcd_write_cmd(ili_init_cmds[cmd].cmd);
lcd_write_data(ili_init_cmds[cmd].data,
ili_init_cmds[cmd].databytes & 0x1F);
if (ili_init_cmds[cmd].databytes & 0x80)
{
vTaskDelay(120);
}
cmd++;
}
lcd_display_dir(1); /* 设置屏幕方向 */
LCD_PWR(1);
lcd_clear(WHITE); /* 清屏 */
}
从上的代码中可以看出,本章实验的SPILCD驱动是兼容了正点原子的1.3寸与2.4寸SPILCD模块的,因此在加载完SPI设备后,会与SPILCD进行通讯,确定SPILCD的型号,然后根据型号针对性地对SPILCD模块进行配置。
SPILCD驱动中与SPILCD模块通讯的函数,如下所示:
/**
* @brief 发送命令到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方 式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param cmd 传输的8位命令数据
* @retval 无
*/
void lcd_write_cmd(const uint8_t cmd)
{
LCD_WR(0);
spi2_write_cmd(MY_LCD_Handle, cmd);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方 式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的8位数据
* @retval 无
*/
void lcd_write_data(const uint8_t *data, int len)
{
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, data, len);
}
/**
* @brief 发送数据到LCD,使用轮询方式阻塞等待传输完成(由于数据传输量很少,因此在轮询方式处理可提高速度。使用中断方式的开销要超过轮询方式)
* @param data 传输的16位数据
* @retval 无
*/
void lcd_write_data16(uint16_t data)
{
uint8_t dataBuf[2] = {0,0};
dataBuf[0] = data >> 8;
dataBuf[1] = data & 0xFF;
LCD_WR(1);
spi2_write_data(MY_LCD_Handle, dataBuf,2);
}
在上述代码中,lcd_write_cmd()和lcd_write_data()在调用spi的驱动函数前,按照LCD时序图,前者需要先将WR引脚电平信号置0,后者则需要置1。
通过上面介绍的驱动函数就能够与SPILCD模块进行通讯了,而在SPILCD模块的显示屏上显示出特定的图案或字符或设置SPILCD模块的显示方向等等的操作都是能够通过SPILCD模块规定的特定命令来完成的,想深究的读者可以产看正点原子SPILCD模块的用户手册或查看实际使用的SPILCD模块的相关文档。
22.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
set(src_dirs
IIC
LCD
LED
SPI
XL9555)
set(include_dirs
IIC
LCD
LED
SPI
XL9555)
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的红色LCD与SPI驱动需要由开发者自行添加,以确保SPI_LCD驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了SPI_LCD驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
22.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
uint8_t x = 0;
esp_err_t ret;
ret = nvs_flash_init(); /* 初始化NVS */
if (ret == ESP_ERR_NVS_NO_FREE_PAGES
|| ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
led_init(); /* 初始化LED */
i2c0_master = iic_init(I2C_NUM_0); /* 初始化IIC0 */
spi2_init(); /* 初始化SPI2 */
xl9555_init(i2c0_master); /* IO扩展芯片初始化 */
lcd_init(); /* 初始化LCD */
while (1)
{
switch (x)
{
case 0:
{
lcd_clear(WHITE);
break;
}
case 1:
{
lcd_clear(BLACK);
break;
}
case 2:
{
lcd_clear(BLUE);
break;
}
case 3:
{
lcd_clear(RED);
break;
}
case 4:
{
lcd_clear(MAGENTA);
break;
}
case 5:
{
lcd_clear(GREEN);
break;
}
case 6:
{
lcd_clear(CYAN);
break;
}
case 7:
{
lcd_clear(YELLOW);
break;
}
case 8:
{
lcd_clear(BRRED);
break;
}
case 9:
{
lcd_clear(GRAY);
break;
}
case 10:
{
lcd_clear(LGRAY);
break;
}
case 11:
{
lcd_clear(BROWN);
break;
}
}
lcd_show_string(10, 40, 240, 32, 32, "ESP32", RED);
lcd_show_string(10, 80, 240, 24, 24, "SPILCD TEST", RED);
lcd_show_string(10, 110, 240, 16, 16, "ATOM@ALIENTEK", RED);
x++;
if (x == 12)
{
x = 0;
}
LED_TOGGLE();
vTaskDelay(500);
}
}
从上面的代码中可以看出,在初始化完LCD后,便在LCD上显示一些本实验的相关信息,随后便每间隔500毫秒就更换一次LCD屏幕显示的背景色。
22.4 下载验证
在完成编译和烧录操作后,可以看到SPI LCD上不断变换着不同的颜色,LED灯闪烁。