树莓派如何用SPI读取ADC?手把手教你搭建高精度模拟信号采集系统
你有没有遇到过这样的情况:想用树莓派读一个温度传感器的电压,却发现它没有模拟输入口?没错,尽管树莓派功能强大,但它天生缺少ADC(模数转换器)。这意味着所有来自电位器、光敏电阻、热敏电阻甚至麦克风的模拟信号,都无法直接被树莓派“看懂”。
那怎么办?难道只能换平台或者加一块Arduino做中转?
其实不用。我们只需要一片几块钱的芯片——比如MCP3008,再通过树莓派自带的SPI 接口,就能轻松实现多路模拟信号的高速、稳定采集。
今天我就带你从零开始,一步步搞定这个在物联网和嵌入式项目中最常用也最关键的技能:用 SPI 读取外部 ADC 数据。不只是贴代码,更要讲清楚每一步背后的逻辑,让你真正掌握原理,举一反三。
为什么是 SPI?而不是 GPIO 模拟读或 I²C?
在动手之前,先回答一个问题:为什么非得用 SPI?
有人可能会说:“我听说过可以用脉冲宽度调制(RC 充放电)来‘模拟’读取模拟值。” 这种方法确实存在,但问题很多:
- 精度极低,受寄生电容影响大;
- 响应慢,不适合连续采样;
- 占用 CPU 时间长,无法用于实时系统。
而另一个常见选择 I²C 虽然接线简单(只有两根线),但它的速度通常限制在几百 kHz 到 1MHz,对于需要快速轮询多个通道的应用来说显得力不从心。
相比之下,SPI 的优势就非常明显了:
- 支持高达数十 MHz 的时钟频率(树莓派实测可达 32MHz);
- 全双工通信,发送命令的同时就能接收数据;
- 协议简单,无地址仲裁开销,延迟更低;
- 可靠性高,特别适合与 ADC、DAC、Flash 存储等外设配合使用。
所以,在追求性能和稳定性的项目中,SPI + 外部 ADC 是最佳组合之一。
MCP3008:你的第一块 SPI ADC 芯片
我们要用的主角是Microchip 出品的 MCP3008—— 一款经典的 10 位、8 通道 SPI ADC 芯片。别看它小,能力可不小。
它到底能干什么?
简单说:把 0~3.3V 的电压变成 0~1023 的数字值,而且可以同时接 8 个不同的传感器!
比如你可以:
- CH0 接光照强度,
- CH1 接温度,
- CH2 接声音,
- ……一直到 CH7 接压力传感器。
然后让树莓派轮流问:“现在每个传感器是多少?”——一次通信只要几十微秒,完全感觉不到卡顿。
关键参数一览
| 参数 | 数值 | 说明 |
|---|---|---|
| 分辨率 | 10 位 | 最大输出值为 $2^{10} - 1 = 1023$ |
| 输入范围 | 0 ~ VREF | 一般接 3.3V,即每 LSB ≈ 3.22mV |
| 最大采样率 | ~200ksps | 实际受 SPI 速率和软件控制限制 |
| 工作电压 | 2.7V ~ 5.5V | 完美兼容树莓派 3.3V 电平 |
| 接口 | SPI Mode 0 / 1 | 默认推荐 Mode 0 |
✅ 提示:如果你需要更高精度,后续可以升级到 MCP3208(12 位)或 ADS1115(16 位,I²C 接口)
硬件怎么连?一张图看懂 SPI 接线
先把 MCP3008 插到面包板上,然后按照下面这张表连接到树莓派 GPIO 引脚(以标准 40 针 Raspberry Pi 为例):
| MCP3008 引脚 | 功能说明 | 连接到树莓派 GPIO | 物理引脚号 |
|---|---|---|---|
| Pin 9 (VDD) | 电源正极 | 3.3V | Pin 1 |
| Pin 10 (VSS) | 地 | GND | Pin 6 |
| Pin 11 (CLK) | SCLK(时钟) | SCLK → GPIO11 | Pin 23 |
| Pin 12 (DOUT) | MISO(主入从出) | MISO → GPIO9 | Pin 21 |
| Pin 13 (DIN) | MOSI(主出从入) | MOSI → GPIO10 | Pin 19 |
| Pin 14 (CS/SHDN) | 片选 | CE0 → GPIO8 | Pin 24 |
| Pin 15~18 | 模拟输入 CH0~CH7 | 接传感器输出 | - |
| Pin 16 (VREF) | 参考电压 | 接 3.3V | 同 VDD |
📌特别注意:
- 所有电源引脚都要接稳!建议在 VDD 和 GND 之间并联一个0.1μF 陶瓷电容,滤除高频噪声。
- VREF 必须接干净的参考电压。如果和电机共用电源,读数会跳得厉害。
- GND 一定要共地!否则会出现“地弹”,导致数据错乱。
软件准备:开启 SPI 接口
硬件接好了,接下来要告诉树莓派:“我要用 SPI。”
默认情况下,树莓派的 SPI 是关闭的,我们需要手动启用。
打开终端,运行:
sudo raspi-config进入菜单:
Interface Options → SPI → Yes → Enable SPI interface
确认后重启:
sudo reboot重启完成后,检查设备是否识别成功:
ls /dev/spi*你应该看到两个设备文件:
/dev/spidev0.0 /dev/spidev0.1这代表 SPI 总线 0 上的两个片选设备(CE0 和 CE1)。我们现在用的是 CE0(GPIO8),所以对应/dev/spidev0.0。
Python 实现:三步走策略读取 ADC
我们使用 Python 中的spidev库来操作 SPI 设备。它是 Linux 下标准的用户空间 SPI 驱动接口封装,轻量又高效。
第一步:安装依赖库
pip install spidev不需要 root 权限也可以读写 SPI 设备(只要用户在spi组里,通常默认已加入)。
第二步:编写核心读取函数
下面是完整可运行的代码,我已经加上了详细注释,帮你理解每一行的意义。
import spidev import time # 初始化 SPI spi = spidev.SpiDev() spi.open(0, 0) # 总线0,设备0(对应 CE0) spi.max_speed_hz = 1350000 # 设置时钟频率为 1.35MHz spi.mode = 0 # CPOL=0, CPHA=0 → SPI Mode 0 def read_adc(channel): """ 读取 MCP3008 指定通道的 ADC 值(0~7) """ if not 0 <= channel <= 7: raise ValueError("通道必须是 0~7") # 构造发送的三个字节 cmd1 = 0x01 # 起始位 cmd2 = (0x08 | channel) << 4 # SGL=1 (单端), 并设置通道号 cmd3 = 0x00 # 无意义占位 # 发送并接收等长数据 raw = spi.xfer2([cmd1, cmd2, cmd3]) # 解析返回值: # 返回格式: [dummy, MSB部分, LSB部分] # 真正的数据在 reply[1] 的低2位 + reply[2] 的全部8位 value = ((raw[1] & 0x03) << 8) | raw[2] return value # 主循环测试 try: while True: adc_val = read_adc(0) # 读取 CH0 voltage = (adc_val / 1023.0) * 3.3 # 转成电压(V) print(f"ADC: {adc_val}, Voltage: {voltage:.3f}V") time.sleep(0.5) except KeyboardInterrupt: print("\n退出程序") finally: spi.close() # 记得关闭设备🔍 关键点解析
1.spi.open(0, 0)是什么意思?
- 第一个
0表示 SPI 总线编号(通常是 0); - 第二个
0表示设备选择(CE0 → spidev0.0,CE1 → spidev0.1)。
2. 为什么要设max_speed_hz = 1350000?
虽然 MCP3008 支持最高 3.6MHz,但在实际布线中,过高的频率容易引起信号完整性问题(尤其是面包板)。1.35MHz 是一个兼顾速度与稳定的折中值,实测非常可靠。
3. 为什么是mode=0?
因为 MCP3008 要求:
- 时钟空闲时为低电平(CPOL=0)
- 数据在时钟上升沿采样(CPHA=0)
合起来就是SPI Mode 0,必须匹配,否则通信失败。
4.xfer2()和xfer()有什么区别?
xfer():每次传输结束后释放 CS(片选),适合分段通信;xfer2():保持 CS 拉低完成整个事务,更符合 MCP3008 的时序要求,推荐使用。
5. 数据是怎么解析出来的?
这是最容易出错的地方!我们来看手册里的时序图:
MCP3008 在收到命令后,会在第三个字节期间开始返回数据。但由于 SPI 是同步移位,主机必须发送三个字节才能换来三个字节。
返回的数据结构如下:
reply[0]: dummy byte(无效) reply[1]: 包含结果的高两位(bit 9 和 bit 8) reply[2]: 结果的低八位(bit 7 ~ bit 0)所以我们这样提取:
value = ((raw[1] & 0x03) << 8) | raw[2]raw[1] & 0x03:取出低两位(即原始数据的 bit9 和 bit8)- 左移 8 位后与
raw[2]合并,得到完整的 10 位数值。
常见坑点与调试技巧
别以为写完代码就万事大吉。我在实际项目中踩过的坑,现在都告诉你,帮你省下三天调试时间。
❌ 问题 1:总是返回 0 或 1023
可能原因:
- 电源没接好,VDD 或 VREF 浮空;
- GND 没共地,形成电压差;
- SPI 模式不对(误设为 Mode 1/2/3);
✅解决方法:
用万用表测一下 MCP3008 的 VDD 是否确实是 3.3V;检查 GND 是否连通;确认spi.mode = 0。
❌ 问题 2:数据剧烈跳动
可能原因:
- 电源噪声大(比如和电机共用电源);
- 模拟输入线太长,成了天线;
- 缺少去耦电容。
✅解决方法:
- 加一个0.1μF 陶瓷电容贴近 MCP3008 的 VDD-GND;
- 使用屏蔽线或尽量缩短走线;
- 对采集值做软件滤波(如滑动平均)。
# 示例:3 点滑动平均滤波 buffer = [0, 0, 0] def smooth_read(channel): buffer.pop(0) buffer.append(read_adc(channel)) return sum(buffer) // len(buffer)❌ 问题 3:程序报错 “Permission denied” 或找不到设备
可能原因:
- SPI 未启用;
- 当前用户不在spi用户组。
✅解决方法:
运行:
sudo usermod -aG spi $USER然后重新登录或重启。
实际应用场景举例
掌握了这项技术,你能做的事情远不止“读个电压”。
🌡️ 场景 1:温湿度监控系统
将 NTC 热敏电阻接入 CH0,配合查表法或 Steinhart-Hart 方程,精确计算环境温度。
💡 场景 2:智能灯光控制
用光敏电阻检测环境亮度,自动调节 LED 灯带亮度,实现“随光而变”。
🎤 场景 3:简易音频频谱仪
麦克风经过前置放大后接入 ADC,用 Python 实时绘制声波曲线,做个迷你示波器。
🧪 场景 4:工业传感器网关
一台树莓派挂 8 个不同类型的模拟传感器,定时采集并通过 MQTT 发送到云端服务器。
更进一步:设计考量与进阶方向
当你已经能稳定读取数据,就可以思考如何做得更好。
✅ 提升精度的方法
- 使用独立的基准电压源(如 REF3033)替代树莓派的 3.3V;
- 改用差分输入模式(MCP3008 支持),抑制共模干扰;
- 添加校准机制:记录零点漂移和满量程误差,动态补偿。
⚙️ 提高性能的方向
- 使用DMA + SPI实现不间断高速采样(需 C/C++ 或内核模块);
- 结合RT-Preempt 补丁构建实时系统,确保定时精度;
- 利用 FIFO 缓冲批量读取,降低 CPU 占用率。
🔄 替代方案对比
| 芯片 | 接口 | 分辨率 | 优点 | 缺点 |
|---|---|---|---|---|
| MCP3008 | SPI | 10 位 | 快速、便宜、多通道 | 精度有限 |
| MCP3208 | SPI | 12 位 | 更高分辨率 | 成本略高 |
| ADS1115 | I²C | 16 位 | 内置 PGA,支持小信号放大 | 速度较慢,仅 4 通道 |
根据需求灵活选择才是高手之道。
如果你现在正打算做一个传感器采集项目,不妨试试这套 SPI + MCP3008 的组合拳。成本不到十元,却能带来工业级的可靠性与扩展性。
更重要的是,一旦你掌握了这种“打通物理世界与数字系统”的能力,你会发现——原来树莓派的潜力,远远不止跑个网页或播个视频那么简单。
你已经在构建真正的感知系统了。
如果有任何问题,欢迎在评论区留言交流。下次我们可以聊聊如何用 DMA 实现百万级采样率,或者如何把 ADC 数据绘制成实时图表。