【ESP32笔记】ADC获取NTC温度
本节目标搞懂 ADC 单次采样与校准能从分压电路的采样值反推 NTC 阻值并用查表 二分查找 线性插值算出温度一、本节要解决的问题DHT11、DS18B20 这类数字传感器内部已经把模拟量处理成数字量MCU 直接读结果就行而NTC 热敏电阻是最便宜的测温器件它本身只是一个阻值随温度变化的电阻没有数字接口。要用 ESP32 读出温度必须自己打通一条转换链温度 → NTC 阻值 → 分压点电压 → ADC 原始码值 →校准电压 → 阻值 → 温度。前半段靠硬件分压电路后半段靠软件换算普通 GPIO 只能读高/低两种状态所以中间必须使用ADC模数转换器。本节的重点和难点都在后半段。二、硬件基础2.1 NTC 与 B 参数方程NTC 是 Negative Temperature Coefficient 的缩写即负温度系数热敏电阻温度越高阻值越小。它的阻值-温度关系是非线性的通常用B 参数方程描述Rt R0 * exp(Bn * (1/T - 1/T0)) 已知温度求阻值 1/T 1/T0 (1/Bn) * ln(Rt/R0) 已知阻值求温度 T(℃) 1 / (1/T0 ln(Rt/R0)/Bn) - 273.15 输出摄氏度公式里的 T、T0 单位是开尔文K算完要减 273.15 才是摄氏度。教程给出的典型参数T025℃298.15K时 R010KΩ材料常数 Bn3950。注意教程讲解了 B 参数公式但配套工程没有用公式直接算温度而是使用厂家 R-T 表 查表插值见 3.4 节取舍在后面说明。2.2 分压电路阻值没法被 MCU 直接读取必须先转成电压3.3V ──── 10K 电阻 ────●──── NTC ──── GND │ Vout → GPIO36上臂是 10K 参考电阻对应代码里的NTC_RES下臂是 NTC分压点 Vout 接 ESP32 的 GPIO36原理图中的 SENSOR_VP 网络由分压公式Vout 3.3V × R_ntc / (10K R_ntc)反解R_ntc Vout × 10K / (3.3V − Vout)也就是代码res (voltage*NTC_RES)/(ADC_V_MAX-voltage)的由来。温度升高 → NTC 阻值下降 → Vout 下降25℃ 时 NTC 正好是 10KVout 1.65V工作点落在量程中间。2.3 ESP32 的 ADC 资源ESP32 有 ADC1、ADC2 两个转换模块每个 8 路通道。支持 ADC1 的 GPIO 只有这 8 个GPIOADC1 通道GPIOADC1 通道GPIO36VPCH0GPIO32CH4GPIO37CH1GPIO33CH5GPIO38CH2GPIO34CH6GPIO39VNCH3GPIO35CH7注意ADC2 与 WiFi 不能同时使用教程明确不推荐 ADC2本节只用 ADC1。这些引脚号是 ESP32 特有的换成 ESP32-S3 后 ADC1_CH0 对应的 GPIO 完全不同仓库里个别工程如 ledc已被切到 esp32s3但 ntc/sr04/sdcard 三个工程的sdkconfig目标仍是esp32。2.4 衰减与量程ESP32 内部 ADC 参考电压只有 1100mV要测更大电压必须设置衰减衰减设置可测范围ADC_ATTEN_DB_0100 ~ 950 mVADC_ATTEN_DB_2_5100 ~ 1250 mVADC_ATTEN_DB_6150 ~ 1750 mVADC_ATTEN_DB_12150 ~ 2450 mV注意公式里的 3300mV是分压电路上端的 3.3V 参考电压不是 ADC 量程。12dB 衰减实际只能正确测到约 2450mVNTC 在低温时阻值变大、Vout 可能超过 2450mV 而饱和读数停在 4095这是模拟测温的硬件边界。三、知识点3.1 单次转换与连续转换ESP-IDF 提供两类 ADC 驱动单次转换adc_oneshot_*按需触发一次读完就停连续转换adc_continuous_*用 DMA 不停搬运适合高速采样。教程作者实测连续转换精度差、易受其他通道影响因此本工程只用单次转换。3.2 分辨率决定原始值范围12 位分辨率意味着原始码值范围是 0 ~ 2¹²−1 0 ~ 4095输入 0mV 读数接近 0输入约 2450mV 读数到 4095超过量程则饱和为 4095、不会再变大。3.3 校准把原始值换成毫伏每颗芯片出厂时在 eFuse 里预烧了 ADC 校准参数adc_cali_raw_to_voltage()利用它们把原始码值换算成比较准确的毫伏值。ESP32 上优先使用曲线拟合Curve Fitting方案example_adc_calibration_init()在曲线拟合不可用时会退回线性拟合Line Fitting两者都不可用则返回 false、跳过校准。3.4 从阻值到温度查表 二分查找 线性插值难点不用 B 参数公式直接算温度的原因B 值本身有误差不同批次、不同厂家的 NTC 并不相同求温度要算对数在 MCU 上不划算而厂家会直接提供标定好的温度-阻值表。于是问题变成给我一个阻值怎么快速查到温度工程分三步建表结构体数组保存温度 该温度下的阻值从 -10℃ 到 50℃。温度越高阻值越小所以表的阻值是降序排列的二分查找表有 61 项遍历最坏要看 61 次二分查找最多 6 次比较就能定位线性插值算出的阻值几乎不可能正好等于表中某一项。把相邻两点 (X1,Y1)、(X2,Y2) 当作一条直线两点距离近时足够近似曲线用两点式求 k、b再代入 R 得到带小数的温度k (y2 - y1) / (x2 - x1) b (x2 * y1 - x1 * y2) / (x2 - x1) T k * R b注意教程与仓库代码的差异教程伪代码写的是R s_ntc_table[index].res且R s_ntc_table[index1].res把表当成阻值升序来夹逼。但仓库的s_ntc_table[]是阻值降序排列index 越大阻值越小find_ntc_index()实际返回的是满足s_ntc_table[index].res R的下标因此s_ntc_table[index1].res比 R 更小。代码实际是在 index 和 index1 两点上做直线近似因为相邻两点很近、曲线平缓误差很小但阅读时应按仓库的实际行为理解不要照抄教程的不等式。四、代码分析4.1 工程结构ntc/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt # idf_component_register(SRCS ntc.c main.c INCLUDE_DIRS .) │ ├── main.c # app_main()每秒打印一次温度 │ ├── ntc.c # ADC 初始化、采样任务、NTC 表、二分查找、线性插值 │ └── ntc.h # 对外提供 temp_ntc_init() 和 get_temp() └── sdkconfig # 目标芯片 esp324.2 关键宏定义#defineTEMP_ADC_CHANNELADC_CHANNEL_0//ADC转换通道(ADC1有8个通道对应gpio32 - gpio39)#defineNTC_RES10000//NTC电阻标称值(在电路中和NTC一起串进电路的那个电阻,一般是10K100K)#defineADC_V_MAX3300//最大接入电压值#defineADC_VALUE_NUM10//每次采样的电压ADC_CHANNEL_0在 ESP32 上对应 GPIO36与教程说的Vout 接到 GPIO36一致ADC_V_MAX的单位是 mV所以能和adc_cali_raw_to_voltage()输出的毫伏值直接做运算。4.3 初始化建单元、配通道、校准、建任务voidtemp_ntc_init(void){adc_oneshot_unit_init_cfg_tinit_config1{.unit_idADC_UNIT_1,//WIFI和ADC2无法同时启用这里选择ADC1};//启用单次转换模式ESP_ERROR_CHECK(adc_oneshot_new_unit(init_config1,s_adc_handle));//-------------ADC1 Config---------------//adc_oneshot_chan_cfg_tconfig{.bitwidthADC_BITWIDTH_12,//分辨率.attenADC_ATTEN_DB_12,//衰减倍数ESP32设计的ADC参考电压为1100mV,只能测量0-1100mV之间的电压如果要测量更大范围的电压//需要设置衰减倍数下面在源码里还有一段可测量范围注释内容与 2.4 节表格相同此处从略};ESP_ERROR_CHECK(adc_oneshot_config_channel(s_adc_handle,TEMP_ADC_CHANNEL,config));//-------------ADC1 Calibration Init---------------//do_calibration1example_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);}流程是四步创建 ADC1 单次转换单元 → 配置通道12 位、12dB 衰减→ 初始化校准 → 创建采样任务优先级 2、绑定内核 1、栈 2048 字节。校准函数内部按曲线拟合 → 线性拟合的顺序尝试成功返回 true失败打印警告并返回 false。4.4 采样任务10 次平均滤波 换算staticvoidtemp_adc_task(void*param){uint16_tadc_cnt0;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_cnt10){inti0;//用平均值进行滤波uint32_tvoltage0;uint32_tres0;for(i0;iADC_VALUE_NUM;i){voltages_voltage_raw[i];}voltagevoltage/ADC_VALUE_NUM;if(voltageADC_V_MAX){//电压转换为相应的电阻值res(voltage*NTC_RES)/(ADC_V_MAX-voltage);//根据电阻值查表找出对应的温度s_temp_valueget_ntc_temp(res);}adc_cnt0;}vTaskDelay(pdMS_TO_TICKS(100));}}每轮采一次原始值存入s_adc_raw[]校准成功才把毫伏值存入s_voltage_raw[]采满 10 次求平均抑制噪声教程说的用平均值进行滤波再用分压公式算阻值、查表得温度。voltage ADC_V_MAX既是防除零也是防异常数据每轮延时 100ms所以温度大约每秒更新一次。app_main()main.c只做两件事调用temp_ntc_init()然后每 1000ms 打印一次get_temp()温度的计算全部发生在采样任务里两者通过 volatile 变量s_temp_value通信。4.5 NTC 表、二分查找与线性插值表用结构体数组保存每一项是temp_res_t成员tempint8_t温度和resuint32_t阻值。表共 61 项从 -10℃ 到 50℃下面只摘录几项完整表见ntc.cstaticconsttemp_res_ts_ntc_table[]{{-10,51815},//……{25,10000},//……{50,3592},};二分查找因为表按阻值降序排列可以直接折半收缩区间。/** 二分查找通过电阻值查找出温度值对应的下标 * param res 电阻值left/right 为 ntc 表的左右边界 * return 温度值数组下标 */staticintfind_ntc_index(uint32_tres,uint16_tleft,uint16_tright){uint16_tmiddle(leftright)/2;if(rightleft||leftmiddle){if(ress_ntc_table[left].res)returnleft;elsereturnright;}if(ress_ntc_table[middle].res){rightmiddle;returnfind_ntc_index(res,left,right);}elseif(ress_ntc_table[middle].res){leftmiddle;returnfind_ntc_index(res,left,right);}elsereturnmiddle;}线性插值由linera_interpolation()实现算法就是 3.4 节的两点式函数名少了一个字母 n仓库原样如此返回值是带小数的温度。//根据电阻值、NTC表查出对应温度staticfloatget_ntc_temp(uint32_tres){uint16_tleft0;uint16_trights_us_ntc_table_num-1;intindexfind_ntc_index(res,left,right);if(ress_ntc_table[index].res)returns_ntc_table[index].temp;if(index0||indexs_us_ntc_table_num-1){returns_ntc_table[index].temp;}else{intnext_indexindex1;returnlinera_interpolation(res,s_ntc_table[index].res,s_ntc_table[next_index].res,s_ntc_table[index].temp,s_ntc_table[next_index].temp);}}get_ntc_temp()处理三种情况阻值正好命中表中某一项 → 直接返回整数温度落到了表的边界index 为 0 或最后一项→ 返回端点温度其余情况 → 在 index 与 index1 两点之间做线性插值。五、关键 API 速查API作用adc_oneshot_new_unit()创建 ADC 单次转换单元选 ADC1/ADC2拿到句柄adc_oneshot_config_channel()配置指定通道的分辨率与衰减adc_oneshot_read()触发一次转换读出原始码值0~4095adc_cali_create_scheme_curve_fitting()用芯片 eFuse 参数建立曲线拟合校准方案adc_cali_raw_to_voltage()原始码值 → 毫伏值xTaskCreatePinnedToCore()创建任务并绑定到指定内核vTaskDelay(pdMS_TO_TICKS(ms))毫秒级延时六、编译与现象验证cdesp32-board/ntc# 工程当前配置的目标就是 esp32idf.py build idf.py-pCOM端口flash monitor启动日志先打印校准信息如adc: calibration scheme version is Curve Fitting、adc: Calibration Success之后每秒打印一行I (xxxx) main: current temp:25.32。用手指捏住 NTC读数会缓慢上升松手后慢慢回落。