news 2026/9/9 10:15:51

树莓派Pico ADC应用全攻略:从machine.ADC API到定时器中断实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Pico ADC应用全攻略:从machine.ADC API到定时器中断实战

几个月前有个朋友问我:树莓派 Pico 的 ADC 到底怎么用?他买了一堆传感器,结果到手发现读出来的数要么跳得离谱,要么电压永远不对。后来我跟他说,问题大概率不在传感器,而在你对 Pico 这个 12 位 ADC 的理解方式。Pico 能跑 MicroPython,machine.ADC用起来确实简单,可简单不等于没讲究。这篇东西就把我从 API 到定时温度采集的全部经验捋一遍,包含定时器中断的踩坑记录,给准备用 Pico 做数据采集的兄弟们当个参考。

文章会按这个顺序推进:先搞清楚 RP2040 的 ADC 硬件底细,再聊machine.ADC的常用姿势和换算公式,然后拿“定时采集内部温度”这种典型场景做实战,最后重点讲定时器中断里调 ADC 的那些坑。内容不难,但都是能直接抄作业的经验。

1. 项目概述:Pico 的 ADC 到底怎么定位

1.1 硬件上先看清楚:ADC 引脚与参考电压

树莓派 Pico 的主控 RP2040 内置一个 SAR 型 ADC,12 位分辨率,最多支持 5 个输入通道。其中 4 个从 GPIO26~GPIO29 引出,对应内部 ADC0~ADC3;第 5 个通道比较特殊,接在芯片内部温度传感器上。普通 MicroPython 用户最常用的就是 GPIO26~28,因为 GPIO29 在很多最小系统板子上被 VSYS 分压电路占掉了,你接到别的信号会导致读数异常。

参考电压对读数影响很大。RP2040 的 ADC 直接以电源电压 VDDA 作为参考,Pico 开发板上 VDDA 是 3.3V,所以输入范围就是 0~3.3V。这意味着如果你把 5V 电平输入直接怼进去,芯片会损坏,一定要先分压或用运放做电平转换。这一点新手特别容易翻车,我在社区里见过不止一次“GPIO 冒烟”的帖子,全是忽略参考电压导致的。

1.2 12 位分辨率够不够用,心里要有数

ADC 分辨率 12 位意味着满量程 4096 个台阶,在 3.3V 下每一步大约 0.806mV。注意,这是理论值,实际会受到电源噪声、参考电压温漂、PCB 布局等影响,有效位数可能只有 10 位左右。所以如果你追求 0.1% 以上的高精度测量,直接拿 Pico 内置 ADC 做精密仪器是不现实的,你的瓶颈根本不在软件 API,而在模拟链路。

但是做常规的电池电压监测、温度采集、旋钮控制、电流粗略检测,12 位完全够用。比如我们要测温度,传感器输出变化几十毫伏对应几摄氏度,用 12 位分辨率能轻松分辨 0.5 摄氏度,配合后续软件滤波,工程上很香。很多人容易犯的毛病是拿着 12 位 ADC 想干 24 位 Δ-Σ ADC 的活,那不是 Pico 该承担的工作。

1.3 ADC 采样原理:SAR 是逐次逼近,不是采样就完事

很多初学者以为 ADC 就是一个“电压整形”的映射,实际上 SAR ADC 内部有一个采样保持电路,先把模拟电压快速“抓住”到内部电容上,然后通过一个比较器,从最高位到最低位逐次比较,12 位就需要 12 个时钟周期完成逼近。所以每次转换至少需要一个采样周期加一个转换周期,再加上软件读取时间,MicroPython 的read_u16()底层已经把这些周期封装好了,我们不用碰寄存器,但心里要明白:你不能像读数字 GPIO 那样高频连续采集,每次转换之间必须留足时间。

RP2040 的 ADC 支持最高 500ksps 采样率,也就是每秒 50 万次,理论转换周期约 2us。但 MicroPython 因为解释开销,实际上你连续调用read_u16()的速度远达不到这个上限,实测大概每秒十几万到几十万次,主要看固件版本和代码复杂度。如果你需要几 MHz 的高速 ADC,那 Pico 不太合适,考虑带独立高速 ADC 或换更强平台才是正道。

2. machine.ADC API 使用全解

2.1 初始化 ADC 的三种姿势,千万搞清 GPIO 编号和通道号

