---
title: FPGA通信双雄：手把手教你玩转I2C与SPI的Verilog实现
url: "https://www.fpgalearn.com/28302.html"
kind: article
id: 28302
excerpt: 嗨，各位FPGA探索者！今天我们来聊聊项目开发中几乎绕不开的两个“老朋友”——I2C和SPI。你是不是也经常需要让FPGA跟各种传感器、存储芯片或者显示屏“对话…
published_at: 2026-04-05
author: FPGA小白
tags: fpga, I2C, SPI
word_count: 1769
source: "https://www.fpgalearn.com"
---

# FPGA通信双雄：手把手教你玩转I2C与SPI的Verilog实现

> 标签：fpga、I2C、SPI
> 嗨，各位FPGA探索者！今天我们来聊聊项目开发中几乎绕不开的两个“老朋友”——I2C和SPI。你是不是也经常需要让FPGA跟各种传感器、存储芯片或者显示屏“对话…

> 嗨，各位FPGA探索者！今天我们来聊聊项目开发中几乎绕不开的两个“老朋友”——I2C和SPI。你是不是也经常需要让FPGA跟各种传感器、存储芯片或者显示屏“对话…

嗨，各位FPGA探索者！今天我们来聊聊项目开发中几乎绕不开的两个“老朋友”——I2C和SPI。你是不是也经常需要让FPGA跟各种传感器、存储芯片或者显示屏“对话”？没错，I2C和SPI就是实现这种板级“悄悄话”最经典、最常用的两种串行通信协议。掌握它们，就像拿到了打开外部世界大门的钥匙，是从爱好者迈向专业工程师的必经之路。
这篇文章，我们就一起把这两个协议掰开揉碎，从核心原理到Verilog代码实现，再到仿真验证，给你讲得明明白白。准备好了吗？我们开始吧！
## 先看本质：I2C和SPI，到底有啥不一样？
在动手写代码之前，咱们得先搞清楚这俩兄弟各自的脾气。简单来说，I2C像是个“节约派”，用最少的线办最多的事；而SPI则是个“速度派”，追求极致的通信效率。下面这个表格帮你快速抓住重点：
| 特性 | I2C | SPI |
| --- | --- | --- |
| **线路数量** | 2根线搞定（SCL时钟线，SDA数据线） | 通常4根线（SCLK, MOSI, MISO, SS片选），也可以精简到3根 |
| **聊天方式** | 半双工，一根数据线来回用（像对讲机） | 全双工，可以同时发和收（像打电话） |
| **连接方式** | “总线式”，一根线上可以挂很多设备，靠地址找人 | “点对点式”，一个主人（Master）带多个小弟（Slave），靠片选信号点名 |
| **速度** | 标准模式100kbps，快速模式400kbps等 | 通常快得多，轻松跑到几十Mbps |
| **协议复杂度** | 稍高，有起始、停止、应答等“礼节” | 相对简单，主要关心时钟极性和相位 |
| **FPGA实现关键** | 状态机设计、处理双向IO（三态门）是核心 | 时钟域处理、管理多个从设备的片选信号是关键 |
## 动手实战：I2C Master的Verilog实现要点
在FPGA里，我们通常扮演“主人”（Master）的角色，去控制外部的“客人”（Slave设备）。实现I2C Master的核心，就是设计一个靠谱的**有限状态机（FSM）**。
**状态机怎么设计？**
想象一下一次完整的I2C通信流程：空闲 -> 发出开始信号 -> 发送设备地址 -> 等待对方应答 -> 发送或接收数据 -> 发出停止信号。我们的状态机就要严格遵循这个剧本，典型状态包括：`IDLE`, `START`, `SEND_ADDR`, `CHECK_ACK`, `SEND_DATA`, `RECV_DATA`, `STOP` 等。
**难点攻克：双向SDA线怎么处理？**
这是I2C Verilog实现中最容易卡壳的地方。SDA这根线很“忙”，它有时候要输出数据，有时候又要读取从机发来的数据。怎么办呢？标准做法是使用“三态门”控制：
- 定义两个寄存器：`sda_out`（要输出的数据）和 `sda_oe`（输出使能信号）。
- 在顶层模块，用一行条件赋值实现三态门：
`assign SDA = (sda_oe == 1'b1) ? sda_out : 1'bz;` // oe为1时输出数据，为0时呈高阻态（相当于断开，让从机控制）
- 再定义一个信号来读取SDA线上的实际电平：
`assign sda_in = SDA;`
**时钟SCL的生成**
SCL时钟完全由主机产生。通常是从系统主时钟分频得到。记住黄金法则：**在SCL为高电平期间，SDA上的数据必须保持稳定**；数据的变化只能发生在SCL为低电平的时候。这样才能满足建立和保持时间的要求。
**来看一段状态机的核心代码片段（感受一下节奏）：**
```
always @(posedge clk) begin
    case(state)
        START: begin
            if (clk_cnt == DIVIDER) begin
                sda_oe <= 1'b1; // 使能输出
                sda_out <= 1'b0; // 拉低SDA，产生起始条件
                // ... 状态转移和其他逻辑
            end
        end
        // ... 其他状态
    endcase
end
```
看到这里，你对I2C的实现是不是有了更具体的画面？别急，关于SPI的实现精要、仿真验证的“组合拳”怎么打，我们下回接着聊。记住，理解协议是骨架，写出健壮的代码是血肉，而充分的验证则是给你的设计穿上盔甲。一步步来，你一定能搞定！
