ESP-IoT-Solution 通信总线组件(Bus)深度指南:i2c_bus 与 spi_bus 的初始化、读写与源码原理
2026/9/19 9:03:48 网站建设 项目流程

ESP-IoT-Solution 通信总线组件(Bus)深度指南:i2c_bus 与 spi_bus 的初始化、读写与源码原理

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

导读

通信总线组件(Bus)是 esp-iot-solution 中建立在 ESP-IDF 外设驱动之上的一套应用层代码,包含i2c_busspi_bus两大模块,用于解决 ESP 芯片与外置设备(传感器、显示面板、扩展芯片等)之间的总线通信问题。本文以 docs/zh_CN/basic/bus/index.rst 为骨架,完整讲解总线/设备两级抽象模型、I2C 与 SPI 总线的创建、设备挂载、数据读写与资源释放全流程,并结合仓库源码剖析其单例管理、线程安全与动态配置切换等底层实现,帮助开发者快速上手并写出稳定可靠的外设驱动代码。

一、设计思想:总线(Bus)与设备(Device)两级抽象

总线组件从应用开发的角度出发,主要解决三类痛点:

  1. 简化外设初始化步骤:把 ESP-IDF 原生的i2c_param_config/i2c_driver_installspi_bus_initialize/spi_bus_add_device等繁琐步骤封装为一步创建调用;
  2. 线程安全的设备操作:读写操作内部通过互斥量(mutex)保护总线,避免多任务并发访问同一总线导致时序错乱;
  3. 简单灵活的读写操作:提供 Byte、bit 粒度的读写 API,屏蔽底层 command link / transaction 细节。

其核心是对两类概念进行抽象:

  • 总线(Bus):通信时设备之间共同拥有的资源和配置项,例如 I2C 的端口号、SDA/SCL 引脚、上下拉模式,SPI 的 MOSI/MISO/SCLK 引脚等。这些配置在系统设计阶段已经确定,一般不在运行时切换。
  • 设备(Device):通信时设备特有的资源和配置项,例如 I2C 设备地址与运行频率,SPI 设备的 CS 引脚、工作模式与时钟频率。

在电气条件允许的前提下,每个物理总线可以挂载一到多个设备:SPI 总线根据 CS 引脚对设备进行寻址,I2C 总线根据 7 位设备地址进行寻址,从而实现同一条物理总线上不同设备在软件层面的相互独立。

连接框图分别见 i2c_bus 连接框图 与 spi_bus 连接框图:

二、i2c_bus:快速上手指南

i2c_bus的源码位于 components/i2c_bus,公开 API 定义在 include/i2c_bus.h。它的使用遵循"创建总线 → 创建设备 → 读写数据 → 删除资源"的固定流程。

1. 创建总线:i2c_bus_create

调用i2c_bus_create(port, conf)创建总线实例,需要指定 I2C 端口号以及总线配置i2c_config_t

i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = I2C_MASTER_SDA_IO, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_io_num = I2C_MASTER_SCL_IO, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 100000, }; // i2c_bus configurations

关键字段说明:

字段含义
mode总线模式,组件目前仅支持主模式I2C_MODE_MASTER,源码中会显式校验(见 i2c_bus.c)
sda_io_num/scl_io_numSDA、SCL 引脚号,系统设计时确定,一般不在运行时切换
sda_pullup_en/scl_pullup_en引脚内部上拉是否使能
master.clk_speed总线默认时钟频率(Hz),仅在设备不指定频率时使用;头文件注释注明当前不超过 1MHz

从源码结构看,i2c_bus采用单例(singleton)管理模式:每个 I2C 端口只有一组全局参数生效(static i2c_bus_t s_i2c_bus[I2C_NUM_MAX])。当同一端口重复调用i2c_bus_create时,若配置未变化会直接返回已有句柄并打印ref_counter告警;若配置发生变化,则会以新配置重新初始化驱动(见 i2c_bus.c)。

2. 创建设备:i2c_bus_device_create

i2c_bus_device_handle_t i2c_device1 = i2c_bus_device_create(i2c0_bus, 0x28, 400000); // address: 0x28 , clk_speed: 400000 i2c_bus_device_handle_t i2c_device2 = i2c_bus_device_create(i2c0_bus, 0x32, 0); // address: 0x32 , clk_speed: no-specified

