nordic的nrf52系列——ADC在使用时如何校准增益误差(基于SDK)
简介: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"); 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采样偏差大