(verilog)IIC协议详讲与实现
相比与SPI与UART,IIC协议非常优雅,因为它在非常轻的基础设施上提供了非常高级的功能,例如自动多主机冲突处理和内置寻址管理。然而,由于其功能上的复杂安全,在传输性能上会有所缺失。
一、原理介绍
IIC是英文Inter-Integrated Circuit的缩写,直译为内部集成电路,I2C总线于1982年由飞利浦公司开发,它最初的目的是提供一种将CPU连接到电视机外围芯片的简单方法。只需要两根导线就可以将所有外围设备连接到微控制器。原始规范定义了100 kb/s的总线速度,后续引入了400 kb/s和 3.4 Mb/s。
IIC总线上只有两根信号线,串行数据线SDA和串行时钟线SCL,两根主线均为双向主线,主机和从机都可以控制主线,所有从机和主机都挂在总线上,同时总线还外接上拉电阻。

IIC是半双工的通信协议,因为只有一根数据线SDA,所以只能进行读数据或是写数据。IIC是多主多从通信协议,主机和从机可以相互转换,先发起请求的作为主机,收到主机发出的请求后响应的机器作为从机。常见的其他串行通信协议中,UART是单主单从全双工通信协议,在读数据的同时也可以进行写数据。SPI是单主多从全双工通信协议。
IIC是同步通信协议,所有主机都挂在SCL时钟总线上,当主线空闲时SCL信号始终为高,当有主机占用主线时,SCL时钟信号开始翻转,为主机与从机之间的数据传输提供同步时钟。 时钟速率必须在100 kb/s、400 kb/s和3.4 Mb/s之间选择,分别称为标准模式、快速模式和高速模式。一些I2C变体也有10 kb/s(低速模式)和1 Mb/s(快速模式+)的时钟频率。常见的其他串行通信协议中,UART是异步通信协议,SPI是同步通信协议。
SDA控制
IIC协议规定,SDA只能在SCL为低时变化,SCL为高时保持不变。如果在SCL为高的时候,SDA发生变化,被认为是start 和 stop 标志的产生。如下图所示,没有主机占用主线的时候,SCL信号为高,当有主机需要占用主线时,主动拉低SDA信号,即视为产生 start 标志。当数据传输结束后,主机在SCL为高时拉高SDA信号,视为产生 stop标志。

仲裁
由于IIC独特的物理结构,使得它不需要额外的仲裁电路模块,只通过总线本身就能完成多主机之间的仲裁,这也是IIC协议真正优雅的地方。在物理层上,SCL和SDA线路都是带上拉电阻的开漏I/O,将这样的线路拉到地上被解码为逻辑0,而释放线路让其浮动为逻辑1。
如果多个设备尝试在SDA线路上写入同样的逻辑,不会有任何冲突,SDA线路变为相应的逻辑,但是如果有设备试图写入逻辑0,有的设备试图写入逻辑1,则只有0被成功写入,也就是说,当出现多个主机同时向总线发送请求时,他们发送的前几位相同的信号都会成功写入总线,所有设备也都可以接收到数据,直到占用总线的主机们出现分歧时,一个要写入0,另一个要写入1,则主线变为0,写入0的主机继续发送数据,写入1的主机失去同步,转为从机接收主线上的数据。
由于前几位相同数据都被成功发送,不会发生数据丢失,而且写入0的主机甚至不会意识到自己发生过竞争的事情,只有失去同步的主机才会意识到自己参与过竞争,他们会在本次总线产生 stop 之后再次尝试发起请求。
数据位规定
IIC标准协议规定数据传输格式为下图所示,

第一个字节中的数据,前 7bit 为id寻址位,每个支持IIC协议的设备都会有一个id号,根据其出产厂家不同而不同,为了放置相同的设备id号一样,设备的id号低三位(A1,A2,A3)可以自定义,这样一个总线上可以挂8个相同的设备。
第一个字节的最低位为读/写识别位,为0表示写,为1表示读。
第二个字节中为要传输的数据,有的设备中有多个寄存器,所以还需要多1个或2个字节的address,同样的data也可以为两个字节。
当主机拉高start信号,所有从机收到start信号,开始接收SDA上的数据,第一个字节的id接收后与自己的id对比,如果相同,则返回一个ack信号,主机收到ack信号后继续发送数据,从机收到后再返回一个ack,主机收到后产生stop信号并释放总线。其他未参与通信的从机全程接收SDA数据直到收到stop信号。
写模式时,两次ack均为从机发送给主机,读模式的时候,数据由从机发送给主机,所以第二个ack为主机发送给从机。ack信号为1代表有效,为0代表无效(也可以设置为低有效),当收到无效信号时,发送stop信号,结束本次通信,稍后再次重新发起。
伪握手机制
由于IIC独特的物理结构,还可以通过SCL进行主机与从机之间的伪握手,即从机收到id后,由于内部寄存器还没准备好,可以通过主动拉低SCL,使主机被迫等待,直到从机准备好后,拉高SCL时钟线,继续通信。
其他
IIC是一个规定很多的成熟的通信协议,因此还会有很多额外的功能模式,例如10bit寻址模式,快速模式,高速模式等。另外,第一个id寻址字节中的数据全为1,或全为0等特殊字节还有额外的功能,类似TCP-IP协议中的广播和ARP请求。
下面的链接是IIC的协议标准中文版,读者如有需求或想更详细的了解IIC协议,请自行下载。
链接:https://pan.baidu.com/s/1lfK7diD1Imv4vWy-4FpvBA?pwd=jgc2
提取码:jgc2
连接有效期30天,失效后评论区补。
二、verilog实现
我们设计一个7bit 寻址模式的IIC设备模块,功能框图如下所示:

