树莓派ADS1115传感器数据采集实战:从零到精通的Python3代码详解与深度排错指南

如果你手头有一块树莓派,并且正打算用它来连接一些模拟传感器——比如测量土壤湿度的探头、检测气体浓度的模块,或者仅仅是读取一个电位器的电压——那么你很快就会遇到一个核心问题:树莓派本身没有模拟输入引脚。这时候,一块像ADS1115这样的模数转换器(ADC)就成了连接物理世界与数字世界的桥梁。今天,我们不只讲怎么把线接上、让代码跑起来,更要深入到配置的细节、代码的优化,以及那些让你调试到深夜的“坑”究竟在哪里。这篇文章面向的是已经对树莓派基础操作(如SSH连接、命令行操作)有所了解,但可能在硬件交互和Python编程上希望获得更扎实、更深入理解的开发者和爱好者。

1. 硬件连接与I2C总线基础:不止是接对线

很多教程会告诉你,把ADS1115的VCC接树莓派的3.3V或5V,GND接GND,SDA和SCL分别对应接好,然后就能用了。这没错,但知其然更要知其所以然。我们先从I2C总线说起。

I2C是一种同步、多主从、多从机的串行通信总线。在树莓派上,它通过两个GPIO引脚实现:SDA(数据线) 和 SCL(时钟线)。ADS1115作为从设备,通过这个总线与树莓派(主设备)通信。接线看似简单,但有几个细节决定了项目的稳定性:

  • 电源选择:ADS1115的工作电压范围是2.0V到5.5V。树莓派的5V引脚是直接从外部电源适配器引入的,而3.3V引脚是经过板载稳压器降压得到的。如果你的传感器(比如某些MQ系列气体传感器)也需要从ADS1115取电,并且工作电压是5V,那么ADS1115的VDD就必须接5V。否则,接3.3V通常更安全,可以减少噪声干扰。关键点:确保整个系统的逻辑电平一致。如果ADS1115用5V供电,其I2C引脚输出高电平也是5V,而树莓派的GPIO可承受电压通常是3.3V,长期使用可能有风险。稳妥起见,如果使用5V供电,建议在SDA和SCL线上添加一个简单的电平转换电路,或者直接选择支持3.3V逻辑电平的传感器。
  • 地址引脚(ADDR):这是新手最容易出错的地方之一。ADS1115的I2C地址由ADDR引脚的连接方式决定。这个设计允许你在同一条I2C总线上挂载最多四个ADS1115模块。
ADDR引脚连接至I2C设备地址(7位)备注
GND0x48最常见,模块默认状态
VDD0x49
SDA0x4A
SCL0x4B

注意:i2cdetect工具显示的地址是7位地址。在Python代码中,我们通常直接使用这个十六进制数值(如0x48)。有些库或代码可能会使用8位地址(写地址),但Adafruit库接受的是7位地址。

  • 模拟输入通道:ADS1115有4个单端输入通道(A0-A3),也可以配置为2个差分输入对。接线时,要清楚你的传感器输出是单端信号(对GND的电压)还是差分信号(两个引脚间的电压差)。对于像MQ-2这样的传感器,通常输出的是单端信号,接A0即可,传感器的GND也需要与ADS1115的GND相连。

硬件检查清单:

  1. 使用公对母或母对母杜邦线,确保连接牢固,避免接触不良。
  2. 如果线路较长(超过20厘米),考虑给I2C总线的SDA和SCL各加上一个上拉电阻(通常4.7kΩ到10kΩ)。很多模块已经板载了这些电阻,但如果总线上设备较多或线路长,信号可能变差,额外添加强上拉有时是必要的。
  3. 为模拟部分(传感器到ADS1115的输入线)和数字部分(I2C总线)提供一条干净、低阻抗的接地路径,可以减少读数噪声。

2. 软件环境配置与I2C总线启用

硬件连接妥当后,我们需要在树莓派系统层面进行配置。这个过程在最新的Raspberry Pi OS(原Raspbian)上已经简化了很多。

首先,通过终端或SSH连接到你的树莓派。第一步是确保系统是最新的,这能避免很多因软件包版本过旧导致的问题。

sudo apt update
sudo apt upgrade -y

接下来,启用I2C接口。你可以使用图形化的raspi-config工具:

sudo raspi-config

在菜单中,依次选择 Interface Options -> I2C -> Yes 来启用I2C驱动。完成后,选择 Finish 并重启树莓派。

