nrf52805: ADC polling mode issue

Hardware setup: Custom board using BC805M-P (with App protect)
Software: nrf5 SDK S112 v17.0.2 ->Segger V7.30

My Objective: 

measure the Li-ion battery voltage(4.2V -100% till 3.6V-0%) through an external resistive divider & internal 0.6v ref voltage. (I'm not worried about the current consumption as of this moment. would like to make this concept work & all the other optimisations shall see later. Regarding optimisations yes I'm aware of this page: https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/measuring-lithium-battery-voltage-with-nrf52)

What works so far?
for the moment my example measured voltage value is being transmitted over BLE through the battery service. it's all working well.
But the ADC is just measuring some garbage value (random) which is clearly not correct.

My concept of ADC polling mode is taken from this post & modified for my work. https://devzone.nordicsemi.com/f/nordic-q-a/14486/measuring-the-battery-voltage-with-nrf52832/129422

// Input range of External Vdd measurement = (0.6 V)/(1/5) = 3 V
// 3.0 volts ->  16383 ADC counts with 14-bit sampling:  5461 counts per volt
// 3.0 volts ->  4095 ADC counts with 12-bit sampling:  1365 counts per volt

#define ADC12_COUNTS_PER_VOLT 5461

void Adc12bitPolledInitialise(void)
{
    uint32_t timeout = 10;
    nrf_saadc_channel_config_t myConfig =
    {
        .resistor_p = NRF_SAADC_RESISTOR_DISABLED,
        .resistor_n = NRF_SAADC_RESISTOR_DISABLED,
        .gain       = NRF_SAADC_GAIN1_5,            // (1/5) Gain
        .reference  = NRF_SAADC_REFERENCE_INTERNAL, // 0.6V internal Ref Voltage
        .acq_time   = NRF_SAADC_ACQTIME_40US,       // See max source resistancetable
        .mode       = NRF_SAADC_MODE_SINGLE_ENDED,
        .burst      = NRF_SAADC_BURST_DISABLED,
        .pin_p      = NRF_SAADC_INPUT_AIN2,         // AIN2 for input Pin
        .pin_n      = NRF_SAADC_INPUT_DISABLED
    };

    nrf_saadc_resolution_set((nrf_saadc_resolution_t) 3);   // 2 is 12-bit , 3 for 14-bit 
    nrf_saadc_oversample_set((nrf_saadc_oversample_t) 2);   // 2 is 4x, about 150uSecs total
    nrf_saadc_int_disable(NRF_SAADC_INT_ALL);
    nrf_saadc_event_clear(NRF_SAADC_EVENT_END);
    nrf_saadc_event_clear(NRF_SAADC_EVENT_STARTED);
    nrf_saadc_enable();

    NRF_SAADC->CH[1].CONFIG =
              ((myConfig.resistor_p << SAADC_CH_CONFIG_RESP_Pos)   & SAADC_CH_CONFIG_RESP_Msk)
            | ((myConfig.resistor_n << SAADC_CH_CONFIG_RESN_Pos)   & SAADC_CH_CONFIG_RESN_Msk)
            | ((myConfig.gain       << SAADC_CH_CONFIG_GAIN_Pos)   & SAADC_CH_CONFIG_GAIN_Msk)
            | ((myConfig.reference  << SAADC_CH_CONFIG_REFSEL_Pos) & SAADC_CH_CONFIG_REFSEL_Msk)
            | ((myConfig.acq_time   << SAADC_CH_CONFIG_TACQ_Pos)   & SAADC_CH_CONFIG_TACQ_Msk)
            | ((myConfig.mode       << SAADC_CH_CONFIG_MODE_Pos)   & SAADC_CH_CONFIG_MODE_Msk)
            | ((myConfig.burst      << SAADC_CH_CONFIG_BURST_Pos)  & SAADC_CH_CONFIG_BURST_Msk);

    NRF_SAADC->CH[1].PSELN = myConfig.pin_n;
    NRF_SAADC->CH[1].PSELP = myConfig.pin_p;
}

