I²C 总线与 EXIO:扩展更多 GPIO

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 19 章。配套完整工程:examples/14-i2c-xl9555。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
I²C0 SDAGPIO41,板载上拉
I²C0 SCLGPIO42,板载上拉
XL95557 位地址 0x20
KEY0 / 蜂鸣器IO1_7 / IO0_3,均低电平有效

以下为正点原子 DNESP32S3 V1.2 原理图的相关页,保留原厂标识;表格用于快速定位,遇到板卡版本差异以实际原理图为准。点击图片可查看引脚与网络名。

DNESP32S3 原理图第 2 页

DNESP32S3 原理图第 3 页

本课信号与数据处理流程

两根信号线为什么能连接多个芯片

SDA 传数据,SCL 给出时钟。主机先发送目标的 7 位地址,再发送读写位;被点名的器件在第 9 个时钟应答 ACK。程序扫描的是 0x08~0x77,跳过保留地址。扫描到一个地址,只说明有器件应答,不代表已经识别了型号。

I²C 采用开漏输出:芯片能拉低线路,逻辑高依靠上拉电阻。V1.2 原理图中总线上已有 2.2 kΩ 上拉,内部弱上拉只是辅助。多个模块各自带上拉并联后阻值会降低,不能越多越好。全总线无应答时先检查供电、公共地、SDA/SCL 和上拉。

GPIO 编号和扩展 IO 编号不是同一个空间

GPIO1 是 ESP32-S3 自身引脚;IO1_7 是 XL9555 的第 1 组第 7 位。后者不能传给 gpio_set_level()。读写扩展口要通过 I²C 寄存器:

寄存器组 0 / 组 1用途
输入0 / 1读取当前引脚电平
输出锁存2 / 3设置输出电平
极性反转4 / 5对输入读数反相
方向6 / 71 输入,0 输出

两个 8 位寄存器合并成 16 位数后,KEY0 对应 bit15,即 0x8000;蜂鸣器对应 bit3,即 0x0008。应用只修改这一个输出位,其他屏幕复位、电源和中断脚不能被顺带覆盖。

先设置输出值,再改变方向

exio_output(mask, value) 先读输出锁存寄存器,只替换 mask 选中的位,再把对应方向位清零。这样把蜂鸣器切为输出前,已经预置为高电平关闭,减少上电误鸣。

读改写包含两次总线事务,虽然驱动保证单次 I²C 事务串行,整个读改写仍不是原子操作。因此该接口要求同一任务调用;若以后多个任务控制 XL9555,应在 BSP 内增加互斥锁。

本例每 20 ms 读取 KEY0,用按住鸣叫、松开关闭展示输入和输出。不生成“单次按下事件”,所以这里只做电平控制;要计数仍需第 03 课的消抖。

工程如何组织

驱动放在 components/BSP/,应用放在 main/main.c。本工程包含 BUS、EXIO 模块。项目根目录的 components/BSP 会被 ESP-IDF 自动发现,main 的 PRIV_REQUIRES BSP 声明组件依赖;INCLUDE_DIRS 导出头文件搜索路径,不是运行时查找文件。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stdbool.h>
 4#include <string.h>
 5#include <stdlib.h>
 6#include <assert.h>
 7#include "esp_err.h"
 8#include "esp_log.h"
 9#include "esp_timer.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include "exio.h"
13void app_main(void)
14{
15    i2c_master_bus_handle_t bus;
16    ESP_ERROR_CHECK(bus_init(0,CONFIG_LESSON_I2C_SDA,CONFIG_LESSON_I2C_SCL,&bus));
17    for(int addr=8;addr<0x78;addr++)
18        if(i2c_master_probe(bus,addr,10)==ESP_OK) ESP_LOGI("scan","ACK 0x%02x",addr);
19    ESP_ERROR_CHECK(exio_init(bus));
20    ESP_ERROR_CHECK(exio_output(0x0008,0x0008)); // 蜂鸣器默认关闭。
21    uint16_t previous=0xffff;
22    while(1) {
23        uint16_t value;
24        esp_err_t err=exio_read(&value);
25        if(err==ESP_OK) {
26            // KEY0 接 IO1_7,按下为 0;控制 IO0_3 蜂鸣器,松开即关闭。
27            bool pressed=!(value&0x8000);
28            err=exio_output(0x0008,pressed ? 0 : 0x0008);
29            if(value!=previous) ESP_LOGI("exio","input=0x%04x KEY0=%d",value,pressed);
30            previous=value;
31        }
32        if(err!=ESP_OK) ESP_LOGW("exio","%s",esp_err_to_name(err));
33        vTaskDelay(pdMS_TO_TICKS(20));
34    }
35}

BSP 关键实现:EXIO

以下是本课对应驱动的完整源文件。对外声明见 exio.h。公共 BUS 模块统一使用新 I²C master API,将单次事务等待限制在 50 ms;设备 API 使用 7 位地址。

 1#include "exio.h"
 2static i2c_master_dev_handle_t s_dev;
 3static esp_err_t update(uint8_t reg, uint16_t mask, uint16_t value)
 4{
 5    uint8_t b[2];
 6    ESP_RETURN_ON_ERROR(reg_read(s_dev, reg, b, 2), "exio", "read");
 7    uint16_t old = b[0] | ((uint16_t)b[1] << 8);
 8    uint16_t next = (old & ~mask) | (value & mask);
 9    uint8_t packet[] = {reg, next & 255, next >> 8};
10    return i2c_master_transmit(s_dev, packet, sizeof(packet), 50);
11}
12esp_err_t exio_init(i2c_master_bus_handle_t bus)
13{
14    return bus_device(bus, 0x20, &s_dev);
15}
16esp_err_t exio_output(uint16_t mask, uint16_t value)
17{
18    // 先预置输出锁存器,再切换输出方向,避免瞬间输出错误电平。
19    ESP_RETURN_ON_ERROR(update(2, mask, value), "exio", "output");
20    return update(6, mask, 0); // 配置位 0=输出,1=输入。
21}
22esp_err_t exio_read(uint16_t *value)
23{
24    uint8_t b[2];
25    ESP_RETURN_ON_ERROR(reg_read(s_dev, 0, b, 2), "exio", "input");
26    *value = b[0] | ((uint16_t)b[1] << 8);
27    return ESP_OK;
28}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer BSP )

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 设置已关闭普通 CMake 自动配置,C/C++ 语言服务读取 build/compile_commands.json;首次构建前文件尚不存在,应先激活 IDF 并构建,而不是手工拼头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/14-i2c-xl9555
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1scan: ACK 0x20
2exio: input=0xffff KEY0=0
3exio: input=0x7fff KEY0=1

故障定位与逐项验收

先检查目标是否是 esp32s3、实际端口与供电,再按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,无法证明模块型号、物理线路或时序已经正确。

遇到通信错误先看错误码和对应模块,禁止简单用“错误时返回 0”掩盖异常。预期日志是阅读程序的参考,验收时应保存自己的现象、日志与接线照片。

与教材和官方实现对照

对照 08_iic_exio:保持地址、寄存器及蜂鸣器极性;从旧 i2c.h 迁移到 i2c_master.h;输出改为按位保留,避免初始化时影响屏幕和其他设备。输入其他位可能不同,日志数值仅示意。

本地验证记录与参考

返回课程目录