《DNESP32P4开发指南_V1.0》第十九章 IIC_EXIO实验

第十九章 IIC_EXIO实验

本章将学习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总线挂载多个器件的示意图,如下图所示。

image001

图19.1.1.1 IIC总线挂载多个器件
下面来学习IIC总线协议,IIC总线时序图如下所示:

image004

图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的基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,如下图所示。

image005

图19.1.1.3 写操作通讯过程图
主机首先在IIC总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,如下图所示。

image007

图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位寻址,单次命令序列的场景,如下图所示。

image009

图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引脚图如下图所示。

image011

图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位的,具体格式如下图。

image013

图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器件的八个寄存器,如下图所示。

image015

图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输入状态。寄存器如下图所示。

image017

图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输出电平。寄存器如下图所示。

image019

图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输入/输出模式。寄存器如下图所示。

image021

图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进行使用。这里的时序主要就是写寄存器时序和读寄存器时序,我们一一介绍。
写寄存器时序

image023

图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)的写时序图,简单来说,就是在一个时序中,把两个寄存器都进行设置,大家自行去查看。当然用单字节写入寄存器时序也可完成配置,只不过是需要一个一个寄存器进行配置,这样子的配置过程更为清晰明了。
读寄存器时序

image025

图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器件相关原理图,如下图所示。

image027

图19.2.3.1 XL9555硬件原理图
从上图可知,ESP32P4开发板对XL9555器件16个IO口的设计情况。EXIO_0EXIO_5、EXIO_7和EXIO_13被用作输出IO,而EXIO_8EXIO_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总线,其函数原型如下:

esp_err_t i2c_new_master_bus(const i2c_master_bus_config_t *bus_config, 
i2c_master_bus_handle_t *ret_bus_handle);

函数形参:

QQ截图20260424110615

表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);

函数形参:

QQ截图20260424110631

表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);

函数形参:

QQ截图20260424110640

表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);

函数形参:

QQ截图20260424110654

表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 程序流程图

image030

图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通信配置以及引脚做了相关定义。
#define IIC_NUM_PORT       I2C_NUM_0        /* IIC0 */
#define IIC_SPEED_CLK      400000           /* 速率400K */
#define IIC_SDA_GPIO_PIN   GPIO_NUM_33      /* IIC0_SDA引脚 */
#define IIC_SCL_GPIO_PIN   GPIO_NUM_32      /* IIC0_SCL引脚 */

我们选择使用IIC0,且通信速率设置为400K,IIC0引脚方面,选择IO33作为IIC的SDA数据线,IO32作为IIC的SCL时钟线。
下面我们再解析myiic.c的程序,看一下初始化函数myiic_init,代码如下:

/**
 * @brief     	初始化MYIIC
 * @param   	无
 * @retval    	ESP_OK:初始化成功
 */
esp_err_t myiic_init(void)
{
    i2c_master_bus_config_t i2c_bus_config = {
        .clk_source                     = I2C_CLK_SRC_DEFAULT,  /* 时钟源 */
        .i2c_port                       = IIC_NUM_PORT,         /* I2C端口 */
        .scl_io_num                     = IIC_SCL_GPIO_PIN,     /* SCL管脚 */
        .sda_io_num                     = IIC_SDA_GPIO_PIN,     /* SDA管脚 */
        .glitch_ignore_cnt              = 7,                    /* 故障周期 */
        .flags.enable_internal_pullup   = true,                 /* 内部上拉 */
    };
    /* 新建I2C总线 */
    ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_config, &bus_handle));

    return ESP_OK;
}

在IIC初始化函数中,定义了i2c_bus_config变量,并对其成员进行赋值,IIC端口设置为IIC_NUM_PORT,时钟线设置为IIC_SCL_GPIO_PIN,而数据线设置为IIC_SDA_GPIO_PIN,启用内部上拉,最终调用i2c_new_master_bus函数初始化IIC总线。

  1. XL9555驱动代码
    这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。XL9555驱动源码包括两个文件:xl9555.c和xl9555.h。
    下面先解析XL9555.h的程序。对XL9555的中断引脚和器件地址做了相关定义。
#define XL9555_INT_IO 					GPIO_NUM_36	/* XL9555_INT引脚 */
#define XL9555_ADDR    					0X24		/* 器件地址 */

通过前面的介绍可知,XL9555器件有8个寄存器,所以这里我们也定义了对应的宏,如下所示。

#define XL9555_INPUT_PORT0_REG      	0			/* 输入寄存器0地址 */
#define XL9555_INPUT_PORT1_REG      	1 			/* 输入寄存器1地址 */
#define XL9555_OUTPUT_PORT0_REG     	2     		/* 输出寄存器0地址 */
#define XL9555_OUTPUT_PORT1_REG     	3     		/* 输出寄存器1地址 */
#define XL9555_INVERSION_PORT0_REG  	4     		/* 极性反转寄存器0地址 */
#define XL9555_INVERSION_PORT1_REG  	5     		/* 极性反转寄存器1地址 */
#define XL9555_CONFIG_PORT0_REG     	6    		/* 方向配置寄存器0地址 */
#define XL9555_CONFIG_PORT1_REG     	7    		/* 方向配置寄存器1地址 */

通过前面对XL9555寄存器介绍,我们知道这16个IO口在寄存器的位置都是固定的,基于单个IO操作单位的考虑,所以定义了每个引脚的宏,如下所示:

#define BEEP_IO                     	0x0001		/* 蜂鸣器控制引脚 */
#define SPK_EN_IO                   	0x0002		/* 功放使能引脚 */
#define GBC_LED_IO                  	0x0004		/* ATK_MODULE接口LED引脚 */
#define GBC_KEY_IO                  	0x0008		/* ATK_MODULE接口KEY引脚 */
#define RS485_RE_IO                 	0x0010		/* 485切换发送/接收引脚 */ 
#define SLCD_PWR_IO                 	0x0020		/* SPI_LCD控制背光引脚 */
#define EXIO_6_IO                	 	0x0040		/* 未使用引脚 */
#define SLCD_RST_IO                 	0x0080		/* SPI_LCD复位引脚 */
#define KEY_0_IO                    	0x0100		/* 按键0引脚 */
#define KEY_1_IO                    	0x0200		/* 按键1引脚 */
#define KEY_2_IO                    	0x0400		/* 按键2引脚 */
#define AP_INT_IO                   	0x0800		/* AP3216C中断引脚 */
#define QMI_INT_IO                  	0x1000		/* 六轴传感器中断引脚 */
#define LED_1_IO                    	0x2000		/* LED1引脚 */
#define EXIO_14_IO                  	0x4000		/* 未使用引脚 */
#define EXIO_15_IO                  	0x8000		/* 未使用引脚 */

在程序中,就是通过调用以上宏进行设置使用。
接下来,解析一下xl9555.c的程序,首先先来看一下XL9555器件的初始化函数xl9555_init,代码如下:

/**
 * @brief    	初始化XL9555
 * @param    	无
 * @retval   	ESP_OK:初始化成功
 */
esp_err_t xl9555_init(void)
{
    uint8_t r_data[2];

    /* 未调用myiic_init初始化IIC */
    if (bus_handle == NULL)
    {
        ESP_ERROR_CHECK(myiic_init());
    }

    i2c_device_config_t xl9555_i2c_dev_conf = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,  /* 从机地址长度 */
        .scl_speed_hz    = IIC_SPEED_CLK,       /* 传输速率 */
        .device_address  = XL9555_ADDR,         /* 从机7位的地址 */
    };
    /* I2C总线上添加XL9555设备 */
ESP_ERROR_CHECK(i2c_master_bus_add_device(bus_handle, &xl9555_i2c_dev_conf, 
&xl9555_handle));

    /* 输入模式下,中断才有效(读取IO电平) */
    // xl9555_int_init();

    /* 上电先读取一次清除中断标志 */
    xl9555_read_byte(r_data, 2);
    /* 配置那些扩展管脚为输入输出模式 */
    xl9555_ioconfig(0x1F00);
    /* 关闭蜂鸣器 */
    xl9555_pin_write(BEEP_IO, 1);
    /* 关闭喇叭 */
    xl9555_pin_write(SPK_EN_IO, 1);

    return ESP_OK;
}

在XL9555初始化函数中,首先对xl9555_i2c_dev_conf变量的成员进行赋值,设置XL9555的地址长度、设备地址以及传输速率,然后调用i2c_master_bus_add_device函数对XL9555设备进行初始化。随后调用xl9555_read_byte函数对寄存器进行读取,清除中断标志,以防出错。xl9555_ioconfig函数就是对XL9555设备的IO设置为输入输出功能。为了不让蜂鸣器和喇叭工作,通过xl9555函数设置高电平输出。
接下来,看一下如何向XL9555寄存器写入数据的函数xl9555_write_byte,代码如下。

/**
 * @brief     	向XL9555寄存器写入数据
 * @param    	reg:寄存器地址
 * @param    	data:要写入数据的存储区
 * @param     	len:要写入数据的大小
 * @retval    	ESP_OK:读取成功; 其他:读取失败
 */
esp_err_t xl9555_write_byte(uint8_t reg, uint8_t *data, size_t len)
{
    esp_err_t ret;

    uint8_t *buf = malloc(1 + len);
    if (buf == NULL)
    {
        ESP_LOGE(xl9555_tag, "%s memory failed", __func__);
        return ESP_ERR_NO_MEM;      /* 分配内存失败 */
    }

    buf[0] = reg;                   /* 0号元素为寄存器数值 */
    memcpy(buf + 1, data, len);     /* 拷贝数据至存储区中 */

    ret = i2c_master_transmit(xl9555_handle, buf, len + 1, -1);

    free(buf);                      /* 发送完成释放内存 */

    return ret;
}

