GPIOTE IN event silently drops a rising edge when preceded by a sub-microsecond glitch

Symptoms

A GPIOTE event channel is configured to detect a rising edge on a GPIO input. Via PPI, the event drives a GPIO output low within a few hundred nanoseconds. The setup works correctly in isolation, but intermittently randomly, sometimes several times per second, sometimes seconds apart; the output does not respond when the input transitions. The GPIO input register confirms the pin is high. The GPIOTE channel is correctly configured. The PPI channel is enabled. The CPU can be halted in the debugger and the fault still occurs; it is a pure hardware path failure.

The fault is not in the PPI endpoints, group configuration, channel enable register, or GPIOTE channel assignment. It is in GPIOTE event generation: the peripheral sees the pin transition but does not emit the event signal to the PPI.

Reproduction

The following bare-metal program reproduces the miss on any nRF52805 DK. No softdevice needed; the [155] workaround register is applied explicitly.

Hardware: bridge P0.02 to P0.03 with a wire. Observe P0.02, P0.03, P0.04 on a signal analyzer.

Phase 1 (first 2 s): a single clean 500 ns pulse on P0.02 each cycle. GPIOTE detects every rising edge; P0.04 responds on every cycle.

Phase 2 (after 2 s): a 62.5 ns pre-pulse is added immediately before the main pulse. P0.04 stops responding; GPIOTE misses the main pulse rising edge on every cycle.

#include <nrf.h>
#include <nrf_delay.h>

/*
 * nRF52805 GPIOTE edge-detection bug reproducer.
 *
 * Bridge P0.02 -> P0.03 with a wire.
 *
 * TIMER1: 16 MHz, 16-bit, 50 kHz period (CC[2]=319).
 * TIMER0: 16 MHz, 16-bit, cleared each cycle by TIMER1 CC[2] via PPI fork.
 *
 * P0.02 (output): pulse source driven by TIMER0 via PPI.
 * P0.03 (input):  GPIOTE LOTOHI event, bridged to P0.02.
 * P0.04 (output): driven LOW on each detected rising edge, HIGH at period restart.
 *
 * Phase 1 (2s): single 500 ns pulse — P0.04 responds on every cycle.
 * Phase 2:      62.5 ns pre-pulse added before the main pulse —
 *               P0.04 stops responding (GPIOTE misses the main pulse edge).
 *
 * Errata [155]: workaround register written for event channel.
 * Errata [156]: TASKS_CLR[even] replaced with POLARITY_HiToLo + TASKS_OUT[even].
 */

#define PIN_OUT_A  2
#define PIN_IN_B   3
#define PIN_OUT_C  4

#define GPIOTE_CH_A  0   /* task, P0.02 */
#define GPIOTE_CH_B  4   /* event, P0.03, LOTOHI */
#define GPIOTE_CH_C  2   /* task, P0.04 */

#define PPI_CH_PERIOD    0
#define PPI_CH_PRE_SET   1
#define PPI_CH_PRE_CLR   2
#define PPI_CH_DETECT    3
#define PPI_CH_PULSE_SET 4
#define PPI_CH_PULSE_CLR 5

