From 8145b7ae93b84c135d74a96a772f6245c2c2eb7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 15 Oct 2026 14:44:25 -1000 Subject: [PATCH] [i2c] Add I2CDevice::update_register for read-modify-write of register bits --- esphome/components/i2c/i2c.cpp | 7 ++ esphome/components/i2c/i2c.h | 5 ++ .../i2c/test_i2c_update_register.cpp | 82 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 tests/components/i2c/test_i2c_update_register.cpp diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 8fc0a503276..6fda6644bf2 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -58,6 +58,13 @@ bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { return true; } +bool I2CDevice::update_register(uint8_t a_register, uint8_t mask, uint8_t value) { + uint8_t data; + if (!this->read_byte(a_register, &data)) + return false; + return this->write_byte(a_register, (data & ~mask) | (value & mask)); +} + bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 00929db6207..1565abef56e 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -266,6 +266,11 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } + /// Read a register, replace the bits in @p mask with those of @p value and write it back. + /// The write always happens; bits of @p value outside @p mask are ignored. + /// @return false if the read or the write failed (nothing is written after a failed read) + bool update_register(uint8_t a_register, uint8_t mask, uint8_t value); + protected: uint8_t address_{0x00}; ///< store the address of the device on the bus I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance diff --git a/tests/components/i2c/test_i2c_update_register.cpp b/tests/components/i2c/test_i2c_update_register.cpp new file mode 100644 index 00000000000..f973a726cad --- /dev/null +++ b/tests/components/i2c/test_i2c_update_register.cpp @@ -0,0 +1,82 @@ +#include + +#include +#include + +#include "esphome/components/i2c/i2c.h" + +namespace esphome::i2c::testing { + +static constexpr uint8_t ADDRESS = 0x40; +static constexpr uint8_t REG = 0x12; + +class RegisterBus final : public I2CBus { + public: + std::array regs{}; + std::vector writes; + bool fail_read{false}; + bool fail_write{false}; + + ErrorCode write_readv(uint8_t address, const uint8_t *out, size_t out_size, uint8_t *in, size_t in_size) override { + EXPECT_EQ(address, ADDRESS); + if (out_size == 0) + return ERROR_TIMEOUT; + const uint8_t reg = out[0]; + if (in_size > 0) { + if (this->fail_read) + return ERROR_TIMEOUT; + for (size_t i = 0; i < in_size; i++) + in[i] = this->regs[reg + i]; + return ERROR_OK; + } + if (this->fail_write) + return ERROR_TIMEOUT; + this->writes.push_back(reg); + for (size_t i = 1; i < out_size; i++) + this->regs[reg + i - 1] = out[i]; + return ERROR_OK; + } +}; + +class UpdateRegisterTest : public ::testing::Test { + protected: + void SetUp() override { + this->device_.set_i2c_bus(&this->bus_); + this->device_.set_i2c_address(ADDRESS); + } + + RegisterBus bus_; + I2CDevice device_; +}; + +TEST_F(UpdateRegisterTest, ReplacesOnlyMaskedBits) { + this->bus_.regs[REG] = 0b1010'0101; + EXPECT_TRUE(this->device_.update_register(REG, 0b0000'1111, 0b0000'0011)); + EXPECT_EQ(this->bus_.regs[REG], 0b1010'0011); +} + +TEST_F(UpdateRegisterTest, IgnoresValueBitsOutsideMask) { + this->bus_.regs[REG] = 0x00; + EXPECT_TRUE(this->device_.update_register(REG, 0x0F, 0xFF)); + EXPECT_EQ(this->bus_.regs[REG], 0x0F); +} + +TEST_F(UpdateRegisterTest, WritesEvenWhenUnchanged) { + this->bus_.regs[REG] = 0x3C; + EXPECT_TRUE(this->device_.update_register(REG, 0x0C, 0x0C)); + EXPECT_EQ(this->bus_.regs[REG], 0x3C); + EXPECT_EQ(this->bus_.writes, std::vector{REG}); +} + +TEST_F(UpdateRegisterTest, FailedReadWritesNothing) { + this->bus_.fail_read = true; + EXPECT_FALSE(this->device_.update_register(REG, 0xFF, 0x01)); + EXPECT_TRUE(this->bus_.writes.empty()); +} + +TEST_F(UpdateRegisterTest, FailedWriteReturnsFalse) { + this->bus_.fail_write = true; + EXPECT_FALSE(this->device_.update_register(REG, 0xFF, 0x01)); +} + +} // namespace esphome::i2c::testing