ADC获取NTC

ADC获取NTC

一、NTC介绍

       NTC是Negative Temperature Coefficient的缩写,一般指负温度系数半导体器件,而在我们物联网实验中,称为NTC热敏电阻。NTC热敏电阻阻值计算公式如下:

image

 

       式中 RT、 RT0 分别为温度 T 、 T0 时的电阻值,Bn为材料常数。
       对于 T0=25 ℃,R0=10KΩ ,Bn=3950 的电阻-温度曲线如下所示:

image

 

       由此可见,温度越高,阻值越小。对于NTC的电路相对简单,如下图 :

image

 

       当NTC阻值发生变化时,Vout也随之发生变化,通过采样Vout电压,然后根据欧姆定律计算出电阻值,再由电阻值可算出对应的温度值。 

二、NTC例程
       由上节可知,我们需要采样Vout的值,需要用到ESP32的ADC功能,esp-idf库对ADC的操作已经封装的相当好,直接引出API给我们使用,虽然如此,但依然有一些东西我们要注意,开发板中的Vout接到了GPIO36上,关于ESP32的ADC,ESP32上具有两个ADC转化模块,分别是ADC1和ADC2,每个ADC模块均具有8路,由于在启用ADC2时,无法使用WIFI功能,因此本例程不介绍ADC2,也不推荐大家使用ADC2。然后并不是所有的GPIO口都具有ADC功能,只有如下GPIO口具有ADC功能:

1)GPIO32  ADC1_CH4
2)GPIO33  ADC1_CH5
3)GPIO34  ADC1_CH6
4)GPIO35  ADC1_CH7
5)GPIO36  ADC1_CH0
6)GPIO37  ADC1_CH1
7)GPIO38  ADC1_CH2
8)GPIO39  ADC1_CH3

       以下是部分初始化代码:

/**
 * 温度检测初始化
 * @param 无
 * @return 无
*/
void temp_ntc_init(void)
{
    adc_oneshot_unit_init_cfg_t init_config1 = {
        .unit_id = ADC_UNIT_1,  //WIFI和ADC2无法同时启用,这里选择ADC1
    };
    //启用单次转换模式
    ESP_ERROR_CHECK(adc_oneshot_new_unit(&init_config1, &s_adc_handle));
    //-------------ADC1 Config---------------//
    adc_oneshot_chan_cfg_t config = {
        .bitwidth = ADC_BITWIDTH_12,       //分辨率
        .atten = ADC_ATTEN_DB_12,          
        //衰减倍数,ESP32设计的ADC参考电压为1100mV,只能测量0-1100mV之间的电压,如果要测量更大范围的电压
        //需要设置衰减倍数
        /*以下是对应可测量范围
        ADC_ATTEN_DB_0      100 mV ~ 950 mV
        ADC_ATTEN_DB_2_5    100 mV ~ 1250 mV
        ADC_ATTEN_DB_6      150 mV ~ 1750 mV
        ADC_ATTEN_DB_12     150 mV ~ 2450 mV
        */
    };
    ESP_ERROR_CHECK(adc_oneshot_config_channel(s_adc_handle, TEMP_ADC_CHANNEL, &config));
    //-------------ADC1 Calibration Init---------------//
    do_calibration1 = example_adc_calibration_init(ADC_UNIT_1, ADC_ATTEN_DB_12, &adc1_cali_handle);
    //新建一个任务,不断地进行ADC和温度计算
    xTaskCreatePinnedToCore(temp_adc_task, "adc_task", 2048, NULL,2, NULL, 1);
}

        这里先说下adc_oneshot_new_unit这个函数,这个函数是启用单次转换,ESP32中有两种转化模式,分别是单次转换和连续转换,单次转换的意思是,我启动ADC转换,ADC模块就只转换一次值然后停止,连续转换是启动ADC转换后,ADC模块会不断地执行ADC转换,除非我们手动调用停止。经过我本人亲自的试验,连续转换的精度非常差,而且还受其他通道影响,可能之后乐鑫官方后续推出的系列芯片会修复这些问题,因此本教程只用单次转换。
       然后需要填充
