1、写在前面
IIC协议系列博文:
FPGA实现IIC协议(一)—-初识IIC总线
FPGA实现IIC协议(二)—-IIC总线的FPGA实现(单次读写驱动)
上一篇文章已经对IIC总线做了详细的介绍,了解了IIC总线的读写方式。这篇文章我们编写一个基于FPGA的IIC驱动模块,并对这个模块进行仿真及上板验证。
2、单次读写时序
首先来回顾一下IIC总线单次读写时序。
单次写时序如下:


单次读时序如下:


大致总结一下单次写时序的过程(假设从机均正确响应,若响应不正确或不响应则跳转到初始状态重新开始写操作):
- 发送起始信号,一次传输开始
- 发送器件地址+低电平(表示写),等待从机正确回应
- 发送寄存器地址(16位或者8位地址),等待从机正确回应
- 发送8位数据写入从机,等待从机正确回应
- 发送停止信号,终结本次传输
单次读时序的过程(假设从机均正确响应,若响应不正确或不响应则跳转到初始状态重新开始读操作):
- 发送起始信号,一次写传输开始
- 发送器件地址+低电平(表示写,这次写操作为虚写,真正目的是将地址指针指向需要读取的地址),等待从机正确回应
- 发送寄存器地址(16位或者8位地址),等待从机正确回应
- 再次发送起始信号,一次读传输开始
- 发送器件地址+高电平(表示读,这次读操作才是真正的读操作),等待从机正确回应
- 接收总线上由从机发送的8位数据,发送非响应信号给从机,表示不需要再接收数据
- 发送停止信号,终结本次传输
3、状态机
本文的目的是设计一个IIC的驱动模块,包含单次读、单次写,也兼容16位寄存器与8位寄存器。据次及上述时序图,可以绘制出IIC单次读写驱动的状态机如下:

上图状态机将单字节写操作和随机读操作相结合,可以实 现 I2C 设备单次写操作和单次随机读操作的状态跳转。需要说明的是状态ACK1、ACK2、ACK3、ACK4、ACK5还在主机没有正确接收到从机的响应信号时跳转回初始状态。
对各个状态、状态跳转条件、输出进行说明(假设主机为FPGA):
- 上电后处于 IDLE(初始状态),主机接收到有效的单字节数据读/写开始信号后,跳转到 START1(起始状态),在该状态主机发送起始信号;
- 随后跳转到 SEND_D_ADDR_W(发送器件地址状态+写标志),在此状态下主机发送控制指令,控制指令高7位为器件地址,最低位为读写控制字,写入“0”,表示执行写操作;然后跳转到 ACK1(应答状态)。
- 在 ACK1状态下,根据从机寄存器地址字节数进行不同状态的跳转。
- 当主机正确接收到应答信号且从机寄存器为16位 , 状态机跳转到SEND_R_ADDR_H(发送高字节地址状态),将寄存器地址的高 8 位写入从机,然后状态机跳转到 ACK2(应答状态);主机正确接收到应答信号后,跳转到 SEND_R_ADDR_L(发送低字节地址状态);
- 当主机正确接收到应答信号且从机寄存器为8位,状态机状态机直接跳转到 SEND_R_ADDR_L(发送低字节地址状态);将寄存器地址的低8位写入从机完成后,跳转到 ACK3(应答 状态)。
- 在 ACK3状态下,要根据读写使能信号做不同的状态跳转(判断此次操作为写操作还是读操作,1–读;0–写)。
- 当主机正确接收到从机的应答信号且读写使能信号为低,状态机跳转到WR_DATA(写数据状态);
- 在WR_DATA状态, 向从机写入单字节数据后,跳转到 ACK4(应答状态);
- 在 ACK4状态下,当主机正确接收到从机的应答信号后,跳转到 STOP(停止状态);
- 当主机正确接收到从机的应答信号且读写使能信号为高, 状态机跳转到 START_2(起始状态);
- START_2(起始状态);主机再次发送起始信号,状态跳转到 SEND_D_ADDR_R(发送器件地址状态+读标志);
- 主机再次发送控制字节,高 7 位器件地址不变,读写控制位写入“1”,表示进行读操作,控制字节写入完毕后,状态机跳转到 ACK5(应答状态);
- 当主机正确接收到应答信号,状态机跳转到 RD_DATA(读数据状态);
- 在 RD_DATA(读数据状态)状态,主机分8次读取总线上的数据,待数据读取完成后跳转到 NACK(无应答状态),在此状态下向从机写入一个时钟的高电平,表示数据读取完成,随后状态机跳转到 STOP(停止状态)。
- 当主机正确接收到从机的应答信号且读写使能信号为低,状态机跳转到WR_DATA(写数据状态);
- 在 STOP(停止状态)状态,FPGA 向 EEPROM 发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回 IDLE(初始状态),等待下一次单字节数据读/写开始信号。
4、接口定义与整体设计
Verilog编写的IIC驱动的整体框图、输入输出信号如下所示:

其中信号描述如下表:
| 属性 | 名称 | 位宽 | 说明 | |
| 系统接口 | 输入 | sys_clk | 1 | 输入系统时钟,50MHz |
| sys_rst_n | 1 | 输入复位信号,低电平有效 | ||
| I2C时序控制接口 | i2c_rw | 1 | 读写使能信号—-1:读;0:写 | |
| i2c_start | 1 | i2c开始信号 | ||
| i2c_num | 1 | i2c字节地址字节数—-1:16位;0:8位 | ||
| i2c_addr | 16 | i2c字节地址 | ||
| i2c_data_w | 8 | 写入i2c数据 | ||
| i2c_clk | 1 | i2c驱动时钟 | ||
| 输出 | i2c_end | 1 | i2c一次读/写操作完成 | |
| i2c_data_r | 8 | i2c读取数据 | ||
| I2C物理接口 | scl | 1 | 输出至i2c设备的串行时钟信号scl | |
| 双向 | sda | 1 | 输出至i2c设备的串行数据信号sda |
需要注意的是:i2c_clk是本驱动模块的工作时钟,由系统时钟 sys_clk 分频而来,它的时钟频率为串行时钟scl频率的 4 倍。为什么是4倍呢,不妨看一下下图的IIC基础时序:

在SCL为高电平器件,要求SDA上的数据保持稳定;在SCL为低电平器件,允许SDA上的数据变化。我们知道驱动模块对IIC总线上的数据进行读写肯定是需要一个驱动时钟的,根据上图就可以看到当驱动时钟是SCL的4倍频率时,操作起来是最方便的。如下图:

5、Verilog代码
根据上述状态机描述、整体设计不难编写出驱动模块的Verilog代码(注释很详细):
这里主要讲几点需要注意的和大概思路:
- 1、首先三段式状态机肯定是跑不了的,需要结合上面的状态转移图理解(FPGA状态机(一段式、二段式、三段式)、摩尔型(Moore)和米勒型(Mealy))
- 2、SDA数据线是双向接口,需要使用处理双向接口的方法来处理(如何规范地使用双向(inout)信号?)
- 3、需要一个寄存器来标记写入(或读取)数据的个数
- 4、驱动时钟i2c_clk是SCL的4倍频,声明一个计数器来进行分频,这个计数器同时还能很方便的找到SCL高、低电平的中间(数据最稳定的时候)
module i2c_drive #( parameter DEVICE_ADDR = 7'b1010_000 , //i2c从机地址 parameter SYS_CLK_FREQ = 26'd50_000_000 , //系统时钟频率 parameter I2C_FREQ = 18'd250_000 //i2c时钟频率,250k ) ( //系统接口 input sys_clk , //输入系统时钟,50MHz input sys_rst_n , //输入复位信号,低电平有效 //I2C时序控制接口 input i2c_rw , //读写使能信号----1:读;0:写 input i2c_start , //i2c开始信号 input i2c_num , //i2c字节地址字节数----1:16位;0:8位 input [15:0] i2c_addr , //i2c字节地址 input [7:0] i2c_data_w , //写入i2c数据 output reg i2c_clk , //i2c驱动时钟 output reg i2c_end , //i2c一次读/写操作完成 output reg [7:0] i2c_data_r , //i2c读取数据 //I2C物理接口 output reg scl , //输出至i2c设备的串行时钟信号scl inout wire sda //输出至i2c设备的串行数据信号sda ); //状态机定义 localparam IDLE = 4'd0, //初始化状态 START1 = 4'd1, //发送开始信号状态1 SEND_D_ADDR_W = 4'd2, //设备地址写入状态 + 控制写 ACK1 = 4'd3, //等待从机响应信号1 SEND_R_ADDR_H = 4'd4, //发送寄存器地址高8位 ACK2 = 4'd5, //等待从机响应信号2 SEND_R_ADDR_L = 4'd6, //发送寄存器地址低8位 ACK3 = 4'd7, //等待从机响应信号3 WR_DATA = 4'd08, //写数据状态 ACK4 = 4'd09, //应答状态4 START2 = 4'd10, //发送开始信号状态12 SEND_D_ADDR_R = 4'd11, //设备地址写入状态 + 控制读 ACK5 = 4'd12, //应答状态5 RD_DATA = 4'd13, //读数据状态 NACK = 4'd14, //非应答状态 STOP = 4'd15; //结束状态 //根据系统频率及IIC驱动频率计算分频系数 localparam CLK_DIVIDE = SYS_CLK_FREQ / I2C_FREQ >> 2'd3; //reg定义 reg [9:0] clk_cnt ; //分频时钟计数器,最大计数1023 reg [3:0] cur_state ; //状态机现态 reg [3:0] next_state ; //状态机次态 reg i2c_clk_cnt_en ; //驱动时钟计数使能 reg [1:0] i2c_clk_cnt ; //驱动计数时钟,方便在SCL的高电平中间采集数据;和在SCL的低电平中间变化数据 reg sda_out ; //IIC总线三态输出 reg sda_en ; //IIC总线三态门使能 reg [2:0] bit_cnt ; //接收数据个数计数器 reg ack_flag ; //应答信号标志 reg [7:0] i2c_data_r_temp ; //读取数据寄存器,暂存读到的数据 //wire定义 wire sda_in ; //IIC总线三态输入 wire [7:0] addr_r ; //器件地址+读控制位 wire [7:0] addr_w ; //器件地址+写控制位 assign addr_r = {DEVICE_ADDR,1'b1}; //器件地址+读控制位 assign addr_w = {DEVICE_ADDR,1'b0}; //器件地址+写控制位 //双向口处理 assign sda_in = sda; assign sda = sda_en ? sda_out : 1'bz; //scl4分频时钟=IIC驱动时钟i2c_clk,方便操作对采集数据及变化数据操作 always@(posedge sys_clk or negedge sys_rst_n)begin if(~sys_rst_n)begin i2c_clk <= 1'b0; clk_cnt <= 10'd0; end else if(clk_cnt == CLK_DIVIDE - 1'b1)begin i2c_clk <= ~i2c_clk; clk_cnt <= 10'd0; end else begin i2c_clk <= i2c_clk; clk_cnt <= clk_cnt + 1'd1; end end //i2c_clk计数器使能 always@(posedge i2c_clk or negedge sys_rst_n)begin if(!sys_rst_n) i2c_clk_cnt_en <= 1'b0; //只有在发送完了结束信号或者没有接收到IIC开始传输信号的初始状态下才不停对i2c_clk计数器复位(使能为0) else if ((cur_state == STOP && i2c_clk_cnt == 2'd3 && bit_cnt == 2'd3)||(cur_state == IDLE && !i2c_start )) i2c_clk_cnt_en <= 1'b0; else if(i2c_start) i2c_clk_cnt_en <= 1'b1; //接收到开始信号,代表一次传输开始,计数器开始计数 else i2c_clk_cnt_en <= i2c_clk_cnt_en; //其他时候保持不变 end //i2c_clk_cnt计数器 always@(posedge i2c_clk or negedge sys_rst_n)begin if(!sys_rst_n) i2c_clk_cnt <= 2'd0; else if(i2c_clk_cnt_en) i2c_clk_cnt <= i2c_clk_cnt + 1'd1; //使能信号有效,计数器开始计数 else i2c_clk_cnt <= 2'd0; //使能信号无效,计数器清零 end //三段式状态机第一段 always@(posedge i2c_clk or negedge sys_rst_n)begin if(~sys_rst_n) cur_state <= IDLE; else cur_state <= next_state; end //三段式状态机第二段 always@(*)begin next_state = IDLE; case(cur_state) IDLE: if(i2c_start) next_state = START1; //接收到开始信号,跳转到发送起始信号状态 else next_state = IDLE; START1: if(i2c_clk_cnt == 2'd3) //i2c_clk 计数到最大值3,跳转到发送器件地址+写标志位状态 next_state = SEND_D_ADDR_W; else next_state = START1; SEND_D_ADDR_W: if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7) //发送了8位地址后跳转到从机响应状态 next_state = ACK1; else next_state = SEND_D_ADDR_W; ACK1: if(ack_flag && i2c_clk_cnt == 2'd3)begin //响应标志有效 //根据地址状态位判断是16位地址还是8位地址,从而跳转到不同状态 if(i2c_num) //16位地址 next_state = SEND_R_ADDR_H; //跳转到寄存器高8位地址发送状态 else //8位地址 next_state = SEND_R_ADDR_L; //跳转到寄存器低8位地址发送状态 end else if(i2c_clk_cnt == 2'd3) //响应无效或者响应不及时则跳转回初始状态 next_state = IDLE; else next_state = ACK1; SEND_R_ADDR_H: if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7) //发送了寄存器高8位地址后跳转到从机响应状态 next_state = ACK2; else next_state = SEND_R_ADDR_H; ACK2: if(ack_flag && i2c_clk_cnt == 2'd3) next_state = SEND_R_ADDR_L; //响应标志有效则跳转到寄存器低8位地址发送状态 else if(i2c_clk_cnt == 2'd3) //响应无效或者响应不及时则跳转回初始状态 next_state = IDLE; else next_state = ACK2; SEND_R_ADDR_L: if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7) //发送了寄存器低8位地址后跳转到从机响应状态 next_state = ACK3; else next_state = SEND_R_ADDR_L; ACK3: if(ack_flag && i2c_clk_cnt == 2'd3)begin //响应标志有效 if(i2c_rw) //读状态 next_state = START2; //跳转到第二次发送起始信号 else //写状态 next_state = WR_DATA; //跳转到写数据状态 end else if(i2c_clk_cnt == 2'd3) next_state = IDLE; //响应无效或者响应不及时则跳转回初始状态 else next_state = ACK3; START2: if(i2c_clk_cnt == 2'd3) next_state = SEND_D_ADDR_R; //第二次发送起始信号后跳转到发送器件地址+读标志位状态 else next_state = START2; SEND_D_ADDR_R: if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7) //发送完了8位地址后跳转到从机响应状态 next_state = ACK5; else next_state = SEND_D_ADDR_R; ACK5: if(ack_flag && i2c_clk_cnt == 2'd3) next_state = RD_DATA; //响应标志有效则跳转到读数据状态 else if(i2c_clk_cnt == 2'd3) next_state = IDLE; //响应无效或者响应不及时则跳转回初始状态 else next_state = ACK5; RD_DATA: if(i2c_clk_cnt == 2'd3 && bit_cnt == 3'd7) //接收完了8位数据后跳转到主机发送非响应状态 next_state = NACK; else next_state = RD_DATA; NACK: if(i2c_clk_cnt == 2'd3) next_state = STOP; //发送完了非响应信号后跳转到发送结束信号状态 else next_state = NACK; WR_DATA: if(bit_cnt == 3'd7 && i2c_clk_cnt == 2'd3) next_state = ACK4; //写完了8位数据后跳转到从机响应状态 else next_state = WR_DATA; ACK4: if(ack_flag && i2c_clk_cnt == 2'd3) next_state = STOP; //响应标志有效则跳转到发送结束信号状态 else if(i2c_clk_cnt == 2'd3) next_state = IDLE; //响应无效或者响应不及时则跳转回初始状态 else next_state = ACK4; STOP: if(bit_cnt == 2'd3 && i2c_clk_cnt == 2'd3) //结束信号发送完毕(这里还预留了2个周期)跳转到初始状态,等待下一次传输开始信号 next_state = IDLE; else next_state = STOP; default:next_state = IDLE; endcase end //三段式状态机第三段 always@(posedge i2c_clk or negedge sys_rst_n)begin if(~sys_rst_n)begin //初始状态 sda_en <= 1'b1; sda_out <= 1'b1; bit_cnt <= 3'd0; i2c_end <= 1'b0; i2c_data_r <= 8'd0; i2c_data_r_temp <= 8'd0; end else begin i2c_end <= 1'b0; case(cur_state) IDLE:begin sda_en <= 1'b1; //控制总线 sda_out <= 1'b1; //拉高总线 end START1:begin if(i2c_clk_cnt == 2'd3)begin //发送完了开始信号 if(addr_w[7])begin //如果器件地址的最高位为1则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //如果器件地址的最高位为0则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end else begin //还没发送完开始信号则保持低电平 sda_en <= 1'b1; sda_out <= 1'b0; end end SEND_D_ADDR_W:begin if(bit_cnt == 3'd7)begin if(i2c_clk_cnt == 2'd3)begin //发送了8个数据(器件地址+写标志位) bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b0; //释放总线 end end else if(i2c_clk_cnt == 2'd3)begin //发送完了一个数据 bit_cnt <= bit_cnt + 1'd1; //发送数据计数器清零 sda_en <= 1'b1; //控制总线 sda_out <= addr_w[6-bit_cnt]; //总线依次串行输出地址 end end ACK1:begin if(i2c_clk_cnt == 2'd3)begin if(i2c_num)begin //如果器件地址为16位 if(i2c_addr[15])begin //如果器件地址的16位为1则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //如果器件地址的16位为0则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end else begin //如果器件地址为8位 if(i2c_addr[7])begin //如果器件地址的8位为1则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //如果器件地址的8位为0则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end end end SEND_R_ADDR_H:begin if(bit_cnt == 3'd7)begin //8个数据发送完了 if(i2c_clk_cnt == 2'd3)begin bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b0; //释放总线 end end else if(i2c_clk_cnt == 2'd3)begin bit_cnt <= bit_cnt + 1'd1; //发送数据计数器清零 sda_en <= 1'b1; //控制总线 sda_out <= i2c_addr[14-bit_cnt];//总线依次串行输出地址 end end ACK2:begin if(i2c_clk_cnt == 2'd3)begin if(i2c_addr[7])begin //下一个要发送数据的首个数据为高则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //下一个要发送数据的首个数据为低则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end end SEND_R_ADDR_L:begin if(bit_cnt == 3'd7)begin //8个数据发送完了 if(i2c_clk_cnt == 2'd3)begin bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b0; //释放总线 end end else if(i2c_clk_cnt == 2'd3)begin bit_cnt <= bit_cnt + 1'd1; //发送数据计数器清零 sda_en <= 1'b1; //控制总线 sda_out <= i2c_addr[6-bit_cnt]; //总线依次串行输出地址 end end ACK3:begin if(!i2c_rw)begin //是写操作 if(i2c_clk_cnt == 2'd3)begin if(i2c_data_w[7])begin //下一个要发送数据的首个数据为高则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //下一个要发送数据的首个数据为低则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end end else begin //是读操作 if(i2c_clk_cnt == 2'd3)begin //提前拉高总线进入再次发送起始信号状态 sda_en <= 1'b1; sda_out <= 1'b1; end else begin sda_en <= 1'b1; sda_out <= 1'b0; end end end START2:begin if(i2c_clk_cnt == 2'd1)begin //拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end else if(i2c_clk_cnt == 2'd3)begin if(addr_r[7])begin //下一个要发送数据的首个数据为高则提前拉高总线 sda_en <= 1'b1; sda_out <= 1'b1; end else begin //下一个要发送数据的首个数据为低则提前拉低总线 sda_en <= 1'b1; sda_out <= 1'b0; end end end SEND_D_ADDR_R:begin if(bit_cnt == 3'd7)begin //8个数据发送完了 if(i2c_clk_cnt == 2'd3)begin bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b0; //释放总线 end end else if(i2c_clk_cnt == 2'd3)begin bit_cnt <= bit_cnt + 1'd1; //发送数据计数器清零 sda_en <= 1'b1; //控制总线 sda_out <= addr_r[6-bit_cnt]; //总线依次串行输出地址 end end ACK5: sda_en <= 1'b0; //下一个状态是接收数据,所以释放总线 RD_DATA: if(i2c_clk_cnt == 2'd3)begin if(bit_cnt == 3'd7)begin //接收了8个数据 bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b1; //控制总线 sda_out <= 1'b1; //拉高总线(为了下一步发送非响应信号) i2c_data_r <= i2c_data_r_temp; //将读取的数据输出 end else begin //数据还未接收完毕 bit_cnt <= bit_cnt + 3'd1; end end else if(i2c_clk_cnt == 2'd1)begin //在SCL的中间采集数据 i2c_data_r_temp[7-bit_cnt] <=sda_in;//将总线上的数据依次串行采集 end NACK: if(i2c_clk_cnt == 2'd3)begin sda_en <= 1'b1; //控制总线 sda_out <= 1'b0; //拉高总线 end WR_DATA: if(bit_cnt == 3'd7)begin //写完了8个数据 if(i2c_clk_cnt == 2'd3)begin bit_cnt <= 3'd0; //发送数据计数器清零 sda_en <= 1'b0; //释放总线 end end else if(i2c_clk_cnt == 2'd3)begin //没有写完8个数据 bit_cnt <= bit_cnt + 1'd1; //发送数据计数器累加 sda_en <= 1'b1; sda_out <= i2c_data_w[6-bit_cnt]; //依次输出数据 end ACK4: if(i2c_clk_cnt == 2'd3)begin sda_en <= 1'b1; //控制总线 sda_out <= 1'b0; //拉低总线(为了下一步发送终止信号) end STOP: if(i2c_clk_cnt == 2'd2 && bit_cnt == 2'd0)begin //拉高信号作为终止信号 sda_en <= 1'b1; sda_out <= 1'b1; end else if( i2c_clk_cnt == 2'd3 )begin if(bit_cnt == 2'd3)begin bit_cnt <= 2'd0; i2c_end <= 1'b1; //发送完了终止信号且延时一段时间发送IIC结束信号 end else bit_cnt <= bit_cnt + 1'd1; end default:; endcase end end //i2c时钟生成 always@(posedge i2c_clk or negedge sys_rst_n)begin if(~sys_rst_n) scl <= 1'b1; else if(cur_state != STOP)begin if(i2c_clk_cnt == 2'd2) scl <= 1'b0; else if(i2c_clk_cnt == 2'd0) scl <= 1'b1; end else scl <= 1'b1; end //从机响应信号标志 always@(posedge i2c_clk or negedge sys_rst_n)begin if(~sys_rst_n) ack_flag <= 1'b0; else case(cur_state) ACK1,ACK2,ACK3,ACK4,ACK5: if(i2c_clk_cnt == 2'd1 && !sda_in) //在从机响应状态正确接收到了从机发送的响应信号则拉高响应标志 ack_flag <= 1'b1; else if(i2c_clk_cnt == 2'd3) ack_flag <= 1'b0; default:ack_flag <= 1'b0; endcase end endmodule
6、Testbench及仿真结果
现在我们的驱动模块就写好了,接下来分别用一个简单的和稍复杂的Testbench来对其进行测试。需要注意的是这里的仿真还需要一个EEPROM_AT24C64的仿真模型,这个模型在夏宇闻老师的书里可以找到。
6.1、ATAT24C64(EEPROM芯片)的简单介绍
AT24C64是一种非易失性存储器(断掉数据仍保留),可以通俗的理解为一个大的二维数组。
AT24C64 的存储空间为 64 Kbit(8Kbyte),一共有256页,每页32个数据。需要 13 位存储地址才可满足所有存储单元的寻址,存储地址为 2 字节,存储地址示意图如下:

最高支持400k通信速率,每次写入数据需间隔最少10ms.。
6.2、1个数据写入然后读取对比
编写Testbench实现如下目标:
- 1.上电复位结束后,延时1ms(模拟器件稳定),开始往随机地址中写入随机数据
- 2.写入数据10ms后对刚刚的地址进行读取操作,比对读取的数据是否与写入的数据一致
`timescale 1ns/1ns //时间单位/精度 //------------
<模块及端口声明>
---------------------------------------- module tb_i2c_drive(); reg sys_clk ; reg sys_rst_n ; reg i2c_rw ; reg i2c_start ; reg [15:0] i2c_addr ; reg [7:0] i2c_data_w ; wire [7:0] i2c_data_r ; wire scl ; wire sda ; //------------
<例化被测试模块>
---------------------------------------- //例化IIC驱动模块 i2c_drive #( .DEVICE_ADDR (7'b1010_000 ), //i2c从机地址 .SYS_CLK_FREQ (26'd50_000_000 ), //系统时钟频率50M .I2C_FREQ (18'd250_000 ) //i2c时钟频率,250k ) i2c_drive_inst( .sys_clk (sys_clk ), //输入系统时钟,50MHz .sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效 .i2c_rw (i2c_rw ), //读写使能信号----1:读;0:写 .i2c_start (i2c_start ), //i2c开始信号 .i2c_num (1'b1 ), //i2c字节地址字节数----1:16位;0:8位 .i2c_addr (i2c_addr ), //i2c字节地址 .i2c_data_w (i2c_data_w ), //写入i2c数据 .i2c_clk ( ), //i2c驱动时钟 .i2c_end ( ), //i2c一次读/写操作完成 .i2c_data_r (i2c_data_r ), //i2c读取数据 .scl (scl ), //输出至i2c设备的串行时钟信号scl .sda (sda ) //输出至i2c设备的串行数据信号sda ); //从机模块 EEPROM_AT24C64 u_EEPROM_AT24C64_inst( .scl (scl), .sda (sda) ); //------------
<设置初始测试条件>
---------------------------------------- initial begin sys_clk = 1'b0; //初始时钟为0 sys_rst_n <= 1'b0; //初始复位 i2c_rw <= 1'b0; i2c_start <= 1'b0; i2c_addr <= 16'd0; i2c_data_w <= 8'd0; #65 //65个时钟周期后 sys_rst_n <= 1'b1; //拉高复位,系统进入工作状态 #1_000_000 //1ms延时 i2c_rw <= 1'b0; //写状态控制 i2c_start <= 1'b1; //拉高开始信号 //i2c_addr <= ADDR; i2c_addr <= ($random%65536);//生成一个0~65535的随机数,即16位的二进制随机数 i2c_data_w <= ($random%256);//生成一个0~255的随机数,即8位的二进制随机数 #1200 //这里保证i2c_start超过一个i2c_clk周期,保证能采集到该信号 i2c_start <= 1'b0; #10_000_000 i2c_rw <= 1'b1; //写状态控制 i2c_start <= 1'b1; //拉高开始信号 #1200 //这里保证i2c_start超过一个i2c_clk周期,保证能采集到该信号 i2c_start <= 1'b0; end //------------
<设置时钟>
---------------------------------------------- always #10 sys_clk = ~sys_clk; //系统时钟周期20ns endmodule
设置时钟>
设置初始测试条件>
例化被测试模块>
模块及端口声明>
使用modelsi'm软件进行仿真,仿真结果如下图:
- 在约1ms处,往从机的地址为‘h3524的寄存器写入数据’h81,一段时间后i2c_end拉高,表示数据写入完成
- 在11ms处,向从机的地址为‘h3524的寄存器读取数据,一段时间后i2c_end拉高,表示数据读取完成,读到的数据为’h81
- 读取的数据与写入的数据一致,证明仿真验证成功

截取写操作部分的详细仿真结果:

截取读操作部分的详细仿真结果:

6.3、若干个数据写入然后读取对比
在6.2节我们通过对AT24C64模型的某个地址写入随机数据然后读出对比验证的方式来对编写的IIC驱动代码做了验证,验证结果成功。可是上节的激励是来自编写的Testbench,而不是主机,也就是说无法模拟主机调用IIC总线,这样的测试难免不够周全。
在这一节,我们再编写两个Verilog文件--i2c_top、i2c_rw:
- i2c_rw作为读写模块来调用IIC驱动对从机AT24C64模型实现写若干次操作后,再对操作过的地址进行读取操作,通过对结果的比对来验证主机对从机的IIC写读是否成功
- i2c_top作为顶层模块,例化i2c_rw读写模块、及i2c_drive驱动模块
i2c_rw读写模块预期实现的功能:
- 上电复位后每隔一定的时间(可设置,默认10ms)对IIC进行写入操作,地址从零开始,长度可设置(默认64),但不能大于8192(最大地址),写入数据与地址相同
- 写入完成对上面操作过的地址进行读取操作
i2c_rw读写模块代码:
module i2c_rw( input i2c_clk , //i2c驱动时钟 input rst_n , //低电平有效的复位信号 input i2c_end , //i2c一次读/写操作完成 input [7:0] i2c_data_r , //i2c读取数据 output reg i2c_rw , //读写使能信号----1:读;0:写 output reg i2c_start , //i2c开始信号 output reg [15:0] i2c_addr , //i2c字节地址 output reg [7:0] i2c_data_w //写入i2c数据 ); parameter DATA_MAX = 16'd64; //最大256页地址(每页32个),最多8192个地址 parameter WR_CYCLE_MAX = 32'd10_000; //每次写入需要间隔的时间,默认10ms(1M时钟下) reg [31:0] cnt_wr_cycle; //写入间隔计数器 //写入间隔计数 always@(posedge i2c_clk or negedge rst_n)begin if(!rst_n) cnt_wr_cycle <= 32'd0; else if(cnt_wr_cycle == WR_CYCLE_MAX) //已计数到最大则清零(间隔时间结束) cnt_wr_cycle <= 32'd0; else cnt_wr_cycle <= cnt_wr_cycle + 1'd1; //否则计数器累加 end //读写状态控制 always@(posedge i2c_clk or negedge rst_n)begin if(!rst_n) i2c_rw <= 1'b0; else if((i2c_addr == DATA_MAX - 1'd1) && (i2c_end)) //数据全部写入则将读写控制拉高(表示读) i2c_rw <= 1'b1; else i2c_rw <= i2c_rw; end //AT24C64地址设置 always@(posedge i2c_clk or negedge rst_n)begin if(!rst_n) i2c_addr <= 16'd0; else if(i2c_end)begin //一次读写操作结束 if(i2c_rw) begin //读操作 if(i2c_addr == DATA_MAX - 1'd1) i2c_addr <= 16'd0; //已到达最大操作个数地址则清零 else i2c_addr <= i2c_addr + 1'd1; //否则地址累加 end else begin //写操作 if(i2c_addr == DATA_MAX - 1'd1) i2c_addr <= 16'd0; //已到达最大操作个数地址则清零 else i2c_addr <= i2c_addr + 1'd1; //否则地址累加 end end else i2c_addr <= i2c_addr; end //读写AT24C64 always @(posedge i2c_clk or negedge rst_n) begin if(!rst_n) begin i2c_start <= 1'b0; i2c_data_w <= 8'd0; end else begin if(!i2c_rw) begin //从eeprom的第1页的第1个字节开始写入数据 if(cnt_wr_cycle == WR_CYCLE_MAX - 1'b1) begin //一次写间隔结束,可以开始新一轮写入 i2c_start <= 1'b1; //拉高IIC开始信号,准备写入数据 i2c_data_w <= i2c_addr[7:0]; //写入数据与写入地址相同 // i2c_data_w <= 0; //暴力清零 end else begin //否则保持状态不变 i2c_start <= 1'b0; i2c_data_w <= i2c_data_w; end end else begin //从eeprom的第1页的第1个字节开始读取数据 if(cnt_wr_cycle == 0 && i2c_addr == 0) //第一次读取延时 i2c_start <= 1'b1; //拉高IIC开始信号,准备读数据 else if(i2c_end ) //一次读操作完成 i2c_start <= 1'b1; //拉高IIC开始信号,准备再次读数据 else i2c_start <= 1'b0; end end end endmodule
i2c_top顶层模块代码:
module i2c_top( input sys_clk , //输入系统时钟,50MHz input sys_rst_n , //输入复位信号,低电平有效 output scl , //输出至i2c设备的串行时钟信号scl inout sda //输出至i2c设备的串行数据信号sda ); wire i2c_rw ; wire i2c_start ; wire [15:0] i2c_addr ; wire [7:0] i2c_data_w ; wire i2c_clk ; wire i2c_end ; wire [7:0] i2c_data_r ; i2c_rw i2c_rw_inst( .i2c_clk (i2c_clk ), //i2c驱动时钟 .rst_n (sys_rst_n ), //低电平有效的复位信号 .i2c_end (i2c_end ), //i2c一次读/写操作完成 .i2c_data_r (i2c_data_r ), //i2c读取数据 .i2c_rw (i2c_rw ), //读写使能信号----1:读;0:写 .i2c_start (i2c_start ), //i2c开始信号 .i2c_addr (i2c_addr ), //i2c字节地址 .i2c_data_w (i2c_data_w ) //写入i2c数据 ); i2c_drive #( .DEVICE_ADDR (7'b1010_000 ), //i2c设备地址,7'b1010_000 .SYS_CLK_FREQ(26'd50_000_000 ), //输入系统时钟频率 .I2C_FREQ (18'd250_000 ) //i2c设备时钟频率 ) i2c_drive_inst( .sys_clk (sys_clk ), //输入系统时钟,50MHz .sys_rst_n (sys_rst_n ), //输入复位信号,低电平有效 .i2c_rw (i2c_rw ), //读写使能信号----1:读;0:写 .i2c_start (i2c_start ), //i2c开始信号 .i2c_num (1'b1 ), //i2c字节地址字节数----1:16位;0:8位 .i2c_addr (i2c_addr ), //i2c字节地址 .i2c_data_w (i2c_data_w ), //写入i2c数据 .i2c_clk (i2c_clk ), //i2c驱动时钟 .i2c_end (i2c_end ), //i2c一次读/写操作完成 .i2c_data_r (i2c_data_r ), //i2c读取数据 //I2C物理接口 .scl (scl ), //输出至i2c设备的串行时钟信号scl .sda (sda ) //输出至i2c设备的串行数据信号sda ); endmodule
编写Testbench例化i2c_top顶层模块和AT24C64仿真模型,同时打印读写信息(读写状态;地址;数据):
`timescale 1ns/1ns //时间单位/精度 //------------
<模块及端口声明>
---------------------------------------- module tb_i2c_top(); reg sys_clk ; reg sys_rst_n ; wire scl ; wire sda ; defparam i2c_top_inst.i2c_rw_inst.WR_CYCLE_MAX = 1000; //仿真时将写入间隔改为1ms(节省时间) //defparam i2c_top_inst.i2c_rw_inst.DATA_MAX = 10; //可以重新修改写入个数 //------------
<例化被测试模块>
---------------------------------------- i2c_top i2c_top_inst( .sys_clk (sys_clk ), .sys_rst_n (sys_rst_n ), .scl (scl ), .sda (sda ) ); EEPROM_AT24C64 u_EEPROM_AT24C64_inst( .scl (scl), .sda (sda) ); //------------
<设置初始测试条件>
---------------------------------------- initial begin sys_clk = 1'b0; //初始时钟为0 sys_rst_n <= 1'b0; //初始复位; #65 //65个时钟周期后 sys_rst_n <= 1'b1; //拉高复位,系统进入工作状态 end //------------
<命令窗口监控>
---------------------------------------- always @(*) begin if(i2c_top_inst.i2c_rw_inst.i2c_rw == 1'b0)begin //写操作 if (i2c_top_inst.i2c_rw_inst.i2c_end) //写操作结束 //打印:时间--地址--数据 $display("@time %t write: addr=%d data=%d",$time,i2c_top_inst.i2c_rw_inst.i2c_addr, i2c_top_inst.i2c_rw_inst.i2c_data_w); end else begin if (i2c_top_inst.i2c_rw_inst.i2c_end) //读操作 //打印:时间--地址--数据 //读操作结束 $display("@time %t read: addr=%d data=%d",$time,i2c_top_inst.i2c_rw_inst.i2c_addr, i2c_top_inst.i2c_rw_inst.i2c_data_r); end end //------------
<设置时钟>
---------------------------------------------- always #10 sys_clk = ~sys_clk; //系统时钟周期20ns endmodule
设置时钟>
命令窗口监控>
设置初始测试条件>
例化被测试模块>
模块及端口声明>
使用modelsi'm软件进行仿真,仿真结果如下图:
- 前一段时间i2c_rw信号是低电平,表示此时在进行数据写入操作
- 前一段时间i2c_rw信号是高电平,表示此时在进行数据读取操作

接下来局部截图进行讲解:
下图中:
i2c_rw为低电平,代表在进行写操作,分别往地址d55中写入d55·······地址d62中写入d62等,直到往地址d63中写入d63后,i2c_rw被拉高,开始进行读操作。重新从地址0开始到63进行读取操作。

下图中:
i2c_rw为高电平,代表在进行读操作,分别从地址d42中读取d42·······直到地址d63中写入d63等后,i2c_rw被拉高,重新从地址0开始到63进行读取操作。需要注意的是,下图与上图在IIC时序上,sda信号存在蓝色(不定态)和红色(x状态),产生的原因我们最后再讲,这里只需要知道它们对时序没有影响即可。
还记得之前我们在testbench加入了命令打印行吗?接下来就看一下命令打印窗口:
首先对地址0-63写入数据0-63,然后重复对地址0-63读取,读取的数据为0-63,与写入数据一致

7、上板验证
至此已经顺利完成了IIC驱动的仿真验证,接下来使用一块Cyclone IV E的开发板上板验证,使用signaltap II抓一下波形。
因为每两次写入操作中间隔10ms,所以在FPGA芯片资源有限的情况下肯定没办法同时抓到两次写入的波形,只好随便抓几个个波形分析下IIC时序:
下图往地址d18写入数据d18:

下图往地址d50写入数据d50:

其他地址类推。
读操作的截图如下:
从地址48-63(实际上是0-63,截图问题)读取了数据48-63,然后进行新一轮的读取。读取的数据与写入的数据一致。

对地址50进行读操作的截图如下:

8、问题与思考
8.1、读写仿真出现的高阻态与未知态问题
在使用仿真模型进行读写仿真时出现了高阻态与未知态,见下图(写操作):

上图的蓝色高阻态均发生在sda_en为低电平期间,此时主机放弃了总线控制权,放弃总线的原因是为了等待从机发送响应信号(低电平),而高阻态的出现就是因为总线控制权的过渡需要时间。同时还可以发现高阻态发生在scl低电平(基本算是中间),此时是允许数据变化的,所以不会对写时序造成影响。
读操作出现的问题如下:

读操作的情况基本与写操作一致,不过还出现了x状态,但也不影响读写时序。
8.2、从机应答时间问题
在上板验证时发现在从机应答时出现了高电平,分别如下两图:

画图说明:
- A部分----1010_000是从机的器件地址;
- B部分----读写控制位;
- C部分----是从机的应答信号;

从A部分可以看出,发送数据的时候,数据的改变都是在低电平的中间,所以发送给从机的单bit数据时间宽度==scl时钟的半个低电平+scl时钟的一个高电平+scl时钟的半个低电平;我一开始在C部分检测从机的应答信号的时候,先是在前一个低电平的中间释放了总线,然后等待总线被从机拉低后再拉高。根据A部分的数据发送经验我以为,从机的响应应该也会持续到下一个低电平的中间才会发生变化(也就是从机的响应会维持一个scl时钟周期),可实际上从机提前了1/4个scl时钟周期,在scl时钟的下降沿就发生了变化。
这个问题我一开始一直以为是响应不成功,加上状态机的状态跳转我又是设计的在低电平的中间检测从机是否响应,导致我的状态跳转一直是错的。后面仔细查了数据手册,发现了这个响应的时序如下(AT24C64):

可以看到,时序图上的响应信号也确实是在scl的下降沿变化,而不是低电平的中间。
然后又搜了点资料,发现正点原子的开发板的教学资料上EEPROM章节,signaltap II 抓的时序如下:

图中黑圈都是从机响应后被上拉电阻拉高,而且从机都是在scl的下降沿将总线释放了,和我抓到的图是一致。
解决办法: 在响应信号的scl的上升沿中间进行采样,一旦在采样中检测到响应信号,就把标志信号拉高(ack_flag),这样状态的跳转就可以通过标志信号来判断了。
8.3、写入间隔(写回时间)
下图说明:
- A部分----第一次写操作;
- B部分----100us延时操作;
- C部分----第二次写操作;
- D部分----从机回应的无效响应信号

从上图可以看到,两次写操作的间隔时间只有100us,这就导致第二次写操作失败(回了非响应信号)。每个I2C从器件的写间隔时间不一样,具体以手册为准。
解决办法:查看AT24CC64的手册,发现写入时间间隔twr==10ms,所以把两次写入的间隔时间改成10ms就好了。
8.4、读操作要先发送写指令
读操作在发送“器件地址+读”之前应该先进行一次哑写操作,这样就可以把地址指针指向想写入数据的寄存器地址。
我一开始调试的时候就是忘了这点,直接发送的读操作,然后就收到了从机的非响应信号,导致时序错误。
解决办法:先进行写操作就可以了。
8.5、其他
想要整个工程的朋友可以在评论区留下邮箱。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/208746.html原文链接:https://javaforall.net
