串行通信协议——UART通信协议、I2C通信协、SPI通信协议
这三种协议是微控制器与传感器、存储器、显示器等外设对话的“语言”。
先来了解一些基础的概念吧~
1. 串行是什么?
串行是一种数据通信方式,指数据位(0和1)在一条通道上依次、一位一位地进行传输。
你可以把它想象成单车道公路:所有的车辆(数据位)必须排成一列,一辆接一辆地通过。
- 对立面:并行
- 并行通信是在多条通道上同时传输多个数据位。
- 这就像多车道高速公路:8辆车(代表一个字节的8个位)可以并排同时开过去。
- 缺点:需要更多的线(比如8位并行需要8条数据线+控制线),线路更复杂,成本更高,且各信号线之间容易产生干扰(串扰),速度提升到一定程度后难度很大。
为什么现在串行更流行?
虽然并行一次传输的数据更多,但串行可以通过极大地提高时钟频率来弥补。而且串行线路更简单、成本更低、抗干扰能力更强。现代的高速接口(如USB, PCIe, SATA, MIPI, I2C, SPI)都是串行协议。
总结:串行 = 单线顺序传输;并行 = 多线同时传输。
2. I2C协议中地址的最后一位决定读写?
完全正确。 这是I2C协议的一个关键规则。
一个I2C从设备的7位地址(例如SHT30的地址是 0b1000100)在实际传输时,会被放入一个8位的字节中。这个字节的最低位(LSB)就是读写位(R/W位)。
- 完整的8位地址字节结构:
[地址位6][地址位5][地址位4][地址位3][地址位2][地址位1][地址位0][R/W位] - R/W位的含义:
- 0 = 写 (Write):表示主设备将要向从设备发送数据。(例如,主设备要发送一个命令给传感器)
- 1 = 读 (Read):表示主设备想要从从设备读取数据。(例如,主设备要向传感器请求数据)
举例:
SHT30传感器的7位地址是 0x44,用二进制表示是 b1000100。
- 当主设备要写(发送命令)时,它发出的地址字节是:
(0x44 << 1) | 0 = 0b10001000-> 0x88 - 当主设备要读(请求数据)时,它发出的地址字节是:
(0x44 << 1) | 1 = 0b10001001-> 0x89
在Arduino的Wire库中,我们通常只需要输入7位地址(如0x44),库函数会自动帮我们完成这个移位和组合的操作。
3. 上拉电阻的作用?
在I2C电路中,上拉电阻是必须的,它的作用至关重要。
I2C总线(SDA和SCL)的设计是开源 drain(或开源集电极,OC)。这意味着芯片内部的电路只能把总线电压拉低(连接到GND),而不能主动拉高(连接到VCC)。
-
如果没有上拉电阻:当没有设备拉低线路时,线路就处于悬空状态,电压是不确定的(既不是高电平也不是低电平),会导致信号错误和通信失败。
-
有了上拉电阻:
- 提供高电平:电阻一端接电源(VCC),另一端接SDA/SCL线。当没有设备主动拉低总线时,电阻会将总线的电压上拉到VCC,形成一个确定的高电平。
- 限流保护:当某个设备(主或从)需要发送低电平时,它内部的MOS管会导通,将总线直接连接到GND。此时,上拉电阻起到了限流的作用,防止了VCC和GND之间形成短路而烧毁芯片。
简单比喻:
上拉电阻就像一根弹簧,把SDA和SCL线始终向上(高电平)拉。当设备需要发送低电平时,它就用力把这条线按下去(拉到GND)。一旦松手,弹簧又会把它拉回高电平。
典型阻值: 常用的是 4.7kΩ 或 10kΩ,具体值取决于总线电容和通信速度。
4. SCL和SDA是怎么配合的?
SCL(串行时钟)和SDA(串行数据)的配合是I2C通信的“舞蹈节拍”,遵循非常严格的时序规则。
核心关系:SCL是时钟,是指挥官;SDA是数据,是士兵。数据必须在时钟的规定下进行变化和采样。
-
稳定性(数据有效):
- 当SCL为高电平期间,SDA线上的数据必须保持稳定,不能变化。此时,发送方和接收方会来读取(采样) SDA上的数据位(是0还是1)。
- 数据变化只能发生在SCL为低电平期间。
-
起始(S)和停止(P)条件:
- 起始条件 (Start Condition):当SCL为高电平时,SDA线发生一个从高到低的跳变。这个特殊的信号告诉总线上所有设备:“注意,一次传输开始了!”。只有主设备可以发起起始条件。
- 停止条件 (Stop Condition):当SCL为高电平时,SDA线发生一个从低到高的跳变。这个信号表示:“本次传输结束,大家释放总线”。
-
数据传输过程:
- 数据传输以字节(8位) 为单位,每个字节后跟一个应答位(ACK/NACK)。
- 主设备控制SCL产生9个时钟脉冲来传输1个字节(8位数据 + 1位应答)。
- 发送数据:在SCL的低电平期间,发送端改变SDA的数据位;在SCL的高电平期间,接收端读取SDA的数据位。
- 应答:每传输完8位数据后,发送端(无论是主还是从)会释放SDA线(让其变高)。在第9个时钟脉冲的高电平期间,接收端必须将SDA线拉低,以此向发送端表示:“我这个字节成功收到了!”。如果接收端没有拉低(保持高电平),则是一个NACK(非应答)信号,通常表示接收失败或通信结束。
总结:
- SCL高,SDA变 -> 起始或停止信号。
- SCL高,SDA稳 -> 读取数据。
- SCL低,SDA变 -> 准备/改变数据。
- SCL第9个脉冲 -> 检查应答信号。
这种严格的配合确保了即使主从设备速度有差异,也能实现可靠的数据同步。
!!!举一个UART的应用实例
一个UART应用实例:Arduino(或任何MCU)与电脑之间的串口通信。
这个例子是几乎所有嵌入式开发者的“Hello World”,它完美地展示了UART的核心作用:在两个设备之间进行异步的、串行的、全双工的数据交换。
1. 实例概述
- 设备A: 电脑 (PC)
- 设备B: Arduino Uno (微控制器)
- 通信协议: UART (通过USB转串口芯片桥接)
- 目标: 实现电脑和Arduino之间的双向通信。电脑通过串口监视器发送指令(如
ON),Arduino收到后控制LED灯,并将状态信息(如LED is ON!)发送回电脑显示。
2. 硬件连接
对于Arduino和电脑通信,硬件连接非常简单,因为USB本身已经提供了桥梁:
| Arduino Uno Pin | PC / USB Cable | 说明 |
|---|---|---|
| 0 (RX) | 通过芯片连接到USB的TX | 接收数据 |
| 1 (TX) | 通过芯片连接到USB的RX | 发送数据 |
| GND | GND | 信号地 |
关键点:
- 交叉连接: Arduino的RX(接收)要接电脑的TX(发送),Arduino的TX(发送)要接电脑的RX(接收)。这样数据才能正确流动。
- USB转串口: Arduino板上有一颗芯片(如ATmega16U2),它的作用就是将USB协议转换成UART协议,所以你可以直接用USB线连接,而无需关心底层的UART电平转换。
- 如果是两个MCU直接相连(如Arduino和ESP32),则需要手动交叉连接:
- MCU_A.TX -> MCU_B.RX
- MCU_A.RX -> MCU_B.TX
- MCU_A.GND -> MCU_B.GND
3. 软件编程 (Arduino Sketch)
// 定义LED引脚
const int ledPin = 13; // Arduino板载LED
// 用于存储来自串口的数据的变量
String incomingData = "";
void setup() {
// 初始化数字引脚13为输出模式
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW); // 初始状态为关闭
// 启动串行通信,设置波特率为9600
// 这个数值必须与串口监视器设置的波特率完全一致!
Serial.begin(9600);
// 等待串口连接(对于有原生USB的板卡很重要)
while (!Serial) {
;
}
// 发送一条欢迎信息到电脑
Serial.println("Hello PC! Arduino is ready.");
Serial.println("Please send 'ON' or 'OFF' to control the LED.");
}
void loop() {
// 检查是否有串口数据可用(是否有数据到达缓存区)
while (Serial.available() > 0) {
// 读取一个字节的数据
char incomingByte = Serial.read();
// 判断是否是结束符(这里用换行符'\n'作为一条命令的结束)
if (incomingByte != '\n') {
// 如果不是结束符,就将字符添加到字符串中
incomingData += incomingByte;
} else {
// 如果收到结束符,说明一条命令接收完毕,开始处理
// 打印接收到的原始数据,用于调试
Serial.print("I received: ");
Serial.println(incomingData);
// 处理命令
if (incomingData == "ON") {
digitalWrite(ledPin, HIGH); // 打开LED
Serial.println("LED is now ON!");
} else if (incomingData == "OFF") {
digitalWrite(ledPin, LOW); // 关闭LED
Serial.println("LED is now OFF!");
} else {
Serial.print("Unknown command: '");
Serial.print(incomingData);
Serial.println("'. Try 'ON' or 'OFF'.");
}
// 清空字符串,准备接收下一条命令
incomingData = "";
}
}
}
4. UART通信流程详解
这个实例展示了UART通信的完整过程:
-
初始化 (Setup):
- 双方约定好通信参数,主要是波特率(9600)。这意味着每秒传输9600个比特位。数据位(8位)、停止位(1位)、奇偶校验位(无)通常使用默认值。
Serial.begin(9600);就完成了这个初始化。
-
电脑发送数据 (PC -> Arduino):
- 你在电脑的串口监视器中输入
ON并点击发送。 - 电脑的串口驱动通过USB线,将字符
'O'和'N'以及一个换行符'\n'(如果你勾选了“换行符”选项)按照UART帧格式异步地发送出去。 - UART帧格式:每个字符(如
'O')被包装成一个帧。- 起始位:先发一个低电平信号,告诉接收方“数据开始了”。
- 数据位:接着发送8个比特位(如
01001111代表大写字母O),从最低位(LSB)开始发。 - 停止位:最后发一个高电平信号,表示“这个帧结束了”。
- Arduino的RX引脚检测到这个波形,硬件UART外设自动解析,将数据字节存入接收缓存区。
- 你在电脑的串口监视器中输入
-
Arduino接收和处理数据:
Serial.available()函数发现缓存区有数据。Serial.read()逐个字节读取数据,并拼接成字符串,直到遇到换行符\n(作为命令的终止符)。- Arduino根据字符串内容执行相应操作(点亮或熄灭LED)。
-
Arduino回复数据 (Arduino -> PC):
- Arduino通过
Serial.println()函数将状态信息(如"LED is now ON!")发送回去。 - 硬件UART外设在TX引脚上自动生成符合UART协议的波形:起始位(低)-> 8个数据位 -> 停止位(高)。
- 电脑通过USB接收这个波形,串口驱动将其解析成字符,最终显示在串口监视器上。
- Arduino通过
5. 其他常见的UART应用实例
UART是用途最广泛的串行通信协议之一,随处可见:
- GPS模块: GPS模块通过UART持续向MCU发送NMEA 0183格式的定位数据(经纬度、时间、卫星数等)。
- 蓝牙/Wi-Fi模块: 如HC-05蓝牙模块、ESP8266 Wi-Fi模块,通常使用UART与主MCU通信(AT指令集),让单片机具备无线连接能力。
- 与另一个微控制器通信: 两个Arduino之间,或者Arduino与树莓派、ESP32等之间交换数据。
- 工业设备和传感器: 许多工业级传感器、PLC、变频器等使用UART(常表现为RS-232或RS-485标准,它们是UART的电平转换版本)进行通信。
- 调试和日志输出: 在项目开发中,通过UART将程序内部的变量、状态信息打印出来,是最简单有效的调试方法。
总结
这个简单的LED控制例子涵盖了UART应用的所有核心要素:
- 异步通信:无需时钟线,依靠事先约定的波特率。
- 全双工:可以同时收发数据(RX和TX线独立)。
- 点对点:通常在两个设备之间进行。
- 帧结构:数据被打包成包含起始位、数据位、停止位的帧。
- 广泛应用:从简单的调试到复杂的设备控制,UART都是最可靠、最常用的通信方式之一。
!!!举一个I2C的应用实例
这个例子非常经典且实用:使用Arduino(作为主设备)读取温湿度传感器SHT30(作为从设备)的数据。
通过这个实例,可以清晰地理解I2C通信的整个流程、硬件连接和软件编程。
1. 实例概述
- 主设备 (Master): Arduino Uno (微控制器)
- 从设备 (Slave): SHT30 (温湿度传感器)
- 通信协议: I2C
- 目标: Arduino通过I2C总线向SHT30发送命令,请求测量数据,然后读取SHT30返回的温湿度数据,并通过串口打印出来。
2. 硬件连接 (I2C布线)
I2C总线只需要两根线,连接非常简单:
| Arduino Uno Pin | SHT30 Sensor Pin | 说明 |
|---|---|---|
| A4 (SDA) | SDA | 串行数据线 |
| A5 (SCL) | SCL | 串行时钟线 |
| 5V | VIN | 电源正极 |
| GND | GND | 电源地 |
注意:
- 大多数I2C传感器模块都内置了上拉电阻。如果你的模块没有(或者为了可靠起见),你需要在SDA和SCL线上分别连接一个4.7kΩ的电阻到5V(或3.3V,根据器件电压决定)。
- 确保主从设备共地。
3. 软件编程 (Arduino Sketch)
我们将使用Arduino的Wire库,这个库专门用于处理I2C通信。
// 引入I2C库
#include <Wire.h>
// 定义SHT30的I2C地址,Datasheet中查到为0x44
// 地址引脚接GND时为0x44,接VCC时为0x45
#define SHT30_ADDRESS 0x44
void setup() {
// 初始化串口,用于打印数据
Serial.begin(9600);
while (!Serial); // 等待串口连接(对于Leonardo等板卡)
// 初始化I2C总线
Wire.begin();
Serial.println("I2C SHT30 Sensor Test");
}
void loop() {
// 1. 发送测量命令
startMeasurement();
// 2. 等待测量完成(SHT30需要约15ms)
delay(15);
// 3. 读取6个字节的数据
readData();
// 每次循环间隔2秒
delay(2000);
}
void startMeasurement() {
// 开始一次I2C传输,指定从设备地址
Wire.beginTransmission(SHT30_ADDRESS);
// 发送测量命令的高字节 (0x2C)
Wire.write(0x2C);
// 发送测量命令的低字节 (0x06)
// 命令 0x2C06 代表:重复性高,时钟拉伸禁用
Wire.write(0x06);
// 结束传输,释放总线
Wire.endTransmission();
}
void readData() {
// 请求从设备(SHT30)发送6个字节的数据
Wire.requestFrom(SHT30_ADDRESS, 6);
// 检查从设备是否发送了数据
if (Wire.available() == 6) {
// 按顺序读取6个字节
byte data[6];
for (int i = 0; i < 6; i++) {
data[i] = Wire.read();
}
// 4. 数据转换和CRC校验(此处简化,省略CRC校验)
// 将两个字节组合成一个16位的温度原始值
// 第一个字节是高8位,第二个字节是低8位
uint16_t rawTemp = (data[0] << 8) | data[1];
// 将原始值转换为实际温度,公式来自Datasheet
double celsius = -45 + (175 * (rawTemp / 65535.0));
double fahrenheit = (celsius * 1.8) + 32;
// 将两个字节组合成一个16位的湿度原始值
uint16_t rawHumidity = (data[3] << 8) | data[4];
// 将原始值转换为实际湿度
double humidity = 100 * (rawHumidity / 65535.0);
// 5. 通过串口打印结果
Serial.print("Temperature: ");
Serial.print(celsius);
Serial.print(" °C, ");
Serial.print(fahrenheit);
Serial.print(" °F | Humidity: ");
Serial.print(humidity);
Serial.println(" %");
} else {
Serial.println("Error: Could not read data from sensor!");
}
}
4. I2C通信流程详解 (对应代码中的步骤)
这个实例完美展示了I2C主从通信的标准流程:
-
主设备发送命令 (启动传输)
Wire.beginTransmission(SHT30_ADDRESS);:主设备在总线上发出起始信号 (S),并广播SHT30的地址(0x44),表示要与之通信。Wire.write(0x2C);和Wire.write(0x06);:主设备将两个字节的命令发送到SDA线上,SCL线提供时钟脉冲。这个命令告诉SHT30:“请开始一次高精度的测量”。Wire.endTransmission();:主设备发出停止信号 §,结束这次写入传输。
-
等待从设备处理
delay(15);:传感器需要时间执行测量。主设备简单等待15毫秒。
-
主设备读取数据 (请求数据)
Wire.requestFrom(SHT30_ADDRESS, 6);:主设备再次发出起始信号 (S),广播SHT30的地址,但这次是读模式(I2C协议中地址的最后一位决定读写),并请求6个字节的数据。Wire.read();:SHT30(从设备)开始掌控SDA线,在主机提供的SCL时钟下,依次发送6个字节的数据。主机在每个时钟脉冲读取一位,组合成一个字节。
-
数据处理
- 将从设备返回的原始数据(Raw Data)根据传感器数据手册(Datasheet)中提供的公式进行计算,转换成有实际意义的温度和湿度值。
-
结果输出
- 将计算好的结果通过串口打印到电脑上。
5. 其他常见的I2C应用实例
除了传感器,I2C总线还广泛应用于各种外设,例如:
- 与显示器的通信: OLED显示屏 (如SSD1306驱动芯片)、LCD显示器。
- 与存储器的通信: EEPROM芯片 (如AT24C32),用于存储少量需要掉电保存的数据。
- 与RTC时钟模块的通信: DS3231等实时时钟芯片,用于获取精确的时间。
- 与IO扩展芯片的通信: PCF8574等芯片,可以用I2C协议扩展出更多的GPIO口来控制LED、按钮等。
- 多主设备系统: 例如,一个系统中的两个微控制器通过I2C交换数据。
总结
这个Arduino + SHT30的实例涵盖了I2C应用的核心要素:
- 硬件连接简单:SDA、SCL、VCC、GND四线制。
- 地址寻址:每个从设备有唯一地址。
- 主从模式:主设备控制时钟并发起通信。
- 标准流程:开始传输 -> 发送命令/数据 -> 结束传输 -> (等待) -> 请求数据 -> 读取数据 -> 处理数据。
!!!举一个SPI的应用实例
好的,我们来深入探讨一个非常典型的SPI应用实例:使用Arduino(作为主设备)控制APA102/SK9822智能RGB LED灯带(作为从设备)。
这个例子完美展示了SPI协议的核心优势:高速、全双工、同步通信,非常适合需要快速传输大量数据的场景,比如控制数百个LED的颜色。
1. 实例概述
- 主设备 (Master): Arduino Uno (微控制器)
- 从设备 (Slave): APA102或SK9822 LED灯珠(每个LED都是一个SPI从设备,以菊花链形式连接)
- 通信协议: SPI (采用类SPI协议,APA102对其有微小改动)
- 目标: Arduino通过SPI总线,向LED灯带发送颜色数据,从而控制每个LED显示特定的颜色和亮度,形成流光溢彩的效果。
2. 硬件连接
SPI需要4根标准线(MOSI, MISO, SCK, SS),但APA102灯带简化了连接,通常只使用3根(因为不需要从LED回传数据,MISO可以不用)。
| Arduino Uno Pin | SPI Signal | APA102 LED Strip | 说明 |
|---|---|---|---|
| Pin 11 | MOSI (主出从入) | DATA (DI) | 主设备发送数据到从设备 |
| Pin 13 | SCK (串行时钟) | CLK (CI) | 主设备产生的同步时钟 |
| Pin 5 | 自定义SS (从设备选择) | 不连接 | 用于控制通信起始(非标准用法) |
| 5V | VCC | VCC | 电源正极 (注意:长灯带需外部供电!) |
| GND | GND | GND | 电源地 (至关重要!必须共地) |
关键点:
- 电平匹配: 大多数APA102灯带是5V逻辑,与Arduino Uno匹配。如果主设备是3.3V系统(如ESP32、树莓派),则需要逻辑电平转换器。
- 供电: Arduino的5V引脚无法提供大电流。驱动超过10个LED时,必须为灯带使用外部5V电源,并确保电源地与Arduino地线相连。
- 数据传输方向: 数据从Arduino的MOSI引脚流出,进入第一个LED的DATA输入,然后从第一个LED的DATA输出流出,进入第二个LED的DATA输入,以此类推,形成菊花链(Daisy-Chain)。这是SPI在LED应用中的典型拓扑。
3. 软件编程 (Arduino Sketch)
我们将使用Arduino的SPI库进行硬件SPI通信,速度更快。
#include <SPI.h>
// 定义控制引脚
// 注意:APA102不需要标准的SS引脚来控制数据帧,但我们用一个自定义引脚来起始通信
const int ledStripPin = 5; // 可以是任何数字引脚
// 设置LED数量
#define NUM_LEDS 10
// APA102通信的起始帧和结束帧(4个字节的0x00)
#define START_FRAME 0x00000000
#define END_FRAME 0xFFFFFFFF // 实际上需要多个字节的0xFF,具体看协议
void setup() {
Serial.begin(9600);
// 初始化SPI
SPI.begin();
// 可以设置SPI时钟速度,APA102支持很高速度
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); // 8MHz时钟
// 初始化自定义控制引脚
pinMode(ledStripPin, OUTPUT);
digitalWrite(ledStripPin, LOW);
Serial.println("SPI APA102 LED Strip Test");
}
void loop() {
// 创建一个数组来存储所有LED的颜色数据
// 每个LED需要4个字节: [亮度标志 0xE7 | 全局亮度] [Blue] [Green] [Red]
uint32_t ledData[NUM_LEDS];
// 示例1: 让所有LED显示红色,半亮
for (int i = 0; i < NUM_LEDS; i++) {
// 结构: 0xE7 (亮度标志) | (亮度值0-31), 然后B, G, R
// ledData[i] = (0xE0 | 0x0F) << 24 | (0x00 << 16) | (0x00 << 8) | 0xFF; // 半亮红色
// 更简单的写法:忽略单独亮度,使用最高亮度,直接用RGB
ledData[i] = 0xFF0000FF; // 格式: 0x[BB][GG][RR],
// 但最后字节先发送,所以实际是 0xE0 0x00 0x00 0xFF
}
sendSPIData(ledData);
delay(1000);
// 示例2: 跑马灯效果(蓝色光点遍历)
for (int pos = 0; pos < NUM_LEDS; pos++) {
for (int i = 0; i < NUM_LEDS; i++) {
if (i == pos) {
ledData[i] = 0xFFFF0000; // 蓝色 (B=FF, G=00, R=00)
} else {
ledData[i] = 0x00000000; // 关闭
}
}
sendSPIData(ledData);
delay(50);
}
}
// 函数:通过SPI发送所有LED的数据
void sendSPIData(uint32_t ledData[]) {
// 1. 拉低自定义控制引脚(可选,用于同步或使能外部电平转换器)
digitalWrite(ledStripPin, LOW);
// 延迟一点点确保信号稳定
delayMicroseconds(1);
// 2. 发送起始帧(根据APA102协议,通常是多个字节的0x00)
SPI.transfer(0x00);
SPI.transfer(0x00);
SPI.transfer(0x00);
SPI.transfer(0x00);
// 3. 发送每个LED的数据(32位 / 4字节 每个LED)
for (int i = 0; i < NUM_LEDS; i++) {
// 分解32位数据为4个字节
// APA102协议要求先发送:亮度字节,然后是蓝、绿、红
uint32_t color = ledData[i];
// 按顺序发送4个字节
// 由于SPI是MSB优先,且APA102期望这个顺序,直接发送即可
SPI.transfer((color >> 24) & 0xFF); // 亮度字节 (0xE0 | brightness)
SPI.transfer((color >> 16) & 0xFF); // 蓝色
SPI.transfer((color >> 8) & 0xFF); // 绿色
SPI.transfer( color & 0xFF); // 红色
}
// 4. 发送结束帧(根据LED数量,需要足够的时钟脉冲将数据锁存到最后一个LED)
// 规则:至少发送 (NUM_LEDS / 16) 个32位的0xFF帧,这里简单发送多个0xFF
for (int i = 0; i < 4; i++) { // 发送4个字节的0xFF作为结束
SPI.transfer(0xFF);
}
// 5. 拉高自定义控制引脚,结束本次数据传输
digitalWrite(ledStripPin, HIGH);
// 延迟一点点
delayMicroseconds(1);
}
4. SPI通信流程详解 (对应代码)
这个实例展示了SPI通信的典型流程:
-
主设备初始化总线 (Begin Transaction)
SPI.beginTransaction()配置SPI外设的时钟速度、数据顺序(MSB/LSB)、时钟极性(CPOL)和相位(CPHA)。这里设置为模式0(CPOL=0, CPHA=0),是最常见的模式。
-
主设备发起通信 (Slave Select)
- 传统的SPI通过拉低SS引脚来选择特定的从设备。在这个例子中,我们使用一个自定义引脚 (
ledStripPin) 来模拟这个动作,标志着一次数据传输的开始和结束。APA102本身不需要SS线,但这是一个好的实践。
- 传统的SPI通过拉低SS引脚来选择特定的从设备。在这个例子中,我们使用一个自定义引脚 (
-
同步数据传输 (Clock & Data)
- 主设备控制SCK产生时钟脉冲。
- 在每个时钟脉冲的边沿(根据CPHA设置),数据被移位和采样。
- 主设备通过MOSI线发送数据帧(起始帧、每个LED的4字节颜色数据、结束帧)。
- 从设备(第一个LED) 在SCK上升沿(模式0)从MOSI线读取数据位。
- 由于是菊花链,第一个LED在接收完自己的数据后,会将它后面LED的数据通过自己的DOUT引脚推到下一个LED的DIN引脚,依此类推。SCK时钟是共享的,所有LED同时移位数据。
-
结束通信和锁存数据 (Latching)
- 发送完所有数据后,主设备拉高自定义SS引脚。
- APA102协议要求在一长串0xFF(结束帧)后暂停SCK一段时间。这个“暂停”信号告诉所有LED:“数据已经全部传输完毕,现在将你们移位寄存器中的数据锁存到输出寄存器,并显示出来”。
sendSPIData函数中的结束帧就是为了实现这个“锁存”功能。
5. 其他常见的SPI应用实例
SPI因其高速特性,被广泛应用于各种需要快速数据交换的设备中:
- 存储器: SD/MicroSD卡、SPI Flash芯片(如W25Q64)、FRAM。
- 显示屏: TFT液晶屏(如ILI9341、ST7789驱动芯片)、OLED屏(部分型号)。
- 传感器: 高精度或高速传感器,如数字加速度计/陀螺仪(MPU-6050, ICM-20602)、气压传感器(BMP280)。
- 无线模块: 2.4GHz射频模块(nRF24L01+)、LoRa模块(SX1276/SX1262)、Wi-Fi/蓝牙模块(如ESP32的SPI接口)。
- 数字音频: 音频编解码器(CODEC)、数字电位器。
- ADC/DAC: 高速模数/数模转换器。
总结
这个Arduino控制APA102 LED灯带的例子完美体现了SPI的核心特点:
- 高速同步: 由SCK时钟严格同步,速度远高于I2C和UART。
- 全双工: 数据可以同时收发(虽然本例中未使用MISO)。
- 主从结构: 绝对由主设备控制时钟和通信发起。
- 菊花链能力: 多个从设备可以共享MOSI、MISO、SCK线,通过SS线片选或通过数据链式传递。
- 硬件实现: 通常由微控制器硬件实现,CPU开销极小,效率极高。
通过这个实例,你可以直观地理解SPI如何高效地操纵需要大量数据的高速外设。
TFT液晶屏(如ILI9341、ST7789驱动芯片)、OLED屏(部分型号)。
- 传感器: 高精度或高速传感器,如数字加速度计/陀螺仪(MPU-6050, ICM-20602)、气压传感器(BMP280)。
- 无线模块: 2.4GHz射频模块(nRF24L01+)、LoRa模块(SX1276/SX1262)、Wi-Fi/蓝牙模块(如ESP32的SPI接口)。
- 数字音频: 音频编解码器(CODEC)、数字电位器。
- ADC/DAC: 高速模数/数模转换器。
总结
这个Arduino控制APA102 LED灯带的例子完美体现了SPI的核心特点:
- 高速同步: 由SCK时钟严格同步,速度远高于I2C和UART。
- 全双工: 数据可以同时收发(虽然本例中未使用MISO)。
- 主从结构: 绝对由主设备控制时钟和通信发起。
- 菊花链能力: 多个从设备可以共享MOSI、MISO、SCK线,通过SS线片选或通过数据链式传递。
- 硬件实现: 通常由微控制器硬件实现,CPU开销极小,效率极高。
通过这个实例,你可以直观地理解SPI如何高效地操纵需要大量数据的高速外设。
5708

被折叠的 条评论
为什么被折叠?



