上期我们介绍了ota,实际只介绍了ota的设备端部分,生产环境的ota需要有云端或者上位机一起配合,单方面在设备端做了ota功能,并不能产生实际的价值;而且ota属于升级功能,这部分实现仅仅是单纯的升级功能的本身实现,但实际工程项目和商业产品中,升级是一个大的系统工程,包含升级前后软硬的功能兼容性(协议、数据库等),软件版本管理,固件可信,升级计划,过程管理及监控等等,不是一两期内容能覆盖全的,有机会后面再说明。
下面我们介绍新的内容:i2c。
实物图

原理图
Pasted image 20260621111405.png
Pasted image 20260621105954.png
ESP32通过3个管脚和XL9555相连:
1️⃣ SDA:ESP32-S3的IO41和XL9555的SDA相连
2️⃣ SCL:ESP32-S3的IO42和XL9555的SCL相连
3️⃣ INT:ESP32-S3的IO0通过跳线帽连接XL9555的INT
ESP32-IDF示例代码解析
- 使用peripherals/i2c/i2c_basic工程


- 分析示例代码:
cpp
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
/* ==========================================================================
* I2C 简单示例
*
* 本示例展示如何使用 ESP-IDF 的 I2C Master 驱动:
* 1. 初始化 I2C 主机总线
* 2. 通过 I2C 总线读写外部传感器(MPU9250)的寄存器
*
* 硬件连接:
* - ESP32 SDA 引脚 <---> MPU9250 SDA 引脚
* - ESP32 SCL 引脚 <---> MPU9250 SCL 引脚
* - 无需外接上拉电阻(驱动已启用内部上拉)
*
* 传感器简介:
* MPU9250 是一款 9 轴惯性测量单元(IMU),集成了三轴加速度计、
* 三轴陀螺仪和三轴磁力计。
* ========================================================================== */
#include <stdio.h>
#include "sdkconfig.h" /* Kconfig 自动生成的配置头文件 */
#include "freertos/FreeRTOS.h" /* FreeRTOS 基础定义 */
#include "freertos/task.h" /* FreeRTOS 任务相关 API */
#include "esp_log.h" /* ESP-IDF 日志输出模块 */
#include "driver/i2c_master.h" /* I2C 主机驱动 API */
static const char *TAG = "example"; /* 日志标签,用于 ESP_LOGI 等宏 */
/* ==========================================================================
* I2C 主机配置宏(部分值来自 menuconfig 图形化配置)
* ========================================================================== */
#define I2C_MASTER_SCL_IO CONFIG_I2C_MASTER_SCL /*!< I2C 时钟线 GPIO 编号 */
#define I2C_MASTER_SDA_IO CONFIG_I2C_MASTER_SDA /*!< I2C 数据线 GPIO 编号 */
#define I2C_MASTER_NUM I2C_NUM_0 /*!< 使用 I2C 控制器 0 */
#define I2C_MASTER_FREQ_HZ CONFIG_I2C_MASTER_FREQUENCY /*!< I2C 时钟频率(默认 400kHz) */
#define I2C_MASTER_TX_BUF_DISABLE 0 /*!< 禁用发送缓冲区(不使用异步传输) */
#define I2C_MASTER_RX_BUF_DISABLE 0 /*!< 禁用接收缓冲区(不使用异步传输) */
#define I2C_MASTER_TIMEOUT_MS 1000 /*!< I2C 操作超时时间(毫秒) */
/* ==========================================================================
* MPU9250 传感器寄存器定义
* ========================================================================== */
#define MPU9250_SENSOR_ADDR 0x68 /*!< MPU9250 的 7 位 I2C 从机地址(AD0 引脚接地) */
#define MPU9250_WHO_AM_I_REG_ADDR 0x75 /*!< WHO_AM_I 寄存器地址,上电默认值为 0x71 */
#define MPU9250_PWR_MGMT_1_REG_ADDR 0x6B /*!< 电源管理 1 寄存器地址,bit7 写 1 触发复位 */
#define MPU9250_RESET_BIT 7 /*!< 电源管理寄存器中的复位位(第 7 位) */
/* ==========================================================================
* 函数:mpu9250_register_read
* --------------------------------------------------------------------------
* @brief 从 MPU9250 的指定寄存器读取一段连续数据
*
* @param dev_handle I2C 设备句柄(由 i2c_master_bus_add_device 创建)
* @param reg_addr 要读取的起始寄存器地址
* @param data 接收数据的缓冲区指针
* @param len 要读取的字节数
* @return esp_err_t ESP_OK 表示成功,其他值表示失败
*
* 说明:
* 该函数使用 i2c_master_transmit_receive 实现典型的 I2C 读操作:
* 1. 主机先发送 1 字节的寄存器地址(写操作)
* 2. 随后立即从从机读取 len 字节数据(读操作)
* 超时时间由 I2C_MASTER_TIMEOUT_MS 宏定义,转换为 FreeRTOS tick。
* ========================================================================== */
static esp_err_t mpu9250_register_read(i2c_master_dev_handle_t dev_handle, uint8_t reg_addr, uint8_t *data, size_t len)
{
return i2c_master_transmit_receive(dev_handle, ®_addr, 1, data, len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
/* ==========================================================================
* 函数:mpu9250_register_write_byte
* --------------------------------------------------------------------------
* @brief 向 MPU9250 的指定寄存器写入一个字节
*
* @param dev_handle I2C 设备句柄
* @param reg_addr 目标寄存器地址
* @param data 要写入的 1 字节数据
* @return esp_err_t ESP_OK 表示成功,其他值表示失败
*
* 说明:
* 将寄存器地址和数据拼接成 2 字节数组后,通过 i2c_master_transmit
* 一次性发送。这是 I2C 写操作的典型流程。
* ========================================================================== */
static esp_err_t mpu9250_register_write_byte(i2c_master_dev_handle_t dev_handle, uint8_t reg_addr, uint8_t data)
{
uint8_t write_buf[2] = {reg_addr, data}; /* 第 1 字节:寄存器地址,第 2 字节:数据 */
return i2c_master_transmit(dev_handle, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS);
}
/* ==========================================================================
* 函数:i2c_master_init
* --------------------------------------------------------------------------
* @brief 初始化 I2C 主机总线并添加 MPU9250 设备
*
* @param[out] bus_handle 返回 I2C 总线句柄
* @param[out] dev_handle 返回 I2C 设备句柄
*
* 步骤说明:
* 1. 配置 I2C 总线参数(引脚、时钟源、毛刺滤波、内部上拉等)
* 2. 调用 i2c_new_master_bus 创建总线实例
* 3. 配置设备参数(7 位地址模式、从机地址、时钟速率)
* 4. 调用 i2c_master_bus_add_device 将 MPU9250 挂载到总线上
*
* 注意:
* - 所有 ESP_ERROR_CHECK 宏在出错时会直接 abort,便于调试
* - 本示例启用了内部上拉电阻,无需外接上拉
* ========================================================================== */
static void i2c_master_init(i2c_master_bus_handle_t *bus_handle, i2c_master_dev_handle_t *dev_handle)
{
/* --- 1. 配置 I2C 总线参数 --- */
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_MASTER_NUM, /* 使用 I2C_NUM_0 控制器 */
.sda_io_num = I2C_MASTER_SDA_IO, /* SDA 数据引脚编号 */
.scl_io_num = I2C_MASTER_SCL_IO, /* SCL 时钟引脚编号 */
.clk_source = I2C_CLK_SRC_DEFAULT, /* 使用默认时钟源 */
.glitch_ignore_cnt = 7, /* 毛刺滤波计数(过滤短脉冲干扰) */
.flags.enable_internal_pullup = true, /* 启用 GPIO 内部上拉电阻 */
};
/* --- 2. 创建 I2C 主机总线 --- */
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, bus_handle));
/* --- 3. 配置从设备参数 --- */
i2c_device_config_t dev_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7, /* 使用 7 位地址模式 */
.device_address = MPU9250_SENSOR_ADDR, /* MPU9250 的 I2C 地址 0x68 */
.scl_speed_hz = I2C_MASTER_FREQ_HZ, /* I2C 时钟频率 */
};
/* --- 4. 将 MPU9250 设备挂载到总线上 --- */
ESP_ERROR_CHECK(i2c_master_bus_add_device(*bus_handle, &dev_config, dev_handle));
}
/* ==========================================================================
* 函数:app_main
* --------------------------------------------------------------------------
* @brief FreeRTOS 应用程序入口点
*
* 执行流程:
* 1. 初始化 I2C 总线及 MPU9250 设备
* 2. 读取 WHO_AM_I 寄存器,验证通信正常(期望值 0x71)
* 3. 向电源管理寄存器写复位命令,演示写操作
* 4. 卸载设备、删除总线,释放资源
* ========================================================================== */
void app_main(void)
{
uint8_t data[2]; /* 通用数据缓冲区 */
i2c_master_bus_handle_t bus_handle; /* I2C 总线句柄 */
i2c_master_dev_handle_t dev_handle; /* I2C 设备句柄 */
/* ---- 第 1 步:初始化 I2C 总线和设备 ---- */
i2c_master_init(&bus_handle, &dev_handle);
ESP_LOGI(TAG, "I2C initialized successfully");
/* ---- 第 2 步:读取 WHO_AM_I 寄存器 ---- */
/* MPU9250 上电后 WHO_AM_I 寄存器默认值为 0x71,用于验证通信是否正常 */
ESP_ERROR_CHECK(mpu9250_register_read(dev_handle, MPU9250_WHO_AM_I_REG_ADDR, data, 1));
ESP_LOGI(TAG, "WHO_AM_I = %X", data[0]);
/* ---- 第 3 步:通过写寄存器复位 MPU9250 ---- */
/* 向 PWR_MGMT_1 寄存器的 bit7 写入 1,触发设备复位 */
ESP_ERROR_CHECK(mpu9250_register_write_byte(dev_handle, MPU9250_PWR_MGMT_1_REG_ADDR, 1 << MPU9250_RESET_BIT));
/* ---- 第 4 步:反初始化,释放资源 ---- */
ESP_ERROR_CHECK(i2c_master_bus_rm_device(dev_handle)); /* 从总线移除设备 */
ESP_ERROR_CHECK(i2c_del_master_bus(bus_handle)); /* 删除总线实例 */
ESP_LOGI(TAG, "I2C de-initialized successfully");
}
- 配置

