diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 0c5e80bfe5..be38317497 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -4,6 +4,7 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" namespace esphome::i2c { @@ -53,6 +54,15 @@ class I2CBus { return this->write_readv(address, buffer, len, nullptr, 0); } +#ifdef I2C_PORT_FREQUENCY_COUNT + /// @brief Switch the bus frequency for the transactions that follow; an external bus + /// that does not override this reports an error and keeps its frequency + virtual ErrorCode switch_frequency(uint32_t frequency) { return ERROR_UNKNOWN; } + + /// @brief The frequency the bus runs at + virtual uint32_t get_frequency() const { return 0; } +#endif + protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 95aee2bddb..215cc1364f 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -15,7 +15,7 @@ ESPHOME_LOG_TAG(TAG, "i2c"); static constexpr size_t I2C_MAX_LOG_BYTES = 32; void ArduinoI2CBus::setup() { - recover_(); + this->recover_(); #if defined(USE_ESP8266) wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory) @@ -25,9 +25,9 @@ void ArduinoI2CBus::setup() { // RP2040 datasheet Table 2 (section 1.4.3): https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf // RP2350 datasheet Table 7 (section 9.4): https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf if ((this->sda_pin_ / 2) % 2 == 0) { - wire_ = &Wire; + this->wire_ = &Wire; } else { - wire_ = &Wire1; + this->wire_ = &Wire1; } #endif @@ -42,22 +42,22 @@ void ArduinoI2CBus::setup() { void ArduinoI2CBus::set_pins_and_clock_() { #ifdef USE_RP2 - wire_->setSDA(this->sda_pin_); - wire_->setSCL(this->scl_pin_); - wire_->begin(); + this->wire_->setSDA(this->sda_pin_); + this->wire_->setSCL(this->scl_pin_); + this->wire_->begin(); #else - wire_->begin(static_cast(sda_pin_), static_cast(scl_pin_)); + this->wire_->begin(static_cast(this->sda_pin_), static_cast(this->scl_pin_)); #endif - if (timeout_ > 0) { // if timeout specified in yaml + if (this->timeout_ > 0) { // if timeout specified in yaml #if defined(USE_ESP8266) // https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h - wire_->setClockStretchLimit(timeout_); // unit: us + this->wire_->setClockStretchLimit(this->timeout_); // unit: us #elif defined(USE_RP2) // https://github.com/earlephilhower/ArduinoCore-API/blob/e37df85425e0ac020bfad226d927f9b00d2e0fb7/api/Stream.h - wire_->setTimeout(timeout_ / 1000); // unit: ms + this->wire_->setTimeout(this->timeout_ / 1000); // unit: ms #endif } - wire_->setClock(frequency_); + this->wire_->setClock(this->frequency_); } void ArduinoI2CBus::dump_config() { @@ -67,7 +67,7 @@ void ArduinoI2CBus::dump_config() { " SCL Pin: GPIO%u\n" " Frequency: %u Hz", this->sda_pin_, this->scl_pin_, this->frequency_); - if (timeout_ > 0) { + if (this->timeout_ > 0) { #if defined(USE_ESP8266) ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_); #elif defined(USE_RP2) @@ -87,10 +87,10 @@ void ArduinoI2CBus::dump_config() { } if (this->scan_) { ESP_LOGI(TAG, "Results from bus scan:"); - if (scan_results_.empty()) { + if (this->scan_results_.empty()) { ESP_LOGI(TAG, "Found no devices"); } else { - for (const auto &s : scan_results_) { + for (const auto &s : this->scan_results_) { if (s.second) { ESP_LOGI(TAG, "Found device at address 0x%02X", s.first); } else { @@ -106,7 +106,7 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe #if defined(USE_ESP8266) this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances #endif - if (!initialized_) { + if (!this->initialized_) { ESP_LOGD(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } @@ -118,22 +118,22 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe uint8_t status = 0; if (write_count != 0 || read_count == 0) { - wire_->beginTransmission(address); - size_t ret = wire_->write(write_buffer, write_count); + this->wire_->beginTransmission(address); + size_t ret = this->wire_->write(write_buffer, write_count); if (ret != write_count) { ESP_LOGV(TAG, "TX failed"); return ERROR_UNKNOWN; } - status = wire_->endTransmission(read_count == 0); + status = this->wire_->endTransmission(read_count == 0); } if (status == 0 && read_count != 0) { - size_t ret2 = wire_->requestFrom(address, read_count, true); + size_t ret2 = this->wire_->requestFrom(address, read_count, true); if (ret2 != read_count) { ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2); return ERROR_TIMEOUT; } for (size_t j = 0; j != read_count; j++) - read_buffer[j] = wire_->read(); + read_buffer[j] = this->wire_->read(); } // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266 if (status == 0) @@ -154,6 +154,19 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe return ERROR_UNKNOWN; } +#ifdef I2C_PORT_FREQUENCY_COUNT +ErrorCode ArduinoI2CBus::switch_frequency(uint32_t frequency) { + if (this->frequency_ == frequency) { + return ERROR_OK; + } + this->frequency_ = frequency; + if (this->initialized_) { + this->wire_->setClock(this->frequency_); + } + return ERROR_OK; +} +#endif + /// 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 @@ -167,15 +180,15 @@ void ArduinoI2CBus::recover_() { const auto half_period_usec = 1000000 / 100000 / 2; // Activate input and pull up resistor for the SCL pin. - pinMode(scl_pin_, INPUT_PULLUP); // NOLINT + pinMode(this->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); - if (digitalRead(scl_pin_) == LOW) { // NOLINT + if (digitalRead(this->scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus"); - recovery_result_ = RECOVERY_FAILED_SCL_LOW; + this->recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } @@ -189,18 +202,18 @@ void ArduinoI2CBus::recover_() { // Make sure that switching to output mode will make SCL low, just in // case other code has setup the pin for a HIGH signal. - digitalWrite(scl_pin_, LOW); // NOLINT + digitalWrite(this->scl_pin_, LOW); // NOLINT 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 + pinMode(this->scl_pin_, INPUT); // NOLINT + pinMode(this->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 + pinMode(this->scl_pin_, INPUT); // NOLINT + pinMode(this->scl_pin_, INPUT_PULLUP); // NOLINT delayMicroseconds(half_period_usec); // When SCL is kept LOW at this point, we might be looking at a device @@ -211,28 +224,28 @@ void ArduinoI2CBus::recover_() { // should recover in a few ms or less else not likely to recovery at // all. auto wait = 250; - while (wait-- && digitalRead(scl_pin_) == LOW) { // NOLINT + while (wait-- && digitalRead(this->scl_pin_) == LOW) { // NOLINT App.feed_wdt(); delayMicroseconds(half_period_usec * 2); } - if (digitalRead(scl_pin_) == LOW) { // NOLINT + if (digitalRead(this->scl_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); - recovery_result_ = RECOVERY_FAILED_SCL_LOW; + this->recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } } // 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 + pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT + digitalWrite(this->sda_pin_, LOW); // NOLINT // By now, any stuck device ought to have sent all remaining bits of its // transaction, meaning that it should have freed up the SDA line, resulting // in SDA being pulled up. - if (digitalRead(sda_pin_) == LOW) { // NOLINT + if (digitalRead(this->sda_pin_) == LOW) { // NOLINT ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); - recovery_result_ = RECOVERY_FAILED_SDA_LOW; + this->recovery_result_ = RECOVERY_FAILED_SDA_LOW; return; } @@ -248,8 +261,8 @@ void ArduinoI2CBus::recover_() { // SCL and SDA are already high at this point, so we can generate a START // condition by making the SDA signal LOW. delayMicroseconds(half_period_usec); - pinMode(sda_pin_, INPUT); // NOLINT - pinMode(sda_pin_, OUTPUT); // NOLINT + pinMode(this->sda_pin_, INPUT); // NOLINT + pinMode(this->sda_pin_, OUTPUT); // NOLINT // From the specification: // "A START condition immediately followed by a STOP condition (void @@ -258,10 +271,10 @@ void ArduinoI2CBus::recover_() { // Finally, we'll bring the I2C bus into a starting state by generating // a STOP condition. delayMicroseconds(half_period_usec); - pinMode(sda_pin_, INPUT); // NOLINT - pinMode(sda_pin_, INPUT_PULLUP); // NOLINT + pinMode(this->sda_pin_, INPUT); // NOLINT + pinMode(this->sda_pin_, INPUT_PULLUP); // NOLINT - recovery_result_ = RECOVERY_COMPLETED; + this->recovery_result_ = RECOVERY_COMPLETED; } } // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 71e91e770b..9feea9f7ff 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -22,13 +22,17 @@ class ArduinoI2CBus final : public InternalI2CBus, public Component { size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } - void set_scan(bool scan) { scan_ = scan; } - void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } - void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } - void set_frequency(uint32_t frequency) { frequency_ = frequency; } - void set_timeout(uint32_t timeout) { timeout_ = timeout; } + void set_scan(bool scan) { this->scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } int get_port() const override { return 0; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } +#ifdef I2C_PORT_FREQUENCY_COUNT + ErrorCode switch_frequency(uint32_t frequency) override; + uint32_t get_frequency() const override { return this->frequency_; } +#endif private: void recover_(); diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index b13b525abb..931bcfa088 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -32,8 +32,8 @@ void IDFI2CBus::setup() { i2c_master_bus_config_t bus_conf{}; memset(&bus_conf, 0, sizeof(bus_conf)); - bus_conf.sda_io_num = gpio_num_t(sda_pin_); - bus_conf.scl_io_num = gpio_num_t(scl_pin_); + bus_conf.sda_io_num = gpio_num_t(this->sda_pin_); + bus_conf.scl_io_num = gpio_num_t(this->scl_pin_); bus_conf.glitch_ignore_cnt = 7; #if SOC_LP_I2C_SUPPORTED if (this->lp_mode_) { @@ -59,7 +59,7 @@ void IDFI2CBus::setup() { } #endif bus_conf.i2c_port = this->port_; - bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_; + bus_conf.flags.enable_internal_pullup = this->sda_pullup_enabled_ || this->scl_pullup_enabled_; esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_); if (err != ESP_OK) { ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err)); @@ -67,18 +67,21 @@ void IDFI2CBus::setup() { return; } - i2c_device_config_t dev_conf{}; - memset(&dev_conf, 0, sizeof(dev_conf)); - dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7; - dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED; - dev_conf.scl_speed_hz = this->frequency_; - dev_conf.scl_wait_us = this->timeout_; - err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err)); + this->dev_ = this->add_device_(this->frequency_); + if (this->dev_ == nullptr) { this->mark_failed(); return; } +#ifdef I2C_PORT_FREQUENCY_COUNT + this->add_frequency(this->frequency_); + for (auto &device : this->devices_) { + device.dev = device.frequency == this->frequency_ ? this->dev_ : this->add_device_(device.frequency); + if (device.dev == nullptr) { + this->mark_failed(); + return; + } + } +#endif this->initialized_ = true; @@ -88,6 +91,21 @@ void IDFI2CBus::setup() { } } +i2c_master_dev_handle_t IDFI2CBus::add_device_(uint32_t frequency) { + i2c_device_config_t dev_conf{}; + dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7; + dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED; + dev_conf.scl_speed_hz = frequency; + dev_conf.scl_wait_us = this->timeout_; + i2c_master_dev_handle_t dev{}; + const esp_err_t err = i2c_master_bus_add_device(this->bus_, &dev_conf, &dev); + if (err != ESP_OK) { + ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err)); + return nullptr; + } + return dev; +} + void IDFI2CBus::dump_config() { ESP_LOGCONFIG(TAG, "I2C Bus:"); ESP_LOGCONFIG(TAG, @@ -95,7 +113,7 @@ void IDFI2CBus::dump_config() { " SCL Pin: GPIO%u\n" " Frequency: %" PRIu32 " Hz", this->sda_pin_, this->scl_pin_, this->frequency_); - if (timeout_ > 0) { + if (this->timeout_ > 0) { ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "us", this->timeout_); } switch (this->recovery_result_) { @@ -111,10 +129,10 @@ void IDFI2CBus::dump_config() { } if (this->scan_) { ESP_LOGCONFIG(TAG, "Results from bus scan:"); - if (scan_results_.empty()) { + if (this->scan_results_.empty()) { ESP_LOGCONFIG(TAG, "Found no devices"); } else { - for (const auto &s : scan_results_) { + for (const auto &s : this->scan_results_) { if (s.second) { ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first); } else { @@ -129,7 +147,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s size_t read_count) { // logging is only enabled with v level, if warnings are shown the caller // should log them - if (!initialized_) { + if (!this->initialized_) { ESP_LOGW(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } @@ -198,14 +216,44 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s return ERROR_OK; } +#ifdef I2C_PORT_FREQUENCY_COUNT +void IDFI2CBus::add_frequency(uint32_t frequency) { + for (const auto &device : this->devices_) { + if (device.frequency == frequency) { + return; + } + } + this->devices_.push_back({frequency, nullptr}); +} + +ErrorCode IDFI2CBus::switch_frequency(uint32_t frequency) { + if (this->frequency_ == frequency) { + return ERROR_OK; + } + if (!this->initialized_) { + this->frequency_ = frequency; + return ERROR_OK; + } + // Every handle exists since setup(); a frequency nobody registered has none + for (const auto &device : this->devices_) { + if (device.frequency == frequency) { + this->dev_ = device.dev; + this->frequency_ = frequency; + return ERROR_OK; + } + } + return ERROR_UNKNOWN; +} +#endif + /// 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 IDFI2CBus::recover_() { ESP_LOGI(TAG, "Performing bus recovery"); - const auto scl_pin = static_cast(scl_pin_); - const auto sda_pin = static_cast(sda_pin_); + const auto scl_pin = static_cast(this->scl_pin_); + const auto sda_pin = static_cast(this->sda_pin_); // For the upcoming operations, target for a 60kHz toggle frequency. // 1000kHz is the maximum frequency for I2C running in standard-mode, @@ -218,7 +266,7 @@ void IDFI2CBus::recover_() { // Configure SCL pin for open drain input/output, with a pull up resistor. gpio_set_level(scl_pin, 1); gpio_config_t scl_config{}; - scl_config.pin_bit_mask = 1ULL << scl_pin_; + scl_config.pin_bit_mask = 1ULL << this->scl_pin_; scl_config.mode = GPIO_MODE_INPUT_OUTPUT_OD; scl_config.pull_up_en = GPIO_PULLUP_ENABLE; scl_config.pull_down_en = GPIO_PULLDOWN_DISABLE; @@ -228,7 +276,7 @@ void IDFI2CBus::recover_() { // Configure SDA pin for open drain input/output, with a pull up resistor. gpio_set_level(sda_pin, 1); gpio_config_t sda_conf{}; - sda_conf.pin_bit_mask = 1ULL << sda_pin_; + sda_conf.pin_bit_mask = 1ULL << this->sda_pin_; sda_conf.mode = GPIO_MODE_INPUT_OUTPUT_OD; sda_conf.pull_up_en = GPIO_PULLUP_ENABLE; sda_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; @@ -240,7 +288,7 @@ void IDFI2CBus::recover_() { delayMicroseconds(half_period_usec); if (gpio_get_level(scl_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SCL is held LOW on the bus"); - recovery_result_ = RECOVERY_FAILED_SCL_LOW; + this->recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } @@ -271,7 +319,7 @@ void IDFI2CBus::recover_() { } if (gpio_get_level(scl_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SCL is held LOW during clock pulse cycle"); - recovery_result_ = RECOVERY_FAILED_SCL_LOW; + this->recovery_result_ = RECOVERY_FAILED_SCL_LOW; return; } } @@ -281,7 +329,7 @@ void IDFI2CBus::recover_() { // in SDA being pulled up. if (gpio_get_level(sda_pin) == 0) { ESP_LOGE(TAG, "Recovery failed: SDA is held LOW after clock pulse cycle"); - recovery_result_ = RECOVERY_FAILED_SDA_LOW; + this->recovery_result_ = RECOVERY_FAILED_SDA_LOW; return; } @@ -308,7 +356,7 @@ void IDFI2CBus::recover_() { delayMicroseconds(half_period_usec); gpio_set_level(sda_pin, 1); - recovery_result_ = RECOVERY_COMPLETED; + this->recovery_result_ = RECOVERY_COMPLETED; } } // namespace esphome::i2c diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 92e96f649b..b9d8824d25 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -34,13 +34,28 @@ class IDFI2CBus final : public InternalI2CBus, public Component { #endif int get_port() const override { return this->port_; } +#ifdef I2C_PORT_FREQUENCY_COUNT + /// Register a frequency used on the bus so setup() creates its device handle + void add_frequency(uint32_t frequency); + ErrorCode switch_frequency(uint32_t frequency) override; + uint32_t get_frequency() const override { return this->frequency_; } +#endif private: void recover_(); + i2c_master_dev_handle_t add_device_(uint32_t frequency); RecoveryCode recovery_result_{}; protected: i2c_master_dev_handle_t dev_{}; +#ifdef I2C_PORT_FREQUENCY_COUNT + // One handle per frequency used on the bus, registered at code generation + struct FrequencyDevice { + uint32_t frequency; + i2c_master_dev_handle_t dev; + }; + StaticVector devices_; +#endif i2c_master_bus_handle_t bus_{}; i2c_port_t port_{}; uint8_t sda_pin_{}; diff --git a/esphome/components/i2c/i2c_bus_host.h b/esphome/components/i2c/i2c_bus_host.h index 8064016a52..086baab26c 100644 --- a/esphome/components/i2c/i2c_bus_host.h +++ b/esphome/components/i2c/i2c_bus_host.h @@ -21,7 +21,12 @@ class HostI2CBus final : public I2CBus, public Component { void set_device(const std::string &device) { this->device_ = device; } void set_scan(bool scan) { this->scan_ = scan; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } +#ifdef I2C_PORT_FREQUENCY_COUNT + // The kernel driver owns the clock, so switch_frequency keeps the base default + uint32_t get_frequency() const override { return this->frequency_; } +#endif const std::string &get_device() const { return this->device_; } diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp index 4145cd5859..ee6e66893e 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.cpp +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -120,15 +120,34 @@ ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer return ERROR_OK; } -void ZephyrI2CBus::set_frequency(uint32_t frequency) { - this->dev_config_ &= ~I2C_SPEED_MASK; - if (frequency >= 400000) { - this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST); - } else { - this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD); - } +static uint32_t speed_config(uint32_t dev_config, uint32_t frequency) { + const uint32_t speed = frequency >= 400000 ? I2C_SPEED_FAST : I2C_SPEED_STANDARD; + return (dev_config & ~I2C_SPEED_MASK) | I2C_SPEED_SET(speed); } +void ZephyrI2CBus::set_frequency(uint32_t frequency) { +#ifdef I2C_PORT_FREQUENCY_COUNT + this->frequency_ = frequency; +#endif + this->dev_config_ = speed_config(this->dev_config_, frequency); +} + +#ifdef I2C_PORT_FREQUENCY_COUNT +ErrorCode ZephyrI2CBus::switch_frequency(uint32_t frequency) { + if (this->frequency_ == frequency) { + return ERROR_OK; + } + const uint32_t dev_config = speed_config(this->dev_config_, frequency); + // Until ready, setup() applies dev_config_ + if (this->is_ready() && i2c_configure(this->i2c_dev_, dev_config) < 0) { + return ERROR_UNKNOWN; + } + this->dev_config_ = dev_config; + this->frequency_ = frequency; + return ERROR_OK; +} +#endif + } // namespace esphome::i2c #endif diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h index 3ada1e0a0f..413d78d6a3 100644 --- a/esphome/components/i2c/i2c_bus_zephyr.h +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -22,6 +22,10 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component { void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } void set_frequency(uint32_t frequency); +#ifdef I2C_PORT_FREQUENCY_COUNT + ErrorCode switch_frequency(uint32_t frequency) override; + uint32_t get_frequency() const override { return this->frequency_; } +#endif int get_port() const override { return 0; } @@ -31,6 +35,9 @@ class ZephyrI2CBus final : public InternalI2CBus, public Component { uint8_t sda_pin_{}; uint8_t scl_pin_{}; uint32_t dev_config_{}; +#ifdef I2C_PORT_FREQUENCY_COUNT + uint32_t frequency_{}; +#endif }; } // namespace esphome::i2c diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py index a1945e97c5..368e34f325 100644 --- a/esphome/components/tca9548a/__init__.py +++ b/esphome/components/tca9548a/__init__.py @@ -1,7 +1,14 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_CHANNELS, CONF_ID +from esphome.const import ( + CONF_CHANNEL, + CONF_CHANNELS, + CONF_FREQUENCY, + CONF_I2C_ID, + CONF_ID, +) +from esphome.core import CORE, ID from esphome.types import ConfigType CODEOWNERS = ["@andreashergert1984"] @@ -16,14 +23,51 @@ TCA9548AChannel = tca9548a_ns.class_("TCA9548AChannel", i2c.I2CBus) MULTI_CONF = True CONF_BUS_ID = "bus_id" +# Sizes the root bus's frequency table; only emitted when a frequency is set +_request_port_frequency_slot = cg.slot_counter("I2C_PORT_FREQUENCY_COUNT") + + +def _root_bus_id(bus_id: ID) -> ID: + """The hardware bus behind a port, through any multiplexers in between.""" + parents = { + str(chan[CONF_BUS_ID]): mux[CONF_I2C_ID] + for mux in CORE.config.get(DOMAIN, []) + for chan in mux.get(CONF_CHANNELS, []) + } + while (parent := parents.get(str(bus_id))) is not None: + bus_id = parent + return bus_id + + +def _declared_bus_type(bus_id: ID) -> cg.MockObjClass | None: + """The class the i2c component declared for a bus, or None for an external bus.""" + for bus in CORE.config.get(i2c.DOMAIN, []): + if bus[CONF_ID].id == bus_id.id: + return bus[CONF_ID].type + return None + + +async def _add_frequency(root_bus_id: ID, frequency: int) -> None: + """Count the frequency against the root bus; the built-in ESP32 bus also + creates its device handle in setup().""" + _request_port_frequency_slot(str(root_bus_id)) + if _declared_bus_type(root_bus_id) is i2c.IDFI2CBus: + root = await cg.get_variable(root_bus_id) + cg.add(root.add_frequency(frequency)) + + +_FREQUENCY = cv.All(cv.frequency, cv.Range(min=0, min_included=False)) + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(TCA9548AComponent), + cv.Optional(CONF_FREQUENCY): _FREQUENCY, cv.Optional(CONF_CHANNELS, default=[]): cv.ensure_list( { cv.Required(CONF_BUS_ID): cv.declare_id(TCA9548AChannel), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), + cv.Optional(CONF_FREQUENCY): _FREQUENCY, } ), } @@ -37,8 +81,15 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) + root_bus_id = _root_bus_id(config[CONF_I2C_ID]) + if (frequency := config.get(CONF_FREQUENCY)) is not None: + await _add_frequency(root_bus_id, int(frequency)) + cg.add(var.set_frequency(int(frequency))) for conf in config[CONF_CHANNELS]: chan = cg.new_Pvariable(conf[CONF_BUS_ID]) cg.add(chan.set_parent(var)) cg.add(chan.set_channel(conf[CONF_CHANNEL])) + if (frequency := conf.get(CONF_FREQUENCY)) is not None: + await _add_frequency(root_bus_id, int(frequency)) + cg.add(chan.set_frequency(int(frequency))) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index f10578c97c..ac33ca8ded 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -1,4 +1,6 @@ #include "tca9548a.h" + +#include #include "esphome/core/log.h" namespace esphome::tca9548a { @@ -7,20 +9,111 @@ ESPHOME_LOG_TAG(TAG, "tca9548a"); i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, size_t read_count) { - auto err = this->parent_->switch_to_channel(channel_); +#ifdef I2C_PORT_FREQUENCY_COUNT + const uint32_t select_frequency = this->select_frequency_(); + if (select_frequency != 0) { + return this->write_readv_at_port_frequency_(select_frequency, address, write_buffer, write_count, read_buffer, + read_count); + } +#endif + auto err = this->parent_->switch_to_channel(this->channel_); if (err != i2c::ERROR_OK) return err; err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); this->parent_->disable_all_channels(); return err; } + +#ifdef I2C_PORT_FREQUENCY_COUNT +// The multiplexer's own frequency, else the slower of the bus and the port +uint32_t TCA9548AChannel::select_frequency_() const { + if (this->parent_->frequency_ != 0) + return this->parent_->frequency_; + if (this->frequency_ == 0) + return 0; + // A bus that does not report its frequency gets the port's, and fails the switch + const uint32_t bus_frequency = this->parent_->bus_->get_frequency(); + return bus_frequency != 0 ? std::min(this->frequency_, bus_frequency) : this->frequency_; +} + +// The port frequency, else the multiplexer's +uint32_t TCA9548AChannel::transfer_frequency_() const { + return this->frequency_ != 0 ? this->frequency_ : this->parent_->frequency_; +} + +i2c::ErrorCode TCA9548AChannel::write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address, + const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { + const uint32_t original_frequency = this->parent_->bus_->get_frequency(); + // A failed switch leaves the bus as it was + auto err = this->switch_bus_(select_frequency); + if (err != i2c::ERROR_OK) + return err; + + err = this->parent_->switch_to_channel(this->channel_); + if (err == i2c::ERROR_OK) { + err = this->transfer_(select_frequency, address, write_buffer, write_count, read_buffer, read_count); + this->parent_->disable_all_channels(); + } + const i2c::ErrorCode restored = this->switch_bus_(original_frequency); + return err != i2c::ERROR_OK ? err : restored; +} + +// Transfer at the port frequency, back to the select frequency for the deselect +i2c::ErrorCode TCA9548AChannel::transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer, + size_t write_count, uint8_t *read_buffer, size_t read_count) { + const uint32_t transfer_frequency = this->transfer_frequency_(); + if (transfer_frequency == select_frequency) { + return this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); + } + auto err = this->switch_bus_(transfer_frequency); + if (err == i2c::ERROR_OK) { + err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); + } + const i2c::ErrorCode restored = this->switch_bus_(select_frequency); + return err != i2c::ERROR_OK ? err : restored; +} + +// Forward to the shared upstream bus +i2c::ErrorCode TCA9548AChannel::switch_frequency(uint32_t frequency) { + return this->parent_->bus_->switch_frequency(frequency); +} + +i2c::ErrorCode TCA9548AChannel::switch_bus_(uint32_t frequency) { + const i2c::ErrorCode err = this->switch_frequency(frequency); + if (err != i2c::ERROR_OK) { + this->parent_->status_set_error(LOG_STR("Failed to switch the bus frequency")); + } + return err; +} + +uint32_t TCA9548AChannel::get_frequency() const { return this->parent_->bus_->get_frequency(); } +#endif + void TCA9548AComponent::setup() { uint8_t status = 0; - if (this->read(&status, 1) != i2c::ERROR_OK) { +#ifdef I2C_PORT_FREQUENCY_COUNT + const uint32_t original_frequency = this->bus_->get_frequency(); + if (this->frequency_ != 0 && this->bus_->switch_frequency(this->frequency_) != i2c::ERROR_OK) { + this->mark_failed(LOG_STR("Failed to switch the bus frequency")); + return; + } +#endif + const i2c::ErrorCode err = this->read(&status, 1); +#ifdef I2C_PORT_FREQUENCY_COUNT + const bool restored = this->frequency_ == 0 || this->bus_->switch_frequency(original_frequency) == i2c::ERROR_OK; +#endif + if (err != i2c::ERROR_OK) { ESP_LOGE(TAG, "TCA9548A failed"); this->mark_failed(); return; } +#ifdef I2C_PORT_FREQUENCY_COUNT + if (!restored) { + this->mark_failed(LOG_STR("Failed to switch the bus frequency")); + return; + } +#endif ESP_LOGD(TAG, "Channels currently open: %d", status); } void TCA9548AComponent::dump_config() { @@ -32,14 +125,13 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) { if (this->is_failed()) return i2c::ERROR_NOT_INITIALIZED; - uint8_t channel_val = 1 << channel; + const uint8_t channel_val = 1 << channel; return this->write(&channel_val, 1); } void TCA9548AComponent::disable_all_channels() { if (this->write(&TCA9548A_DISABLE_CHANNELS_COMMAND, 1) != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Failed to disable all channels."); - this->status_set_error(); // couldn't disable channels, set error status + this->status_set_error(LOG_STR("Failed to disable all channels.")); } } diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index a98c226d32..5b0d8a0a76 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -10,13 +10,30 @@ static const uint8_t TCA9548A_DISABLE_CHANNELS_COMMAND = 0x00; class TCA9548AComponent; class TCA9548AChannel final : public i2c::I2CBus { public: - void set_channel(uint8_t channel) { channel_ = channel; } - void set_parent(TCA9548AComponent *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { this->channel_ = channel; } + void set_parent(TCA9548AComponent *parent) { this->parent_ = parent; } i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, size_t read_count) override; +#ifdef I2C_PORT_FREQUENCY_COUNT + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + i2c::ErrorCode switch_frequency(uint32_t frequency) override; + uint32_t get_frequency() const override; +#endif + protected: +#ifdef I2C_PORT_FREQUENCY_COUNT + uint32_t select_frequency_() const; + uint32_t transfer_frequency_() const; + i2c::ErrorCode write_readv_at_port_frequency_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer, + size_t write_count, uint8_t *read_buffer, size_t read_count); + i2c::ErrorCode transfer_(uint32_t select_frequency, uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count); + /// Switch the upstream bus, flagging the multiplexer on failure + i2c::ErrorCode switch_bus_(uint32_t frequency); + uint32_t frequency_{0}; +#endif uint8_t channel_; TCA9548AComponent *parent_; }; @@ -30,8 +47,15 @@ class TCA9548AComponent final : public Component, public i2c::I2CDevice { i2c::ErrorCode switch_to_channel(uint8_t channel); void disable_all_channels(); +#ifdef I2C_PORT_FREQUENCY_COUNT + /// Addressing frequency, and the default for the ports + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } +#endif protected: friend class TCA9548AChannel; +#ifdef I2C_PORT_FREQUENCY_COUNT + uint32_t frequency_{0}; +#endif }; } // namespace esphome::tca9548a diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 731c0d2017..d4aca85962 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -137,6 +137,7 @@ #define USE_LVGL_TOUCHSCREEN #define USE_MDNS #define USE_MDNS_STORE_SERVICES +#define I2C_PORT_FREQUENCY_COUNT 1 #define MDNS_SERVICE_COUNT 3 #define USE_MDNS_DYNAMIC_TXT #define MDNS_DYNAMIC_TXT_COUNT 2 diff --git a/tests/components/tca9548a/common.yaml b/tests/components/tca9548a/common.yaml index bfaf1e26fb..eea1b9964d 100644 --- a/tests/components/tca9548a/common.yaml +++ b/tests/components/tca9548a/common.yaml @@ -2,9 +2,13 @@ tca9548a: - id: multiplex0 i2c_id: i2c_bus address: 0x70 + frequency: 400 kHz channels: - bus_id: multiplex0_chan0 channel: 0 + - bus_id: multiplex0_chan1 + channel: 1 + frequency: 1 MHz - id: multiplex1 i2c_id: i2c_bus address: 0x71 diff --git a/tests/components/tca9548a/test.nrf52-adafruit.yaml b/tests/components/tca9548a/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..a664179137 --- /dev/null +++ b/tests/components/tca9548a/test.nrf52-adafruit.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + tca9548a: !include common.yaml