[tca9548a] Allow separate I²C bus frequencies on each multiplexer port (#10422)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
DAVe3283
2026-10-10 10:58:09 -10:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] J. Nick Koston J. Nick Koston
parent ebbe3860ec
commit 6559ea470b
14 changed files with 380 additions and 84 deletions
+10
View File
@@ -4,6 +4,7 @@
#include <utility>
#include <vector>
#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.
+53 -40
View File
@@ -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<int>(sda_pin_), static_cast<int>(scl_pin_));
this->wire_->begin(static_cast<int>(this->sda_pin_), static_cast<int>(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
+9 -5
View File
@@ -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_();
+72 -24
View File
@@ -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<gpio_num_t>(scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(sda_pin_);
const auto scl_pin = static_cast<gpio_num_t>(this->scl_pin_);
const auto sda_pin = static_cast<gpio_num_t>(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
+15
View File
@@ -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<FrequencyDevice, I2C_PORT_FREQUENCY_COUNT + 1> devices_;
#endif
i2c_master_bus_handle_t bus_{};
i2c_port_t port_{};
uint8_t sda_pin_{};
+5
View File
@@ -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_; }
+26 -7
View File
@@ -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
+7
View File
@@ -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
+52 -1
View File
@@ -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)))
+97 -5
View File
@@ -1,4 +1,6 @@
#include "tca9548a.h"
#include <algorithm>
#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."));
}
}
+26 -2
View File
@@ -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
+1
View File
@@ -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
+4
View File
@@ -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
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
tca9548a: !include common.yaml