树莓派Pico ADC实战:从SAR原理到滤波校准与定时采集
如果你手里正好有一块树莓派 Pico又不想让它只当个点灯玩具那么认真玩一遍 ADC 几乎是必经之路。Pico 的 ADC 看起来太简单了machine.ADC(26)一行代码就能读数可等你真的把传感器接上去开始做定时温度采集遇到读数漂移、ISR 回调崩溃、参考电压不稳这些问题时才会发现这个外设背后藏着不少门道。这篇文章我从 RP2040 的硬件原理说起顺着machine.ADC的 API 用法一路写到定时采集、中断避坑、滤波校准和一个多按键检测的实战扩展希望你看完能少走弯路。1. RP2040 的 ADC 到底什么水平1.1 SAR ADC 的“二分查找”原理与核心参数Pico 用的 RP2040 芯片内置了一个 12 位逐次逼近型 ADC英文简称 SAR ADC。逐次逼近这名字听起来玄乎本质就是“用二分法猜电压”。想象你在天平上称一袋糖先放一个 1kg 砝码发现不够重就再加 0.5kg又超了退回换 0.25kg……每次只在当前最优解上砍半调整N 位 ADC 就是最多这样试 N 次最终猜出来一个最接近真实电压的量化值。RP2040 这颗 SAR ADC 的具体参数我列个表方便你对照参数数值备注分辨率12 bit输出 0~4095MicroPython 中常被左对齐成 16 位返回最高采样率500 kSPS数据手册标称值实际使用要考虑引脚驱动能力输入电压范围0 ~ 3.3V以 3.3V 为参考电压超过会损坏引脚线性度稳定区间0 ~ 2.4V接近 3.3V 时 INL 明显变差详见下文通道数5 路ADC0~ADC3 对应用户可用的 4 个 GPIOADC4 是内部温度传感器量化最小电压3.3V / 4096 ≈ 0.806 mV/LSB12 位 ADC 的最小可分辨电压这里有个很实用的换算逻辑。既然是 12 位 ADC当输入电压是 1V 时理想情况下采样原始值就是 1V / (3.3V / 4096) ≈ 1241。也就是说如果你读到原始值 1241就能反向推算出电压约为 1V。这个“电压除以 LSB”的思路是后面所有换算公式的基础建议你先在心里把它焊死。量化过程本身还会有量化噪声。理想 N 位 ADC 的量化噪声 RMS 值为 LSB / √12对应的满量程正弦波信噪比公式是 SNR 6.02N 1.76 dB。所以 12 位 ADC 的理想 SNR 大约是 74 dB这是理论天花板。实际芯片因为偏移误差、增益误差和 INL到这个值多少会打个折扣。1.2 为什么 Pico 不用 Σ-Δ 或折叠内插型 ADCADC 的架构其实有好几种Pico 选 SAR 并不是随手定的而是综合考虑了成本、功耗、速度和易用性。我拿几种主流架构做个横向对比架构速度分辨率延迟典型场景SAR ADC中高速中高8~16 bit单次转换无流水线延迟工业控制、MCU 内置 ADC、数据采集Σ-Δ ADC慢极高可达 24 bit有滤波延迟高精度称重、音频、传感器前端Flash ADC极快低通常 8 bit 以下接近零示波器前端、高速通信折叠内插型快中有少量流水线延迟高速数据采集场景SAR 方案最大的优点是在 12~16 位这个区间内速度、精度、面积和功耗都相对均衡而且转换过程没有流水线延迟非常适合 MCU 里那种“先切通道、再读结果、马上换下一个通道”的轮询式玩法。Σ-Δ 虽然分辨率吊打 SAR但它需要很长的抽取滤波时间完成一次有效转换可能要几毫秒这在温度采集场景里倒无所谓但对高速多通道扫描来说就不合适了。折叠内插型则主要解决 Flash ADC 比较器数量爆炸的问题Pico 这种低功耗单片机根本用不上。1.3 手册里的“2.4V 红线”和 INL/DNL 问题很多人在 Pico 上直接测 3.3V 附近的电压发现读数怎么都不准线性度也不行。这不是代码问题而是硬件特性。RP2040 数据手册给出的 INL积分非线性指标通常只保证在 0~2.4V 范围内良好越靠近 3.3V 电源轨内部采样结构和开关的线性度越差。通俗地说这个 ADC 在 0~2.4V 区间内实际转换曲线还能接近一条直线过了 2.4V 之后曲线就开始弯算出来的电压自然就对不上。助记把 ADC 当成一把尺子12 位精度只是说尺子有 4096 个刻度但不代表每个刻度都等宽。DNL微分非线性描述的是每个相邻刻度之间宽度差多少INL 则描述整把尺子的弯曲程度。对大多数业余项目来说标注的“12 位”就是“它能读出 4096 个不同数值”而真正能保证精度的有效区间得看 INL 曲线。实操建议很简单如果被测信号超过 2.4V优先用电阻分压把它降到 0.4~2.0V 区间再送 ADC这样既躲开了线性度差的区间又给自己留出了安全工作裕量。2. machine.ADC 核心 API 与接线避坑2.1 三种构造方式ADC(0)、ADC(Pin(26)) 还是 ADCBlockMicroPython 在 rp2 端口上提供了machine.ADC类用法比 C SDK 简单太多。常见的构造方式有三种我直接贴代码from machine import ADC, Pin # 方式一按通道号访问最常用 adc0 ADC(0) # 对应 GP26 / ADC0 adc1 ADC(1) # 对应 GP27 / ADC1 adc2 ADC(2) # 对应 GP28 / ADC2 # 方式二按 Pin 对象访问语义更清晰 adc0_pin ADC(Pin(26)) print(adc0_pin.read_u16()) # 方式三通过 ADCBlock 访问 block ADCBlock(0) # RP2040 上只有一个 ADC 外设 adc0_block block.channel(0, pinGP26) print(adc0_block.read_u16())我实际用的最多的还是ADC(0)这种写法因为 OSError 信息明确查手册也方便。ADCBlock 的好处是可读性强适合教学和代码 Review但普通项目里不必为了用而用。这里解释一个容易踩的坑ADC(26)传的是 GPIO 端口号而ADC(0)传的是通道号。GPIO 26 正好对应 ADC 通道 0所以你经常看到两种写法并存。但如果哪天你把ADC(27)和ADC(1)混着用发现都能读到值也别惊讶它们本来就是同一个物理引脚的两种表示法。真正要注意的是传参别超范围ADC(29)是允许的对应 VSYS 采样但ADC(4)已经不是外部引脚而是内部温度传感器用法不一样。2.2 read_u16() 与 read_uv()返回值的本质区别MicroPython 的 ADC 对象有两个最常用的读取方法read_u16()和read_uv()。它们的底层都是读同一个 12 位 ADC 寄存器区别在于返回格式read_u16()返回 0~65535 的 16 位整数相当于把 12 位原始值左移 4 位。换算电压的公式是V raw_u16 / 65535 * 3.3。read_uv()直接返回 0~3300000 的微伏整数省去了手动乘基准电压的步骤。两种方法怎么选我个人习惯在跨平台项目里用read_u16()因为它是一个标准的 MicroPython API在 ESP32、STM32 等平台上行为一致read_uv()更直观但要注意它内部会把参考电压当成精确的 3.3V 来计算如果实际 3.3V 电压有偏差读数也会有等比例偏差。一个常见的换算误区是拿read_u16()的结果直接除 4096因为有些人会想“12 位 ADC 最大值就是 4095”。但read_u16()返回的并不是原始 12 位值而是左对齐后的 16 位值。最稳妥的做法是reading / 65535 * 3.3或者干脆调read_uv()它的返回值和硬件贴合度更高。2.3 接线规范三个最容易导致读数稀碎的坑ADC 读数不稳、偏低、异常跳变很大概率不是代码问题而是电路问题。我踩过的坑基本集中在下面三类第一信号源阻抗太高。RP2040 的 ADC 前端是采样电容结构采样瞬间要从信号源吸一笔电荷。如果信号源是高阻抗输出比如一个 100kΩ 电位器直接接 ADC电容充不满采样结果就会偏低且抖动。对策是在 ADC 引脚到地之间并联一个 0.1uF 陶瓷电容组成简单的 RC 低通如果信号本身变化很慢这个电容还能顺便滤掉高频噪声。第二地线回路干扰。板子上的 ADC 引脚和 DC-DC 电感、舵机电源共地时大电流变化会在 PCB 地线上产生压降ADC 读到的电压实际上是“信号源电压地线噪声”的叠加。实战中最快的验证办法用一根粗跳线把 Pico 的 GND 和信号源的 GND 就近短接看读数是否明显变稳。第三参考电压不稳。Pico 的 ADC 参考电压就是板上的 3.3V而 3.3V 通常来自板载 RT6150 或外部输入的电源芯片。如果在 ADC 采样时恰好有大负载开关3.3V 会波动ADC 读数就会跟着漂。做高精度测量时建议在 3.3V 引脚附近加一级 LC 滤波或者改用外部高精度基准给 ADC 供电虽然 Pico 板子上并不方便改但你要有这个意识。如果被测量程超过 3.3V常规手段是电阻分压。比如测 12V 电池用 10kΩ 和 3.3kΩ 分压将 12V 映射到约 2.97V。分压电阻的精度直接决定了测量精度1% 精度的贴片电阻在这个场景下还算勉强够用想再抠精度就得用 0.1% 的低温漂电阻了。3. 内部温度传感器与定时采集实战3.1 RP2040 内部温度传感器原理与换算公式RP2040 内部集成了一个温度传感器挂在 ADC 通道 4 上。它的工作原理是二极管正向压降随温度近似线性下降芯片出厂时标定了一个粗略的线性模型27℃ 时输出电压约 0.706V温度每升高 1℃电压下降约 1.721mV。换算公式为T 27 - (V - 0.706) / 0.001721其中 V 是传感器输出电压单位是 VT 是摄氏度。直接用公式写进 MicroPython 就能跑from machine import ADC sensor_temp ADC(4) v sensor_temp.read_uv() / 1_000_000 temperature 27 - (v - 0.706) / 0.001721 print(Temperature:, temperature)但这里有个实战中几乎一定会遇到的坑公式算出来的温度往往偏高。我在室温约 25℃ 的环境下实测直接套公式经常读到 28~30℃最大偏差能到 3~4℃。原因在于芯片出厂标定并不精细每颗芯片的温度传感器曲线都存在个体差异。Raspberry Pi 官方的 Pico Python SDK 示例代码也直接用了这条公式但对精度有要求的项目最好用外部温度计比如 DS18B20 或水银温度计做一次单点校准。校准的思路不复杂在稳定温度下用高精度温度计读一个真实温度 T_real同时用公式读一个 T_raw然后计算偏移差offset T_real - T_raw后续所有读数都加上这个 offset。用代码表示下面这个改进版就稳多了def read_temp_calibrated(offset0.0): v sensor_temp.read_uv() / 1_000_000 temp 27 - (v - 0.706) / 0.001721 return temp offset我实测下来不同批次 Pico 的偏差会比较稳定校准一次之后在 10~50℃ 范围内大致能做到 ±1.5℃ 以内的精度。对“看个大致温度”这种场景完全够用了。3.2 用 machine.Timer 实现每隔 1 秒的定时采集定时温度采集最常见的做法是用machine.Timer周期性地触发回调函数在回调里读一次 ADC 原始值并把结果存进预分配缓冲区主循环再负责换算出温度。完整代码如下from machine import ADC, Timer import array BUF_SIZE 128 sensor_temp ADC(4) # 用 array 预分配缓冲区避免在 ISR 里触发内存分配 raw_buf array.array(H, [0]) * BUF_SIZE head 0 count 0 def on_timer(t): global head, count raw_buf[head] sensor_temp.read_u16() head (head 1) % BUF_SIZE if count BUF_SIZE: count 1 timer Timer(0) timer.init(period1000, modeTimer.PERIODIC, callbackon_timer) import time while True: if count 0: # 取最新一个样本因为温度变化很慢不必把 128 个点全拿出来 idx (head - 1) % BUF_SIZE raw raw_buf[idx] v raw / 65535 * 3.3 temp 27 - (v - 0.706) / 0.001721 print(temp %.2f C, raw %d % (temp, raw)) time.sleep(0.5)这段代码的关键在于中断回调里只做了两件事一次read_u16()和一次数组写入没有浮点运算没有print没有创建新对象。等主循环有空了再把缓冲区里的原始值取出来做电压换算和温度计算。这样设计有一个直接好处即使主循环因为别的事卡了 2 秒定时器依然会按 1 秒间隔往缓冲区里写数据不会因为单次换算时间过长而丢失采样节拍。有朋友可能会问能不能直接在回调里打印温度当然能短时间跑也不会立刻崩但随着系统里其他中断和任务增多打印的阻塞效应会不断累积最终导致回调永远跑不完。详见下一节 ISR 避坑。3.3 采样频率怎么选温度传感器根本不需要那么快很多人一看到 ADC 有 500kSPS 的采样能力就忍不住把定时器周期设成 1ms。但温度传感器不是测扭矩信号它的响应速度通常很慢。内部温度传感器测的是硅片温度热容在那摆着从 25℃ 跳到 30℃ 往往要好几秒你 1ms 采一次得到的只是一串数值几乎相同的样本白白浪费 CPU 和中断资源。我的建议是分场景定周期观察环境温度变化1Hz 采样就够了1000ms 周期。监测负载温升趋势5~10Hz 足够100~200ms 周期。温控反馈调节10~20Hz 基本是天花板再高意义不大。MicroPython 的Timer精度本身也不是硬实时的回调延迟可能抖动几十毫秒。对温度这种慢变量来说完全无所谓但如果你在做电流环控制或 PWM 中心对齐采样就得换思路了这个我放到后面第四节聊。4. ISR 避坑指南为什么我的回调一跑就崩4.1 MicroPython ISR 的硬性限制在 MicroPython 里写中断回调舒适度和 C 语言完全不同。C 语言中你可以在中断里做很多事只要保证时间短但 MicroPython 的回调是在 Python 虚拟机里执行的而 Python 虚拟机本身有很多“隐藏开销”。我把实践中踩过的硬性限制总结成三条必须刻进脑子第一不能在 ISR 里触发内存分配。Python 里的整数、字符串、列表、字典都是堆上的对象即便写一个看似简单的也可能触发gc分配内存。在中断上下文里分配内存轻则导致延迟抖动重则直接报MemoryError然后系统重置。实际表现就是“回调跑着跑着板子重启了”。第二不要在 ISR 里调用print()。print需要加锁、拼接字符串、驱动串口这几步每一件都可能阻塞而且拼接字符串就是在分配内存。有时候短字符串偶然没崩但一旦连续打印板子重启的概率迅速上升。第三不要在 ISR 里做浮点运算。RP2040 的内核是 Cortex-M0没有硬件 FPUMicroPython 的浮点数全靠软件模拟一次乘法可能消耗几十微秒在中断里用浮点数等于无限放大延迟。一句话概括中断回调里只做“读寄存器、写预分配数组、置标志位”这类最少操作其余全部丢给主循环。4.2 安全 ISR 写法缓冲区 标志位安全写法的核心思想是把“采集”和“处理”彻底分离。采集端在中断里被触发只做最薄的写入处理端在主循环里做所有复杂计算。刚才定时器例子里已经是这个模式我再补一个带标志位的版本更接近真实项目from machine import ADC, Timer import array BUF_SIZE 64 sensor ADC(4) # 预分配两个缓冲区ISR 写 data_buf主循环读处理完再置空 data_buf array.array(H, [0]) * BUF_SIZE write_idx 0 new_data 0 def on_timer(t): global write_idx, new_data data_buf[write_idx] sensor.read_u16() write_idx (write_idx 1) % BUF_SIZE new_data 1 timer Timer(0) timer.init(period200, modeTimer.PERIODIC, callbackon_timer) while True: if new_data 0: # 把 new_data 个样本一次性取出然后清零标志 n new_data new_data 0 for i in range(n): raw data_buf[(write_idx - n i) % BUF_SIZE] # 在这里做电压换算、滤波、存盘等重活 pass注意new_data只是个整数变量在中断里自增不会触发内存分配。当主循环发现new_data 0把该处理的样本数量抄到一个局部变量n里然后立刻清标志避免在处理过程中中断函数又把新的new_data叠进来导致数据错位。这种“抄数量、清标志、分批取数”的手法是我从串口接收驱动的经验里带过来的实测在 Pico 上非常稳。4.3 为什么底层工程师更偏爱 DMA而不是 ISR如果你在 STM32、GD32 这类芯片上用 C 语言开发ADC 高速采集的“标准答案”其实是 DMA而不是中断。DMA 可以在 ADC 转换完成之后由硬件直接把结果搬运到内存缓冲区传输完毕才触发一次中断软件只需要处理整个缓冲区。这比“每采一个样本就触发一次中断、软件再搬一次数据”高效得多。GD32E230 的 ADCDMA 我用过最容易出的问题就是 DMA 传输大小和 ADC 转换模式不匹配导致缓冲区里数据“错位乱跳”。比如开了多通道扫描但 DMA 缓冲区设成单通道长度数据就会覆盖写得乱七八糟。解决方案通常是把 DMA 设为循环模式并且用双缓冲区一组填满时另一组还能继续采集软件在中断里切换读写指针即可。对比来看Pico 的 MicroPython 固件没有把 ADCFIFODMA 的能力完全暴露出来所以在 MicroPython 生态里你只能用定时器ISR 这种方案。如果你的采集频率超过 1kHz或者希望 CPU 占用率极低那就得考虑改用 Pico C/C SDK或者外接一片独立 ADC 芯片。这不是 MicroPython 不行而是脚本语言的运行时开销确实不适合高频采样场景。4.4 PWM 中心对齐采样时刻的启发热词里有一条“STM32 高级定时器 PWM 中心对齐模式和 ADC 采样时刻点设置”虽然说的是 STM32但背后的思想在 Pico 项目里一样有价值。很多电力电子闭环系统需要在 PWM 波形的特定时刻采样比如电流纹波最小的时刻这时用定时器软件触发很难做到精确必须让 ADC 由定时器的触发事件硬件启动把采样时刻和 PWM 中心对齐。Pico 的 MicroPython 里没有这么精细的硬件触发配置但如果你在写 C SDK可以关注 ADC 的 FIFO 和定时器 PWM 触发协同。对绝大多数温控、舵机控制项目用主循环轮询就够了只有当项目需要“精确到微秒级的采样节拍”才需要往底层走。这也是我为什么一直强调先把软件定时器的方案跑通再根据需求升级到 DMA 或底层 SDK不要一上来就上高射炮打蚊子。5. 数据漂移、波动与滤波让 ADC 值真正“靠谱”5.1 漂移的三大来源与排查策略ADC 读数漂移是个综合症。按我排查的顺序你会先怀疑代码再怀疑电路最后发现是电源纹波。我把常见原因分成三类一是参考电压漂移。ADC 的结果本质上是输入电压与参考电压的比值。如果 3.3V 参考电压从 3.30V 降到 3.29V即便输入电压完全不变ADC 读数也会按比例往上漂。这类漂移的特征是所有通道读数同时变幅度接近等比例。排查方法是用示波器看 3.3V 波形或者用一个已知稳定的电压源比如一颗 TL431 输出的 2.5V 基准看读数是否跟着飘。二是采样电容和输入网络受温度影响。ADC 引脚到地如果有电解电容温度变化会让容量改变。分压电阻的温漂更明显普通贴片电阻温漂可能到 100ppm/℃当环境温度波动 10℃ 时读数误差能到 0.1% 以上。这个问题的特征是慢速漂移跟着环境温度走。三是电磁干扰叠加。电机启动、继电器吸合、开关电源切换都会在 ADC 信号线上感应尖峰特征是高频率的跳变而不是持续的漂移。这种问题优先在硬件上加滤波电容软件只能用滤波算法兜底。排查策略就一句话先把“信号源”短路到地看 ADC 读数是不是接近 0再换已知电压测试逐段排除。我见过不少项目一开始就在代码里加各种滤波结果最后发现是电源引脚虚焊别学这个弯路。5.2 直接用得上的三种软件滤波软件滤波算法很多但适合在 Pico 上处理缓变信号的我推荐三种中值滤波、滑动平均和一阶低通。直接上代码import array from random import randint # 1. 中值滤波抗脉冲尖峰适合温控场景 def median_filter(values): sorted_vals sorted(values) n len(sorted_vals) return sorted_vals[n // 2] # 2. 滑动平均平滑效果好但响应变慢 class MovingAverage: def __init__(self, size10): self.buf array.array(H, [0]) * size self.size size self.idx 0 self.sum 0 def push(self, sample): old self.buf[self.idx] self.buf[self.idx] sample self.idx (self.idx 1) % self.size self.sum self.sum - old sample return self.sum // self.size # 3. 一阶低通兼顾响应和平滑alpha 越小越平滑 def lowpass_filter(new_sample, last_out, alpha0.3): return last_out alpha * (new_sample - last_out)中值滤波的做法很简单连续采 5 个或 9 个值排序后取中间那个。它对突变尖峰有极强的抑制力适合电机干扰大的环境。缺点是每来一个点都要重新排序在 MicroPython 里效率一般但温度采集本身频率很低完全跑得动。滑动平均是经典中的经典但注意它有一个副作用——会让数据“变钝”。比如真实温度从 25℃ 跳到 30℃滑动平均窗口越大达到目标值需要的时间越长。对于温度这种本来就很慢的信号窗口取 10~20 个样本就够了。一阶低通滤波是性价比最高的方案只需要两个变量就能维护当前滤波结果响应速度由 alpha 控制。我的经验是 alpha 取 0.1~0.3 之间既能看到趋势变化又能滤掉底部噪声。它的数学原理其实就是数字 RC 滤波器理解了这一点你在调参数时就会知道alpha 越大跟踪越快但留下的噪声越多。5.3 校准从两点到三点为什么三点比两点更靠谱软件滤波解决的是“随机噪声”校准解决的是“固定偏差”。RP2040 的 ADC 内部存在偏移误差和增益误差前者表现为“输入 0V 时读数不为 0”后者表现为“输入满量程时读数偏大或偏小”。两点校准可以同时矫正这两个问题。两点校准的做法准备两个已知电压比如用可调电源输出 0.5V 和 2.0V分别读 ADC 原始值然后构造一次线性拟合V_real k * V_adc b其中 k 是增益修正b 是偏移修正。实测代码可以这样写# 两个标定点(adc_reading, real_voltage) cal_points [ (0.500, 0.498), # 电源显示 0.500V万用表实测 0.498V (2.000, 1.997), ] def linear_calibrate(samples): v1_adc, v1_real cal_points[0] v2_adc, v2_real cal_points[1] k (v2_real - v1_real) / (v2_adc - v1_adc) b v1_real - k * v1_adc return k, b两点校准的约束条件是“ADC 曲线近似线性”。如果 ADC 的 INL 比较差曲线呈 S 形只在两端拉一条线中间区域误差可能仍然很大。这时候引入第三个标定点做分段线性拟合整体精度会明显改善。比如选 0.5V、1.5V、2.5V 三个点把测量范围拆成两段每段用两点线性公式代码稍微复杂一点但换来的是在非线性区间的精度回血。我对 Pico 的实际感受是0~2.4V 区间内两点校准已经能把误差压到 10mV 以内如果非要在 2.4V~3.3V 区间测量三次标定的优势才会显现。所以我的建议是对普通项目做两点校准就够追求数据严谨再上三点。6. 扩展实战ADC 按键、VSYS 监测与舵机闭环6.1 用单个 ADC 引脚读取多个按键ADC 不仅能测传感器还能做按键。原理是利用电阻分压网络让每个按键按下时产生不同的分压值软件根据 ADC 读数落在哪个区间来判断是哪个按键。这样只需占用一个 ADC 引脚就能扩展出多个按键对 GPIO 紧张的板子很友好。以两个按键为例电路可以这样接ADC 引脚通过一个 10kΩ 上拉电阻接到 3.3V两个按键分别串联不同电阻到地。假设按键 A 串联 10kΩ、按键 B 串联 2kΩ那么不按任何键时 ADC 读到接近 3.3V按下 A 时分压点约为 3.3V * 10k / (10k 10k) 1.65V按下 B 时分压点约为 3.3V * 10k / (10k 2k) ≈ 2.75V。代码里只要判断读数落在哪个范围from machine import ADC, Pin adc_key ADC(Pin(26)) KEY_A_LOW 0.9 / 3.3 * 65535 KEY_A_HIGH 1.1 / 3.3 * 65535 KEY_B_LOW 2.2 / 3.3 * 65535 KEY_B_HIGH 2.4 / 3.3 * 65535 def read_key(): val adc_key.read_u16() if KEY_A_LOW val KEY_A_HIGH: return A if KEY_B_LOW val KEY_B_HIGH: return B return None做 ADC 按键时电阻选择的关键是让各档电压差至少大于噪声幅度。我的经验是相邻档位电压差不要小于 200mV判据区间也不要取得太宽否则两个按键按下时会被误判成同一个键。另外要小心“按键同时按下”的情况分压结果往往落到某个未定义区间代码里遇到这种情况直接返回 None不要把未定义值映射到真实按键上。6.2 用 ADC(3) 监测 VSYS 电压顺便聊电压监测设备的原理Pico 板子上有一个被很多人忽略的 ADC 通道ADC3 对应 GP29但它在板卡内部已经被分压电阻接到了 VSYS。也就是说你可以直接用 ADC(3) 读取外部供电电压判断电池电量或 USB 供电是否正常。换算公式是V_vsys ADC(3).read_u16() / 65535 * 3.3 * 2乘 2 是因为板载分压电阻是 100kΩ 和 100kΩ分压比 1:1VSYS 经过两个电阻分压后送到 ADC 引脚软件侧把这 1:1 的衰减还原回来即可。如果你想监控 5V 供电或锂电池电压不需要额外接任何硬件写一段代码就能在主循环里定期打印from machine import ADC import time vsys_adc ADC(3) while True: v vsys_adc.read_u16() / 65535 * 3.3 * 2 print(VSYS %.2f V % v) time.sleep(1)这种“分压ADC 读电压”的思路其实就是服务器管理控制器 BMC 检测板卡各路电压的简化版。BMC 通过一颗多通道 ADC 配合一堆分压电阻把主板上 12V、5V、3.3V、Vcore 等电压统一采进来上报给上位机做监控告警。Pico 也能玩同样的活儿只不过规模和精度上了一个台阶。项目中如果要做电池供电的设备我强烈建议先把 VSYS 监测接进去电量预警能帮你省掉大量排查“突然断电”的时间。6.3 温度采集延伸到舵机控制的 PWM 思路ADC 的值最终要服务于控制。比如做一个恒温箱温度高了就开风扇温度低了就关风扇再进一步还可以用舵机控制风门开度。舵机控制的本质是 50Hz 的 PWM0.5ms~2.5ms 的脉宽对应 0~180 度。Pico 上用machine.PWM就能驱动from machine import Pin, PWM servo PWM(Pin(16)) servo.freq(50) # 50Hz 是舵机标准频率 def set_servo_angle(angle): # 将 0~180 度映射到 0.5ms~2.5ms 脉宽 duty_cycle (angle / 180) * 2000 500 # 单位微秒 # 50Hz 周期是 20000usduty_u16 满量程 65535 对应 20000us servo.duty_u16(int(duty_cycle / 20000 * 65535)) set_servo_angle(90)把 ADC 温度采样和舵机 PWM 结合起来就是典型的温控闭环主循环里读温度 → 滤波 → 计算目标角度 → 更新 PWM 占空比。这种控制回路对实时性要求并不高10Hz 的采样率已经非常充裕。真正要抠细节的时候才需要考虑 PWM 中心对齐采样、DMA 连续采集这些问题那也就是另一个段位的话题了。在实际项目里我最深的体会是Pico 的 ADC 是一颗“中等生”水准的外设你用好了它可以稳定完成绝大多数传感器采集任务但你也不能对它抱有不切实际的期望毕竟它只有 12 位精度、500kSPS 速率参考电压还是直接挂在 3.3V 上的。学会在他能力范围内做设计比硬逼它跑出偏离手册的性能要靠谱得多。这句话不是劝退而是让你在动手前就把预期管理好后续的调试过程才会顺利很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →