Chapter 05 · 树莓派与嵌入式 Linux

传感器:I2C/SPI/1-Wire

让树莓派感知世界:温度、湿度、气压、光照。掌握三大总线,就能接上市面上 90% 的传感器模块。

课程进度42%

关键概念

I2C(两线串行总线)
Inter-Integrated Circuit,只用 两根线(SDA 数据 + SCL 时钟)就能挂载多个设备。每个设备有唯一 7 位地址(如 0x76)。树莓派用 GPIO2(SDA)/GPIO3(SCL)。优点是省引脚、可挂多个;缺点是速度中等。BMP280、OLED、大量传感器用它。
SPI(高速串行总线)
Serial Peripheral Interface,用 4 根线(MOSI 主发/MISO 主收/SCLK 时钟/CE 片选)。速度远高于 I2C,靠独立片选线区分设备。树莓派用 GPIO10/9/11 + CE0/CE1。适合高速设备:屏幕、ADC、Flash。缺点是每个设备多占一根片选线。
1-Wire(单总线)
Dallas/Maxim 的单线协议,仅用一根数据线(+电源+地)传输,每个设备有全球唯一 64 位 ID。最著名的是 DS18B20 温度传感器。树莓派内核有原生驱动,设备直接以文件形式出现在 /sys/bus/w1/devices/,读文件即得数据,极简单。默认用 GPIO4。
i2cdetect 与设备地址
扫描 I2C 总线、列出所有已连接设备地址的命令(属于 i2c-tools 包)。接好 I2C 传感器后跑 i2cdetect -y 1,会显示一张地址表,找到设备(如 0x76)说明接线正确。扫不到通常是接线错、没启用 I2C、或没接上拉电阻。
smbus2 库
Python 的 I2C 通信库(SMBus 是 I2C 的子集)。提供 read_byte_data(addr, reg)write_byte_data() 等方法,直接读写设备的寄存器。是手动驱动 I2C 传感器的基础库。很多传感器也有更高层的专用库(如 Adafruit CircuitPython 系列)。
ADC(模数转换器)
Analog-to-Digital Converter,把连续的模拟电压转成数字值。树莓派没有内置 ADC(不能直接读电位器、光敏电阻等模拟传感器),这是它与 Arduino/ESP32 的重要差异。需外接 ADC 芯片(如 MCP3008,通过 SPI 连接)来补上模拟输入能力。
MCP3008
经典的 8 通道 10 位 SPI ADC 芯片,为树莓派补上模拟输入。接上后可读 8 路模拟传感器(电位器、光敏电阻、土壤湿度等)。gpiozero 内置 MCP3008 类,读值返回 0.0~1.0,极其方便。
寄存器(Register)
传感器芯片内部的存储单元,每个有地址。读取传感器 = 读它的数据寄存器;配置传感器 = 写它的控制寄存器。手动驱动 I2C/SPI 设备时,要查芯片数据手册(datasheet)找到寄存器地址和含义。用专用库时这些细节已被封装。

三大总线接线对比

先直观理解三种总线怎么接、各占几根线、能挂几个设备:

I2C / SPI / 1-Wire 拓扑对比 ═══════════════════════════════════════════════════════════════ I2C(2 线,共享总线,靠地址区分) Pi ──SDA(GPIO2)──┬──────────┬──────────┐ ──SCL(GPIO3)──┤ ┤ ┤ [BMP280] [OLED] [传感器] 0x76 0x3C 0x40 ← 各有唯一地址 SPI(4 线,靠独立片选 CE 区分设备) Pi ──MOSI(GPIO10)─┬─────────┬ ──MISO(GPIO9)──┤ ┤ ──SCLK(GPIO11)─┤ ┤ ──CE0(GPIO8)───┤ [MCP3008] ──CE1(GPIO7)───────────┤ [屏幕] ← 每设备一根片选 1-Wire(1 线数据,最省,靠 64位ID区分) Pi ──DATA(GPIO4)──┬──────────┬────── (+ 4.7kΩ 上拉到 3.3V) [DS18B20] [DS18B20] 28-xxxx 28-yyyy ← 全球唯一序列号

