ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

树莓派Pico ADC测温实战:从NTC原理到误差校准全解析

树莓派Pico ADC测温实战:从NTC原理到误差校准全解析 1. 项目缘起为什么用树莓派 Pico 测温度最近在折腾一个需要多点温度监控的小项目手头正好有几块闲置的树莓派 Pico。这玩意儿核心是 RP2040 微控制器自带几个 12 位的 ADC模数转换器通道。我就在想能不能直接用它的 ADC 来读取温度传感器的数据省去外接专用温度芯片的麻烦和成本毕竟对于很多精度要求不那么苛刻的场景比如环境温度监测、设备散热预警一个简单的 ADC 方案可能更直接、更“够用”。这个想法其实挺自然的。很多嵌入式开发者无论是玩 Arduino、STM32 还是 ESP32都接触过用 ADC 配合热敏电阻NTC或热电偶来测温的方案。树莓派 Pico 作为一款性价比极高的 MCU 开发板其 ADC 性能如何用它来测温到底靠不靠谱过程中有哪些坑这正是我这次想和大家深入聊聊的。我会从原理、硬件选型、代码实现、误差处理到实际应用一步步拆解目标是让你看完就能自己动手搭一个稳定可用的 Pico 温度测量系统。2. 核心原理ADC 如何“感知”温度要理解用 ADC 测温得先抛开“温度传感器”这个抽象概念回到最基础的物理量变化。温度本身不能直接被 ADC 读取ADC 只能读取电压。所以测温的核心是找到一个随温度变化而线性或近似线性变化的电气参数最常见的就是电阻。2.1 热敏电阻最亲民的温度敏感元件在 DIY 和成本敏感的项目里负温度系数热敏电阻绝对是主角。它的电阻值会随着温度升高而降低关系是非线性的但可以通过查表或公式计算来对应温度。为什么选 NTC 而不是 PTC正温度系数对于常见的测温范围比如-40°C到125°CNTC 的灵敏度更高电阻变化范围大更容易被 ADC 分辨出来而且价格极其低廉。一个常用的型号是NTC 10K B值 3950意思是它在 25°C 时电阻为 10KΩ其 B 值一个描述材料特性的常数为 3950。这个型号资料丰富网上有现成的电阻-温度对应表用起来非常方便。2.2 分压电路将电阻变化转为电压变化ADC 测的是电压而 NTC 是电阻所以我们需要一个简单的分压电路来完成转换。这是整个硬件设计的核心。典型的接法是将 NTC 与一个精度较高的固定电阻称为上拉或下拉电阻串联在它们中间连接点引出电压送到 ADC 引脚。电路的一端接参考电压比如 Pico 的 3.3V另一端接地。3.3V (ADC_VREF) --- [固定电阻 R_fixed] --- V_ADC (连接到 Pico ADC 引脚) --- [NTC] --- GND这里有个关键选择固定电阻 R_fixed 的阻值该用多大很多人随手就用一个 10KΩ因为 NTC 在 25°C 时就是 10KΩ这样在常温下 V_ADC 正好是 1.65V处于 ADC 量程中间看起来很美。但这其实不是最优解。注意固定电阻的选取目标是让整个测温范围内V_ADC 的变化范围尽可能大以充分利用 ADC 的分辨率减小量化误差。对于 NTC 10K B3950如果你主要关心中低温段例如 0-50°C用一个 10KΩ 固定电阻没问题。但如果你希望在整个 NTC 的有效范围内比如-20°C到100°C都有较好的线性度和分辨率经过计算阻值在 5KΩ 到 8.2KΩ 左右的固定电阻往往能提供更均匀的电压输出跨度。我手头有 5.1KΩ 的 1% 精度金属膜电阻实测下来效果不错。ADC 读取到的原始值adc_raw是一个 0-4095 的数字对应 12 位分辨率。V_ADC 电压的计算公式为V_ADC (adc_raw / 4095) * ADC_VREF其中ADC_VREF是 ADC 的参考电压对于 Pico默认是供电电压 3.3V但这个电压本身可能有波动这是误差来源之一。知道了 V_ADC根据分压公式就能反推出 NTC 的当前电阻值R_ntcR_ntc R_fixed * (ADC_VREF / V_ADC - 1)或者R_ntc R_fixed * (4095 / adc_raw - 1)2.3 从电阻到温度查表法与公式法得到R_ntc后就需要把它转换成温度。有两种主流方法1. 查表法这是最准确、计算最快的方法。提前根据 NTC 的数据手册将一系列温度点对应的电阻值或者电阻值对应的温度做成一个数组表存储在程序里。测量时通过查找当前电阻值在表中相邻的两个点进行线性插值得到温度值。对于 B3950 10K NTC网上很容易找到现成的、步长为 1°C 或 0.1°C 的表格。在资源有限的 MCU 上这是首选。2. 公式法Steinhart-Hart 方程这是一个描述 NTC 电阻与温度关系的经验公式比简单的 B 值公式更精确尤其适用于宽温区。1/T A B * ln(R) C * [ln(R)]^3其中 T 是开尔文温度R 是 NTC 电阻A, B, C 是元件特定的系数可以从数据手册中获得。这个方法需要浮点数运算和自然对数计算对 PicoRP2040来说负担不重但比查表法慢一些。如果找不到精确的 A,B,C 系数使用简化的 B 值公式1/T 1/T0 (1/B) * ln(R/R0)也是可以的其中 T0 是参考温度如 298.15K 即 25°CR0 是该温度下的电阻如 10KΩ。在我的实现里为了兼顾精度和方便我选择了查表法。我找到了一个从 -40°C 到 125°C步长 1°C 的电阻表将其转换成adc_raw值直接存入数组省去了运行时计算电阻的步骤速度更快。3. 硬件搭建细节决定精度原理清楚了动手搭电路。别看电路简单几个细节没处理好测量结果可能飘得亲妈都不认识。3.1 元器件选择与电路连接树莓派 Pico任何版本均可引脚定义一致。NTC 热敏电阻我选用的是MF52-103/3950即 10KΩ B3950玻璃封装的比环氧树脂封装的稳定性稍好。固定电阻一颗5.1KΩ精度1%温度系数50ppm的金属膜电阻。精度很重要1%是起码要求温度系数影响相对次之。连接线尽量短。如果传感器需要引线到较远位置建议使用屏蔽线或双绞线并将屏蔽层单点接地在 Pico 端以减少干扰。滤波电容在 ADC 输入引脚与地之间并联一个 0.1uF 的陶瓷电容。这个电容非常关键它可以滤除电源噪声和引线引入的高频干扰使 ADC 读数更稳定。位置要尽量靠近 Pico 的 ADC 引脚。最终电路连接如下Pico 的3V3(OUT)引脚第36脚连接到固定电阻一端。固定电阻另一端连接到ADC 输入引脚我选用的是GP26对应 ADC0和 NTC 的一端。NTC 的另一端连接到 Pico 的GND任意一个接地脚。在GP26引脚与GND之间焊接上0.1uF (104)的陶瓷电容。可选如果你担心 3.3V 电源的噪声可以在 3V3(OUT) 和 GND 之间也加一个 10uF 的电解电容或钽电容。3.2 关于参考电压的深入讨论这是影响 ADC 精度的最核心因素之一。Pico 的 ADC 参考电压默认来自内部的3.3V LDO 输出。这个电压并非绝对稳定负载变化当板上其他电路如 LED、外设工作电流变化时3.3V 会有微小波动。温度变化LDO 本身输出电压随温度漂移。初始精度出厂就有一定误差。对于要求不高的温度测量±2°C 以内可以接受这个误差。但如果想提升精度有两大改进方向方向一使用外部精密电压基准源这是最专业的方法。你可以断开 ADC 的内部参考通过 Pico 的ADC_VREF引脚第35脚接入一个外部的、高精度、低温漂的基准电压芯片例如REF30333.3V精度0.1%。这样无论主电源如何波动ADC 的参考电压都是稳定的测量结果自然准确。不过这会增加成本和复杂度。方向二测量参考电压进行软件补偿一个更取巧且低成本的方法是用 ADC 测量它自己的参考电压。Pico 有一个特殊的 ADC 通道连接到内部一个由固定电压驱动的分压器这个固定电压是相对稳定的。通过读取这个通道的值可以反推出当前实际的 3.3V 电压是多少然后在计算其他通道电压时进行补偿。在 MicroPython 中可以读取ADC(4)来获取这个值。在 C/C SDK 中也有相应的函数。我后续的代码示例会展示这种方法。3.3 布局与接地要点模拟地与数字地Pico 没有独立的模拟地引脚但 PCB 布局上已将模拟电路部分的地进行了优化。我们搭建外部电路时所有模拟部分分压电路、滤波电容的接地线应最终汇集到Pico 的同一个 GND 引脚上避免形成地环路。远离噪声源尽量让分压电路远离 Pico 的开关电源电路、高频晶振以及 GPIO 快速翻转的区域。电源去耦除了在 ADC 引脚加小电容在 Pico 的 3.3V 和 GND 入口处增加一个更大的电容如 100uF也有助于稳定整体电源。4. 软件实现MicroPython 与 C SDK 双视角硬件准备好了我们来写代码。我会分别用 MicroPython快速原型和 C SDK高性能应用两种方式实现。4.1 MicroPython 实现快速上手MicroPython 语法简单交互性强非常适合验证想法和快速开发。import machine import math import time # 初始化 ADC使用 GP26 (ADC0) adc machine.ADC(26) # 初始化 ADC(4) 用于测量内部参考电压实现补偿 adc_vref machine.ADC(4) # NTC 参数 R_FIXED 5100.0 # 固定电阻 5.1K NTC_R0 10000.0 # NTC在25°C时的电阻 10K T0 25.0 273.15 # 参考温度单位开尔文 B_VALUE 3950.0 # B值 # 简易中值滤波函数 def median_filter(samples): samples.sort() return samples[len(samples)//2] def read_temperature_mp(sample_count11): 读取温度 - MicroPython版本 sample_count: 采样次数取中值建议为奇数 adc_samples [] vref_samples [] for _ in range(sample_count): adc_samples.append(adc.read_u16()) # 读取16位值 (0-65535) vref_samples.append(adc_vref.read_u16()) time.sleep_us(100) # 短暂延时避免采样过快 adc_raw median_filter(adc_samples) vref_raw median_filter(vref_samples) # 计算实际的参考电压 (3.3V 的理想值对应 vref_raw 的理论值) # 根据手册ADC(4) 连接到内部一个 ~0.706 * VREF 的分压。 # 因此实际 VREF (vref_raw / 65535) * VREF / 0.706 # 简化处理我们计算一个比例因子 # 理论情况下如果 VREF 正好是 3.3V那么 vref_raw 应该是一个固定值。 # 我们采用经验法vref_raw 通常在 52600 左右。用此值来校准。 VREF_THEORY 3.3 VREF_RAW_IDEAL 52600.0 # 这是一个需要根据实际板子校准的经验值 vref_correction vref_raw / VREF_RAW_IDEAL actual_vref VREF_THEORY * vref_correction # 计算 NTC 两端电压 # 注意adc.read_u16() 返回的是 16 位值但 ADC 是 12 位所以值范围是 0-65535对应 0-3.3V voltage (adc_raw / 65535.0) * actual_vref # 计算 NTC 电阻 if voltage 0: return None # 避免除零错误 r_ntc R_FIXED * (actual_vref / voltage - 1) # 使用 B 值公式计算温度 (开尔文) steinhart (1.0 / T0) (1.0 / B_VALUE) * math.log(r_ntc / NTC_R0) steinhart 1.0 / steinhart # 转换为摄氏度 temp_c steinhart - 273.15 return temp_c # 主循环 while True: temp read_temperature_mp() if temp is not None: print(温度: {:.2f} °C.format(temp)) time.sleep(2)代码解读与注意事项中值滤波这是处理 ADC 读数跳动的有效且简单的方法。采集奇数个样本如11个排序后取中间值可以很好地抑制偶发的尖峰噪声。参考电压补偿我们通过ADC(4)读取了内部参考电压相关的读数vref_raw。通过一个经验值VREF_RAW_IDEAL进行比例校准从而得到更接近真实的actual_vref。这个VREF_RAW_IDEAL需要校准方法是在已知精确电压比如用万用表测得的稳定3.3V下运行程序打印出vref_raw的值将其作为理想值填入。公式计算使用了简化的 B 值公式。对于更高精度可以换成完整的 Steinhart-Hart 公式或查表法。4.2 C SDK 实现追求性能与精度对于需要高速采样、低功耗或更复杂控制的项目使用 C/C 和 Pico 的官方 SDK 是更好的选择。#include stdio.h #include pico/stdlib.h #include hardware/adc.h #define FIXED_RESISTOR 5100.0f #define NTC_R0 10000.0f #define T0 (25.0f 273.15f) #define B_VALUE 3950.0f // 查表法示例片段仅包含部分数据 static const float temp_table[] { -40.0, -30.0, -20.0, -10.0, 0.0, 10.0, 20.0, 25.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0, 110.0, 120.0, 125.0 }; static const uint16_t adc_table[] { /* 这里需要预先计算或测量出对应温度下的ADC原始值 */ }; #define TABLE_SIZE (sizeof(temp_table)/sizeof(temp_table[0])) uint16_t read_adc_median(uint8_t channel, uint8_t samples) { uint16_t adc_values[samples]; for (int i 0; i samples; i) { adc_select_input(channel); sleep_us(100); // 等待通道切换稳定 adc_values[i] adc_read(); } // 简易冒泡排序找中值 (samples 应为奇数) for (int i 0; i samples - 1; i) { for (int j 0; j samples - i - 1; j) { if (adc_values[j] adc_values[j 1]) { uint16_t temp adc_values[j]; adc_values[j] adc_values[j 1]; adc_values[j 1] temp; } } } return adc_values[samples / 2]; } float calculate_temperature_c(uint16_t adc_raw, uint16_t vref_raw) { // 1. 计算实际参考电压 (校准) const float VREF_THEORY 3.3f; const uint16_t VREF_RAW_IDEAL 52600; // 需校准 float vref_correction (float)vref_raw / VREF_RAW_IDEAL; float actual_vref VREF_THEORY * vref_correction; // 2. 计算电压和电阻 float voltage ((float)adc_raw / 4095.0f) * actual_vref; // 注意C SDK的adc_read()返回0-4095 if (voltage 0.0f || voltage actual_vref) { return -273.15f; // 错误值 } float r_ntc FIXED_RESISTOR * (actual_vref / voltage - 1.0f); // 3. 使用查表法 线性插值 for (int i 0; i TABLE_SIZE - 1; i) { if (adc_raw adc_table[i1] adc_raw adc_table[i]) { // 注意ADC值随温度升高而减小 // 线性插值 float temp_range temp_table[i1] - temp_table[i]; float adc_range (float)(adc_table[i1] - adc_table[i]); float adc_offset (float)(adc_raw - adc_table[i]); return temp_table[i] (temp_range / adc_range) * adc_offset; } } // 如果超出表格范围退回使用B值公式计算 float steinhart (1.0f / T0) (1.0f / B_VALUE) * logf(r_ntc / NTC_R0); steinhart 1.0f / steinhart; return steinhart - 273.15f; } int main() { stdio_init_all(); // 初始化ADC adc_init(); adc_gpio_init(26); // 使用GP26 (ADC0) adc_set_temp_sensor_enabled(true); // 如果需要读取内部温度传感器可以启用但这里我们不用它 while (true) { // 读取传感器通道 (GP26, ADC0) 和内部参考电压通道 (ADC4) uint16_t sensor_raw read_adc_median(0, 11); // 通道0采样11次取中值 uint16_t vref_raw read_adc_median(4, 11); // 通道4采样11次取中值 float temperature calculate_temperature_c(sensor_raw, vref_raw); printf(ADC Raw: %u, Vref Raw: %u, Temperature: %.2f C\n, sensor_raw, vref_raw, temperature); sleep_ms(2000); } return 0; }C 版本关键点性能与效率直接操作硬件寄存器速度远快于 MicroPython。中值滤波和查表运算效率极高。查表法实现示例展示了查表加线性插值的思路。你需要预先根据你的 NTC 参数和电路计算出每个温度点对应的adc_raw值填充adc_table数组。这是最精确的方法。ADC 读取adc_read()返回的是0-4095的 12 位原始值与 MicroPython 的read_u16()范围不同计算时需注意。错误处理加入了电压边界检查防止除零或计算溢出。5. 误差分析与校准让读数更可信即使电路和代码都正确测量值也可能和实际温度有偏差。我们需要系统地分析和减小这些误差。5.1 主要误差来源误差来源影响描述缓解方法ADC 参考电压误差3.3V LDO 的初始误差、温漂、负载调整率。这是最大误差源之一。1. 使用外部精密电压基准。2. 使用ADC(4)进行软件补偿校准。固定电阻误差1% 精度的电阻在 5.1KΩ 上就可能带来 ±51Ω 的绝对误差直接影响分压比。选用更高精度如 0.1%的电阻或进行单个电阻的实测校准。NTC 参数离散性B 值和 R25 值存在公差通常 B 值 ±1%R25 ±5%。购买精度更高的 NTC或进行两点校准。ADC 非线性与积分误差ADC 本身并非理想的线性器件存在微分非线性DNL和积分非线性INL误差。RP2040 的 ADC 典型 INL 为 ±2 LSB。软件上难以完全消除但通过滤波和平均可以平滑其影响。自热效应流过 NTC 的电流会导致其自身发热。Pico 的 ADC 输入阻抗并非无穷大也会吸取微小电流。增大分压电阻阻值以减小电流但会降低信噪比。在软件中仅在采样时给分压电路上电需要额外 GPIO 控制平时断电。环境噪声电源纹波、数字电路开关噪声耦合到模拟线路。良好的滤波电容0.1uF 就近并联、合理的布线、远离噪声源。5.2 两点校准法大幅提升绝对精度对于大多数应用我们更关心测量的绝对精度即读数和真实温度的差值。两点校准是提升绝对精度最有效的方法。你需要准备一个你认为可信的温度参考源。比如冰水混合物标准 0°C 环境。将传感器与温度计探针一起插入冰水混合的保温杯中确保充分接触且不结冰。恒温水浴/油浴提供稳定的已知温度点如 50°C。高精度温度计用于测量传感器所处环境的实际温度。校准步骤低温点校准如 0°C将传感器置于 0°C 环境等待读数稳定至少5分钟。记录程序输出的原始 ADC 值adc_raw_low和vref_raw_low以及实际的参考温度T_low_true(0.0)。高温点校准如 50°C同理将传感器置于 50°C 环境稳定后记录adc_raw_highvref_raw_high和T_high_true(50.0)。软件修正在计算函数中不再使用理论上的 NTC_R0 和 B_VALUE而是利用这两个实测点反向拟合出适用于你这套具体硬件包含了电阻误差、NTC离散性、参考电压误差的“等效”R0 和 B 值或者直接建立一条两点间的线性或指数校正曲线。简易线性补偿示例在得到电阻值 R_ntc 后假设你在T_low_true时测得的电阻为R_low在T_high_true时测得的电阻为R_high。 你可以用这两个点对通过公式计算出的温度进行线性偏移补偿。更精确的做法是将(1/T, ln(R))两个点代入 B 值公式解算出你这颗 NTC 的实际 B 值和实际 R0。网上有很多现成的两点校准计算工具。实操心得我自己的校准方法是先用高精度万用表测量我使用的 5.1KΩ 固定电阻的实际阻值比如是 5097Ω替换代码中的理论值。然后将传感器和校准用的热电偶探头紧贴在一起放入可控温的加热台中在 25°C 和 75°C 两个点记录 ADC 读数。用这两个点去反推 NTC 的实际参数更新到代码里。经过这样校准后在 20-80°C 范围内我的系统误差可以控制在 ±0.5°C 以内完全满足项目需求。6. 进阶应用与优化思路一个基础的测温系统工作后可以考虑以下方向进行优化和扩展6.1 多通道与分布式测量RP2040 有 5 个 ADC 通道GP26, GP27, GP28, GP29 和内部温度传感器。你可以轻松扩展为4 路外部温度监测。只需复制分压电路连接到不同的 ADC 引脚即可。在代码中循环读取不同通道。对于更远的距离1米建议考虑以下方案以增强抗干扰能力使用屏蔽电缆。在传感器端增加一个电压跟随器运算放大器利用运放的高输入阻抗、低输出阻抗特性将信号与长导线隔离。采用差分信号传输需要更多线缆和电路或直接改用数字接口的温度传感器如 DS18B20后者在长距离抗干扰方面有天然优势。6.2 低功耗设计如果你的设备是电池供电需要降低功耗间歇采样大部分时间让 MCU 和传感器电路进入睡眠模式定时唤醒测量。切断传感器电源用一个 GPIO 口控制一个 MOS 管为分压电路供电。仅在需要测量时通电彻底消除自热效应和静态电流消耗。降低采样速率ADC 采样速率越高功耗越大。对于缓慢变化的温度每秒采样一次都绰绰有余。6.3 与内部温度传感器对比RP2040 芯片内部有一个温度传感器可以通过ADC(4)在特定模式下读取。但这个传感器主要是用来监测芯片结温的精度较差典型 ±3°C且受芯片自身发热影响巨大。它不适合测量环境温度。我们的外部 NTC 方案在精度和稳定性上远超内部传感器。6.4 数据记录与上传将测量到的温度数据存储或上传才能发挥其价值。本地存储Pico 没有内置 Flash但可以通过 SPI 接口连接一个 MicroSD 卡模块将带时间戳的温度数据写入 CSV 文件。无线传输给 Pico 配上 WiFi 模块如 ESP-01S或者蓝牙模块可以将数据发送到手机、电脑或云平台如 ThingsBoard、Home Assistant。有线传输通过串口UART实时将数据发送给上位机电脑进行处理和显示。7. 常见问题排查QA在实际操作中你可能会遇到以下问题Q1ADC 读数跳动很大不稳定。A首先检查硬件ADC 输入引脚是否并联了0.1uF 滤波电容电容是否尽量靠近 Pico 引脚电源是否稳定尝试用示波器观察 ADC 引脚波形。其次检查软件是否实施了软件滤波如中值滤波、移动平均采样间隔是否太短可以尝试在adc_read()之间增加微小延时 (sleep_us(50-100))。Q2测量值整体偏高或偏低且不随温度变化。A这很可能是电路接错了。用万用表检查固定电阻和 NTC 的连接顺序是否正确ADC 引脚是否确实连接在两者的连接点上3.3V 和 GND 是否接反固定电阻值是否用错比如误用了 100KΩQ3温度变化时读数反应迟钝。ANTC 本身的热响应时间。玻璃封装的比环氧树脂的快。确保传感器与被测物体/介质良好接触使用导热硅脂。自热效应也会导致响应变慢和读数偏高检查流经 NTC 的电流是否过大。Q4如何测量 100°C 以上的高温A普通 NTC如 MF58的长期工作温度上限通常在 125°C~150°C。对于更高温度需要选用高温型 NTC或热电偶。热电偶需要配合热电偶放大器如 MAX6675, MAX31855使用它们输出数字信号或模拟信号后者可以接 ADC。另一种方案是使用PT100/PT1000 铂电阻其线性度好、范围广但需要更复杂的恒流源驱动和放大电路。Q5代码编译/下载到 Pico 后没反应。A检查接线是否牢固Pico 是否正常供电USB 或外部 5V。对于 C 项目确认CMakeLists.txt配置正确并正确选择了编译目标。最简单的验证方法是先跑一个点亮 LED 的例程确保开发环境没问题。折腾完这一套你应该对用树莓派 Pico 的 ADC 进行温度测量有了从理论到实践的完整认识。这个方案成本极低灵活性高虽然绝对精度可能比不上专业的数字温度传感器但在很多工业和家居应用场景如温室大棚、鱼缸、3D打印机热床、设备机箱中已经完全够用并且其过程本身对于理解模拟信号采集、传感器特性、误差分析和嵌入式系统设计非常有价值。
返回列表