引言
通信协议是嵌入式系统的核心。无论你是读取传感器、与 SD 卡通信,还是打印调试信息,你都在使用三大主要协议之一:串口 (UART)、I2C (TWI) 或 SPI。在本教程中,我们将使用 Velxio 浏览器内模拟器,在 Arduino Uno 上同时运行这三种协议。你将看到如何扫描 I2C 总线、读写 EEPROM、执行 SPI 传输,并通过串口记录所有信息。

电路讲解
本示例仅使用 Arduino Uno 板本身——无需任何外部元件。Arduino 的内置外设提供了我们所需的一切:
- 串口 (UART):使用引脚 0 (RX) 和 1 (TX) 与串口监视器通信。
- I2C (TWI):使用引脚 A4 (SDA) 和 A5 (SCL)。Wire 库会启用内部上拉电阻。
- SPI:使用引脚 10 (SS)、11 (MOSI)、12 (MISO) 和 13 (SCK)。引脚 10 被配置为从机选择输出。
由于所有连接都在 Arduino 内部,因此无需接线。Velxio 画布会显示带有引脚标签的 Uno,你可以通过打开元件选择器来检查虚拟元件。

代码讲解
让我们逐步分析这段代码。
库和定义
#include <Wire.h>
#include <SPI.h>
#define DS1307_ADDR 0x68
#define EEPROM_ADDR 0x50
#define SS_PIN 10
我们包含了用于 I2C 的 Wire 库和用于 SPI 的 SPI 库。定义了两个 I2C 地址:0x68 用于 DS1307 RTC(虽然物理上不存在,但我们会扫描它),0x50 用于 24Cxx EEPROM。SPI 从机选择引脚设置为数字引脚 10。
辅助函数
byte bcdToDec(byte val) {
return ((val >> 4) * 10) + (val & 0x0F);
}
void readRTC(byte &hr, byte &min, byte &sec) {
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 3);
sec = bcdToDec(Wire.read() & 0x7F);
min = bcdToDec(Wire.read());
hr = bcdToDec(Wire.read() & 0x3F);
}
bcdToDec 函数将二进制编码的十进制 (BCD) 转换为十进制。readRTC 从 DS1307 RTC 读取秒、分和时。& 0x7F 和 & 0x3F 掩码用于清除未使用的位。
void writeEEPROM(byte reg, byte value) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
delay(5);
}
byte readEEPROM(byte reg) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
这些函数用于向地址为 0x50 的 I2C EEPROM 写入和读取单个字节。delay(5) 确保 EEPROM 有足够时间完成写入周期。
byte spiTransfer(byte data) {
digitalWrite(SS_PIN, LOW);
byte result = SPI.transfer(data);
digitalWrite(SS_PIN, HIGH);
return result;
}
该函数执行全双工 SPI 传输:拉低 SS,发送一个字节,接收响应,然后拉高 SS。
初始化
void setup() {
Serial.begin(9600);
Wire.begin();
pinMode(SS_PIN, OUTPUT);
digitalWrite(SS_PIN, HIGH);
SPI.begin();
Serial.println("===================================");
Serial.println(" 多协议演示");
Serial.println(" 串口 (USART) + I2C (TWI) + SPI");
Serial.println("===================================");
Serial.println();
// ── I2C: 扫描总线 ──
Serial.println("[I2C] 正在扫描总线...");
int found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print(" 在地址 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
found++;
}
}
Serial.print(" ");
Serial.print(found);
Serial.println(" 个设备在 I2C 总线上。");
Serial.println();
// ── I2C: 写入/读取 EEPROM ──
Serial.println("[I2C] EEPROM 写入/读取测试:");
writeEEPROM(0, 42);
writeEEPROM(1, 99);
byte v0 = readEEPROM(0);
byte v1 = readEEPROM(1);
Serial.print(" 写入 42,读取 ");
Serial.print(v0);
Serial.println(v0 == 42 ? " [成功]" : " [失败]");
Serial.print(" 写入 99,读取 ");
Serial.print(v1);
Serial.println(v1 == 99 ? " [成功]" : " [失败]");
Serial.println();
// ── SPI: 传输测试 ──
Serial.println("[SPI] 传输测试:");
byte spiData[] = {0xAA, 0x55, 0x42};
for (int i = 0; i < 3; i++) {
byte rx = spiTransfer(spiData[i]);
Serial.print(" 发送=0x");
if (spiData[i] < 16) Serial.print("0");
Serial.print(spiData[i], HEX);
Serial.print(" 接收=0x");
if (rx < 16) Serial.print("0");
Serial.println(rx, HEX);
}
Serial.println();
Serial.println("初始化完成。正在读取 RTC...");
Serial.println();
}
在 setup() 中,我们初始化了所有三种协议。然后我们:
- 扫描 I2C 总线,查找地址从 1 到 126 的设备。
- 测试 EEPROM,将值 42 和 99 写入地址 0 和 1,然后读回并验证。
- 测试 SPI,发送三个字节(
0xAA、0x55、0x42)并打印接收到的字节。
主循环
void loop() {
// ── 串口:每 2 秒打印 RTC 时间 ──
byte hr, min, sec;
readRTC(hr, min, sec);
Serial.print("[RTC] ");
if (hr < 10) Serial.print("0");
Serial.print(hr);
Serial.print(":");
if (min < 10) Serial.print("0");
Serial.print(min);
Serial.print(":");
if (sec < 10) Serial.print("0");
Serial.print(sec);
Serial.print(" | 运行时间:");
Serial.print(millis() / 1000);
Serial.println("s");
delay(2000);
}
每两秒,主循环读取 RTC 时间并连同系统运行时间一起打印。这演示了在 I2C 和 SPI 空闲时持续进行串口输出。
关键概念
- 串口 (UART):异步、点对点。用于调试输出以及与 PC 或 GPS 等模块通信。
- I2C:同步、多主控,使用两根线 (SDA, SCL)。每个设备有唯一地址。非常适合传感器和 EEPROM。
- SPI:同步、全双工,使用四根线 (MOSI, MISO, SCK, SS)。比 I2C 更快,但需要更多引脚。
- 总线扫描:遍历 I2C 地址以发现连接的设备。
- EEPROM 读写:非易失性存储器,断电后仍保留数据。
- SPI 传输:同时发送和接收数据。
常见陷阱与调试技巧
- I2C 上拉电阻:Wire 库会启用内部上拉,但对于较长的总线,建议使用外部电阻 (4.7kΩ)。
- SPI SS 引脚:始终将 SS 设置为输出,并在不使用时保持高电平,以避免意外进入从机模式。
- EEPROM 写入时序:写入后,等待至少 5 毫秒再进行下一次操作。
- 串口监视器波特率:确保串口监视器设置为 9600 波特。
- 无外部设备:在此模拟中,I2C 扫描不会发现任何设备(可能除了 Arduino 的内部 TWI)。EEPROM 测试使用模拟器内置的虚拟 EEPROM。
建议扩展
- 添加真实传感器:将 DS1307 RTC 和 24Cxx EEPROM 连接到 I2C 总线,并通过 SPI 连接 SD 卡模块。
- 将数据记录到 EEPROM:存储传感器读数并在复位后检索。
- 使用中断:通过按钮触发 SPI 传输。
- 实现命令接口:解析串口命令以控制 I2C 和 SPI 操作。
亲自尝试
准备好看到这三种协议的实际运行了吗?在 Velxio 中打开实时示例,编译并运行。观察串口监视器输出,Arduino 将扫描 I2C 总线、测试 EEPROM、执行 SPI 传输,然后持续打印 RTC 时间。

祝你编程愉快!