adc_oneshot_chan_cfg_t结构体,这个结构体只有两项,分辨率和衰减系数,分辨率的意思是,采样回来的最大值,比如说我们满量程是2450mV,分辨率设置成12位,如果外部输入的电压是2450mV则,我们通过adc_oneshot_read读取到的值是2^12-1=4095。
       本例程中ADC_BITWIDTH_12配置成12位分辨率。.atten = ADC_ATTEN_DB_12这个特性可以说是ESP32较特殊的特性,ESP32内部的ADC参考电压只有1100mV,理论上最大只能采样1100mV,如果我们要采样大于这个值,我们就必须设置衰减,让外部电压到了ESP32内部后进行衰减,然后整体来看,我们就可以采用大于1100mV电压。以下是衰减对应的测量范围

1)ADC_ATTEN_DB_0      100 mV ~ 950 mV
2)ADC_ATTEN_DB_2_5    100 mV ~ 1250 mV
3)ADC_ATTEN_DB_6      150 mV ~ 1750 mV
4)ADC_ATTEN_DB_12     150 mV ~ 2450 mV 

       由此可见,最大的衰减倍数,最高能测量的电压是2450mV,当输入大于这个值(注意,不能超过3300mV,否则会损坏芯片),程序中读取到的值都是4095。
       example_adc_calibration_init函数里面用ESP32芯片内部预烧录的参数值对电压采样结果进行校准。

       最后新建一个temp_adc_task函数不断读取ADC值
       我们接下来看一下temp_adc_task这个函数

static void temp_adc_task(void* param)
{
    uint16_t adc_cnt = 0;
    while(1)
    {
        adc_oneshot_read(s_adc_handle, TEMP_ADC_CHANNEL, &s_adc_raw[adc_cnt]);
        if (do_calibration1) {
            adc_cali_raw_to_voltage(adc1_cali_handle, s_adc_raw[adc_cnt], &s_voltage_raw[adc_cnt]);
        }
        adc_cnt++;
        if(adc_cnt >= 10)
        {
            int i = 0;
            //用平均值进行滤波
            uint32_t voltage = 0;
            uint32_t res = 0;
            for(i = 0;i < ADC_VALUE_NUM;i++)
            {
                voltage += s_voltage_raw[i];
            }
            voltage = voltage/ADC_VALUE_NUM;
            if(voltage < ADC_V_MAX)
            {
                //电压转换为相应的电阻值
                res = (voltage*NTC_RES)/(ADC_V_MAX-voltage);
                //根据电阻值查表找出对应的温度
                s_temp_value = get_ntc_temp(res);
            }
            adc_cnt = 0;
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

       adc_oneshot_read函数触发一次转换,并且将转换后的值返回。
       
adc_cali_raw_to_voltage函数将读取的ADC值转化成相应的电压值
       这个任务重每个周期会执行10次转换,并且将转化值保存在s_voltage_raw中,10次转换后,通过计算平均值,达到滤波的效果。 

       由上节的采样电路图以及欧姆定律可知,电阻计算公式如下:

       R=(V*10K)/(3.3V-V);
       其中V是采样得到的数值。10K是NTC参考电阻。3.3V是电路中的参考电压(非ESP32内部ADC参考电压)
       计算得到电阻值后,我们就可以通过电阻值R获取到温度值了。
       那问题来了

        

image

 

这个公式这么复杂,代入进去计算基本算不出来,一般我们不这样做,我们一般通过查表来得到温度值。如何做呢?

       当我们买了NTC热敏电阻后,厂家一般都会给我们一份NTC电阻温度对应表。如下所示:

image

 

       具体的还要参考厂家提供。那我们如何在程序中使用这个表呢?首先我们需要建一个常量数组,把这些值保存起来,在本例程中是这样做的。

image

 

       这个数组单元是一个结构体,第一个成员是温度值,第二个成员是电阻值。
       现在有两个问题
       1)查找效率
       2)值问题 

       先看查找效率,我们计算出电阻值后,如果查找。一种最笨的方法是遍历,但相对耗时,还有一种是二分查找,二分查找的前提是,表是按顺序排列的,我们看这个表,阻值是从大大小的顺序排列的,因此我们可以用二分查找的方法进行查找,代码如下:

/** 二分查找,通过电阻值查找出温度值对应的下标
 * @param res 电阻值
 * @param left ntc表的左边界
 * @param right ntc表的右边界
 * @return 温度值数组下标
*/
static int find_ntc_index(uint32_t  res,uint16_t left, uint16_t right)
{
    uint16_t middle = (left + right) / 2;
    if (right <= left || left == middle)
    {
        if (res >= s_ntc_table[left].res)
            return left;
        else
            return right;
    }
    if (res > s_ntc_table[middle].res)
    {
        right = middle;
        return find_ntc_index(res,left, right);
    }
    else if (res < s_ntc_table[middle].res)
    {
        left = middle;
        return find_ntc_index(res,left, right);
    }
    else
        return middle;
}

       再看问题2
       我们计算出的电阻值,很有可能在这个表上查不到。那我们如何处理呢?上述经过二分查找后,我们会返回一个下标,这个下标对应数组中的电阻值会比计算出来的电阻值稍小,而这个下标的下一个数组值又会比计算出来的电阻值稍大。伪代码就是:

        R >= s_ntc_table[index].rec;
       R< s_ntc_table[index+1].rec;
       R是我们采样电压计算出来的电阻值,index是二分查找返回的数组下标
       假设我们刚查到这个电阻值在表中,那么很简单,直接用下标对应的温度即可。但很多时候我们不会这么理想,大多数情况都是R>s_ntc_table[index].rec && R < s_ntc_table[index+1].rec。
       这种情况我们该如何处理最好?

       有两种处理方式,
       第一种我们直接用s_ntc_table[index].temp或s_ntc_table[index+1].temp就行,简单粗暴,这种没有计算出小数点,因为表中的温度值全部是整数。
       第二种就是使用线性插值
       何为线性插值?请看下图 、

       

image

 

       假设y=f(x)是一段较复杂的曲线方程,而坐标(X0,Y0)和(X1,Y1)是落在这条曲线上的两点,因为这个方程比较复杂,我们如果有一些点需要计算,直接代入到原方程计算比较困难,如果要计算的点满足1)落在X0,X1之间,2)X0和X1距离比较近,这时后我们就可以把坐标(X0,Y0)和(X1,Y1)之间当成是一条直线。通过两点式计算出这条直线的方程,然后再将要计算的值代入这条直线方程,计算就简单多了。代码如下: 

 

