调一个 I2C 温湿度传感器,读回来全是 0xFF,怀疑芯片焊坏了——结果是 SDA/SCL 的上拉电阻没接,总线一直浮在高电平,0xFF 就是“压根没通信”的默认值。
后来换 SPI 的 Flash,读回来也是全 0xFF——这次是片选 CS 没拉、或者时钟相位(CPHA)配错,主机压根没收到数据。
再往后串口接一个 GPS 模块,收的全是乱码——波特率、数据位、停止位三样里总有一项没对上。
三种总线都叫“串行”,但一个是异步、两个是同步,线数、时钟、地址、应答机制全不一样。各自最容易出的坑也不同:UART 坏在“约定对不上”,SPI 坏在“时钟相位错”,I2C 坏在“上拉和地址”。这篇把它们放到一起对比,讲清差异、逐个拆坑,最后给一句怎么选。
一、先分清:一个异步,两个同步
嵌入式里说的“串行通信”,就是数据一个 bit 一个 bit 按顺序发。但同样是串行,分两大类:
- 同步:收发双方共用一根时钟线,谁发数据、什么时候采样,都由这根时钟决定。SPI 的 SCK、I2C 的 SCL 就是这种。
- 异步:没有时钟线,双方事先约好一个速度(波特率),靠起始位来对齐,剩下的自己数。UART 就是这种。
这个区别决定了后面所有的坑:同步协议不怕“速度差一点”,因为时钟说了算;异步协议全靠双方约定,任何一项没对上就乱码。
顺带解释两个词,后面会反复用到:
- 全双工:收发各走各的线,能同时发、同时收(UART 的 TX/RX、SPI 的 MOSI/MISO)。
- 半双工:一根线又要发又要收,同一时刻只能一个方向(I2C 的 SDA)。
二、一张总对比表
这张表不用背,看到“哪一列能对上你的需求”,再往下看对应那一节的坑就行。
三、UART:没有时钟,全靠“约定”
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)最老也最常见,TX、RX 两根线,接模块、接电脑、打日志都靠它。
一个字节是怎么发出去的
空闲时 TX 保持高电平。开始发一个字节:
- 先拉低一个 bit,叫起始位(告诉对方“我要开始了”);
这一整套叫“一帧”。常用写法是 8N1:8 数据位、无校验(None)、1 停止位。
波特率是什么
波特率(Baud rate)= 每秒钟发送的符号数。没有时钟,双方必须约好同一个波特率,否则收到的就是乱码。
比如 115200 波特率,每个 bit 大约 8.68 微秒。收发双方的晶振都有误差,误差太大、或者长时间连续发,采样点会慢慢漂到 bit 边界上,照样出错。这就是为什么异步通信别选太高波特率、也别在没流控时连续大量发。
最容易踩的四个坑
- 波特率不匹配:最常见。设备默认 9600,你按 115200 收,全是乱码。调试第一步永远是先确认“两边波特率是不是一样”。
- 帧格式不匹配:数据位、停止位、校验位,
8N1 和 8E1(偶校验)混用也会乱。 - 电平标准搞混:UART 只是“协议”,实际信号电平有好几种: - TTL(3.3V / 5V):MCU 引脚直接出来的那种,空闲高、起始位低。 - RS232:±3V~±15V,而且逻辑是反的。TTL 直接接 RS232 口,轻则不通,重则烧引脚,中间要加电平转换芯片(MAX232 之类)。 - RS485:差分信号(A/B 两根线),抗干扰强、能拉很远、还能挂多机,工业现场常用。
- 没有流控会丢数据:UART 全双工但收发独立,接收方来不及处理就会丢。要可靠就用硬件流控(RTS/CTS)或软件流控(XON/XOFF)。
怎么验证
逻辑分析仪抓 TX 波形,量第一个 bit 的宽度,反推波特率(115200 → 8.68µs)。看到乱码,大概率是波形对、波特率/帧格式不对。
四、SPI:快,但“发了不管收到没”
SPI(Serial Peripheral Interface)是同步全双工,四根线,速度最快,常见于 Flash、TFT 屏、ADC、WiFi 模块。
四根线是干什么的
- SCK / SCLK:时钟,由主机产生,从机自己不产时钟。
- MOSI(Master Out Slave In):主机发、从机收。
- MISO(Master In Slave Out):从机发、主机收。
- CS / SS:片选,低有效。一个从机一根 CS,主机拉低哪个,哪个从机才参与通信。
全双工:发的同时也在收
SPI 最特别的一点:时钟每跳一下,主机通过 MOSI 发一个 bit,同时通过 MISO 收一个 bit。所以你想读数据,也得先“发点东西”把时钟跑起来——这也是为什么读 SPI 寄存器时,经常看到“先写一个假字节(dummy)再读”:
// 读一个字节:SPI 是发一个同时收一个,所以读也要发uint8_t spi_read_byte(void){ // 发 0xFF 纯粹是为了产生 8 个时钟,把从机的数据”顶”出来 return spi_transfer(0xFF);}
最大的坑:CPOL 和 CPHA
SPI 没有规定“时钟边沿怎么用”,主机和从机可以各用各的。为了对齐,有了两个参数:
- CPOL(时钟极性):空闲时 SCK 是高还是低。0 = 空闲低,1 = 空闲高。
- CPHA(时钟相位):数据在哪个边沿被采样。0 = 第一个边沿采样,1 = 第二个边沿采样。
两个参数组合出 4 种模式:
主机和从机的模式必须一致,否则读回来就是错位的数据,最常见就是全 0xFF 或全 0x00。多数器件文档会直接告诉你用 Mode 0 还是 Mode 3,照着配就行;不放心就抓波形看边沿。
两个容易被忽略的细节:
- 改 CPOL/CPHA 之前先关掉 SPI,很多 MCU 要求这些配置位在 SPI 使能状态下改动会出问题。
- CPHA=0 时,CS 要每个字节拉高一次;CPHA=1 时 CS 可以一直拉低。所以“从机片选常接低”这种接法,只对 CPHA=1 有效。
其他几个坑
- 没有应答机制:SPI 主机发了就发了,从机收没收到、数据对不对,主机不知道。可靠场景下固件要自己加校验(读回一个固定 ID 寄存器,或加 CRC)。
- CS 没拉 / 忘拉高:CS 低有效,传输前拉低、传完拉高。多个从机时 CS 拉错了,数据就发给了别的芯片。
- MOSI/MISO 接反:硬件上最容易犯的错,读回来全 0xFF 先查这两根线。
- 速度别一上来就拉满:先低速调通再提速。高速(几十 MHz)在长排线上会有振铃、往返延迟,导致 MISO 采样错位(有的 MCU 提供 LATE_MISO_SAMPLE 这类延迟采样来补救)。
- 多从机想省 CS 脚:可以用“菊花链”,把上一级的 MISO 接到下一级的 MOSI,数据一路串行移位过去。省脚,但速度慢、协议绕,一般只在从机特别多时用。
怎么验证
逻辑分析仪同时抓 CS、SCK、MOSI、MISO。重点看两件事:CS 有没有在传输前拉低、SCK 的空闲电平和采样边沿对不对得上器件文档。
五、I2C:省线,但“上拉和地址”两个坑最多
I2C(Inter-Integrated Circuit,也写作 IIC / TWI)只有 SCL、SDA 两根线,却能挂一堆器件,是传感器、EEPROM、RTC 的首选。代价是:它有两个 I2C 独有的坑——上拉电阻和地址。
起始和停止,是两根线上的“暗号”
I2C 用两根线上的电平跳变来表示“开始”和“结束”:
- 起始条件(START):SCL 为高时,SDA 从高跳低。
- 停止条件(STOP):SCL 为高时,SDA 从低跳高。
一次传输从 START 开始、STOP 结束。读写传感器时经常是“先写寄存器地址,再读数据”,中间用重复起始(Repeated Start)把两段连起来,不释放总线。
为什么一定要上拉电阻
I2C 的两根线是开漏(open-drain)输出:任何器件只能把线“拉低”,不能主动“拉高”。所以总线上必须外接上拉电阻到电源,让空闲时线回到高电平。
- 不接上拉 → 总线浮空 → 读回来全 0xFF(开头那个坑)。
- 上拉阻值有讲究:太小,器件拉低时电流大、拉不到合格的低电平;太大,线从低回到高的时间(上升沿)变慢,高速时采样出错。
常见取值:100kHz 用 4.7k,400kHz 用 2.2k 左右,1MHz 用 1k 左右。拿不准就先 4.7k、降到 100kHz 调通再说。
地址:7 位还是 8 位,是最经典的坑
I2C 每个器件有个地址,主机先发地址,匹配上的从机才回应。地址有两种写法,混了就“没应答”:
- 7 位地址:比如 0x68(常见于 RTC、IMU)。实际发送时会把这 7 位左移 1 位,末位补 0(写)或 1(读),变成 0xD0 / 0xD1。
- 8 位地址:有些 datasheet 直接写“写地址 0xD0、读地址 0xD1”,这已经是左移并带上读写位的了。
驱动库(比如 STM32 HAL)要的是 7 位地址。所以 datasheet 写 0xD0,你代码里得写 0x68,或者把 0xD0 右移一位。地址对不上,从机不认,主机就一直收 NACK。
// datasheet 写 8 位写地址 0xD0 / 读地址 0xD1,对应 7 位地址是 0x68#define DEV_ADDR_7BIT 0x68// HAL 的地址参数 = 7 位地址左移一位(HAL 内部会再拼上读写位)#define DEV_ADDR_HAL (DEV_ADDR_7BIT << 1) // 0xD0
ACK / NACK:每发一个字节,对方都要“回一句”
I2C 每传完 8 个 bit,第 9 个 bit 是应答位:
- 接收方保持高 = NACK(没收到 / 没有这个器件)。
所以“地址发出去后收到 NACK”,几乎等于“没有器件在这个地址上”——先查地址写法,再查器件供电。还有个细节:主机读最后一个字节时,要主动回一个 NACK,告诉从机“不用再发了”,然后发 STOP。
时钟拉伸和死锁
从机如果一时处理不过来(比如正在转换),可以把 SCL 拉低,让主机“等等”。这叫时钟拉伸(clock stretching)。主机必须支持“等 SCL 真正变高再继续”,不能用固定延时。
问题在于:如果从机卡死了,SCL 就永远低,总线死锁。恢复办法:把 SCL 当普通 GPIO,手动发 9 个时钟脉冲,让从机把 SDA 释放掉,再发一个 STOP。
其他几个坑
- 速率与线长:总线电容、线长、上拉阻值共同决定能跑多快。器件多了、线长了,就降速。
- 多主仲裁:I2C 允许挂多个主机,靠“线与”和仲裁决定谁说话。但嵌入式里绝大多数是单主机,遇到多主先确认是不是配错了。
怎么验证
逻辑分析仪用 I2C 解码,直接看地址、读写位、ACK/NACK、数据。先看第 9 个 bit 有没有 ACK,没有就先查地址和上拉。
六、到底怎么选
先回答三个问题:要快吗?能占几根线?要挂几个器件?
一句话概括它们的性格:SPI 快、I2C 省、UART 简单。
七、读回 0xFF,是三种总线共用的“没通信”信号
调外设时,读寄存器全是 0xFF(或全是 0x00),别急着怀疑芯片坏,先按总线对号入座:
| |
|---|
| 波特率/帧格式对不对、电平标准对不对、对方到底发没发 |
| CS 拉没拉低、CPOL/CPHA 对不对、MOSI/MISO 接反没 |
| 上拉电阻接没接、地址(7 位/8 位)写对没、有没有 ACK |
最后一句实用的
三条排查口诀,背下来能省很多时间:
- UART 乱码 → 先对波特率,再对帧格式,最后量电平。
- SPI 读 0xFF → 先查 CS,再对 CPOL/CPHA,最后查 MOSI/MISO 接线。
- I2C 读 0xFF → 先查上拉,再看 ACK,最后查地址。
三者共通的判断:先确认“物理上的线和电平对不对”,再确认“协议参数对不对”,最后才怀疑芯片和代码。
相关阅读
- 《数组指针 vs 指针数组:名字反过来,用法全变了》
- 《a、&a、&a[0]:地址一样,凭什么说它们不一样?》
- 《网口调试绕不开的基础常识:IPv4、IPv6、PoE、交换机和子网掩码》
「外设与通信」方向后续还会单点展开:SPI 四种模式的波形实拍、I2C 上拉电阻怎么算、UART 电平标准(TTL/232/485)选型。