|
第十九章 IIC_EXIO实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
本章将学习ESP32-P4的硬件IIC接口去驱动IO扩展芯片XL9555,达到扩展IO的目的。在本章节,实现和XL9555之间的双向通信,将使用其IO的输入输出功能。
本章分为如下几个小节:
19.1 IIC及XL9555介绍
19.2 硬件设计
19.3 程序设计
19.4 下载验证
19.1 IIC及XL9555介绍
19.1.1 IIC介绍
IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据,在CPU与被控IC之间、IC与IC之间进行双向传送。
IIC总线有如下特点:
①总线由数据线SDA和时钟线SCL构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
②总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
③数据线SDA和时钟线SCL都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平。
④总线上数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s。
⑤总线支持设备连接。在使用IIC通信总线时,可以有多个具备IIC通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容400pF的限制决定。IIC总线挂载多个器件的示意图,如下图所示。
图19.1.1.1 IIC总线挂载多个器件
下面来学习IIC总线协议,IIC总线时序图如下所示:
图19.1.1.2 IIC总线时序图
为了便于大家更好的了解IIC协议,我们从起始信号、停止信号、应答信号、数据有效性、数据传输以及空闲状态等6个方面讲解,大家需要对应图19.1.1.2的标号来理解。
① 起始信号
当SCL为高电平期间,SDA由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
② 停止信号
当SCL为高电平期间,SDA由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
观察上图标号③就可以发现,有效应答的要求是从机在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主机接收器发送一个停止信号。
④ 数据有效性
IIC总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态
IIC总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
了解前面的知识后,下面介绍一下IIC的基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,如下图所示。
图19.1.1.3 写操作通讯过程图
主机首先在IIC总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,如下图所示。
图19.1.1.4 读操作通讯过程图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的8bit数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回8bit数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
19.1.2 ESP32-P4的IIC介绍
ESP32-P4有三个硬件IIC控制器,主系统两个,而低功耗系统一个。主系统中的两个IIC控制器可以作为主控制器或从控制器,而低功耗系统中的IIC控制器只能作为主控制器。本章节主要针对主系统的IIC控制器作讲解。
ESP32-P4的IIC控制器有以下几个特点:
支持主机模式和从机模式
支持多主机和从机通信
支持标准模式(100 Kbit/s)、快速模式(400 Kbit/s)
支持7位以及10位地址寻址
支持拉低SCL时钟实现连续数据传输
支持可编程数字噪音滤波功能
支持从机地址和从机内存或寄存器地址的双寻址模式
IIC控制器通过GPIO交换矩阵可配置使用任意GPIO管脚。
下面介绍ESP32-P4的IIC主机写入从机,7位寻址,单次命令序列的场景,如下图所示。
图19.1.2.1 IIC主机写7位寻址的从机
在ESP32-P4硬件IIC控制器中,都有相对应的空间存放相对应的内容。比如上图中,在cmd内存区中存放的是就是命令序列,就比如前面提及到的起始信号、写过程、读过程、停止信号;在RAM内存区中存放的就是某些命令序列携带的内容。
当主机在软件配置好命令序列和RAM数据后,操作寄存器启动数据传输时。控制器的行为可分为以下四步:
1、等待SCL线位高电平,以避免SCL线被其他主机或者从机占用。
2、执行RSTART命令发送START位。即发送起始信号。
3、执行WRITE命令从RAM的首地址开始取出N+1个字节并一次发送给从机,其中第一个字节为地址。这个过程中会产生对应的时序,携带数据进行发送。
4、发送STOP命令,即发送停止信号。
19.1.3 XL9555介绍
XL9555是一款24引脚的CMOS器件,支持IIC总线或SMBus接口进行驱动。XL9555器件是一个16位通用并行输入/输出(GPIO)扩展器,可用其GPIO连接按键、LED、传感器等,解决需要额外的I/O的需求。
XL9555有如下特性:
IIC总线至16位GPIO扩展器
工作电源电压范围为2.3 V至5.5 V
低待机电流消耗
5 V容错I/O端口
400 kHz快速模式IIC总线时钟频率
SCL/SDA输入上的噪声滤波器
内部通电复位
器件地址由3个硬件地址引脚决定,最多可在总线上挂载8个器件
中断脚为开漏输出模式(低电平有效)
16个I/O引脚,默认为16个输入
简单概括一下,XL9555可使用400kHz速率的IIC通信接口与微控制器进行连接,也就是用2根通信线可扩展使用16个IO。XL9555器件地址会由三个硬件地址引脚决定,理论上在这个IIC总线上可挂载8个XL9555器件,足以满足IO引脚需求。XL9555上电进行复位,16个I/O口默认为输入模式,当输入模式的IO口状态发生变化时,即发生从高电平变低电平或者从低电平变高电平,中断脚会拉低。当中断有效后,必须对XL9555进行一次读取/写入操作,复位中断,才可以输出下一次中断,否则中断将一直保持。
XL9555引脚图如下图所示。
图19.1.3.1 XL9555器件引脚图
XL9555器件总共有24个管脚,分别为电源线VCC、地线GND、GPIO口、通信线、地址线,上图用不同底色标注出来了。16个GPIO分为了2组,一组是8个,分为是P0x和P1x,这些GPIO都可通过器件寄存器进行配置作为输出或者输出使用。通信线就是SDA和SCL,中断线INT也划分过来通信线。而地址线就是A0、A1和A2,用来决定器件地址。
19.1.3.1 XL9555寻址
要进行IIC通信,首先得知道器件地址,XL9555器件地址是7位的,具体格式如下图。
图19.1.3.1.1 XL9555地址格式
从上图可以知道,XL9555器件地址由两部分组成,一部分就是“Fixed bits”即固定的4位“0100”;另一部分就是“Programmable bits”即可编程的3位“A2 A1 A0”,在硬件上,把A0和A1连接GND,而把A2连接VCC,所以这三位为“100”。最终可得到,XL9555器件地址为“0100100”即0x24。读操作地址就为0x49,即0100 1001;写操作地址就为0x48,即0100 1000。
19.1.3.2 XL9555寄存器介绍
接下来,介绍一下XL9555器件的八个寄存器,如下图所示。
图19.1.3.2.1 XL9555寄存器
由于在IIC通信中,数据都是以字节作为单位,表示寄存器地址的数据也是1个字节。由于XL9555器件只有八个寄存器,所以这里1个字节用3个位表示,即Table 5中的B2、B1和B0。这8个寄存器都是XL9555器件的16个GPIO进行配置,其实分为4种:输入查询、输出设置、极性翻转和端口配置,每种都有两个寄存器对应的就是P0端口和P1端口。
地址0x00和0x01的寄存器是“Input Port0”和“Input Port1”寄存器,主要用于获取P0和P1的IO输入状态。寄存器如下图所示。
图19.1.3.2.2 XL9555的Input Port Register详情
该寄存器只反应引脚输入逻辑电平情况,不管IO是设置成输入还是输出模式。打个比方,从0x00地址处(Input Port 0 Register)读出的数据是0x55,以二进制展开为01010101,从高位到低位对应的就是P07~P00的IO状态,P00的输入电平状态就为高电平。
地址0x02和0x03的寄存器是“Output Port0”和“Output Port1”寄存器,主要用于设置P0和P1的IO输出电平。寄存器如下图所示。
图19.1.3.2.3 XL9555的Output Port Register详情
该寄存器设置的是已经配置成输出模式的IO口的IO输出状态,1代表的是高电平,0代表的都是低电平,配置IO为输出模式的寄存器为Configuration Port寄存器。寄存器的一些位值对已经设置成输入模式的IO口是没有影响的。该寄存器还支持读取,读取到的值只是设置值,并不是实际引脚电平值,实际电平值通过Input Port寄存器查询即可。
地址0x04和0x05的寄存器是“Polarity Inversion Port0”和“Polarity Inversion Port1”寄存器,用于对端口0和端口1进行极性翻转。该寄存器值默认为0,所以对IO电平翻转功能并没有启用,且在本实验也没有用到,所以不做讲解,详细说明可看《XL9555数据手册》P13。
地址0x06和0x07的寄存器是“Configuration Port1”和“Configuration Port0”寄存器,用于配置P0和P1的IO输入/输出模式。寄存器如下图所示。
图19.1.3.2.4 XL9555的Configuration Port Register详情
该寄存器某一个位设置成1即作为输入模式,设置成0即作为输入模式。打个比方,要向0x06地址处(Configuration Port 0 Register)写入的数据是0x55,以二进制展开为01010101,从高位到低位对应的就是P07~P00的IO配置模式,P00、P02、P04、P06这四个IO口即配置为输入模式,而P01、P03、P05、P07这四个IO口配置为输出模式。XL9555上电复位后,所有IO口默认都是输入状态,即上图这两个寄存器读出来的值都是0xFF。
19.1.3.3 XL9555时序介绍
ESP32-S3是通过IIC总线跟XL9555进行通信的,对XL9555相关寄存器进行写入配置,对其16个IO进行使用。这里的时序主要就是写寄存器时序和读寄存器时序,我们一一介绍。
写寄存器时序
图19.1.3.3.1 单字节写入到寄存器时序图
上图中展示的是主机将单字节写入到寄存器的时序,主机在IIC总线发送第1个字节的数据为XL9555的写操作地址0x40(设备地址0x20 << 1 | 0),用于寻找总线上找到XL9555,在获得XL9555的应答信号之后,继续发送第2个字节数据,该字节数据是XL9555的寄存器地址,再等到XL9555的应答信号,主机继续发送第3字节数据,这里的数据即是写入在第2字节寄存器地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
在《XL9555数据手册》P16中,还提供有对Output Port寄存器组(0x02和0x03)的写时序图,简单来说,就是在一个时序中,把两个寄存器都进行设置,大家自行去查看。当然用单字节写入寄存器时序也可完成配置,只不过是需要一个一个寄存器进行配置,这样子的配置过程更为清晰明了。
读寄存器时序
图19.1.3.3.2 单字节读取寄存器时序图
上图中展示的是主机从寄存器中读取一个字节数据的时序图。XL9555读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送XL9555的写操作地址0x48(设备地址0x24<< 1 | 0),获取从机应答信号后,接着发送需要读取的寄存器地址;在读时序中,起始信号产生后,主机发送XL9555的读操作地址0x49(设备地址0x24 << 1 | 1),获取从机应答信号后,接着从机返回刚刚在写时序中寄存器地址的数据,以字节为单位传输在总线上,主机接收到寄存器的数据后,发出非应答信号并以停止信号结束通信过程。
假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,即把往后寄存器的数据也发送到总线上,这就是《XL9555数据手册》P17中从寄存器中读取多个字节的时序,大家可自行去查看。
19.2 硬件设计
19.2.1 例程功能
通过按下KEY0~2按键来控制蜂鸣器和LED灯开关状态,KEY0打开LED1和BEEP;KEY1关闭LED1;KEY2关闭BEEP。
19.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)XL9555
IIC_INT - IO36
IIC_SDA - IO33
IIC_SCL - IO32
EXIO_0 - BEEP
EXIO_8 - KEY0
EXIO_9 - KEY1
EXIO_10 - KEY2
EXIO_13 - LED1
19.2.3 原理图
XL9555器件相关原理图,如下图所示。
图19.2.3.1 XL9555硬件原理图
从上图可知,ESP32P4开发板对XL9555器件16个IO口的设计情况。EXIO_0~EXIO_5、EXIO_7和EXIO_13被用作输出IO,而EXIO_8~EXIO_12被用作输入IO,EXIO6、EXIO14和EXIO15作为未使用IO。
本实验主要就是用到XL9555器件的5个IO,分为EXIO_0(BEEP)、EXIO_8(KEY0)、EXIO_9(KEY1)、EXIO_10(KEY2)和EXIO_13(LED1)。
19.3 程序设计
19.3.1 IIC的IDF驱动
IIC外设驱动位于ESP-IDF下的components/esp_driver_i2c目录下。使用IIC功能,必须先导入以下头文件:
- #include "driver/i2c_master.h"
复制代码 接下来,作者将介绍一些常用的函数,这些函数的描述及其作用如下:
1,IIC总线初始化函数i2c_new_master_bus
该函数用于初始化IIC总线,其函数原型如下:
- <font size="3">esp_err_t i2c_new_master_bus(const i2c_master_bus_config_t *bus_config, </font>
- <font size="3">i2c_master_bus_handle_t *ret_bus_handle);</font>
复制代码 函数形参:
表19.3.1.1 i2c_new_master_bus函数形参描述
函数返回值:
ESP_OK表示IIC总线初始化成功。
ESP_ERR_INVALID_ARG表示由于错误参数,IIC总线初始化失败。
ESP_ERR_NO_MEM表示由于内存不足,IIC总线创建失败。
ESP_ERR_NOT_FOUND表示没有空闲的IIC总线 。
bus_config为指向IIC总线配置结构体指针。接下来,笔者将介绍i2c_master_bus_config_t结构体中各个成员,如下代码所示:
- typedef struct {
- i2c_port_num_t i2c_port; /* IIC端口 */
- gpio_num_t sda_io_num; /* SDA管脚 */
- gpio_num_t scl_io_num; /* SCL管脚 */
- union {
- i2c_clock_source_t clk_source; /* 时钟源 */
- #if SOC_LP_I2C_SUPPORTED
- lp_i2c_clock_source_t lp_source_clk; /* 低功耗IIC外设时钟源 */
- #endif
- };
- uint8_t glitch_ignore_cnt; /* 总线的故障周期阈值 */
- int intr_priority; /* IIC中断优先级 */
- size_t trans_queue_depth; /* 内部传输队列的深度 */
- struct {
- uint32_t enable_internal_pullup: 1; /* 启用内部上拉 */
- } flags; /* 配置标记 */
- } i2c_master_bus_config_t; /* IIC主机总线配置 */
复制代码 i2c_master_bus_config_t结构体用于配置IIC总线各种参数,以下对各个成员做的简单介绍。
1)i2c_port:
设置IIC控制器使用的IIC端口号,可选I2C_NUM_0或I2C_NUM_1。
2)sda_io_num:
IIC总线的SDA引脚。
3)scl_io_num:
IIC总线的SCL引脚。
4)clk_source:
IIC总线选择源时钟。
5)glitch_ignore_cnt:
IIC总线的故障周期,若线上的故障周期小于此值,便可过滤,通常为7。
6)intr_priority:
IIC的中断优先级。
7)trans_queue_depth:
内部传输队列的深度,仅在异步事务中有效,可不进行配置。
8)enable_internal_pullup:
启用内部上拉,建议在高速通信时,还是得需要外部上拉。
ret_bus_handle为指向IIC总线句柄结构体的指针,而i2c_master_bus_handle_t结构体保存着IIC总线的信息,由于参数非常多,且在此也不需要了解他的成员,所以不作展开,想要了解可自行搜索查看。
2,添加IIC设备到IIC总线函数i2c_master_bus_add_device
该函数用于设置IIC总设备,并挂载在IIC总线上,其函数原型如下:
- esp_err_t i2c_master_bus_add_device(i2c_master_bus_handle_t bus_handle,
- const i2c_device_config_t *dev_config,
- i2c_master_dev_handle_t *ret_handle);
复制代码 函数形参:
表19.3.1.2 i2c_master_bus_add_device函数形参描述
函数返回值:
ESP_OK表示创建IIC从设备成功。
ESP_ERR_INVALID_ARG表示由于错误参数,创建IIC从设备失败。
ESP_ERR_NO_MEM表示由于内存不足,创建IIC从设备失败。
bus_handle为IIC总线句柄结构体,前面已经有说明了。
dev_config为指向IIC设备配置结构体的指针,i2c_device_config_t结构体其定义如下:
- typedef struct {
- i2c_addr_bit_len_t dev_addr_length; /* 从设备的地址长度 */
- uint16_t device_address; /* 从设备的地址 */
- uint32_t scl_speed_hz; /* 从设备的SCL频率 */
- uint32_t scl_wait_us; /* SCL等待时间 */
- struct {
- uint32_t disable_ack_check: 1; /* 关闭ack检查 */
- } flags; /* 配置标记 */
- } i2c_device_config_t; /* IIC设备配置 */
复制代码 i2c_device_config_t结构体用于配置IIC设备的各种参数,以下对各个成员做的简单介绍
1)dev_addr_length:
从设备地址长度,选I2C_ADDR_BIT_LEN_7或I2C_ADDR_BIT_LEN_10。
2)device_address:
IIC设备的设备地址(7/10bit地址,不带读写位)。
3)scl_speed_hz:
IIC的时钟线频率。
4)scl_wait_us:
SCL等待时间,可不对该成员赋值。
5)disable_ack_check:
关闭ACK检查。若开启ack,即对该成员赋值为0,总线上检测到nack,传输将停止。
ret_handle为指向IIC总线从设备句柄结构体的指针,i2c_master_dev_handle_t结构体其实是i2c_master_dev_t,其定义如下:
- struct i2c_master_dev_t {
- i2c_master_bus_t *master_bus; /* 总线 */
- uint16_t device_address; /* 设备地址 */
- uint32_t scl_speed_hz; /* SCL频率 */
- uint32_t scl_wait_us; /* SCL等待时间 */
- i2c_addr_bit_len_t addr_10bits; /* 设备地址(10位) */
- bool ack_check_disable; /* 关闭ack检查 */
- i2c_master_callback_t on_trans_done; /* IIC传输完成回调 */
- void *user_ctx; /* 回调函数传参 */
- };
复制代码 3,IIC发送函数i2c_master_transmit
该函数用于在IIC总线上主机发送数据给从机,其函数原型如下:
- esp_err_t i2c_master_transmit(i2c_master_dev_handle_t i2c_dev,
- const uint8_t *write_buffer,
- size_t write_size,
- int xfer_timeout_ms);
复制代码 函数形参:
表19.3.1.3 i2c_master_transimit函数形参描述
函数返回值:
ESP_OK表示主机发送数据成功。
ESP_ERR_INVALID_ARG表示主机发送数据的参数有误。
ESP_ERR_TIMEOUT表示操作超时,可能总线被占用着或硬件异常。
4,IIC发送和接收函数i2c_master_transmit_receive
该函数用于在IIC总线上主机发送数据并接收数据,其函数原型如下:
- esp_err_t i2c_master_transmit_receive(i2c_master_dev_handle_t i2c_dev,
- const uint8_t *write_buffer,
- size_t write_size,
- uint8_t *read_buffer,
- size_t read_size,
- int xfer_timeout_ms);
复制代码 函数形参:
表19.3.1.4 i2c_master_transimit_receive函数形参描述
函数返回值:
ESP_OK表示主机发送数据成功。
ESP_ERR_INVALID_ARG表示发送数据的参数有误。
ESP_ERR_TIMEOUT表示操作超时,可能总线被占用着或硬件异常。
注意:由于IIC的读操作是一个复合的过程,所以使用i2c_master_transmit_receive函数会比较方便,当然,IIC的IDF驱动是有提供IIC读操作函数i2c_master_receive。不过,在使用i2c_master_receive函数时,若前面需要写操作,还需要调用i2c_master_transmit函数,而i2c_master_transmit_receive就可以将这两步,在一个函数中实现。i2c_master_receive函数这里就不列出来,大家自行查看即可。
19.3.2 程序流程图
图19.3.2.1 IIC_EXIO实验程序流程图
19.3.3 程序解析
在09_iic_exio例程中,作者在09_iic_exio\components\BSP路径下新建了2个文件夹,分别是MYIIC和XL9555,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. IIC驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。IIC驱动源码包括两个文件:myiic.c和myiic.h。
下面先解析myiic.h的程序。对IIC通信配置以及引脚做了相关定义。
- <font size="3">#define IIC_NUM_PORT I2C_NUM_0 /* IIC0 */</font>
- <font size="3">#define IIC_SPEED_CLK 400000 /* 速率400K */</font>
- <font size="3">#define IIC_SDA_GPIO_PIN GPIO_NUM_33 /* IIC0_SDA引脚 */</font>
- <font size="3">#define IIC_SCL_GPIO_PIN GPIO_NUM_32 /* IIC0_SCL引脚 */</font>
复制代码 我们选择使用IIC0,且通信速率设置为400K,IIC0引脚方面,选择IO33作为IIC的SDA数据线,IO32作为IIC的SCL时钟线。
下面我们再解析myiic.c的程序,看一下初始化函数myiic_init,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief 初始化MYIIC</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] 无</font>
- <font size="3"> * @retval ESP_OK:初始化成功</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t myiic_init(void)</font>
- <font size="3">{</font>
- <font size="3"> i2c_master_bus_config_t i2c_bus_config = {</font>
- <font size="3"> .clk_source = I2C_CLK_SRC_DEFAULT, /* 时钟源 */</font>
- <font size="3"> .i2c_port = IIC_NUM_PORT, /* I2C端口 */</font>
- <font size="3"> .scl_io_num = IIC_SCL_GPIO_PIN, /* SCL管脚 */</font>
- <font size="3"> .sda_io_num = IIC_SDA_GPIO_PIN, /* SDA管脚 */</font>
- <font size="3"> .glitch_ignore_cnt = 7, /* 故障周期 */</font>
- <font size="3"> .flags.enable_internal_pullup = true, /* 内部上拉 */</font>
- <font size="3"> };</font>
- <font size="3"> /* 新建I2C总线 */</font>
- <font size="3"> ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &bus_handle));</font>
- <font size="3"> return ESP_OK;</font>
- <font size="3">}</font>
复制代码 在IIC初始化函数中,定义了i2c_bus_config变量,并对其成员进行赋值,IIC端口设置为IIC_NUM_PORT,时钟线设置为IIC_SCL_GPIO_PIN,而数据线设置为IIC_SDA_GPIO_PIN,启用内部上拉,最终调用i2c_new_master_bus函数初始化IIC总线。
2. XL9555驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。XL9555驱动源码包括两个文件:xl9555.c和xl9555.h。
下面先解析XL9555.h的程序。对XL9555的中断引脚和器件地址做了相关定义。
- <font size="3">#define XL9555_INT_IO GPIO_NUM_36 /* XL9555_INT引脚 */</font>
- <font size="3">#define XL9555_ADDR 0X24 /* 器件地址 */</font>
复制代码 通过前面的介绍可知,XL9555器件有8个寄存器,所以这里我们也定义了对应的宏,如下所示。
- <font size="3">#define XL9555_INPUT_PORT0_REG 0 /* 输入寄存器0地址 */</font>
- <font size="3">#define XL9555_INPUT_PORT1_REG 1 /* 输入寄存器1地址 */</font>
- <font size="3">#define XL9555_OUTPUT_PORT0_REG 2 /* 输出寄存器0地址 */</font>
- <font size="3">#define XL9555_OUTPUT_PORT1_REG 3 /* 输出寄存器1地址 */</font>
- <font size="3">#define XL9555_INVERSION_PORT0_REG 4 /* 极性反转寄存器0地址 */</font>
- <font size="3">#define XL9555_INVERSION_PORT1_REG 5 /* 极性反转寄存器1地址 */</font>
- <font size="3">#define XL9555_CONFIG_PORT0_REG 6 /* 方向配置寄存器0地址 */</font>
- <font size="3">#define XL9555_CONFIG_PORT1_REG 7 /* 方向配置寄存器1地址 */</font>
复制代码 通过前面对XL9555寄存器介绍,我们知道这16个IO口在寄存器的位置都是固定的,基于单个IO操作单位的考虑,所以定义了每个引脚的宏,如下所示:
- <font size="3">#define BEEP_IO 0x0001 /* 蜂鸣器控制引脚 */</font>
- <font size="3">#define SPK_EN_IO 0x0002 /* 功放使能引脚 */</font>
- <font size="3">#define GBC_LED_IO 0x0004 /* ATK_MODULE接口LED引脚 */</font>
- <font size="3">#define GBC_KEY_IO 0x0008 /* ATK_MODULE接口KEY引脚 */</font>
- <font size="3">#define RS485_RE_IO 0x0010 /* 485切换发送/接收引脚 */ </font>
- <font size="3">#define SLCD_PWR_IO 0x0020 /* SPI_LCD控制背光引脚 */</font>
- <font size="3">#define EXIO_6_IO 0x0040 /* 未使用引脚 */</font>
- <font size="3">#define SLCD_RST_IO 0x0080 /* SPI_LCD复位引脚 */</font>
- <font size="3">#define KEY_0_IO 0x0100 /* 按键0引脚 */</font>
- <font size="3">#define KEY_1_IO 0x0200 /* 按键1引脚 */</font>
- <font size="3">#define KEY_2_IO 0x0400 /* 按键2引脚 */</font>
- <font size="3">#define AP_INT_IO 0x0800 /* AP3216C中断引脚 */</font>
- <font size="3">#define QMI_INT_IO 0x1000 /* 六轴传感器中断引脚 */</font>
- <font size="3">#define LED_1_IO 0x2000 /* LED1引脚 */</font>
- <font size="3">#define EXIO_14_IO 0x4000 /* 未使用引脚 */</font>
- <font size="3">#define EXIO_15_IO 0x8000 /* 未使用引脚 */</font>
复制代码 在程序中,就是通过调用以上宏进行设置使用。
接下来,解析一下xl9555.c的程序,首先先来看一下XL9555器件的初始化函数xl9555_init,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief 初始化XL9555</font>
- <font size="3"> * @param 无</font>
- <font size="3"> * @retval ESP_OK:初始化成功</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t xl9555_init(void)</font>
- <font size="3">{</font>
- <font size="3"> uint8_t r_data[2];</font>
- <font size="3"> /* 未调用myiic_init初始化IIC */</font>
- <font size="3"> if (bus_handle == NULL)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(myiic_init());</font>
- <font size="3"> }</font>
- <font size="3"> i2c_device_config_t xl9555_i2c_dev_conf = {</font>
- <font size="3"> .dev_addr_length = I2C_ADDR_BIT_LEN_7, /* 从机地址长度 */</font>
- <font size="3"> .scl_speed_hz = IIC_SPEED_CLK, /* 传输速率 */</font>
- <font size="3"> .device_address = XL9555_ADDR, /* 从机7位的地址 */</font>
- <font size="3"> };</font>
- <font size="3"> /* I2C总线上添加XL9555设备 */</font>
- <font size="3">ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &xl9555_i2c_dev_conf, </font>
- <font size="3">&xl9555_handle));</font>
- <font size="3"> /* 输入模式下,中断才有效(读取IO电平) */</font>
- <font size="3"> // xl9555_int_init();</font>
- <font size="3"> /* 上电先读取一次清除中断标志 */</font>
- <font size="3"> xl9555_read_byte(r_data, 2);</font>
- <font size="3"> /* 配置那些扩展管脚为输入输出模式 */</font>
- <font size="3"> xl9555_ioconfig(0x1F00);</font>
- <font size="3"> /* 关闭蜂鸣器 */</font>
- <font size="3"> xl9555_pin_write(BEEP_IO, 1);</font>
- <font size="3"> /* 关闭喇叭 */</font>
- <font size="3"> xl9555_pin_write(SPK_EN_IO, 1);</font>
- <font size="3"> return ESP_OK;</font>
- <font size="3">}</font>
复制代码 在XL9555初始化函数中,首先对xl9555_i2c_dev_conf变量的成员进行赋值,设置XL9555的地址长度、设备地址以及传输速率,然后调用i2c_master_bus_add_device函数对XL9555设备进行初始化。随后调用xl9555_read_byte函数对寄存器进行读取,清除中断标志,以防出错。xl9555_ioconfig函数就是对XL9555设备的IO设置为输入输出功能。为了不让蜂鸣器和喇叭工作,通过xl9555函数设置高电平输出。
接下来,看一下如何向XL9555寄存器写入数据的函数xl9555_write_byte,代码如下。
- <font size="3">/**</font>
- <font size="3"> * @brief 向XL9555寄存器写入数据</font>
- <font size="3"> * @param reg:寄存器地址</font>
- <font size="3"> * @param data:要写入数据的存储区</font>
- <font size="3"> * @param len:要写入数据的大小</font>
- <font size="3"> * @retval ESP_OK:读取成功; 其他:读取失败</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t xl9555_write_byte(uint8_t reg, uint8_t *data, size_t len)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> uint8_t *buf = malloc(1 + len);</font>
- <font size="3"> if (buf == NULL)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(xl9555_tag, "%s memory failed", __func__);</font>
- <font size="3"> return ESP_ERR_NO_MEM; /* 分配内存失败 */</font>
- <font size="3"> }</font>
- <font size="3"> buf[0] = reg; /* 0号元素为寄存器数值 */</font>
- <font size="3"> memcpy(buf + 1, data, len); /* 拷贝数据至存储区中 */</font>
- <font size="3"> ret = i2c_master_transmit(xl9555_handle, buf, len + 1, -1);</font>
- <font size="3"> free(buf); /* 发送完成释放内存 */</font>
- <font size="3"> return ret;</font>
- <font size="3">}</font>
复制代码 该函数的实现,主要调用i2c_master_transmit函数。在这里需要进行数据整合,把寄存器地址和要写入到寄存器的数据重新存放到一个buf。在这里需要注意存放顺序,寄存器地址要在写入寄存器的数据前面,这样子通过i2c_master_transmit函数发送出去的数据才符合XL9555写数据操作。
继续看一下如何读取XL9555的IO值的函数xl9555_read_byte,代码如下。
- <font size="3">/**</font>
- <font size="3"> * @brief 读取XL9555的IO值</font>
- <font size="3"> * @param data:读取数据的存储区</font>
- <font size="3"> * @param len:读取数据的大小</font>
- <font size="3"> * @retval ESP_OK:读取成功; 其他:读取失败</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t xl9555_read_byte(uint8_t *data, size_t len)</font>
- <font size="3">{</font>
- <font size="3"> uint8_t reg_addr = XL9555_INPUT_PORT0_REG;</font>
- <font size="3"> </font>
- <font size="3"> return i2c_master_transmit_receive(xl9555_handle, ®_addr, 1,data,len,-1);</font>
- <font size="3">}</font>
复制代码 在上述函数中,需要指定寄存器地址XL9555_INPUT_PORT0_REG,通过传参len,即可决定读取多少个寄存器数据,在本例程中,len主要传的是2,即把XL9555的16个IO状态读取。
xl9555.c文件中的xl9555_pin_write、xl9555_pin_read和xl9555_ioconfig函数都是基于以上的读和写函数实现,只不过就是多了数据的解析处理,这里就不罗列出来了。
由于ESP32-P4相比其他芯片引脚还是有点少,所以用芯片IO控制的按键只有一个。在实际应用场景中,按键还得需要多几个,所以设置XL9555器件有三个IO连接按键。在进行按键检测时,就需要按键扫描函数,在xl9555.c文件里就有一个按键扫描函数xl9555_key_scan函数,定义如下。
- <font size="3">/**</font>
- <font size="3"> * @brief 按键扫描函数</font>
- <font size="3"> * @param mode:0->不连续;1->连续</font>
- <font size="3"> * @retval 键值, 定义如下:</font>
- <font size="3"> * KEY0_PRES, 1, KEY0按下</font>
- <font size="3"> * KEY1_PRES, 2, KEY1按下</font>
- <font size="3"> * KEY2_PRES, 3, KEY2按下</font>
- <font size="3"> */</font>
- <font size="3">uint8_t xl9555_key_scan(uint8_t mode)</font>
- <font size="3">{</font>
- <font size="3"> uint8_t keyval = 0;</font>
- <font size="3"> static uint8_t key_up = 1;</font>
- <font size="3"> if (mode)</font>
- <font size="3"> {</font>
- <font size="3"> key_up = 1;</font>
- <font size="3"> }</font>
- <font size="3"> </font>
- <font size="3"> if (key_up && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0))</font>
- <font size="3"> {</font>
- <font size="3"> esp_rom_delay_us(10000);</font>
- <font size="3"> key_up = 0;</font>
- <font size="3"> if (KEY0 == 0)</font>
- <font size="3"> {</font>
- <font size="3"> keyval = KEY0_PRES;</font>
- <font size="3"> }</font>
- <font size="3"> if (KEY1 == 0)</font>
- <font size="3"> {</font>
- <font size="3"> keyval = KEY1_PRES;</font>
- <font size="3"> }</font>
- <font size="3"> if (KEY2 == 0)</font>
- <font size="3"> {</font>
- <font size="3"> keyval = KEY2_PRES;</font>
- <font size="3"> }</font>
- <font size="3"> }</font>
- <font size="3"> else if (KEY0 == 1 && KEY1 == 1 && KEY2 == 1)</font>
- <font size="3"> {</font>
- <font size="3"> key_up = 1;</font>
- <font size="3"> }</font>
- <font size="3"> return keyval;</font>
复制代码 上述函数中,实现的逻辑跟key_scan函数是相似,只不过在这里,是通过xl9555_read_pin函数对IO口状态进行读取和判断。KEY0、KEY1、KEY2都是宏函数,定义如下:
- <font size="3">#define KEY0 xl9555_pin_read(KEY_0_IO)</font>
- <font size="3">#define KEY1 xl9555_pin_read(KEY_1_IO)</font>
- <font size="3">#define KEY2 xl9555_pin_read(KEY_2_IO)</font>
复制代码 xl9555_key_scan函数的形参mode,可用于设置是否支持连按。而函数的返回值为按键的键值,比如KEY0_PRES、KEY1_PRES和KEY2_PRES,这三个宏都在头文件中存在,定义如下。
- <font size="3">#define KEY0_PRES 1</font>
- <font size="3">#define KEY1_PRES 2</font>
- <font size="3">#define KEY2_PRES 3</font>
复制代码 文件中的其他函数请大家自行查看源码,都有详细的注释。
3. CMakeLists.txt文件
本例程的功能实现主要依靠IIC驱动和XL9555驱动。要在main函数中,成功调用XL9555文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
- <font size="3">set(src_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> MYIIC</font>
- <font size="3"> XL9555)</font>
- <font size="3">set(include_dirs</font>
- <font size="3"> LED</font>
- <font size="3"> MYIIC</font>
- <font size="3"> XL9555)</font>
- <font size="3">set(requires</font>
- <font size="3"> driver)</font>
- <font size="3">idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})</font>
- <font size="3">component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)</font>
复制代码 4. main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> uint8_t exio_key = 0;</font>
- <font size="3"> </font>
- <font size="3"> ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> myiic_init(); /* 初始化IIC0 */</font>
- <font size="3"> xl9555_init(); /* 初始化XL9555 */</font>
- <font size="3"> while(1)</font>
- <font size="3"> {</font>
- <font size="3"> exio_key = xl9555_key_scan(0);</font>
- <font size="3"> </font>
- <font size="3"> switch (exio_key)</font>
- <font size="3"> {</font>
- <font size="3"> case KEY0_PRES: /* 打开LED1和蜂鸣器 */</font>
- <font size="3"> xl9555_pin_write(LED_1_IO, 0);</font>
- <font size="3"> xl9555_pin_write(BEEP_IO, 0);</font>
- <font size="3"> break;</font>
- <font size="3"> case KEY1_PRES: /* 关闭LED1 */</font>
- <font size="3"> xl9555_pin_write(LED_1_IO, 1);</font>
- <font size="3"> break;</font>
- <font size="3"> case KEY2_PRES: /* 关闭蜂鸣器 */</font>
- <font size="3"> xl9555_pin_write(BEEP_IO, 1);</font>
- <font size="3"> break;</font>
- <font size="3"> default:</font>
- <font size="3"> break;</font>
- <font size="3"> }</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(10));</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 在app_main函数中,调用完myiic_init函数和xl9555_init函数,就进入到死循环。在循环中,每隔10毫秒就调用xl9555_key_scan函数扫描按键状态,如果KEY0被按下,打开LED1和蜂鸣器;如果KEY1被按下,关闭LED1;如果KEY2被按下,关闭蜂鸣器。
19.4 下载验证
下载代码完成后,我们可以按KEY0、KEY1和KEY2来看看LED1和蜂鸣器的变化,是否跟我们预期的结果一致?。
|