GPIO P1.13 Edge Detection Causes High Floor Current on nRF54L15DK

## Description

On the nRF54L15DK, the measured floor current increases significantly when the GPIO is configured for button edge detection.

### Observed Behavior

* Floor current baseline: ~4 µA (no button interrupt configured)
* Floor current after enabling GPIO P1.13 interrupt (edge detection for button): ~23 µA

### Expected Behavior

* Floor current should remain close to the baseline (~4 µA) regardless of button edge detection configuration.

---

## Steps to Reproduce

1. Configure GPIO pin for button input **without** interrupt → observe floor current (~4 µA).
2. Enable edge detection interrupt on the button GPIO → observe floor current (~23 µA).

---

## Device Tree Source

```dts
/* Direct motive, button device, button 0 */
gpio_button0: gpio_button {
compatible = "motive,button";
status = "okay";
gpios = <&gpio1 13 (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)>;
};
```

## C Code

```c
/**
* @brief Enable button interrupt processing
* @param dev Device pointer
* @return 0 on success, negative error code on failure
*/
static int gpio_button_enable_impl(const struct device *dev)
{
struct gpio_button_data *data = dev->data;
const struct gpio_button_config *config = dev->config;
int ret;

atomic_set(&data->enabled, 1);

ret = gpio_pin_interrupt_configure(config->gpio_dev, config->pin,
GPIO_INT_EDGE_BOTH);
if (ret < 0) {
LOG_ERR("Failed to configure GPIO interrupt: %d", ret);
atomic_set(&data->enabled, 0);
return ret;
}

LOG_INF("Button enabled");
return 0;
}


/**
* @brief Initialize button device
* @param dev Device pointer
* @return 0 on success, negative error code on failure
*/
int gpio_button_init(const struct device *dev)
{
struct gpio_button_data *data = dev->data;
const struct gpio_button_config *config = dev->config;
int ret;

if (!device_is_ready(config->gpio_dev)) {
LOG_ERR("GPIO device not ready");
return -ENODEV;
}

/* Store device pointer for callbacks */
data->dev = dev;

/* Configure GPIO pin */
ret = gpio_pin_configure(config->gpio_dev, config->pin, config->flags);
if (ret < 0) {
LOG_ERR("Failed to configure GPIO pin: %d", ret);
return ret;
}

/* Initialize GPIO callback */
gpio_init_callback(&data->gpio_cb, _gpio_interrupt_handler, BIT(config->pin));
ret = gpio_add_callback(config->gpio_dev, &data->gpio_cb);
if (ret < 0) {
LOG_ERR("Failed to add GPIO callback: %d", ret);
return ret;
}

/* Initialize state */
atomic_set(&data->lastState, 0);
data->callback = _default_button_callback;
data->user_data = NULL;
data->pressStartTime = 0;
atomic_set(&data->enabled, 0);

LOG_INF("GPIO button initialized on pin %d", config->pin);
return 0;
}

```

---

## Impact

* Increases standby current consumption by nearly **6×**.
* May significantly reduce battery life in low-power operation.

Parents Reply
  • Hi Sigurd,

    Awesome! Your solution worked for me, though I made a small change. Could you explain why this issue is resolved and if there are any potential problems I should be aware of?

    @ src/embedded/bc_hbii/ble_beacon_v2_nrf54l/boards/motive/beacon_v2_overlay/beacon_v2_nrf54l15_cpuapp-dk.overlay:64 @
        ...
        /* Direct motive,button device, button 0 */
        gpio_button0: gpio_button {
            compatible = "motive,button";
            status = "okay";
            gpios = <&gpio1 13 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
        };
    };
    
    &gpio1 {
        status = "okay";
        sense-edge-mask = <(1 << 13)>;
    };

