资讯动态

树莓派 Pico ADC 实战指南:滤榖校准与定时温度采集避坑

发布时间:2026/9/8 21:30:08 来源:尧图企业网站定制
树莓派 Pico 的 ADC 可能是很多人入门模拟量采集的第一站但也是踩坑最多的地方。我之前拿 Pico 做电池电压监测读出来的数值一直在 2.95V 到 3.15V 之间乱跳换了好几个滤波思路才稳住后来想用定时器中断定期采温度结果又在 ISR 里栽了跟头主程序直接被拖到像死机一样。这篇就把我从 machine.ADC API 到定时温度采集的完整思路、代码和踩坑过程都盘一遍尤其 ISR 那部分网上很少有人把细节讲透新手照着做能少走很多弯路。1. 先认识 Pico 的 ADC 硬件底子3 路输入加一路温度传感器1.1 RP2040 ADC 的硬件参数树莓派 Pico 用的是 RP2040 芯片内置一个逐次逼近型SARADC分辨率 12 位。不过 MicroPython 里读取的时候不会直接给你 0~4095而是返回 0~65535 的 16 位范围这个后面细说。Pico 引出的 ADC 通道一共 4 个ADC0 对应 GPIO26ADC1 对应 GPIO27ADC2 对应 GPIO28ADC3 对应 GPIO29。但 GPIO29 在官方板上被用作测量 VSYS 电源电压了如果你用的是普通 Pico 而不是订制板这个通道通常不建议接外部信号。此外内部还有一个 ADC4专门接芯片内部的温度传感器不需要外部接线直接读寄存器或者用 MicroPython 封装好的方法就能拿到温度。有几个硬件参数会影响你后面的电路设计。首先是输入电压范围ADC 输入最高不能超过 3.3V超过会损坏引脚其次是参考电压RP2040 的 ADC 参考电压默认就是 3.3V它来自板上的 3.3V 电源而 3.3V 电源又由 USB 的 5V 经过稳压芯片得到所以参考电压并非绝对精确的 3.3V实测可能是 3.26V 或者 3.31V这直接决定了你算出来的电压值会有偏差后面必须做校准。ADC 时钟最高可以到 48MHz一次转换需要 96 个 ADC 时钟周期粗算下来转换时间大约 2 微秒硬件速度其实不慢真正慢的是 MicroPython 的软件封装层。1.2 引脚复用与连接注意事项GPIO26~GPIO28 这三个引脚复用功能很多既能当 ADC 输入也能当普通的数字输入输出还能作为 I2C、SPI、UART 的引脚。但要注意用 machine.ADC 创建对象之后这个引脚就被 ADC 功能占用了想再拿它当数字引脚用是不行的需要重新初始化。另一个容易忽略的点是引脚的输入阻抗问题。RP2040 的 ADC 内部有一个采样保持电容在采样阶段会从外部信号源吸取一点电荷。如果你的信号源输出阻抗很高比如直接用一个几十千欧的可变电阻分压采样瞬间电容充电不完全读数就会偏低而且不稳定。官方建议信号源阻抗最好低于 10kΩ如果超过这个值最好在 ADC 引脚和信号源之间加一个运放缓冲器或者至少并联一个 100nF 的电容到地起到储能和滤波的作用。我实测用一个 100kΩ 电位器直接分压接 ADC读数比用万用表量的电压低了近 50mV并联 1uF 电容之后误差明显减小。自带温度传感器精度一般误差可能达到 ±2°C 甚至更多用来监测芯片有没有过热还行做精密温度采集不推荐这也是为什么我在后面的实战里选了外接 NTC 热敏电阻。2. machine.ADC 常用 API 逐个过一遍构造、read_u16 与换算公式2.1 ADC() 构造与通道选择MicroPython 的 machine.ADC 类用法很简单from machine import ADC adc0 ADC(26) # 通过引脚号构造 adc1 ADC(ADC1) # 也可以传通道名两种方式都支持但要注意不同固件版本可能对字符串参数的解析有差异保守起见我建议直接用引脚号兼容性最好。如果你是给 Pico W 写代码ADC 的用法和普通 Pico 完全一致这不冲突。有一个坑如果你把引脚号写成 29在普通 Pico 上能创建对象但读取出来的值并不是你期望的信号它测的是 VSYS 经过分压后的电压。这个通道并不是不能碰但要知道它背后有分压电阻网络官方原理图里 VSYS 通过两个电阻分压后接入 ADC3换算系数要仔细查手册。新手做实验就别碰这个通道了。2.2 read_u16() 返回值怎么换算成真实电压核心读取方法是 read_u16()返回 0 到 65535 之间的整数。这个设计是 MicroPython 为了统一不同平台 ADC 分辨率而做的底层是 12 位左对齐后变成 16 位也就是把 12 位的 0~4095 映射到 0~65535。换算公式如下voltage raw_value / 65535 * 3.3但这里就引出了刚才说的参考电压问题。如果你直接用 3.3 算得到的是理论电压实际 Pico 的 3.3V 引脚输出未必正好是 3.300V。更准确的做法是先实测板子 3.3V 引脚的电压再代入公式v_ref 3.28 # 万用表实测值 voltage raw_value / 65535 * v_ref我手头这块板子的 3.3V 实测是 3.28V用 3.3 算的话大概有 0.6% 的误差对多数场景无所谓但做电池电量百分比这种需要一定精度的项目时就会明显感觉续航估算不准。read_u16() 每次调用都是一次完整的采样转换默认是单次模式。如果你需要连续采样MicroPython 没有提供一直自动采集的 API你需要自己循环读取或者用 DMA。RP2040 的 MicroPython 固件目前没有对 DMA 采集做高层封装所以普通场景下循环读就行性能问题主要体现在 Python 解释层的开销一次 read_u16() 大约耗时几十微秒跑个几百赫兹采样率还是没有问题的。2.3 内部温度传感器怎么用Pico 内部温度传感器的读取方式比较隐蔽。在较新的 MicroPython 固件中你可以直接这样读import machine sensor_temp machine.ADC(4) raw sensor_temp.read_u16() # 转换为电压 voltage raw / 65535 * 3.3 # 转换为摄氏温度RP2040 数据手册给出的公式 temp 27 - (voltage - 0.706) / 0.001721公式中的 0.706V 是 27°C 时的典型输出电压0.001721 是温度系数V/°C。不同芯片个体会有差异如果对精度有要求可以在已知环境温度下做一次单点校准调整公式中的偏移量。这个温度传感器测的是芯片内部温度而不是环境温度芯片运行越久、负载越高读数会比环境温度高好几度所以别拿它当室温计用。3. 让读数稳定的关键一步滤波、校准与输入阻抗的坑3.1 为什么单次采样结果会跳很多新手第一次读 ADC 会奇怪同一个电压连续读十次每次都不一样。这非常正常。原因主要有三个一是 ADC 转换本身有量化误差12 位分辨率在 3.3V 参考下一个 LSB 大约对应 0.8mV信号在临界点时跳一个 LSB 很正常二是外部噪声尤其当你的 Pico 用 USB 供电时USB 电源本身就有纹波会耦合到参考电压和信号里三是采样保持电容充电不完全导致的误差这个在信号源阻抗偏高时尤其明显。所以单次采样结果只有参考意义真正的工程做法是多次采样后做软件滤波。下面分享三种我实际用过的方案各有适用场景。3.2 三种常见的软件滤波方式第一种是最朴素的均值滤波连续读 N 次取平均适合噪声比较对称、没有明显尖峰干扰的场景def read_avg(pin, samples16): adc ADC(pin) total 0 for i in range(samples): total adc.read_u16() return total // samplesN 取多大要看你的采样周期和噪声频率。我通常取 16 到 64 次16 次在 Pico 上耗时大概 1~2 毫秒肉眼完全无感。第二种是中值滤波取奇数个采样值排序后取中间值对尖峰脉冲干扰特别有效。比如附近有继电器开关、电机启动产生的瞬时干扰中值滤波能把异常值直接剔除。缺点是会丢一些真实变化信息而且排序在 Python 里稍微有点慢但对低频信号来说无所谓def read_median(pin, samples9): adc ADC(pin) values [adc.read_u16() for _ in range(samples)] values.sort() return values[len(values) // 2]第三种是滑动平均一阶低通滤波适合需要平滑跟踪缓慢变化的信号比如温度、液位、光照强度。每次新采样和上次结果做加权平均既能平滑噪声又不会像均值滤波那样需要攒一堆数据class LowPassFilter: def __init__(self, alpha0.2, initial0): self.alpha alpha self.last initial def update(self, raw): self.last self.last * (1 - self.alpha) raw * self.alpha return self.lastalpha 越大响应越快但平滑效果差一般取 0.1~0.3 之间做温度采集就挺好。你在温度变化时观察一下滤波后的数据会有明显的滞后这是低通滤波的固有特性做控制类项目需要注意。3.3 两点校准把看起来对变成真对前面提到参考电压不准实际电路中分压电阻也有误差。如果你做的是精度要求较高的项目比如电池电压监测后换算电量百分比强烈建议做一次两点校准。方法是准备两个已知电压比如用可调电源输出 1V 和 3V或者用精密基准源分别读取 raw 值然后建立线性校正关系# 校准数据已知电压 vs 读到的电压 known [1.0, 3.0] measured [1.008, 3.023] # 线性校正系数 k (known[1] - known[0]) / (measured[1] - measured[0]) b known[0] - k * measured[0] corrected_voltage k * raw_voltage b实测下来校准后误差可以控制在 10mV 以内。两点校准假设 ADC 是线性的RP2040 在 0.1V 到 3.3V 范围内线性度还不错这个假设成立。没条件做两点校准时至少要做上面的参考电压修正把 3.3 替换成实测值能解决 90% 的精度问题。另外提醒一下如果 ADC 引脚信号来自高阻源光靠软件滤波解决不了系统误差必须从硬件入手。并联电容是性价比最高的方案我一般用 100nF 陶瓷电容并在 ADC 引脚对地信号质量改善非常明显。4. 定时温度采集实战NTC 分压、Timer 调度与数据输出4.1 传感器选型NTC 和模拟温度传感器怎么选温度采集是 ADC 最常见的应用之一。常见的模拟输出温度传感器有这么几类NTC 热敏电阻便宜、体积小、响应快但输出是非线性的需要软件换算而且个体互换性差精度一般。模拟输出线性温度传感器如 TMP36、LM35输出和温度线性对应比如 TMP36 是 10mV/°C0°C 时输出 0.5V换算简单精度尚可。数字传感器如 DS18B20不走 ADC走单总线直接读温度精度高、可多点组网但需要盯时序。如果你只是想学习 ADC 采集同时又要做温度监测NTC 是最能体现软件控制深度的选择因为你需要完整走一遍分压电路设计、ADC 采样、电阻换算、温度换算。下面就以 NTC 为例展开。4.2 分压电路计算从 NTC 阻值到电压的换算NTC 的阻值随温度升高而降低常见的有 10kΩ 25°C也就是常温下阻值 10kΩB 值 3950。电路上最简单的接法是一个固定电阻 R 和 NTC 分压把 NTC 放在下端挨地上端接 3.3V中间点接 ADC 引脚3.3V ---- [ R ] -------- ADC 引脚 | [NTC] | GND这个接法中ADC 读到的电压 Vout 3.3 * R_ntc / (R R_ntc)。固定电阻 R 的取值要参考 NTC 在目标温度范围的阻值。如果目标是室温测量10°C~50°C10kΩ NTC 在这个范围内阻值大约 4kΩ 到 20kΩ取 R10kΩ 时中点电压变化范围大致是 0.94V 到 2.2VADC 分辨率利用得比较好。如果目标温度很高NTC 阻值可能降到 1kΩ 以下这时建议把 R 换成 1kΩ让分压更匹配。从电压推回温度先算 NTC 当前阻值r_ntc R * (3.3 / voltage - 1) # 注意按 NTC 在下端的分压公式推导然后用 B 值公式换算温度import math B 3950 T0 298.15 # 25°C 对应的开尔文温度 R0 10000 # 25°C 时的阻值 T_kelvin 1 / (1/T0 (1/B) * math.log(r_ntc / R0)) temp_celsius T_kelvin - 273.15B 值公式在窄温度范围内精度足够-10°C 到 80°C 之内实测误差在 1°C 左右。如果想更准可以用 Steinhart-Hart 方程需要三个系数但普通项目没必要。4.3 用 machine.Timer 实现定时采集温度变化是慢信号不需要连续高速采样我用 machine.Timer 定期触发采集。MicroPython 的 Timer 在 Pico 上支持硬件定时器回调间隔可以做到毫秒级但注意回调运行在中断上下文里面不能做耗时操作。下面是我常用的框架from machine import ADC, Timer import math, time R_FIXED 10000 V_REF 3.28 B 3950 T0 298.15 R0 10000 adc ADC(26) def read_temp_once(): raw adc.read_u16() voltage raw / 65535 * V_REF r_ntc R_FIXED * (V_REF / voltage - 1) temp_k 1 / (1/T0 (1/B) * math.log(r_ntc / R0)) return voltage, temp_k - 273.15 def sample_timer(timer): # 中断上下文只做读取和存储 global latest_temp v, t read_temp_once() latest_temp (v, t) latest_temp (0, 0) timer Timer() timer.init(period1000, modeTimer.PERIODIC, callbacksample_timer) while True: v, t latest_temp print(voltage{:.3f}V temp{:.1f}C.format(v, t)) time.sleep_ms(200)这个框架里有几个细节值得说。一是 latest_temp 用全局变量在中断和主循环之间共享二是主循环打印的不一定是本次刚采到的值而是中断里上一次更新好的值这样解耦了采集和输出。你如果需要记录时间戳就在采样时一起存进去。4.4 数据输出方式print、文件存储还是接屏采集到温度数据后怎么输出简单实验直接 print 最简单配合 Thonny 的绘图器还能看到实时曲线。Windows 下的 PuTTY 串口工具也可以。如果你需要记录数据做离线分析建议把数据写入 CSV 文件用 Pico 的板载存储或者 SD 卡都行。写 CSV 时注意要注意不要在中断里做文件写入文件系统操作可能阻塞最好在主循环中集中写# 在主循环中定期把数据累积到列表攒够一定数量再写文件 data_buffer [] while True: v, t latest_temp data_buffer.append((time.ticks_ms(), v, t)) if len(data_buffer) 60: with open(temp_log.csv, a) as f: for row in data_buffer: f.write({},{:.3f},{:.1f}\n.format(row[0], row[1], row[2])) data_buffer.clear() time.sleep_ms(200)这样写的好处是减少文件写入次数避免频繁写 flash 损伤存储介质。速度要求不高的场景60 条一次写入完全没问题。5. ISR 避坑指南为什么我不建议在中断回调里直接读 ADC5.1 我在 ISR 里读 ADC 踩的坑最早做定时采集时我图省事直接把滤波、换算、打印全写在 Timer 回调里。现象是程序偶尔会卡住一段时间后又恢复而且打印的数据会突然停滞几秒然后连续蹦出来。后来加了计时器统计才发现回调函数执行时间有时候会飙到几十毫秒远超定时器周期主循环中被 Wait 的很多事情都被切断了。究其原因MicroPython 的 Timer 回调运行在中断上下文在 RP2040 的 MicroPython 移植里中断回调期间主 Python 程序是被暂停的。我在回调里做了这些操作多次 read_u16()每次几十微秒、math.log 浮点运算几百微秒到几毫秒、print 输出串口阻塞可能非常久。这些操作叠加在一起中断处理时间就失控了。5.2 ISR 中哪些操作绝对不能做有几个原则是我用血泪换来的不要在 ISR 里做 print。串口输出本质是轮询发送如果外设还没准备好print 会阻塞等待中断里一卡就是几毫秒甚至更多。不要在 ISR 里申请内存。MicroPython 的 float 运算、字符串格式化、列表拼接都涉及内存分配在中断上下文做可能触发垃圾回收垃圾回收会暂停一切其他任务这是中断里最严重的性能杀手。不要在 ISR 里操作文件系统。SD 卡、flash 写入可能涉及较长的 I/O 等待同样会阻塞。保持 ISR 尽量短。如果只是读一次 ADC 然后把结果存到一个变量这没问题但如果你读完还要做滤波、换算、存储、显示那中断肯定会被拖垮。我在前面那个 sample_timer 里只做了一次 read_u16() 换算平均耗时大约 100 微秒这在一个 1 秒周期的定时器中完全可接受。但如果我把滤波和平均也放进去一次调用要 16 次采样加排序加浮点运算几百微秒起步主循环的响应肯定会受影响。5.3 推荐架构ISR 里只置标志位主循环里做处理如果你有更多处理需求更稳妥的做法是 ISR 里只设置一个标志主循环轮询这个标志后执行完整的采样和计算from machine import Timer, ADC import math adc ADC(26) sample_pending False def on_timer(timer): global sample_pending sample_pending True timer Timer() timer.init(period1000, modeTimer.PERIODIC, callbackon_timer) while True: if sample_pending: sample_pending False v, t read_temp_once() print(voltage{:.3f}V temp{:.1f}C.format(v, t)) # 其他主循环任务这个模式把采集时机交给定时器把耗时操作放回主循环既保证了采样周期准确又不会阻塞其他任务。我用这个架构后主循环再也没有出现卡顿。值得注意的是MicroPython 中 Python 层面的全局变量赋值在 ISR 里是安全的它底层就是指针更新不会触发锁问题。如果你真的需要在 ISR 中做多个 ADC 通道采样建议把读取代码精简到最小然后只存原始值数组换算放外面做。另外要注意同一时刻只应该有一个中断回调在运行MicroPython 的 Timer 回调不会嵌套触发但这不代表你可以无限往里面塞代码。6. 实测效果与后续扩展的几点心得6.1 滤波和校准后的实测对比我把前面说的方法凑齐后做了个连续 1 小时的温度记录实验。NTC 放在室内固定电阻 10kΩTimer 周期 1 秒。原始单次采样换算出来的温度在 24.3°C 到 25.8°C 之间波动加了 16 次均值滤波后波动缩小到 24.6°C 到 25.1°C再用两点校准修正参考电压误差后整体偏移纠正了大约 0.4°C。用另一只工业铂电阻温度计对比校准后的误差基本在 ±0.3°C 以内这个精度对于 NTC 方案来说已经相当不错了。这个实验也验证了一个观点软件能解决随机噪声但解决不了系统误差。随机噪声靠滤波压下去参考电压和电阻容差带来的固定偏差必须靠校准环节手动修正。所以 ADC 项目的基本流程都是硬件兼顾阻抗匹配、软件做滤波、最后加校准三步缺一不可。6.2 扩展方向多通道扫描、低功耗与数据落地做完单通道温度采集你会发现这套代码很容易扩展成多通道。比如房间温度、湿度传感器输出电压、光敏电阻三路信号可以共用一个定时器每次中断读一个通道轮换读取。这时 ISR 里只做一次 read_u16() 的优势就体现了多通道也不会让中断时间成倍增长。你也可以在每次中断里加一个通道索引主循环根据索引分发处理CHANNELS [26, 27, 28] current_ch 0 def scan_timer(timer): global current_ch global last_raw_values adc ADC(CHANNELS[current_ch]) last_raw_values[current_ch] adc.read_u16() current_ch (current_ch 1) % len(CHANNELS)低功耗方面Pico 的 ADC 在读取时会自动开关内部电源读完后会进入低功耗状态这一点 MicroPython 已经处理了但要注意如果你用外部传感器传感器的供电不能一直开着最好用 GPIO 控制传感器电源只在采样前打开采完就关这样电池供电时能省不少电。如果数据量大了建议把采样数据先缓冲在内存中批量写入 SD 卡或通过串口发送给上位机。注意 Pico 的内存只有 264KBPython 对象开销大积攒太多数据会触发内存不足我的经验是缓冲区最多放 500 条记录超出就及时写盘。最后再分享一个操作细节实际焊接或插接传感器时尽量让 ADC 引脚的走线短一些避免和电源线、电机驱动线平行走线否则即使做了软件滤波你还是会看到明显的 50Hz 工频干扰。必要时 ADC 引脚到地并联一个 1uF 电容能显著抑制这种干扰。想让读数更干净这几招比任何高级滤波算法都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价