
树莓派 Pico 的 ADC网上资料不少但多数停留在“读个电位器电压”这一步。前阵子我做一个小型温控记录仪需要定时采集温度、把数据存下来结果发现从 ADC 基础 API 到真正稳定可靠地做定时采集中间坑多得超出预期。尤其是中断服务程序ISR那一块稍不注意就会遇到程序卡死、采集抖动、内存报错这些诡异问题。这篇我把整个调试过程完整梳理一遍从 machine.ADC 的 API 细节到定时温度采集的完整实战再到 ISR 的避坑思路一次性讲透。内容面向刚接触 Pico 的 MicroPython 玩家也适合从 Arduino/STM32 转过来、想快速上手 Pico ADC 的朋友。读完你不仅能写出能跑的采集代码还能理解每一步为什么这样写遇到问题知道从哪里入手排查。1. Pico 的 ADC 到底行不行先把硬件基础摸清1.1 引脚分布和核心参数树莓派 Pico 用的是 RP2040 芯片内置一个 12 位逐次逼近型SARADC。很多人一看到“12 位”就觉得精度不错但实际使用时要留个心眼这个 12 位是硬件分辨率也就是说满量程 3.3V 被分成 4096 个台阶每个台阶大约 0.8mV。Pico 开发板上可用的 ADC 通道有这些通道GPIO说明ADC0GP26可作普通 ADC 输入ADC1GP27可作普通 ADC 输入ADC2GP28可作普通 ADC 输入ADC3GP29只读通道内部连接 VSYS 分压不适合做通用模拟输入ADC4无外部引脚芯片内部温度传感器官方建议 ADC 输入范围是 0 ~ 3.3V。如果输入电压超过 3.3V需要先做分压处理否则轻则读数一直是 4095/65535 这类满量程值重则直接烧坏 GPIO。我在初学时犯过一个低级错误把一个输出 0~5V 的模块直接接到 GP26 上结果读数永远顶满我还以为是代码写错了后来量了引脚电压才发现问题的根源。Pico 的引脚没有 5V 容忍能力这个一定记牢。关于 GP29 多说一句。这个引脚在开发板上通过 200kΩ 和 100kΩ 电阻分压后连接到 VSYSMicroPython 里可以用ADC(29)读取但因为是分压后的值要自己在代码里换算回去。它也不能当普通 GPIO 输出使用看到一些帖子把 GP29 当作可用 ADC 通道推荐给新手其实容易造成误解。1.2 逐次逼近型 ADC 的工作原理用天平来理解SAR ADC 的全称是 Successive Approximation Register逐次逼近寄存器。名字听着复杂原理其实像用天平称重先放一个大砝码看是否超重超了就换小一号的没超就保留这个大砝码再继续加更小的砝码逐步逼近真实重量。放到 ADC 里“砝码”就是二进制权重。一个 12 位 ADC内部比较器会先假设最高位为 1也就是把输入电压和 1.65V 比一次如果输入比 1.65V 高最高位保留为 1否则置 0。接着看第二位把基准电压按 1/4 步进比较逐位逼近直到 12 位全部确定。整个过程只需要 12 个时钟周期所以 SAR ADC 的采样速度可以做得比较快Pico 的 ADC 官方标称采样率可达 500kS/s 左右。这个“逐渐逼近”的思路也解释了为什么输入电压必须落在基准范围内如果输入超过 3.3V比较器永远判断“更高”最后得到满量程值如果输入是负电压比较器永远判断“更低”最后得到 0。理解这一点对排查问题特别有帮助。比如你发现 ADC 读数恒定在 65535通常不是软件 bug而是输入电压超量程了读数打扫干净零先怀疑接线再怀疑传感器供电最后才去检查代码逻辑。1.3 什么时候用片内 ADC什么时候需要外挂Pico 的片内 ADC 适合慢速、直流、单端信号测量。温度传感器、电位器、电池电压监测、简易光照度检测这些都是它的主场。但它也有明显短板需要心里有数精度只有 12 位噪声环境下有效位数可能降到 10 位以下没有真正的差分输入共模干扰抑制能力弱输入阻抗不高接高阻信号源时需要加缓冲多个通道之间切换后需要建立时间否则会有串扰。如果你的项目需要 24 位分辨率或者要测量称重传感器这类毫伏级微弱信号基本就得考虑外部 ADC 了。HX711 是称重模块最常见的 24 位 ADCADS1232 适合桥式传感器MCP3561 是高性能多通道 24 位方案。这些外部芯片通常通过 I2C 或 SPI 与 Pico 通信MicroPython 下也有现成库价格也不贵。所以我的建议是Pico 片内 ADC 解决 80% 的常规需求剩下的 20% 果断外挂别硬撑。2. machine.ADC API 手把手教学三步走清楚2.1 最基础的读取姿势创建对象、读原始值、算电压MicroPython 的machine.ADC是官方内置模块不需要额外安装。创建一个 ADC 对象很简单from machine import ADC, Pin adc ADC(Pin(26)) value adc.read_u16() print(value)read_u16()返回的是一个 0~65535 之间的整数。注意这里不是 16 位精度而是把 12 位原始结果左移 4 位后映射到 16 位整数范围里。也就是说真实有效的分辨率还是 12 位返回值的有效间隔是 1665536 / 4096 16。如果你想要的是“实际电压值”需要自己换算voltage value * 3.3 / 65535有的固件版本提供了read_uv()方法直接返回微伏µV省去手写换算voltage_uv adc.read_uv() voltage voltage_uv / 1_000_000 # 转成伏我比较推荐用read_u16()加手动换算的方式因为这样你能明确知道自己用的参考电压是多少后面做校准时也更灵活。默认情况下代码里的 3.3V 应该替换为实际测量到的 3V3 引脚电压这个后面我会专门讲。2.2 切换通道必须等待建立时间否则读数串扰Pico 的 ADC 前面有一个采样保持电容切换通道后电容里的残留电荷会叠加到下一次采样结果上。如果上一个通道电压很高下一个通道电压很低切换后立刻读取会得到一个偏高的值然后逐渐回落。我在实际调试时观察到的现象是切换通道后立刻读取误差可能达到数十个 LSB等待 1ms 左右读数才会稳定下来信号源内阻越大需要的等待时间越长。所以多通道采集时标准做法是切换后加一个小延时adc1 ADC(Pin(26)) adc2 ADC(Pin(27)) v1 adc1.read_u16() time.sleep_ms(2) # 让采样电路稳定 v2 adc2.read_u16()如果对速度要求高可以把延时缩到几百微秒但别完全不加。很多新手写双通道采集时发现“两个通道数值混在一起”十有八九就是没处理建立时间。2.3 参考电压不是完美 3.3V精度问题要提前布局Pico 的 ADC 参考电压来自板载 3V3 电源。问题在于这个 3.3V 并不一定精确等于标称值。USB 供电时根据负载、线缆质量、稳压芯片批次差异实际电压可能在 3.25V 到 3.35V 之间浮动。假设你用的是精确的 3.3V 作为换算系数但实际参考是 3.28V那么所有测量结果都会有大约 0.6% 的系统误差。温度传感器读数的体现就是误差可能在 2°C 到 3°C 左右对于温控场景往往不可接受。解决办法是把参考电压当成一个未知常量用万用表实测 3V3 引脚的对地电压然后代入换算公式VREF 3.28 # 实测值 voltage value * VREF / 65535如果还想更进一步就用外部精密基准源或者采用两点校准来做传感器标定。关于校准的具体操作第 5 节会给出方案。3. 定时温度采集实战从电路到代码一次到位3.1 传感器选型LM35 还是 MCP9700AADC 本身只能测电压要测温度需要把温度转成电压的传感器。市面上常见的模拟温度传感器有 LM35、MCP9700A、热敏电阻等各自适配的电路不一样。传感器输出特性优点缺点LM3510mV/°C0°C 时 0V线性好灵敏度高不能测负温3.3V 供电下线性范围有限MCP9700A10mV/°C0°C 时 500mV可测负温3.3V 可直接供电0°C 偏移量需要软件扣除NTC 热敏电阻非线性阻值变化便宜响应快需要查表或公式计算精度受分压电阻影响我的建议是 LM35 和 MCP9700A 二选一。LM35 的输出电压直接等于温度×10mV例如 25°C 时输出电压 250mV计算最简单MCP9700A 则把 0°C 对应到 500mV计算公式要偏移一下# MCP9700A temp_c (voltage - 0.5) * 100下面我以 LM35 为例完整演示因为它的公式最简单适合理解整个采集链路。实际项目中我后来换成了 MCP9700A因为我的应用场景需要测量负温度但代码逻辑只有那一行换算的区别。3.2 硬件接线看起来简单其实有细节LM35 是三个引脚的器件接法如下VCC 接 Pico 的 3V3GND 接 GNDVOUT 接 GP26ADC0这里有几个容易踩的坑LM35 的数据手册写的是 4V~30V 供电但 3.3V 供电时绝大多数批次仍然能正常工作只是输出特性在高温段可能略微偏离。如果手头的 LM35 在 3.3V 下读数有明显异常换 MCP9700A 更稳妥。传感器和 Pico 之间的连线尽量短并且避开 PWM 输出线、电机驱动线等干扰源。ADC 是模拟电路对噪声敏感长线加上邻近的开关信号读数的跳动会让你怀疑人生。如果传感器离 Pico 比较远可以在 GP26 对地之间加一个 100nF 到 1µF 的电容做 RC 滤波能显著改善稳定性。3.3 基础采集代码先把温度读出来先写一个最简单的读取函数验证硬件连接没问题from machine import ADC, Pin import time adc ADC(Pin(26)) def read_temp(): value adc.read_u16() voltage value * 3.3 / 65535 temp voltage * 100 # LM35: 10mV/°C return temp while True: print(温度: {:.2f} °C.format(read_temp())) time.sleep(1)运行后用双手捏住传感器温度读数应该会缓慢上升松开后又会回落到室温附近。如果你看到的数值是负数或者超过 100先检查换算公式里的符号再看接线。3.4 定时采集用 Timer 中断实现精确调度需求升级一下每 60 秒记录一次温度持续记录一整天。如果我用time.sleep(60)加循环也能实现但问题是主循环里如果还要处理按键、显示、存储等其他任务sleep会阻塞整个程序“定时”就不再可靠。这时用硬件定时器更合适。MicroPython 的machine.Timer可以设定周期触发回调函数在中断上下文里执行采集动作from machine import Timer, ADC, Pin import time adc ADC(Pin(26)) sample_count 0 def read_temp(): value adc.read_u16() voltage value * 3.3 / 65535 return voltage * 100 def on_timer(timer): global sample_count temp read_temp() sample_count 1 print(第 {} 次采样: {:.2f} °C.format(sample_count, temp)) timer Timer() timer.init(modeTimer.PERIODIC, period60000, callbackon_timer) while True: # 主循环可以干别的事情定时采集会自动进行 time.sleep(1)这段代码看起来很美但实际跑起来会发现两个问题一是 print 输出偶尔会卡顿二是如果回调里执行时间过长定时会逐渐漂移。这正是第 4 节要重点讨论的 ISR 避坑内容。3.5 数据落地把采样结果写入文件避免断电丢失定时采集通常需要保存数据。Pico 上电后运行 MicroPython可以用内置的open()直接写文件这样把数据保存到板载 Flash 中def log_temperature(temp): with open(temp_log.csv, a) as f: f.write({},{:.2f}\n.format(time.time(), temp))注意写成 CSV 格式方便后期用 Excel 或 Python 分析。不过有个现实问题频繁写入 Flash 会缩短 Flash 寿命。每分钟写一次一天 1440 次理论上还能接受但如果需要秒级写入建议改接 SD 卡模块或者通过串口把数据发到上位机。还有一点time.time()在 Pico 上默认从开机时间开始计数如果你需要真实时间需要外接 RTC 模块或者在启动时通过网络校时。我这个项目是本地记录用开机累计秒数就够用了。4. ISR 避坑指南定时采集最容易翻车的地方4.1 中断回调里不能做的事清单machine.Timer的回调函数是在中断上下文中执行的。MicroPython 里的中断上下文有严格限制以下操作尽量不要放在回调里大量内存分配比如创建大列表、大字符串调用time.sleep()阻塞等待执行耗时过长的打印访问需要复杂初始化的外设原因要理解中断处理期间主程序被挂起。如果你在中断里 sleep 100ms主循环就停摆 100ms。如果两个定时器中断发生竞争还可能造成回调重入带来不可预料的 behavior。曾经有人问为什么定时回调里一执行print主循环就卡顿明显这是因为 print 涉及串口输出和缓冲区管理在 MicroPython 的中断上下文里效率极低。4.2 正确姿势中断只做标记主循环做实事我推荐的架构是定时器中断里只设置一个布尔标志主循环检测到标志后再去执行采集和存储逻辑。这个模式叫“标志位通知”虽然多了一次主循环轮询的时延但对稳定性提升非常明显。from machine import Timer, ADC, Pin import time adc ADC(Pin(26)) sample_requested False def read_temp(): value adc.read_u16() voltage value * 3.3 / 65535 return voltage * 100 def on_timer(timer): global sample_requested sample_requested True # 中断里只置标志位不做耗时操作 timer Timer() timer.init(modeTimer.PERIODIC, period60000, callbackon_timer) while True: if sample_requested: sample_requested False temp read_temp() print(温度: {:.2f} °C.format(temp)) # 在这里写文件、显示、上传等操作 # 主循环还可以处理其他任务这样做的好处是采集动作实际发生在主循环里可以放心调用 print、open、time 等函数而不会阻塞中断上下文。代价是如果主循环在处理一个耗时任务采样时刻会有轻微延迟。对于秒级以上的定时采集这个延迟完全可以忽略。如果你的应用对时间精度要求非常高可以在置标志位的同时记录一个time.ticks_us()时间戳后续处理时把时间戳一并保存这样即使处理延迟了也知道精确的采样时刻。4.3 定时漂移为什么会出现有人发现定时器设了 1 秒实际一天下来累积偏差好几秒。原因有两个一是 MicroPython 的 Timer 回调本身基于软件计时可能因为系统中断、垃圾回收等因素产生小漂移二是回调函数本身耗时较长时下一次触发的“时刻”会顺延。比如你设 1 秒周期但回调执行花了 200ms那实际周期可能是 1.2 秒。解决方法是在回调中记录上一次触发的时间戳然后计算实际周期必要时校准或者把回调执行时间压到极短按照 4.2 节的标志位架构来做。我在温控记录仪里最终采用的方案是定时器周期设为 10 秒回调里只做标志位标记主循环里执行大约 20ms 的采样和存储工作。连续运行 48 小时实际记录间隔误差控制在 0.5 秒以内满足需求。4.4 中断里的内存分配为什么会报错MicroPython 对中断上下文有特殊的内存管理策略。在中断回调中直接拼接字符串、创建列表一旦触发堆内存分配可能抛出MemoryError或导致 MicroPython 直接崩溃。原因在于中断处理时垃圾回收器可能处于不安全状态不能正常分配内存。一个常见例子def on_timer(timer): temp read_temp() msg 温度: {:.2f}.format(temp) # 在中断里格式化字符串可能报错 print(msg)不同固件版本表现不同有的默认允许小量分配但不保证稳定。遇到莫名崩溃或MemoryError时最先检查的就是中断里有没有字符串格式化、列表生成等操作。我的经验法则是无论当前固件允不允许中断回调里不写任何从字面量构造新对象的代码。字符串拼接、列表追加、dict 操作全都禁止。5. 进阶扩展滤波、校准与外部高精度 ADC5.1 软件滤波滑动平均和中值滤波Pico 的 ADC 噪声不可忽视。裸读一个稳定电压连续采 100 次结果可能有十几个 LSB 的波动。对于温度采集而言这一点波动可能是零点几摄氏度不算严重但如果源信号更微弱就明显了。最简单的软件滤波是滑动平均def read_temp_smoothed(samples10): total 0 for _ in range(samples): total read_temp() time.sleep_ms(5) return total / samples平均法能抑制随机噪声但对脉冲型干扰敏感。如果环境中存在偶发的开关噪声一个异常值会把平均值拉偏。这时改用中值滤波更稳def read_temp_median(samples10): vals [] for _ in range(samples): vals.append(read_temp()) time.sleep_ms(5) vals.sort() return vals[samples // 2]中值滤波对脉冲干扰免疫效果更好代价是代码量多一点。我实际用的是“中值平均”的组合采 5 次去掉最高最低取剩下 3 个的平均效果不错能达到约 0.5 LSB 的有效精度。5.2 两点校准消除传感器和参考电压的复合误差温度传感器的标称曲线和真实特性之间存在误差光靠“把 3.3V 换成实测值”是不够的。更可靠的方案是两点校准。找一个稳定的低温点和高温点分别记录传感器读数和标准温度计读数然后构建直线方程# 假设校准数据 # 低温点: 标准温度 0.2°C, 读数值 raw_low 130 # 高温点: 标准温度 49.8°C, 读数值 raw_high 5020 raw_low 130 raw_high 5020 temp_low 0.2 temp_high 49.8 scale (temp_high - temp_low) / (raw_high - raw_low) offset temp_low - scale * raw_low def read_temp_calibrated(): raw adc.read_u16() return scale * raw offset实际操作中低温点可以用冰水混合物0°C 附近高温点可以用恒温水浴或精准的电子温度计作为参考。校准完成后整体误差可以控制在 ±0.2°C 以内对于常规温控记录完全够用。5.3 需要 24 位分辨率时怎么办外部 ADC 方案速览如果项目要称重、精密应变测量Pico 内部 12 位 ADC 就捉襟见肘了。常见的方案是使用外部高精度 ADC 芯片HX71124 位 Σ-Δ ADC专门用于桥式传感器常见于电子秤。双通道增益可编程通过两线时钟/数据接口读取。MicroPython 有现成驱动操作简单。ADS1232同样是 24 位 ADC适合称重和应变应用性能比 HX711 更好一些价格略贵。MCP3561Microchip 的多通道 24 位 ADC支持 SPI 接口采样率和分辨率配置灵活适合需要高速高精度采集的场景。这些芯片的驱动原理基本一致通过 SPI 或专用时序读取转换结果然后在代码里通过查询或中断方式获取数据。Pico 的 SPI 外设在 MicroPython 中封装得比较友好machine.SPI类可以直接操作。不过要提醒一点使用外部 ADC 后代码复杂度会显著上升。不是简单的“换个库”就能完事需要考虑时序、参考电压、接线布局、驱动能力等一系列问题。如果没有硬性精度需求先把手头的片内 ADC 调好收益更高。5.4 DMA 多通道采集怎么实现有人问 Pico 能不能像 STM32 那样用 DMA 做多通道连续采样。STM32 的 ADC 自带 DMA 传输功能可以在后台高速采样多路数据不打断 CPU。Pico 的 RP2040 在硬件上也有 DMA 控制器但 MicroPython 固件没有直接暴露 ADCDMA 的 API。如果你确实需要高速多通道采样有两个思路一是切换到 C/C SDK 环境。RP2040 的官方 C SDK 支持 ADC 的 FIFO 和 DMA 模式可以实现不占 CPU 的连续采样。对性能要求高的项目用 C 环境更合适。二是在 MicroPython 中用多线程模拟。_thread模块可以开一个后台线程持续采样主线程负责数据处理。因为 GIL 的存在和 MicroPython 的调度机制实际采样精度只能到毫秒级但用来采集音频或高频信号是远远不够的。我的建议是MicroPython 适合原型验证和低频采集真正的高速高精度采集移到 C/C 或直接选择 STM32 这类更适合的硬件平台。6. 常见问题快查表实测中踩过的坑都在这把我在调试过程中遇到过的、以及身边朋友问过最多的 ADC 问题整理成快查表方便大家快速定位。现象可能原因解决方法读数一直是 65535输入电压超过 3.3V或引脚被外部电路拉高到危险电平万用表测量输入电压确认分压电路读数一直是 0接线错误、传感器未供电、GPIO 编号写错检查传感器 VCC/GND 接反确认 Pin(26) 对应 ADC0读数波动很大电源噪声、长导线引入干扰、传感器输出电压太低加 RC 滤波缩短连线软件加中值滤波切换通道后前几次读数异常采样电容没有完成充放电通道建立时间不足切换后延时 1~2ms 再读取定时采集实际间隔不准确中断回调执行时间过长或主循环被阻塞中断里只置标志位主循环里执行实际采集中断回调里运行崩溃或 MemoryError在 ISR 里进行了内存分配或字符串操作回调里只做标志位标记所有耗时操作移到主循环打印温度比实际高很多参考电压实际值高于或低于 3.3V换算公式系数不准用万用表实测 3V3 引脚电压并替换公式中的 3.3LM35 读数偏高且不稳定LM35 在 3.3V 供电下高温段线性变差换 MCP9700A或改用三点分段校准还有一个特别隐蔽的问题GP29 引脚在某些 MicroPython 版本中初始化 ADC 时会报错或者返回异常值。因为 GP29 内部连接到 VSYS 分压同时板载开关电源稳压器也会对它产生影响不要在需要精确测量的场景里用它老老实实用 GP26~GP28。关于供电方式也要多说一句。Pico 通过 USB 供电和电池供电时3V3 电压会有细微差别。我实测 USB 供电时 3V3 约 3.31V而锂电池通过 VSYS 供电时约 3.28V这个差异直接反映到 ADC 读数上。所以更换供电方式后最好重新校准参考电压不要盲目复用之前的换算系数。另外还有一个容易被忽视的点MicroPython 固件版本不同ADC类的行为可能有细微差异。比如早期固件的read_u16()返回值范围是 0~4095后来统一映射到 0~65535。如果你参考的教程是几年前的注意看一下固件版本和 API 差异不要直接照搬代码。最简单的方法是写一段读取代码打印几个值和万用表对照一下马上就知道固件行为对不对。在我实际做完温控记录仪之后最大的感受是ADC 采集看起来只是“读一个数”但真正要让它稳定、准确、长时间可靠运行涉及硬件电路、软件滤波、校准、任务调度好几个层面的问题。尤其当项目从“在串口打印温度”升级到“每天自动记录几万条数据”的时候每一个之前忽略的小问题都会放大成必须解决的障碍。如果这篇文章能帮你少走几步弯路那整理这些经验就值了。最后再分享一个小技巧调试 ADC 电路时养成用万用表实测引脚电压的习惯。代码里的数值再漂亮也不如一个真实电压读数有说服力。很多时候问题不在代码而在你没怀疑过的 3.3V 参考电压上。