逻辑分析仪实战:从波形一眼看出 UART/SPI/I2C 通信错在哪

适用人群:会用 HAL 库调 UART/SPI/I2C,但一旦不通就只会改代码瞎试 的同学。手上有块 STM32/ESP32 开发板、一个几十块的 USB 逻辑分析仪就够了。

读完你能得到:① 一套"通信不通先抓波形"的固定动作;② UART 波特率误差怎么手算、为什么同样是 8MHz@115200,STM32 没事 AVR 就废;③ SPI 的 CPOL/CPHA 在波形上到底长什么样、配错时你会看到什么;④ I2C 上拉电阻按公式算出来(不是抄 4.7k)、7 位/8 位地址在解码器里怎么对得上;⑤ 一张 10 条的踩坑表 + 5 个"故意搞坏再抓波形"的动手练。

和《工具实践_03 示波器和逻辑分析仪》互补:那篇讲仪器本身怎么用,这篇讲波形怎么读,没看过那篇也不影响。


一、先说说我为什么开始用逻辑分析仪

大二下我调一块 SHT30 温湿度板,HAL_I2C_Master_Transmit() 一直返回 HAL_ERROR。我干了什么?

  • 换 I2C1 → 不行
  • 改成软件 I2C → 还是不行
  • 把延时从 10ms 改到 100ms → 依然不行
  • 怀疑传感器坏了,又买了一个 → 一样

前后耗了整整两个晚上。后来学长把他那个 100 块的 USB 逻辑分析仪插上,抓了一次,屏幕上明明白白写着:

复制代码
START  →  Address write: 22  →  NACK  →  STOP

我代码里明明按手册写的是 0x44,可解码器读出来的地址是 0x22------正好是 0x44 的一半 。原因是 STM32 HAL 要的是"左移一位之后"的值 ,我少移了那一位,总线上跑的地址就缩水成了 0x22,自然没有从机应答(正确应该传 0x88,细节见第五节)。整个问题 30 秒定位完。

那天我明白一件事:

通信调试里,"改代码试"是最慢的方法。示波器和逻辑分析仪不是"高级玩家的玩具",而是让你从"猜"切换到"看"的开关。

这篇就把 UART / SPI / I2C 三种最常见的通信,在波形上错成什么样、怎么一眼认出来 讲清楚。工具用开源免费的 sigrok / PulseView,硬件用几十块的 FX2 逻辑分析仪,学生党零门槛。

💡 为什么不用示波器?示波器强在看"模拟质量"(电平、过冲、纹波、上升沿),逻辑分析仪强在看"数字内容"(这一串 0/1 到底是什么字节)。通信不通,90% 先用逻辑分析仪抓协议;只有怀疑电平/上升沿这种模拟问题时才上示波器。 后面 I2C 上拉那节你会看到两者的交界处。


二、开抓之前,这 5 个概念必须懂

新手第一次插上逻辑分析仪,最常见的失败不是"抓不到",而是"抓到一堆看不懂的东西"。原因基本都出在下面这 5 个设置上。

概念 白话解释 设错的后果
采样率(Samplerate) 每秒对引脚电平"拍照"多少次 太低 → 窄脉冲被漏掉,波形看着"好好的"但解码全错
存储深度 / 采样数 这次一共拍多少张照 太小 → 只抓到开头一丁点,错误发生在后面根本没录到
触发(Trigger) 满足什么条件才开始录 没设 → 录到的全是空闲电平,真正出错那一瞬间没抓着
pre-trigger 比例 触发点之前保留多少数据 设成 0% 会漏掉第一个起始位(下面细说)
协议解码器(Decoder) 软件替你把 0/1 翻译成字节/地址/ACK 参数配错 → 解出来是乱码,你会误以为硬件坏了

2.1 采样率到底要多高?

课本上说的奈奎斯特(2 倍)在这里不够用。数字解码要的是"每一位都能踩准中间点",工程上的经验值是:

复制代码
最低要求:采样率 ≥ 信号最高频率 × 4
建议值  :采样率 ≥ 信号最高频率 × 10

(这里的"信号频率":UART 就是波特率,I2C/SPI 就是 SCL/SCK 的时钟频率)

套到我们三种协议上(这个表建议直接背下来):

协议 典型速率 一位/一个时钟的时间 建议采样率
UART 115200 8N1 115200 bps 1 bit ≈ 8.68 µs 1 MHz(≈ 8.7 点/位,够用;求稳 2 MHz)
UART 921600 921600 bps 1 bit ≈ 1.085 µs 8~12 MHz
I2C 标准模式 100 kHz 半周期 5 µs 1 MHz
I2C 快速模式 400 kHz 半周期 1.25 µs 4~8 MHz
SPI 1 MHz 半周期 500 ns 8~16 MHz
SPI 8 MHz 半周期 62.5 ns 24 MHz 已经紧张,建议换更快的设备

⚠️ 便宜的 FX2(CY7C68013A)逻辑分析仪,板上是 24 MHz 晶振 ,sigrok 里实际能稳定用的档位到 24 MHz 。淘宝页面上写"48M 采样"的多半是单通道/超频宣传,USB 2.0 的带宽在 8 通道 24 MHz 时已经很吃紧,容易丢样本。结论:用它抓 UART、I2C、8MHz 以下的 SPI 很舒服;抓 20MHz+ 的 SPI Flash 就别为难它了。

按上面的表反推一个常见翻车现场:你用 1 MHz 采样率去抓 8 MHz 的 SPI,一个 SCK 周期只有 0.125 个采样点------你看到的根本不是时钟,是随机噪声。很多人以为"SPI 打不通",其实是"逻辑分析仪没看清"。

2.2 存储深度:别只抓开头那 1ms

FX2 这类设备是流式传输到电脑的,所以"深度"取决于你设的采样数和 USB 带宽。实战建议:

复制代码
需要的采样点数 ≈ 采样率 × 你想观察的时间长度

例:1 MHz 采样,想看 1 秒钟的 I2C 通信
    → 1,000,000 × 1 = 1M samples

抓周期性上报(比如每秒读一次传感器)时,至少抓 2~3 个完整周期,否则你不知道"第一次成功、后面全失败"这种典型现象。

2.3 触发:别再"点开始然后手忙脚乱按复位"

