I2C 与 SPI 总线
两大主流通信总线:I2C 两根线挂一堆设备靠地址寻址,SPI 四根线换来高速全双工。
两大主流通信总线:I2C 两根线挂一堆设备靠地址寻址,SPI 四根线换来高速全双工。
SDA(数据)、SCL(时钟)。半双工、主从架构,同一时刻只有一个主机发起通信。速度较慢(标准 100kHz、快速 400kHz),但接线极省、支持多主多从。Uno 的 I2C 固定在 A4(SDA)/A5(SCL)。SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、SS/CS(片选)。全双工、速度快(可达数 MHz~几十 MHz),适合高速大数据(SD 卡、TFT 屏、高速 ADC)。缺点:每多一个从机就多一根片选线。Uno:D13=SCK、D11=MOSI、D12=MISO。Wire 驱动 I2C:begin() 入总线、beginTransmission(addr)+write()+endTransmission() 发数据、requestFrom()+read() 收数据。SPI 驱动 SPI:begin()、beginTransaction() 配时钟/模式、transfer() 收发一个字节。大多数传感器/屏幕的专用库底层都在调它俩。SPISettings(速度, 位序, 模式) 设置。最常见是 Mode 0(SPI_MODE0)。大多数时候你会用现成库,但理解底层 Wire 收发很重要。下例直接读 MPU6050(地址 0x68)的加速度寄存器:
#include <Wire.h> const int MPU = 0x68; // MPU6050 的 I2C 地址 void setup() { Serial.begin(9600); Wire.begin(); // 作为主机加入 I2C 总线 // 唤醒 MPU6050:向电源管理寄存器 0x6B 写 0 Wire.beginTransmission(MPU); // 开始跟 0x68 通信 Wire.write(0x6B); // 指定寄存器地址 Wire.write(0); // 写入的值:0=退出睡眠 Wire.endTransmission(true); // 结束并发送 } void loop() { // 从加速度寄存器 0x3B 开始连读 6 字节(X/Y/Z 各 2 字节) Wire.beginTransmission(MPU); Wire.write(0x3B); Wire.endTransmission(false); // false=保持连接,准备读 Wire.requestFrom(MPU, 6, true); // 请求 6 字节 int16_t ax = (Wire.read() << 8) | Wire.read(); // 高字节<<8 拼低字节 int16_t ay = (Wire.read() << 8) | Wire.read(); int16_t az = (Wire.read() << 8) | Wire.read(); Serial.print("ax="); Serial.print(ax); Serial.print(" ay="); Serial.print(ay); Serial.print(" az="); Serial.println(az); delay(200); }
上面手撕 Wire 是为了让你理解原理。实际项目里,MPU6050、BMP280、DS3231 等常用器件都有成熟库(如 Adafruit_MPU6050),几行就能读到已换算好的物理量,不必自己拼字节、查寄存器手册。理解底层 + 用好库 = 又懂又快。
SPI 靠片选选设备、transfer() 同时收发一个字节(全双工)。下例是读写 SPI 器件的通用骨架:
#include <SPI.h> const int CS = 10; // 片选引脚,可用任意数字脚 void setup() { pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); // 空闲时片选拉高(不选中) SPI.begin(); // 初始化 SCK/MOSI/MISO } byte readRegister(byte reg) { // 配置:1MHz 速率, 高位先发, 模式 0 SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); digitalWrite(CS, LOW); // 拉低片选,选中该从机 SPI.transfer(reg | 0x80); // 发寄存器地址(最高位1常表示读) byte value = SPI.transfer(0x00); // 发空字节,同时收回数据 digitalWrite(CS, HIGH); // 拉高片选,释放 SPI.endTransaction(); return value; } void loop() { byte id = readRegister(0x0F); // 比如读器件 ID 寄存器 Serial.println(id, HEX); delay(500); }
① 同一时刻只能拉低一个片选,否则多个从机同时往 MISO 输出会冲突;② 5V Uno 与 3.3V SPI 器件(如很多 TFT/SD 模块)之间要做电平转换,直连可能烧器件;③ MOSI/MISO 别接反(主出接从入、主入接从出),有的模块把 MOSI 标成 SDI、MISO 标成 SDO,看清楚。
ATmega328P 自带 1KB EEPROM,断电后数据仍在,适合存配置/校准值。用自带 EEPROM 库(注意它不是总线,是片内存储,这里一并讲):
#include <EEPROM.h> void setup() { Serial.begin(9600); // 写:把开机次数 +1 存回 EEPROM 地址 0 byte boots = EEPROM.read(0); // 读地址 0 boots++; EEPROM.write(0, boots); // 写回(仅在变化时写,省寿命) Serial.print("这是第 "); Serial.print(boots); Serial.println(" 次开机"); // 存多字节(如 int/float)用 put/get float cal = 3.14; EEPROM.put(10, cal); // 从地址 10 存一个 float float back; EEPROM.get(10, back); // 读回 } void loop() {}
EEPROM 每个单元约 10 万次擦写寿命,写太频繁会写坏。切忌在 loop 里高频 write!只在数据真正变化时写(EEPROM.update 会先比较、相同就不写,更省寿命)。需要频繁记录用外接 FRAM 或 SD 卡。ESP32 没有真 EEPROM,用 Preferences 库存 Flash 模拟。
| 特性 | I2C | SPI |
|---|---|---|
| 线数 | 2 (SDA/SCL) | 4 (SCK/MOSI/MISO/CS) |
| 速度 | 慢 (100k~400kHz) | 快 (数 MHz~几十 MHz) |
| 多设备 | 靠地址,不加线 | 每个从机加一根 CS |
| 双工 | 半双工 | 全双工 |
| 上拉电阻 | 需要 | 不需要 |
| 典型器件 | OLED、RTC、小传感器 | SD卡、TFT屏、高速ADC |
| 一句话 | 省线、慢、挂得多 | 费线、快、传得多 |
I2C 和 SPI 是嵌入式两大主流总线。I2C 只用 SDA/SCL 两根线,半双工、靠 7 位地址寻址,一根总线能挂多个设备(不加线),速度较慢(100k/400kHz),需外接上拉电阻(模块多自带);用 Wire 库:beginTransmission/write/endTransmission 发、requestFrom/read 收,读寄存器的经典套路是"写寄存器地址→repeated START→读数据"(endTransmission(false) 保持总线)。SPI 用 SCK/MOSI/MISO/CS 四线,全双工、速度快,SCK/MOSI/MISO 三线共享、每个从机一根独立片选(拉低选中);用 SPI 库:beginTransaction(SPISettings(速度,位序,模式))+transfer(),主从必须用同一 SPI 模式(CPOL/CPHA,常用 Mode0)。片内 EEPROM(328P 1KB)断电不丢,用 EEPROM.read/write/put/get 存配置,注意约 10 万次写寿命、切忌 loop 高频写(用 update 省寿命)。选型口诀:省线挂得多用 I2C,要快传得多用 SPI。