更直接的方式是通过命令行修改配置并加载模块:

# 编辑配置
sudo nano /boot/config.txt
# 确保以下行存在且未被注释(如果没有就添加)
dtparam=i2c_arm=on
# 保存退出 (Ctrl+X, 然后 Y, 回车)
# 重启或手动加载模块
sudo modprobe i2c-dev

重启后,安装I2C工具集来检测设备:

sudo apt install i2c-tools -y

使用以下命令检测连接到I2C总线1(树莓派40针GPIO排针使用的总线)上的设备:

sudo i2cdetect -y 1

你会看到一个表格。如果ADS1115连接正确且地址设置无误(例如ADDR接GND),你应该能在对应的地址(如0x48)上看到一个数字,而不是两个短横线--。这是硬件连接成功的第一个明确信号。

提示:如果i2cdetect没有发现设备,请按以下步骤排查:1) 确认接线(VCC, GND, SDA, SCL); 2) 确认ADDR引脚连接是否正确; 3) 尝试命令sudo i2cdetect -y 0(针对老款26针树莓派); 4) 检查模块上的电源指示灯是否亮起。

3. Python生态与Adafruit库深度解析

Python是树莓派上进行硬件交互的首选语言,生态丰富。对于ADS1115,最常用的是Adafruit CircuitPython ADS1x15库的“纯Python”端口。这个库封装了与ADS1115/ADS1015通信的底层细节,让我们可以用高级命令读取数据。

使用pip3进行安装。为了获得更快的下载速度,可以使用国内镜像源:

pip3 install Adafruit-ADS1x15 -i https://pypi.tuna.tsinghua.edu.cn/simple

现在,让我们深入库的核心用法。首先是一个最基本的读取单通道的例子:

import time
import Adafruit_ADS1x15

# 创建ADS1115实例,默认地址0x48,总线1
adc = Adafruit_ADS1x15.ADS1115()
# 如果你想指定地址,比如ADDR接VDD的0x49:
# adc = Adafruit_ADS1x15.ADS1115(address=0x49)

GAIN = 1  # 增益设为1,量程为±4.096V

print('开始读取ADS1115数值,按Ctrl-C退出...')
print('| {0:>6} | {1:>6} | {2:>6} | {3:>6} |'.format(*range(4)))
print('-' * 37)

while True:
    values = [0]*4
    for i in range(4):
        # 读取单端输入
        values[i] = adc.read_adc(i, gain=GAIN)
    print('| {0:>6} | {1:>6} | {2:>6} | {3:>6} |'.format(*values))
    time.sleep(0.5)

这段代码清晰易懂,但它只是起点。read_adc方法有几个关键参数:

  • channel: 通道号(0-3)。
  • gain: 可编程增益放大器(PGA)的设置,直接影响测量量程和分辨率。
  • data_rate: 数据速率(SPS,每秒采样数),影响转换速度和噪声。

增益(Gain)的选择是精度与量程的权衡。ADS1115的PGA可以在转换前放大输入信号。增益越大,能分辨的微小电压变化越精细(LSB值越小),但允许的输入电压范围也越小。

增益设置满量程范围 (FSR)最小有效位 (LSB)适用场景
2/3±6.144V187.5μV测量5V系统传感器,通用性强
1±4.096V125μV最常用,匹配3.3V系统,精度与量程平衡
2±2.048V62.5μV测量0-3.3V信号,更高精度
4±1.024V31.25μV测量0-1.8V或更小信号
8±0.512V15.625μV测量热电偶等微弱信号
16±0.256V7.8125μV最高精度,测量极微弱信号

注意:选择增益时,务必确保你的输入信号电压绝对值不超过对应的满量程电压。例如,增益设为2(±2.048V)时,如果输入3.3V,会导致ADC前端过载,读数不准确甚至损坏芯片(虽然ADS1115有过压保护,但应避免)。

数据速率(data_rate) 影响转换时间和噪声性能。速率越高,每秒可读次数越多,但可能引入更多噪声;速率越低,噪声越小,但响应变慢。ADS1115支持从8SPS到860SPS的速率。对于缓慢变化的信号(如温度、湿度),128SPS或更低是很好的选择。对于需要快速采样的应用,可以选择更高的速率。

# 以最高速率860SPS读取通道0,适用于需要快速响应的场景
fast_value = adc.read_adc(0, gain=1, data_rate=860)