MicroPython 的machine模块提供了极其简洁的 ADC 类,初始化方式拢共就三种:

from machine import ADC, Pin # 方式一:直接传 Pin 对象 adc = ADC(Pin(26)) # 方式二:传 GPIO 编号 adc = ADC(26) # 方式三:读内部温度传感器,传特殊通道号 temp_sensor = ADC(4)

这里就有个坑:在 RP2040 移植版里,ADC(26)中的 26 是 GPIO 引脚号,不是 ADC 通道号;而ADC(4)里的 4 又代表内部温度传感器通道,不是 GPIO4。不同固件对整数参数的解析有差异,网上有的教程写ADC(0)也能读到 GPIO26 的数据,那是因为某些版本做了通道号兼容。为了跨固件稳定,我建议传递 Pin 对象或者 GPIO 编号,不要用通道号。内部温度传感器则固定用ADC(4),这是官方约定,几乎所有主流固件都支持。

还有一个注意事项:如果你用的是 Pico W,ADC 引脚位置和经典 Pico 一样,但 W 版增加了无线模块,有些引脚不能再用,需要查 datasheet。比如 Pico W 的 GPIO29 被用于测量无线模块供电(VSYS),所以如果你在 W 板子上用 GP29 做普通 ADC 采集,会读到固定的分压值,而不是你想要的信号。

2.2 读数方法:read_u16 默认 16 位,但本质是 12 位向左漂移

MicroPython 的 ADC 对象常用方法有两个:read_u16()read_ut16()。后者在较新固件里已经废弃,别再用。read_u16()返回 0~65535 的整数,看起来像 16 位分辨率,实际上是把 12 位 ADC 原始值左移 4 位填充出来的。也就是说,原始 12 位读数(0~4095)乘以 16,就得到read_u16()结果。

这一点必须想清楚,否则你会误以为 Pico 的 ADC 有 16 位分辨率。想得到真正的 12 位原始值,直接右移 4 位就行:

raw_12 = adc.read_u16() >> 4

如果你只关心相对位置,比如判断电位器转了多少,直接拿read_u16()的值做比例计算也可以,但和电压对应时,务必先右移回 12 位再计算,或者直接用read_u16() / 65535 * 3.3的公式,结果是一样的。

2.3 数值到电压、温度的换算,这才是关键

假设我们读了一个外部电压信号,接在 GPIO26 上,参考电压为 3.3V。那么电压计算公式是:

raw = adc.read_u16() # 0~65535 voltage = raw * 3.3 / 65535 # 单位 V

或者更贴近底层:

raw_12 = adc.read_u16() >> 4 voltage = raw_12 * 3.3 / 4095

两个公式的差别很小,因为 65535 和 409516 并不完全相等(409516=65520),所以两种算法在满量程时会有约 0.02% 的差异,实际使用可以忽略。

读内部温度传感器时,官方数据手册给出的计算公式为:

温度(°C) = 27 - (电压 - 0.706) / 0.001721

这个公式怎么来的?RP2040 的温度传感器在 27°C 时典型输出电压为 0.706V,温度每升高 1°C,输出电压下降约 1.721mV。所以你读到的电压越高,实际温度越低。这和很多负温度系数热敏电阻的趋势类似。换算代码:

from machine import ADC import time sensor = ADC(4) raw = sensor.read_u16() voltage = raw * 3.3 / 65535 temp_c = 27 - (voltage - 0.706) / 0.001721 print(temp_c)

运行上面的代码,如果读数比室温高个五到十度,别奇怪,因为芯片自身发热会影响内部温度传感器读数。想要准确测环境温度,还是乖乖外接 NTC 或数字温度传感器。

3. 实战:定时温度采集系统的设计

3.1 内部温度传感器校准数据去哪了

刚才的公式是典型值,受制造工艺影响,每颗芯片的斜率(1.721mV/°C)和零偏(0.706V)都有一定偏差。好在 RP2040 出厂时,芯片 OTP 里已经存了两个校准点,SDK 里提供接口可以直接读取。可惜 MicroPython 固件默认没有把这组值暴露给我们,所以你如果直接用典型公式,可能读出来偏差几度。