没有触发,你的操作是这样的:点"运行" → 冲过去按开发板复位 → 回来一看,波形早跑完了。

FX2 这类廉价设备没有硬件触发 ,sigrok 用的是软件触发:数据先流进电脑,由 libsigrok 在数据流里找条件,条件满足前的数据丢掉。够用,只是不能像专业设备那样零丢包。

PulseView 里点通道名旁边的触发图标就能设:

触发条件 图标含义 什么时候用
下降沿 ↓ falling edge UART 抓起始位(空闲是高,起始位拉低);I2C 抓 SCL 第一个下降沿
上升沿 ↑ rising edge 抓片选释放、抓中断引脚
低电平 0 low level SPI 抓 CS 拉低期间(最常用)
高电平 1 high level 抓某根使能信号有效期

⚠️ 这是官方文档白纸黑字写的坑 :sigrok wiki 在 uart 解码器页面明确写着------如果你在 PulseView 里把 pre-trigger capture ratio 设成 0% ,解码器很可能漏掉第一个起始位 ,导致开头几个字节因帧错误显示成乱码;把 pre-trigger 比例设成 1% 就能解决

我第一次抓 UART 就栽在这:前 3 个字节全是乱码,后面全对,还以为是 MCU 上电时钟没稳。不是板子的锅,是软件设置的锅。

2.4 阈值与接地(唯一两个"硬件"注意事项)

复制代码
        逻辑分析仪                     被测板
      ┌─────────────┐              ┌──────────────┐
      │  CH0  ──────┼──────────────┤ PA9  (TX)    │
      │  CH1  ──────┼──────────────┤ PA10 (RX)    │
      │  GND  ──────┼──────────────┤ GND  ← 必接! │
      └─────────────┘              └──────────────┘
  1. GND 必须接,而且要接被测板的 GND。不接地你会抓到一堆随机跳变------这是新手第一天最常问的"为什么波形在乱抖"。
  2. 电平要匹配 。FX2(CY7C68013A)是 3.3V LVTTL 输入、5V 容忍 ,判"高"要求输入电压约 2.0V 以上 ,所以抓 3.3V 系统毫无压力;但抓 1.8V 系统就不行了 ------1.8V 的高电平根本够不着 2.0V 门限,你会看到"一直是低"或时高时低。抓 1.8V 请换支持可调阈值的设备(比如 DSLogic)。

2.5 软件选型:学生就用 PulseView

方案 价格 优点 缺点
sigrok / PulseView + FX2 分析仪 软件免费开源,硬件 30~100 元 100+ 协议解码器;Win/Linux/Mac 全平台;sigrok-cli 可脚本化 只有软件触发;24 MHz 封顶
Saleae Logic 2 + 正品 Saleae 硬件较贵 软件体验最好,解码器丰富,分析仪自带存储 学生党预算劝退
DSLogic + DSView 200~400 元 阈值可调、深存储、比 FX2 强一档 生态不如前两者广

我的建议:先花几十块买 FX2 的 8 通道版,配 PulseView。 能把 UART/SPI/I2C 全抓明白,等你真的需要抓 50MHz QSPI 时,你已经知道自己要买什么了。

安装(Ubuntu/Debian):

bash 复制代码
# 装 PulseView + 命令行工具 + FX2 固件
sudo apt install pulseview sigrok-cli sigrok-firmware-fx2lafw

# 让普通用户能访问设备(否则要 sudo 才能识别)
sudo usermod -aG plugdev $USER   # 重新登录生效

# 插上设备后确认能识别
sigrok-cli --scan
# 期望输出类似:fx2lafw:conn=1.5 - sigrok FX2 LA (8ch) with 8 channels: D0 D1 ... D7

Windows 直接去 sigrok 官网下 PulseView 安装包,里面带 Zadig 帮你换 WinUSB 驱动。


三、UART 实战:乱码 ≠ 波特率一定错

UART 是最容易调、也是最容易"看起来玄学"的协议。先把它的样子刻进脑子里。

3.1 一帧 8N1 长什么样

复制代码
                    ┌──── 8 个数据位(LSB 先发)────┐
        idle        │                               │      idle
  ───────────┐  ┌───┬───┬───┬───┬───┬───┬───┬───┐   ┌──────────
             │  │ D0│ D1│ D2│ D3│ D4│ D5│ D6│ D7│   │
             └──┘   │   │   │   │   │   │   │   └───┘
             ↑START ↑   ↑   ↑   ↑   ↑   ↑   ↑   ↑ STOP
             (低)   ▲采样点在每一位的正中间          (高)

  一位宽度 Tbit = 1 / 波特率      115200 → 8.68 µs
  一帧 8N1 共 10 位:1 起始 + 8 数据 + 1 停止 = 86.8 µs

三个关键点,考试和面试都爱问:

  1. 空闲是高电平 ,起始位是,所以接收方靠"下降沿"发现有数据来了。
  2. 数据位 LSB 先发 。所以你在波形上从左往右读到 0 1 0 0 0 1 1 0,实际字节是把它倒过来 0110 0010 = 0x62 = 'b'新手 90% 第一次手工解码都读反了。
  3. 接收方是在每一位的正中间采样的------这就是下面波特率容差的全部来源。

3.2 波特率能差多少?先把这个数算明白

UART 没有时钟线,收发双方各用各的时钟,全靠"约定好的波特率"对齐。那么允许差多少

理论上限很好推:接收方在检测到起始位下降沿后开始计时,最后一个要判定的位是停止位,它的采样点在 9.5 个位宽之后(1 个起始 + 8 个数据 = 9 个位宽,再加半个位宽到停止位中央)。只要累计漂移不超过半个位宽(0.5 Tbit),就还能采对:

复制代码
累计漂移 = 9.5 × Tbit × 误差率  <  0.5 × Tbit

  ⇒ 误差率 < 0.5 / 9.5 = 5.26%

但 5.26% 是"双方合计、且一切理想"的天花板,千万别当设计指标用。 真实 UART 接收器还要做多数表决(一位内采 3 次投票)、还有采样时钟的量化误差。Microchip 的 AVR 数据手册给了官方公式(S=16 表示每位采 16 次,S_F=8、S_M=9 是投票用的采样序号,D 是"数据位+校验位"的个数,8N1 对应 D=8):