- 调试
由于和我们开发板上的芯片不一样,这里就不演示了。
结果为:
bash
I (328) example: I2C initialized successfully
I (338) example: WHO_AM_I = 71
I (338) example: I2C de-initialized successfully
关键知识点
硬件
基础知识
- 全称:Inter-Integrated Circuit,集成电路间总线,飞利浦推出的低速串行通信协议。
- 物理线 :仅两根信号线 + GND
- SDA:数据线,双向传输(半双工)
- SCL:时钟线,由主机产生时钟
- 拓扑 :多主机、多从机总线结构,一根总线可挂载多个设备,靠7 位 / 10 位从机地址区分。
- 电平 :开漏输出,必须外部上拉电阻(典型 4.7kΩ);总线空闲时 SDA、SCL 均为高电平。
注意:若没有上拉电阻,总线就是悬空的,什么也传不了。 - 典型速率
- 标准模式:100kHz
- 快速模式:400kHz
- 高速模式:1MHz、3.4MHz、5Mhz
- 适合场景
- 传感器(温湿度、气压、IMU、环境光)
- EEPROM(AT24C64 等小容量存储)
- RTC 芯片(DS3231 等)
- GPIO 扩展芯片(PCF8574)
1982 年,飞利浦的工程师面对一个问题:
电视机里有几十颗芯片,总线接线已经乱成一锅粥。
他们需要一种只用两根线就能管所有芯片的协议。
I2C 就是这样诞生的。

