ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Arduino ESP32 的 Wire (I2C) 库完全指南:主从模式、API 详解与实战示例

2026/9/13 15:06:24 拓冰建站 浏览量
Arduino ESP32 的 Wire (I2C) 库完全指南:主从模式、API 详解与实战示例 Arduino ESP32 的 Wire (I2C) 库完全指南主从模式、API 详解与实战示例【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32导读I2CInter-Integrated Circuit集成电路间总线又称 TWITwo-wire Interface两线制接口是 Arduino-ESP32 生态中最常用的低速串行外设总线广泛用于连接传感器、EEPROM、RTC、ADC/DAC、显示屏幕如 OLED等设备。本文将基于官方 API 文档 docs/en/api/i2c.rst完整梳理 Arduino-ESP32 的Wire库在主模式与从模式下的全部 API、默认引脚与时钟配置、回调机制以及底层实现原理并给出可直接编译运行的主从机示例程序。读完本文你将能够熟练使用Wire库完成多从设备扫描、主从双向读写、重复起始Repeated Start传输以及基于std::function的功能回调编程。I2C 总线基础什么是 I2C / TWII2C 是一种短距离串行通信协议通过两根线连接多个设备SDAserial data line串行数据线承载数据位SCLserial clock line串行时钟线承载时钟信号。每个挂在总线上的设备都有一个唯一的 7 位地址理论上一条总线最多可挂载128 个设备地址0x00–0x7F其中部分地址为保留地址实际可用地址少于 128 个。由于是低速总线且采用开漏open-drain结构SDA 与 SCL 两根线都必须外接上拉电阻pull-up resistors上拉电阻的取值与工作电压请以对应设备的数据手册为准见 i2c.rst 中的 note 说明。I2C 的两种工作模式I2C 总线上存在两种角色I2C 主模式Master ModeESP32 作为主设备生成时钟信号并主动发起与从设备的通信总线上的时序完全由主设备掌控I2C 从模式Slave ModeESP32 作为从设备时钟由主设备生成只有当主设备发送的目的地址与自身地址匹配时才响应。两种模式的连接示意分别如下从图中可以看到主模式下主设备如 ESP32-MINI-1通过两条总线并联连接多个不同地址的从设备而从模式下两块 ESP32 通过 SDA/SCL 交叉互联即可通信。这正是后续 Master/Slave 两种示例程序的硬件接线基础。Arduino-ESP32 I2C API 总览ESP32 的 I2C 库基于 Arduino 官方 Wire 库设计保持与 Arduino 生态的兼容性并在此基础上扩展了更多 API见 i2c.rst。所有 API 都封装在TwoWire类中其类声明位于 libraries/Wire/src/Wire.h实现位于 libraries/Wire/src/Wire.cpp。TwoWire继承自 cores/esp32/HardwareI2C.h 中定义的抽象基类HardwareI2C其本身又继承自Stream因此Wire天然具备printf、print、println、read、available等流式接口。全局实例与多总线支持在 Wire.cpp 的末尾定义了全局实例TwoWire Wire TwoWire(0);—— 默认 I2C 总线当芯片具备多个 I2C 外设时如 ESP32 的SOC_I2C_NUM 1还会定义Wire1以及 ESP-IDF ≥ 5.4 且SOC_I2C_NUM 2时的Wire2。默认引脚来自对应开发板的pins_arduino.h。以Generic ESP32variants/esp32/pins_arduino.h为例信号默认 GPIOSDAGPIO21SCLGPIO22不同开发板默认引脚可能不同使用前请查阅你所使用板卡的变体文件。库属性文件 libraries/Wire/library.properties 显示当前 Wire 库版本为 3.3.11类别为 Signal Input/Output仅面向 esp32 架构。I2C 通用 API主从模式共用以下 API 在两种模式下均可使用。begin()启动 I2C 外设并使用默认配置bool begin();初始化成功返回true。在主模式下也可传入引脚与频率详见后文 I2C Master APIs。setPins()自定义 SDA 与 SCL 引脚bool setPins(int sdaPin, int sclPin);sdaPin指定作为数据线的 GPIOsclPin指定作为时钟线的 GPIO。注意必须在调用begin()之前调用setPins()才能生效。从实现看Wire.cpp如果总线已经初始化setPins会打印bus already initialized. change pins only when not.并拒绝修改。另一个实用技巧是先 setPins 再无参 begin这正是 Wire.h 注释call setPins() first, so that begin() can be called without arguments from libraries所推荐的模式便于第三方库以无参方式初始化。配置成功返回true。setClock() / getClock()设置与读取总线时钟频率bool setClock(uint32_t frequency); uint32_t getClock();frequency总线时钟频率单位 Hz不调用时使用默认值通常为 100 kHz具体由底层i2cInit决定setClock配置成功返回truegetClock返回当前频率配置Wire.cpp 中通过底层i2cGetClock读取实际生效值。注意从源码看Wire.cppsetClock在从模式下会报错Bus is in Slave Mode并返回false即该函数主要用于主模式。setTimeOut() / getTimeOut()设置与读取总线超时时间void setTimeOut(uint16_t timeOutMillis); uint16_t getTimeOut();timeOutMillis超时时间毫秒默认值为 50 ms在 Wire.cpp 的构造函数中初始化为_timeOutMillis(50)getTimeOut返回当前超时配置。write()向发送缓冲区写入数据注意不是直接写入从设备size_t write(uint8_t); size_t write(const uint8_t *, size_t);返回值是实际写入缓冲区的字节数。缓冲区默认大小为 128 字节Wire.h 中I2C_BUFFER_LENGTH 128可通过setBufferSize()调整最小 32 字节即 ESP32/S2/S3/C3 的 I2C FIFO 长度见 Wire.cpp。当缓冲区写满时write返回 0。end()结束通信并释放所有已分配资源bool end();调用end()之后如需再次使用必须重新调用begin()初始化。实现上Wire.cpp会根据当前是从模式还是主模式分别调用i2cSlaveDeinit或i2cDeinit并释放收发缓冲区。I2C Master 模式主模式用于主动发起与从设备的通信。基本使用流程第一步在草图sketch中包含头文件#include Wire.h第二步初始化总线Wire.begin();无参调用将使用全部默认配置默认引脚、默认频率。想自定义参数可参考下文 Master APIs 中的begin重载。第三步发起传输并写数据Wire.beginTransmission(I2C_DEV_ADDR); // 指定从设备 7 位地址 Wire.write(x); // 支持多种数据类型 uint8_t error Wire.endTransmission(true); // 真正把缓冲区的数据发出去关键要点write只把数据追加进 I2C 发送缓冲区并不会立即发往从设备必须调用endTransmission才会把缓冲字节真正发送出去i2c.rst 中的 note 特别强调了这一点。第四步向从设备请求读取uint8_t bytesReceived Wire.requestFrom(I2C_DEV_ADDR, SIZE); uint8_t temp[SIZE]; Wire.readBytes(temp, bytesReceived); // 读出请求到的字节requestFrom向指定地址请求指定大小的数据返回实际读取到的字节数readBytes将接收缓冲区中的数据读出。Master 专用 APIbegin(int sdaPin, int sclPin, uint32_t frequency)主模式初始化可同时指定引脚与频率bool begin(int sdaPin, int sclPin, uint32_t frequency)其中frequency 0时使用默认频率Wire.h。也可完全无参调用以使用全部默认值。初始化成功返回true。底层实现Wire.cpp会依次完成互斥锁创建与加锁 → 分配收发缓冲区allocateWireBuffer→ 引脚解析initPins→ 调用i2cInit(num, sda, scl, frequency)只有i2cInit返回ESP_OK才算启动成功。beginTransmission(uint16_t address)开始与从设备的一次通信过程须在向缓冲区写数据之前传入从设备addressvoid beginTransmission(uint16_t address)endTransmission(bool sendStop)写完缓冲区后调用它把消息发送给beginTransmission指定的从设备地址uint8_t endTransmission(bool sendStop);sendStoptrue发送停止信号false不发送仅在主模式下有意义用于实现重复起始 / Repeated Start 连续传输不传参调用等价于sendStop trueuint8_t endTransmission(void);返回值为错误码。Arduino 标准错误码定义源码注释见 Wire.cpp返回值含义0成功1数据过长无法放入发送缓冲区2发送地址时收到 NACK从设备无应答3发送数据时收到 NACK4其他错误5超时在实现中Wire.cppESP_OK映射为 0ESP_FAIL与ESP_ERR_NOT_FOUND映射为 2ESP_ERR_TIMEOUT映射为 5其余映射为 4。requestFrom(uint16_t address, uint8_t size, bool sendStop)从从设备读取数据uint8_t requestFrom(uint16_t address, uint8_t size, bool sendStop)address设备地址size请求读取的字节数sendStoptrue使能停止信号false关闭配合endTransmission(false)实现 Repeated Start 读写。返回实际从设备读取到的字节数。从源码看Wire.cpp若处于未完成的 Repeated Start 传输状态nonStop truerequestFrom会校验地址是否与beginTransmission中设置的一致一致则调用i2cWriteReadNonStop完成写后读组合事务否则调用i2cRead执行纯读。请求大小超过缓冲区时会被截断到缓冲区大小并打印提示日志。完整示例WireMaster.inoWireMaster.ino 展示了完整的主模式读写流程每 5 秒向从设备写一条带计数器的消息再请求读取 16 字节#include Arduino.h #include Wire.h #define I2C_DEV_ADDR 0x55 uint32_t i 0; void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Wire.begin(); } void loop() { delay(5000); //Write message to the slave Wire.beginTransmission(I2C_DEV_ADDR); Wire.printf(Hello World! % PRIu32, i); uint8_t error Wire.endTransmission(true); Serial.printf(endTransmission: %u\n, error); //Read 16 bytes from the slave uint8_t bytesReceived Wire.requestFrom(I2C_DEV_ADDR, 16); Serial.printf(requestFrom: %u\n, bytesReceived); if ((bool)bytesReceived) { //If received more than zero bytes uint8_t temp[bytesReceived]; Wire.readBytes(temp, bytesReceived); log_print_buf(temp, bytesReceived); } }这段代码同时展示了两个值得注意的细节Wire.printf(Hello World! % PRIu32, i)之所以可用是因为TwoWire继承自Stream直接复用流式格式化输出log_print_buf是 ESP32 的二进制打印工具函数可将收到的字节以十六进制形式输出到调试串口。配套的 CI 配置位于 WireMaster/ci.yml可用于自动化编译验证。I2C Slave 模式从模式用于接受主设备发起的通信。注意从模式并非所有 ESP32 系列芯片都支持Wire.h与Wire.cpp中所有从模式代码都被#if SOC_I2C_SUPPORT_SLAVE包裹如 Wire.cpp若芯片不支持如某些仅支持主模式的型号调用begin(addr)会打印I2C slave is not supported on ...并返回falseWire.h。基本使用流程第一步包含头文件#include Wire.h第二步注册两个回调函数以处理与主设备的通信Wire.onReceive(onReceive); Wire.onRequest(onRequest);onReceive处理主设备发来的数据对应主设备写、从设备收onRequest应答主设备的读请求对应主设备读、从设备发。第三步以设备地址初始化从模式Wire.begin((uint8_t)I2C_DEV_ADDR);从模式begin也可以同时指定引脚与频率见下文 Slave APIs。ESP32 专属slaveWrite 预写入为兼容 Arduino 的从机响应模型**ESP32原版芯片**提供slaveWrite预写入接口Wire.slaveWrite((uint8_t *)message, strlen(message));其作用是在主设备发起读请求之前先把响应数据预写入从设备响应缓冲区。警告该函数仅 ESP32 需要使用ESP32-S2 与 ESP32-C3 不需要见 i2c.rst 的 warning 说明。这是因为 ESP32 原版芯片从机硬件没有自动响应的写缓冲需要软件预填充以保持与 Arduino Wire 行为的兼容。Slave 专用 APIbegin(uint8_t addr, int sdaPin, int sclPin, uint32_t frequency)从模式初始化必须传入从设备地址同时可选定义引脚与总线频率bool Wire.begin(uint8_t addr, int sdaPin, int sclPin, uint32_t frequency)初始化成功返回true。底层实现Wire.cpp在分配缓冲区、解析引脚之后调用i2cSlaveInit(num, sda, scl, addr, frequency, bufferSize, bufferSize)完成从机初始化。onReceive()定义主设备向本从机发送数据时的回调void onReceive(const std::functionvoid(int) callback);回调函数签名必须为void(int numBytes)其中numBytes表示本次从主设备收到的字节数。五种典型用法// Method 1: 普通函数 void handleReceive(int numBytes) { Serial.printf(Received %d bytes: , numBytes); while (Wire.available()) { char c Wire.read(); Serial.print(c); } Serial.println(); } Wire.onReceive(handleReceive); // Method 2: Lambda 函数 Wire.onReceive([](int numBytes) { Serial.printf(Master sent %d bytes\n, numBytes); while (Wire.available()) { uint8_t data Wire.read(); // Process received data Serial.printf(Data: 0x%02X\n, data); } }); // Method 3: 带捕获的 Lambda访问外部变量 int deviceId 42; Wire.onReceive(deviceId { Serial.printf(Device %d received %d bytes\n, deviceId, numBytes); // Process data... }); // Method 4: 使用 std::function 变量 std::functionvoid(int) receiveHandler [](int bytes) { Serial.printf(Handling %d received bytes\n, bytes); }; Wire.onReceive(receiveHandler); // Method 5: 类成员函数通过 Lambda 包装 class I2CDevice { private: int deviceAddress; public: I2CDevice(int addr) : deviceAddress(addr) {} void handleReceive(int numBytes) { Serial.printf(Device 0x%02X received %d bytes\n, deviceAddress, numBytes); } void setup() { Wire.onReceive(this { this-handleReceive(bytes); }); } };在回调内部请使用Wire.available()与Wire.read()取回接收到的数据。从实现看Wire.cpp当底层 I2C 从机中断触发时onReceiveService会把收到的字节拷贝进rxBuffer设置rxIndex 0、rxLength numBytes再调用用户注册的user_onReceive(numBytes)这就是回调参数numBytes的来源。onRequest()定义主设备向本从机发起读请求时的应答回调void onRequest(const std::functionvoid() callback);回调签名必须为void()无参数。触发时机主设备请求本从机数据时。五种典型用法// Method 1: 普通函数 void handleRequest() { static int counter 0; Wire.printf(Response #%d, counter); } Wire.onRequest(handleRequest); // Method 2: Lambda 函数发送传感器数据 Wire.onRequest([]() { // Send sensor data to master int sensorValue analogRead(A0); Wire.write(sensorValue 8); // High byte Wire.write(sensorValue 0xFF); // Low byte }); // Method 3: 带捕获的 Lambda int deviceStatus 1; String deviceName Sensor1; Wire.onRequest([deviceStatus, deviceName]() { Wire.write(deviceStatus); Wire.write(deviceName.c_str(), deviceName.length()); }); // Method 4: 使用 std::function 变量 std::functionvoid() requestHandler []() { Wire.write(Hello Master!); }; Wire.onRequest(requestHandler); // Method 5: 类成员函数通过 Lambda 包装 class TemperatureSensor { private: float temperature; public: void updateTemperature() { temperature 25.5; // Read from actual sensor } void sendTemperature() { // Convert float to bytes and send uint8_t* tempBytes (uint8_t*)temperature; Wire.write(tempBytes, sizeof(float)); } void setup() { Wire.onRequest([this]() { this-sendTemperature(); }); } };在onRequest回调内部请使用Wire.write()向主设备发送响应数据。从实现看Wire.cpponRequestService会先把txLength清零然后调用用户回调回调中Wire.write的数据累积进txBuffer回调返回后若txLength 0则自动调用slaveWrite把数据写入从机响应缓冲区发送给主设备——这就是在回调里Wire.write即回发数据的底层机制。slaveWrite()在收到响应消息之前预写从机响应缓冲区size_t slaveWrite(const uint8_t *, size_t);仅用于为 ESP32 添加从机兼容能力ESP32-S2 / ESP32-C3 不需要。返回写入的字节数。实现位于 Wire.cpp直接调用底层i2cSlaveWrite(num, buffer, len, _timeOutMillis)。完整示例WireSlave.inoWireSlave.ino 与上文的 WireMaster 配对使用从机地址同为0x55#include Arduino.h #include Wire.h #define I2C_DEV_ADDR 0x55 uint32_t i 0; void onRequest() { Wire.print(i); Wire.print( Packets.); Serial.println(onRequest); } void onReceive(int len) { Serial.printf(onReceive[%d]: , len); while (Wire.available()) { Serial.write(Wire.read()); } Serial.println(); } void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Wire.onReceive(onReceive); Wire.onRequest(onRequest); Wire.begin((uint8_t)I2C_DEV_ADDR); #if CONFIG_IDF_TARGET_ESP32 char message[64]; snprintf(message, 64, % PRIu32 Packets., i); Wire.slaveWrite((uint8_t *)message, strlen(message)); #endif } void loop() {}注意其中#if CONFIG_IDF_TARGET_ESP32的条件编译只有编译目标是原版 ESP32 芯片时才调用slaveWrite预填充响应缓冲区这与文档中该函数仅 ESP32 需要的警告完全对应。函数式回调示例WireSlaveFunctionalCallback.ino仓库还提供了使用 Lambda 表达式的函数式回调版本 WireSlaveFunctionalCallback.ino其核心区别在于用Wire.onRequest([](){...})与Wire.onReceive([](int len){...})直接内联回调逻辑无需单独定义全局函数代码更加紧凑Wire.onRequest([]() { Wire.print(i); Wire.print( Packets.); Serial.println(onRequest); }); Wire.onReceive([](int len) { Serial.printf(onReceive[%d]: , len); while (Wire.available()) { Serial.write(Wire.read()); } Serial.println(); });该示例与WireSlave的其余初始化流程一致Wire.begin((uint8_t)I2C_DEV_ADDR)加 ESP32 专属的slaveWrite预写入。实践技巧与排障指南扫描总线上的 I2C 设备接线或地址不确定时可使用 WireScan.ino 扫描0x01–0x7F全部 7 位地址#include Arduino.h #include Wire.h void setup() { Serial.begin(115200); Wire.begin(); } void loop() { byte error, address; int nDevices 0; delay(5000); Serial.println(Scanning for I2C devices ...); for (address 0x01; address 0x7f; address) { Wire.beginTransmission(address); error Wire.endTransmission(); if (error 0) { Serial.printf(I2C device found at address 0x%02X\n, address); nDevices; } else if (error ! 2) { Serial.printf(Error %u at address 0x%02X\n, error, address); } } if (nDevices 0) { Serial.println(No I2C devices found); } }其原理正是利用endTransmission的错误码返回 0 表示该地址有设备应答返回 2NACK表示该地址无设备属于正常现象无需打印。常见问题排查上拉电阻缺失SDA/SCL 无上拉会导致总线电平不稳定、设备无法识别请按设备手册配置上拉电阻默认引脚不符各开发板默认 SDA/SCL 不同Generic ESP32 为 GPIO21/GPIO22务必查阅 variants 目录下对应板卡的头文件write后数据未发送write只写缓冲区必须调用endTransmission才会真正传输从模式初始化失败先确认目标芯片SOC_I2C_SUPPORT_SLAVE是否支持从模式原版 ESP32 需额外使用slaveWrite预填充响应缓冲区缓冲区溢出默认缓冲区 128 字节传输大数据时先用setBufferSize()扩容最小 32 字节总线卡死遇到挂死可调小setTimeOut超时默认 50 msendTransmission返回 5 即表示超时。总结Arduino-ESP32 的Wire库在完整继承 Arduino Wire 标准 APIbeginTransmission/endTransmission/requestFrom/onReceive/onRequest等的基础上扩展了引脚自定义setPins、时钟读写setClock/getClock、超时控制setTimeOut/getTimeOut、缓冲区扩容setBufferSize、资源释放end以及 ESP32 专属的slaveWrite预写入接口并支持std::function函数式回调与Wire1/Wire2多总线。无论是驱动外部传感器、读写 EEPROM/RTC还是实现两块 ESP32 之间的 I2C 主从互联官方文档 docs/en/api/i2c.rst 结合 libraries/Wire 下的四个示例WireMaster、WireSlave、WireSlaveFunctionalCallback、WireScan都可以作为你快速上手的直接参考。【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考