nordic的nrf52系列——ADC在使用时如何校准增益误差(基于SDK)

发布时间 2024-01-02 18:28:43作者: 星辰_stars

简介:ADC在实际使用的时候都要进行误差校准,那Nordic的nrf52系列如何进行校准,如果不校准又有什么影响尼,接下来我将通过实验进行测试,验证不校准和校准的影响(本测试的基础是,默认输入阻抗和采样时间都是合理范围的,没有超标)。

测试环境:

硬件:nrf52DK(nrf52832)

软件:基于nRF5_SDK_17.1.0_ddde560中的SAADC例子进行修改

一、误差确定

  在数据手册ADC的电器章节有这样一个数据表,这个表中对应不同的增益模式有不同的误差范围,如我本次测试采用的就是1/6增益,那么对于芯片来说,误差范围在3%以内都是正常的

 上面提到了误差,那这个误差影响什么值尼,主要影响了采样精度,如配置一个1/6增益,然后12bit分辨率的ADC然后进行采样,2^12=4096,也就是说在满量程也就是采样电压等于VCC的时候,理论值的采样值应该为4096,也就是说如果我给芯片供电为3.3V,那么我去采样一个3.3V的电源(公地)是采样值应该是4096,采样GND时应该是0,这都是理论值,实际情况是肯定有偏差的,而且没一个芯片的偏差还不一样,但是对于正常的芯片这个偏差都在一个范围,也就是我上表截图的范围。根据表格有这样一个计算,4096*3%=90 ,也就是说在采集3.3V时,值可以为4006~4096都是正常的,因为有90的偏差。

而我们进行校准就是减小这个误差,为什么是减小,是因为误差是不可能消除的。

二、代码

1、测试方式

  添加校准代码,在校准ADC后启动单次采样,单次采样使用定时器触发,每采样10次就进行一次校准,校准期间不进行采样,如果有转换也先进行停止。保证结果的准确性。

2、代码添加

由于是基于历程:SDK\examples\peripheral\saadc 进行测试,已经有想过的timer源文件加入,所以不用再加入timer相关的源文件了,如果你不是用这个例子,是使用自己的工程代码进行添加,那么应该注意timer的选择,如果是ble的项目,ble默认使用的timer0,timer0就不能用了,需要使用timer1,需要启动sdk_config中的关于timer1的所有宏定义,不然就会编译报错。

在下面这份代码中通过设置宏 Y_and_N_calibrate_change 来确定是否启用校准功能。

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "nrf.h"
#include "nrf_drv_saadc.h"
#include "nrf_drv_ppi.h"
#include "nrf_drv_timer.h"
#include "boards.h"
#include "app_error.h"
#include "nrf_delay.h"
#include "app_util_platform.h"
#include "nrf_pwr_mgmt.h"

#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"

/*是否启用校准代码*/
#define Y_and_N_calibrate_change 0

nrf_saadc_value_t test_value; //ADC原始采样值

float V_test=0; //ADC转换后的采样值

static const nrf_drv_timer_t m_timer = NRF_DRV_TIMER_INSTANCE(0);

typedef struct{
#if Y_and_N_calibrate_change
    bool offset_calibrate_flag;    //校准标志
#endif
    bool adc_sample_flag;                //采样标志
    uint32_t adc_sample_timer;    //采样时间
}ADC_sample_t;

ADC_sample_t m_ADC;


#if Y_and_N_calibrate_change
void saadc_callback(nrf_drv_saadc_evt_t const * p_event)
{
    switch(p_event->type)
    {
        case NRF_DRV_SAADC_EVT_CALIBRATEDONE:
                m_ADC.offset_calibrate_flag  = true;//校准完成回调
            break;
        
        default:
            break;
    }
}

/*确保要在ADC模块初始化完成后调用校准函数*/
void adc_offset_calibrate(void)
{
    ret_code_t err_code;
    
    /*检查是否使能的ADC,如果没有就直接返回,不能进行校准*/
    if(!nrf_saadc_enable_check())
    {
        NRF_LOG_INFO("Cannot be calibrated without ADC enabled");
        return;
    }
    err_code    =    nrf_drv_saadc_calibrate_offset();
    APP_ERROR_CHECK(err_code);
    
    while(!m_ADC.offset_calibrate_flag)
        {
            __WFE();
        };
    NRF_LOG_INFO("End of calibration");
}

#endif

void timer_handler(nrf_timer_event_t event_type, void * p_context)
{
#if Y_and_N_calibrate_change
    static uint8_t counter=0;
#endif
    switch(event_type)
    {
        case NRF_TIMER_EVENT_COMPARE0:
            #if Y_and_N_calibrate_change
                    if(counter<10)
                    {
                        counter++;
            #endif            
                        m_ADC.adc_sample_flag = true;
                        NRF_LOG_INFO("ADC sample start");
            #if Y_and_N_calibrate_change
                    }
                    else
                    {
                        counter=0;
                        /*定时时间到开始校验*/
                        NRF_LOG_INFO("offset starting....");
                        nrf_drv_timer_disable(&m_timer);
                        /*如果有转化,终止转化准备开始校验*/
                        nrfx_saadc_abort();
                        /*校准标志位*/
                        m_ADC.offset_calibrate_flag =false;
                    }
            #endif
            break;
        default:
            break;
    }

}