4. 从原始读数到实际电压:理解数据转换

adc.read_adc()返回的是一个整数,对于16位的ADS1115,这个值范围是-32768到32767(有符号整数)。这个数字本身没有直接的物理意义,我们需要将其转换为电压值。

转换公式是:

电压 = (读数 * 满量程电压) / (2^(位数-1))

对于ADS1115(16位,最高位为符号位),公式简化为:

电压 (V) = (读数 * FSR) / 32768

其中,FSR(Full-Scale Range)就是由增益决定的那个满量程电压绝对值,例如增益为1时,FSR是4.096V。

让我们写一个更实用的函数,它直接返回电压值,并处理差分输入:

def read_voltage(adc_instance, channel, gain=1, differential=False):
    """
    读取指定通道的电压值。
    参数:
        adc_instance: ADS1x15实例
        channel: 通道 (0-3)
        gain: 增益设置
        differential: 是否为差分模式。如果为True,channel参数对含义不同。
    """
    # 增益与满量程对应关系字典
    gain_to_fsr = {
        2/3: 6.144,
        1: 4.096,
        2: 2.048,
        4: 1.024,
        8: 0.512,
        16: 0.256
    }
    fsr = gain_to_fsr.get(gain, 4.096)  # 默认4.096V

    if differential:
        # 差分模式:通道0-3对应特定的差分对
        # 0: A0 - A1, 1: A0 - A3, 2: A1 - A3, 3: A2 - A3
        raw_value = adc_instance.read_adc_difference(channel, gain=gain)
    else:
        # 单端模式
        raw_value = adc_instance.read_adc(channel, gain=gain)

    # 将原始读数转换为电压
    voltage = (raw_value * fsr) / 32768.0
    return voltage, raw_value

# 使用示例
adc = Adafruit_ADS1x15.ADS1115()
voltage_a0, raw_a0 = read_voltage(adc, channel=0, gain=1)
print(f"通道0: 原始值 = {raw_a0}, 电压 = {voltage_a0:.4f} V")

# 读取A0和A1之间的差分电压
voltage_diff, raw_diff = read_voltage(adc, channel=0, gain=1, differential=True)
print(f"差分(A0-A1): 原始值 = {raw_diff}, 电压 = {voltage_diff:.4f} V")

这个函数的好处是,它将底层细节封装起来,你直接得到有物理意义的电压值,便于后续处理,比如根据传感器特性曲线将电压转换为温度、压力或浓度。

5. 高级应用与实战排错指南

掌握了基础读写后,我们可以探索更复杂的应用场景,并系统性地看看那些常见问题。

场景一:多传感器与连续转换模式 如果你需要同时监控多个快速变化的信号,轮询读取四个通道可能不够快。ADS1115支持连续转换模式,但Adafruit库的简单API没有直接暴露此功能。不过,你可以通过配置寄存器进行底层操作。更实用的方法是使用库的start_adc()和get_last_result()方法来实现伪连续读取,减少每次读取的命令开销。

import Adafruit_ADS1x15

adc = Adafruit_ADS1x15.ADS1115()
GAIN = 1
# 启动指定通道的连续转换
adc.start_adc(0, gain=GAIN)
print('正在连续读取通道0,按Ctrl-C退出...')

try:
    while True:
        # 获取最后一次转换结果,非阻塞,速度很快
        value = adc.get_last_result()
        voltage = (value * 4.096) / 32768.0
        print(f'原始值: {value:>6d}, 电压: {voltage:.4f} V')
        # 可以在这里进行其他处理,延时可以很短
except KeyboardInterrupt:
    pass
finally:
    # 非常重要:停止连续转换
    adc.stop_adc()

场景二:实现一个简单的数据记录器 结合Python的csv模块和time模块,可以轻松创建一个数据记录器。

import time
import csv
import Adafruit_ADS1x15
from datetime import datetime

adc = Adafruit_ADS1x15.ADS1115(address=0x48)
GAIN = 2  # 假设传感器输出在0-2V之间

filename = f"sensor_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"

