IIC协议的简单介绍
1.IIC通讯设备的链接图
注:一个IIC总线可以挂载多个设备,一个IIC总线有两条线,一个是数据线,一个是时钟线。主机通过访问不同的从机地址来进行不同设备之间的通信。细节请自己百度,这里不做过多介绍。
2.IIC协议的时序
2.1整体时序图
注:图片纯手画,有些丑,不喜勿喷。
由图中可以看出,整体的时序图由A,B,C,D分割。下面我将详细介绍这四部分。
A:表示空闲状态,此时SCL和SDA都为高电平。
B:表示开始状态,当SCL为高电平时,SDA出现下降沿之后,表示进入了开始状态,数据将要发送或者接受。
C:表示数据读写状态,其中的一段时序波形如下图所示:
D:表示结束状态,当SCL为高电平时,SDA由低电平变为高电平,表示结束。
3.IIC的读写操作以及状态机的实现
3.1 写操作时序
IIC单字节写操作时序
IIC双字节写操作时序
注:这个只介绍2字节写操作,因为掌握了2字节的读写操作,一个字节的读写操作也是轻而易举的。这里不在做过多的解释。
3.1.1状态机的实现
图片太长我分两部分,由上面的写操作的时序,我抽象出不同的状态,状态1,空闲状态,状态2,开始状态,状态3,发送器件地址状态(send_device_addr),状态4,发送存储地址高位(send_storage_addr_H),状态5,发送存储地址高位(send_storage_addr_L),
状态6,写数据(wr_data),状态7,停止状态(stop)。
3.2读操作时序
3.2.1状态机的实现
图片太长我分三部分,由上面的读操作的时序,我抽象出不同的状态,状态1,空闲状态,状态2,开始状态,状态3,发送器件地址状态(send_device_addr),状态4,发送存储地址高位(send_storage_addr_H),
状态5,发送存储地址高位(send_storage_addr_L),
状态6,开始二状态(start_2),状态7,读数据状态(RD-DATA),状态8,停止状态(STOP).
由于二者之间有相同的部分,故我在一个状态转移图中表示所有状态的转移,整体的状态转移图如下图所示:
下面开始介绍如何实现这些状态机,在第一节中我们知道,状态跳转最重要的知道跳转的条件是什么,同时也要对系统的输入输出有清晰的认识。再此之前我先将状态跳转的程序附上。
/***************************组合逻辑表示状态转移*********************************/
always @(*) begin
case(current_state)
IDLE:
if(i2c_start == 1'b1)
next_state = START;
else
next_state = IDLE;
START:
if(cnt_driver_clk == 2'd3)
next_state = SEND_DEVICE_ADDR;
else
next_state = START;
SEND_DEVICE_ADDR:
if(addr_num == 1'b1) begin
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = SEND_STORAGE_ADDR_H;
else
next_state = SEND_DEVICE_ADDR;
end
else begin
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = SEND_STORAGE_ADDR_L;
else
next_state = SEND_DEVICE_ADDR;
end
SEND_STORAGE_ADDR_H:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = SEND_STORAGE_ADDR_L;
else
next_state = SEND_STORAGE_ADDR_H;
SEND_STORAGE_ADDR_L:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8)&&(i2c_rh_wl == 1'b0))
next_state = WR_DATA;
else if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8)&&(i2c_rh_wl == 1'b1))
next_state = START_2;
else
next_state = SEND_STORAGE_ADDR_L;
WR_DATA:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = STOP;
else
next_state = WR_DATA;
STOP:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd3))
next_state = IDLE ;
else
next_state = STOP ;
START_2:
if(i2c_rh_wl == 1'b1) begin
if(cnt_driver_clk == 2'd3)
next_state = SEND_DEVICE_ADDR_RD ;
else
next_state = START_2;
end
else
next_state = START_2 ;
SEND_DEVICE_ADDR_RD:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = RD_DATA ;
else
next_state = SEND_DEVICE_ADDR_RD;
RD_DATA:
if((cnt_driver_clk == 2'd3)&&(cnt_bit == 4'd8))
next_state = STOP ;
else
next_state = RD_DATA ;
default:next_state = IDLE;
endcase
end
/*****************************************************************************
模块说明:状态机中的时序逻辑表示输出
输出参数:i2c_scl
*****************************************************************************/
always @(posedge driver_clk or negedge sys_rst) begin
if (!sys_rst)
i2c_scl <= 1'b1;
else if ((current_state == IDLE)|| (current_state == STOP))
i2c_scl <= 1'b1;
else
i2c_scl <= (cnt_driver_clk == 2'd2) ? 1'b0 :
(cnt_driver_clk == 2'd0) ? 1'b1 : i2c_scl;
end
解释:在IDLE状态下,当出现一个有效的下降沿时,也就是开始信号(i2c_start),状态跳转到START状态。当经过一个iic的时钟周期之后(cnt_driver_clk == 2‘d3),表示START状态持续了一个时钟周期,此时跳转到SEND_DEVICE_ADDR状态,在这里我参考的野火和原子的教程,对存储地址的字节数进行了判断,当addr_num=1时,表示是两个字节,当(cnt_driver_clk=3,cnt_bit=8,sda_in=0时),表示器件地址写入完成,此时进入下一个状态SEND_STORAGE_ADDR_H。相反 addr_num = 0时为一个字节的存储器地址,此时直接跳到SEND_STORAGE_ADDR_L状态。在SEND_STORAGE_ADDR_H状态时,当写入8个字节的高八位存储器地址时,跳入SEND_STORAGE_ADDR_L状态,在SEND_STORAGE_ADDR_L状态下,在写入的八位低存储器地址,此时若i2c_rh_wl= 0则进入写数据状态WR_DATA,若此时i2c_rh_wl = 1则进入读状态START_2。在WR_DATA状态的完成时,进入STOP状态,在时钟的上升沿的同时数据拉高,状态结束。在START_2状态下,再次经过一个IIc时钟周期,进入SEND_DEVICE_ADDR_RD状态,在这个状态下,当发送完第八个字节,切iic从机的相应为0时,进入读数据状态。无论是在WR_DATA还是RD_DATA状态他们的状态转移条件都是,写或者读满8个字节,之后进行状态的跳转。
下面附上不同状态的输入输出程序:
/*****************************************************************************
模块说明:状态机中的组合逻辑表示输出
输出参数:sda_out
*****************************************************************************/
always @(*) begin
if(! sys_rst ) begin
sda_out = 1'b1 ;
out_flag = 1'b1 ;
end
else begin
case(current_state)
IDLE:begin
sda_out = 1'b1 ;
out_flag = 1'b1 ;
end
START:
if(cnt_driver_clk <= 2'b0)
sda_out = 1'b1 ;
else
sda_out = 1'b0 ;
SEND_DEVICE_ADDR: begin
out_flag = 1'b1;
if(cnt_bit <= 4'd6 )
sda_out = DIVICE_ADDR[6-cnt_bit];
else if(cnt_bit == 4'd7)
sda_out = 1'b0; //写数据
else begin//if(cnt_bit == 3'd8)
out_flag = 1'b0;
sda_out = 1'b1;
end
end
SEND_STORAGE_ADDR_H: begin
out_flag = 1'b1 ;
if(cnt_bit <= 4'd7)
sda_out = byte_addr[15-cnt_bit];
else begin//if(cnt_bit == 3'd8)
out_flag = 1'b0;
sda_out = 1'b1;
end
end
SEND_STORAGE_ADDR_L: begin
out_flag = 1'b1 ;
if(cnt_bit <= 4'd7)
sda_out = byte_addr[7-cnt_bit];
else begin//if(cnt_bit == 3'd8)
out_flag = 1'b0;
sda_out = 1'b1;
end
end
WR_DATA: begin
out_flag = 1'b1 ;
if(cnt_bit <= 4'd7)
sda_out = wr_data[7-cnt_bit];
else begin //if(cnt_bit == 3'd8)
sda_out = 1'b1;
out_flag = 1'b0;
end
end
STOP: begin
out_flag = 1'b1 ;
if((cnt_bit == 3'd0)&&(cnt_driver_clk == 2'b0))
sda_out = 1'b0;
else if((cnt_bit == 3'd0)&&(cnt_driver_clk == 2'd3))
sda_out = 1'b1;
else
sda_out = sda_out ;
end
START_2:begin
out_flag = 1'b1 ;
if(cnt_driver_clk < 2'd2)
sda_out = 1'b1 ;
else
sda_out = 1'b0 ;
end
SEND_DEVICE_ADDR_RD: begin
out_flag = 1'b1 ;
if(cnt_bit <= 4'd6)
sda_out = DIVICE_ADDR[6-cnt_bit];
else if(cnt_bit == 4'd7)
sda_out = 1'b1;
else begin //if(cnt_bit == 3'd8)
sda_out = 1'b1;
out_flag = 1'b0;
end
end
RD_DATA: begin
out_flag = 1'b0;
if((cnt_bit <= 4'd7)&&(cnt_driver_clk=='d2))
rd_data_reg[7-cnt_bit] = sda_in ;
else if(cnt_bit == 4'd8) begin
out_flag = 1'b1;
sda_out = 1'b1; //非应答信号
end
end
default:;
endcase
end
end
再次之前我先将一部分波形图附上:
1.写操作的波形图
2.读操作的波形图
cnt_driver_clk 先解释一下,这个变量的意思:本实验输出的IIC频率为250k,fpga的时钟输入时钟为50mhz,先进行分频将1mhz时钟driver_clk,这时采用一个cnt_driver_clk 计数器,计数到3的时候就说明iic经过一个时钟周期。cnt_bit,字节计数器。
大家这里可以参考波形图,理解代码,这里要写的太多了,如果有需要建议私信,我们好友联系。
最后一个知识点:数据io可以是双向的,这个可以将io口设计为三态模式的。
/************************三态门的使用**************************************/
assign i2c_sda = out_flag ? sda_out : 1'bz ; //当out_flag输出为高时,表示输出
assign sda_in = i2c_sda ;
这段代码的解释就是,当out_flag为高电平时,i2c_sda = sda_out,当为低电平时,i2c_sda为高阻态。out_flag和sda_out的赋值,详细请看上面的代码,里面详细写了不同状态下,sda_out和out_flag是如何赋值的。当out_flag为低电平时,i2c_sda为输入作用。sda_in的赋值也在上面的代码中有详细的介绍。
IIC的介绍就到这里,如果需要工程源码,私信即可,欢迎大家交流沟通。
关于应答信号的一些问题,题主在这里卡了好久,通过ila抓取,实际上的应答信号不是持续iic_scl的一个完整时钟周期的,这里我用的板子是黑金zynq7020+24lc04。为了程序的严谨性,我同样将ack采集出来了。这里的iic是单字节写入和读出,所以在理论上一个完成的iic协议应该有4个(寄存器地址为8位)或者5个ack(寄存器地址为16位)回应。故通过这样的分析我采集的逻辑如下:
assign ack = (current_state == SEND_DEVICE_ADDR || current_state == SEND_DEVICE_ADDR_RD
|| current_state == WR_DATA || current_state == SEND_STORAGE_ADDR_H
||current_state == SEND_STORAGE_ADDR_L)
? ((cnt_bit == 'd8&&cnt_driver_clk<= 'd2) ? sda_in:1'b1):1'b1;
always @(posedge driver_clk or negedge sys_rst) begin
if(!sys_rst)
ack_d1 <= 'd1;
else
ack_d1 <= ack;
end
assign ack_pos = (~ack_d1)&ack;
always @(posedge driver_clk or negedge sys_rst) begin
if(!sys_rst)
i2c_ack <= 1'b1;
else if(ack_num == 'd4 && addr_num == 'd1)
i2c_ack <= 1'b0;
else if(ack_num == 'd3 && addr_num == 'd0)
i2c_ack <= 1'b0;
else
i2c_ack <= i2c_ack;
end
always @(posedge driver_clk or negedge sys_rst) begin
if(!sys_rst)
ack_num <= 'd0;
else if(ack_num == 'd4 && addr_num == 'd1)
ack_num <= 'd0;
else if(ack_num == 'd3 && addr_num == 'd0)
ack_num <= 'd0;
else if(ack_pos)
ack_num <= ack_num + 1'b1;
else
ack_num <= ack_num;
end
初始i2c_ack 信号为高电平,当ack_num满足条件时,拉低。程序可以通过这样的逻辑来检测ack应答信号是否正确。
过段时间更新以太网的状态机实现,至此状态机将不再做更新。