复制代码
Rslow = (D+1)·S / (S−1 + D·S + S_F) = 9×16 / (15 + 128 + 8) = 144/151 = 95.36%
Rfast = (D+2)·S / ((D+1)·S + S_M)   = 10×16 / (144 + 9)     = 160/153 = 104.58%

  ⇒ 绝对上限约 +4.58% / −4.64%
  ⇒ 手册"推荐的最大接收误差"这一列,D=8 时只给 ±2.0%

记住两个数就够了:工程上按 ±2% 设计,超过 ±3% 就开始随缘,接近 5% 必炸。

3.3 关键修正:8MHz 跑 115200 到底行不行?要分平台

网上(包括我以前写的笔记)到处流传一句话:"8MHz 主频跑 115200 误差太大,会乱码。" 这句话只对了一半 ,害我误判过一次。真相是:误差大小取决于这颗 MCU 的波特率发生器有没有小数分频。

情况 A:STM32(有小数分频)------ 完全没问题

STM32 的分频值不是整数,而是带小数部分的定点数------这就是它和 AVR 的根本差别:

text 复制代码
(以下是手算过程,不是可编译代码)
/* STM32F1/F4 写法,USART 时钟 f_CK = 8 MHz,OVER8 = 0(16 倍过采样) */
USARTDIV = f_CK / (16 × baud) = 8 000 000 / (16 × 115200) = 4.34
/* BRR 用 12 位整数 + 4 位小数来存它,也就是分频值的分辨率是 1/16 */
BRR = round(4.34 × 16) = 69 = 0x45     /* 尾数 4,小数 5/16 → 实际分频 4.3125 */

实际波特率 = 8 000 000 / (16 × 4.3125) = 8 000 000 / 69 = 115 942 bps
误差       = (115942 − 115200) / 115200 = +0.64%     ← 远小于 ±2%,稳

/* F0/F3/L4/G0/H7 等新系列的手册换了个等价写法:BRR 直接 = f_CK / baud
   = round(8 000 000 / 115200) = round(69.44) = 69 = 0x45,结果完全一样 */

情况 B:AVR / 老式 8051(只有整数分频)------ 真的会炸

ATmega328P 这类 AVR 的波特率寄存器 UBRR 只能取整数,分辨率是"一整个分频档":

text 复制代码
(以下是手算过程,不是可编译代码)
/* ATmega328P,F_CPU = 8 MHz,U2X0 = 0(16 分频) */
UBRR = F_CPU / (16 × baud) − 1 = 8000000 / (16 × 115200) − 1 = 3.34
UBRR 只能取整 → 3
实际波特率 = 8000000 / (16 × (3+1)) = 125 000 bps
误差 = (125000 − 115200) / 115200 = +8.51%          ← 超过 5.26% 天花板,必乱码

/* 打开倍速模式 U2X0 = 1(8 分频)能救一点 */
UBRR = 8000000 / (8 × 115200) − 1 = 7.68 → 取整 8
实际波特率 = 8000000 / (8 × 9) = 111 111 bps
误差 = −3.55%    ← 勉强能通,但超过手册推荐的 ±2%,长报文/温漂时容易出错

⚠️ 所以正确的说法是 :不是"8MHz 不能跑 115200",而是"没有小数分频的 MCU 在 8MHz 下跑 115200 误差过大"。AVR 圈子里为什么爱用 7.3728MHz / 11.0592MHz 晶振?就是因为这些频率能被 115200 整除,误差直接为 0。STM32 有小数分频,用 8MHz 内部/外部时钟跑 115200 误差只有 +0.64%,你要是照搬这句话去改 STM32 的时钟树,就是白折腾。

3.4 PulseView 里配 UART 解码器

抓完波形,点右上角 "Add protocol decoder" → 选 UART,然后配这几项:

选项 填什么 说明
RX / TX 选对应通道 只抓单向就只配一个,另一个留空
Baud rate 115200 填代码里设的值,不是你猜的值
Data bits 8
Parity none 配错的现象见下一节
Stop bits 1.0
Bit order lsb-first UART 默认 LSB 先发,别改
Sample point 50(%) 在位中间采样;信号边沿很糟时可微调
Invert no 直连 MCU 引脚选 no;接 RS-232 电平(±12V 且逻辑反相)才要考虑

💡 不同版本的 libsigrokdecode 选项名可能微调(例如老版本的校验位选项叫 parity_type,新版本叫 parity)。sigrok-cli --show -P uart 可以打印出你这版解码器的确切选项名和取值,比猜靠谱。

命令行党可以直接一条命令抓+解码,不用开图形界面:

bash 复制代码
# 1 MHz 采样 D0 通道抓 200ms,直接按 115200 8N1 解码
sigrok-cli --driver fx2lafw --config samplerate=1M \
           --channels D0 --time 200ms \
           --protocol-decoders uart:rx=D0:baudrate=115200:data_bits=8:parity=none:stop_bits=1.0 \
           --protocol-decoder-annotations uart=rx-data

3.5 三种 UART 错误在波形上的样子

① 波特率不匹配 ------ 靠"量最窄脉冲"反推真实波特率

不知道对方实际跑多少波特率时,有个万能招:在波形上找最窄的那一段高电平或低电平,它的宽度就是 1 个位宽(因为连续相同的位会连成一片,最窄的那段一定只有 1 位)。

复制代码
用 PulseView 的游标量出来:最窄脉冲 = 8.68 µs
  实际波特率 = 1 / 8.68µs ≈ 115 200   → 和代码一致,波特率没问题

如果量出来 = 6.94 µs
  实际波特率 = 1 / 6.94µs ≈ 144 000   → 比 115200 快 25%
  25% ≈ 常见时钟树配错的比例(比如 HSE 晶振写成 8MHz 实际是 10MHz,
  或者 PLL 分频写错),去查 SystemClock_Config()

波特率差多少才乱码? 按 3.2 的结论:差 2% 以内基本无感;差 3~5% 时短帧偶尔对、长帧开始错(这最坑,会让你以为是"偶发干扰");差 5% 以上稳定乱码。

② 停止位/数据位配错 ------ 解码器报 framing error

波形本身是对的,但你在 PulseView 里把 Data bits 填成 7、或者 Stop bits 填成 2,解码器就会在该出现停止位的位置读到低电平,标出帧错误(framing error)

