2021-09-20 11:47:51 +02:00
|
|
|
#ifdef USE_ARDUINO
|
|
|
|
|
|
|
|
#include "i2c_bus_arduino.h"
|
|
|
|
#include "esphome/core/log.h"
|
2021-09-23 20:11:40 +02:00
|
|
|
#include <Arduino.h>
|
2021-09-20 11:47:51 +02:00
|
|
|
#include <cstring>
|
|
|
|
|
|
|
|
namespace esphome {
|
|
|
|
namespace i2c {
|
|
|
|
|
|
|
|
static const char *const TAG = "i2c.arduino";
|
|
|
|
|
|
|
|
void ArduinoI2CBus::setup() {
|
2021-09-24 18:02:28 +02:00
|
|
|
recover_();
|
2021-10-04 12:33:25 +02:00
|
|
|
|
2021-09-20 11:47:51 +02:00
|
|
|
#ifdef 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)
|
|
|
|
next_bus_num++;
|
|
|
|
#else
|
|
|
|
wire_ = &Wire; // NOLINT(cppcoreguidelines-prefer-member-initializer)
|
|
|
|
#endif
|
|
|
|
|
|
|
|
wire_->begin(sda_pin_, scl_pin_);
|
|
|
|
wire_->setClock(frequency_);
|
|
|
|
initialized_ = true;
|
|
|
|
}
|
|
|
|
void ArduinoI2CBus::dump_config() {
|
|
|
|
ESP_LOGCONFIG(TAG, "I2C Bus:");
|
|
|
|
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_);
|
2021-10-01 12:53:37 +02:00
|
|
|
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;
|
|
|
|
}
|
2021-09-20 11:47:51 +02:00
|
|
|
if (this->scan_) {
|
|
|
|
ESP_LOGI(TAG, "Scanning i2c bus for active devices...");
|
|
|
|
uint8_t found = 0;
|
2021-09-22 19:08:42 +02:00
|
|
|
for (uint8_t address = 8; address < 120; address++) {
|
|
|
|
auto err = writev(address, nullptr, 0);
|
2021-09-20 11:47:51 +02:00
|
|
|
if (err == ERROR_OK) {
|
|
|
|
ESP_LOGI(TAG, "Found i2c device at address 0x%02X", address);
|
|
|
|
found++;
|
|
|
|
} else if (err == ERROR_UNKNOWN) {
|
|
|
|
ESP_LOGI(TAG, "Unknown error at address 0x%02X", address);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (found == 0) {
|
|
|
|
ESP_LOGI(TAG, "Found no i2c devices!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
|
2021-10-06 00:57:23 +02:00
|
|
|
// logging is only enabled with vv level, if warnings are shown the caller
|
|
|
|
// should log them
|
|
|
|
if (!initialized_) {
|
|
|
|
ESP_LOGVV(TAG, "i2c bus not initialized!");
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_NOT_INITIALIZED;
|
2021-10-06 00:57:23 +02:00
|
|
|
}
|
2021-09-20 11:47:51 +02:00
|
|
|
size_t to_request = 0;
|
|
|
|
for (size_t i = 0; i < cnt; i++)
|
|
|
|
to_request += buffers[i].len;
|
|
|
|
size_t ret = wire_->requestFrom((int) address, (int) to_request, 1);
|
|
|
|
if (ret != to_request) {
|
2021-10-06 00:57:23 +02:00
|
|
|
ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret);
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_TIMEOUT;
|
|
|
|
}
|
2021-10-06 00:57:23 +02:00
|
|
|
|
2021-09-20 11:47:51 +02:00
|
|
|
for (size_t i = 0; i < cnt; i++) {
|
|
|
|
const auto &buf = buffers[i];
|
|
|
|
for (size_t j = 0; j < buf.len; j++)
|
|
|
|
buf.data[j] = wire_->read();
|
|
|
|
}
|
2021-10-06 00:57:23 +02:00
|
|
|
|
|
|
|
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
|
|
|
char debug_buf[4];
|
|
|
|
std::string debug_hex;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < cnt; i++) {
|
|
|
|
const auto &buf = buffers[i];
|
|
|
|
for (size_t j = 0; j < buf.len; j++) {
|
|
|
|
snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]);
|
|
|
|
debug_hex += debug_buf;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str());
|
|
|
|
#endif
|
|
|
|
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_OK;
|
|
|
|
}
|
|
|
|
ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) {
|
2021-10-06 00:57:23 +02:00
|
|
|
// logging is only enabled with vv level, if warnings are shown the caller
|
|
|
|
// should log them
|
|
|
|
if (!initialized_) {
|
|
|
|
ESP_LOGVV(TAG, "i2c bus not initialized!");
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_NOT_INITIALIZED;
|
2021-10-06 00:57:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
|
|
|
char debug_buf[4];
|
|
|
|
std::string debug_hex;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < cnt; i++) {
|
|
|
|
const auto &buf = buffers[i];
|
|
|
|
for (size_t j = 0; j < buf.len; j++) {
|
|
|
|
snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]);
|
|
|
|
debug_hex += debug_buf;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str());
|
|
|
|
#endif
|
2021-09-20 11:47:51 +02:00
|
|
|
|
|
|
|
wire_->beginTransmission(address);
|
2021-10-06 00:57:23 +02:00
|
|
|
size_t written = 0;
|
2021-09-20 11:47:51 +02:00
|
|
|
for (size_t i = 0; i < cnt; i++) {
|
|
|
|
const auto &buf = buffers[i];
|
|
|
|
if (buf.len == 0)
|
|
|
|
continue;
|
|
|
|
size_t ret = wire_->write(buf.data, buf.len);
|
2021-10-06 00:57:23 +02:00
|
|
|
written += ret;
|
2021-09-20 11:47:51 +02:00
|
|
|
if (ret != buf.len) {
|
2021-10-06 00:57:23 +02:00
|
|
|
ESP_LOGVV(TAG, "TX failed at %u", written);
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_UNKNOWN;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
uint8_t status = wire_->endTransmission(true);
|
|
|
|
if (status == 0) {
|
|
|
|
return ERROR_OK;
|
|
|
|
} else if (status == 1) {
|
|
|
|
// transmit buffer not large enough
|
2021-10-06 00:57:23 +02:00
|
|
|
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_UNKNOWN;
|
|
|
|
} else if (status == 2 || status == 3) {
|
2021-10-06 00:57:23 +02:00
|
|
|
ESP_LOGVV(TAG, "TX failed: not acknowledged");
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_NOT_ACKNOWLEDGED;
|
|
|
|
}
|
2021-10-06 00:57:23 +02:00
|
|
|
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
|
2021-09-20 11:47:51 +02:00
|
|
|
return ERROR_UNKNOWN;
|
|
|
|
}
|
|
|
|
|
2021-10-01 12:53:37 +02:00
|
|
|
/// 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
|
2021-09-24 18:02:28 +02:00
|
|
|
void ArduinoI2CBus::recover_() {
|
2021-10-01 12:53:37 +02:00
|
|
|
ESP_LOGI(TAG, "Performing I2C bus recovery");
|
|
|
|
|
2021-10-04 12:33:25 +02:00
|
|
|
// 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);
|
2021-10-01 12:53:37 +02:00
|
|
|
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
|
2021-10-04 12:33:25 +02:00
|
|
|
// nine clock pulses here, just in case SDA is stuck. Actual checks on
|
|
|
|
// the SDA line status will be done after the clock pulses.
|
2021-09-23 20:11:40 +02:00
|
|
|
|
2021-10-04 12:33:25 +02:00
|
|
|
// Make sure that switching to output mode will make SCL low, just in
|
|
|
|
// case other code has setup the pin for a HIGH signal.
|
2021-10-01 12:53:37 +02:00
|
|
|
digitalWrite(scl_pin_, LOW); // NOLINT
|
2021-09-23 20:11:40 +02:00
|
|
|
|
2021-10-01 12:53:37 +02:00
|
|
|
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
|
2021-09-23 20:11:40 +02:00
|
|
|
delayMicroseconds(half_period_usec);
|
2021-10-01 12:53:37 +02:00
|
|
|
|
|
|
|
// Release output and activate pull up resistor to make the signal HIGH.
|
|
|
|
pinMode(scl_pin_, INPUT); // NOLINT
|
|
|
|
pinMode(scl_pin_, INPUT_PULLUP); // NOLINT
|
2021-09-23 20:11:40 +02:00
|
|
|
delayMicroseconds(half_period_usec);
|
2021-10-01 12:53:37 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-04 12:33:25 +02:00
|
|
|
// 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
|
|
|
|
|
2021-10-01 12:53:37 +02:00
|
|
|
// 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;
|
2021-09-23 20:11:40 +02:00
|
|
|
}
|
|
|
|
|
2021-10-01 12:53:37 +02:00
|
|
|
// 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.
|
2021-10-04 12:33:25 +02:00
|
|
|
delayMicroseconds(half_period_usec);
|
2021-10-01 12:53:37 +02:00
|
|
|
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.
|
2021-10-04 12:33:25 +02:00
|
|
|
delayMicroseconds(half_period_usec);
|
2021-10-01 12:53:37 +02:00
|
|
|
pinMode(sda_pin_, INPUT); // NOLINT
|
|
|
|
pinMode(sda_pin_, INPUT_PULLUP); // NOLINT
|
|
|
|
|
|
|
|
recovery_result_ = RECOVERY_COMPLETED;
|
2021-09-23 20:11:40 +02:00
|
|
|
}
|
2021-09-20 11:47:51 +02:00
|
|
|
} // namespace i2c
|
|
|
|
} // namespace esphome
|
|
|
|
|
|
|
|
#endif // USE_ESP_IDF
|