协议时序
起始信号、停止信号
- 起始信号(S)
SCL 高电平时,SDA 由高电平跳变到低电平,代表一次通信开始。 - 重复起始信号(Sr)
不发停止,直接再次发起始,常用于连续读写、切换读写方向。 - 停止信号(P)
SCL 高电平时,SDA 由低电平跳变到高电平,结束本次传输,总线回归空闲。
数据位传输规则
- 每一个数据位,在 SCL 高电平期间采样 SDA;SCL 低电平期间允许 SDA 电平翻转。
- 一字节固定 8bit,高位先行(MSB first)。
- 每发送完 1 字节(8 位),接收方必须回复 应答位 ACK/NACK (第 9 个时钟)
- ACK(应答,0):接收方拉低 SDA,表示收到数据
- NACK(非应答,1):SDA 保持高,代表未接收、通信结束
总线时序图

① 起始信号
当 SCL 为高电平期间,SDA 由高到低的跳变。起始信号是一种电平跳变时序信号,而不是 一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传 输。
② 停止信号
当 SCL 为高电平期间,SDA 由低到高的跳变。停止信号也是一种电平跳变时序信号,而不 是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号
发送器每发送一个字节,就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK 简称应答位),表示接收器已经成功地接收了 该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成 功。 观察上图标号③就可以发现,有效应答的要求是从机在第 9 个时钟脉冲之前的低电平期间 将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它 收到最后一个字节后,发送一个 NACK信号,以通知被控发送器结束数据发送,并释放 SDA线, 以便主机接收器发送一个停止信号。
④ 数据有效性
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 SCL串行时钟的配合下,在 SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态
I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个 器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
基本读写过程
- 写操作

