[zephyr] Implement GPIO interrupts (ISRInternalGPIOPin) (#17077)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com>
This commit is contained in:
Anunay Kulshrestha
2026-07-03 15:13:21 +05:30
committed by GitHub
parent 5fe36a45ed
commit c3233739c5
4 changed files with 118 additions and 6 deletions
@@ -39,7 +39,6 @@ CONFIG_SCHEMA = (
# due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior.
# nrf52 has no gpio interrupts implemented yet
cv.SplitDefault(
CONF_USE_INTERRUPT,
bk72xx=False,
@@ -47,7 +46,7 @@ CONFIG_SCHEMA = (
esp8266=True,
host=True,
ln882x=False,
nrf52=False,
nrf52=True,
rp2040=True,
rtl87xx=False,
): cv.boolean,
+78 -4
View File
@@ -1,6 +1,7 @@
#ifdef USE_ZEPHYR
#include "gpio.h"
#include <zephyr/drivers/gpio.h>
#include <zephyr/sys/util.h>
#include "esphome/core/log.h"
namespace esphome {
@@ -33,20 +34,80 @@ static gpio_flags_t flags_to_mode(gpio::Flags flags, bool inverted, bool value)
return ret;
}
// ESPHome's InterruptType is expressed in logical levels, but the pin is configured active-high in Zephyr (inversion is
// applied in software by digital_read()/digital_write(), see the `!= inverted_` convention below). So when the pin is
// inverted we must swap the physical edge/level the interrupt arms on: a logical rising edge is a physical falling
// edge, etc. GPIO_INT_EDGE_BOTH is symmetric and needs no swap.
static gpio_flags_t interrupt_type_to_flags(gpio::InterruptType type, bool inverted) {
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
return inverted ? GPIO_INT_EDGE_FALLING : GPIO_INT_EDGE_RISING;
case gpio::INTERRUPT_FALLING_EDGE:
return inverted ? GPIO_INT_EDGE_RISING : GPIO_INT_EDGE_FALLING;
case gpio::INTERRUPT_ANY_EDGE:
return GPIO_INT_EDGE_BOTH;
case gpio::INTERRUPT_LOW_LEVEL:
return inverted ? GPIO_INT_LEVEL_HIGH : GPIO_INT_LEVEL_LOW;
case gpio::INTERRUPT_HIGH_LEVEL:
return inverted ? GPIO_INT_LEVEL_LOW : GPIO_INT_LEVEL_HIGH;
}
return inverted ? GPIO_INT_EDGE_FALLING : GPIO_INT_EDGE_RISING;
}
// Zephyr calls this with a pointer to the gpio_callback the interrupt fired on.
// Recover the owning ZephyrGPIOInterrupt and dispatch to the ESPHome ISR.
static void gpio_interrupt_handler(const device * /*dev*/, gpio_callback *cb, uint32_t /*pins*/) {
auto *interrupt = CONTAINER_OF(cb, ZephyrGPIOInterrupt, callback);
if (interrupt->func != nullptr) {
interrupt->func(interrupt->arg);
}
}
struct ISRPinArg {
const device *gpio;
uint8_t pin;
uint8_t gpio_size;
bool inverted;
};
ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->gpio = this->gpio_;
arg->pin = this->pin_;
arg->gpio_size = this->gpio_size_;
arg->inverted = this->inverted_;
return ISRInternalGPIOPin((void *) arg);
}
void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
// TODO
if (!device_is_ready(this->gpio_)) {
ESP_LOGE(TAG, "Cannot attach interrupt: GPIO device not ready");
return;
}
// Drop any interrupt previously attached to this pin before re-registering.
this->detach_interrupt();
this->interrupt_.func = func;
this->interrupt_.arg = arg;
uint8_t port_pin = this->pin_ % this->gpio_size_;
gpio_init_callback(&this->interrupt_.callback, gpio_interrupt_handler, BIT(port_pin));
int ret = gpio_add_callback(this->gpio_, &this->interrupt_.callback);
if (ret != 0) {
ESP_LOGE(TAG, "gpio_add_callback failed for pin %u: %d", this->pin_, ret);
return;
}
ret = gpio_pin_interrupt_configure(this->gpio_, port_pin, interrupt_type_to_flags(type, this->inverted_));
if (ret != 0) {
ESP_LOGE(TAG, "gpio_pin_interrupt_configure failed for pin %u: %d", this->pin_, ret);
gpio_remove_callback(this->gpio_, &this->interrupt_.callback);
return;
}
ESP_LOGD(TAG, "Interrupt attached to pin %u (type=%d)", this->pin_, (int) type);
}
void ZephyrGPIOPin::setup() {
@@ -88,15 +149,28 @@ void ZephyrGPIOPin::digital_write(bool value) {
}
gpio_pin_set(this->gpio_, this->pin_ % this->gpio_size_, value != this->inverted_ ? 1 : 0);
}
void ZephyrGPIOPin::detach_interrupt() const {
// TODO
if (this->gpio_ == nullptr) {
return;
}
uint8_t port_pin = this->pin_ % this->gpio_size_;
gpio_pin_interrupt_configure(this->gpio_, port_pin, GPIO_INT_DISABLE);
gpio_remove_callback(this->gpio_, &this->interrupt_.callback);
this->interrupt_.func = nullptr;
this->interrupt_.arg = nullptr;
}
} // namespace zephyr
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
// TODO
return false;
auto *arg = (zephyr::ISRPinArg *) this->arg_;
if (arg == nullptr || arg->gpio == nullptr) {
return false;
}
return bool(gpio_pin_get(arg->gpio, arg->pin % arg->gpio_size) != arg->inverted);
}
} // namespace esphome
+15
View File
@@ -3,8 +3,19 @@
#ifdef USE_ZEPHYR
#include "esphome/core/hal.h"
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
namespace esphome::zephyr {
// Bundles the Zephyr gpio_callback together with the ESPHome ISR function and
// argument. Keeping them in one POD struct lets the static handler recover the
// owning data straight from the callback pointer via CONTAINER_OF, so no global
// pin->instance lookup table is needed.
struct ZephyrGPIOInterrupt {
struct gpio_callback callback;
void (*func)(void *){nullptr};
void *arg{nullptr};
};
class ZephyrGPIOPin : public InternalGPIOPin {
public:
ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) {
@@ -36,6 +47,10 @@ class ZephyrGPIOPin : public InternalGPIOPin {
uint8_t gpio_size_{};
bool inverted_{};
bool value_{false};
// attach_interrupt()/detach_interrupt() are const (matching the base class), so
// the interrupt state they manage has to be mutable.
mutable ZephyrGPIOInterrupt interrupt_{};
};
} // namespace esphome::zephyr
@@ -1,7 +1,31 @@
# P0.2, P0.4 and P0.5 all live on the same Zephyr port device (gpio0) and each
# attaches its own interrupt. This locks in shared-port behavior: every pin owns
# a separate gpio_callback initialized with its own BIT(pin) mask, so Zephyr
# dispatches to each pin independently even though the port device is shared.
binary_sensor:
- platform: gpio
pin: 2
id: gpio_binary_sensor
use_interrupt: true
interrupt_type: ANY
# Inverted pin with an edge-specific interrupt: exercises the inversion-aware
# interrupt-arming path (logical RISING must arm on the physical falling edge).
- platform: gpio
pin:
number: P0.4
inverted: true
id: gpio_binary_sensor_inverted
use_interrupt: true
interrupt_type: RISING
# Second non-inverted interrupt on the same port (gpio0) as P0.2 above: verifies
# multiple pins sharing one port device each get their own callback/pin_mask.
- platform: gpio
pin: P0.5
id: gpio_binary_sensor_shared_port
use_interrupt: true
interrupt_type: FALLING
output:
- platform: gpio