1. 项目缘起:从“亮灯”到“读数”的跨越
玩过树莓派Pico的朋友,最开始做的项目十有八九是点亮一个LED。这就像学编程的“Hello World”,简单直接,能立刻看到反馈,成就感满满。但点亮LED只是数字世界的“开”和“关”,它无法感知我们身处的这个充满连续变化信息的模拟世界。温度的高低、光线的明暗、声音的大小、手柄摇杆的偏转,这些都是连续变化的模拟信号。要让Pico真正与物理世界互动,我们得教会它“读取”这些连续变化的量,而实现这个功能的核心钥匙,就是ADC。
ADC,全称模数转换器,你可以把它想象成一个高度精密的“翻译官”。它的工作是把传感器(比如电位器)输出的、连续变化的电压信号(模拟量),翻译成微控制器能理解和处理的数字值。Pico内部就集成了这样一个ADC模块,这让我们无需额外复杂的电路,就能轻松读取像电位器、光敏电阻、热敏电阻这类模拟传感器的数值。
我之所以选择“读取电位器数值”作为Pico模拟输入入门的第一个实战项目,原因有三。第一,硬件极简:只需要一个电位器、三根杜邦线,成本几乎可以忽略不计。第二,反馈直观:转动旋钮,数值实时变化,所见即所得,理解ADC工作原理毫无障碍。第三,应用广泛:这是后续无数项目的基础。无论是用旋钮控制LED亮度(PWM)、舵机角度,还是作为游戏设备的输入,都从这里开始。网上很多关于“树莓派Pico控制舵机”、“树莓派智能小车”的项目,其手动控制部分,核心就是读取一个或多个电位器的值。
所以,今天我们就来彻底搞懂这件事。我会带你从硬件接线、原理理解,到代码编写、数值处理,最后再到常见问题的排查与优化,手把手完成从“只会开关”到“读懂世界”的关键一步。
2. 硬件连接:三根线背后的电路逻辑
动手之前,我们得先弄清楚要接什么,以及为什么要这么接。本次项目的全部家当如下:
- 树莓派Pico:主角。
- 一个10kΩ电位器:这是我们的模拟信号源。电位器本质上是一个可调电阻,中间有一个滑臂。旋转旋钮,滑臂接触的位置改变,从而改变滑臂与两端引脚之间的电阻值。
- 三根母对母杜邦线:用于连接。
- 一条Micro-USB数据线:用于供电和程序上传。
电位器通常有三个引脚。假设我们将其平放,引脚朝下,从左至右依次编号为1、2、3。那么:
- 引脚1:通常接电源正极(3.3V)。
- 引脚3:通常接电源负极(GND)。
- 引脚2:这是滑臂(Wiper),输出一个可变的电压,它需要接到Pico的ADC输入引脚。
为什么是3.3V而不是5V?这是很多新手会困惑的地方。树莓派Pico的GPIO引脚,包括ADC输入引脚,其耐受电压最高就是3.3V。如果你接入5V,有很大的风险会永久损坏Pico的ADC模块甚至整个芯片。所以,切记:Pico的ADC参考电压就是其内部的3.3V电源,输入电压绝对不得超过3.3V。
Pico板子上哪些引脚可以做ADC输入呢?看板子背面或者引脚图,找到标有“ADCx”字样的引脚。Pico有3个专用的ADC通道(GP26/ADC0, GP27/ADC1, GP28/ADC2)。此外,板载的温度传感器也占用了一个通道(ADC4)。我们最常用、也最方便的就是GP26,也就是物理引脚31。
现在,我们来连接:
- 取一根杜邦线,连接Pico的3V3(OUT)引脚(物理引脚36)到电位器的引脚1。
- 取第二根杜邦线,连接Pico的任意一个GND引脚(例如物理引脚38)到电位器的引脚3。
- 取第三根杜邦线,连接Pico的GP26引脚(物理引脚31)到电位器的引脚2(滑臂)。
至此,一个完整的分压电路就搭建好了。3.3V的电压加在电位器两端(引脚1和3之间)。当我们旋转旋钮,滑臂(引脚2)处的电压值会在0V到3.3V之间线性变化。这个变化的电压,就通过GP26引脚送入了Pico内部的ADC模块。
注意:在接线时,尤其是通电状态下,尽量避免杜邦线金属部分相互触碰或碰到其他引脚,以防短路。可以先接好GND和3.3V,最后连接信号线GP26。
3. 代码实战:用MicroPython读取ADC原始值
硬件准备就绪,接下来就是让Pico“开口说话”。我们使用MicroPython,因为它交互性强,代码简洁,非常适合学习和快速原型开发。如果你还没有给Pico烧录MicroPython固件,需要先去树莓派官网下载固件,然后用拖拽的方式烧录,这里不再赘述。
我们将使用Thonny IDE进行编程和调试。打开Thonny,确保右下角选择了正确的解释器和端口(Raspberry Pi Pico)。
3.1 基础读取:ADC与read_u16()
MicroPython提供了非常简单的ADC操作类。核心代码只有寥寥几行,但每一行都值得深究。
from machine import ADC, Pin import time # 1. 初始化ADC对象,指定引脚为GP26 pot = ADC(Pin(26)) # 2. 循环读取并打印ADC值 while True: # 读取ADC值,范围 0-65535 pot_value = pot.read_u16() print(“ADC Value:”, pot_value) time.sleep(0.1) # 延时100毫秒,避免打印刷屏代码逐行解析:
from machine import ADC, Pin:导入必要的硬件控制模块。machine是MicroPython用于控制微控制器硬件的基础模块。pot = ADC(Pin(26)):这是关键的一步。Pin(26):首先创建一个代表GP26引脚的对象。ADC(...):然后,将这个引脚对象传递给ADC类构造函数。这个操作在硬件层面完成了两件事:一是将GP26的功能复设为ADC模式(它本身是数字GPIO),二是内部连接到ADC模块的通道0(ADC0)。- 最终,我们得到一个
pot对象,它代表了一个配置好的ADC输入通道。
pot.read_u16():这是读取ADC值的方法。它返回一个0到65535之间的整数。为什么是这个范围?因为Pico的ADC分辨率是12位。12位二进制数最大是2^12 - 1 = 4095。但read_u16()方法内部对这个12位的值进行了缩放,将其左移4位,映射到了16位无符号整数的范围(0-65535)。这样做的目的是为了提供一个标准化的、范围更大的整数值,方便后续处理。但你需要记住,其有效精度仍然只有12位,即4096个离散的等级。print和time.sleep:打印读取到的值,并延时一小段时间,让输出不至于太快而看不清。
将这段代码保存到Pico(例如命名为potentiometer.py)并运行。旋转电位器,你应该能在Thonny的“Shell”窗口看到数值在0到65535之间变化。当滑臂转到靠近GND一端时,数值接近0;转到靠近3.3V一端时,数值接近65535。
3.2 理解原始值:从65535到电压的换算
打印出来的原始数字虽然直观,但不如电压值有物理意义。我们可以根据ADC的原理,将其换算回实际的电压值。
ADC的转换公式是:测量电压 = (ADC原始值 / ADC最大数值) * 参考电压
对于read_u16()返回的值,其“ADC最大数值”是65535,参考电压是3.3V。因此:电压值 = (pot_value / 65535) * 3.3
修改一下打印部分:
while True: pot_value = pot.read_u16() voltage = pot_value / 65535 * 3.3 print(“ADC Value:”, pot_value, “-> Voltage: {:.2f}V”.format(voltage)) time.sleep(0.1)运行后,你会看到电压值在0.00V到3.30V之间变化。{:.2f}是格式化字符串,表示保留两位小数。
实操心得:这里会引出一个常见疑问——“为什么我的电压最大值到不了3.3V,可能只有3.28V或3.29V?” 这非常正常。原因有三:第一,USB端口提供的5V电压经过Pico板载稳压芯片转换为3.3V,本身存在微小误差。第二,ADC模块本身存在偏移误差和增益误差。第三,你的万用表测量也有误差。对于大多数应用,这个精度已经足够。如果项目对电压测量精度要求极高,则需要通过校准来补偿这些误差,这涉及到更复杂的知识,如测量多个标准电压点来拟合校正曲线。
4. 数据处理:让读数变得稳定可用
如果你仔细观察上一节打印的数值,即使手完全不动,数值也可能在最后几位上下跳动几个数字。这是电子系统中不可避免的噪声。直接使用这个跳动的原始值去控制其他设备(比如舵机),会导致设备抖动,体验极差。因此,我们必须对ADC读数进行滤波处理。
4.1 软件滤波:简单而有效的移动平均滤波
最常用且易于实现的滤波算法是移动平均滤波。它的思想很简单:连续采样N次,将这N个值相加后求平均,用这个平均值作为本次的有效输出。这样可以平滑掉随机的高频噪声。
from machine import ADC, Pin import time pot = ADC(Pin(26)) # 定义采样窗口大小 NUM_SAMPLES = 10 samples = [0] * NUM_SAMPLES sample_index = 0 while True: # 1. 采集一个新样本,放入数组 samples[sample_index] = pot.read_u16() sample_index = (sample_index + 1) % NUM_SAMPLES # 循环覆盖旧数据 # 2. 计算窗口内所有样本的平均值 avg_value = sum(samples) // NUM_SAMPLES # 3. 转换为电压 avg_voltage = avg_value / 65535 * 3.3 print(“Filtered ADC:”, avg_value, “-> Voltage: {:.2f}V”.format(avg_voltage)) time.sleep(0.05) # 这里延时可以短一些,因为滤波本身有平滑作用代码解析与参数选择:
NUM_SAMPLES:这个值决定了滤波的“强度”和“响应速度”。- 值越大,滤波效果越好,输出越平滑,但对信号变化的响应越慢(有延迟)。想象一下,一个装满旧水的杯子,新加一滴清水,杯子的平均颜色变化很慢。
- 值越小,响应越快,但平滑效果越差。
- 对于手动调节的电位器,
NUM_SAMPLES = 10是一个不错的起点。你可以根据实际观感调整。如果控制舵机,可能需要更平滑(值更大),如果做快速响应的游戏控制器,则需要更灵敏(值更小)。
sum(samples) // NUM_SAMPLES:使用整数除法//,因为ADC值是整数,求平均后保持整数运算效率更高。- 循环缓冲区:
sample_index = (sample_index + 1) % NUM_SAMPLES这是一个经典的技巧。它让数组Samples像一个环,新的数据不断覆盖最旧的数据,我们始终维护着最近N次的采样,无需在内存中频繁移动大量数据,效率很高。
运行滤波后的代码,你会看到输出数值变得非常稳定,只有在你明显转动电位器时才会平稳地变化。这就是软件滤波的魅力。
4.2 映射与死区处理:让数值适配你的应用
获取了稳定、准确的ADC值后,我们通常需要将它映射到另一个范围,以便直接控制其他设备。例如,用电位器控制一个0-180度的舵机。
MicroPython没有内置的map函数,但我们可以自己实现一个:
def map_value(x, in_min, in_max, out_min, out_max): """将数值x从输入范围[in_min, in_max]线性映射到输出范围[out_min, out_max]""" return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min同时,电位器在物理极限位置可能无法达到理想的0或65535,且我们可能希望在中位有一个“不敏感”区域,防止因微小抖动导致的误动作,这就需要死区处理。
下面是一个综合了滤波、映射和死区处理的示例,模拟控制舵机角度:
from machine import ADC, Pin, PWM import time # 初始化硬件 pot = ADC(Pin(26)) # 假设舵机信号线接在GP15 servo = PWM(Pin(15)) servo.freq(50) # 舵机标准PWM频率为50Hz # 滤波参数 NUM_SAMPLES = 15 samples = [0] * NUM_SAMPLES s_idx = 0 # ADC输入范围(实测调整,因为电位器可能到不了理论极限) ADC_MIN = 500 # 实测的最小值 ADC_MAX = 65000 # 实测的最大值 # 舵机PWM占空比范围 (对应0.5ms ~ 2.5ms脉冲, 50Hz周期为20000us) SERVO_MIN_DUTY = 1638 # 0.5ms / 20ms * 65535 ≈ 1638 SERVO_MAX_DUTY = 8192 # 2.5ms / 20ms * 65535 ≈ 8192 # 死区范围(中间区域不动作) DEAD_ZONE_CENTER = (ADC_MAX + ADC_MIN) // 2 DEAD_ZONE_WIDTH = 2000 # 死区半宽,可根据需要调整 def map_value(x, in_min, in_max, out_min, out_max): return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min while True: # 1. 采样与滤波 samples[s_idx] = pot.read_u16() s_idx = (s_idx + 1) % NUM_SAMPLES filtered_adc = sum(samples) // NUM_SAMPLES # 2. 死区处理 servo_duty = SERVO_MIN_DUTY # 默认值 if abs(filtered_adc - DEAD_ZONE_CENTER) > DEAD_ZONE_WIDTH: # 超出死区,正常映射 # 将ADC值约束在有效范围内,防止计算溢出 constrained_adc = max(ADC_MIN, min(filtered_adc, ADC_MAX)) servo_duty = map_value(constrained_adc, ADC_MIN, ADC_MAX, SERVO_MIN_DUTY, SERVO_MAX_DUTY) # 3. 输出到舵机 servo.duty_u16(servo_duty) # 4. 打印调试信息(可注释掉以提升性能) voltage = filtered_adc / 65535 * 3.3 print(“ADC: {:5d} | Volts: {:4.2f}V | Duty: {:5d}”.format(filtered_adc, voltage, servo_duty)) time.sleep(0.02)这个例子展示了从读取、处理到应用的全流程。ADC_MIN和ADC_MAX的实测校准、死区的引入,都是工程实践中让系统更鲁棒、用户体验更好的关键细节。
5. 进阶探索与深度优化
掌握了基础读取和滤波后,我们可以探讨一些更深入的话题,让你的项目更专业、更可靠。
5.1 理解Pico ADC的特性与局限性
Pico的ADC是12位分辨率,理论上能将0-3.3V的电压分成4096个等级。每个等级代表的电压是3.3V / 4095 ≈ 0.000806V,即大约0.8mV。但这只是理论值,实际性能受多种因素影响:
- 参考电压噪声:Pico使用内部电源作为ADC参考电压。当数字电路部分(如CPU、IO口)高速工作时,会产生电源噪声,直接影响ADC精度。这就是为什么在读取ADC时,如果同时进行Wi-Fi通信或高速GPIO切换,读数可能会发现周期性波动。
- 采样时间:ADC对输入电压进行采样需要一定时间。如果信号源内阻较大(如光敏电阻),或者采样时间太短,电容充电不充分,会导致采样值不准确。MicroPython的
ADC类通常使用默认采样时间,在大多数情况下够用。但在C/C++ SDK中,可以精细配置采样周期。 - 输入阻抗:Pico的ADC输入引脚内部有一个采样电容。在采样瞬间,它需要从信号源“吸取”一小部分电荷。如果信号源无法快速提供这个电荷(即输出阻抗太高),就会导致测量电压被拉低,产生误差。对于电位器,其输出阻抗在0-10kΩ之间变化,在中间值时约为5kΩ,对于Pico的ADC来说通常是可以接受的。但对于某些输出阻抗很高的传感器,可能需要加入电压跟随器(运算放大器电路)进行缓冲。
5.2 硬件滤波:在信号入口处净化
软件滤波是在数字域处理噪声,而硬件滤波是在模拟信号进入ADC之前就将其净化,是更根本的解决方案。最简单的硬件滤波是在ADC输入引脚和GND之间添加一个去耦电容,也称为“旁路电容”。
具体做法:在电位器滑臂(GP26)与Pico的GND之间,焊接一个0.1uF(104)的陶瓷电容。这个电容就像一个微型水库,可以吸收高频噪声的波动,为ADC引脚提供一个更稳定的电压。电容应尽可能靠近Pico的引脚放置。
对于低频信号(如缓慢转动的电位器),还可以考虑增加一个更大的电容(如10uF)来滤除低频噪声,但要注意,大电容会延长信号的响应时间。一个经典的组合是0.1uF + 10uF并联。
5.3 多路ADC读取与性能考量
Pico有多个ADC通道(GP26, GP27, GP28),你可以同时初始化多个ADC对象来读取多个电位器或传感器。
from machine import ADC, Pin import time pot1 = ADC(Pin(26)) pot2 = ADC(Pin(27)) # pot3 = ADC(Pin(28)) # 如果需要第三个 while True: val1 = pot1.read_u16() val2 = pot2.read_u16() print(“Pot1:”, val1, “Pot2:”, val2) time.sleep(0.1)但需要注意:Pico的ADC模块是单次转换的,意味着同一时刻只能对一个通道进行采样。当你快速轮流读取多个通道时,代码执行、通道切换本身会引入微小的时间差。对于需要严格同步采样的应用(比如读取立体声电位器),这个时间差可能不可接受。在这种情况下,更高级的做法是使用DMA(直接内存访问),让ADC在后台自动连续采样并存入内存,CPU只在需要时读取结果。这需要用到Pico的C/C++ SDK或更复杂的MicroPython底层库,超出了本文基础范围,但这是高性能数据采集的必经之路。
5.4 校准与提高精度
如果你需要尽可能高的精度,可以进行简单的两点校准:
- 将电位器调到已知的最低电压(如接GND,应为0V),读取此时的ADC原始值
adc_min。 - 将电位器调到已知的最高电压(如接3.3V,应为3.3V),读取此时的ADC原始值
adc_max。 - 使用这两个实测值
adc_min和adc_max,代替之前公式中的0和65535进行换算。voltage = (raw_value - adc_min) * 3.3 / (adc_max - adc_min)这可以消除ADC的偏移误差和部分增益误差。
对于追求极致精度的场合,需要考虑使用外部基准电压源。Pico的ADC参考电压引脚(ADC_VREF)在内部连接到了3.3V电源。通过一个高精度、低温漂的基准电压芯片(如REF3033)提供一个更稳定的3.3V给这个引脚,可以显著提升ADC的整体精度和稳定性。但这属于硬件改造,需要一定的电路设计和焊接能力。
6. 项目扩展:从读数到控制
掌握了电位器读数的精髓后,你就可以打开一片新天地。这里抛砖引玉,提供几个直接的扩展方向:
- 调光台灯:将ADC读取的数值,通过
map_value函数映射到0-65535范围,然后输出给一个连接LED的PWM引脚(PWM.duty_u16())。转动电位器,就能无级调节LED亮度。 - 舵机控制面板:使用两个电位器,一个控制舵机角度(0-180度),另一个控制舵机转动速度(通过改变发送角度指令的延时)。这可以用来制作一个简单的机械臂控制器。
- 模拟量数据记录仪:将ADC读取的电压值,连同时间戳,写入到Pico的Flash存储中,或者通过串口发送到电脑。你可以用它来记录一天中环境光强的变化,或者温度的变化趋势。
- 结合其他传感器:把电位器换成热敏电阻(NTC)或光敏电阻(LDR),配合一个固定电阻组成分压电路,就可以测量温度和光照。计算过程稍复杂,需要根据传感器的电阻-温度/光照曲线进行换算,但核心的ADC读取和滤波代码是完全通用的。
读取电位器,这个看似简单的项目,实际上是连接数字与模拟世界的桥梁。它涉及的ADC原理、噪声处理、值映射和校准思想,是嵌入式传感器应用中最核心的基础。希望这篇详细的拆解,能让你不仅成功让Pico读出那个旋转的数值,更能理解其背后的每一处细节,为后续更复杂的项目打下坚实的基础。在实际操作中,如果遇到数值异常跳动,首先检查接线是否牢固,电源是否稳定,然后尝试增加软件滤波的样本数,或者如5.2节所述增加一个硬件去耦电容,问题大多都能迎刃而解。