该函数的实现,主要调用i2c_master_transmit函数。在这里需要进行数据整合,把寄存器地址和要写入到寄存器的数据重新存放到一个buf。在这里需要注意存放顺序,寄存器地址要在写入寄存器的数据前面,这样子通过i2c_master_transmit函数发送出去的数据才符合XL9555写数据操作。
继续看一下如何读取XL9555的IO值的函数xl9555_read_byte,代码如下。

/**
 * @brief    	读取XL9555的IO值
 * @param     	data:读取数据的存储区
 * @param     	len:读取数据的大小
 * @retval    	ESP_OK:读取成功; 其他:读取失败
 */
esp_err_t xl9555_read_byte(uint8_t *data, size_t len)
{
    uint8_t reg_addr = XL9555_INPUT_PORT0_REG;
    
    return i2c_master_transmit_receive(xl9555_handle, &reg_addr, 1,data,len,-1);
}

在上述函数中,需要指定寄存器地址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函数,定义如下。

/**
 * @brief   	按键扫描函数
 * @param       mode:0->不连续;1->连续
 * @retval      键值, 定义如下:
 *              KEY0_PRES, 1, KEY0按下
 *              KEY1_PRES, 2, KEY1按下
 *              KEY2_PRES, 3, KEY2按下
 */
uint8_t xl9555_key_scan(uint8_t mode)
{
    uint8_t keyval = 0;
    static uint8_t key_up = 1;

    if (mode)
    {
        key_up = 1;
    }
    
    if (key_up && (KEY0 == 0 || KEY1 == 0 || KEY2 == 0))
    {
        esp_rom_delay_us(10000);
        key_up = 0;

        if (KEY0 == 0)
        {
            keyval = KEY0_PRES;
        }

        if (KEY1 == 0)
        {
            keyval = KEY1_PRES;
        }

        if (KEY2 == 0)
        {
            keyval = KEY2_PRES;
        }

    }
    else if (KEY0 == 1 && KEY1 == 1 && KEY2 == 1)
    {
        key_up = 1;
    }

    return keyval;
}

上述函数中,实现的逻辑跟key_scan函数是相似,只不过在这里,是通过xl9555_read_pin函数对IO口状态进行读取和判断。KEY0、KEY1、KEY2都是宏函数,定义如下:

#define KEY0                        xl9555_pin_read(KEY_0_IO)
#define KEY1                        xl9555_pin_read(KEY_1_IO)
#define KEY2                        xl9555_pin_read(KEY_2_IO)

xl9555_key_scan函数的形参mode,可用于设置是否支持连按。而函数的返回值为按键的键值,比如KEY0_PRES、KEY1_PRES和KEY2_PRES,这三个宏都在头文件中存在,定义如下。

#define KEY0_PRES                   1
#define KEY1_PRES                   2
#define KEY2_PRES                   3

文件中的其他函数请大家自行查看源码,都有详细的注释。

  1. CMakeLists.txt文件
    本例程的功能实现主要依靠IIC驱动和XL9555驱动。要在main函数中,成功调用XL9555文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:
set(src_dirs
           	LED
           	MYIIC
          	XL9555)

set(include_dirs
          	LED
           	MYIIC
           	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)
  1. main.c驱动代码
    在main.c里面编写如下代码。
void app_main(void)
{
    esp_err_t ret;
    uint8_t exio_key = 0;
    
    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());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    myiic_init();       		/* 初始化IIC0 */
    xl9555_init();      		/* 初始化XL9555 */

    while(1)
    {
        exio_key = xl9555_key_scan(0);
        
        switch (exio_key)
        {
            case KEY0_PRES: /* 打开LED1和蜂鸣器 */
                xl9555_pin_write(LED_1_IO, 0);
                xl9555_pin_write(BEEP_IO, 0);
                break;

            case KEY1_PRES: /* 关闭LED1 */
                xl9555_pin_write(LED_1_IO, 1);
                break;

            case KEY2_PRES: /* 关闭蜂鸣器 */
                xl9555_pin_write(BEEP_IO, 1);
                break;

            default:
                break;
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

在app_main函数中,调用完myiic_init函数和xl9555_init函数,就进入到死循环。在循环中,每隔10毫秒就调用xl9555_key_scan函数扫描按键状态,如果KEY0被按下,打开LED1和蜂鸣器;如果KEY1被按下,关闭LED1;如果KEY2被按下,关闭蜂鸣器。

19.4 下载验证
下载代码完成后,我们可以按KEY0、KEY1和KEY2来看看LED1和蜂鸣器的变化,是否跟我们预期的结果一致?

posted @ 2026-04-28 14:27  正点原子  阅读(6)  评论(0)    收藏  举报