FPGA 进阶 09:低速接口——UART、SPI、I2C 协议与 FPGA 实现
本文是
fpga/进阶层第 13 篇。高速接口地图 讲了 PCIe/DDR/以太网这些「大件」,但 FPGA 板上每天打交道最多的是三类低速接口:UART(串口)、SPI、I2C——接调试串口、ADC/DAC、Flash、传感器、配置芯片全靠它们。这篇讲清协议时序和 FPGA 里的实现骨架。
本文要回答的问题
- UART 异步收发,没有共享时钟怎么对齐?为什么要过采样?
- SPI 的 CPOL/CPHA 四种模式差在哪,搞反会怎样?
- I2C 为什么必须开漏+上拉?地址/ACK/起停条件怎么实现?
- 三者速度/线数/用途怎么权衡、选哪个?
一、三者速览
| UART | SPI | I2C | |
|---|---|---|---|
| 线数 | 2(TX/RX)+地 | 4(SCLK/MOSI/MISO/CS)+地 | 2(SDA/SCL)+地(带上拉) |
| 同步方式 | 异步(约定波特率) | 同步(SCLK) | 同步(SCL) |
| 双工 | 全双工 | 全双工 | 半双工 |
| 驱动 | 推挽 | 推挽 | 开漏 + 上拉 |
| 多设备 | 点对点 | 一主多从(CS 选片) | 多主多从(地址) |
| 典型速率 | 9600~921600 bps | 几~几十 MHz | 100k/400k/1M |
| 典型用途 | 调试日志、模块通信 | ADC/Flash/屏/传感器 | 配置慢设备、EEPROM |
二、UART:异步串口
帧格式:空闲为高,一次传输 起始位(低)→ 数据位(常 8,LSB 先发)→ 可选校验位 → 停止位(高)。
空闲 起始 D0 D1 D2 ... D7 (校验) 停止 空闲
──┐ ┌──┐
└────┘ └──... 每位宽 = 1/波特率关键是波特率发生器 + 过采样。没有共享时钟,靠「16x 过采样 + 中点采样」对齐:
// 16x 采样 tick:每 (F_CLK / baud / 16) 个系统周期一个 tick
localparam DIV = F_CLK / (BAUD * 16);
reg [15:0] tick_cnt;
wire tick_16 = (tick_cnt == DIV-1);
always @(posedge clk) tick_cnt <= tick_16 ? 0 : tick_cnt + 1;
// 接收:检测起始位下降沿后,在每位中点(数 8 个 tick)采样
// 简化思路:状态机 WAIT_START → 收到起始 → 每位第 8 tick 采一次、采 8 位 → 校验/停止要点:
- 起始位下降沿启动,之后每位数到第 8 个 16x tick(位中点)采样,避开跳变沿;
- 连续几次读到相同电平再确认(多数表决)滤毛刺;
- 发送端用同一波特率,按帧把并行数据串行移出,起始位拉低、停止位拉高;
- 波特率分频系数 =
F_CLK /(波特率 × 过采样倍数),分频比取整误差过大会在高位数累积漂移。
三、SPI:同步全双工主从
主设备产生 SCLK、用 CS 选从设备,MOSI 主出从入、MISO 主入从出,每个 SCLK 沿移出一位、对端在另一沿采样,全双工同时收发。
CPOL/CPHA 四模式:
| 模式 | CPOL(空闲电平) | CPHA(采样沿) | 典型 |
|---|---|---|---|
| 0 | 0(空闲低) | 0:第一沿(上升)采样、第二沿移出 | 最常用 |
| 1 | 0 | 1:第一沿移出、第二沿(下降)采样 | |
| 2 | 1(空闲高) | 0 | |
| 3 | 1 | 1 | 与模式 0 同为上升沿采样、但空闲高 |
实现:一个移位寄存器 + 位计数器 + SCLK 分频/生成状态机,按所选模式决定「哪个沿更新、哪个沿采样」:
// 模式 0 主端发送(上升沿从机采样 MOSI,下降沿主机采 MISO),简化骨架
// 位计数 0..7;按 SCLK 半周期翻转 sclk;
// 沿动作:sclk 上升沿前更新 mosi,下降沿锁存 miso → 移位寄存器三大坑:① CPOL/CPHA 和从机手册不一致(采到错位数据);② 采样沿/移出沿弄反;③ CS 时序/字节间隔。调试用 ILA 同时抓 SCLK/MOSI/MISO/CS 对照手册时序。
四、I2C:两线开漏、地址寻址
SDA(数据)、SCL(时钟)都是开漏 + 外部上拉,线与逻辑。一次传输:主机发 START(SCL 高时 SDA 由高到低) → 发 7 位地址 + 1 位 R/W → 从机回 ACK(拉低 SDA) → 数据字节(每 8 位一个 ACK)→ STOP(SCL 高时 SDA 由低到高)。
FPGA 必须用三态开漏驱动 SDA/SCL:
// 开漏:drv=1 释放(高阻,由上拉拉高),drv=0 拉低
assign sda = sda_drv ? 1'bz : 1'b0;
assign scl = scl_drv ? 1'bz : 1'b0;
// 输入方向读回线电平
always @(posedge clk) sda_in <= sda; // 经同步器去 metastable状态机骨架:IDLE → START → 发地址位(8) → 读 ACK → 发/收数据字节 → ACK → ... → STOP。SCL 由主机分频产生(100k/400k),数据在 SCL 低时改变、高时稳定被采样。
五、上板调试要点
| 现象 | 可能原因 | 排查 |
|---|---|---|
| UART 收到乱码 | 波特率/分频错、时钟频率不对 | 核对 F_CLK 与分频比;先回环测试;16x 中点采样 |
| UART 能收但丢字节 | 没流控、FIFO 满 | 收侧加 FIFO 缓冲、过采样滤波 |
| SPI 数据错位/全 FF | CPOL/CPHA 反、采样沿错 | 按从机手册设模式,ILA 抓四线对照 |
| SPI 偶发错 | 建立时间/速度过快 | 降 SCLK、加约束 input/output delay |
| I2C 总线卡死/无 ACK | 没开漏、缺上拉、地址错 | 三态开漏 + 上拉电阻;核对 7 位地址 |
| I2C 多主冲突 | 未读回仲裁 | 发 1 读 0 检测仲裁丢失 |
通用手段:先用 ILA 抓物理线电平对照数据手册时序图;低速接口在 约束实战 里给 set_input_delay/output_delay(尤其 SPI 跑上 MHz 时),纯慢速配置可放宽但不能完全不约束。
常见坑对照
| 坑 | 后果 | 正确做法 |
|---|---|---|
| UART 不做过采样 | 采样漂移、乱码 | 16x 过采样 + 中点采样 + 多数表决 |
| 波特率分频算错 | 全乱码 | DIV=F_CLK/(波特率×16),和晶振三处对齐 |
| SPI CPOL/CPHA 反 | 数据错位/全 FF | 按从机手册模式,抓波形核对沿 |
| I2C 推挽驱动 | 短路/仲裁坏 | 三态开漏 1'bz + 外部上拉 |
| 不读 ACK | 误以为成功 | 每字节读回 SDA 判 ACK |
| SPI/I2C 不约束 IO | 高速时不稳 | input/output delay + 时钟约束 |
相关与延伸
后续:AXI4-Stream 与 AXI4-Lite(片内/片上数据与寄存器接口);高速接口见 高速接口地图。 接口协议不通的典型坑汇总见规划 #44 接口协议调试案例;调试用 调试技巧 ILA/VIO。
一句话总结
三类低速接口各有定位:UART 是异步点对点串口,靠约定波特率、用 16x 过采样 + 位中点采样 + 多数表决对齐(分频比 = 时钟/波特率/过采样倍数,算错就乱码),适合调试日志;SPI 是同步全双工四线(SCLK/MOSI/MISO/CS),关键是按从机手册设对 CPOL 空闲电平和 CPHA 采样沿四种模式,沿搞反就数据错位,用 ILA 抓四线对照时序图;I2C 是两线半双工、靠 START/STOP + 7 位地址 + ACK 寻址多设备,SDA/SCL 必须三态开漏(1'bz 释放)+ 外部上拉电阻(线与、仲裁、ACK 都靠它),忘记开漏或上拉是总线卡死首因;速度/线数上 SPI 快而简单(ADC/Flash/屏)、I2C 省线多设备(配置慢器件)、UART 异步调试最方便;上板前给 SPI/I2C 高速 IO 补 set_input/output_delay,抓物理波形对照数据手册是排查铁律。