with open(filename, 'w', newline='') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(['Timestamp', 'Channel0', 'Channel1', 'Voltage0', 'Voltage1'])
    
    print(f"开始记录数据到 {filename},按Ctrl-C停止...")
    try:
        while True:
            timestamp = datetime.now().isoformat()
            raw0 = adc.read_adc(0, gain=GAIN)
            raw1 = adc.read_adc(1, gain=GAIN)
            volt0 = (raw0 * 2.048) / 32768.0
            volt1 = (raw1 * 2.048) / 32768.0
            
            writer.writerow([timestamp, raw0, raw1, f"{volt0:.6f}", f"{volt1:.6f}"])
            csvfile.flush()  # 确保数据及时写入文件
            print(f"{timestamp} - Ch0:{volt0:.4f}V, Ch1:{volt1:.4f}V")
            time.sleep(1)  # 每秒记录一次
    except KeyboardInterrupt:
        print("\n数据记录已停止。")

常见问题排查清单:

  1. i2cdetect找不到设备(地址显示--)

    • 检查物理连接:这是最常见的原因。重新插拔所有杜邦线,尤其是电源和地线。用万用表测量ADS1115模块VCC和GND之间的电压,确认在3.3V或5V左右。
    • 确认I2C地址:仔细检查ADDR引脚的连接。如果你悬空了ADDR引脚,其状态是不确定的。最好用跳线帽或杜邦线将其明确连接到GND、VCC、SDA或SCL。
    • 检查I2C是否启用:运行lsmod | grep i2c,应该能看到i2c_dev和i2c_bcm2835(或类似)模块。如果没有,用sudo raspi-config重新启用。
    • 尝试降低总线速度:在/boot/config.txt文件中添加一行dtparam=i2c_arm_baudrate=10000将I2C总线速度降到10kHz,排除因线路干扰导致的高速通信失败。测试成功后可以再移除或调整。
  2. 读数不稳定、跳动大

    • 电源噪声:尝试使用线性稳压电源为树莓派和传感器单独供电,或者至少在模拟部分(ADS1115和传感器)的电源引脚附近并联一个10μF的电解电容和一个0.1μF的陶瓷电容,用于滤波。
    • 信号干扰:让模拟信号线远离树莓派的高频数字电路(如SD卡槽、HDMI接口)和电源线。使用双绞线或屏蔽线连接传感器。
    • 接地环路:确保所有设备的“地”是等电位的。单点接地是理想情况。
    • 降低数据速率:在read_adc函数中尝试设置较低的data_rate,如data_rate=8,虽然慢,但噪声更低。
    • 软件滤波:在代码中实现简单的数字滤波,如移动平均滤波。
      import collections
      class MovingAverage:
          def __init__(self, size):
              self.queue = collections.deque(maxlen=size)
          def next(self, val):
              self.queue.append(val)
              return sum(self.queue) / len(self.queue)
      
      filter_adc = MovingAverage(10)  # 10点移动平均
      raw = adc.read_adc(0, gain=1)
      filtered = filter_adc.next(raw)
      
  3. 读数始终为0、最大值或固定值

    • 增益设置过高:如果输入电压很小(如几毫伏),但增益设为了1或2/3,那么读数可能始终在很小的数字范围内波动,看起来像0。尝试增大增益(如设为16)。
    • 增益设置过低/输入超量程:如果输入电压超过当前增益下的满量程,读数可能会被钳位在最大值(如32767)或出现异常。用万用表测量实际输入电压,并选择合适增益。
    • 通道配置错误:确认你读取的通道号(0-3)与实际传感器连接的物理通道(A0-A3)一致。
    • 传感器或信号源问题:断开ADS1115的输入,用万用表直接测量信号源输出,确认信号本身是否正确。
  4. Python报错OSError: [Errno 121] Remote I/O error

    • 这通常是I2C通信失败。可能的原因包括:I2C设备在通信过程中无响应(接触不良、设备损坏)、总线被锁死。尝试执行sudo i2cdetect -y 1,如果连总线都检测不到,可能需要重启树莓派。如果总线正常但设备消失,检查硬件连接和电源。

硬件项目总是伴随着调试。我的经验是,一套万用表和一颗耐心是最重要的工具。从一个最简单、最确定能工作的配置开始(比如只接电源和I2C线,用i2cdetect检测),然后逐步添加传感器和代码逻辑,每步都验证,这样能最快地定位问题所在。ADS1115是一块非常可靠且精密的芯片,绝大多数问题都出在接线、配置或电源上。当你看到稳定的、随传感器变化而变化的读数出现在屏幕上时,那种连接数字与物理世界的成就感,正是嵌入式开发的乐趣所在。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