第一步:启用总线

树莓派默认关闭这些接口,需先在 raspi-config 里启用(第 2 章讲过):

# 图形菜单方式:3 Interface Options → 分别启用 I2C / SPI / 1-Wire
sudo raspi-config

# 或命令行一键启用(0 = 启用)
sudo raspi-config nonint do_i2c 0
sudo raspi-config nonint do_spi 0
sudo raspi-config nonint do_onewire 0

# 安装 I2C 工具
sudo apt install -y i2c-tools python3-smbus2

# 重启使内核模块加载
sudo reboot

# 重启后确认设备文件已出现
ls /dev/i2c-*      # 应看到 /dev/i2c-1
ls /dev/spidev*    # 应看到 /dev/spidev0.0 spidev0.1

I2C 实战:读 BMP280 温度气压

BMP280 是常见的 I2C 温度+气压传感器。接线:VCC→3.3V,GND→GND,SDA→GPIO2(脚3),SCL→GPIO3(脚5)。先扫描确认:

# 扫描 I2C 总线 1 上的设备
i2cdetect -y 1
# 输出地址表,在 76 位置看到 "76" 表示 BMP280 已识别:
#      0  1  2 ... 6  7  8  9  a  b  c  d  e  f
# 70:                       76 --

用 Adafruit 专用库读取(比手撸寄存器省心):

# 在虚拟环境中安装专用库
python3 -m venv env && source env/bin/activate
pip install adafruit-circuitpython-bmp280
# bmp280_read.py — 读取温度与气压
import board
import adafruit_bmp280
from time import sleep

# board.I2C() 自动使用 GPIO2/3(默认 I2C-1)
i2c = board.I2C()
sensor = adafruit_bmp280.Adafruit_BMP280_I2C(i2c, address=0x76)

# 设置海平面气压用于计算海拔(可选,按当地天气填)
sensor.sea_level_pressure = 1013.25

while True:
    print(f"温度: {sensor.temperature:.1f} °C")
    print(f"气压: {sensor.pressure:.1f} hPa")
    print(f"海拔: {sensor.altitude:.1f} m")
    print("-" * 20)
    sleep(2)

如果想理解底层,用 smbus2 手动读寄存器:

# raw_i2c.py — 用 smbus2 直接读 I2C 寄存器(理解底层)
from smbus2 import SMBus

BMP280_ADDR = 0x76
REG_ID = 0xD0        # 芯片 ID 寄存器(查数据手册得知)

with SMBus(1) as bus:   # 打开 I2C 总线 1
    # 读取芯片 ID,BMP280 应返回 0x58
    chip_id = bus.read_byte_data(BMP280_ADDR, REG_ID)
    print(f"芯片 ID: 0x{chip_id:02X}")  # 0x58 = 确认是 BMP280

1-Wire 实战:读 DS18B20 温度

DS18B20 是防水温度探头,1-Wire 协议。接线:VCC→3.3V,GND→GND,DATA→GPIO4(脚7),DATA 与 VCC 之间接 4.7kΩ 上拉电阻。启用 1-Wire 后,内核驱动自动把它变成文件:

# 列出检测到的 1-Wire 设备(28- 开头是 DS18B20)
ls /sys/bus/w1/devices/
# 28-3c01d607... 00-800000000000  (前者是温度传感器)

# 读取原始数据文件
cat /sys/bus/w1/devices/28-*/w1_slave
# 输出:
# a1 01 4b 46 7f ff 0c 10 5c : crc=5c YES
# a1 01 4b 46 7f ff 0c 10 5c t=26062   ← t=26062 表示 26.062°C
# ds18b20.py — 读 DS18B20 温度(读文件即可,无需库)
import glob
from time import sleep

# 自动找到设备文件路径
device_file = glob.glob("/sys/bus/w1/devices/28-*/w1_slave")[0]

