mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Fix I2C recovery on Arduino (#2412)
Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
This commit is contained in:
		| @@ -32,6 +32,17 @@ void ArduinoI2CBus::dump_config() { | |||||||
|   ESP_LOGCONFIG(TAG, "  SDA Pin: GPIO%u", this->sda_pin_); |   ESP_LOGCONFIG(TAG, "  SDA Pin: GPIO%u", this->sda_pin_); | ||||||
|   ESP_LOGCONFIG(TAG, "  SCL Pin: GPIO%u", this->scl_pin_); |   ESP_LOGCONFIG(TAG, "  SCL Pin: GPIO%u", this->scl_pin_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Frequency: %u Hz", this->frequency_); |   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_) { |   if (this->scan_) { | ||||||
|     ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); |     ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); | ||||||
|     uint8_t found = 0; |     uint8_t found = 0; | ||||||
| @@ -92,31 +103,110 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn | |||||||
|   return ERROR_UNKNOWN; |   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_() { | void ArduinoI2CBus::recover_() { | ||||||
|   // Perform I2C bus recovery, see |   ESP_LOGI(TAG, "Performing I2C bus recovery"); | ||||||
|   // 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 |  | ||||||
|  |  | ||||||
|   // try to get about 100kHz toggle frequency |   // Activate the pull up resistor on the SCL pin. This should make the | ||||||
|   const auto half_period_usec = 1000000 / 100000 / 2; |   // signal on the line HIGH. If SCL is pulled low on the I2C bus however, | ||||||
|   const auto recover_scl_periods = 9; |   // then some device is interfering with the SCL line. In that case, | ||||||
|  |   // the I2C bus cannot be recovered. | ||||||
|   // configure scl as output |   pinMode(scl_pin_, INPUT_PULLUP);     // NOLINT | ||||||
|   pinMode(scl_pin_, OUTPUT);  // NOLINT |   if (digitalRead(scl_pin_) == LOW) {  // NOLINT | ||||||
|  |     ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the I2C bus"); | ||||||
|   // set scl high |     recovery_result_ = RECOVERY_FAILED_SCL_LOW; | ||||||
|   digitalWrite(scl_pin_, 1);  // NOLINT |     return; | ||||||
|  |  | ||||||
|   // 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 |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // 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); |   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 i2c | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -9,6 +9,12 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace i2c { | namespace i2c { | ||||||
|  |  | ||||||
|  | enum RecoveryCode { | ||||||
|  |   RECOVERY_FAILED_SCL_LOW, | ||||||
|  |   RECOVERY_FAILED_SDA_LOW, | ||||||
|  |   RECOVERY_COMPLETED, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class ArduinoI2CBus : public I2CBus, public Component { | class ArduinoI2CBus : public I2CBus, public Component { | ||||||
|  public: |  public: | ||||||
|   void setup() override; |   void setup() override; | ||||||
| @@ -24,6 +30,7 @@ class ArduinoI2CBus : public I2CBus, public Component { | |||||||
|  |  | ||||||
|  private: |  private: | ||||||
|   void recover_(); |   void recover_(); | ||||||
|  |   RecoveryCode recovery_result_; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   TwoWire *wire_; |   TwoWire *wire_; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user