先说个我自己的经历。第一次拿树莓派 Pico 玩 ADC,接了个 10k 电位器到 ADC0,读回来的数值在 18000 到 21000 之间乱跳,一开始我还以为板子坏了或者线没接好。后来把输入阻抗、参考电压、SAR 采样原理这些东西捋清楚,才发现这颗芯片的 ADC 其实没那么玄,但也确实没那么省心。这篇文章就围绕 machine.ADC 展开,从 API 细节讲到定时温度采集的完整实现,最后重点说说我在 ISR 里踩过的那些坑。无论你是刚拿到 Pico 想跑通第一个模拟量读取,还是已经在做数据采集项目、被中断回调折磨得头疼,这篇应该都能给你一些直接能用的东西。
1. 先搞清 Pico 的 ADC 到底是个什么水平
1.1 引脚和物理特性:不是所有 GPIO 都能当模拟输入
树莓派 Pico 用的 RP2040 芯片内部集成的是一个 12 位 SAR ADC,即逐次逼近型模数转换器。很多初学者拿到板子第一件事是翻引脚图,看了一圈发现只有 GPIO26、GPIO27、GPIO28 三个引脚标了 ADC0、ADC1、ADC2,就以为 ADC 资源很少。实际上板子上还有一个 GPIO29 对应 ADC3,只是它默认被板载电路占用,接到了 VSYS 电源分压网络上,用来检测系统供电电压。
所以你在 Pico 上能直接用的 ADC 输入其实是四个:ADC0(GPIO26)、ADC1(GPIO27)、ADC2(GPIO28)、ADC3(GPIO29,测 VSYS)。但在绝大多数教程和项目里,大家默认用的就是前三个,GPIO29 更多是作为系统电源监控来用,不适合当普通外部输入。
还有一点要特别注意:ADC 输入电压范围是 0 到 3.3V。RP2040 不是那种带 ±5V 输入容限的芯片,引脚内部虽然有钳位二极管,但长期输入超过 3.3V 的话,轻则读数异常、重则烧引脚。我之前见过一个朋友直接把一个 5V 供电的电位器输出接到 ADC0,读数直接顶到 65535 然后开始乱跳,还好没把芯片烧了,但这种操作风险极大。如果非要去测 5V 甚至更高电压,必须加电阻分压,把信号缩放到 0-3.3V 范围内。
再说分辨率。RP2040 的 ADC 是 12 位的,理论上输出 0~4095。这个精度在单片机里算中规中矩,STM32 的很多型号也是 12 位。做温度采集、电位器旋钮、光敏电阻这类慢速信号完全够用,但如果你想着拿它去做称重仪表或者高精度传感器采集,那就要好好掂量一下噪声和参考电压的问题了。
1.2 12 位分辨率与 read_u16 的 16 位返回值:一个老生常谈的误区
在 machine.ADC 这个 API 里,最常用的读取方法是 read_u16()。这个函数名很容易让人产生误会:既然返回的是 16 位无符号整数(0~65535),那是不是说 Pico 的 ADC 是 16 位分辨率?
不是。RP2040 的 ADC 硬件只有 12 位,read_u16() 是把 12 位的原始值左移 4 位,凑成了一个 16 位的数值范围。也就是说,默认最低的 4 位其实永远是 0。你读到 32768 和读到 32767 的区别,反映的是硬件 12 位分辨率下不同的量化结果,而不是芯片真的能分辨出 65536 个层级。
这个设计其实是 MicroPython 为了跨平台统一 API 行为。其他开发板上 machine.ADC 返回 0~65535,那 Pico 也返回同样范围,这样同样的代码在不同板子上逻辑是一致的。但如果你要算电压,就知道不能把 65535 当成 16 位满量程精度去宣传。
换算电压的公式很简单:
voltage = raw / 65535 * 3.3但这里的 3.3 是一个理想值。Pico 板载的 3.3V 是由 RT6150 这类 DC-DC 或 LDO 产生的,实际输出电压可能在 3.25V 到 3.35V 之间浮动,具体取决于 USB 供电质量。如果你要测量的是绝对电压,这个误差会直接反映到结果里。后面我会讲到这个问题怎么处理。
1.3 参考电压与 ADC_VREF:系统误差的最大来源
Pico 板上有个引脚叫 ADC_VREF,是 ADC 的参考电压输入。默认情况下这个引脚和板上的 3.3V 电源是连通的,所以你直接用 3.3 去换算问题不大。但如果你的项目对绝对电压精度有要求,可以把这个引脚与板载 3.3V 的连接切断,然后外接一个高精度基准电压源,比如 3.0V 或 3.3V 的参考芯片。
不过这里有个非常关键的点:如果你的测量对象是电阻型传感器,比如 NTC 热敏电阻、光敏电阻,它们通常和固定电阻组成分压电路,分压电源和 ADC 参考电压用的是同一个 3.3V,那么参考电压误差理论上会被抵消掉。因为 ADC 读到的数值是输入电压和参考电压的比例,而分压点电压本身就是 3.3V 按电阻比分配出来的。所以无论 3.3V 实际是 3.2 还是 3.4,ADC 读数都只反映电阻比例,温度计算不会受到参考电压影响。这个特性在做温度采集时特别有用,后面实战部分会再验证一遍。
ADC 的采样原理也需要理解一下。SAR ADC 内部有一个采样保持电容,转换开始前要先给电容充电,让电容电压跟随输入信号。如果信号源输出阻抗太高,电容充电时间不够,读数就会偏低,而且信号变化越快时误差越明显。这就是为什么 ADC 输入前端要尽量低阻抗。如果传感器的输出阻抗很高,一个常见的做法是在 ADC 引脚和地之间并联一个 100nF 电容,配合内部采样电容构成低通滤波,可以让读数稳定很多。但要注意,加了电容之后信号响应会变慢,对于温度这种慢变信号没问题,测音频或快速变化信号就要重新评估。
2. machine.ADC 的正确打开方式
2.1 最小读取代码与电压换算
在 MicroPython 里使用 Pico 的 ADC 就这么几行:
from machine import ADC, Pin adc = ADC(Pin(26)) # 用 Pin 对象构造 raw = adc.read_u16() # 返回 0~65535 voltage = raw / 65535 * 3.3 # 换算成电压 print("raw:", raw, "voltage:", voltage)也可以直接用数字构造:
adc = ADC(0) # 0 对应 GPIO26推荐用 ADC(26) 这种把 GPIO 编号和 ADC 通道对应起来的方式,写代码的时候不容易搞混。有些固件版本还支持 read_uv() 直接返回微伏单位的值,省去手动换算,但要注意这需要比较新的 MicroPython 版本,老版本固件没有这个函数。
read_u16() 读到的原始值除了用来换算电压,还可以直接用来做电阻比例计算。比如下面这段代码,就是读取 NTC 分压点的 ADC 值并反推 NTC 的当前电阻值:
from machine import ADC, Pin import math adc = ADC(Pin(26)) R_FIXED = 10000 # 固定电阻 10k def read_ntc_resistance(): raw = adc.read_u16() if raw == 0: return float('inf') if raw >= 65535: return 0.0 # 分压电路中固定电阻接在 3.3V 和测量点之间,NTC 接在测量点和 GND 之间 ratio = raw / 65535 r_ntc = R_FIXED * ratio / (1 - ratio) return r_ntc这个公式里完全没有出现 3.3V 这个参数,原因就是前面说的:ADC 读数本质是比例值,电阻反推只需要比例关系。这是很多教程里没讲透的一个点。
2.2 多通道切换的坑:为什么忽然读到上一个通道的值
如果你需要轮流读 ADC0、ADC1、ADC2,可能会遇到一个怪异现象:从某一路切换过去之后,第一次读到的数据明显偏大或偏小,像是还残留着上一通道的信号。这就是 ADC 输入多路复用器的通道切换问题。
SAR ADC 在转换之前,内部采样电容要先接到当前选中的通道上充电。切换通道后,如果你立刻发起转换,电容可能还没来得及完全跟上新通道的信号,读到的自然不是准确的电压。RP2040 的数据手册里也提到了通道切换后需要一定的建立时间。
在实际使用中,最简单的对策是切换通道后先丢弃第一次采样,再进行正式读取:各自的底层适配会有差异,但整体思路是一样的。我自己习惯封装一个函数,每次读某个通道时连续读两次,把第一次结果丢掉,第二次才返回:
def read_adc_channel(adc_obj): adc_obj.read_u16() # 丢弃切换后的第一次采样 return adc_obj.read_u16()这个"丢第一次"的做法在工业采集里非常常见,不只是 Pico,很多单片机 ADC 切换通道后都有类似问题。如果你看完代码觉得"多读一次是不是浪费",那点浪费换来的稳定性绝对值回票价。
再者,不要试图在一个程序里用极短的间隔去频繁切换 ADC 通道。如果你用同一个 ADC 对象交替读三四个通道,而且每次只间隔几十微秒,出来的数据很可能互相串扰。更好的做法是每个 ADC 通道独立分配一个 ADC 对象,每个对象在构造之后就绑定对应的 GPIO,读取互不干扰。当然这只是一种工程习惯,实际是否必要要看具体场景和数据质量。
2.3 软件滤波与过采样:给读数"去毛刺"
ADC 原始读数抖动是必然的,尤其是低成本的板载 LDO 供电时,纹波和开关噪声都会反馈到采样结果上。别指望硬件的 12 位分辨率能在噪声环境下真的稳定到 1~2 个 LSB。
最常见的滤波手段是移动平均:维护一个队列,每来一个新采样就淘汰最旧的一个,取平均值。这样既能平滑噪声,又不会像普通均值滤波那样需要攒够 N 个点才输出一次。
from collections import deque BUFFER_SIZE = 16 sample_buffer = deque(maxlen=BUFFER_SIZE) def filtered_read(adc_obj): sample_buffer.append(adc_obj.read_u16()) return sum(sample_buffer) // len(sample_buffer)如果你的数据里偶尔有尖峰干扰,比如电机启动瞬间导致的电压跌落,这时滑动平均会把尖峰"抹"进结果里,产生一个缓慢的假波动。更好的方案是中值滤波:连续取 N 个点,排序后取中间值。N 一般选 5 或者 7,奇数个比较好处理。中值滤波对脉冲噪声的抑制效果比平均滤波强很多,代价是计算量稍大。
再往上就是过采样。这个概念本质是通过对同一信号做多次采样取平均,来提高有效分辨率或者降低噪声。Pico 的 ADC 输入噪声如果接近 1~2 个 LSB,那你读 100 次取平均,等效分辨率可以提升一些。但要清醒:过采样只能降低随机噪声,对系统误差没有帮助。如果参考电压偏了 2%,采样一万次平均值还是偏 2%。所以滤波负责平滑,校准负责修正系统误差,两者缺一不可。
3. 定时温度采集实战:从 NTC 到稳定可用的温度值
3.1 为什么温度采集适合拿 ADC 练手
温度是一个变化很慢的物理量,对采样率要求极低,对噪声不太敏感(前提是把平均滤波做好),而且 NTC 传感器的阻值变化明显,非常适合用来理解 ADC 分压、比例测量、标定公式这一整套流程。如果把 ADC 第一个实战项目定为采集温度,你会同时锻炼到模拟前端设计、软件滤波、非线性换算和定时调度,算是一举多得。
这里我把 NTC 和 DS18B20 两条路线对比一下:
| 方案 | 输出信号 | 优点 | 缺点 |
|---|---|---|---|
| NTC + 分压电阻 | 模拟电压 | 响应快、成本极低、体验 ADC 全流程 | 非线性、需标定、受分压电阻精度影响 |
| DS18B20 | 单总线数字 | 直接读温度、精度稳定、代码简单 | 不能体验 ADC、响应较慢、单总线时序有要求 |
既然标题是关于 ADC 的,那核心当然是 NTC 方案。但我也建议你在实际项目里综合评估:如果只是需要温度数据、不想碰标定,DS18B20 省心很多;如果是为了学 ADC 或者后续要接入更多模拟传感器,NTC 这条路线必须走一遍。
3.2 NTC 测温电路与计算公式
NTC 是负温度系数热敏电阻,温度升高时阻值下降。最常用的型号是 10k NTC,B 值 3950,在 25 摄氏度时阻值正好是 10k。这个参数是后面所有计算的基础。
电路接法我推荐这样:3.3V 接到一个 10k 固定电阻,固定电阻另一端引出测量点,测量点再接 NTC 到 GND,测量点同时接到 ADC 引脚。当温度上升,NTC 阻值下降,测量点的电压也跟着下降。ADC 读到的是这个电压的比例值。
反推温度的公式需要两步:
第一步,从 ADC 读数反推 NTC 当前阻值,前面已经写过:
ratio = raw / 65535 r_ntc = R_FIXED * ratio / (1 - ratio)第二步,用 NTC 的 B 值公式把阻值换算成开尔文温度:
import math R0 = 10000.0 # 25℃ 时阻值,10k T0 = 298.15 # 25℃ 对应开尔文温度 B = 3950.0 def r2temp(r_ntc): if r_ntc <= 0: return None inv_t = 1 / T0 + (1 / B) * math.log(r_ntc / R0) return 1 / inv_t - 273.15 # 转成摄氏度这个公式是 NTC 测温的标准做法,在 -20℃ 到 100℃ 范围内能保持不错的精度。要注意的是,B 值公式本身是一个近似模型,不同厂家生产的 NTC 在高温和低温段的实际阻值曲线跟公式会有偏差。如果对精度要求高,可以通过校准来修正。
为了让你对 ADC 读数有一个直觉认知,我列一组典型值(假设固定电阻 10k、NTC 10k B=3950、VREF 3.3V):
| 温度(℃) | NTC 阻值(kΩ) | 分压点电压(V) | ADC 读数(12位原始) | read_u16 读数 |
|---|---|---|---|---|
| 0 | 33.9 | 2.55 | 3170 | 50720 |
| 25 | 10.0 | 1.65 | 2048 | 32768 |
| 50 | 3.5 | 0.86 | 1072 | 17152 |
| 80 | 1.3 | 0.38 | 475 | 7600 |
从表格可以看出,温度越高 ADC 读数越小,而且曲线是非线性的。这个特性决定了前面要加滤波,后面的温度换算也要用公式,不能简单用线性映射。
3.3 定时采集的两种结构:轮询与 Timer,怎么选
定时温度采集的实现其实有两种完全不同的调度方式。第一种是"主循环轮询",就是整个单片机只有一个主循环,每隔一段时间去读一次 ADC。第二种是"定时器驱动",用 machine.Timer 设置一个周期中断,在中断回调里更新采样标志位或者直接读 ADC。
先说结论:对于温度这种慢变信号,我强烈推荐主循环轮询。原因是温度变化时间常数远大于单片机轮询周期,哪怕主循环里做了一堆别的事,稍微推迟几百毫秒读一次 ADC,对温度数据的影响几乎可以忽略。用轮询还能避免中断带来的各种隐藏问题,也就是后面第四章的重点。
不过很多教程喜欢用 Timer 来展示"定时采集",因为代码看起来更有实时系统的感觉。这里我给一个折中方案:用 Timer 在中断里只做一个事——置一个标志位,真正的 ADC 读取和温度计算全部放在主循环里。
from machine import Timer, ADC, Pin import math adc = ADC(Pin(26)) sample_flag = False def on_timer(timer): global sample_flag sample_flag = True timer = Timer(period=1000, mode=Timer.PERIODIC, callback=on_timer) while True: if sample_flag: sample_flag = False raw = adc.read_u16() voltage = raw / 65535 * 3.3 # 做滤波、算温度、输出或存储这种"中断置标志位、主循环做事情"的模式在嵌入式里非常经典。它的好处是,中断回调只做最简单的布尔赋值,出问题的可能性极低,而所有的计算都有主循环兜底,即便耗时长也不会影响中断的及时性。
3.4 数据输出与展示:文件记录、OLED 显示
定时采集的温度数据如果只是 print 到终端,调试没问题,但做长时间记录就不够用了。在 Pico 上最简单的方式是把数据追加写入一个 CSV 文件。
import time def log_temperature(temperature): with open('temp_log.csv', 'a') as f: f.write('{},{:.2f}\n'.format(time.time(), temperature))注意,每次打开文件写入后要及时关闭,否则数据会停留在缓冲区里,掉电丢失。MicroPython 在 Pico 上打开文件的 IO 开销不小,如果每秒钟写一次,会占用不少时间,还容易磨损闪存。更友好的做法是先在内存里攒一批数据,比如攒 10 条或者 1 分钟的数据,再一次写入文件。这样既省时间,又减少闪存写入次数。
如果做的是带屏幕的桌面小监控,OLED 显示比文件记录更直观。SSD1306 系列 0.96 寸 OLED 很常见,I2C 接线,MicroPython 有现成库。显示温度和采样间隔足够用。但要记住,I2C 和 ADC 读取不要在中断回调里混在一起操作,I2C 通信时序受中断打断很容易出问题,这也是为什么要按"主循环处理"的逻辑来组织代码。
4. ISR 避坑指南:我在定时器回调里踩过的那些雷
4.1 中断回调里的"禁手":print、浮点和动态分配
直接说结论,在 MicroPython 的 Timer 中断回调函数里,以下几类操作一定不要做:
第一,不要 print。看似无害的输出函数,实际涉及底层文件系统、字符编码、I2C/UART 驱动等复杂操作。在中断上下文里执行这些操作,轻则造成数据错乱,重则直接抛出异常导致整个程序崩溃。而且这个问题极难复现,你可能跑几十分钟才崩一次,排查起来非常痛苦。
第二,不要做浮点运算。RP2040 的 Cortex-M0+ 内核没有硬件浮点单元,所有浮点运算都是通过软件库模拟实现的,计算一次浮点除法或者对数函数可能需要几微秒甚至几十微秒。在温度采集的 NTC 换算中肯定要调用 math.log,这个函数在中断里一旦运行,整个系统的中断延迟会急剧增大。对于慢速的温度采集好像"感觉不出问题",但一旦你同时用中断做 PWM 控制或者频率测量,问题就会连锁爆发。
第三,不要做动态内存分配。list.append、字符串拼接、甚至某些看似简单的操作都可能在背后触发内存分配。MicroPython 的内存管理带垃圾回收机制,垃圾回收器在中断上下文里运行是非常危险的。我见过最典型的例子:中断回调里执行 values.append(raw),看似只是往列表里塞一个整数,但列表扩容的时候需要分配新内存,如果此时刚好触点 GC,程序就直接挂死。
第四,不要在中断里调用 time.sleep 或者任何带延时的函数。睡眠函数会让出 CPU 给其他任务,但这个"调度"机制在中断上下文里根本站不住。你的回调不是任务,Sleep 一秒就是在中断里死等一秒,这期间所有其他中断都被卡住。
4.2 一次真实崩溃的排查链路
我之前做过一个项目,用 Pico 读温度,Timer 每 200ms 触发一次,回调里读 ADC、算温度、把结果显示到 OLED 上。代码跑起来正常,但是跑几分钟到十几分钟不等就会死机,表现为 OLED 停止刷新,程序完全无响应。
我当时的排查过程是这样一步步走过来的。
第一步,怀疑是 Timer 回调里直接读 ADC 导致的。我先注释掉回调里的 ADC 读取,改成只打印一串字符,跑了一会儿还是死机。这里的关键现象是:只 print 也会崩,说明问题不完全出在 ADC 上。
第二步,把 print 也去掉,回调里什么都不做,光一个空函数,程序就不死了。这就定位到问题集中在中断回调里的函数调用链上。
第三步,再逐个回调函数试,最后锁定是在回调里调用了一个计算函数,而那个函数内部用了浮点运算和列表操作。当时我完全不理解为什么一个简单的数学计算能导致死机,直到我意识到 MicroPython 在中断回调里的执行环境跟主循环完全不同,它没有办法安全地触发垃圾回收。
第四步,把所有繁重操作全部移到主循环,中断回调只保留一个标志位赋值。改完以后程序连续跑了两天也没再出问题。
这个排查过程让我直观体会到 MicroPython 里 ISR 的边界:不是"看着像不会出事的代码就能在里面跑",而是"只要不是绝对安全简单的基础操作,就要放在中断外面"。
4.3 推荐的安全模式:标志位 + 主循环
基于上面的经验,我现在写所有带定时采集的 MicroPython 程序,都默认采用标志位模式。
from machine import Timer, ADC, Pin adc = ADC(Pin(26)) sample_ready = False sample_raw = 0 def on_timer(t): global sample_ready, sample_raw sample_raw = adc.read_u16() # 只读一个值,不计算 sample_ready = True timer = Timer(period=500, mode=Timer.PERIODIC, callback=on_timer) while True: if sample_ready: sample_ready = False raw = sample_raw # 在这里做滤波、换算、显示、保存这个模式里,中断回调只做两件事:读 ADC 原始值,置一个标志位。读 ADC 本身是同步阻塞操作,但在 Pico 上转换一次通常只需要几微秒,在中断里是可以接受的。真正复杂的计算都在主循环,中断响应保持轻快。
如果你需要更高效的采样,可以退一步用预分配的环形缓冲区:
from array import array BUFFER_SIZE = 32 buf = array('H', [0]) * BUFFER_SIZE idx = 0 def on_timer(t): global idx buf[idx] = adc.read_u16() idx = (idx + 1) % BUFFER_SIZE这个写法在回调中不产生新的内存分配,只是原地写入预分配数组,理论上是可以安全运行的。但它的缺点是主循环不知道缓冲区什么时候更新了,需要自己再维护一个读取索引来判断,逻辑复杂一些。如果你刚开始接触,我还是推荐标志位模式,等清楚了底层机制再去优化也不迟。
4.4 定时器回调与主循环之间的数据一致性
还有一个容易忽略的问题:主循环正在读取 sample_raw 的同时,定时器回调恰好更新了 sample_raw,可能导致读到"半个旧值、半个新值"。对于单字节或单个整数来说,在 Pico 上不太可能读到撕裂的数据,因为整数读取本身是原子的。但如果你的数据是元组、字符串或者结构体,就要格外小心。
更稳的做法是,主循环先把 sample_raw 复制到局部变量再使用,同时确保在复制期间中断回调不会再次写入。但 MicroPython 里你很难直接关中断。实用一点的策略是:把"读 ADC 然后置标志位"这个过程视为一个整体,主循环检测到标志位后立即复制数据并清标志位,回调在清标志位之前不会写新数据。只要顺序设计好,就不容易出现脏读。
5. 精度还能怎么救:校准、硬件调整与系统延展
5.1 校准流程:不需要高级仪器也能做
前面提到过,分压式电阻测量对参考电压误差有一定的免疫能力,但这并不代表整个测量链路没有系统误差。固定电阻的精度、NTC 的 B 值容差、ADC 的失调和增益误差,都会导致最终温度读数和标准温度计之间有偏差。要消除这些偏差,最直接的办法是校准。
最简单的两点校准:准备一个冰水混合物(约 0℃)和一个室温环境(约 25℃,用标准温度计确认),分别记录 Pico 测量的温度值和真实温度值,然后计算一个偏移量和比例系数,实际显示时套用线性修正。代码如下:
def calibrated_temp(raw_temp): # 假设标准温度 0℃ 时读到 1.8℃,标准 25℃ 时读到 26.4℃ return offset + gain * raw_temp这个线性修正对 NTC 的 B 值误差能起到很大的改善,尤其是你需要关注的温度范围比较窄时,效果接近完美。如果要求更精确,可以做三点校准,在低、中、高三个温度点分别记录误差,然后用分段线性插值修正。这就是热词里那个"外部 ADC 三点校准"思路的简化版,其实在 Pico 内部 ADC 上同样适用。
校准时候的注意事项是:每次校准都要等温度完全稳定,读数处于稳定状态再记录。NTC 热容量比较大,手捏一下温度就会上升好几度,必须等它恢复到环境温度,否则校准数据没有意义。
5.2 硬件上还能怎么提升 ADC 的数据质量
软件滤波管随机噪声,校准管系统误差,但硬件上的一些基本设计能让你省掉很多后续麻烦。
第一,ADC 输入引脚旁边放一个 100nF 到 1uF 的陶瓷电容,并联在测量点和 GND 之间。它可以把高频噪声旁路掉,同时也给内部采样电容提供了电荷储备,有效降低因输入阻抗过高导致的采样不足问题。这个电容对温度信号几乎没影响,但对 ADC 稳定性帮助非常大。
第二,分压电阻和 NTC 本身的精度要选好。固定电阻买 1% 精度的金属膜电阻,比 5% 碳膜电阻贵不了几毛钱,但温度误差能缩小不少。NTC 也尽量选 B 值容差小的,工业级一般在 ±1% 以内。
第三,电源尽量干净。Pico 如果用 USB 供电,注意不要和高功耗外设共用一个劣质 HUB。测量点、分压电阻和 ADC VREF 之间保持较短的走线,远离开关节点。虽然 Pico 这种开发板谈不上什么布板艺术,但给 ADC 的前级留出独立的 3.3V 走线,比从 LED 驱动旁边硬拉一根线要靠谱得多。
5.3 从单点测温到完整监控节点的扩展思路
一个温度采集节点做通之后,可以扩展的方向很多。最简单的是增加采样通道,把 NTC 接到 ADC1、ADC2 上,分别测多个测温点。每路一个固定电阻,代码上稍微抽象一下,封装一个 NTC 传感器类,每个实例对应一个 ADC 对象。
再往后可以做数据上报。Pico 如果接上树莓派、ESP32 或者局域网模块,就能定时把温度数据推到 MQTT 服务器或者本地 Web 服务。这时候采样频率和上报频率可以分开:ADC 每 500ms 读一次本地滤波,每 5 秒或每 30 秒上报一次平均值,既保证了数据新鲜度,又不会频繁打网络请求。
还有一个误区要提醒:不要为了追求更高分辨率轻易外接 HX711、ADS1232、MCP3561 这类高精度 ADC。HX711 是称重专用的 24 位 ADC,内部自带 PGA,适合接电桥而不是 NTC;ADS1232 也是称重/桥式传感器方向。如果只是测温,Pico 内部 ADC 配合好电阻和校准,做到 ±0.5℃ 以内并不难。真要用外部 ADC,也要想清楚自己到底需要多高的精度和采样率,再选型,否则只会增加布线和代码复杂度。
回到文章开头的那个问题:电位器读数乱跳,到底是板子坏了还是操作不对?多半是噪声和输入阻抗的问题。理解了 Pico 的 ADC 是 12 位 SAR、read_u16 返回 16 位范围、参考电压与分压比例的关系之后,很多现象都能解释通了。ADC 这个外设看起来简单,但真正用稳定,需要把硬件前端、软件滤波、调度结构和中断安全全部处理好。希望这篇实战记录能让你在 Pico 的 ADC 上少走一些弯路。