#pragma once #include #include #include #include #include #include "esphome/core/hal.h" namespace esphome::gpio_expander { /// @brief A class to cache the read state of a GPIO expander. /// This class caches reads between GPIO Pins which are on the same bank. /// This means that for reading whole Port (ex. 8 pins) component needs only one /// I2C/SPI read per main loop call. It assumes that one bit in byte identifies one GPIO pin. /// /// Template parameters: /// T - Type which represents internal bank register. Could be uint8_t or uint16_t. /// Choose based on how your I/O expander reads pins: /// * uint8_t: For chips that read banks separately (8 pins at a time) /// Examples: MCP23017 (2x8-bit banks), TCA9555 (2x8-bit banks) /// * uint16_t: For chips that read all pins at once (up to 16 pins) /// Examples: PCF8574/8575 (8/16 pins), PCA9554/9555 (8/16 pins) /// N - Total number of pins (maximum 65535) /// P - Type for pin number parameters (automatically selected based on N: /// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified /// if needed (e.g., for components like SN74HC165 with >256 pins) template 256), uint16_t, uint8_t>::type> class CachedGpioExpander { public: /// @brief Read the state of the given pin. /// By default, each read invalidates the pin's cache entry so the next read /// of the same pin triggers a fresh hardware read. When invalidate_on_read /// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_(). /// @param pin Pin number to read /// @return Pin state bool digital_read(P pin) { const P bank = pin / BANK_SIZE; const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { if (this->invalidate_on_read_) { // Invalidate pin so next read triggers hardware read this->read_cache_valid_[bank] &= ~pin_mask; } } else { // Read whole bank from hardware if (!this->digital_read_hw(pin)) return false; // Mark bank cache as valid except the pin that is being returned now // (when not invalidating on read, mark all pins including this one as valid) this->read_cache_valid_[bank] = std::numeric_limits::max() & ~(this->invalidate_on_read_ ? pin_mask : 0); } return this->digital_read_cache(pin); } void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); } protected: /// @brief Read GPIO bank from hardware into internal state /// @param pin Pin number (used to determine which bank to read) /// @return true if read succeeded, false on communication error /// @note This does NOT return the pin state. It returns whether the read operation succeeded. /// The actual pin state should be returned by digital_read_cache(). virtual bool digital_read_hw(P pin) = 0; /// @brief Get cached pin value from internal state /// @param pin Pin number to read /// @return Pin state (true = HIGH, false = LOW) virtual bool digital_read_cache(P pin) = 0; /// @brief Write GPIO state to hardware /// @param pin Pin number to write /// @param value Pin state to write (true = HIGH, false = LOW) virtual void digital_write_hw(P pin, bool value) = 0; /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } /// @brief Control whether digital_read() invalidates the pin's cache entry after reading. /// When enabled (default), each read self-invalidates so the next read triggers a hardware read. /// When disabled, cache stays valid until reset_pin_cache_() is explicitly called. void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; } static constexpr uint16_t BITS_PER_BYTE = 8; static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; static constexpr size_t BANKS = N / BANK_SIZE; static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); T read_cache_valid_[BANKS]{0}; bool invalidate_on_read_{true}; }; } // namespace esphome::gpio_expander