系统内部总线发来 command(寻址id),data(发送的数据),req(发送指示),模块接收到req后,判断总线状态,等待总线空闲状态时发送start信号开始通信。读过来的数据通过dout_reg缓存,再通过dout发送到内部总线。当模块作为slave时,写模式数据写进内部寄存器inner_data中,读模式把内部寄存器inner_data中的数据读出到SDA总线上。
SCL频率为 100K 的标准模式。
状态机跳转如下图

在idle状态下,如果IIC总线处于空闲状态,且收到了内部发来的请求req信号,作为master进入send状态,根据读写指示位进入读或写状态。
在idle状态下,如果没有内部请求req信号,收到了start信号,则作为slave进入busy状态,如果id检测没有与自己的id匹配,就维持在busy状态,直到stop信号后,进入idle状态。
如果id检测与自己的id匹配,则根据读写指示位进入读写状态,需要注意的时,此时的读写是根据主机的角度来判断,如果读写指示位是0,是主机的写状态,则作为从机需要进入读状态,即sread状态。
需要注意的是,由于SDA和SCL是inout双向信号,在FPGA编译软件中会直接被综合成IOBUF模块,通过三态门来实现对IO口的控制。
I口为从FPGA输出的信号,O口为输入给FPGA的信号,IO口为与外部双向io信号连接的接口,T可以理解为选择信号,当T为1时,IO与I相连,IO口向外部输出FPGA想要输出的信号;当T为0时,I口变为高阻态,IO与O相连,IO向内部输入信号。
我们在FPGA内部使用的时候可以使用ip核,在top顶层例化的方式与SDA信号相连:
IOBUF IOBUF_inst (
.datain ( datain_sig ),
.oe ( oe_sig ),
.dataio ( dataio_sig ),
.dataout ( dataout_sig )
);
也可以用force 和 release语句来使IO口作为输入端口或者释放IO口作为输入端口。
或者用一个使能信号来做选择(IOBUF中的T口),用assign语句选择输入和输出,做输入的时候就令SDA为高阻态。
IIC模块中
assign iic_SDA = io_en ? iic_SDA_reg : 1'bz;
top顶层中
assign iic_SDA_top = (!io_en)? ack : 1'bz;
下面是我们本次实现的代码详情:
端口声明与内部信号
module IIC#(parameter id0 = 1'b0,id1 = 1'b0,id2 = 1'b1)(
input CLK_50M,
input rst_n,
input [7:0] data,
input [7:0] command,
input req,
inout iic_SLC,
inout iic_SDA,
output reg [7:0]data_out,
output reg finish,
output reg io_en
);
//iic控制
reg [8:0] SLC_cnt;
reg cnt_flag;
reg [4:0] n_cnt;
reg n_cnt_flag;
reg iic_SDA_reg;
reg iic_SLC_reg;
//外部输入缓存
reg [7:0] data_reg;
reg [7:0] command_reg;
reg req_reg;
//输出数据缓存
reg [7:0] data_out_reg;
//内部需要被访问的寄存器
reg [7:0] inner_data;
//状态信号
reg busy;
wire send;
wire start;
wire stop;
//id寻找
wire match_flag;
//ack错误
reg error;
//状态机
reg [6:0]cur_state;
reg [6:0]next_state;
localparam s_idle = 7'b000_0001;
localparam s_send = 7'b000_0010;
localparam s_mwrite = 7'b000_0100;
localparam s_mread = 7'b000_1000;
localparam s_busy = 7'b001_0000;
localparam s_swrite = 7'b010_0000;
localparam s_sread = 7'b100_0000;
//id序列检测
reg [7:0]id_state;
localparam id_idle = 8