反过来也一样:MCU 那边设了 8E1(带偶校验),你的解码器配 8N1 ,那校验位会被当成停止位读------校验位是 0 时就报帧错误,是 1 时"蒙对"。所以你会看到一半字节正常、一半字节报错的诡异现象。

💡 判断口诀:解码全乱 → 先怀疑波特率;解码"时对时错、错得有规律" → 先怀疑数据位/校验位/停止位配置。

③ 电平反了 ------ 整条线空闲是低

抓到的波形空闲时是低电平 ,那多半是你接在了 RS-232 的 DB9 上(RS-232 逻辑 1 是 −3~−15V,且和 TTL 反相),或者中间过了一级反相驱动。这种情况不能直接接逻辑分析仪(负电压会伤设备),必须先过 MAX3232 转成 TTL 再抓


四、SPI 实战:CPOL/CPHA 配错,波形"看着都对"但数据全废

SPI 有时钟线,所以不存在波特率误差问题,但它有个更阴的坑:四种模式

4.1 CPOL 和 CPHA 到底在说什么

  • CPOL(时钟极性):SPI 空闲(CS 未选中)时,SCK 是低(CPOL=0)还是高(CPOL=1)。
  • CPHA(时钟相位) :在时钟的第一个边沿 采样(CPHA=0),还是第二个边沿采样(CPHA=1)。
模式 CPOL CPHA 空闲时 SCK 采样边沿
Mode 0 0 0 上升沿采样,下降沿输出
Mode 1 0 1 下降沿采样,上升沿输出
Mode 2 1 0 下降沿采样,上升沿输出
Mode 3 1 1 上升沿采样,下降沿输出

画成波形(以 Mode 0 和 Mode 3 对比,这两个最常用):

复制代码
Mode 0 (CPOL=0, CPHA=0):空闲低,第一个边沿=上升沿,在它上面采样
                ┌──┐  ┌──┐  ┌──┐  ┌──┐
  SCK  ─────────┘  └──┘  └──┘  └──┘  └────
                ↑     ↑     ↑     ↑
              采样  采样   采样  采样   (都是上升沿)
  MOSI ──┬─────────┬─────┬─────┬─────┬────
         │  b7     │ b6  │ b5  │ b4  │
         └─────────┴─────┴─────┴─────┴
         ↑ 数据必须在第一个上升沿"之前"就摆好

  CS   ──┐                                ┌──
         └────────────────────────────────┘
         ↑ CS 拉低后、第一个时钟前,MOSI 已有效


Mode 3 (CPOL=1, CPHA=1):空闲高,第一个边沿=下降沿(不采样),第二个边沿=上升沿采样
  SCK  ─────────┐  ┌──┐  ┌──┐  ┌──┐  ┌────
                └──┘  └──┘  └──┘  └──┘
                   ↑     ↑     ↑     ↑
                 采样   采样  采样   采样  (还是上升沿!)

看出来了吗?Mode 0 和 Mode 3 的采样边沿都是上升沿,只是空闲电平相反。这就解释了一个新手常疑惑的现象:

为什么很多 SPI 器件的手册写着"支持 Mode 0 和 Mode 3"? 因为对从机来说,它只关心"在哪个边沿把 MOSI 采进去"。Mode 0 与 Mode 3 的采样边沿等效(都是上升沿采样、下降沿换数据),从机做时序对齐时可以不管 SCK 空闲时停在高还是低。W25Q64 这颗最常见的 SPI Flash 就是典型:Mode 0 和 Mode 3 都支持,Mode 1/2 不支持。
⚠️ CPHA=0 的隐含要求 :既然第一个边沿就要采样,那数据必须在第一个时钟边沿到来之前的半个周期内就建立好 (也就是 CS 拉低之后、SCK 动之前)。硬件 SPI 会自动处理,但你写**软件模拟 SPI(bit-bang)**时最容易漏掉这一步:先给 SCK 边沿再摆 MOSI,从机采到的永远是上一位。

4.2 实战:用读 JEDEC ID 当"SPI 通不通"的试金石

调 SPI Flash(W25Q64 这类)时,第一件事永远不是读数据,而是读 ID。它只有 4 个字节,不涉及地址、不涉及擦写,最适合验证物理连接和模式:

c 复制代码
/* STM32 HAL,W25Q64 读 JEDEC ID(命令 0x9F)
 * 正确返回:0xEF 0x40 0x17
 *   0xEF = Winbond 厂商 ID
 *   0x40 = 存储器类型(SPI Flash)
 *   0x17 = 容量码,2^0x17 = 8MB = 64Mbit → 所以叫 W25Q64
 */
uint8_t w25q_read_jedec_id(SPI_HandleTypeDef *hspi, uint8_t id[3])
{
    uint8_t cmd = 0x9F;
    HAL_StatusTypeDef st;

    HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET);   /* CS 拉低 */

    st = HAL_SPI_Transmit(hspi, &cmd, 1, 100);        /* 100ms 超时,不要写 HAL_MAX_DELAY */
    if (st == HAL_OK) {
        st = HAL_SPI_Receive(hspi, id, 3, 100);
    }

    HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET);     /* CS 拉高 */
    return (st == HAL_OK) ? 0 : 1;
}

⚠️ 超时参数别写 HAL_MAX_DELAY。SPI 从机不响应时(比如 CS 接错),这个函数会永远阻塞,看门狗一复位你还以为是"死机"。所有外设调用都给一个有限超时,这是工程习惯。

四种结果对应四种病因,对着查:

读到的 ID 病因 怎么确认
EF 40 17 一切正常 收工
00 00 00 MISO 一直是低 / 从机没供电 / GND 没共 抓波形看 MISO 是不是死在低电平
FF FF FF MISO 悬空(上拉/浮空)、CS 没拉低、接线接反 抓波形看 CS 有没有真的拉低
DE 80 2E 这种"整体左移一位" CPHA 配反了 见下面分析

为什么 CPHA 配反会"左移一位"? 因为主机在错误的边沿采样,等于把第一位漏掉了、后面每一位都往前挪一格

复制代码
从机实际输出的位流: 1110 1111  0100 0000  0001 0111   = EF 40 17
                     ↑漏掉第一位
主机采到的位流:      110 1111 0  100 0000 0  001 0111 X
重新分组:           1101 1110  1000 0000  0010 111X   = DE 80 2E/2F

