#ifdef USE_ARDUINO #include "i2c_bus_arduino.h" #include #include #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { namespace i2c { static const char *const TAG = "i2c.arduino"; void ArduinoI2CBus::setup() { recover_(); #if defined(USE_ESP32) static uint8_t next_bus_num = 0; if (next_bus_num == 0) { wire_ = &Wire; } else { wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory) } this->port_ = next_bus_num; next_bus_num++; #elif defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) #elif defined(USE_RP2040) static bool first = true; if (first) { wire_ = &Wire; first = false; } else { wire_ = &Wire1; // NOLINT(cppcoreguidelines-owning-memory) } #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_ESP32) // https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp wire_->setTimeOut(timeout_ / 1000); // unit: ms #elif 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_ESP32) ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000); #elif 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; } ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); 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(); } switch (status) { case 0: return ERROR_OK; case 1: // transmit buffer not large enough ESP_LOGVV(TAG, "TX failed: buffer not large enough"); return ERROR_UNKNOWN; case 2: case 3: ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status); return ERROR_NOT_ACKNOWLEDGED; case 5: ESP_LOGVV(TAG, "TX failed: timeout"); return ERROR_UNKNOWN; case 4: default: 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 i2c } // namespace esphome #endif // USE_ESP_IDF