创建时需要指定总线句柄、设备的 7 位 I2C 地址和设备运行时钟频率。设备时钟速率可配置为 0,表示使用当前总线频率——源码中clk_speed == 0时保持conf_active不变(见 i2c_bus.c)。此外,源码对clk_speed<= 400000(400kHz)的上限校验。

每条 I2C 传输会根据设备的配置项动态切换频率。这一特性依赖 Kconfig 中的I2C_BUS_DYNAMIC_CONFIG(默认开启),其实现位于i2c_master_cmd_begin_with_conf:每次传输前比较当前激活配置与设备配置,若不一致则先i2c_driver_reinit再发送(见 i2c_bus.c)。因此同一总线上不同频率的设备可以共存。

3. 数据读取

直接使用以下 API 即可完成读取,只需传入设备句柄、寄存器地址、存放数据的 buf 与长度:

  • i2c_bus_read_byte(dev_handle, mem_address, &data):读单个 Byte;
  • i2c_bus_read_bytes(dev_handle, mem_address, data_len, data):读多个 Byte;
  • i2c_bus_read_bit(dev_handle, mem_address, bit_num, &data):读某字节中指定 bit(bit_num 取 0~7);
  • i2c_bus_read_bits(dev_handle, mem_address, bit_start, length, &data):读某字节中连续多个 bit(bit_start以 MSB 为 0 计数,length为 1~8)。
uint8_t data_rd[2] = {0}; i2c_bus_read_bytes(i2c_device1, NULL_I2C_MEM_ADDR, 2, data_rd); // 无寄存器地址,直接读 2 字节

寄存器地址传NULL_I2C_MEM_ADDR(0xFF)表示设备没有内部寄存器/存储器地址,此时地址阶段会被跳过(见 i2c_bus.c 中的条件编译分支)。从源码看,读写流程由底层构造 I2C command link 完成:start → 写入(设备地址<<1 | 写位)→ 写入寄存器地址 → 重新 start → 写入(设备地址<<1 | 读位)→ 读数据(末字节 NACK)→ stop。

4. 数据写入

  • i2c_bus_write_byte/i2c_bus_write_bytes:写单字节/多字节;
  • i2c_bus_write_bit/i2c_bus_write_bits:写指定 bit/连续 bits。实现采用读-改-写策略:先读回整字节,按位掩码合并新值后再写回(见 i2c_bus.c)。
uint8_t data_wr[2] = {0x01, 0x21}; i2c_bus_write_bytes(i2c_device2, 0x10, 2, data_wr); // 向 device2 的寄存器 0x10 写入 2 字节

5. 删除设备与总线

i2c_bus_device_delete(&i2c_device1); // 删除设备 1 i2c_bus_device_delete(&i2c_device2); // 删除设备 2 i2c_bus_delete(&i2c0_bus); // 删除总线

组件通过**引用计数(ref_counter)**管理生命周期:每个设备创建时ref_counter++,删除时--;只有ref_counter == 0时才真正反初始化底层 I2C 驱动并释放互斥量。如果设备尚未删除就调用i2c_bus_delete,总线不会被执行删除(见 i2c_bus.c)。

6. 特殊应用场景

  1. 16 位寄存器地址:部分设备(如 EEPROM、音频编解码芯片)内部寄存器地址为 16 位,此时使用i2c_bus_read_reg16/i2c_bus_write_reg16。实现中会将uint16_t地址拆分为高、低字节依次发送(见 i2c_bus.c),无内部地址时可传NULL_I2C_MEM_16BIT_ADDR(0xFFFF)。
  2. 自定义时序:对于需要跳过地址阶段或增加命令阶段的设备,可以使用底层 APIi2c_bus_cmd_begin(dev_handle, cmd)结合 ESP-IDF 的 I2C command link(i2c_cmd_link_create/i2c_master_start/i2c_master_write_byte等)自行构造任意时序。
  3. 软件 I2C:当硬件 I2C 端口不足或需要软件 I2C 调试时,可通过menuconfig(Top) → Component config → Bus Options → I2C Bus Options中开启Enable software I2C supportI2C_BUS_SUPPORT_SOFTWARE),然后在i2c_bus_createport参数传入i2c_sw_port_t类型的端口号,例如I2C_NUM_SW_0

7. 辅助 API 与 Kconfig 配置项

除了核心读写 API,i2c_bus.h 还提供:

  • i2c_bus_scan(bus_handle, buf, num):扫描总线上所有存在的从设备地址(1~126),返回发现数量并可选回填地址列表;
  • i2c_bus_get_current_clk_speed(bus_handle):获取当前激活的时钟频率;
  • i2c_bus_get_created_device_num(bus_handle):获取总线上已创建的设备数量(即引用计数)。

I2C 总线相关的全部可配置项定义在 components/i2c_bus/Kconfig:

配置项默认值说明
I2C_BUS_DYNAMIC_CONFIGy每次传输前动态检查配置并按需重装驱动,支持同总线多设备不同配置
I2C_MS_TO_WAIT200(范围 50~5000)获取总线互斥量的最大阻塞时间,单位 ms
I2C_BUS_BACKWARD_CONFIGn在 IDF v5.3 及以上强制使用旧版driver/i2c.h驱动以保持向后兼容
I2C_BUS_SUPPORT_SOFTWAREn使能软件 I2C(GPIO 模拟时序)
I2C_BUS_SOFTWARE_MAX_PORT2(范围 1~5)软件 I2C 端口最大数量
I2C_BUS_REMOVE_NULL_MEM_ADDRn关闭NULL_I2C_MEM_ADDR语义,任意寄存器地址都会被实际发送

三、spi_bus:快速上手指南

spi_bus的源码位于 components/spi_bus,公开 API 定义在 include/spi_bus.h。使用流程与 i2c_bus 对称。

1. 创建总线:spi_bus_create

spi_config_t bus_conf = { .miso_io_num = 19, .mosi_io_num = 23, .sclk_io_num = 18, }; // spi_bus configurations bus_handle = spi_bus_create(SPI2_HOST, &bus_conf); // create spi bus

需要指定 SPI 端口号(可选SPI2_HOSTSPI3_HOST,源码会按芯片支持的 SPI 外设数量做范围校验,见 spi_bus.c)以及总线配置spi_config_t

字段含义
miso_io_num/mosi_io_num/sclk_io_num对应引脚号,-1 表示未使用;系统设计时确定,一般不在运行时切换
max_transfer_sz单次传输的最大数据量,设置为 0 将使用默认值 4096

从源码看,spi_bus_create内部会补齐quadwp_io_num = -1quadhd_io_num = -1,并调用spi_bus_initialize(host_id, &buscfg, SPI_DMA_CH_AUTO)(IDF v4.3 及以上自动分配 DMA 通道),随后把激活配置保存到全局静态数组中。

2. 创建设备:spi_bus_device_create

spi_device_config_t device_conf = { .cs_io_num = 19, .mode = 0, .clock_speed_hz = 20 * 1000 * 1000, }; // spi_device configurations device_handle = spi_bus_device_create(bus_handle, &device_conf); // create spi device

创建时需要指定总线句柄、设备的 CS 引脚号、设备运行模式(mode 0~3,对应四种时钟极性/相位组合)以及时钟频率。SPI 传输时会根据设备的配置项动态切换模式和频率

源码在spi_bus_device_create中把用户配置转换为 ESP-IDF 的spi_device_interface_config_t,其中duty_cycle_pos = 128(50% 占空比)、cs_ena_posttrans = 3(事务结束后 CS 保持低电平 3 个时钟周期,防止从机因 CS 传播延迟漏采最后一位)、queue_size = 3,并为每个设备创建独立的互斥量以保证线程安全(见 spi_bus.c)。

3. 数据传输

由于 SPI 是全双工通信,每次传输的发送和接收可以同时进行:

uint8_t data8_in = 0; uint8_t data8_out = 0xff; uint16_t data16_in = 0; uint32_t data32_in = 0; spi_bus_transfer_bytes(device_handle, &data8_out, &data8_in, 1); // 收发 1 字节 spi_bus_transfer_bytes(device_handle, NULL, &data8_in, 1); // 仅读 1 字节 spi_bus_transfer_bytes(device_handle, &data8_out, NULL, 1); // 仅写 1 字节 spi_bus_transfer_reg16(device_handle, 0x1020, &data16_in); // 传输 16 位数据 spi_bus_transfer_reg32(device_handle, 0x10203040, &data32_in); // 传输 32 位数据

API 一览:

  • spi_bus_transfer_byte(dev_handle, data_out, &data_in):单字节收发;
  • spi_bus_transfer_bytes(dev_handle, data_out, data_in, data_len):多字节收发,data_outdata_inNULL可跳过发送或接收阶段;
  • spi_bus_transfer_reg16/spi_bus_transfer_reg32:传输 16/32 位定长数据,默认 MSB 先发(如0x1234先发0x12再发0x34,见 spi_bus.c)。