想在没有官方 API 的情况下校准,有两条路:一条是自己记录几个温度点做线性插值或最小二乘拟合;另一条是直接用经过校准的外部温度传感器作为基准,调换公式里的 0.706 和 0.001721。后面第 5 章会写校准方法。

3.2 用 machine.Timer 做定时采集

先不看中断的坑,我们设计一个需求:每 5 秒采集一次内部温度,连续采集 120 个点,并打印出来。最简单的方案就是用machine.Timer的周期模式:

from machine import ADC, Timer import time temp_sensor = ADC(4) count = 0 def read_temp(): raw = temp_sensor.read_u16() voltage = raw * 3.3 / 65535 return 27 - (voltage - 0.706) / 0.001721 def on_timer(t): global count t_c = read_temp() print(f"{count:03d}: {t_c:.2f} °C") count += 1 if count >= 120: t.deinit() timer = Timer() timer.init(freq=0.2, mode=Timer.PERIODIC, callback=on_timer)

这段代码能从功能上完成任务,但存在隐患:print在中断回调里执行,如果串口缓冲区满了或者阻塞,会把定时器回调卡住;而且每次回调都会新建浮点对象、打印字符串,这在 MicroPython 的 ISR 上下文里是不推荐的。下面马上说架构问题。

3.3 中断回调还是轮询:架构取舍

在 MCU 开发里,定时采集这类需求有两种典型架构:中断驱动和主循环轮询。对 Pico 这种轻量级单片机,我倾向这样选:

  • 如果采集周期较大(几百毫秒甚至几秒),而且允许有几十毫秒抖动,用主循环配合time.sleep()utime.ticks_diff()就行,代码简单、可读性强。
  • 如果采集周期要求严格,必须精确到毫秒,比如 1kHz 采样率做音频分析,那就用定时器中断,但在中断里只做最基本的数据搬移和置位,不要做任何耗时操作。
  • 如果只是“每秒读一次温度”,不是特别苛刻,直接主循环轮询足够,否则你会把精力和调试时间浪费在处理中断优先级、资源竞争等问题上。

很多新手觉得用定时器中断才显得专业,其实过度设计比不用中断更痛苦。底下我会给一个折中方案:定时器中断只置标志位,主循环看到标志后再去采集,这样既不丢节奏,又躲开了 ISR 的雷区。

3.4 完整代码:定时器置标志 + 主循环采集

下面这个版本是我在实际项目中常用的一种模式。定时器每 200ms 触发一次回调,回调里什么都不做,只把全局变量flag置为 True。主循环里检测到flag为 True,就执行一次 ADC 采集、滤波、记录和打印。这样调度精度由定时器保证,而 ADC 读取这种耗时操作全部放在主循环里,安全系数高很多。

from machine import ADC, Timer # 采集配置 ADC_PIN = 26 # 外部信号,也可以用 ADC(4) SAMPLE_INTERVAL_MS = 200 adc = ADC(ADC_PIN) flag = False samples = [] def on_timer(t): global flag flag = True timer = Timer() timer.init(period=SAMPLE_INTERVAL_MS, mode=Timer.PERIODIC, callback=on_timer) while True: if flag: flag = False raw = adc.read_u16() # 简单滑动滤波,保留最近 4 个读数 samples.append(raw) if len(samples) > 4: samples.pop(0) filtered = sum(samples) // len(samples) voltage = filtered * 3.3 / 65535 print(f"raw={raw}, filtered={filtered}, uv={voltage*1000:.2f}mV")

注意flag在中断回调和主循环之间共享,理论上存在竞争,但因为 MicroPython 的字节码执行有 GIL,同一时刻只有一个线程执行,而且中断回调会打断主循环,赋值操作简单,这里不会出问题。更讲究的做法是把flag换成machine.IRQ相关机制,但没必要。

如果需要批量记录 120 个点,可以在主循环里用count控制退出,并灵活地把数据存入数组或列表。列表 append 和 pop 在主循环里没问题,但绝不要放进 ISR。

4. ISR 避坑指南:在中断里调 ADC 是门玄学

4.1 为什么不能在 ISR 里做完整采集流程