int main(void)
{
    /* GPIO */
    NRF_P0->PIN_CNF[PIN_OUT_A] =
        (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) |
        (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) |
        (GPIO_PIN_CNF_DRIVE_S0S1 << GPIO_PIN_CNF_DRIVE_Pos);

    NRF_P0->PIN_CNF[PIN_IN_B] =
        (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos) |
        (GPIO_PIN_CNF_INPUT_Connect << GPIO_PIN_CNF_INPUT_Pos) |
        (GPIO_PIN_CNF_PULL_Pulldown << GPIO_PIN_CNF_PULL_Pos);

    NRF_P0->PIN_CNF[PIN_OUT_C] =
        (GPIO_PIN_CNF_DIR_Output << GPIO_PIN_CNF_DIR_Pos) |
        (GPIO_PIN_CNF_INPUT_Disconnect << GPIO_PIN_CNF_INPUT_Pos) |
        (GPIO_PIN_CNF_DRIVE_H0H1 << GPIO_PIN_CNF_DRIVE_Pos);

    /* GPIOTE — [156]: even task channels use HiToLo + TASKS_OUT */
    NRF_GPIOTE->CONFIG[GPIOTE_CH_A] =
        (GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos) |
        (PIN_OUT_A << GPIOTE_CONFIG_PSEL_Pos) |
        (GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos) |
        (GPIOTE_CONFIG_OUTINIT_Low << GPIOTE_CONFIG_OUTINIT_Pos);

    NRF_GPIOTE->CONFIG[GPIOTE_CH_B] =
        (GPIOTE_CONFIG_MODE_Event << GPIOTE_CONFIG_MODE_Pos) |
        (PIN_IN_B << GPIOTE_CONFIG_PSEL_Pos) |
        (GPIOTE_CONFIG_POLARITY_LoToHi << GPIOTE_CONFIG_POLARITY_Pos);

    /* [155]: workaround for double-fire on edges < 1.3 us apart */
    *(volatile uint32_t *)(NRF_GPIOTE_BASE + 0x600 + (4 * GPIOTE_CH_B)) = 1;

    NRF_GPIOTE->CONFIG[GPIOTE_CH_C] =
        (GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos) |
        (PIN_OUT_C << GPIOTE_CONFIG_PSEL_Pos) |
        (GPIOTE_CONFIG_POLARITY_HiToLo << GPIOTE_CONFIG_POLARITY_Pos) |
        (GPIOTE_CONFIG_OUTINIT_High << GPIOTE_CONFIG_OUTINIT_Pos);

    /* TIMER1: 50 kHz period */
    NRF_TIMER1->MODE      = TIMER_MODE_MODE_Timer;
    NRF_TIMER1->BITMODE   = TIMER_BITMODE_BITMODE_16Bit;
    NRF_TIMER1->PRESCALER = 0;
    NRF_TIMER1->CC[2]     = 319;
    NRF_TIMER1->SHORTS    = TIMER_SHORTS_COMPARE2_CLEAR_Msk;

    /* TIMER0: pulse generator, cleared each cycle */
    NRF_TIMER0->MODE      = TIMER_MODE_MODE_Timer;
    NRF_TIMER0->BITMODE   = TIMER_BITMODE_BITMODE_16Bit;
    NRF_TIMER0->PRESCALER = 0;
    NRF_TIMER0->CC[0]     = 56;   /* pre-pulse HIGH  (62.5 ns wide) */
    NRF_TIMER0->CC[1]     = 57;   /* pre-pulse LOW */
    NRF_TIMER0->CC[2]     = 58;   /* main pulse HIGH (500 ns wide) */
    NRF_TIMER0->CC[3]     = 66;   /* main pulse LOW */

    /* PPI */
    NRF_PPI->CH[PPI_CH_PERIOD].EEP   = (uint32_t)&NRF_TIMER1->EVENTS_COMPARE[2];
    NRF_PPI->CH[PPI_CH_PERIOD].TEP   = (uint32_t)&NRF_GPIOTE->TASKS_SET[GPIOTE_CH_C];
    NRF_PPI->FORK[PPI_CH_PERIOD].TEP = (uint32_t)&NRF_TIMER0->TASKS_CLEAR;

    NRF_PPI->CH[PPI_CH_PRE_SET].EEP = (uint32_t)&NRF_TIMER0->EVENTS_COMPARE[0];
    NRF_PPI->CH[PPI_CH_PRE_SET].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[GPIOTE_CH_A];

    NRF_PPI->CH[PPI_CH_PRE_CLR].EEP = (uint32_t)&NRF_TIMER0->EVENTS_COMPARE[1];
    NRF_PPI->CH[PPI_CH_PRE_CLR].TEP = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CH_A];

    NRF_PPI->CH[PPI_CH_DETECT].EEP  = (uint32_t)&NRF_GPIOTE->EVENTS_IN[GPIOTE_CH_B];
    NRF_PPI->CH[PPI_CH_DETECT].TEP  = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CH_C];

    NRF_PPI->CH[PPI_CH_PULSE_SET].EEP = (uint32_t)&NRF_TIMER0->EVENTS_COMPARE[2];
    NRF_PPI->CH[PPI_CH_PULSE_SET].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[GPIOTE_CH_A];

    NRF_PPI->CH[PPI_CH_PULSE_CLR].EEP = (uint32_t)&NRF_TIMER0->EVENTS_COMPARE[3];
    NRF_PPI->CH[PPI_CH_PULSE_CLR].TEP = (uint32_t)&NRF_GPIOTE->TASKS_OUT[GPIOTE_CH_A];

    NRF_PPI->CHENSET = (1 << PPI_CH_PERIOD) |
                       (1 << PPI_CH_DETECT) |
                       (1 << PPI_CH_PULSE_SET) |
                       (1 << PPI_CH_PULSE_CLR);

    NRF_POWER->TASKS_CONSTLAT = 1;

    NRF_TIMER0->TASKS_CLEAR = 1;
    NRF_TIMER1->TASKS_CLEAR = 1;
    NRF_TIMER0->TASKS_START = 1;
    NRF_TIMER1->TASKS_START = 1;

    /* Phase 1: single 500 ns pulse, ~2 s */
    nrf_delay_ms(2000);

    /* Phase 2: enable pre-pulse — bug triggers */
    NRF_PPI->CHENSET = (1 << PPI_CH_PRE_SET) | (1 << PPI_CH_PRE_CLR);

    while (1) {}
}

What to look for on the analyzer

Phase 1 — working:
P0.02: ___|‾‾‾‾‾|_________________  (500 ns pulse)
P0.03: ___|‾‾‾‾‾|_________________  (same, bridged)
P0.04: ‾‾‾‾|_______|‾‾‾‾‾‾‾‾‾‾‾‾‾  (responds to every edge)

Phase 2 — bug:
P0.02: ____|‾|_|‾‾‾‾‾‾‾|__________  (62.5 ns pre-pulse + 500 ns main pulse)
P0.03: ____|‾|_|‾‾‾‾‾‾‾|__________  (same, bridged)
P0.04: ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾  (locked HIGH — main edge missed)

Workaround

Adding hysteresis in hardware so that the pre-pulse does not exist.

  • That sounds like a classic case of the "GPIOTE glitch filter" or a clock-domain synchronization issue, which can be incredibly maddening since the hardware registers look perfectly fine. I’ve seen similar behavior when the input signal has a slow slew rate or high-frequency noise right at the threshold, causing the internal edge detector to misfire or fail to latch. Have you checked the signal integrity with a high-bandwidth scope to see if there's any ringing or "flat-spotting" during the transition.

Related