NRF54L15 ADC sampling gets stuck inside adc_read

Hi Dev,

Development platform: NRF54L15 + NCS 3.2.3

We are facing an issue with ADC sampling. The code occasionally gets stuck/hangs inside the adc_read function. This issue does not occur every time — it happens sporadically after long-term operation or under certain unknown conditions.

I have already narrowed down the issue and confirmed that the hang occurs inside adc_read.

Here is the code snippet:

c
int adc_single_channel_poll(uint8_t chan)
{
    if (chan >= ADC_CH_COUNT) return -EINVAL;

    int16_t             local_sample = 0;
    struct adc_sequence seq          = {
                 .channels    = BIT(channel_cfgs[chan].channel_id),
                 .buffer      = &local_sample,
                 .buffer_size = sizeof(local_sample),
                 .resolution  = 12,
    };

    int err = adc_read(adc_dev, &seq);

    if (likely(err == 0))
    {
        adc_raw[chan] = (local_sample < 0) ? 0 : local_sample;
    }

    return err;
}

The execution gets stuck at adc_read() and never returns.

Could you please help clarify:

  1. Is this a driver-layer bug in the ADC driver provided with NCS 3.2.3?

  2. Or could it be a hardware issue with the NRF54L15 device itself?

  3. Are there any known issues or errata related to ADC on NRF54L15 that could cause occasional hangs in adc_read?

Any suggestions on how to further debug or work around this issue would be greatly appreciated.

Thanks,
Chen

Parents
  • Hi Chen,

    sorry for late reply. I do not think I have seen before. Before commenting on whether this could be a driver level bug or hardware issue, I need to replicate this at my end. Can you please help me (provide code or code snippets) for me to easily reproduce this on the DK?

  • Hi Team,

    Thank you for your continued support. Here is my complete DTS configuration and source code for your reference.

    DTS overlay (overlay.dts):

    dts
    &adc {
        status = "okay";
        #address-cells = <1>;
        #size-cells = <0>;
    
        channel@0 {
            reg = <0>;
            zephyr,gain = "ADC_GAIN_1_4";
            zephyr,reference = "ADC_REF_INTERNAL";
            zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
            zephyr,input-positive = <NRF_SAADC_AIN4>; 
            zephyr,resolution = <12>;
        };
    
        channel@1 {
            reg = <1>;
            zephyr,gain = "ADC_GAIN_1_4";
            zephyr,reference = "ADC_REF_INTERNAL";
            zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
            zephyr,input-positive = <NRF_SAADC_AIN5>;
            zephyr,resolution = <12>;
        };
    };

    Source code (source.c):

    c
    static K_MUTEX_DEFINE(adc_lock);
    #define ADC_NODE         DT_NODELABEL(adc)
    static const struct device *adc_dev = DEVICE_DT_GET(ADC_NODE);
    
    static const struct adc_channel_cfg channel_cfgs[] = {
        ADC_CHANNEL_CFG_DT(DT_CHILD(ADC_NODE, channel_0)),
        ADC_CHANNEL_CFG_DT(DT_CHILD(ADC_NODE, channel_1)),
    };
    #define ADC_CH_COUNT ARRAY_SIZE(channel_cfgs)
    
    int adc_init(void)
    {
        int err;
        if (!device_is_ready(adc_dev)) return -ENODEV;
    
        for (uint8_t i = 0; i < ADC_CH_COUNT; i++)
        {
            err = adc_channel_setup(adc_dev, &channel_cfgs[i]);
            if (err != 0) return err;
        }
    
        int16_t             dummy_buf;
        struct adc_sequence cal_seq = {
            .channels    = BIT(channel_cfgs[0].channel_id),
            .buffer      = &dummy_buf,
            .buffer_size = sizeof(dummy_buf),
            .resolution  = 12,
            .calibrate   = true,
        };
        adc_read(adc_dev, &cal_seq);
        adc_err_cnt = 0;
        return 0;
    }
    
    int adc_single_channel_poll(uint8_t chan)
    {
        if (chan >= ADC_CH_COUNT) return -EINVAL;
    
        int lock_err = k_mutex_lock(&adc_lock, K_MSEC(100));
        if (lock_err != 0)
        {
            return -EBUSY;
        }
    
        int16_t             local_sample = 0;
        struct adc_sequence seq          = {
                     .channels    = BIT(channel_cfgs[chan].channel_id),
                     .buffer      = &local_sample,
                     .buffer_size = sizeof(local_sample),
                     .resolution  = 12,
        };
    
        int err = adc_read(adc_dev, &seq);
    
        if (likely(err == 0))
        {
            adc_raw[chan] = (local_sample < 0) ? 0 : local_sample;
        }
    
        k_mutex_unlock(&adc_lock);
        return err;
    }

    Important additional context:

    1. Concurrent access: I am calling adc_single_channel_poll() from different threads concurrently. However, I have already protected the ADC access with a mutex (adc_lock) to prevent concurrent calls. So at any given time, only one thread can enter adc_read().

    2. Critical finding – Timeout configuration: I looked into the Zephyr ADC driver layer and found the following in adc_context.h:

    c
    #ifndef ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT
    #define ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT K_FOREVER
    #endif

    By default, the ADC driver waits forever (K_FOREVER) for a conversion to complete. This means if the ADC hardware never generates a completion event (due to a hang or missed interrupt), the calling thread will be stuck indefinitely inside adc_read().

    This perfectly explains the behavior I am observing — the thread hangs forever in adc_read().

    My questions now become:

    1. Under what conditions would the ADC hardware fail to generate a completion event? Could this be due to:

      • Clock glitches or instability?

      • Power management transitions?

      • Race conditions when multiple threads are involved (even with a mutex)?

      • A hardware errata on NRF54L15?

    2. Is it safe to override ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT to a finite value (e.g., K_MSEC(100))? Would this allow the driver to recover from a hang and return an error instead of hanging forever?

    3. If I set a finite timeout, what is the expected behavior when a timeout occurs? Will the ADC peripheral be properly reset/cleaned up for the next call?

    Looking forward to your insights.

    Thanks,
    Chen

