通信协议¶
Arduino 通过多种通信协议与传感器、模块和其他设备交互。掌握 Serial、I2C、SPI 是实现复杂项目的关键。
一、通信协议概览¶
graph TB
A[Arduino 通信协议] --> B[UART / Serial]
A --> C[I2C / TWI]
A --> D[SPI]
A --> E[软件串口]
B --> B1[点对点<br>全双工<br>2 线]
C --> C1[多设备总线<br>半双工<br>2 线]
D --> D1[高速主从<br>全双工<br>4 线]
E --> E1[任意引脚<br>模拟串口]
| 协议 | 线数 | 速度 | 设备数 | 典型应用 |
|---|---|---|---|---|
| UART | 2 (TX+RX) | 115200 bps 常用 | 1 对 1 | 调试输出、GPS、蓝牙模块 |
| I2C | 2 (SDA+SCL) | 100~400 kbps | 最多 127 个 | OLED、传感器、EEPROM |
| SPI | 4 (MOSI+MISO+SCK+SS) | 数 Mbps | 多个(每个一条 SS) | SD 卡、TFT 屏、NRF24L01 |
| SoftwareSerial | 2(任意引脚) | ≤57600 bps | 1 对 1 | 额外串口设备 |
二、UART 串口通信¶
基本原理¶
UART(通用异步收发器)是最常用的通信方式,数据帧格式:
空闲 起始位 D0 D1 D2 D3 D4 D5 D6 D7 校验位 停止位 空闲
─────┐ ┌───┬───┬───┬───┬───┬───┬───┬───┐ ┌───┐ ┌────────
HIGH └──┤ │ │ │ │ │ │ │ ├─┤ ├─┘
START 8 位数据 PARITY STOP
UART 关键参数
- 波特率:双方必须一致(常用 9600、115200)
- 数据位:通常 8 位
- 校验位:None / Even / Odd(默认 None)
- 停止位:1 位或 2 位(默认 1 位)
- Arduino 默认配置:8N1(8 数据位、无校验、1 停止位)
接线¶
交叉连线
TX 接对方 RX,RX 接对方 TX,这是新手最容易犯的错误!
基本使用¶
void setup() {
Serial.begin(115200); // 初始化串口,波特率 115200
while (!Serial); // 等待串口就绪(Leonardo/Due)
Serial.println("串口初始化完成!");
}
void loop() {
// 发送数据
Serial.print("时间: ");
Serial.print(millis());
Serial.print(" ms, ADC: ");
Serial.println(analogRead(A0));
// 接收数据
if (Serial.available() > 0) {
char c = Serial.read(); // 读取一字节
Serial.print("收到: ");
Serial.println(c);
}
delay(100);
}
接收字符串¶
String inputString = "";
bool stringComplete = false;
void setup() {
Serial.begin(115200);
inputString.reserve(200); // 预分配内存
}
void loop() {
if (stringComplete) {
Serial.print("收到完整消息: ");
Serial.println(inputString);
// 解析命令
if (inputString.startsWith("LED")) {
int val = inputString.substring(3).toInt();
analogWrite(9, val);
}
inputString = "";
stringComplete = false;
}
}
// 串口事件回调(每次 loop 结束后自动调用)
void serialEvent() {
while (Serial.available()) {
char c = (char)Serial.read();
if (c == '\n') {
stringComplete = true;
} else {
inputString += c;
}
}
}
Serial.print 格式化输出
Mega 多串口¶
Arduino Mega 有 4 个硬件串口:
| 串口 | TX | RX | 用途 |
|---|---|---|---|
Serial |
D1 | D0 | USB 调试 |
Serial1 |
D18 | D19 | 设备 1 |
Serial2 |
D16 | D17 | 设备 2 |
Serial3 |
D14 | D15 | 设备 3 |
void setup() {
Serial.begin(115200); // USB 调试
Serial1.begin(9600); // 连接 GPS 模块
Serial2.begin(115200); // 连接蓝牙模块
}
三、I2C 通信¶
基本原理¶
I2C(Inter-Integrated Circuit)是一种==两线制==总线协议,由飞利浦发明:
- SDA(数据线):双向数据传输
- SCL(时钟线):由主机提供时钟
graph LR
M[Arduino<br>主机] --- SDA[SDA 总线]
M --- SCL[SCL 总线]
SDA --- S1[传感器 1<br>地址 0x68]
SCL --- S1
SDA --- S2[OLED<br>地址 0x3C]
SCL --- S2
SDA --- S3[EEPROM<br>地址 0x50]
SCL --- S3
I2C 特点
- 每个从设备有唯一的 7 位地址(0x00~0x7F)
- 需要外部 上拉电阻(4.7KΩ,Arduino Wire 库默认开启内部上拉)
- 标准模式 100kHz,快速模式 400kHz
- 理论最多 128 个设备(实际受地址冲突和总线电容限制)
Arduino I2C 引脚¶
| 开发板 | SDA | SCL |
|---|---|---|
| UNO / Nano | A4 | A5 |
| Mega | D20 | D21 |
| Leonardo | D2 | D3 |
| Due | D20 / SDA1 | D21 / SCL1 |
Wire 库基本使用¶
#include <Wire.h>
// === 主机发送数据 ===
void setup() {
Wire.begin(); // 初始化为主机
}
void loop() {
Wire.beginTransmission(0x50); // 开始传输,目标地址 0x50
Wire.write(0x00); // 发送寄存器地址
Wire.write(0xAB); // 发送数据
Wire.endTransmission(); // 结束传输
delay(100);
}
// === 主机请求数据 ===
void readSensor() {
Wire.beginTransmission(0x68); // MPU6050 地址
Wire.write(0x3B); // 起始寄存器地址
Wire.endTransmission(false); // 发送重复起始信号(不释放总线)
Wire.requestFrom(0x68, 6); // 请求 6 字节数据
if (Wire.available() >= 6) {
int16_t ax = (Wire.read() << 8) | Wire.read();
int16_t ay = (Wire.read() << 8) | Wire.read();
int16_t az = (Wire.read() << 8) | Wire.read();
}
}
I2C 地址扫描器¶
非常实用的调试工具,扫描总线上所有已连接设备:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(115200);
Serial.println("I2C 地址扫描器");
Serial.println("--------------------");
}
void loop() {
int count = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print("发现设备: 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
count++;
}
}
Serial.print("共发现 ");
Serial.print(count);
Serial.println(" 个设备\n");
delay(5000);
}
常见 I2C 设备地址¶
| 设备 | 地址 | 用途 |
|---|---|---|
| MPU6050 | 0x68 / 0x69 | 6 轴陀螺仪加速度计 |
| BMP280 | 0x76 / 0x77 | 气压温度传感器 |
| SSD1306 OLED | 0x3C / 0x3D | 0.96 寸 OLED 显示屏 |
| AT24C256 | 0x50~0x57 | I2C EEPROM |
| PCF8574 | 0x20~0x27 | IO 扩展器 |
| DS3231 | 0x68 | 高精度 RTC 时钟 |
| ADS1115 | 0x48~0x4B | 16 位 ADC |
地址冲突
MPU6050(0x68)和 DS3231(0x68)地址相同!解决方法:
- 选择可配置地址的模块(如 MPU6050 的 AD0 引脚可切换为 0x69)
- 使用 I2C 多路复用器(TCA9548A)
四、SPI 通信¶
基本原理¶
SPI(Serial Peripheral Interface)是==高速全双工==的主从通信协议:
┌──────────┐ ┌──────────┐
│ Arduino │ │ 从设备 │
│ (主机)│ │ │
│ MOSI ├─────────→│ MOSI │
│ MISO │←─────────┤ MISO │
│ SCK ├─────────→│ SCK │
│ SS ├─────────→│ CS/SS │
└──────────┘ └──────────┘
| 信号线 | 全称 | 方向 | 功能 |
|---|---|---|---|
| MOSI | Master Out Slave In | 主→从 | 主机发送数据 |
| MISO | Master In Slave Out | 从→主 | 从机返回数据 |
| SCK | Serial Clock | 主→从 | 时钟信号 |
| SS/CS | Slave Select | 主→从 | 片选(低电平有效) |
SPI 四种模式¶
| 模式 | CPOL | CPHA | SCK 空闲 | 采样时刻 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 |
最常用的是 Mode 0
大多数 SPI 设备(SD 卡、NRF24L01、大多数 TFT 屏)使用 Mode 0,部分传感器使用 Mode 3。查看设备数据手册确认。
SPI 库使用¶
#include <SPI.h>
const int CS_PIN = 10; // 片选引脚
void setup() {
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // 默认不选中
SPI.begin(); // 初始化 SPI
}
// 读取寄存器
byte readRegister(byte reg) {
byte result;
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
// 1MHz 时钟,高位在前,Mode 0
digitalWrite(CS_PIN, LOW); // 选中设备
SPI.transfer(reg | 0x80); // 发送读命令(最高位置1)
result = SPI.transfer(0x00); // 发送空字节,接收数据
digitalWrite(CS_PIN, HIGH); // 取消选中
SPI.endTransaction();
return result;
}
// 写入寄存器
void writeRegister(byte reg, byte value) {
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
digitalWrite(CS_PIN, LOW);
SPI.transfer(reg & 0x7F); // 发送写命令(最高位清0)
SPI.transfer(value); // 发送数据
digitalWrite(CS_PIN, HIGH);
SPI.endTransaction();
}
多个 SPI 设备¶
const int SD_CS = 4;
const int TFT_CS = 10;
const int NRF_CS = 7;
void setup() {
pinMode(SD_CS, OUTPUT);
pinMode(TFT_CS, OUTPUT);
pinMode(NRF_CS, OUTPUT);
// 默认全部不选中
digitalWrite(SD_CS, HIGH);
digitalWrite(TFT_CS, HIGH);
digitalWrite(NRF_CS, HIGH);
SPI.begin();
}
// 访问不同设备时,只拉低对应的 CS
void readSD() {
digitalWrite(SD_CS, LOW);
// ... SPI 操作 ...
digitalWrite(SD_CS, HIGH);
}
SPI vs I2C 对比
| 特性 | SPI | I2C |
|---|---|---|
| 速度 | 更快(数 MHz) | 较慢(100~400 kHz) |
| 线数 | 4+(每增一个设备多一条 CS) | 固定 2 条 |
| 全双工 | ✅ | ❌ |
| 多设备 | 需要额外 CS 引脚 | 通过地址区分 |
| 距离 | 短(PCB 板级) | 较长(可达数米) |
五、SoftwareSerial 软件串口¶
当硬件串口不够用时,可以用软件模拟串口:
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3); // RX=D2, TX=D3
void setup() {
Serial.begin(115200); // 硬件串口(USB 调试)
mySerial.begin(9600); // 软件串口(连接蓝牙模块)
Serial.println("双串口就绪");
}
void loop() {
// 蓝牙→USB
if (mySerial.available()) {
Serial.write(mySerial.read());
}
// USB→蓝牙
if (Serial.available()) {
mySerial.write(Serial.read());
}
}
SoftwareSerial 限制
- 波特率建议不超过 57600(越高越不稳定)
- ==同一时刻只能有一个==软件串口在接收数据
- 使用中断实现,可能影响 PWM 和其他时序敏感功能
- 某些引脚不支持(D0、D1 通常不可用,因为是硬件串口)
- 如果需要更多串口,考虑升级到 Mega(4 个硬件串口)
六、常用函数速查¶
Serial 函数¶
| 函数 | 功能 |
|---|---|
Serial.begin(baud) |
初始化串口 |
Serial.end() |
关闭串口 |
Serial.print(data) |
发送数据 |
Serial.println(data) |
发送数据+换行 |
Serial.write(byte) |
发送原始字节 |
Serial.read() |
读取一字节 |
Serial.peek() |
查看下一字节(不取出) |
Serial.available() |
缓冲区可读字节数 |
Serial.readString() |
读取为字符串 |
Serial.readStringUntil(c) |
读取直到指定字符 |
Serial.setTimeout(ms) |
设置读取超时 |
Serial.flush() |
等待发送完成 |
Wire (I2C) 函数¶
| 函数 | 功能 |
|---|---|
Wire.begin() |
初始化为主机 |
Wire.begin(addr) |
初始化为从机 |
Wire.beginTransmission(addr) |
开始写传输 |
Wire.write(data) |
写入数据 |
Wire.endTransmission() |
结束传输 |
Wire.requestFrom(addr, qty) |
请求数据 |
Wire.read() |
读取一字节 |
Wire.available() |
可读字节数 |
Wire.setClock(freq) |
设置时钟频率 |
SPI 函数¶
| 函数 | 功能 |
|---|---|
SPI.begin() |
初始化 SPI |
SPI.beginTransaction(settings) |
开始事务 |
SPI.transfer(data) |
发送并接收一字节 |
SPI.transfer16(data) |
发送并接收两字节 |
SPI.endTransaction() |
结束事务 |
SPISettings(speed, order, mode) |
配置参数 |
七、常见问题¶
I2C 设备扫描不到怎么办?
- 检查 SDA、SCL 是否接对引脚
- 检查是否有 上拉电阻(4.7KΩ 到 VCC)
- 确认设备供电电压正确(3.3V vs 5V)
- 查看设备数据手册确认正确地址
- 检查线路长度(I2C 总线不宜过长,建议 < 1m)
Serial 乱码怎么解决?
- 波特率不匹配:确保代码和串口监视器的波特率一致
- 电平不匹配:3.3V 设备和 5V Arduino 之间需要电平转换
- 接线错误:TX↔RX 交叉连接
- 编码问题:Arduino IDE 串口监视器需要设置正确的换行符
SPI 设备不响应怎么排查?
- 检查 MOSI、MISO、SCK、CS 接线是否正确
- 确认 CS 引脚==初始化为 HIGH==,操作时拉 LOW
- 检查 SPI 模式(Mode 0~3)是否与设备匹配
- 降低 SPI 时钟速度尝试
- 用逻辑分析仪查看波形