跳转至

通信协议

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 停止位)

接线

 Arduino                  USB-TTL / 其他设备
   TX (D1)  ──────────→  RX
   RX (D0)  ←──────────  TX
   GND      ←──────────→ GND

交叉连线

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 格式化输出

Serial.print(78, BIN);   // "1001110"(二进制)
Serial.print(78, OCT);   // "116"(八进制)
Serial.print(78, HEX);   // "4E"(十六进制)
Serial.print(1.234, 2);  // "1.23"(保留 2 位小数)

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 设备扫描不到怎么办?

  1. 检查 SDA、SCL 是否接对引脚
  2. 检查是否有 上拉电阻(4.7KΩ 到 VCC)
  3. 确认设备供电电压正确(3.3V vs 5V)
  4. 查看设备数据手册确认正确地址
  5. 检查线路长度(I2C 总线不宜过长,建议 < 1m)

Serial 乱码怎么解决?

  • 波特率不匹配:确保代码和串口监视器的波特率一致
  • 电平不匹配:3.3V 设备和 5V Arduino 之间需要电平转换
  • 接线错误:TX↔RX 交叉连接
  • 编码问题:Arduino IDE 串口监视器需要设置正确的换行符

SPI 设备不响应怎么排查?

  1. 检查 MOSI、MISO、SCK、CS 接线是否正确
  2. 确认 CS 引脚==初始化为 HIGH==,操作时拉 LOW
  3. 检查 SPI 模式(Mode 0~3)是否与设备匹配
  4. 降低 SPI 时钟速度尝试
  5. 用逻辑分析仪查看波形