主机首先在 I2C 总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的 数据。主机接着发送从机地址+0(写操作)组成的 8bit 数据,所有从机接收到该 8bit 数据后, 自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。
注意:I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
写数据流程:起始 → 从机地址 (7bit)+ 读写位 (0 写) → 等待 ACK → 内部寄存器地址 → ACK → 发送数据字节 → ACK...... → 停止
- 读操作

主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现, 都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的 8bit 数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机 返回8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么 从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号, 从机才会停止发送数据。
完整的读数据流程:起始 → 从地址 + 写位(0) → ACK → 寄存器地址 → ACK → 重复起始 → 从地址 + 读位 (1) → ACK → 读取数据 → 主机发 NACK 终止 → 停止
软件
函数
i2c_new_master_bus--- 创建 I2C 主机总线
cpp
esp_err_t i2c_new_master_bus(const i2c_master_bus_config_t *bus_config, i2c_master_bus_handle_t *ret_bus_handle)
功能: 根据配置参数分配并初始化一条 I2C 主机总线。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
bus_config |
const i2c_master_bus_config_t * |
I2C 总线配置结构体指针,包含端口号、SCL/SDA 引脚、时钟源、毛刺滤波计数、中断优先级、内部上拉使能等 |
输出参数:
| 参数 | 类型 | 说明 |
|---|---|---|
ret_bus_handle |
i2c_master_bus_handle_t * |
输出参数,指向创建的总线句柄,后续所有总线级操作都需传入此句柄 |
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
总线创建成功 |
ESP_ERR_INVALID_ARG |
参数无效(如引脚号非法) |
ESP_ERR_NO_MEM |
内存不足,无法分配总线资源 |
ESP_ERR_NOT_FOUND |
没有空闲的 I2C 控制器可用 |
注意事项:
i2c_port可设为-1让驱动自动选择可用端口flags.enable_internal_pullup启用后驱动会自动使能 GPIO 内部上拉,但在高速模式下内部上拉可能不够强,建议外接上拉电阻
i2c_master_bus_add_device--- 向总线添加 I2C 从设备
cpp
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)
功能: 在已创建的总线上挂载一个 I2C 从设备,返回设备句柄供后续读写操作使用。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
bus_handle |
i2c_master_bus_handle_t |
已创建的 I2C 总线句柄 |
dev_config |
const i2c_device_config_t * |
设备配置结构体指针,包含地址长度(7/10位)、从机地址、SCL 时钟频率、ACK 检查等 |
输出参数:
| 参数 | 类型 | 说明 |
|---|---|---|
ret_handle |
i2c_master_dev_handle_t * |
输出参数,指向创建的设备句柄,后续读写操作都需传入此句柄 |
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
设备添加成功 |
ESP_ERR_INVALID_ARG |
参数无效 |
ESP_ERR_NO_MEM |
内存不足 |
注意事项:
device_address填写的是原始 7/10 位地址,不包含读写位(驱动内部会自动处理 R/W 位)dev_addr_length需设为I2C_ADDR_BIT_LEN_7(7 位)或I2C_ADDR_BIT_LEN_10(10 位)- 同一条总线上可挂载多个设备,每个设备有独立的
scl_speed_hz flags.disable_ack_check设为 true 会跳过 ACK 检测(不推荐常规使用)
i2c_master_transmit_receive--- I2C 先写后读(复合传输)
cpp
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)
功能: 在 I2C 总线上执行"先发送数据,再接收数据"的复合操作。典型场景是向从设备发送寄存器地址,然后读取寄存器值。该函数会阻塞直到传输完成或超时。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
i2c_dev |
i2c_master_dev_handle_t |
设备句柄 |
write_buffer |
const uint8_t * |
待发送数据的缓冲区指针 |
write_size |
size_t |
发送数据的字节数 |
read_size |
size_t |
期望接收的字节数 |
xfer_timeout_ms |
int |
超时时间(毫秒),-1 表示永久等待 |
输出参数:
| 参数 | 类型 | 说明 |
|---|---|---|
read_buffer |
uint8_t * |
接收数据缓冲区指针,函数返回后其中存放从 I2C 总线读取到的数据 |
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
传输成功 |
ESP_ERR_INVALID_ARG |
参数无效(如空指针、size 为 0) |
ESP_ERR_TIMEOUT |
操作超时(总线忙或硬件异常) |
注意事项:
- 如果已通过
i2c_master_register_event_callbacks注册了回调,此函数会变为异步模式,立即返回而不阻塞,传输完成信息通过回调获取 - 异步模式下,
write_buffer和read_buffer必须在回调触发前保持有效(不能被释放或修改) - 同步模式下,超时时间需合理设置,
-1可能导致死等
i2c_master_transmit--- I2C 纯写传输
cpp
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);
功能: 向 I2C 从设备发送数据。该函数会阻塞直到传输完成或超时。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
i2c_dev |
i2c_master_dev_handle_t |
设备句柄 |
write_buffer |
const uint8_t * |
待发送数据的缓冲区指针 |
write_size |
size_t |
发送数据的字节数 |
xfer_timeout_ms |
int |
超时时间(毫秒),-1 表示永久等待 |
输出参数: 无
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
发送成功 |
ESP_ERR_INVALID_ARG |
参数无效 |
ESP_ERR_TIMEOUT |
操作超时(总线忙或硬件异常) |
注意事项:
- 与
i2c_master_transmit_receive一样,注册回调后会变为异步模式 - 写操作期间如果从设备回复 NACK 且未禁用 ACK 检查,函数会立即返回错误(非超时)
- 本示例中将寄存器地址和数据拼成 2 字节数组一次发送,是 I2C 写寄存器的标准做法
i2c_master_bus_rm_device--- 从总线移除设备
cpp
esp_err_t i2c_master_bus_rm_device(i2c_master_dev_handle_t handle)
功能: 将先前通过 i2c_master_bus_add_device 添加的设备从总线上移除并释放相关资源。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
handle |
i2c_master_dev_handle_t |
要移除的设备句柄 |
输出参数: 无
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
设备移除成功 |
注意事项:
- 移除设备后,该设备句柄即失效,不可再用于读写操作
- 应在删除总线前先移除所有设备
i2c_del_master_bus--- 删除 I2C 主机总线
cpp
esp_err_t i2c_del_master_bus(i2c_master_bus_handle_t bus_handle)
功能: 反初始化并释放 I2C 主机总线资源。
输入参数:
| 参数 | 类型 | 说明 |
|---|---|---|
bus_handle |
i2c_master_bus_handle_t |
要删除的总线句柄 |
输出参数: 无
返回值:
| 返回值 | 说明 |
|---|---|
ESP_OK |
总线删除成功 |
| 其他值 | 部分模块删除失败 |
注意事项:
- 必须先调用
i2c_master_bus_rm_device移除所有设备后,才能删除总线 - 删除后总线句柄失效,不可再使用
- 删除操作会释放 GPIO 引脚并关闭 I2C 外设时钟
使用XL9555获取KEY0的状态
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 器件总共有 24 个管脚,分别为电源线 VCC、地线 GND、GPIO 口、通信线、地址 线,上图用不同底色标注出来了。16 个 IO 分为了 2 组,一组是 8 个,分为是 P0x 和 P1x,这些 IO 都可通过器件寄存器进行配置作为输出或者输出使用。通信线就是 SDA 和 SCL,中断线 INT 也划分过来通信线。而地址线就是 A0、A1 和 A2,用来决定器件地址。
- XL9555寻址
要进行 IIC 通信,首先得知道器件地址,XL9555 器件地址是 7 位的,具体格式如下图。

