1. IIC协议基础与FPGA实现概述
IIC(Inter-Integrated Circuit)总线作为嵌入式系统中最常见的低速串行通信协议之一,在传感器控制、EEPROM读写等场景中应用广泛。作为一名FPGA开发者,理解IIC协议的底层时序逻辑并实现可靠的控制器是必备技能。与常见的UART协议不同,IIC采用双向开漏结构和时钟同步机制,这使得其在多设备共享总线时具有独特优势,但也带来了时序设计的复杂性。
在实际项目中,我们常遇到需要自定义IIC控制器的情况——可能是由于特定性能需求,或是需要与非常规设备通信。基于FPGA的硬件实现相比软件模拟(如ARM的GPIO模拟IIC)具有更精确的时序控制和更高的吞吐量。本文将详细解析IIC协议的读写时序设计要点,并给出经过实际验证的Verilog实现方案,其中包含多个在官方文档中不会提及的实战技巧。
2. IIC协议核心时序解析
2.1 基础信号组成
IIC总线由两条信号线构成:
- SCL(Serial Clock):时钟线,由主设备控制
- SDA(Serial Data):数据线,双向传输
这两个信号都采用开漏输出结构,需要通过上拉电阻保持高电平。这种设计实现了"线与"逻辑,允许多个设备共享总线而不产生冲突。在实际电路设计中,上拉电阻的取值需要根据总线电容和通信速度计算,通常范围在1kΩ到10kΩ之间。
关键提示:上拉电阻过小会导致功耗增加,过大则可能使上升沿过缓导致时序违规。对于400kHz的标准模式,建议使用4.7kΩ电阻进行测试。
2.2 起始与停止条件
起始(START)和停止(STOP)条件是IIC总线状态转换的关键标志:
- 起始条件:当SCL为高时,SDA从高向低跳变
- 停止条件:当SCL为高时,SDA从低向高跳变
在Verilog实现中,我们需要专门的状态机来检测和生成这些条件。以下是起始条件检测的典型代码片段:
verilog复制// 起始条件检测
always @(negedge SDA) begin
if (SCL == 1'b1) begin
start_detected <= 1'b1;
// 其他状态初始化操作...
end
end
2.3 数据有效性规则
IIC协议规定数据在SCL高电平期间必须保持稳定,只有在SCL为低时才能改变数据线状态。这一特性使得接收方可以在时钟上升沿采样数据。在FPGA实现时,我们需要特别注意:
- 数据变化必须发生在SCL低电平期间
- 数据变化到下一个SCL上升沿需要满足建立时间(t_SU:DAT)
- SCL高电平期间必须保持数据稳定
3. IIC写时序设计与实现
3.1 单字节写操作流程
标准IIC写操作包含以下阶段:
- 主设备发送START条件
- 发送7位从设备地址 + 1位写标志(0)
- 等待从设备应答(ACK)
- 发送8位数据
- 等待从设备应答
- 发送STOP条件
在FPGA实现中,我们需要设计精确的状态机来控制这一流程。以下是状态机的典型状态定义:
verilog复制localparam [3:0]
IDLE = 4'b0000,
START = 4'b0001,
ADDR = 4'b0010,
WAIT_ACK1 = 4'b0011,
DATA = 4'b0100,
WAIT_ACK2 = 4'b0101,
STOP = 4'b0110;
3.2 连续写操作实现
连续写操作允许主设备在一次通信中发送多个数据字节,无需重复发送地址信息。其与单字节写的区别在于:
- 每字节传输后从设备都会应答
- 主设备可以持续发送数据直到发送STOP条件
- 从设备内部地址通常会自动递增
在FPGA设计中,连续写需要特别注意总线占用时间。长时间占用总线可能导致其他主设备无法访问,因此建议:
- 单次连续写不超过16字节
- 在字节之间插入微小延迟(约5μs)
- 实现超时机制防止总线死锁
3.3 SCL时钟生成技巧
SCL时钟的稳定性直接影响通信可靠性。在FPGA中生成SCL时钟时,推荐采用以下方法:
- 使用系统时钟分频产生SCL基准频率
- 通过移位寄存器产生相位差信号
- 添加可编程预分频器支持不同速度模式
以下是100kHz SCL时钟生成的示例代码:
verilog复制// 假设系统时钟为50MHz
reg [8:0] clk_divider = 0;
reg scl_out = 1'b1;
always @(posedge clk_50m) begin
if (clk_divider == 249) begin // 50MHz/(250*2) = 100kHz
clk_divider <= 0;
scl_out <= ~scl_out;
end else begin
clk_divider <= clk_divider + 1;
end
end
4. IIC读时序设计与实现
4.1 单字节读操作流程
读操作比写操作更复杂,因为涉及主从设备对SDA线的控制权切换:
- 主设备发送START条件
- 发送7位从设备地址 + 1位读标志(1)
- 等待从设备应答
- 接收8位数据
- 主设备发送非应答(NACK)表示读取结束
- 发送STOP条件
在读操作中,主设备需要在第9个时钟周期控制SDA线发送ACK/NACK信号。这一转换时机非常关键,过早会干扰从设备数据输出,过晚可能导致时序违规。
4.2 连续读操作实现
连续读操作时,主设备在接收每个字节后发送ACK信号(除了最后一个字节发送NACK)。这要求状态机能够:
- 在字节边界正确切换SDA方向
- 精确控制ACK/NACK信号的产生时机
- 处理从设备可能的数据延迟
以下是处理方向切换的推荐方法:
verilog复制// SDA方向控制逻辑
assign SDA = (sda_dir) ? sda_out : 1'bz;
always @(*) begin
case(state)
IDLE: sda_dir = 1'b1; // 主设备控制
ADDR: sda_dir = 1'b1;
WAIT_ACK1: sda_dir = 1'b0; // 从设备控制
DATA: begin
if (bit_cnt == 8)
sda_dir = 1'b1; // 主设备发送ACK/NACK
else
sda_dir = 1'b0; // 从设备发送数据
end
// 其他状态...
endcase
end
5. 实战问题排查与性能优化
5.1 常见问题及解决方案
在实际项目中,我们常遇到以下典型问题:
-
无应答(NACK)问题:
- 检查从设备地址是否正确(包括7位/8位格式)
- 确认从设备电源和复位状态
- 测量总线电压是否达到逻辑高电平阈值
-
数据错位问题:
- 检查SCL/SDA相位关系是否符合规范
- 验证建立时间和保持时间是否满足从设备要求
- 检查是否有其他设备意外干扰总线
-
总线死锁问题:
- 实现超时复位机制(典型值:50ms)
- 在状态机中添加错误恢复路径
- 添加总线监控逻辑检测异常状态
5.2 性能优化技巧
- 时钟拉伸处理:
某些从设备(如某些EEPROM)会在特定操作时拉伸SCL时钟。我们的FPGA设计需要检测和处理这种情况:
verilog复制// 时钟拉伸检测
reg scl_stretched;
always @(negedge SCL) begin
if (SCL == 1'b0 && scl_out == 1'b1) begin
scl_stretched <= 1'b1;
end
end
// 在状态机中等待拉伸结束
WAIT_STRETCH: begin
if (!scl_stretched) next_state = NEXT_STATE;
end
-
多主设备仲裁:
当系统中有多个主设备时,需要实现仲裁逻辑。关键点是:- 持续监控总线状态
- 检测到冲突时立即释放总线
- 实现随机退避算法避免重复冲突
-
时序参数化设计:
将关键时序参数(如建立时间、保持时间)设计为可配置参数,便于适配不同从设备:
verilog复制parameter tSU_STA = 300; // 起始条件建立时间(ns)
parameter tHD_STA = 300; // 起始条件保持时间(ns)
parameter tLOW = 1300; // SCL低电平时间(ns)
parameter tHIGH = 600; // SCL高电平时间(ns)
6. 完整状态机设计示例
以下是经过实际验证的IIC控制器状态机核心部分:
verilog复制module iic_controller (
input wire clk,
input wire rst_n,
// IIC接口
output reg SCL,
inout wire SDA,
// 用户接口
input wire start,
input wire [6:0] dev_addr,
input wire [7:0] reg_addr,
input wire [7:0] data_in,
output reg [7:0] data_out,
output reg busy,
output reg done,
output reg error
);
// 状态定义
localparam [3:0]
IDLE = 4'b0000,
START = 4'b0001,
ADDR = 4'b0010,
WAIT_ACK1 = 4'b0011,
REG_ADDR = 4'b0100,
WAIT_ACK2 = 4'b0101,
WR_DATA = 4'b0110,
WAIT_ACK3 = 4'b0111,
STOP = 4'b1000;
reg [3:0] state, next_state;
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
reg sda_out, sda_dir;
// 状态转移逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
end else begin
state <= next_state;
end
end
// 状态机核心
always @(*) begin
case(state)
IDLE: begin
if (start) next_state = START;
else next_state = IDLE;
end
START: next_state = ADDR;
ADDR: begin
if (bit_cnt == 7) next_state = WAIT_ACK1;
else next_state = ADDR;
end
// 其他状态转移...
default: next_state = IDLE;
endcase
end
// 其他实现细节...
endmodule
在实现完整IIC控制器时,还需要注意以下几点:
- 添加详细的注释说明每个状态的功能
- 实现完整的错误检测和恢复机制
- 提供充分的仿真测试用例
- 在实际硬件上使用逻辑分析仪验证时序
通过本文介绍的这些技术和技巧,我们可以在FPGA上实现稳定可靠的IIC控制器,满足各种嵌入式应用的需求。在实际项目中,建议先用逻辑分析仪捕获标准设备的通信波形,然后基于这些参考波形来调试自己的实现,这是最有效的开发方法。