(错位往哪个方向、末位补什么,取决于相位错的方向和总线空闲电平------反过来配错时会变成"重复采到第一位"。你不用记具体值,记住"能通但整体错位"这个特征就够了。

看到"能通信、但数据整体错位",第一反应就是查 CPOL/CPHA。 这比一个个改配置试快得多。

4.3 CS 时序:另一个高频翻车点

复制代码
正确时序:
  CS   ───┐                              ┌───
          └──────────────────────────────┘
  SCK  ──────┐┌┐┌┐┌┐┌┐┌┐┌┐┌┐┌────────────────
             └┘└┘└┘└┘└┘└┘└┘└┘
             ↑ CS 拉低之后 SCK 才开始动
                             ↑ 全部时钟走完,CS 才拉高


翻车 1:CS 还没拉低,SCK 就开始了
  CS   ──────────┐                       ┌───
                 └───────────────────────┘
  SCK  ──┐┌┐┌┐┌┐┌┐┌┐┌┐┌───────────────────────
         └┘└┘└┘└┘└┘└┘└┘
         ↑ 这几个时钟从机根本没在听 → 后面全部错位

翻车 2:一次传输中途 CS 被拉高又拉低
  CS   ───┐        ┌──┐        ┌───
          └────────┘  └────────┘
                   ↑ 从机认为"这次交易结束了",内部位计数器清零
                     → 多字节读写(比如页写 256 字节)会从中间截断

翻车 2 特别隐蔽 :如果你在 HAL 里用了"每发一个字节就拉一次 CS"的封装(有些例程真这么写),单字节命令能通,一到多字节读写就崩。抓波形一眼就能看出来 CS 中间抬起来过。

4.4 PulseView 里配 SPI 解码器

SPI 要接 4 根线(CLK/MOSI/MISO/CS),解码器选项对应如下:

选项 填什么 备注
CLK / MOSI / MISO / CS 对应通道 CS 强烈建议接上,否则解码器不知道从哪开始分字节
cs_polarity active-low 绝大多数器件低有效
cpol 0 或 1 和你代码里 hspi.Init.CLKPolarity 一致
cpha 0 或 1 hspi.Init.CLKPhase 一致
bitorder msb-first SPI 默认 MSB 先发,和 UART 相反,别搞混
wordsize 8 位宽,STM32 也能配 16 位

小技巧:如果你不确定器件是 Mode 0 还是 Mode 3,就把 CPOL 两个值都解一遍 ------哪个解出来是 EF 40 17,哪个就是对的。这比翻手册快。


五、I2C 实战:坑最多的协议,但也最容易看懂

I2C 只有两根线,却是三个协议里翻车率最高的------因为它是开漏 + 上拉结构,既有协议问题,又有电气问题。

5.1 一次完整的 I2C 写操作长这样

先看整体结构 (以主机向 7 位地址 0x44 的 SHT30 写 2 字节命令为例):

复制代码
 ┌───────┬──────────────────────┬─────┬──────────┬─────┬──────────┬─────┬──────┐
 │ START │ 7位地址 1000100 + W=0 │ ACK │ 数据 0x2C │ ACK │ 数据 0x06 │ ACK │ STOP │
 └───────┴──────────────────────┴─────┴──────────┴─────┴──────────┴─────┴──────┘
              8 个 SCL 时钟        第9个   8 个时钟   第9个  8 个时钟   第9个
              (线上跑的就是 0x88)  ↑从机拉低 SDA 表示"收到"

再看位级规则(三张图,把 I2C 的全部时序规矩讲完):

复制代码
① 普通数据位:SCL 低时才能改 SDA,SCL 高时 SDA 必须稳定

              ┌──── SDA 可以在这段变化 ────┐ ┌── SDA 必须钉住不动 ──┐
  SCL  ───┐                               ┌───────────────────────┐
          └───────────────────────────────┘                       └────────
                        (SCL = 低)              (SCL = 高,接收方在这里采样)


② START / STOP:唯一允许"SCL 高时 SDA 跳变"的两个特例

     START(起始)                      STOP(停止)
  SDA ─────┐                        SDA              ┌──────
           └──────                  ─────────────────┘
  SCL ────────────                  SCL ────────────────────
      (SCL 保持高,SDA 由高变低)        (SCL 保持高,SDA 由低变高)


③ ACK 位(每个字节后的第 9 个时钟)

  SDA  ────────┐             ┌────      ← 主机松手,线被上拉拉高
               └─────────────┘             从机把它拉低 = ACK(收到)
                                           线上仍然是高 = NACK(没人应答)
  SCL  ──┐         ┌──────┐
         └─────────┘      └───────      ← 这就是第 9 个时钟

其余三条要背下来的规则:

  • 地址和数据都是 MSB 先发(和 SPI 一样,和 UART 的 LSB 先发相反)。
  • 地址字节的最低位是 R/W:0 = 主机写,1 = 主机读。
  • SDA/SCL 都是开漏:谁都能把线拉低,但没人能主动拉高------拉高全靠上拉电阻(5.4 节细说)。

5.2 7 位地址 vs 8 位地址:这是我踩过最贵的一个坑

I2C 规范里,从机地址是 7 位 ,加上最低位的 R/W 才凑成实际在线上传输的那 8 位。问题就出在**"手册写的是哪个"和"函数要的是哪个"经常不一致**。

以 SHT30 为例,数据手册写"I2C 地址 0x44",这是 7 位地址。实际在总线上跑的第一个字节是:

复制代码
7 位地址 0x44 = 100 0100b
左移一位,最低位补 R/W:
   写操作: 1000 1000b = 0x88
   读操作: 1000 1001b = 0x89

STM32 HAL 的官方说法stm32xxxx_hal_i2c.c 函数注释原文):

"@param DevAddress Target device address: The device 7 bits address value in datasheet must be shifted to the left before calling the interface"

翻译过来:HAL 要你传"左移后"的 8 位值。所以:

c 复制代码
/* ❌ 错误:直接把手册上的 0x44 传进去 → 总线上跑的是 0x22 + R/W,没有任何从机应答 */
HAL_I2C_Master_Transmit(&hi2c1, 0x44, cmd, 2, 100);