void saadc_init(void)
{
        /*本次配置为使用了单端模式,使用了AIN0通道*/
    ret_code_t err_code;
    nrf_saadc_channel_config_t channel_config =
    NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN0);
    
        /*ADC采样配置,配置为12bit,不使用过采样,中断优先级,低功耗模式*/
        nrf_drv_saadc_config_t config;
        config.resolution = NRF_SAADC_RESOLUTION_12BIT;
        config.oversample    = NRF_SAADC_OVERSAMPLE_DISABLED;
        config.low_power_mode    = NRFX_SAADC_CONFIG_IRQ_PRIORITY;
        config.low_power_mode     = NRFX_SAADC_CONFIG_LP_MODE;
    #if Y_and_N_calibrate_change
        /*nrf_drv_saadc_init的第一个参数为NULL的话将使用默认配置(NRFX_SAADC_DEFAULT_CONFIG),这将使用10bit的分辨率*/
    err_code = nrf_drv_saadc_init(&config, saadc_callback);
    APP_ERROR_CHECK(err_code);
    #else
        /*nrf_drv_saadc_init的第一个参数为NULL的话将使用默认配置(NRFX_SAADC_DEFAULT_CONFIG),这将使用10bit的分辨率*/
        err_code = nrf_drv_saadc_init(&config, NULL);
    APP_ERROR_CHECK(err_code);
    #endif
        /*配置AIN0通道*/
    err_code = nrf_drv_saadc_channel_init(0, &channel_config);
    APP_ERROR_CHECK(err_code);

}

void offset_timer_init(void)
{
        ret_code_t 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);

    /* 通道0作为采样定时*/
    uint32_t adc_sample_ticks = nrf_drv_timer_ms_to_ticks(&m_timer, m_ADC.adc_sample_timer);
    nrf_drv_timer_extended_compare(&m_timer,
                                   NRF_TIMER_CC_CHANNEL0,
                                   adc_sample_ticks,
                                   NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                   true);

    nrf_drv_timer_enable(&m_timer);
}

void adc_sample(void)
{
            m_ADC.adc_sample_flag = false;
            /*采样通道1的值并给到 test_value*/  
            nrfx_saadc_sample_convert(0,&test_value);
            /*
            根据数据手册有如下的转换公式:
            
            V =[V(P)-V(N)]* GAIN / reference *2^(resolution - m)
            V                :    采样的实际电压
            V(P):            使用采用函数获取到的值    
            V(N):            使用采用函数获取到的值(只在差分采样才有,如果是单端采样,这为0)
            GAIN:            增益值,本例程看channel_config,配置为1/6
            reference:参考电压,可以为0.6V的内部电压,或者为VCC/4(四分之一的VCC),
                                本例程看channel_config(NRF_SAADC_REFERENCE_INTERNAL),配置为0.6V
            resolution:采样精度,
            m:                单端输入为0,差分输入为1
            */
            
            V_test=test_value* 3.6/4096;
            //串口查看打印值
            NRF_LOG_INFO("%d",test_value);
            NRF_LOG_INFO("V=" NRF_LOG_FLOAT_MARKER "\r\n", NRF_LOG_FLOAT(V_test)); 
            NRF_LOG_FLUSH();
}

/**
 * @brief Function for main application entry.
 */
int main(void)
{    
    ret_code_t err_code;
           
        /*添加了log*/
    err_code = NRF_LOG_INIT(NULL);
    APP_ERROR_CHECK(err_code);
        NRF_LOG_DEFAULT_BACKENDS_INIT();
        
        /*清零实体*/
        memset(&m_ADC,0,sizeof(m_ADC));
        /*设置轮训采样时间,当前为1s*/
        m_ADC.adc_sample_timer = 1000;

    saadc_init();
    
        /*开启定时器,启动采样*/
        offset_timer_init();
    
        NRF_LOG_INFO("adc test");
        /*本例子中实测误差为±1%*/    
    while (1)
    {
      if(m_ADC.adc_sample_flag)
            {
                adc_sample();
            }
        #if Y_and_N_calibrate_change
            if(!m_ADC.offset_calibrate_flag)
            {
                adc_offset_calibrate();
                /* 校准过程中停止采样,校准完毕后开始采样 */
                nrf_drv_timer_enable(&m_timer);
            }
        #endif
            NRF_LOG_FLUSH();
    }
}

三、对比

直接对GND进行采样,对比采样偏差的大小。

1、不添加校准:

Y_and_N_calibrate_change 为 0时:

 可以看到对于我的板载芯片偏差基本都在-10以上,部分芯片偏差可能更大如-15,-20或者以上,如果在大批量时出现有部分芯片采样值不对,那么可以添加校准,说不定问题就解决了。

2、添加校准:

Y_and_N_calibrate_change 为 1 时:

 可以看到,误差减小了,经过上面测试在nrf52系列使用ADC时校准是很有必要的。

结论:

1、因为校准只会减少误差,并不是把误差消除,所以对于在实际应用中如果有些芯片增益误差达到了3%的临界值,那么就算添加了校准,可能也只校准到了2%,比如12bit的分辨率,满值是4096,那么极限误差是±90,校准后误差还可能是±60,这个时候只能说软件处理了。

2、校准只能在初始化ADC模块,但是在采样开始前,或者结束后进行,所以在代码中进行了是否采样和是否使能了ADC模块的判断。

3、ADC精度还可能和晶振的精度有关,在有些时候晶振偏差过大,也会都在ADC采样偏差大