void ble_Update_BatteryVoltage(void)
{
    // Enable command & Turn on the Power
    nrf_gpio_pin_write(ADC_SWITCH, 1); //turning on the voltage bridge on with a transistor
    nrf_saadc_enable();

    uint16_t result = 9999;         // Some recognisable dummy value
    uint32_t timeout = 100000;       // Trial and error
    volatile int16_t buffer[8];

    NRF_SAADC->RESULT.PTR = (uint32_t)buffer;
    NRF_SAADC->RESULT.MAXCNT = 1;

    nrf_saadc_event_clear(NRF_SAADC_EVENT_END);
    nrf_saadc_task_trigger(NRF_SAADC_TASK_START);
    nrf_saadc_task_trigger(NRF_SAADC_TASK_SAMPLE);

    if (timeout != 0)
    {
        result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/
        5)) / ADC12_COUNTS_PER_VOLT;
    }

    while (0 == nrf_saadc_event_check(NRF_SAADC_EVENT_END) && timeout > 0)
    {
        timeout--;
    }
    nrf_saadc_task_trigger(NRF_SAADC_TASK_STOP);
    nrf_saadc_event_clear(NRF_SAADC_EVENT_STARTED);
    nrf_saadc_event_clear(NRF_SAADC_EVENT_END);

    // Disable command & turn off the Power to ADC Bridge to reduce power consumption
    nrf_saadc_disable();
    
    nrf_gpio_pin_write(ADC_SWITCH, 0); //turning off the voltage bridge on with a transistor
    ble_bas_battery_level_update(&m_bas, result, m_conn_handle); 
}


The ADC result value is clearly not accurately working when I'm just calling the function manually upon a button press. ble_Update_BatteryVoltage();

What am I doing wrong?