/* ✅ 正确:左移一位再传 */
#define SHT3X_ADDR_7BIT   0x44
#define SHT3X_ADDR_HAL    (SHT3X_ADDR_7BIT << 1)   /* = 0x88 */
HAL_I2C_Master_Transmit(&hi2c1, SHT3X_ADDR_HAL, cmd, 2, 100);

⚠️ 别的平台不一样,这才是真正混乱的根源 :Linux 的 i2c-dev、Arduino 的 Wire.beginTransmission()、ESP-IDF 的 i2c_master_bus_add_device() 用的都是7 位地址 (库内部帮你移位);只有 STM32 HAL 要你自己移。换平台移植代码时,这是第一个要检查的地方。

那 PulseView 显示的是哪个? sigrok 的 i2c 解码器有个 address_format 选项,默认值是 shifted ,意思是它会把线上抓到的 8 位右移一位后 显示------也就是显示 7 位地址。所以:

复制代码
PulseView 上显示:  Address write: 44        ← 7 位地址
你代码里应该写的:  0x88                     ← 左移后的值

两者差一位,都是对的,别以为解码器错了。
(想让它显示 0x88,把 address_format 改成 unshifted)

5.3 没有 ACK 时,按这张表查

抓到 START → 地址 → NACK → STOP,说明"没有任何从机认领这个地址"。可能性按概率排序:

序号 原因 怎么快速验证
1 地址没左移(STM32 HAL) 看解码器显示的 7 位地址是不是手册值的一半
2 地址引脚(ADDR/A0A1A2)接错 很多器件靠引脚拉高/拉低选地址,量一下那几个脚的电平
3 从机没供电 / 没共地 万用表量从机 VDD 和 GND
4 SDA/SCL 接反了 抓波形:时钟线应该是规整方波,数据线不规整
5 从机还在忙(上电时间没到) SHT30 上电后需要约 1ms 才能响应,加个 HAL_Delay(2) 试试

💡 万能第一招:先跑一次地址扫描。0x080x77 逐个发一次"写"并看有没有 ACK,把总线上真实存在的地址打印出来。有的话说明硬件通了,问题在你代码的地址;一个都扫不到,那就是硬件(供电/接线/上拉)。

c 复制代码
/* I2C 总线扫描:把总线上真实存在的设备地址打出来(STM32 HAL) */
void i2c_scan(I2C_HandleTypeDef *hi2c)
{
    printf("Scanning I2C bus...\r\n");
    for (uint8_t addr7 = 0x08; addr7 <= 0x77; addr7++) {
        /* 注意这里同样要左移;trials=1, timeout=10ms */
        if (HAL_I2C_IsDeviceReady(hi2c, (uint16_t)(addr7 << 1), 1, 10) == HAL_OK) {
            printf("  found device at 7-bit addr 0x%02X\r\n", addr7);
        }
    }
    printf("Scan done.\r\n");
}

5.4 上拉电阻:别再无脑抄 4.7k,它是能算出来的

I2C 的 SDA/SCL 是开漏输出------器件只能把线拉低,拉高全靠外部上拉电阻。所以这个电阻小了拉不动(灌电流超标),大了上升沿太慢(来不及到高电平就又被拉低了)。

下限(保证能拉到足够低的低电平): I2C 规范要求从机灌电流能力 3 mA 时 V_OL 不超过 0.4V,于是

复制代码
Rp(min) = (Vdd − V_OL) / I_OL = (3.3 − 0.4) / 3mA ≈ 967 Ω     (3.3V 系统)
                                (5.0 − 0.4) / 3mA ≈ 1533 Ω    (5V 系统)

所以 3.3V 系统别用小于 1kΩ 的上拉

上限(保证上升沿够快): NXP 的 I2C 规范(UM10204)给的公式是

复制代码
                 tr
Rp(max) = ─────────────────        (0.8473 = ln(7/3),来自 30%→70% 的 RC 充电时间)
            0.8473 × Cb

  tr 是允许的最大上升时间,规范规定:
    标准模式 100 kHz  → tr ≤ 1000 ns
    快速模式 400 kHz  → tr ≤ 300 ns
    快速模式+ 1 MHz   → tr ≤ 120 ns
  Cb 是总线电容(每个器件约 5~10pF,加上走线,面包板飞线很容易到 100~300pF)

代进去算两个真实例子:

复制代码
例 1:4.7k 上拉 + 100pF 总线电容(正常的小板子)
  tr = 0.8473 × 4700 × 100e-12 = 398 ns
  → 100 kHz 要求 ≤1000ns:✅ 合格
  → 400 kHz 要求 ≤ 300ns:❌ 超标!这就是"降到 100k 就好了"的真相

例 2:10k 上拉 + 300pF(面包板 + 长杜邦线 + 挂了好几个器件)
  tr = 0.8473 × 10000 × 300e-12 = 2.54 µs
  → 连最宽松的 100 kHz(1000ns)都超了 2.5 倍:❌ 直接违规,八成通不了

反推一下 400kHz 该用多大:
  Rp(max) = 300e-9 / (0.8473 × 100e-12) = 3541 Ω  → 选 3.3k(标称值往下取)
  再看下限 967Ω → 所以 400kHz@100pF 的合理区间是 1k ~ 3.3k

💡 经验值也给你 :3.3V/100kHz 用 4.7k;3.3V/400kHz 用 2.2k~3.3k;线长、器件多(Cb 大)就往小选。"4.7k 万能"只在 100kHz、短线、器件少的时候成立。

波形上怎么看出来上拉不合适? 这里有个诚实的提醒:

复制代码
逻辑分析仪看到的(只有 0/1):        示波器看到的(真实电压):
                                          ╭───────
  ────┐        ┌────────              ────┐      ╱
      └────────┘                          └─────╱
       ↑ 边沿看起来还是"垂直"的            ↑ 慢悠悠的 RC 斜坡

逻辑分析仪只判 0/1,看不到斜坡本身 ,它只会表现为"高电平变窄了、时序整体后移了",严重时直接判成一直是低。要量真实的 tr,必须用示波器 (测 30%→70% 或 10%→90% 的时间)。这就是文章开头说的"逻辑分析仪看内容,示波器看质量"的分界线------能解码但偶尔出错、降速就好,八成是电气问题,该上示波器了。

5.5 时钟拉伸:从机说"等我一下"

I2C 允许从机在还没准备好数据时把 SCL 摁住不放 ,主机看到 SCL 没升上来就得等。这叫时钟拉伸(clock stretching)

