ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

树莓派Pico ADC实战:从machine.ADC到NTC定时温度采集与ISR避坑

2026/9/9 7:40:28 拓冰建站 浏览量
树莓派Pico ADC实战:从machine.ADC到NTC定时温度采集与ISR避坑 树莓派 Pico 上手做模拟量采集ADC 是绕不开的一个坎。我最近用 Pico 做了一套定时温度采集的小装置一头扎进machine.ADC这个 API 里从读引脚电压到接 NTC 热敏电阻再到用定时器配合中断做定时采样中间踩了不少坑尤其是 ISR中断服务程序里的各种隐性问题。这篇文章把我从零到跑通的完整过程拆开讲清楚适合刚接触 Pico ADC 的初学者也适合已经写过 ADC 但总被采样不稳、定时不准困扰的朋友。你会拿到可以直接抄的代码、电压温度换算的计算过程以及我在中断里踩过的那些坑。1. Pico 的 ADC 硬件底子先看明白再动手搞软件控制之前最好先把 Pico 这颗 RP2040 芯片的 ADC 外设摸清楚。否则后面写代码时遇到读数不对、引脚失效、通道选错这类问题你会被卡得很痛苦。1.1 三个外部通道一个内部温度通道树莓派 Pico 上真正能直接接到外部模拟信号的 ADC 通道一共有三个GP26、GP27、GP28分别对应 ADC0、ADC1、ADC2。三个通道输入范围都是 0 到 3.3V超过 3.3V 必须用分压电阻处理直接怼上去大概率会烧引脚。很多新手不知道的是RP2040 内部还带了一个温度传感器挂在 ADC4 通道上不需要额外接任何硬件就能读芯片温度。这个传感器精度不算高用来做参考或者粗略的过热保护没问题但如果要做产品级的温度测量还是乖乖外接传感器更靠谱。另外 ADC3 通道被板子上的 VSYS 检测电路占用了用来监测 USB 供电电压一般场景用不上。这段硬件信息直接决定了代码里怎么传参数用machine.ADC(26)读的是 GP26 上的电压而想读芯片内部温度得用machine.ADC(4)因为这里的 4 是 ADC 通道号而不是引脚号。这个细节很容易把人绕晕我见过不少人写machine.ADC(4)以为在接第四个引脚结果完全读不到数据。1.2 SAR 型 ADC 的逐次逼近原理RP2040 的 ADC 内核是经典的 SAR逐次逼近寄存器架构分辨率为 12 位。所谓逐次逼近你可以理解成用二分法猜一个电压值先拿参考电压的一半去比较输入信号结果高了就往下猜低了就往上猜如此反复 12 次最终拼出一个 12 位的二进制结果。这个原理看起来简单但它决定了 ADC 的两个关键行为。第一每次转换都需要足够的时间让内部电容完成充放电也就是采样保持阶段。如果采样时间太短外部信号源无法给采样电容充满电读数就会偏低。第二输入信号源的内阻会影响充电速度高内阻信号源需要更长的采样时间。所以外接传感器时尽量选内阻小的输出电路或者加一个运放做缓冲。12 位分辨率意味着 3.3V 参考电压下每个 LSB最小有效位对应的电压是 3.3 / 4096约等于 0.806mV。这个精度对于温度采集、电位器分压这类场景绰绰有余但如果你想做高精度传感器信号采集比如称重传感器的毫伏级输出这个分辨率就不够看了需要外接 24 位 ADC 芯片。1.3 采样周期和时钟频率的设置逻辑RP2040 的 ADC 时钟理论上最高可以跑到 48MHz但注意这个时钟频率和采样率是两回事。ADC 内部有一个分频器通过ADC_CS寄存器的配置决定实际采样周期。MicroPython 里默认配置通常已经满足大多数场景但如果采样速率特别高比如连续采集音频波形就需要关注采样周期是否足够否则转换结果不稳定。整个过程简单说就是先设置时钟分频然后发起一次转换转换完成后结果会放入结果寄存器。MicroPython 对这些底层寄存器做了封装我们只需要调用read_u16()但了解底层时序有助于理解为什么同样的代码有时快有时慢、读数有时稳有时跳。2. machine.ADC API 逐个拆解从创建对象到电压换算麦克风版的machine.ADC接口本身不复杂核心方法就几个但用法细节特别容易踩雷。这一章我按使用顺序讲清楚。2.1 创建 ADC 对象传引脚号还是通道号这是新手最容易混的地方。machine.ADC()构造函数支持的参数形式有几种from machine import ADC, Pin # 方式一直接传引脚号 adc0 ADC(26) # 方式二传 Pin 对象 adc0 ADC(Pin(26)) # 方式三传 ADC 通道号从 0 开始 adc0 ADC(0)关键问题是ADC(26)和ADC(0)到底是不是同一个东西在 RP2040 上ADC(26)表示引脚号 26即 GP26对应 ADC0而ADC(0)表示 ADC 通道 0结果也指向 GP26。两者最终读到的是同一个物理引脚但写法思路不同。我强烈建议统一用 Pin 对象或引脚号的方式因为这样代码可读性高别人一看就知道你在读哪个引脚。而ADC(4)读的是芯片内部温度传感器相信很多人已经在这里栽过跟头。如果你想读外部信号永远不要用通道号 4那是给内部传感器用的。2.2 read_u16() 的返回值到底是多少位read_u16()这个函数名里的 16 非常容易误导人。RP2040 的 ADC 明明是 12 位分辨率为什么返回 16 位范围的值这是因为 MicroPython 为了让不同开发板的 ADC 接口保持一致统一返回 0 到 65535 之间的无符号整型。底层相当于把 12 位结果左移了 4 位。所以电压换算公式应该是voltage raw_value * 3.3 / 65535而不是raw_value * 3.3 / 4096虽然数值上接近但逻辑上不对。用前者算出来的结果才是真正对应的电压值。比如读到的原始值是 32768电压就是 32768 * 3.3 / 65535约等于 1.65V正好是 3.3V 的一半完全符合直觉。这里还有一个隐藏细节read_u16()返回的是单次采样的结果。如果你在传感器输出噪声比较大的环境下单次采样结果可能波动明显。后续在实战环节我会讲到滤波这是提高读数稳定性的关键操作。2.3 read_uv() 和内部温度传感器的读取较新版本的 MicroPython 固件支持read_uv()方法直接返回微伏1V 1000000uV单位的电压值。这样读取电压就简化成了voltage_uv adc.read_uv() voltage_v voltage_uv / 1000000尤其是读内部温度传感器用read_uv()很方便因为换算温度公式里需要电压值from machine import ADC adc_temp ADC(4) voltage adc_temp.read_uv() / 1000000 # 芯片内部温度换算公式 temperature 27 - (voltage - 0.706) / 0.001721这个公式来自 RP2040 数据手册0.706V 对应 27 摄氏度电压每下降 1.721mV温度上升 1 摄氏度。用这个公式算出来的温度是芯片内部温度不是环境温度这两者差距可能不小尤其是在 Pico 高负载运行时芯片自身发热会让读数明显偏高。2.4 参考电压与衰减问题的处理我第一次用 Pico 的 ADC 时默认以为参考电压是 3.3V。大部分场景这个假设成立但要注意Pico 的 ADC 参考电压由外部引脚ADC_VREF决定板子上默认通过跳线连到了 3.3V 电源。如果你需要更高的精度可以外接一个精密参考电压源到ADC_VREF引脚这样测量结果会更稳定。另外需要特别提醒Pico 的 ADC 没有像 STM32 那样的内部衰减功能输入电压绝对不能超过 3.3V。我之前看到一个项目想测 5V 电压直接往 ADC 引脚上接了结果引脚烧了连带着整个板子都不稳定。正确做法是先用电阻分压或者电压跟随器把 5V 降到 3.3V 以下再进 ADC。3. 定时温度采集实战NTC 热敏电阻方案基础知识梳理完之后进入正题用 Pico 做一个定时温度采集系统。我选用的方案是 NTC 热敏电阻原因很简单它便宜、响应快、电路简单而且能让你真正理解分压电路和 ADC 之间的关系。3.1 传感器选型NTC 内部温度计还是数字传感器表格对比一下常见方案方案优点缺点适合场景芯片内部温度传感器零成本、零接线测的是芯片温度不是环境温度保护性监测、演示实验NTC 热敏电阻成本低、响应快、电路简单需要分压电路和公式换算环境温度采集、水温监测DS18B20 数字温度传感器直接输出数字量、精度高需要单总线协议驱动多点测温、长距离传输我选择 NTC 是因为想演示完整的模拟链路传感器 → 分压电路 → ADC 转换 → 数值换算这套思路对理解其他模拟传感器都通用。如果你纯粹想省事快速拿温度数据DS18B20 反而更快。3.2 硬件连接与分压电阻计算NTC 热敏电阻的阻值随温度变化我用的 10K NTC25 摄氏度时阻值 10KΩ配合一个 10KΩ 固定电阻组成分压电路。电路连接方式如下Pico 的 3.3V 接固定电阻一端固定电阻另一端接 NTC 一端同时引一条线到 ADC 引脚GP26NTC 另一端接 GND这样 ADC 引脚上的电压就是固定电阻两端的分压。当温度升高时NTC 阻值下降ADC 引脚电压升高反之为降低。用固定电阻在上、NTC 在下的接法得到一个正温度系数特性的输出电压便于理解。分压计算关系式设固定电阻为 R_fixedNTC 阻值为 R_ntc则 ADC 引脚电压为V_adc 3.3 * R_ntc / (R_fixed R_ntc)从 ADC 原始值反推 NTC 阻值需要先算电压再反解公式raw adc.read_u16() voltage raw * 3.3 / 65535 r_ntc 10000 * voltage / (3.3 - voltage)这里 10000 是固定电阻的阻值单位欧姆。注意电压接近 3.3V 时分母趋近于零计算容易爆炸这也是为什么 NTC 测温量程有限的原因。3.3 从阻值到温度的 B 值公式NTC 阻值和温度之间的换算最常用的是 B 值公式也叫指数模型T 1 / (1/T0 1/B * ln(R/R0)) - 273.15其中 T0 是 298.15K25 摄氏度R0 是 T0 下的阻值10KΩB 是 NTC 的 B 值参数不同型号不一样我的传感器标称 B 3950。这里 ln 是自然对数温度 T 的单位是摄氏度。代码实现import math def ntc_to_temp(r_ntc): t0 298.15 # 25摄氏度对应的开尔文温度 r0 10000 # 25摄氏度时的NTC阻值 b 3950 # NTC的B值 temp_k 1 / (1/t0 1/b * math.log(r_ntc / r0)) return temp_k - 273.15 # 转为摄氏度B 值公式在较窄的温度范围内精度尚可但如果你要测 -20 到 80 摄氏度的宽范围建议用 Steinhart-Hart 方程也就是更精确的三参数模型。不过对于大多数物联网环境监测场景B 值公式已经够用。3.4 定时器配置与回调函数设计定时温度采集需要解决定时问题。有两种常见思路在while True循环里用time.sleep()延时或者用machine.Timer定时器中断回调。加上含 ISR 避坑指南这个主题我用定时器方案来演示。定时器用法from machine import Timer def timer_callback(timer): # 定时到点触发一次温度采集 read_temperature() timer Timer() timer.init(period2000, modeTimer.PERIODIC, callbacktimer_callback)period2000表示每 2000 毫秒2 秒触发一次回调。modeTimer.PERIODIC是周期模式还有Timer.ONE_SHOT单次模式。Pico 有多个硬件定时器可以同时创建多个定时任务。这里有个设计要点不要在回调函数里做耗时操作。如果在 ISR 里直接调用read_u16()等 ADC 转换完成并换算温度整个过程可能要花好几毫秒但中断回调里的代码越短越好。所以我的做法是回调函数只做一件事就是翻转一个标志位主循环检测到标志位后执行实际采集。import machine from machine import Timer, ADC import math # 全局标志位 sample_flag False adc ADC(26) def timer_callback(timer): global sample_flag sample_flag True timer Timer() timer.init(period2000, modeTimer.PERIODIC, callbacktimer_callback) while True: if sample_flag: sample_flag False raw adc.read_u16() voltage raw * 3.3 / 65535 r_ntc 10000 * voltage / (3.3 - voltage) temp ntc_to_temp(r_ntc) print(raw:, raw, temp:, round(temp, 2), C)这个结构的好处是中断服务函数执行时间极短不会阻塞系统其他任务主循环里可以放心做耗时的数据处理不用担心影响定时精度。这也是 MicroPython 开发中推荐的中断处理模式。3.5 数据滤波与异常值处理ADC 读数不可避免地带有噪声特别是当传感器引线较长或者电源波动时。我实测下来单次采样的波动范围大约是正负 5 个 LSB对应温度大约是正负 0.1 摄氏度在要求不高时可以忽略但如果想做稳定的显示滤波是必须的。最常用的是滑动平均滤波维护一个固定长度的队列每次采样都计算平均值import collections buf collections.deque(maxlen10) def filtered_read(adc): buf.append(adc.read_u16()) return sum(buf) / len(buf)这种方法对高频噪声抑制效果好但会引入一定的响应延迟。如果你的场景对实时性要求高比如控制回路建议用一阶低通滤波只保留上一个滤波值与当前采样值按权重合并filtered 0 def lowpass(raw, alpha0.3): global filtered filtered alpha * raw (1 - alpha) * filtered return filteredalpha 越大滤波越快但噪声也越多alpha 越小曲线越平滑但响应越迟钝。实际调参时要根据采样频率和噪声水平平衡。4. ISR 避坑指南中断回调里的隐形地雷这一章是我特别想写的因为定时采集这个需求看起来简单但一旦把 ADC 读取放进 ISR 里问题就多了。我把自己踩过的坑整理出来希望能帮你绕开。4.1 中断回调里千万别做的三件事第一绝对不要在 ISR 里做阻塞式延时操作。time.sleep()、utime.sleep_ms()这类函数依赖系统节拍在中断上下文里调用可能导致定时器错乱甚至系统崩溃。我刚开始写的时候不够注意在回调里加了time.sleep_ms(50)想等待 ADC 稳定结果系统直接重启了。第二尽量避免在 ISR 里做动态内存分配比如创建列表、字典、字符串拼接。MicroPython 的垃圾回收机制在中断里运行时会与主循环的内存操作产生竞争导致不可预期的错误。print 大量输出也不要放进 ISR串口输出本身就慢会拖垮中断响应。第三不要在 ISR 里执行长时间任务。像文件读写、WiFi 请求这类操作动辄几十上百毫秒一旦放进中断整个系统的实时性就毁了。正确做法是标记事件把耗时操作留给主循环。4.2 ADC 转换时间对中断耗时的直接影响读取一次 ADC 数据实际花费多长时间read_u16()内部的工作流程是发起转换 → 等待转换完成标志位 → 读取结果。在 RP2040 上一次完整转换大约需要几微秒到十几微秒具体取决于配置的时钟分频。这个时间看起来很短但如果你的定时器中断周期本身只有 1 毫秒在中断里花掉 10 微秒到 50 微秒去读 ADC会占用中断处理时间的相当比例。更麻烦的是如果同时用同一个 ADC 外设采集多个通道在第一次read_u16()还没有完成时又发起第二次读取MicroPython 硬件层会阻塞等待导致代码卡住。我在做多通道采集时就遇到过这种情况第二通道的读取永远超时。解决方案就是前面说的标志位模式ISR 只负责置位主循环负责读取。哪怕 ADC 转换时间再长也不会影响定时器的准时触发因为 ISR 里的代码执行时间可以压缩到微秒级。4.3 machine.disable_irq 保护共享数据的正确姿势如果你在主循环和 ISR 之间共享变量比如温度值的更新可能会遇到数据竞争问题。考虑这样一个例子主循环读取shared_temp做显示同时 ISR 正在更新这个变量那么主循环可能读到半个新值加半个旧值的混合数据。MicroPython 提供了machine.disable_irq()和machine.enable_irq()来保护临界区irq_state machine.disable_irq() temp_now shared_temp machine.enable_irq(irq_state)关于disable_irq有一个很关键的细节它会返回一个状态值恢复时必须把这个值传回enable_irq而不能直接调用无参版的enable_irq。否则如果调用前中断本来就是关闭的你的恢复会让中断错误地处于开启状态。另外disable_irq保护的区域要尽量短因为中断被禁用的时间里硬件事件比如定时器回调会得不到及时响应。还有一点在某些 MicroPython 构建中disable_irq会同时影响所有中断包括系统节拍。如果保护区域内有time.sleep之类的调用系统节拍被禁用后这些调用会一直阻塞。所以临界区里只做简单的变量读写绝不调用其他 API。4.4 我实际踩过的三个坑第一个坑是在Timer回调里直接调用print输出 ADC 值。本来想省事直接把采样结果打出来结果发现串口输出速度远低于采样速度回调函数执行时间越来越长最后定时器触发间隔完全失准数据也乱成一团。后来把打印移到主循环问题立即解决。第二个坑是读内部温度传感器时用了ADC(4)却以为自己在读引脚 4。这个问题在代码里非常隐蔽因为ADC(4)语法正确、读出来的数据也正常但那读的是芯片温度不是环境温度。我在测试时怎么都搞不懂为什么温度一直在 40 多度折腾了一天才发现是通道号搞错了。第三个坑是定时器周期太短导致的回调重入。我把period设置成 1 毫秒回调函数却要执行 2 毫秒导致前一个中断还没处理完下一个中断又来了。MicroPython 的中断处理机制不会像实时操作系统那样排队而是可能出现优先级问题。最后我把周期改成了 50 毫秒并且把处理代码移出回调才算稳定下来。5. 常见问题与排查技巧速查这部分是我在实际调试中积累的排查经验整理成表格方便你快速对照。现象可能原因解决方法ADC 读数始终为 0引脚没接对或信号源被拉低万用表测量引脚电压确认接线ADC 读数始终为 65535引脚悬空或电压超过参考值加上拉/下拉电阻检查分压电路读数波动很大电源噪声大或引脚接触不良添加滤波电容改用屏蔽线软件滤波电压换算结果明显偏高分压电路阻值选得不当重新计算分压比确认固定电阻值温度值跳变NTC 引线太长受干扰缩短引线加 RC 滤波提高滤波强度定时器回调不执行回调函数传入错误或周期设置问题检查Timer初始化参数确认回调签名程序在读数后卡死中断里做了耗时操作将 ADC 读取移到主循环芯片温度一直偏高用的是 ADC4 内部传感器明白它测的是芯片温度不是环境温度快速排查第一步永远是量电压。用万用表直接测 ADC 引脚对 GND 的电压如果电压和代码里算出来的值对不上排除软件问题重点查硬件。提示ADC 引脚旁边尽量放一个 0.1uF 到 1uF 的陶瓷电容一端接引脚一端接 GND。这个小电容能滤掉不少高频噪声对提升读数稳定性立竿见影。6. 从定时采集到多通道扩展的下一步思路学会了单通道定时采集后你可以进一步扩展。Pico 的三个 ADC 引脚足够覆盖大部分传感器需求比如同时测温度和光照。多通道采集在代码上并不复杂adc_temp ADC(26) adc_light ADC(27) def read_all_channels(): raw_temp adc_temp.read_u16() raw_light adc_light.read_u16() return raw_temp, raw_light有几个细节值得注意。第一ADC 外设是共享的交替读取多个通道时最好等一次读取完全结束后再发起下一次避免内部状态冲突。第二通道切换后建议丢弃第一次的采样值因为采样电容里还残留着上一个通道的电荷尤其是两个通道电压差异很大时这个残留效应非常明显。第三如果要做更高速或更复杂的采集比如 DMA 批量采样MicroPython 在 Pico 上的支持还比较有限建议切换到 C SDK 或者用 PIO 配合实现但这已经超出本文的范围了。我个人在实际操作中的体会是ADC 调试最花时间的往往不是 API 本身而是电路层面的噪声和参考电压问题。无论软件怎么写硬件不稳定的话后面全是白费劲。先确保万用表读数和代码读数一致再去做滤波、定时这些高阶功能这个顺序千万别反过来。最后再分享一个小技巧调试时把原始 raw 值、换算电压、最终物理量同时打印出来一旦数值对不上扫一眼就能定位是 ADC 层、换算层还是传感器层出了问题。