从上图可以知道,XL9555 器件地址由两部分组成,一部分就是"Fixed bits"即固定的 4 位 "0100";另一部分就是"Programmable bits"即可编程的 3 位"A2 A1 A0",在硬件上,都把这三个引脚接地处理,所以这三位为"000"。
最终可得到,XL9555 器件地址为"0100000"即 0x20。
读操作地址就为 0x41,即 0100 0001;
写操作地址就为 0x40,即 0100 0000。 - XL9555寄存器介绍
接下来,介绍一下 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 输入状态。寄存器如下图所示:

该寄存器只反应引脚输入逻辑电平情况,不管 IO 是设置成输入还是输出模式。打个比方, 从 0x00 地址处(Input Port 0 Register)读出的数据是 0x55,以二进制展开为 01010101,从高位 到低位对应的就是 P07~P00 的 IO 状态,P00 的输入电平状态就为高电平。
地址 0x02 和 0x03 的寄存器是"Output Port0"和"Output Port1"寄存器,主要用于设置 P0 和 P1 的 IO 输出电平。寄存器如下图所示:

该寄存器设置的是已经配置成输出模式的 IO 口的 IO 输出状态,1 代表的是高电平,0 代表 的都是低电平,配置 IO 为输出模式的寄存器为 Configuration Port 寄存器。寄存器的一些位值对 已经设置成输入模式的 IO 口是没有影响的。该寄存器还支持读取,读取到的值只是设置值,并 不是实际引脚电平值,实际电平值通过 Input Port 寄存器查询即可。
地址 0x04 和 0x05 的寄存器是"Polarity Inversion Port0"和"Polarity Inversion Port1"寄存器,用于对端口 0 和端口 1 进行极性翻转。该寄存器值默认为 0,所以对 IO 电平翻转功能 并没有启用。
地址 0x06 和 0x07 的寄存器是"Configuration Port1"和"Configuration Port0"寄存器,用 于配置 P0 和 P1 的 IO 输入/输出模式。寄存器如下图所示。