波形上非常好认------某一个 SCL 低电平特别长

复制代码
 SCL ──┐ ┌─┐ ┌─┐ ┌─┐ ┌───────────────────────┐ ┌─┐ ┌──
       └─┘ └─┘ └─┘ └─┘                       └─┘ └─┘
                      └── 从机摁住不放(可能几 ms)──┘
                          主机在这里必须等

SHT3x 正好可以拿来做对比实验,它的单次测量命令有两套:

命令 含义 主机该怎么写
0x2C 0x06 高重复性,启用时钟拉伸 发完命令直接读 6 字节,从机会拉住 SCL 直到测完
0x24 0x00 高重复性,不拉伸 发完命令后自己延时(高重复性约 15ms)再去读,读早了从机 NACK
c 复制代码
/* 方式 A:用时钟拉伸(0x2C06),STM32 作主机时硬件会自动等 */
uint8_t cmd_stretch[2] = {0x2C, 0x06};
HAL_I2C_Master_Transmit(&hi2c1, SHT3X_ADDR_HAL, cmd_stretch, 2, 100);
HAL_I2C_Master_Receive(&hi2c1, SHT3X_ADDR_HAL, buf, 6, 100);   /* 从机会拉住 SCL 让你等 */

/* 方式 B:不用拉伸(0x2400),自己等 */
uint8_t cmd_nostretch[2] = {0x24, 0x00};
HAL_I2C_Master_Transmit(&hi2c1, SHT3X_ADDR_HAL, cmd_nostretch, 2, 100);
HAL_Delay(20);                                                  /* 高重复性约 15ms,留点余量 */
HAL_I2C_Master_Receive(&hi2c1, SHT3X_ADDR_HAL, buf, 6, 100);

⚠️ 两个坑

软件模拟 I2C 必须自己处理拉伸 ------拉高 SCL 之后要回读 SCL 引脚确认它真的变高了 再继续(还要给个超时,别死等),否则从机还摁着你就往下发,数据全乱。用 STM32 的硬件 I2C 作主机时不用管 ,它天然会等 SCL 真正释放;HAL 里那个 NoStretchMode(NOSTRETCH 位)只在从机模式下有意义,主机模式必须保持清零I2C_NOSTRETCH_DISABLE,也是 CubeMX 默认值)。

有些主控不支持拉伸 (部分 SoC、某些 USB-I2C 转接器)。这种情况就必须用 0x2400 + 自己延时那套。选哪套,取决于你的主机能力,不是"哪个更高级"。


六、新手必踩的 10 个坑(我基本都踩过)

