Battery level reading using ADC pin from nRF chip shows significant fluctuation

Hi everyone,

We're reading BAT level by reading data from ADC pin and the setup diagram is shown below. 

We tested our device at hospital, and we observed significant fluctuation highlighted in red regarding its BAT level. For example, it went from 65% to 20%, then jumped to 60% again. 

We then took the device and battery back to the office and tried to reproduce the issue on benchtop. However, we couldn't reproduce it. In contrast, the following is what we got, and it worked as expected.

Our hypothesis is that it could be due to the impedance mismatch between ADC input pin and the output of voltage divider. However, based on our calculation, the input impedance of the voltage divider is equivalent to R13//R15 (50kOhm) while the input impedance of ADC pin is more than 1MOhm. This would not cause the impedance mismatch. 

I greatly appreciate if anyone has any suggestions for this issue. Thanks.

  • Of course. Here they are. There are pretty standard one for SAADC feature.

    void saadc_sampling_event_init(void)
    {
        ret_code_t err_code;
    
        err_code = nrf_drv_ppi_init();
        APP_ERROR_CHECK(err_code);
    
        nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
        timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;
        err_code = nrf_drv_timer_init(&m_timer, &timer_cfg, timer_handler);
        APP_ERROR_CHECK(err_code);
    
        /* setup m_timer for compare event every 1000ms */
        uint32_t ticks = nrf_drv_timer_ms_to_ticks(&m_timer, 1000);
        nrf_drv_timer_extended_compare(&m_timer,
                                       NRF_TIMER_CC_CHANNEL0,
                                       ticks,
                                       NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                       false);
        nrf_drv_timer_enable(&m_timer);
    
        uint32_t timer_compare_event_addr = nrf_drv_timer_compare_event_address_get(&m_timer,
                                                                                    NRF_TIMER_CC_CHANNEL0);
        uint32_t saadc_sample_task_addr   = nrf_drv_saadc_sample_task_get();
    
        /* setup ppi channel so that timer compare event is triggering sample task in SAADC */
        err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel);
        APP_ERROR_CHECK(err_code);
    
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channel,
                                              timer_compare_event_addr,
                                              saadc_sample_task_addr);
        APP_ERROR_CHECK(err_code);
    }
    
    
    void saadc_sampling_event_enable(void)
    {
        ret_code_t err_code = nrf_drv_ppi_channel_enable(m_ppi_channel);
    
        APP_ERROR_CHECK(err_code);
    }

    Since I wanted to normalize the BAT level from 0 - 100%, I rescaled it using a linear equation. There has been never an issue before, except for two cases recently.

    // Function to initialize the SAADC (Successive Approximation Analog-to-Digital Converter) module
    static void saadc_init(void)
    {
        ret_code_t err_code;
        nrf_saadc_channel_config_t channel_config = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN1);
    
       
        nrf_drv_saadc_config_t saadc_config = NRF_DRV_SAADC_DEFAULT_CONFIG;
        saadc_config.resolution = NRF_SAADC_RESOLUTION_12BIT;
        err_code = nrf_drv_saadc_init(&saadc_config, saadc_callback);
        APP_ERROR_CHECK(err_code);
    
        err_code = nrf_drv_saadc_channel_init(0, &channel_config);
        APP_ERROR_CHECK(err_code);
    
        err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[0], SAMPLES_IN_BUFFER);
        APP_ERROR_CHECK(err_code);
    
        err_code = nrf_drv_saadc_buffer_convert(m_buffer_pool[1], SAMPLES_IN_BUFFER);
        APP_ERROR_CHECK(err_code);
    }
    
    void saadc_callback(nrf_drv_saadc_evt_t const * p_event)
    {
        if (p_event->type == NRF_DRV_SAADC_EVT_DONE)
        {
            ret_code_t err_code;
            uint8_t i;
            nrf_saadc_value_t temp_adc_value = 0;
            nrf_saadc_value_t adc_result = 0;
    
            err_code = nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, SAMPLES_IN_BUFFER);
            APP_ERROR_CHECK(err_code);
    
            for (i = 0; i < SAMPLES_IN_BUFFER; i++)
            {
                //NRF_LOG_INFO("temp_adc_%d: %d", i, p_event->data.done.p_buffer[i]);
                temp_adc_value += p_event->data.done.p_buffer[i];
            }
    
            /****************************************************************************
            *                             Convert ADC value to voltage                   *
            * // V(N) = 0;
            // GAIN = 1/6;
            // REFERENCE Voltage = internal (0.6V);
            // RESOLUTION : 14 bit;
            // 14bit
            // ADC_RESULT = [V(P) – V(N) ] x GAIN / REFERENCE x 2^(RESOLUTION - m)
            // (m=0) if CONFIG.MODE=SE, or (m=1) if CONFIG.MODE=Diff
            // V(P) = ADC_RESULT x REFERENCE / ( GAIN x RESOLUTION) = RESULT x (600 / (1/6 x 2^(14)) =  ADC_RESULT x 0.2197265625;
            ****************************************************************************/
            adc_result = temp_adc_value/SAMPLES_IN_BUFFER;
            voltage = adc_result * 0.87890625;
            first_voltage = 805;// 0.5*Vmax from Duracell Optimum (1.69V MAX);
    
            #if RESCALE_BAT
            float temp_voltage_percent = 281.69* voltage/1000 - 126.76;
            if (temp_voltage_percent > 100){
                temp_voltage_percent = 100;
            } else if (temp_voltage_percent <0){
                temp_voltage_percent =0;
            } 
            #else
            float temp_voltage_percent = 100* voltage/ first_voltage;
            #endif
            voltage_level_percent = (uint8_t)temp_voltage_percent;
            
            if(voltage_level_percent <= 20 && isRedLEDBlink != true){
              isRedLEDBlink = true;
              red_led_timers_start();
            }
            // Print or use the voltage value as needed
            NRF_LOG_INFO("ADC value: %d Reference Voltage: %d Current Voltage: %d", adc_result, first_voltage, voltage);
        }
    }

  • Also SAADC init code? Looks like a lot of work within an interrupt callback complete with floating point calculations not clearly defined; also the compiler needs to know if the hardware FPU is being used to correctly handle the FPU interrupt registers, plus there are known hardware FPU power consumption issues which may or may not be handled by the version of SDK .. often best to simply use fixed-point 32-bit integers with appropriate scaling to avoid those issues

  • Ops. I just added the SAADC init code to my previous comment. Thanks!

  • Using fixed-point 32-bit integers sounds a good idea. I'm thinking of sending the raw ADC values and convert it to BAT level in % on the app side, which would reduce the complexity of what interrupt callback needs to be handled. 

  • On the firmware side, a potential flaw is the application being kept up and waiting for transmission to complete instead of being put to sleep. It's worth checking if this is happening and avoid it.

    Hi  . Just wanted to follow up on this. Could you elaborate it more? There is a parameter, called

    connection supervision timeout
    that determines how long a connection is considered valid after no data packets are received from the central device (i.e., tablet in my case). I'm not sure when to put it into sleep by interpreting your comment. Thanks, Tai!

Related