该寄存器某一个位设置成 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。
- XL9555 时序介绍
ESP32-S3 是通过 IIC 总线跟 XL9555 进行通信的,对 XL9555 相关寄存器进行写入配置,对 其 16 个 IO 进行使用。这里的时序主要就是写寄存器时序和读寄存器时序。
写时序

上图中展示的是主机将单字节写入到寄存器的时序,主机在 IIC总线发送第 1个字节的数据 为 XL9555 的写操作地址 0x40(设备地址 0x20 << 1 | 0),用于寻找总线上找到 XL9555,在获得 XL9555 的应答信号之后,继续发送第 2 个字节数据,该字节数据是 XL9555 的寄存器地址,再 等到 XL9555 的应答信号,主机继续发送第 3 字节数据,这里的数据即是写入在第 2 字节寄存器 地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
读时序

上图中展示的是主机从寄存器中读取一个字节数据的时序图。XL9555 读取数据的过程是一 个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产 生后,主机发送 XL9555 的写操作地址 0x40(设备地址 0x20<< 1 | 0),获取从机应答信号后, 接着发送需要读取的寄存器地址;在读时序中,起始信号产生后,主机发送 XL9555 的读操作 地址 0x41(设备地址 0x20 << 1 | 1),获取从机应答信号后,接着从机返回刚刚在写时序中寄存 器地址的数据,以字节为单位传输在总线上,主机接收到寄存器的数据后,发出非应答信号并 以停止信号结束通信过程。
管脚配置