# 现象 / 波形上看到什么 正确做法
1 "8MHz 跑 115200 误差过大"一刀切 在 STM32 上照这句话去改时钟树,白折腾半天 分平台 :STM32 有小数分频,8MHz@115200 的 BRR=69,误差仅 +0.64% ,没问题;AVR 这类无小数分频 的,UBRR 取整后误差 +8.51% 才真的会乱码(开 U2X 也还有 −3.55%)。AVR 请换 7.3728/11.0592MHz 晶振
2 SPI 的 CPHA 搞反 能通信,但读回来的字节像是"整体左移一位"(EF 40 17 读成 DE 80 2E 看到"错位型"错误先查 CPOL/CPHA。记住 Mode 0 与 Mode 3 采样边沿等效(都是上升沿采),所以器件常常两个都支持,Mode 1/2 反而不行
3 I2C 忘接上拉电阻 SDA/SCL 一直是低或飘忽,扫描一个设备都扫不到 开漏必须外部上拉。3.3V 系统 100kHz 用 4.7k、400kHz 用 2.2~3.3k。有些模块板载已带上拉,多个模块并联会让等效阻值变小,反而要拆掉几个
4 上拉阻值凭感觉抄 4.7k 100kHz 能通,一提到 400kHz 就错 按公式算:Rp(max)=tr/(0.8473×Cb)Rp(min)=(Vdd−0.4)/3mA≈967Ω@3.3V。4.7k+100pF 的 tr=398ns,100k 合格但 400k(限 300ns)超标
5 采样率不够 波形看着"挺规整",解码却全是乱码或干脆没结果 采样率 ≥ 信号频率 ×4(建议 ×10)。FX2 便宜卡最高约 24MHz,别拿它抓 20MHz+ 的 SPI
6 触发没设对 点了运行,抓到满屏空闲电平,真正出错那一瞬间没录到 UART 用下降沿 触发(起始位);SPI 用 CS 低电平触发;I2C 用 SCL 下降沿。FX2 是软件触发,够用
7 pre-trigger 设成 0% UART 前几个字节乱码、报帧错误,后面全对 sigrok 官方明说的坑:把 pre-trigger 比例调到 1% 就好。别去怀疑 MCU 上电时钟
8 I2C 地址没左移(STM32 HAL) START → 地址 → NACK → STOP,解码显示的 7 位地址正好是手册值的一半 STM32 HAL 要左移后的 8 位值(手册 0x44 → 代码写 0x88)。Linux/Arduino/ESP-IDF 反而要 7 位原值
9 忘了接 GND / 抓 1.8V 系统 波形随机乱跳,或同一根线一会儿判 1 一会儿判 0 GND 必接被测板地;FX2 是 3.3V LVTTL 输入(判高约需 2.0V),1.8V 系统请换可调阈值设备(如 DSLogic)
10 UART 手工解码把位序读反 手动数出来的字节和解码器对不上 UART LSB 先发 ,波形从左往右读完要倒过来 ;SPI/I2C 是 MSB 先发。三者别混

一份可以贴在桌上的"通信不通"固定动作

复制代码
1. 接线:GND 共地?电平匹配?(30 秒)
2. 抓:采样率 = 速率×10,触发设对,pre-trigger 1%
3. 看物理层:线上有没有波形?空闲电平对不对?
      └─ 没波形 → 引脚复用/时钟没开/CS 没动 → 回去查 GPIO 和 RCC
4. 看协议层:加解码器,参数照代码填
      └─ 全乱   → 波特率/模式错
      └─ 半乱   → 数据位/校验/停止位错
      └─ 错位   → CPOL/CPHA 错
      └─ NACK   → 地址错 / 从机没活
5. 还不行 → 上示波器看上升沿、看电平幅度(电气问题)

这套动作跑一遍,通信类问题 90% 能定位。 剩下 10% 才是真正值得熬夜的东西。


七、动手练(重点是"故意搞坏",坏过一次你就永远记得)

看十遍不如自己抓一次。下面 5 个练习按顺序做,一个下午能全跑完。

练习 1:把 UART 波特率故意配错 5%,看它怎么烂掉

c 复制代码
/* STM32 CubeMX 生成的 MX_USART1_UART_Init() 里改这一行 */
huart1.Init.BaudRate = 120960;   /* = 115200 × 1.05,误差正好 +5.00%
                                    已经贴到 5.26% 的理论天花板,是 ±2% 工程红线的 2.5 倍 */
  1. 让 MCU 循环发 "Hello, embedded!\r\n"PC 串口助手仍然设 115200
  2. 逻辑分析仪抓 TX 引脚,采样率 1MHz,下降沿触发,pre-trigger 设 1%
  3. UART 解码器填 115200 (模拟接收方视角)→ 观察:越靠后的字节错得越离谱、时不时报帧错误,因为误差是累积的。
  4. 把解码器改成 120960 → 立刻全部正常。这一步能让你彻底明白"波特率不匹配"到底错在哪一位上。
  5. 解码器固定回 115200 不动 ,只把发送端逐步改成 118656(+3%)、117504(+2%)、116352(+1%),看从哪个点开始不再出错------你会亲手测出那条 ±2% 的工程红线。

练习 2:把 I2C 上拉电阻拆掉,看上升沿怎么塌

  1. 先在正常状态(外部 4.7k 上拉)抓一次 SHT30 通信,记下解码结果 Address write: 44 → 2C → 06 → ACK
  2. 拆掉外部上拉 ,只留 STM32 GPIO 的内部弱上拉(约 40kΩ)。按公式估一下:tr ≈ 0.8473 × 40k × 100pF ≈ 3.4 µs是 100kHz 规范上限(1000ns)的 3 倍多
  3. 再抓一次:逻辑分析仪上你会看到高电平"变窄"甚至消失、ACK 判读失败;有示波器的话切过去看,SDA 上升沿是一条明显的 RC 斜坡。
  4. 把 I2C 速率从 100kHz 降到 10kHz 再试------大概率又能通了。这就是"降速能好"背后的物理原因:位周期变长,斜坡有时间爬到高电平。
  5. 换回 4.7k,收工。

⚠️ 练习 2 只是短时间实验,别让总线长期工作在违规状态。另外别把上拉接到 5V:除非两端器件都明确 5V 容忍,否则电流会经 3.3V 器件的 ESD 保护二极管倒灌进电源,轻则跑飞重则烧片。

练习 3:把 SPI 的 CPHA 故意配反,认出"错位"特征

c 复制代码
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase    = SPI_PHASE_2EDGE;   /* 本该是 1EDGE(Mode 0),故意改成 Mode 1 */

抓 SCK/MOSI/MISO/CS 四根线,读 W25Q64 的 JEDEC ID。你会看到:波形完全正常、CS 时序也对、就是数据不是 EF 40 17 。然后在 PulseView 里把解码器的 cpha 从 0 改成 1 再解一次------同一段波形解出两个不同结果,这一刻你就真懂 CPHA 了。

练习 4:对比 I2C 时钟拉伸开/关

分别用 0x2C 0x06(拉伸)和 0x24 0x00 + HAL_Delay(20)(不拉伸)读一次 SHT30,各抓一次波形,对比 SCL 上有没有那个"特别长的低电平"。再故意把不拉伸那版的延时改成 HAL_Delay(2)(不够 15ms),看从机怎么用 NACK 拒绝你。

练习 5:验证 pre-trigger 那个坑

同一段 UART 通信,分别用 pre-trigger 0%1% 各抓一次,对比开头几个字节。亲眼看过一次,以后你就不会再冤枉 MCU 了。


八、小结

你想查什么 用什么 关键动作
这串 0/1 到底是什么字节 逻辑分析仪 + 协议解码器 采样率 ≥ 速率×10,触发 + pre-trigger 1%
波特率对不对 逻辑分析仪量最窄脉冲 波特率 = 1 / 最窄脉冲宽度;工程红线 ±2%
SPI 数据错位 逻辑分析仪,同一段波形换 cpol/cpha 重解 Mode 0 / Mode 3 采样边沿等效
I2C 没应答 先跑地址扫描,再看解码的 7 位地址 STM32 HAL 要左移;PulseView 默认显示 7 位
上升沿慢不慢、电平够不够 示波器(逻辑分析仪看不到斜坡) Rp(max)=tr/(0.8473×Cb)Rp(min)≈967Ω@3.3V

一句话收尾:调通信别再靠改代码碰运气。花几十块买个逻辑分析仪,把"猜"换成"看",这大概是嵌入式路上性价比最高的一笔投资。

文中的上拉电阻公式、波特率误差、各类芯片的时序参数都来自对应 Datasheet 和官方规范(NXP I2C、Winbond、Sensirion 等),选型时以你手上的版本为准。

下一篇:调试排错_03_栈溢出与内存踩踏定位

相关推荐
地衣君1 小时前
从 profile 到 kanban:Hermes 多 Agent 协作解析及示例
网络·数据库·tcp/ip
LCG元1 小时前
STM32H743串口DMA空闲中断不定长接收与D-Cache一致性踩坑实录
stm32·单片机·嵌入式硬件
LCG元2 小时前
STM32内部温度传感器供电漂移8℃的根因定位与VREFINT比例校准实测
stm32·单片机·嵌入式硬件
hoaxxcj12 小时前
零能力 Agent 骗过评测实测:朴素 harness 被骗 75%,加固后仍漏掉「抄答案」
网络·安全·大模型·工程化·ai实战
猫咪宝妖13 小时前
【信息安全工程师】网络与信息安全理论
网络·安全·web安全
Fnetlink116 小时前
FNET 云网安 260819
网络·人工智能·安全·网络安全
lsh曙光17 小时前
playbook剧本(2)--Ansible变量
linux·运维·网络
我是一棵无人问荆的小草17 小时前
stm32f103单片机的NVIC分组2介绍
stm32·单片机·嵌入式硬件
发光小北17 小时前
IO‑Link 从站转接模块如何应用?
网络