Reply
  • Hi Team,

    Thank you for your continued support. Here is my complete DTS configuration and source code for your reference.

    DTS overlay (overlay.dts):

    dts
    &adc {
        status = "okay";
        #address-cells = <1>;
        #size-cells = <0>;
    
        channel@0 {
            reg = <0>;
            zephyr,gain = "ADC_GAIN_1_4";
            zephyr,reference = "ADC_REF_INTERNAL";
            zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
            zephyr,input-positive = <NRF_SAADC_AIN4>; 
            zephyr,resolution = <12>;
        };
    
        channel@1 {
            reg = <1>;
            zephyr,gain = "ADC_GAIN_1_4";
            zephyr,reference = "ADC_REF_INTERNAL";
            zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 40)>;
            zephyr,input-positive = <NRF_SAADC_AIN5>;
            zephyr,resolution = <12>;
        };
    };

    Source code (source.c):

    c
    static K_MUTEX_DEFINE(adc_lock);
    #define ADC_NODE         DT_NODELABEL(adc)
    static const struct device *adc_dev = DEVICE_DT_GET(ADC_NODE);
    
    static const struct adc_channel_cfg channel_cfgs[] = {
        ADC_CHANNEL_CFG_DT(DT_CHILD(ADC_NODE, channel_0)),
        ADC_CHANNEL_CFG_DT(DT_CHILD(ADC_NODE, channel_1)),
    };
    #define ADC_CH_COUNT ARRAY_SIZE(channel_cfgs)
    
    int adc_init(void)
    {
        int err;
        if (!device_is_ready(adc_dev)) return -ENODEV;
    
        for (uint8_t i = 0; i < ADC_CH_COUNT; i++)
        {
            err = adc_channel_setup(adc_dev, &channel_cfgs[i]);
            if (err != 0) return err;
        }
    
        int16_t             dummy_buf;
        struct adc_sequence cal_seq = {
            .channels    = BIT(channel_cfgs[0].channel_id),
            .buffer      = &dummy_buf,
            .buffer_size = sizeof(dummy_buf),
            .resolution  = 12,
            .calibrate   = true,
        };
        adc_read(adc_dev, &cal_seq);
        adc_err_cnt = 0;
        return 0;
    }
    
    int adc_single_channel_poll(uint8_t chan)
    {
        if (chan >= ADC_CH_COUNT) return -EINVAL;
    
        int lock_err = k_mutex_lock(&adc_lock, K_MSEC(100));
        if (lock_err != 0)
        {
            return -EBUSY;
        }
    
        int16_t             local_sample = 0;
        struct adc_sequence seq          = {
                     .channels    = BIT(channel_cfgs[chan].channel_id),
                     .buffer      = &local_sample,
                     .buffer_size = sizeof(local_sample),
                     .resolution  = 12,
        };
    
        int err = adc_read(adc_dev, &seq);
    
        if (likely(err == 0))
        {
            adc_raw[chan] = (local_sample < 0) ? 0 : local_sample;
        }
    
        k_mutex_unlock(&adc_lock);
        return err;
    }

    Important additional context:

    1. Concurrent access: I am calling adc_single_channel_poll() from different threads concurrently. However, I have already protected the ADC access with a mutex (adc_lock) to prevent concurrent calls. So at any given time, only one thread can enter adc_read().

    2. Critical finding – Timeout configuration: I looked into the Zephyr ADC driver layer and found the following in adc_context.h:

    c
    #ifndef ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT
    #define ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT K_FOREVER
    #endif

    By default, the ADC driver waits forever (K_FOREVER) for a conversion to complete. This means if the ADC hardware never generates a completion event (due to a hang or missed interrupt), the calling thread will be stuck indefinitely inside adc_read().

    This perfectly explains the behavior I am observing — the thread hangs forever in adc_read().

    My questions now become:

    1. Under what conditions would the ADC hardware fail to generate a completion event? Could this be due to:

      • Clock glitches or instability?

      • Power management transitions?

      • Race conditions when multiple threads are involved (even with a mutex)?

      • A hardware errata on NRF54L15?

    2. Is it safe to override ADC_CONTEXT_WAIT_FOR_COMPLETION_TIMEOUT to a finite value (e.g., K_MSEC(100))? Would this allow the driver to recover from a hang and return an error instead of hanging forever?

    3. If I set a finite timeout, what is the expected behavior when a timeout occurs? Will the ADC peripheral be properly reset/cleaned up for the next call?

    Looking forward to your insights.

    Thanks,
    Chen

Children
No Data
Related