从原理图看出:
SDA连接ESP32-S3的IO41,
SCL连接ESP32-S3的IO42,
INT连接ESP32-S3的IO0
KEY0对应的是XL9555的IO1_7
XL9555由于A0、A1、A2都连接地,所以地址为0x20。
代码
main.c
cpp
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "driver/gpio.h"
#include "xl9555.h"
#include "driver/i2s_std.h"
#include "esp_log.h"
#define XL9555_SDA GPIO_NUM_41
#define XL9555_SCL GPIO_NUM_42
#define TAG "main"
void xl9555_button_callback(uint16_t num_io,int level)
{
switch(num_io)
{
case IO1_7:
ESP_LOGI(TAG,"Button KEY0 check,level:%d",level);
break;
case IO0_2:
ESP_LOGI(TAG,"Button2 check,level:%d",level);
break;
case IO0_3:
ESP_LOGI(TAG,"Button3 check,level:%d",level);
break;
case IO0_4:
ESP_LOGI(TAG,"Button4 check,level:%d",level);
break;
default:break;
}
}
void i2c_and_xl9555_init(void)
{
xl9555_init(XL9555_SDA,XL9555_SCL,GPIO_NUM_0,xl9555_button_callback);
xl9555_ioconfig(0xFFFF);
}
void app_main(void)
{
i2c_and_xl9555_init();
}
xl9555.c
cpp
#include "xl9555.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include <string.h>
static i2c_master_bus_handle_t xl9555_i2c_master = NULL;
static i2c_master_dev_handle_t xl9555_i2c_device =NULL;
static xl9555_input_cb_t xl9555_input_callback = NULL;
static gpio_num_t xl9555_isr_io = GPIO_NUM_NC;
static uint16_t xl9555_io_config = 0xFFFF;
static EventGroupHandle_t xl9555_isr_event = NULL;
#define TAG "xl9555"
//交换二值
#define SWAP_NUM(X,Y) do{\
(X)=X^(Y);\
(Y)=(X)^(Y);\
(X)=(X)^(Y);\
}while(0)
#define XL9555_ISR_BIT BIT0
#define LITTLE_ENDIAN 0
#define BIG_ENDIAN 1
//判断字节序
static inline int check_endian(void)
{
static const uint32_t endian_value = 0x12345678;
const char* endian_array = (const char*)&endian_value;
if(endian_array[0] == 0x78)
return LITTLE_ENDIAN;
else if(endian_array[0] == 0x12)
return BIG_ENDIAN;
return -1;
}
/** 读取16位的GPIO电平值
* @param data 返回的16个GPIO值,用位表示
* @return 成功或失败
*/
esp_err_t xl9555_read_word(uint8_t reg,uint16_t *data)
{
uint8_t memaddr_buf[1];
memaddr_buf[0] = reg;
esp_err_t ret = i2c_master_transmit_receive(xl9555_i2c_device, &memaddr_buf[0], 1, (uint8_t*)data, 2, 500);
if(check_endian() == BIG_ENDIAN)
{
//如果是大端字节序就交换高低两个字节,16位变量低位存放的是XL9555的前8个数据
uint8_t* value_array = (uint8_t*)data;
SWAP_NUM(value_array[0],value_array[1]);
}
return ret;
}
/** 写入16位的数据
* @param ret寄存器地址
* @param data 要写入的数据,XL9555一般是2个字节,所以这里用16位值
* @return 成功或失败
*/
esp_err_t xl9555_write_word(uint8_t reg, uint16_t data)
{
esp_err_t ret;
uint8_t *write_buf = (uint8_t*)malloc(2 + 1);
if(!write_buf)
return ESP_FAIL;
write_buf[0] = reg;
memcpy(&write_buf[1],&data,2);
if(check_endian() == BIG_ENDIAN)
{
//如果是大端字节序就需要交换
SWAP_NUM(write_buf[1],write_buf[2]);
}
ret = i2c_master_transmit(xl9555_i2c_device, write_buf, 2 + 1, 500);
free(write_buf);
return ret;
}
/** 设置某个gpio口的电平
* @param pin gpio口
* @param val 电平值
* @return 无
*/
esp_err_t xl9555_pin_write(uint16_t pin, int val)
{
uint16_t r_data;
xl9555_read_word(XL9555_INPUT_PORT0_REG,&r_data);
if(val)
r_data |= pin;
else
r_data &= ~pin;
return xl9555_write_word(XL9555_OUTPUT_PORT0_REG, r_data);
}
/** 读取某个gpio口的电平
* @param pin gpio口
* @return 电平值
*/
int xl9555_pin_read(uint16_t pin)
{
uint16_t r_data = 0;
xl9555_read_word(XL9555_INPUT_PORT0_REG,&r_data);
return (r_data & pin) ? 1 : 0;
}
/** 设置输入输出
* @param config_value 配置值,如果对应的位是0,则对应的GPIO口设置为【输出】,如果是1,则为【输入】
* @return 无
*/
esp_err_t xl9555_ioconfig(uint16_t config_value)
{
esp_err_t err;
xl9555_io_config = config_value;
do
{
err = xl9555_write_word(XL9555_CONFIG_PORT0_REG, config_value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "%s configure %X failed, ret: %d", __func__, config_value, err);
}
vTaskDelay(pdMS_TO_TICKS(100));
} while (err != ESP_OK);
return err;
}
/**
* 外部中断服务函数
* @param arg 中断引脚号,在注册中断回调函数时已通过参数带进来
* @return 无
*/
static void IRAM_ATTR xl9555_exit_gpio_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t) arg;
BaseType_t task_woken;
if (gpio_num == xl9555_isr_io)
{
if (gpio_get_level(xl9555_isr_io) == 0)
{
xEventGroupSetBitsFromISR(xl9555_isr_event,XL9555_ISR_BIT,&task_woken);
}
}
}
/**
* 外部中断初始化程序
* @param 无
* @return 无
*/
static void xl9555_isr_init(void)
{
gpio_config_t gpio_init_struct;
xl9555_isr_event = xEventGroupCreate();
/* 配置BOOT引脚和外部中断 */
gpio_init_struct.mode = GPIO_MODE_INPUT; /* 选择为输入模式 */
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 上拉使能 */
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 下拉失能 */
gpio_init_struct.intr_type = GPIO_INTR_NEGEDGE; /* 下降沿触发 */
gpio_init_struct.pin_bit_mask = 1ull << xl9555_isr_io; /* 设置的引脚的位掩码 */
gpio_config(&gpio_init_struct); /* 配置使能 */
/* 注册中断服务 */
gpio_install_isr_service(0);
/* 设置GPIO的中断回调函数 */
gpio_isr_handler_add(xl9555_isr_io, xl9555_exit_gpio_isr_handler, (void*) xl9555_isr_io);
}
static void xl9555_intput_scan(void* param)
{
esp_err_t ret;
EventBits_t ev;
uint16_t last_input = 0;
xl9555_read_word(XL9555_INPUT_PORT0_REG,&last_input);
while(1)
{
uint16_t input;
ev = xEventGroupWaitBits(xl9555_isr_event,XL9555_ISR_BIT,pdTRUE,pdFALSE,pdMS_TO_TICKS(10*1000));
if(ev & XL9555_ISR_BIT)
{
if ( gpio_get_level(xl9555_isr_io) != 0)
{
continue;
}
ret = xl9555_read_word(XL9555_INPUT_PORT0_REG,&input); //读取输入寄存器
if(ret == ESP_OK)
{
for(int i = 0;i < 16;i++)
{
if(xl9555_io_config & (1 <<i))//判断是否已经将对应端口设置为输入
{
uint8_t value = input&(1<<i)?1:0;
uint8_t last_value = last_input&(1<<i)?1:0;
if(value != last_value && xl9555_input_callback)
{
xl9555_input_callback(1<<i,value);
}
}
}
}
last_input = input;
}
}
}
/** 初始化xl9555芯片和用到的i2c总线
* @param sda sda的gpio口
* @param scl scl的gpio口
* @param int_io 中断的gpio口
* @param f 回调函数用于告知gpio口的电平跳变
* @return 无
*/
void xl9555_init(gpio_num_t sda,gpio_num_t scl,gpio_num_t int_io,xl9555_input_cb_t f)
{
uint16_t r_data;
i2c_master_bus_config_t bus_config =
{
.i2c_port = 1,
.sda_io_num = sda,
.scl_io_num = scl,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.trans_queue_depth = 0,
.flags.enable_internal_pullup = 1,
};
i2c_new_master_bus(&bus_config,&xl9555_i2c_master);
i2c_device_config_t dev_config =
{
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = XL9555_ADDR,
.scl_speed_hz = 100000,
};
xl9555_input_callback = f;
xl9555_isr_io = int_io;
i2c_master_bus_add_device(xl9555_i2c_master, &dev_config, &xl9555_i2c_device);
/* 上电先读取一次清除中断标志 */
xl9555_read_word(XL9555_INPUT_PORT0_REG,&r_data);
if(xl9555_isr_io != GPIO_NUM_NC)
{
xl9555_isr_init();
xTaskCreatePinnedToCore(xl9555_intput_scan,"xl9555",4096,NULL,3,NULL,1);
}
}
xl9555.h
cpp
#ifndef __XL9555_H
#define __XL9555_H
#include "driver/gpio.h"
#include "esp_err.h"
#include "driver/i2c_master.h"
/* XL9555命令宏 */
#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地址 */
#define XL9555_ADDR 0x20 /* XL9555地址(左移了一位)-->请看手册(9.1. Device Address) */
/* XL9555各个IO的功能 */
#define IO0_0 0x0001
#define IO0_1 0x0002
#define IO0_2 0x0004
#define IO0_3 0x0008
#define IO0_4 0x0010
#define IO0_5 0x0020
#define IO0_6 0x0040
#define IO0_7 0x0080
#define IO1_0 0x0100
#define IO1_1 0x0200
#define IO1_2 0x0400
#define IO1_3 0x0800
#define IO1_4 0x1000
#define IO1_5 0x2000
#define IO1_6 0x4000
#define IO1_7 0x8000
typedef void(*xl9555_input_cb_t)(uint16_t io_num,int level);
/** 初始化xl9555芯片和用到的i2c总线
* @param sda sda的gpio口
* @param scl scl的gpio口
* @param int_io 中断的gpio口
* @param f 回调函数用于告知gpio口的电平跳变
* @return 无
*/
void xl9555_init(gpio_num_t sda,gpio_num_t scl,gpio_num_t int_io,xl9555_input_cb_t f);
/** 读取某个gpio口的电平
* @param pin gpio口
* @return 电平值
*/
int xl9555_pin_read(uint16_t pin);
/** 设置某个gpio口的电平
* @param pin gpio口
* @param val 电平值
* @return 无
*/
esp_err_t xl9555_pin_write(uint16_t pin, int val);
/** 写入16位的数据
* @param ret寄存器地址
* @param data 要写入的数据,XL9555一般是2个字节,所以这里用16位值
* @return 成功或失败
*/
esp_err_t xl9555_write_word(uint8_t reg, uint16_t data);
/** 读取16位的GPIO电平值
* @param reg 寄存器地址
* @param data 寄存器地址的值
* @return 成功或失败
*/
esp_err_t xl9555_read_word(uint8_t reg, uint16_t *data);
/** 设置输入输出
* @param config_value 配置值,如果对应的位是0,则对应的GPIO口设置为【输出】,如果是1,则为【输入】
* @return 无
*/
esp_err_t xl9555_ioconfig(uint16_t config_value);
#endif
调试
注意烧写后把IO0的跳线帽取掉,重启,再连上,否则会进入下载模式一直等待,因为它的中断线INT正好连的是ESP32-S3的BOOT,如下:
取掉,重启,再连上后:
按下KEY0
实操练习:
实现功能 :
使用KEY0开关蜂鸣器BEEP
答案在gitee: