#if defined(USE_ARDUINO) && !defined(USE_ESP32) #include "i2c_bus_arduino.h" #include #include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::i2c { static const char *const TAG = "i2c.arduino"; // Maximum bytes to log in hex format (truncates larger transfers) static constexpr size_t I2C_MAX_LOG_BYTES = 32; void ArduinoI2CBus::setup() { recover_(); #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) // Select Wire instance based on pin assignment, not definition order. // I2C controller = (gpio / 2) % 2: even pairs (0-1,4-5,...) → I2C0, odd pairs (2-3,6-7,...) → I2C1 // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf if ((this->sda_pin_ / 2) % 2 == 0) { wire_ = &Wire; } else { wire_ = &Wire1; } #endif this->set_pins_and_clock_(); this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); this->i2c_scan_(); } } void ArduinoI2CBus::set_pins_and_clock_() { #ifdef USE_RP2040 wire_->setSDA(this->sda_pin_); wire_->setSCL(this->scl_pin_); wire_->begin(); #else wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); #endif if (timeout_ > 0) { // if timeout specified in yaml #if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h wire_->setClockStretchLimit(timeout_); // unit: us #elif defined(USE_RP2040) // https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h wire_->setTimeout(timeout_ / 1000); // unit: ms #endif } wire_->setClock(frequency_); } void ArduinoI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u\n" " SCL Pin: GPIO%u\n" " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); if (timeout_ > 0) { #if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2040) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); #endif } switch (this->recovery_result_) { case RECOVERY_COMPLETED: ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); break; case RECOVERY_FAILED_SCL_LOW: ESP_LOGCONFIG(TAG, " Recovery: failed, SCL is held low on the bus"); break; case RECOVERY_FAILED_SDA_LOW: ESP_LOGCONFIG(TAG, " Recovery: failed, SDA is held low on the bus"); break; } if (this->scan_) { ESP_LOGI(TAG, "Results from bus scan:"); if (scan_results_.empty()) { ESP_LOGI(TAG, "Found no devices"); } else { for (const auto &s : scan_results_) { if (s.second) { ESP_LOGI(TAG, "Found device at address 0x%02X", s.first); } else { ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); } } } } } ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, size_t read_count) { #if defined(USE_ESP8266) this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances #endif if (!initialized_) { ESP_LOGD(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)]; ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count)); #endif uint8_t status = 0; if (write_count != 0 || read_count == 0) { wire_->beginTransmission(address); size_t ret = wire_->write(write_buffer, write_count); if (ret != write_count) { ESP_LOGV(TAG, "TX failed"); return ERROR_UNKNOWN; } status = wire_->endTransmission(read_count == 0); } if (status == 0 && read_count != 0) { size_t ret2 = wire_->requestFrom(address, read_count, true); if (ret2 != read_count) { ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2); return ERROR_TIMEOUT; } for (size_t j = 0; j != read_count; j++) read_buffer[j] = wire_->read(); } // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266 if (status == 0) return ERROR_OK; if (status == 1) { ESP_LOGVV(TAG, "TX failed: buffer not large enough"); return ERROR_UNKNOWN; } if (status == 2 || status == 3) { ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status); return ERROR_NOT_ACKNOWLEDGED; } if (status == 5) { ESP_LOGVV(TAG, "TX failed: timeout"); return ERROR_UNKNOWN; } ESP_LOGVV(TAG, "TX failed: unknown error %u", status); return ERROR_UNKNOWN; } /// Perform I2C bus recovery, see: /// https://www.nxp.com/docs/en/user-guide/UM10204.pdf /// https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf void ArduinoI2CBus::recover_() { ESP_LOGI(TAG, "Performing bus recovery"); // For the upcoming operations, target for a 100kHz toggle frequency. // This is the maximum frequency for I2C running in standard-mode. // The actual frequency will be lower, because of the additional // function calls that are done, but that is no problem. const auto half_period_usec = 1000000 / 100000 / 2; // Activate input and pull up resistor for the SCL pin. pinMode(scl_pin_, INPUT_PULLUP); // NOLINT // This should make the signal on the line HIGH. If SCL is pulled low // on the I2C bus however, then some device is interfering with the SCL // line. In that case, the I2C bus cannot be recovered. delayMicroseconds(half_period_usec); if (digitalRead(scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus"); recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } // From the specification: // "If the data line (SDA) is stuck LOW, send nine clock pulses. The // device that held the bus LOW should release it sometime within // those nine clocks." // We don't really have to detect if SDA is stuck low. We'll simply send // nine clock pulses here, just in case SDA is stuck. Actual checks on // the SDA line status will be done after the clock pulses. // Make sure that switching to output mode will make SCL low, just in // case other code has setup the pin for a HIGH signal. digitalWrite(scl_pin_, LOW); // NOLINT delayMicroseconds(half_period_usec); for (auto i = 0; i < 9; i++) { // Release pull up resistor and switch to output to make the signal LOW. pinMode(scl_pin_, INPUT); // NOLINT pinMode(scl_pin_, OUTPUT); // NOLINT delayMicroseconds(half_period_usec); // Release output and activate pull up resistor to make the signal HIGH. pinMode(scl_pin_, INPUT); // NOLINT pinMode(scl_pin_, INPUT_PULLUP); // NOLINT delayMicroseconds(half_period_usec); // When SCL is kept LOW at this point, we might be looking at a device // that applies clock stretching. Wait for the release of the SCL line, // but not forever. There is no specification for the maximum allowed // time. We yield and reset the WDT, so as to avoid triggering reset. // No point in trying to recover the bus by forcing a uC reset. Bus // should recover in a few ms or less else not likely to recovery at // all. auto wait = 250; while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT App.feed_wdt(); delayMicroseconds(half_period_usec * 2); } if (digitalRead(scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } } // Activate input and pull resistor for the SDA pin, so we can verify // that SDA is pulled HIGH in the following step. pinMode(sda_pin_, INPUT_PULLUP); // NOLINT digitalWrite(sda_pin_, LOW); // NOLINT // By now, any stuck device ought to have sent all remaining bits of its // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. if (digitalRead(sda_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); recovery_result_ = RECOVERY_FAILED_SDA_LOW; return; } // From the specification: // "I2C-bus compatible devices must reset their bus logic on receipt of // a START or repeated START condition such that they all anticipate // the sending of a target address, even if these START conditions are // not positioned according to the proper format." // While the 9 clock pulses from above might have drained all bits of a // single byte within a transaction, a device might have more bytes to // transmit. So here we'll generate a START condition to snap the device // out of this state. // SCL and SDA are already high at this point, so we can generate a START // condition by making the SDA signal LOW. delayMicroseconds(half_period_usec); pinMode(sda_pin_, INPUT); // NOLINT pinMode(sda_pin_, OUTPUT); // NOLINT // From the specification: // "A START condition immediately followed by a STOP condition (void // message) is an illegal format. Many devices however are designed to // operate properly under this condition." // Finally, we'll bring the I2C bus into a starting state by generating // a STOP condition. delayMicroseconds(half_period_usec); pinMode(sda_pin_, INPUT); // NOLINT pinMode(sda_pin_, INPUT_PULLUP); // NOLINT recovery_result_ = RECOVERY_COMPLETED; } } // namespace esphome::i2c #endif // defined(USE_ARDUINO) && !defined(USE_ESP32)