IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,是一种同步、半双工的通信总线,用于连接微控制器及其外围设备。I2C总线产生于在80年代,最初为音频和视频设备开发,如今主要在服务器管理中使用,其中包括单个组件状态的通信。例如管理员可对各个组件进行查询,以管理系统的配置或掌握组件的功能状态,如电源和系统风扇。可随时监控内存、硬盘、网络、系统温度等多个参数,增加了系统的安全性,方便了管理。IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)。
IIC总线的特点:
简单性和有效性。由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。
支持多主控(multimastering), 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控占用IIC总线。
下图是一个嵌入式系统中处理器仅通过2根线的IIC总线控制多个IIC外设的典型应用图:
下图是I2C多master多slave示意图:
若想实现多master多slave效果:
IIC总线接口是一个标准的双向传输接口,一次数据传输需要主机和从机按照IIC协议的标准进行。I2C总线是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,并且在硬件上都需要接一个上拉电阻到VCC。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。
完整的读写过程如下图:
1) 总线空闲状态:
SDA和SCL同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
2) 总线START:
SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
3) 总线STOP:
SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
4 )总线Restart:
SCL为高电平时,SDA由高电平向低电平跳变,本质上也是START信号,用在完整I2C读过程中的读阶段,在首次发送停止信号之前,master通过发送Restart信号,可以转换与当前slave的通信模式(从写模式到读模式),或是切换到与另一个slave通信。
5 )数据阶段:
在IIC总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定。只有在SCL为低电平期间,才允许SDA上的电平改变状态。简单的说就是,数据在SCL下降会被采样,所以SDA需要在SCL为高电平时保持稳定。
6) ACK与NACK信号:
IIC总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
这段话再说细一点:
写阶段,master写了一字节数据,在第9个时钟脉冲期间释放数据线,由slave反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功;
读阶段,master向slave收数据,slave写了一字节数据,在第9个时钟脉冲期间释放数据线,由master反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功。
还有一种特殊情况:当master决定不再接收数据时,应向slave发送NACK信号,高速slave不再发送。
以下情况会导致出现NACK位:
代码:
`timescale 1ns / 1ps
//
// Module Name: IIC_control
// Tool Versions: Vivado 2018.3
// Description:
//
//
module IIC_control #(
parameter DIV_CLK = 'd500, //系统时钟分频系数
parameter WMEN_LEN = 8'd0, //写数据帧数
parameter RMEN_LEN = 8'd0 //读数据帧数
)
(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位
input iic_en, //iic使能信号
input [WMEN_LEN*8-1'b1:0] w_data, //iic设备的写入数据,其中w_data[7:0]应该是地址帧
input [7:0] wd_cnt, //iic写入数据的帧数
output reg [RMEN_LEN*8-1'b1:0] r_data, //iic读取数据存储
input [7:0] rd_cnt, //iic读取数据的帧数
output reg iic_busy, //iic工作标识,1为忙,2为空闲
//标准的iic设备总线
inout iic_sda, //iic总线的双向数据线
output reg iic_scl //iic总线的时钟线
);
parameter IDLE = 4'd0, //空闲状态
START = 4'd1, //开始
W_WAIT = 4'd2, //写状态
W_ACK = 4'd3, //写响应
R_WAIT = 4'd4, //读状态
R_ACK = 4'd5, //读响应
STOP1 = 4'd6, //结束1
STOP2 = 4'd7; //结束2
reg [3:0] iic_s; // 状态机状态
reg scl_clk; //分频时钟,通过对其移位获得iic串行时钟信号
reg iic_mode; // 设置iic数据线状态,1为输出(写),0为输入(读)
reg [2:0] bcnt; //比特计数
reg [7:0] wcnt; //写字节计数
reg [7:0] rcnt; //读字节计数
reg scl_r; //iic时钟信号寄存,时钟信号的来源包括两部分,一是iic空闲时主动拉高,二是每比特数据传输时拉高
reg sda_r; //iic数据线寄存,W_ACK时进行响应判断
reg sda_o = 0; //iic数据线,写数据
reg [7:0] sda_o_r; //iic数据线,写数据寄存器
reg [7:0] sda_i_r; //iic数据线,读数据寄存
reg [$clog2(DIV_CLK):0] clk_cnt; //分频时钟计数
// reg iic_busy;
reg rd_en; //读数据使能信号
//分频器对系统时钟进行分频处理,产生串行时钟信号
always @(posedge sys_clk or negedge sys_rst_n)
begin
if(!sys_rst_n) begin
clk_cnt <= 0 ;
scl_clk <= 0;
end
else if(clk_cnt == (DIV_CLK>>1) -1'b1)begin//保持50%占空比
clk_cnt <= 0;
scl_clk <= ~scl_clk;
end
else clk_cnt <= clk_cnt +'b1;
end
//
always @(*)
begin
if(iic_s == IDLE || iic_s == STOP1 || iic_s == STOP2) //主动拉升iic时钟线
scl_r <= 1'b1;
else scl_r <= ~scl_clk;
end
//使iic时钟线高电平保持在数据线传输每比特数据的中心
wire scl_offset = (clk_cnt == DIV_CLK>>2);
always @(posedge sys_clk) iic_scl <= scl_offset ? scl_r : iic_scl; //产生iic串行时钟
//iic状态机,首先发送地址帧
always @(negedge scl_clk or negedge sys_rst_n) //在scl_clk时钟下降沿改变状态,为了保证scl与sda的时序
begin
if(!sys_rst_n) begin
iic_s <= IDLE ; //状态机复位
iic_mode <= 1'b1; // 设置iic为输出
bcnt <= 3'd7; //iic每次先发送高位,因此初始化赋值为7,发送最高比特,然后递减
wcnt <= 0; //将已写入数据帧数标识置零
rcnt <= 0; //将已读出数据帧数标识置零
iic_busy <= 0; //iic空闲
rd_en <= 0; //
end
else
case(iic_s)
IDLE : begin // 空闲状态设置scl与sda均为1
if(iic_en || rd_en)begin
iic_s <= START; //接收到使能信号iic开始工作
iic_busy <= 1'b1; //iic忙
iic_mode <= 1'b1; // 设置iic为写入
end
else begin
iic_mode <= 1'b1; // 设置iic为输出
wcnt <= 0; //将已写入数据帧数标识置零
rcnt <= 0; //将已读出数据帧数标识置零
rd_en <= 1'b0;
iic_busy <= 1'b0;
end
end
START: begin
bcnt <= 3'd7; //数据从最高位开始传输
iic_s <= W_WAIT;
end
W_WAIT:begin
if(bcnt >0)
bcnt <= bcnt -1'b1; //计数减一,写入下一位数据
else begin
iic_s <= W_ACK;
wcnt <= wcnt +1'b1; //每帧数据写入完成后,帧计数器加一
iic_mode <= 1'b0 ; // 设置iic为读取,即从机向主机输入
end
end
W_ACK:begin
if(wcnt < wd_cnt) begin
iic_s <= W_WAIT;
// wcnt <= wcnt +1'b1;
bcnt <= 3'd7;
iic_mode <= 1'b1 ; // 下一个状态是W_WAIT,因此设置iic为写入,即主机向从机输入
end
else if(rd_cnt>0) begin //如果rd_cnt大于0表示iic是读模式
if(rd_en == 1'b0) begin //rd_en==0表明还未写入读控制字,跳转到IDLE状态进行读控制字写入
rd_en <= 1'b1;
iic_s <= IDLE;
iic_mode <= 1'b1 ;
end
else
iic_s <= R_WAIT ; //已写入读控制字,开始进行数据读取
bcnt <= 3'd7;
end
else
iic_s <= STOP1; //写入完成跳转到结束状态
if(sda_r !== 1'b0) //如果未接收到从机应答信号,停止发送
iic_s <= STOP1;
end
R_WAIT:begin //读取数据
rd_en <= 1'b0; //置零,及时释放
if(bcnt > 0) begin
bcnt <= bcnt -1'b1;
end
else begin
rcnt <= rcnt +1'b1;
iic_s <= R_ACK;
iic_mode <= 1'b1 ; // 设置iic为写入,输出应答信号
end
end
R_ACK:begin
if(rcnt < rd_cnt) begin
bcnt <= 3'd7;
iic_s <= R_WAIT;
iic_mode <= 1'b0 ; // 设置iic为读取
end
else
begin
iic_s <= STOP1;
end
end
STOP1:begin//sda = 0 scl = 1
iic_s <= STOP2;
end
STOP2:begin//sda = 1 scl =1
iic_s <= IDLE;
end
default:
iic_s <= IDLE;
endcase
end
//IIC总线的SDA数据线是一个双向IO口,使用三态门
assign iic_sda = iic_mode ? sda_o: 1'bz;
//sda输出
always @(*)
begin
if(!sys_rst_n || iic_s == STOP2)
begin //sda = 1
sda_o <= 1'b1;
end
else if(iic_s == START || iic_s == STOP1 || (iic_s == R_ACK && rcnt != rd_cnt))
begin //sda = 0
sda_o <= 0;
end
else if(iic_s == W_WAIT)
begin //输出最高位
sda_o <= sda_o_r[7];
end //其他状态数据线拉高
else sda_o <= 1'b1;
end
always @(negedge scl_clk)
begin
if(iic_s == W_ACK || iic_s == START)begin
sda_o_r <= rd_en?({w_data[7:1],1'b1}):(w_data[(wcnt*8)+:8]);
// sda_o_r <= w_data[(wcnt*8)+:8];
/*写、读模式下均存在wcnt=0的时候,此时w_data应该是地址帧,
即w_data[7:0]为地址帧,其中w_data[0]为模式,0代表写,1代表读*/
// if(rd_en) sda_o_r <= {w_data[7:1],1'b1};
end
else if(iic_s == W_WAIT) //移位寄存器
sda_o_r <= {sda_o_r[6:0],1'b1};
else
sda_o_r <= sda_o_r;
end
//暂存iic数据线的数据,用于检测从机应答
always @(posedge scl_clk) sda_r <= iic_sda;
//读模式,
always @(posedge scl_clk or negedge sys_rst_n)
begin
if(!sys_rst_n)
r_data <= 'b0;
else if(iic_s == R_ACK)
r_data[((rcnt-1)*8)+:8] <= sda_i_r;
else if(iic_s == R_WAIT || iic_s == W_ACK)
sda_i_r <= {sda_i_r[6:0],iic_sda};
else
sda_i_r <= 0;
end
endmodule
参考:
IIC总线解析(包含时钟拉伸,地址扩展,死锁,总线冲突总线仲裁的问题描述)
I2C详解
【接口时序】6、IIC总线的原理与Verilog实现