Thanks, for any valuable input I'm new to NRF. 
Gokunath 

  • BLE transmissions require current bursts, and even small such current bursts load the battery and are observable as voltage dips due to internal battery impedance which show up as disturbances in the measured voltage readings. Several options to reduce these voltage measurement dips: 1) sample the SAADC prior to a BLE burst (see Radio events); 2) add a filter capacitor between ADC IN 2 (AIN) and GND eg 100nF or more. Time constant of 100nF with 120k to battery is 12mSec, but for battery measurement probably an even slower value would be acceptable. The bigger the capacitor, the more even the voltage readings. 3) Use averaging; averaging is always better and for battery capacity response time is not significant.

    Often two battery indications are required; battery voltage for capacity and lowest voltage dip for pending reset or brownout problems. Use averaging for the former and single values for the latter. Maybe even use 2 SAADC inputs, one with filter capacitor and one not to allow both fast sampling via comparator or SAADC and one for slower voltage measurement for capacity, probably overkill.

  • I wrote this filter for a similar application; try it to see if it gives you what you are looking for

    // Limit excursions such as noise spikes; for a step change this enforces a
    // linear ramp or descent
    // For 250 mVolts, 3300mV FSD and 12-bit ADC => ((250*4096)/3300) = 310
    #define NOISE_SPIKE_CLIP_VALUE  310
    
    // Equaliser single-pole low-pass filter value (0 is off). Increasing this value
    // might require checking not overranging variables which FILTER_VALUE is assigned to.
    #define FILTER_VALUE  16
    
    #define FILTER_HOLDOFF_TRIP_LEVEL 16
    
    // ADC Filter initial hold-off counter
    uint16_t FilterHoldoffCounter = 0;
    // Measured battery voltage - filtered value
    uint16_t MeasuredBatteryVoltage;
    
    void LowPassFilter(uint16_t *FilteredValue, uint16_t NewValue)
    /*
     * This function implements a slew-rate limiting noise spike filter
     * followed by a single pole low-pass filter.
     *
     * Noise Spike Filter:
     *
     * Clip any signals to remain within 0.x volts of current filtered value
     *
     * Single-pole low-pass filter:
     *
     *    FV = ((1-n)*FV + n*V)/n where n=1/FILTER_VALUE
     *
     * The filter kicks in after 5 time constants which allows the filter to
     * settle to 12-bit resolution before taking over
     *
     * If FILTER_VALUE is constrained to lie between 0 (off) and 32 then 16-bit
     * unsigned arithmetic may be used since with a full-scale input reading
     * of 0x07FF (32-1)*0x7FF + 0x7FF + (32/2) = 65520 which fits in 16-bits
     * Otherwise 32-bit calculation is required. For ARM use 32-bit always
     */
    {
        uint16_t LocalFilteredValue = *FilteredValue;
        uint16_t SpikeClippedValue;
    
        // Set initial value to be spike-clipped (slew-rate limited)
        SpikeClippedValue = NewValue;
    
        // Check if new value is above or below above current filtered value
        if (NewValue >= LocalFilteredValue)    // +ve signal excursion
        {
            // Clip input if more than 0.5 volts above current filtered value
            if (NewValue > LocalFilteredValue + NOISE_SPIKE_CLIP_VALUE)
            {
                SpikeClippedValue = LocalFilteredValue + NOISE_SPIKE_CLIP_VALUE;
            }
        }
        else  // -ve signal excursion
        {
            // Clip input if more than 0.5 volts below current filtered value
            if ((LocalFilteredValue - NewValue) > NOISE_SPIKE_CLIP_VALUE)
            {
                SpikeClippedValue = LocalFilteredValue - NOISE_SPIKE_CLIP_VALUE;
            }
        }
    
        // Either no filter or no processing required for speed, just return value
        #if (!FILTER_VALUE) //|| defined(USE_ADC12B_MULTIPLE_SAMPLES)
        {
            *FilteredValue = SpikeClippedValue;  // Filter disabled
            return;
        }
        #else //if (!FILTER_VALUE)
    
        LocalFilteredValue = (LocalFilteredValue*(FILTER_VALUE-1))
                           + SpikeClippedValue + (FILTER_VALUE/2);
        LocalFilteredValue /= FILTER_VALUE;
    
        // Filter settles to final 12-bit value within 5 time-constants, check ready
        // This is only used on exit hibernate and initial power-on cold start where
        // all channels are continually read to condition the filters. Prior to that
        // FilterHoldoffCounter must be initialized to 0.
        if ( FilterHoldoffCounter > FILTER_HOLDOFF_TRIP_LEVEL )
        {
            *FilteredValue = LocalFilteredValue;
        }
        else
        {
            FilterHoldoffCounter++;          // Waiting for initial stable reading
            *FilteredValue = NewValue;
        }
        #endif //if (!FILTER_VALUE)
    }

  • Hi    

    I've noticed in debugging that my averaging might not be correct due to that I suspect it's moving a lot. 

    can you please verify it,

        if (timeout != 0)
        {
            result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/
            5)) / ADC12_COUNTS_PER_VOLT;
        }
     especially this line? 


    EDIT:
    sorry there was my typo during copy pasting the code in this thread. my correct code is still this.

    result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/2)) / ADC12_COUNTS_PER_VOLT;


    Thanks,
    Gokulnath A R

  • Hi Gokulnath, sorry for the delay.

    Some context is missing. However, it looks like you are performing many steps in only a few lines. Why not break up the calculations and print out the intermediate values? That way you could confirm that you are performing the intended calculations.

  • There is a typo in this line, which was correct in your earlier post. Also note that this is not averaging, it is rounding; this means the averaging is presumably performed in ble_bas_battery_level_update()

    // This line rounds the reading
    Change
        result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/5)) / ADC12_COUNTS_PER_VOLT;
    To
        result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/2)) / ADC12_COUNTS_PER_VOLT;

    You can add the averaging I posted earlier, something like this:

    void LowPassFilter(uint16_t *FilteredValue, result);
    
    // Measured battery voltage - filtered value
    static uint16_t MeasuredBatteryVoltage = 0;
    
        if (timeout != 0)
        {
            result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/2)) / ADC12_COUNTS_PER_VOLT;
            LowPassFilter(&MeasuredBatteryVoltage, result);
            // Use filtered value instead of raw sample
            result = MeasuredBatteryVoltage;
        }
    .. snip ..
        ble_bas_battery_level_update(&m_bas, result, m_conn_handle); 
    
        return result;

    Edit: If using the circuit first posted,  the numbers in the log don't look correct:

    Vadc = Vbat*300/(120+300) = (Vbat*300)/420
    Vadc411 = 2936 mV
    Vadc368 = 2629 mV
    
    // Input range of internal Vdd measurement = (0.6 V)/(1/5) = 3 V
    // 3.0 volts ->  16383 ADC counts with 14-bit sampling:  5461 counts per volt
    // 3.0 volts ->   4095 ADC counts with 14-bit sampling:  1365 counts per volt
    Vadc411 = 2936 mV = 16033 counts with 14-bit sampling
    Vadc411 = 2936 mV =  4007 counts with 12-bit sampling
    
    result = ((buffer[0] * 1000L)+(ADC12_COUNTS_PER_VOLT/2)) / ADC12_COUNTS_PER_VOLT;
    result411 = ((16033 * 1000)+(5461/2))/5461 = 2936 with 14-bit sampling
    result411 = (( 4007 * 1000)+(1365/2))/1365 = 2936 with 12-bit sampling
    Vbat = 2936 * (120+300)/300 = 4110 mV = 4.11 V
    
    From the log:
    Voltage on BLE // 4.11V on voltage divider input from a Bench Power supply.
    I      0BLEParserBase.CopyToRawData:Data:0x76  <<== 118, should be 0x678 for 14-bit, 0x555 for 12-bit

    Maybe explicitly cast to uint32_t to ensure 32-bit processing:

    // Ensure 32-bit processing:
    result = (uint16_t)(((uint32_t)buffer[0] * 1000UL)+((uint32_t)ADC12_COUNTS_PER_VOLT/2)) / (uint32_t)ADC12_COUNTS_PER_VOLT);

    Helsing's suggestion of printing out intermediate steps is the best way to verify the code is doing what you expect.

Related