mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +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, "  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 | ||||
|   | ||||
| @@ -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_; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user