N-16 | 嵌入式通信协议 SPI/I2C/UART 对比(附 C 语言指针问题)
详解 SPI 四种工作模式(CPOL/CPHA)与 I2C 的起始/停止/传输/仲裁/时钟同步机制,从时钟、通信方式、阻抗、冲突保护、应答五个维度对比 SPI 与 I2C,并整理 UART/SPI/I2C 三协议的适用速率和场景,最后区分野指针、悬空指针、空指针三个易错概念。
UART、SPI、I2C 是嵌入式系统中最基础的三种串行通信协议。理解它们的物理层特性和协议差异,是选型和调试通信问题的前提。
一、SPI — 串行外设接口
1.1 四种工作模式
SPI 通过两个参数配置时钟极性和相位:
CPOL(Clock Polarity)— 时钟极性:
| CPOL | SCK 空闲状态 | |------|------------| | 0 | 低电平 | | 1 | 高电平 |
CPHA(Clock Phase)— 时钟相位:
| CPHA | 采样边沿 | |------|---------| | 0 | 第一个边沿采样数据 | | 1 | 第二个边沿采样数据 |
四种模式:
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 | 使用频率 | |------|------|------|---------|---------|---------| | Mode 0 | 0 | 0 | 低 | 上升沿 | 最常用 | | Mode 1 | 0 | 1 | 低 | 下降沿 | 较少 | | Mode 2 | 1 | 0 | 高 | 下降沿 | 较少 | | Mode 3 | 1 | 1 | 高 | 上升沿 | 较常用 |
⚠️ 主从设备必须配置成相同模式才能正常通信。调试 SPI 通信失败时,CPOL/CPHA 不匹配是首要排查项之一。
1.2 SPI 的特点(与 I2C 对比)
| 特性 | SPI | I2C | |------|-----|-----| | 时钟 | 很高,MHz 级别 | 最高几百 kHz(标准 100k,快速 400k,高速 3.4M) | | 通信方式 | 全双工 | 半双工 | | 输出阻抗 | 推挽输出,低阻抗信号 | 开漏输出,需外接上拉电阻,高阻抗 | | 冲突保护 | 无(多从机靠 CS 片选) | 有仲裁机制 | | 应答机制 | 无应答(无法确认从机是否在线) | 有 ACK 应答 | | 引脚数 | 4 线(SCLK/MOSI/MISO/CS) | 2 线(SCL/SDA) | | 寻址方式 | 硬件片选 | 软件地址(7/10 位地址) |
💡 补充:SPI 没有应答机制意味着主机无法知道从机是否存在或是否正确接收数据。对于可靠性要求高的场景,需要在应用层加入校验(如 CRC)和确认机制。SPI 也没有流控,发送方和接收方必须保持相同的时钟和配置。
二、I2C — 集成电路间总线
2.1 起始信号与停止信号
| 信号 | 条件 | 含义 | |------|------|------| | 起始信号(START) | 当 SCL 为高电平时,SDA 由高变低 | 宣告 I2C 传输开始,总线由空闲转为占用 | | 停止信号(STOP) | 当 SCL 为高电平时,SDA 由低变高(产生上升沿) | 结束本次传输,总线回到空闲状态 |
当 SCL 为低电平时,允许 SDA 随意变化(数据正在改变);只有 SCL 为高期间 SDA 的状态才被解读为数据或控制信号。
2.2 完整数据传输过程
- 主机发送 START:将 SCL 拉高,再将 SDA 拉低,宣告开始
- 发送从机地址 + R/W 位(7 位地址 + 1 位读写,共 8 bit):
- SCL 低电平时主机改变 SDA
- SCL 高电平时从机采样读取 SDA
- 从机应答 ACK:第 9 个时钟周期,从机将 SDA 拉低表示 ACK(不拉低则为 NACK,通信失败)
- 继续发送数据字节:每个字节 8 bit + 1 bit ACK,可发送 N 个字节,无固定长度
- 主机发送 STOP:通信结束
2.3 仲裁机制(多主机冲突避免)
I2C 是**线与(Wired-AND)**结构——SCL 和 SDA 各主机并联到同一根总线上,所有主机都可以拉低总线,但只有上拉电阻能将其拉高。
仲裁过程:
- 多个主机同时发送 START
- 每个主机在发送 SDA 的同时也读取 SDA
- 如果某个主机发送 SDA=1(高),但读到 SDA=0(被其他主机拉低),则该主机仲裁失败,退出总线
- 胜出的主机继续传输,数据不会被破坏
这就是"开漏输出 + 上拉电阻"结构的精妙之处——通过线与逻辑天然实现了无冲突仲裁。SPI 因为使用推挽输出,多主机无法这样仲裁。
2.4 时钟同步(Clock Stretapping)
从机可以在接收/处理一个字节后,主动将 SCL 拉低并保持,迫使主机等待,直到从机准备好再释放 SCL。主机必须检测 SCL 实际电平,不能盲目发送时钟。
这就是为什么 I2C 从机不需要精确匹配主机时钟——它可以通过拉低 SCL 来"暂停"通信。
三、三种通信协议适用场景对比
速率 典型用途
──── ────────
UART ──→ 不适合板上高速大批量数据传输,同一总线挂载多个从设备
常见速率:9600 ~ 115200 bps(最高约几 Mbps)
典型:115200 bps ≈ 14 KB/s
SPI ──→ 不适合走线很长,不支持多主机(多从机靠 CS)
GPIO 占用多(每个从机一根 CS)
速率:几百 kbps ~ 数 Mbps,可到几十 Mbps
适合:ADC、DAC、显示屏、Flash 等需要高速传输的芯片
I2C ──→ 驱动弱(开漏+上拉),不适合长距离走线
大数据量传输效率低于 SPI
速率:100 kbps(标准)、400 kbps(快速)、1~3.4 Mbps(高速)
适合:传感器(温湿度、加速度计、EEPROM)、RTC、小数据量配置寄存器
选型速查
| 需求 | 推荐协议 | |------|---------| | 板间长距离、点对点 | UART | | 高速(>1Mbps)、短距离、点对点 | SPI | | 多设备、引脚少、低速传感器 | I2C | | 与电脑通信 | UART(USB转串口) | | ADC/DAC 高速采样 | SPI | | EEPROM/温度传感器/RTC | I2C |
四、C 语言三个指针概念辨析
4.1 野指针(Wild Pointer)
定义:初始化时未指向任何确定地址的指针,其值为随机垃圾值(随机地址)。
int *p; // 未初始化,p 指向随机地址
*p = 10; // 危险!向未知地址写入数据
危害:可能修改关键内存(全局变量、栈、外设寄存器),导致程序跑飞、HardFault。
4.2 悬空指针(Dangling Pointer)
定义:指针曾经指向有效内存,但该内存已被释放(free)或函数返回后局部变量已销毁,指针未置 NULL。
int *p = (int *)malloc(sizeof(int));
free(p);
// p 现在是悬空指针——它仍然记得那个地址,但那块内存已不属于它
*p = 20; // 危险!写入已释放的内存(堆)
int *func() {
int x = 10;
return &x; // x 在函数返回后销毁,返回的指针悬空
}
4.3 空指针(NULL Pointer)
定义:指针明确指向 NULL(地址 0),表示"不指向任何有效内存"。
int *p = NULL;
if (p != NULL) { // 使用前检查,安全
*p = 10;
}
最佳实践:
malloc后free,立即将指针置为 NULL:free(p); p = NULL; // 防止悬空指针- 指针声明时初始化为 NULL
- 使用前检查
if (p != NULL)
4.4 三者对比
| 类型 | 指针值 | 危险性 | 产生原因 | |------|--------|--------|---------| | 野指针 | 随机垃圾值 | 极高 | 未初始化 | | 悬空指针 | 曾有效,现已失效 | 高 | free 后未置空 / 函数返回局部变量地址 | | 空指针 | NULL(0) | 低(解引用会立即崩,易定位) | 主动置 NULL / 初始化 |
💡 空指针解引用通常触发立即崩溃(HardFault),易于发现;而野指针和悬空指针的写入可能"安静地"破坏数据,等到程序在某处异常时才暴露,调试难度大。防御性编程的核心就是消除野指针和悬空指针。
原始笔记照片

