Fix I2C recovery on Arduino (#2412)

Co-authored-by: Maurice Makaay <mmakaay1@xs4all.net>
This commit is contained in:
Maurice Makaay 2021-10-01 12:53:37 +02:00 committed by GitHub
parent 5a2984d03a
commit d0dfc94a61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 117 additions and 20 deletions

View file

@ -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

View file

@ -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_;