From d0dfc94a61cd6d3758e4284156d5ce707a7803b2 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Fri, 1 Oct 2021 12:53:37 +0200 Subject: [PATCH] Fix I2C recovery on Arduino (#2412) Co-authored-by: Maurice Makaay --- esphome/components/i2c/i2c_bus_arduino.cpp | 130 +++++++++++++++++---- esphome/components/i2c/i2c_bus_arduino.h | 7 ++ 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 87dbcb66d8..539091ed9c 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -32,6 +32,17 @@ void ArduinoI2CBus::dump_config() { ESP_LOGCONFIG(TAG, " SDA Pin: GPIO%u", this->sda_pin_); ESP_LOGCONFIG(TAG, " SCL Pin: GPIO%u", this->scl_pin_); ESP_LOGCONFIG(TAG, " Frequency: %u Hz", this->frequency_); + 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, "Scanning i2c bus for active devices..."); uint8_t found = 0; @@ -92,31 +103,110 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn 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_() { - // Perform I2C bus recovery, see - // https://www.analog.com/media/en/technical-documentation/application-notes/54305147357414AN686_0.pdf - // or see the linux kernel implementation, e.g. - // https://elixir.bootlin.com/linux/v5.14.6/source/drivers/i2c/i2c-core-base.c#L200 + ESP_LOGI(TAG, "Performing I2C bus recovery"); - // try to get about 100kHz toggle frequency - const auto half_period_usec = 1000000 / 100000 / 2; - const auto recover_scl_periods = 9; - - // configure scl as output - pinMode(scl_pin_, OUTPUT); // NOLINT - - // set scl high - digitalWrite(scl_pin_, 1); // NOLINT - - // in total generate 9 falling-rising edges - for (auto i = 0; i < recover_scl_periods; i++) { - delayMicroseconds(half_period_usec); - digitalWrite(scl_pin_, 0); // NOLINT - delayMicroseconds(half_period_usec); - digitalWrite(scl_pin_, 1); // NOLINT + // Activate the pull up resistor on the SCL pin. 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. + pinMode(scl_pin_, INPUT_PULLUP); // NOLINT + if (digitalRead(scl_pin_) == LOW) { // NOLINT + ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the I2C 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. + + // Use a 100kHz toggle frequency (i.e. the maximum frequency for I2C + // running in standard-mode). The resulting 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; + + // Make sure that switching to mode OUTPUT will make SCL low, just in + // case other code has setup the pin to output a HIGH signal. + digitalWrite(scl_pin_, LOW); // NOLINT + + // Activate the pull up resistor for SDA, so after the clock pulse cycle + // we can verify if SDA is pulled high. Also make sure that switching to + // mode OUTPUT will make SDA low. + pinMode(sda_pin_, INPUT_PULLUP); // NOLINT + digitalWrite(sda_pin_, LOW); // NOLINT + + ESP_LOGI(TAG, "Sending 9 clock pulses to drain any stuck device output"); 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'll stick to 500ms here. + auto wait = 20; + while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT + delay(25); + } + 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; + } + } + + // By now, any stuck device ought to have sent all remaining bits of its + // transation, 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. + ESP_LOGI(TAG, "Generate START condition to reset bus logic of I2C devices"); + pinMode(sda_pin_, INPUT); // NOLINT + pinMode(sda_pin_, OUTPUT); // NOLINT + delayMicroseconds(half_period_usec); + + // 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. + ESP_LOGI(TAG, "Generate STOP condition to finalize recovery"); + pinMode(sda_pin_, INPUT); // NOLINT + pinMode(sda_pin_, INPUT_PULLUP); // NOLINT + + recovery_result_ = RECOVERY_COMPLETED; } } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 42589dcfb7..82f043ef7d 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -9,6 +9,12 @@ namespace esphome { namespace i2c { +enum RecoveryCode { + RECOVERY_FAILED_SCL_LOW, + RECOVERY_FAILED_SDA_LOW, + RECOVERY_COMPLETED, +}; + class ArduinoI2CBus : public I2CBus, public Component { public: void setup() override; @@ -24,6 +30,7 @@ class ArduinoI2CBus : public I2CBus, public Component { private: void recover_(); + RecoveryCode recovery_result_; protected: TwoWire *wire_;