Children
  • Hi!

    1)

    So you changed from

    (GPIO_PULL_DOWN | GPIO_ACTIVE_HIGH)

    to

    (GPIO_PULL_UP | GPIO_ACTIVE_LOW)

    What is "correct" here depends on your PCB and how the button is designed to be connected to when the button in pressed. On our DK's, the button-pin is connected to ground when the button is pressed, therefore "(GPIO_PULL_UP | GPIO_ACTIVE_LOW)" is correct for the DK.

    2)

    With this sense-edge-mask, we now use Port event instead of IN event.  PORT events are lower-power.

    You can read more about this here: https://docs.nordicsemi.com/bundle/ps_nrf54L15/page/gpiote.html

  • Hi Sigurd,

    Thanks for your explanation. I have a system design question regarding port events.
    In our system, the nRF54L15 needs to handle three types of interrupts: the nPM2100 host interrupt, the RTC alarm interrupt, and a GPIO button interrupt on GPIO port 1.
    If I use a port event, do I need to determine which specific pin triggered the interrupt?

    For example, do all of these need to share the same _gpiote_port_isr_cb, or can they each use separate ISR callbacks?



    int init_sources(void)
    {
        int ret;
    
        /* Configure PMIC INT as input with pull-up, level-low interrupt */
        if (!device_is_ready(pmicInt.port)) {
            return -ENODEV;
        }
        ret = gpio_pin_configure_dt(&pmicInt, GPIO_INPUT | GPIO_PULL_UP);
        if (ret) {
            return ret;
        }
        ret = gpio_pin_interrupt_configure_dt(&pmicInt, GPIO_INT_LEVEL_LOW);
        if (ret) {
            return ret;
        }
    
        /* Configure RTC alarm: input with appropriate pull, edge to active */
        if (!device_is_ready(rtcInt.port)) {
            return -ENODEV;
        }
        ret = gpio_pin_configure_dt(&rtcInt, GPIO_INPUT |
                                              ((rtcInt.dt_flags & GPIO_PULL_UP) ? GPIO_PULL_UP : 0) |
                                              ((rtcInt.dt_flags & GPIO_PULL_DOWN) ? GPIO_PULL_DOWN : 0));
        if (ret) {
            return ret;
        }
        ret = gpio_pin_interrupt_configure_dt(&rtcInt, GPIO_INT_EDGE_TO_ACTIVE);
        if (ret) {
            return ret;
        }
    
        /* Configure Button: input with pull, edge-both for wake + software debounce */
        if (!device_is_ready(btn0.port)) {
            return -ENODEV;
        }
        ret = gpio_pin_configure_dt(&btn0, GPIO_INPUT |
                                           ((btn0.dt_flags & GPIO_PULL_UP) ? GPIO_PULL_UP : 0) |
                                           ((btn0.dt_flags & GPIO_PULL_DOWN) ? GPIO_PULL_DOWN : 0));
        if (ret) {
            return ret;
        }
        ret = gpio_pin_interrupt_configure_dt(&btn0, GPIO_INT_EDGE_BOTH);
        if (ret) {
            return ret;
        }
    
        ...
    
        /* Register one PORT callback on a representative port device.
         * On nRF54, pins for different ports reside on different GPIO devs.
         * We add callback to *each* unique port used by our lines.
         */
        gpio_init_callback(&gpiotePortCb, _gpiote_port_isr_cb, 0);
    
        /* Add callback to ports actually used */
        ret = gpio_add_callback(pmicInt.port, &gpiotePortCb);
        if (ret) {
            return ret;
        }
        if (rtcInt.port != pmicInt.port) {
            ret = gpio_add_callback(rtcInt.port, &gpiotePortCb);
            if (ret) {
                return ret;
            }
        }
        if (btn0.port != pmicInt.port && btn0.port != rtcInt.port) {
            ret = gpio_add_callback(btn0.port, &gpiotePortCb);
            if (ret) {
                return ret;
            }
        }
    
        return 0;
    }
    
    
    static void _gpiote_port_isr_cb(const struct device *port, struct gpio_callback *cb,
                                    gpio_port_pins_t pins)
    {
        ARG_UNUSED(cb);
    
        /* Check which known pins are toggled/active on this port and set flags.
         * Keep ISR tiny: just set flags and schedule work/debounce.
         */
    
        /* PMIC: level-low -> if asserted (read pin == 0), set flag */
        if (port == pmicInt.port) {
            int val = gpio_pin_get(port, pmicInt.pin);
            if (val == 0) {
                atomic_or(&wakeFlags, WAKE_CAUSE_PMIC);
                k_work_submit(&pmicWork);
            }
        }
    
        /* RTC: edge-to-active -> we treat any interrupt as a cause */
        if (port == rtcInt.port) {
            /* We don't need to read the pin; a pulse already triggered this ISR */
            atomic_or(&wakeFlags, WAKE_CAUSE_RTC);
            k_work_submit(&rtcWork);
        }
    
        /* Button: edge-both -> start/restart debounce window */
        if (port == btn0.port) {
            atomic_or(&wakeFlags, WAKE_CAUSE_BUTTON);
            (void)k_work_reschedule(&btnDebounceWork, K_MSEC(BTN_DEBOUNCE_MS));
        }
    
        /* Record snapshot for quick diagnostics outside ISR */
        lastWakeSnapshot = atomic_get(&wakeFlags);
    }



  • Hi!

    smith.hu said:

    If I use a port event, do I need to determine which specific pin triggered the interrupt?

    No, the underlaying GPIO driver will do that for you.

    smith.hu said:
    For example, do all of these need to share the same _gpiote_port_isr_cb, or can they each use separate ISR callbacks?

    On the application level, you can have seperate callbacks for each pin

Related