一、基本介绍
I2C协议(集成电路总线)使用两根线SDA和SCL实现数据传输,其连接如下图所示,总线上通过上拉电阻可以挂载各种低速外设,例如EEPROM 24C02
,传感器
等。
使用I2C,可以将多个从机(Slave)
连接到单个主设备(Master)
,并且还可以有多个主设备(Master)
控制一个或多个从机(Slave)
。
一、启动时序与时钟产生(START condition)
启动时序如下图所示,
在总线空闲时,SDA,SCL都处于高电平。而在启动I2C传输时,主设备先将SDA拉低,再将SCL拉低。开始标志之后,就开始传输数据,传输数据要求保证在SCL的高电平时期保持不变,否则会被误识别为停止标志或者其他标志。
假设SDA是sda_clk
的时钟下降沿发生变化,其中sda_clk
是I2C模块的内部时钟之一。带有sda_clk的参考时序图如下所示。
该图的绘制代码如下,可以在这个网站使用js进行绘制
{signal: [{name:'scl_clk',wave:'p.......|',data:['data1','data2','data3']},{name:'SDA',wave:'1.0====|.',data:['data1','data2','data3'],phase:-0.5},{name:'SCL',wave:'1.0p....',data:['data1','data2','data3'],phase:-0.75},
]}
如上图,以scl_clk
为基准参考,SDA是由该时钟生成的同步电路,并且我们就假设SDA都是在时钟下降沿发生变化。由于SCL需要满足I2C协议中的以下两个条件:
1.在启动时序中,SCL其拉低是在SDA之后的,而SDA拉低是在时钟下降沿(以上升沿为参考,下降沿的相位移动是phase:-0.5,遵循函数左加右减规则)
2.SCL的高电平区域在传输数据时不允许SDA发生跳变,即SDA的数据变化必须落在SCL的低电平区域
基于以上的要求,我们可以推导出时钟相关信号的生成步骤
时钟产生的逻辑是,首先对高速时钟进行1000分频(即添加500计数器clk_div
,每数到500就进行一次翻转),这样就生成了时钟scl_clk
;
//scl_clk生成(驱动sda)
always@(posege clk_i)if(clkdiv < CLK_DIV)clkdiv=clkdiv+1'b1;else beginclkdiv=16'd0;scl_clk<=!scl_clk;end
接着,根据scl_clk
生成下降沿转换的状态机IIC_S
。SDA由状态机的状态组合逻辑产生,也是在scl_clk
下降沿跳变。
由于SCL
需要满足两个条件(上文提到)。我们先产生与时钟下降沿对齐的scl_r
中间信号,然后再进行调整。scl_r的逻辑就是在IDLE
或者STOP1/2
这些状态为高,其余状态为scl_clk
的取反。
//scl_r生成逻辑,IIC_S为状态机状态
always@(*)beginif(IIC_S==IDLE || IIC_S ==STOP1 || IIC_S ==STOP2)scl_r<=1'b1;elsescl_r<=~scl_clk;
为了满足两个条件,我们发现只要把scl_r
略微右移就可以实现。为了实现略微右移,我们先生成scl_offset
信号。
parameter OFFSET=CLKDIV-CLK_DIV/4;
wire scl_offset=(clkdiv==OFFSET);
scl_clk
是在clk_div==CLKDIV时候翻转,而scl_offset
比其早了0.25个CLKDIV计数,其关系如上图所示。
我们基于scl_offset
对scl_r
进行滞后采样,根据图上关系所示,从而生成SCL
波形(iic_scl
),这样可以满足两个条件
iic_scl<=scl_offset?scl_r:iic_scl;
二、停止时序(STOP condition)
I2C协议中对停止时序的要求是,SCL先拉高,然后SDA再拉高。这个时序刚好与启动时序相反。由于上一节中提到,SCL是scl_r滞后采样得到的,因此需要SCL率先拉高,就需要scl_r比SDA率先拉高。因此可以将STOP状态定义为两个状态,STOP1和STOP2.
如果只有一个停止·状态(例如只有stop1),那么SDA就要在STOP位置高,如上图SDA_wrong所示,而scl_r也同样在此时置高,SCL滞后于scl_r,这样就会导致SCL比SDA_wrong更晚拉高,不满足停止时序所要求的SCL率先拉高。
因此我们分出两个停止状态STOP1,STOP2,将scl_r现在STOP1就拉高,而将SDA在STOP2再拉高,这样一来就满足了SCL先于SDA拉高的时序,并且两者拉高的间隔小于一个scl_clk时钟周期(由于滞后采样的原因)
三、状态机分析(基于EEPROM应用)
以上只是简单写一个包(8bit)的状态演示,其状态还不够完备。
由于是想要用于驱动E2PROM的,因此有些完备状态的补充,我们需要参考E2PROM的用户手册
字节写
如下图所示,为字节写的时序,本质是实现了连续三次的写数据。首先是开始标志,标志着I2C传输的开始。然后紧接着是八位指令
该指令构成如下
其中1010是该器件24C02内部规定好的,用于标志总线的设备类型,其实属于地址的一部分,用来区分不同的设备。然后后面紧接着的A2 to A0是器件的地址,可以在器件的这三个引脚给出高低电平来设置某个EEPROM 24C02器件的地址。最后一位是度或者写指令,其中为0表示写为1表示读。
以上就是指令的构成格式。指令结束后,经过一位ACK,然后主机在总线上写入连续8位(第二个包,指令是第一个包)作为写入E2PROM的地址,然后再经过一位ACK,主机在总线上写入连续8位(第三个包)作为数据,最后经过一位ACK产生停止标志,这样就完成了对特定地址的写操作。
因此,从字节写操作中,我们发现对状态的要求有:在一个包内,需要能用重复发8bit数据。此外包与包之间是连续发送,仅隔了一个ACK,并没有经历新的START。所以发完一个包并不能进入STOP和IDLE状态。因此我们设计满足字节写操作的状态机如下图所示。
连续写(写一页(8BYTE))
是写一个字节操作的扩展,可以发现上述状态机仍然满足连续写,只是再下一个包的循环中多循环几轮的差别,我们可以再状态转移逻辑中对其进行设置,例如设置一个写包计数器,来表征需要连续写入字节的个数,计数器归零后发送停止条件。
读当前字节
EEPROM内部是有指针的,内部计数器指向上一个操作地址的下一地址空间。
如时序图所示,主设备写入读命令后,接收到EEPROM的一个ACK回答,然后连续在总线上读取8位数据地址(其中高位在前),主器件接收完毕后,无需回送ACK,只需要发送停止标志即可。
读随机字节
随机读,即读特定地址的一个字节的数据,时序如上图所示。主机先在总线上写1Byte的指令来表征要进行写操作,紧接着写需要读的8位地址。然后再重新给出起始条件,转为在总线上写1Byte的指令来表征要进行的读操作,然后再读取地址。
连续读字节
连续读可以理解为读的扩展,不管是直接读还是随机读,都可以发送ACK来继续下一次读取,EEPROM内部计数器会自动加一。
综上所述,其实主机发送的指令其实只有两种,分别是读指令和写指令,
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|
1 | 0 | 1 | 0 | A2 | A1 | A0 | W/R |
其中R为1,W为0.由于对应某一个特定的EEPROM器件,其指令高7位地址都是相同的,因此主机对其发送的指令只有最低位为1(读)和最低位为0(写)这两种指令。每次主机发送一种指令都需要一个起始位。因此读写转换,写读转换都需要重新发送起始位。
我们以随机读为例,随机读相对于直接读,前面多了俩字节,而后面的两字节(主机往总线上写入读指令,主机接收读到的数据)是相同的。前面的两字节是往总线上写入写指令和写地址,我们比较一下写字节指令,只给出了要写的地址,并未给出要写的数据,因此它可以改变内部指针计数器,而并未真的写入,这样就实现了随机读的功能。
基于以上分析,就有了下述状态机的设计。