def read_temp():
    with open(device_file) as f:
        lines = f.readlines()
    # 第一行末尾要有 YES(CRC 校验通过)
    if lines[0].strip()[-3:] != "YES":
        return None
    # 第二行 t= 后面是温度×1000
    pos = lines[1].find("t=")
    temp_c = int(lines[1][pos+2:]) / 1000.0
    return temp_c

while True:
    print(f"温度: {read_temp():.2f} °C")
    sleep(1)
1-Wire 为何如此简单?

因为树莓派内核直接内置了 1-Wire 驱动,把硬件协议全部处理好,最终以普通文本文件暴露给用户。你只需 catopen() 读文件即可,无需理解时序、无需任何库。这是 Linux "一切皆文件"哲学的绝佳体现。

SPI 实战:MCP3008 补上模拟输入

树莓派无 ADC,读电位器、光敏电阻这类模拟传感器必须外接 MCP3008。接线(SPI):VDD/VREF→3.3V,AGND/DGND→GND,CLK→GPIO11,DOUT(MISO)→GPIO9,DIN(MOSI)→GPIO10,CS→GPIO8(CE0)。gpiozero 直接支持:

# mcp3008.py — 读电位器(接 MCP3008 通道 0)
from gpiozero import MCP3008
from time import sleep

# channel=0 表示读 MCP3008 的 CH0 引脚
pot = MCP3008(channel=0)

while True:
    # value 返回 0.0 ~ 1.0(10位ADC归一化)
    raw = pot.value
    voltage = raw * 3.3   # 换算成实际电压
    print(f"归一值: {raw:.3f}  电压: {voltage:.2f}V")
    sleep(0.5)
# light_sensor.py — 光敏电阻 + MCP3008 做光照检测
from gpiozero import MCP3008, LED
from time import sleep

ldr = MCP3008(channel=1)   # 光敏电阻接 CH1
led = LED(17)

while True:
    light = ldr.value
    print(f"光照: {light:.2f}")
    # 天黑(值低于阈值)自动开灯
    if light < 0.3:
        led.on()
    else:
        led.off()
    sleep(0.5)

三大总线选型对照

总线线数速度多设备典型器件
I2C2(SDA/SCL)中(100k~400kHz)靠地址,多个BMP280、OLED、RTC
SPI4(+每设备片选)高(数 MHz)靠片选,多个MCP3008、屏幕、Flash
1-Wire1(+电源地)靠64位ID,多个DS18B20 温度
DHT22 是个例外

大纲提到的 DHT11/DHT22 温湿度传感器用的是专有单总线协议(非标准 1-Wire),时序严格。由于树莓派跑 Linux 无法保证微秒级实时性,纯软件读取常失败。推荐用 adafruit-circuitpython-dht 库配合 pip install,或干脆改用 I2C 的 AHT20/SHT31 替代——更稳定。示例:import adafruit_dht; dht = adafruit_dht.DHT22(board.D4)

本章小结

三大总线让树莓派感知世界。I2C(2 线 SDA/SCL,靠地址区分多设备)接 BMP280、OLED 等,先 i2cdetect -y 1 扫描确认地址,再用 Adafruit 专用库或 smbus2 手动读寄存器。1-Wire(1 根数据线 + 4.7kΩ 上拉)接 DS18B20,内核驱动把它变成 /sys/bus/w1/devices/ 下的文件,直接读文件即得温度,最简单。SPI(4 线 + 片选,高速)接屏幕、Flash,以及关键的 MCP3008 ADC——因为树莓派没有内置 ADC,读电位器/光敏电阻等模拟传感器必须外接它,gpiozero 的 MCP3008(channel=0) 返回 0.0~1.0。所有总线都要先在 raspi-config 启用。DHT22 用专有协议在 Linux 上不稳,建议改用 I2C 的 AHT20/SHT31。下一章我们用 PWM 驱动舵机和电机,让树莓派动起来。