1. 简介
IIC(Inter-Integrated Communication)总线是Philips公司开发的两线串行总线,由数据线SDA和时钟线SCL构成
1.1 特点
同步串行总线,支持多主设备,传输速率为400Kbps
1.2 接口
SDA:数据线
SCL:时钟线
- SDA和SCL都是双向线路,都通过一个上拉电阻连接到正的电源电压,当总线空闲时都是高电平。
- SCL由主设备产生,SDA的产生与数据传输方向有关。
1.3 工作模式
2. 传输协议
2.1 时序图
起始信号:SCL为高电平,SDA从高电平到低电平
数据传送:只能在SCL为低电平是数据才能变化
终止信号:SCL为高电平,SDA从低电平变为高电平
每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
特殊情况:
- 从机不对主机寻址信号应答时(如从机正在进行实时性的处理工作而无法接收总线上的数据),主机将数据线置于高电平而产生一个终止信号以结束总线的数据传送。
- 如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应发出终止信号以结束数据的继续传送。与第一种情况类似,都是没有接受到ACK
2.2 数据帧格式
I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
注意:既然是应答位,因此应答信号总是对方发送的。
2.3 模块实现
module I2C (
input wire sys_clk,
input wire sys_rst_n,
input wire [7:0] data_in,
input wire data_vld,
inout wire SDA,
output reg sck
);
reg flag;
always_ff @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
flag <= 'b0;
else if (data_vld)
flag <= 'b1;
else
flag <= ~flag;
end
reg [7:0] data_reg;
always_ff @(posedge sys_clk or negedge sys_rst_n) begin : proc_data_reg
if(~sys_rst_n) begin
data_reg <= 0;
end else if (data_vld)begin
data_reg <= data_in;
end else begin
data_reg <= data_reg;
end
end
parameter IDLE = 5'b00001;
parameter START = 5'b00010;
parameter TRA_DATA = 5'b00100;
parameter ACK = 5'b01000;
parameter STOP = 5'b10000;
reg [4:0] cur_state;
reg [4:0] nxt_state;
reg [3:0] data_cnt;
always_ff @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
cur_state <= IDLE;
else
cur_state <= nxt_state;
end
always_latch begin
case(cur_state)
IDLE :
if (data_vld)
nxt_state = START;
START:
if (flag )
nxt_state = TRA_DATA;
TRA_DATA:
if (data_cnt == 'd7 && flag && sck)
nxt_state = ACK;
ACK :
if (data_cnt == 'd8 && flag && sck)
nxt_state = STOP;
STOP :
if (flag)
nxt_state = IDLE;
endcase // cur_state
end
always_ff @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
sck <= 'b1;
else if ((cur_state == START && !flag) || cur_state == STOP)
sck <= 'b1;
else if ((cur_state == TRA_DATA || cur_state == START) && flag)
sck <= ~sck;
else if (cur_state == ACK && flag&& !sck)
sck <= ~sck;
end
always_ff @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
data_cnt <= 'd0;
else if (data_cnt == 'd8 && flag && sck)
data_cnt <= 'd0;
else if (cur_state == TRA_DATA && flag == 1'b1 && sck)
data_cnt <= data_cnt + 1'b1;
else
data_cnt <= data_cnt;
end
reg data_out;
always_ff @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
data_out <= 'b1;
else if (cur_state == START && !flag)
data_out <= 'b0;
else if (cur_state == TRA_DATA && !flag)
data_out <= data_reg[7-data_cnt];
else if (cur_state == STOP && !flag)
data_out <= 'b1;
else
data_out <= data_out;
end
assign SDA = (cur_state == ACK) ? 1'bz : data_out;
endmodule
这里主要注意几个点
- 我们不采取时钟上升沿和下降沿分别采样的做法,而采用一个SCK电平持续两个时钟周期,这样我们就可以在SCK为低电平的时候改变输出的值。
- 这里我们只进行发送数据的操作,所以ACK信号为高组态。如果是读取数据,那么应该在对应的状态机中返回ACK信号。
2.4 模块测试
module tb_I2C();
reg [7 : 0] data_in;
reg data_vld;
reg sys_clk, sys_rst_n;
wire data_out, sck;
initial begin
sys_clk = 'b0;
sys_rst_n = 'b0;
# 15
sys_rst_n = 'b1;
end
always #5 sys_clk = ~sys_clk;
initial begin
data_vld = 'b0;
data_in = 'd0;
repeat(2)
@ (posedge sys_clk) ;
data_vld = 'b1;
data_in = 'b1001_0010;
@(posedge sys_clk) ;
data_vld = 'b0;
data_in = 'd0;
#10000
$finish;
end
I2C I2C_dut(
sys_clk,
sys_rst_n,
data_in,
data_vld,
data_out,
sck
);
initial begin
$fsdbDumpfile("tb_I2C.fsdb");
$fsdbDumpvars(0, tb_I2C);
$fsdbDumpon;
end
endmodule : tb_I2C
3. 小结
- I2C真正实现的时候第一次发送的是7位设备地址信息,1位方向。然后根据自身的设备型号和读写方向进行对应的操作,我们这里只是根据时序图简单的实现基本的协议。
- 如果需要实现完整的,整个核心状态是没有变化的,不过ACK状态有可能再次回到TRA_DATA状态,需要根据数据的传输次数进行配置。
- 有一个核心问题是,从设备ACK信号的产生是根据主设备发送的SCL信号作为时钟沿产生的,还是从设备也有一个对应的sys_clk信号。