IIC协议系列博文: 上一篇文章是对的IIC总线详细介绍,了解IIC总线的阅读和写作方法。本文基于编写FPGA的IIC驱动模块,模拟和验证模块。
先回顾一下IIC总线单次读写时序。 单写时序如下: 单读时序如下:
总结一下单次写作顺序的过程(假设机器响应正确,如果响应不正确或不正确,跳转到初始状态重新开始写作):
1.发送起始信号,开始一次传输 2.发送器件地址 低电平(表示写)等待从机正确响应 3.发送寄存器地址(16或8位地址),等待从机正确响应 4.将8位数据发送到从机,等待从机正确响应 5.发送停止信号,结束本次传输
单读时序的过程(假设机器响应正确,如果响应不正确或不响应,跳转到初始状态重新开始读操作):
1.发送起始信号,一次写传输开始 2.发送器件地址 低电平(表示写作,写作操作为虚写,真正的目的是指向需要读取的地址),等待从机器中正确回应 3.发送寄存器地址(16或8位地址),等待从机正确响应 4.再次发送起始信号,开始读取和传输 5.发送器件地址 高电平(表示读取,这个读取操作才是真正的读取操作),等待从机正确回应 6.接收从机在总线上发送的8位数据,向从机发送非响应信号,表示无需再次接收数据 7.发送停止信号,结束本次传输
本文的目的是设计一个IIC驱动模块包括单读和单写,与16位寄存器和8位寄存器兼容。根据次数和上述时序图,可以绘制IIC单读写驱动状态机如下:
上图状态机将单字节写作操作与随机读取操作相结合,可以实用 现 I2C 设备单次写作和单次随机读取的状态跳转。需要注意的是状态ACK1、ACK2、ACK3、ACK4、ACK当主机没有正确接收到从机的响应信号时,5仍跳回初始状态。
说明各种状态、状态跳转条件和输出(假设主机是FPGA):
1.上电后 IDLE(初始状态)主机在收到有效的单字节数据读写信号后跳转到START1(起始状态)主机在该状态下发送起始信号; 然后跳转 SEND_D_ADDR_W(发送器件地址状态 写标志),在此状态下,主机发送控制指令,控制指令高7位为设备地址,最低位为读写控制字,写入0,表示执行写作操作;然后跳到 ACK(响应状态)。 3、在 ACK1状态下,根据从机寄存器地址字节数跳转。 3.1.当主机正确接收响应信号时,从机寄存器为16位 , 状态跳转到SEND_R_ADDR_H(发送高字节地址状态) 8 位写入从机,然后状态跳转到 ACK2(响应状态);主机正确接收响应信号后,跳转 SEND_R_ADDR_L(发送低字节地址状态); 3.2.当主机正确接收响应信号,从机寄存器为8位时,状态机状态机直接跳转 SEND_R_ADDR_L(发送低字节地址状态);将寄存器地址的低8位写入机器,跳转到 ACK3(应答 状态)。 4、在 ACK3状态下,根据读写使信号跳转不同状态(判断此操作是写作操作还是读写操作,1–读;0–写)。 4.1当主机正确接收从机响应信号,读写使能信号较低时,状态跳转WR_DATA(写数据状态); 4.1.1在WR_DATA状态, 在从机上写入单字节数据后,跳转到 ACK(应答状态); 4.1.2在 ACK4状态下,当主机正确接收的响应信号时,跳转到 STOP(停止状态); 4.2.当主机正确接收从机响应信号,读写使能信号高时, 状态跳转到 START_2(起始状态); 4.2.1START_2(起始状态);主机再次发送起始信号,状态跳转 SEND_D_ADDR_R(发送器件地址状态 读标志); 4.2.2主机再次发送控制字节,高 7 位置设备地址不变,读写控制位写入1,表示读写操作,控制字节写入后,状态机跳转 ACK5(响应状态); 4.2.3当主机正确接收响应信号时,状态跳转 RD_DATA(读数据状态); 4.2.4在 RD_DATA(读取数据状态)状态,主机在总线上读取数据8次,读取数据后跳转到 NACK(无应答状态),
在这种状态下,将一个时钟的高电平写入从机,表示数据读取完成,然后状态机跳转到 STOP(停止状态)。 5、在 STOP(停止状态)状态,FPGA 向 EEPROM 发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回 IDLE(初始状态)等待下一个单字节数据读写开始信号。
Verilog编写的IIC驱动的整体框图和输入输出信号如下: 在SCL要求高电平设备SDA上述数据保持稳定;在SCL允许使用低电平设备SDA数据变化。我们知道驱动模块对IIC读写总线上的数据肯定需要一个驱动时钟。从上图可以看出,驱动时钟是SCL当频率为4倍时,操作最方便。如下图所示:
根据上述状态机的描述和整体设计,编写驱动模块并不难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:读写 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
;
//发送数据计数