/* @param x 需要计算的X坐标
 * @param x1,x2,y1,y2 两点式坐标
 * @return y值
*/
static float linera_interpolation(int32_t x,int32_t x1, int32_t x2, int32_t y1, int32_t y2)
{
    float k = (float)(y2 - y1) /(float)(x2 - x1);
    float b = (float)(x2 * y1 - x1 * y2) / (float)(x2 - x1);
    float y = k * x + b;
    return y;
}

 

 

 

       本例程中,
       X1=s_ntc_table[index].res
       X2=s_ntc_table[index+1].res
       Y1=s_ntc_table[index].temp
       Y2=s_ntc_table[index+1].temp
       X = R(采用电压对应计算出来的值)
       而返回了的Y值就是线性插值计算出来的温度值

       app_main()函数比较简单

void app_main(void)
{
    temp_ntc_init();
    while(1)
    {
        ESP_LOGI(TAG,"current temp:%.2f",get_temp());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

}

        每隔1000ms获取温度值并且打印

       完成的例程可以参考esp32-board/ntc

 

最后附上相关资料:

ESP32教程资料链接:
https://pan.baidu.com/s/1kCjD8yktZECSGmHomx_veg?pwd=q8er 
提取码:q8er 

配套源码下载地址:
esp32-board: esp32开发板配套的经典例程

ESP32开发板,包含部分传感器模块,1.69寸LCD高亮屏,Type-C一键下载,方便大家学习和做各种实验。开发板链接如下:

https://item.taobao.com/item.htm?ft=t&id=802401650392&spm=a21dvs.23580594.0.0.4fee645eXpkfcp&skuId=5635015963649

posted on 2025-10-10 03:15  &大飞  阅读(18)  评论(0)    收藏  举报

导航