底层实现通过spi_device_polling_transmit轮询方式完成事务,发送/接收借助SPI_TRANS_USE_RXDATA/SPI_TRANS_USE_TXDATA内联缓冲区,不依赖外部 DMA buffer。

4. 删除设备与总线

spi_bus_device_delete(&device_handle); spi_bus_delete(&bus_handle);

spi_bus_delete内部调用spi_bus_free释放总线资源;与 i2c_bus 类似,设备删除通过设备级互斥量保护,保证并发安全。

5. 特殊应用场景

对于标准spi_bus_transfer_xx无法满足的时序需求(如需要自定义 command/address/dummy 位段、非字节对齐传输等),可直接使用底层 APIspi_bus_transmit_begin(dev_handle, &trans)结合 ESP-IDF 的spi_transaction_t结构体构造事务,组件内部同样以互斥量保护后调用spi_device_polling_transmit(见 spi_bus.c)。

四、版本适配与验证

两个组件均适配 ESP-IDF v4.0 及以上版本。针对 IDF v5.3 引入的新版driver/i2c_master.h驱动,i2c_bus通过版本宏自动选择头文件,并提供了I2C_BUS_BACKWARD_CONFIG开关以兼容旧驱动;i2c_bus.h还根据 IDF 版本对gpio_pad_select_gpioportTICK_RATE_MS等做了宏兼容处理。

仓库内提供了完整的自动化测试用例,可作为使用范本参考:

  • components/i2c_bus/test_apps/main/test_i2c_bus.c:覆盖总线创建/删除(含配置不变与配置变化两种分支)、设备挂载/删除、Byte 与 bit 级读写、数据长度 129 字节的读写回环、总线扫描(扫描 100 个地址)以及内存泄漏阈值检测等场景,配套 pytest_i2c_bus.py 与 sdkconfig.ci.software(软件 I2C 的 CI 配置);
  • components/spi_bus/test_apps/main/test_spi_bus.c:验证 SPI 总线创建/删除、设备增删、8/16/32 位数据传输及收发缓冲区的正确性。

五、实践要点小结

  1. 记住"总线全局、设备局部"的分层:引脚与端口等公共资源在总线级配置一次,设备地址、频率等私有属性在设备级配置,天然支持一条总线挂多个设备。
  2. 删除顺序必须"先设备、后总线":引用计数机制决定了总线只有在设备全部删除后才会真正释放,违反顺序只会得到告警而不会释放资源。
  3. 合理利用动态配置:I2C 总线默认开启I2C_BUS_DYNAMIC_CONFIG,同一总线上可混合挂载 100kHz 与 400kHz 的设备;若确定全总线同频率,也可关闭该选项以省去每次传输前的配置比对开销。
  4. 无寄存器设备记得用NULL_I2C_MEM_ADDR:直接访问 I2C 内存型设备(如温度传感器、EEPROM 的 IO 扩展口)时,传 0xFF 可跳过地址阶段。
  5. bit 操作是读-改-写i2c_bus_write_bit/write_bits会先读回整字节再修改指定位,使用前应确保该寄存器可读。
  6. IO 资源不足时启用软件 I2C:通过 menuconfig 开启I2C_BUS_SUPPORT_SOFTWARE后即可使用I2C_NUM_SW_0等软件端口,缓解硬件 I2C 端口数量限制。

六、延伸阅读

  • 总线组件总览:docs/zh_CN/basic/bus/index.rst
  • i2c_bus 完整文档:docs/zh_CN/basic/bus/i2c_bus.rst;spi_bus 完整文档:docs/zh_CN/basic/bus/spi_bus.rst
  • I2C 总线实现源码:components/i2c_bus/i2c_bus.c、软件 I2C 实现:components/i2c_bus/i2c_bus_soft.c
  • SPI 总线实现源码:components/spi_bus/spi_bus.c
  • 基于这两个组件的实际设备驱动示例:如 components/sensors 下的各类传感器驱动、components/display 下的 LCD 面板驱动,均大量使用本组件的 Bus/Device API 组织硬件访问

【免费下载链接】esp-iot-solutionEspressif IoT Library. IoT Device Drivers, Documentations and Solutions.项目地址: https://gitcode.com/GitHub_Trending/es/esp-iot-solution

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询