MicroPython 的定时器回调其实是在硬件中断上下文执行的。虽然它比纯 C 的 ISR 要“宽容”,但在回调里依然要遵守三条铁律:

  • 不要做阻塞操作:比如time.sleep()print()在串口繁忙时可能阻塞,会拖垮整个系统。
  • 不要动态分配内存:MicroPython 的某些函数,比如往列表里append、创建字符串、做浮点运算,都可能触发内存分配,内部会申请堆内存,这在中断上下文里风险和开销都很大,严重时会造成内存碎片或直接触发异常。
  • 不要执行太长操作:ADC 读取本身虽然只有几十微秒,但换算成浮点温度、格式化字符串、输出日志,叠加起来可能远超一个定时器周期,导致中断堆积和漂移。

所以我的原则是:ISR 里只置标志位和记录计数器,其他事情全部放主循环。

4.2 标志位模式:把采集放回主循环

前面 3.4 的代码就是标准示范。这里我再加一个复杂一点的例子,用定时器 ISR 固定采样频率(比如 100Hz),但主循环里负责滤波、存储和显示:

from machine import ADC, Timer adc = ADC(26) sample_flag = False sample_cnt = 0 def isr(t): global sample_flag, sample_cnt sample_flag = True sample_cnt += 1 timer = Timer() timer.init(freq=100, mode=Timer.PERIODIC, callback=isr) while True: if sample_flag: sample_flag = False raw = adc.read_u16() # 到这里处理 raw,滤波、存储、转换等 print(sample_cnt, raw)

注意我把sample_cnt += 1放在 ISR 里,这简单整数操作是安全的。但不能在 ISR 里写samples.append(raw),因为这里 raw 还没读出来呢。如果把读取也放进去,其实大部分情况也能运行,但我见过不少人因此遇到MemoryError或定时器不规律,所以不建议赌这个运气。

4.3 我已踩过的坑和排查记录

分享一个真实经历:我之前做一个环境监测盒,想在每个整分记录一次温度,图省事直接用定时器回调read_temp(),然后print()。结果跑了十几分钟后,串口输出开始乱序,再过一会整个程序直接死机,看门狗都没救回来。排查后来发现是print内部需要把字符串送入 USB CDC 缓冲区,当电脑端串口工具没及时读取时,缓冲区满了,print会被阻塞。而阻塞发生在硬中断回调里,相当于整个定时器被卡死,后面的任务全部宕掉。

后来我把打印移到主循环,中断里只置一个sample_ready标志,程序稳定跑了一周没出问题。所以大家记住,凡是跟外部设备交互(串口、网络、文件写入)都尽量不要出现在 ISR 回调里。如果确实需要在 ISR 中记录数据,用array模块预分配固定长度数组,在索引处赋值,这是少数在 ISR 中相对安全的容器操作。

另外一个坑是定时器初始化参数:Timer.init(period=200)的单位是毫秒,Timer.init(freq=0.2)是 Hz,两种方式别混用。如果你写了freq=0.2,表示 5 秒一次;如果写period=0.2,那是 0.2 毫秒,也就是 5000Hz,开始疯狂采集,板子一下子就卡成 PPT 了。

5. 常见问题与工程化建议

5.1 读数抖动、漂移,怎么滤波和屏蔽

ADC 读数抖动多数来自电源纹波、参考电压波动和引脚阻抗。解决办法按优先级排序:

  • 引脚串联一个 1kΩ 电阻,再加一个 0.1uF 电容到地,组成低通滤波器,能滤掉大部分高频噪声。
  • 软件上做滑动平均,最简单的移动平均窗口取 8~16 个点即可。Pico 的 ADC 噪声不大,窗口太大会拖慢响应。
  • 如果测量对象是缓慢变化量,比如温度,可以不用平均,直接每隔 100ms 采一次,再用一阶低通滤波:filtered = (1-alpha) * filtered + alpha * new_sample,alpha 取 0.2~0.5 就挺好。

下面给一个简单中值滤波的 MicroPython 函数,适合偶尔出现尖峰的场景:

def median_filter(samples): s = sorted(samples) n = len(s) if n % 2 == 1: return s[n // 2] return (s[n // 2 - 1] + s[n // 2]) // 2 # 使用 buf = [adc.read_u16() for _ in range(7)] value = median_filter(buf)

5.2 温度不准?校准才是关键

如果你非要拿内部温度传感器做相对准确的测量,建议做一次两点甚至三点校准。具体做法:用一只已经校准的数字温度计和 Pico 放在同一环境,稳定后记录下 Pico 的换算电压;然后用公式反推出当前芯片的零偏和斜率。最简单的线性校准模型是:

电压 = a * 温度 + b

用两个温度点(比如冰水混合 0°C 和恒温箱 25°C)测出两组电压,就能求出 a 和 b,再反向计算温度。三个点更好,可以检验线性度并用最小二乘法拟合。对应代码:

# 校准点 (temp_c, voltage) p1 = (0.0, 0.721) p2 = (25.0, 0.708) # 由两点求斜率 a 和截距 b a = (p2[1] - p1[1]) / (p2[0] - p1[0]) b = p1[1] - a * p1[0] def voltage_to_temp(v): return (v - b) / a

实际校准要注意让温度充分稳定,且传感器不要受芯片发热影响,最好把 Pico 断电一段时间再测,或者外接热源稳定后读取。

5.3 多通道采集与未来扩展

Pico 的 ADC 内部虽然有多个通道,但只有一个转换器,MicroPython 层面没有提供并行采集 API,多通道只能依次读取。如果你需要同时采集多路信号,并且对时间对齐要求很高,会有一定误差。一种方法是先把每路信号保持采样,再逐个转换,但这需要外部采样保持电路。对普通低速传感器采集,依次读也够。

还有一个常被问到的问题:Pico 能用 DMA 做 ADC 连续采集吗?答案是 RP2040 硬件支持 ADC 的 FCS 队列和 DMA 请求,但官方 MicroPython 固件并没有暴露这一整套机制。如果你非要 DMA 连续采样,要么用 C SDK 写底层代码,要么选 STM32 系列,那边基于 HAL 库的 ADC+DMA 多通道扫描已经非常成熟。Pico 的定位是简单好用,真要拼 ADC 生产力,它不是首选。

最后提一句,如果你想把 ADC 精度往极限推,除了软件滤波和校准,可以在硬件上把参考电压做得更干净,比如用一个低噪声 LDO 单独给 VDDA 供电。这个方法在 Pico 板子上不太好改,但用 RP2040 自己画板时可以这样处理,实测有效位数能明显提升。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 10:14:18

摩托罗拉对讲机写频软件实操指南:从选型到报错排查

简介:摩托罗拉写频软件是一款面向摩托罗拉对讲机用户、无线电爱好者和通信维护人员的专业编程工具,用于读取和修改频率、频道、CTCSS/DCS 哑音码、发射功率等核心通信参数,兼容多种对讲机型号,帮助用户在组网、野外通信和公共安全…

作者头像 李华
网站建设 2026/9/9 10:14:03

Python模拟客户端请求:不依赖前端的接口测试实战指南

1. 为什么需要"不用前端"的模拟客户端请求1.1 前后端并行开发下的测试困局在真正的项目推进节奏里,前端页面和后端接口往往不是同一天交付的。后端把接口定义好、代码写完,前端可能还在切图或者调样式,这时候你面临一个很实际的问题…

作者头像 李华
网站建设 2026/9/9 10:13:18

HLW8032电能计量芯片参考设计深度解读:从原理图到校准实战

简介:HLW8032参考设计资料V10是一套面向嵌入式开发者的完整设计参考包,聚焦HLW8032微控制器的硬件设计、软件开发与物联网接入。资源以22.85MB的RAR压缩包形式封装,内部文件以电路原理图、PCB布局图、元器件选型指南、芯片规格书及调试工具为…

作者头像 李华
网站建设 2026/9/9 10:12:55

Pico非阻塞步进控制:用PIO实现硬件级脉冲生成

1. 为什么非阻塞式步进控制在Pico上不是“可选项”,而是“必选项”我第一次用树莓派 Pico 控制42步进电机时,写了个简单的for循环配合time.sleep_us(1000)来发脉冲——电机转得挺稳,但只要我在主循环里加一句串口打印温度,或者读一…

作者头像 李华
网站建设 2026/9/9 10:12:40

RISC-V向量扩展RVV:可变长度向量架构原理与实战

1. 为什么RISC-V向量扩展不是“锦上添花”,而是架构演进的必经门槛 你手头那块刚流片回来的RISC-V SoC,跑AI推理时功耗飙到3.2W,而隔壁ARM Cortex-A76同频下只用1.8W;你写的图像滤波内核,在RV32IMC上每像素处理要17个周…

作者头像 李华