Opencores上的i2c controller core代码解析
一、i2c_eeprom_test.v
//TO DO…
二、i2c_master_top.v
i2c_master_top.v文件比较简单,写了一个三段式状态机。状态转移图如下:
//TO DO…
三、i2c_master_byte_ctrl.v
//TO DO…
四、i2c_master_bit_ctrl.v
4.1 四类比特
作者把传输过程中的比特分为四类:第一类是起始比特,第二类是停止比特,第三类是写比特,第四类是读比特。每一类比特都包含了SCL和SDA。按照这样分类的话,一次写寄存器的过程就是:
起始比特–写比特7–…--写比特0-读比特-写比特7–…--写比特0–读比特–写比特7–…--写比特0–读比特–停止比特。
其中,第一串写比特是写器件地址,第二串写比特是写寄存器地址,第三串写比特是写8位寄存器的值,读比特是读从机的ACK信号。
4.2 比特时钟和SCL
对于每一类比特,作者将它们的长度分为5个时钟周期,每个时钟周期内对SCL和SDA进行变化,也就是说作者为每一类比特设置了5个状态,时钟信号是状态转移的条件。而且这四类比特共用一个时钟。其中SDA的变化取决于输入信号din,也就是上层i2c_master_byte_ctrl.v的移位寄存器的最高位输入。
假设晶振为50MHz,I2C的时钟SCL是400kHz,一个数据比特长度为一个SCL周期,也就是2.5us=2500ns(这样说并不准确,因为I2C中是SCL高电平时SDA保持稳定,SCL为低电平时SDA进行变化,并没有“传输一个数据比特需要多少个时钟周期”的说法。UART这种异步通信才有。但这里为了方便理解,先这么描述)。那么每一类比特的时间长度就是25000/5=500ns(2MHz),也就是说我需要将50MHz的系统时钟分频为2MHz的时钟,这个时钟姑且称为比特时钟,在i2c_master_bit_ctrl.v中对应变量clk_en。比特时钟是SCL的5倍,所以作者在i2c_master_top.v的模块端口声明中说明了分频系数的计算是(50,000,000/(5xSCL))-1。
每一类比特包含SCL和SDA,SCL有两个周期为高电平,三个周期为低电平,所以这样得到的SCL的正负占空比是2:3。换个说法,作者把SCL的每个周期分为5份,通过每类比特中的状态转移来产生SCL的高低电平。这就实现了SCL的波形产生。
4.3 比特时钟clk_en的产生
clk_en并不是简单的分频,代码如下。可以看到clk_en的生成包含两部分,一部分是拉高,另一部分是拉低。先说拉高的部分。rst和ena是输入信号,rst为0,ena为1。假设scl_sync为0,那么当cnt计数到0时,条件判断为真,clk_en拉高,cnt重新置数。再说拉低部分。两种情况下clk_en会被拉低,第一种情况是当从机还没准备好的时候,从机可以拉低SCL,让主机继续等待,直到从机释放SCL,这时slave_wait为1。另一种情况就是cnt递减的过程中clk_en保持为低。通过分析可以看出,clk_en只有一个系统时钟周期的时间为高电平,其余时间均为低电平。
always @(posedge clk or negedge nReset)
if (~nReset)
begin
cnt <= #1 16'h0;
clk_en <= #1 1'b1;
end
else if (rst || ~|cnt || !ena || scl_sync)
begin
cnt <= #1 clk_cnt;
clk_en <= #1 1'b1;
end
else if (slave_wait)
begin
cnt <= #1 cnt;
clk_en <= #1 1'b0;
end
else
begin
cnt <= #1 cnt - 16'h1;
clk_en <= #1 1'b0;
end
4.4 SCL和SDA的高低控制
作者在i2c_master_bit_ctrl.v中将输出scl_o和sda_o都接地了,将信号scl_oen和sda_oen作为选择信号提供给最顶层i2c_eeprom_test.v,对顶层最终的scl和sda输出进行控制。以时钟线为例,当scl_oen为高时,顶层的scl输出高阻态,由于实际硬件电路中时钟线通过上拉电阻接到电源端,所以顶层的scl会被拉高,也就是高电平。所以作者并不是通过给scl赋值0和1来产生最终的SCL,而是通过数据选择的方式,要么为0,要么为高阻(然后被上拉为1)来产生SCL的。这一点很特别。
4.5 该.v文件剩下的部分
我觉得i2c_master_bit_ctrl.v可以分成两部分。第一部分就是前面四点提到的SCL的产生和SDA的变化。第二部分就是一组信号(cSCL/fSCL/sSCL/dSCL和cSDA/fSDA/sSDA/dSDA)及相应的功能。这部分我尝试用modelsim仿真观察波形,但是因为没有实际电路的上拉电阻的模型以及从机的响应,所有的高阻态都只能是高阻态,进而导致了诸多信号为不确定态,无法具体分析。所以我暂时把我理解的记录一下。
首先是scl_i和sda_i信号,其实就是把顶层的SCL和SDA接进来,是相同的信号。然后是cSCL/cSDA和fSCL和fSDA。以cSCL和fSCL为例进行分析。它们构成的是一个五位的移位寄存器,如图
其中clk可以看作125xSCL,clk2是125/7xSCL(具体的计算要看clk2的产生。clk2是clk的分频,分频系数是clk_en的分频系数减一后除以4,再加一得到的。以SCL=400kHz为例,clk_en的分频系数是50,000/(5x400)=25,频率是2MHz,那么clk2的分频系数就是(25-1)/4+1=6+1=7,所以频率是50/7MHz,约等于7.1MHz),或者说clk2是clk的7分频。
假设t时刻(以ns为单位)clk和clk2的上升沿同时到达,则cSCL[0]存储的是t时刻的SCL电平,cSCL[1]存储的是t-20时刻的SCL电平,fSCL[0]存储t-2x20时刻的SCL电平,fSCL[1]存储t-2x20-7x20=t-9x20时刻的SCL电平,fSCL[2]存储t-2x20-2x7x20=t-16x20时刻的SCL电平。事实上clk2从clk分频出来并没有相移,所以理论上clk2的上升沿跟clk的上升沿是同时到达的。
cSCL[0] t ns
cSCL[1] t-20 ns
fSCL[0] t-40 ns
fSCL[1] t-180 ns
fSCL[2] t-320 ns
通过以上分析可以知道,cSCL[1:0]和fSCL[2:0]相当于一个采样窗口,在这个窗口里对SCL进行间隔为140、140、20、20共5次采样,窗口每次向右(时间增大的方向)移动20。这个东西作者叫做滤波,怎么个滤波法呢?
// generate filtered SCL and SDA signals
always @(posedge clk or negedge nReset)
if (~nReset)
begin
sSCL <= #1 1'b1;
sSDA <= #1 1'b1;
dSCL <= #1 1'b1;
dSDA <= #1 1'b1;
end
else if (rst)
begin
sSCL <= #1 1'b1;
sSDA <= #1 1'b1;
dSCL <= #1 1'b1;
dSDA <= #1 1'b1;
end
else
begin
sSCL <= #1 &fSCL[2:1] | &fSCL[1:0] | (fSCL[2] & fSCL[0]);
sSDA <= #1 &fSDA[2:1] | &fSDA[1:0] | (fSDA[2] & fSDA[0]);
dSCL <= #1 sSCL;
dSDA <= #1 sSDA;
end
sSCL是滤波后的信号。可以看到sSCL只取决于fSCL,fSCL总共3个位,有8个组合,只有当fSCL有两个1时sSCL才为1。而且它比较的顺序在写法上似乎有讲究(从功能上来讲无差别),因为fSCL三个比特有时间顺序之分,作者先比较fSCL[2]和fSCL[1],也就是320ns前的SCL和180ns前的SCL,如果它们为1,就说明当前的SCL为高,那么令sSCL为高;否则,比较fSCL[1]和fSCL[0],如果它们为1,说明当前时刻的SCL为高;再否则,比较SCL[2]和SCL[0],也就是320ns和40ns前的SCL,如果它们为1,则当前时刻的SCL为高。如果前面两个判断都为假,而最后一个判断为真,那么说明SCL依次出现了 高低高(101) 的情况,可以认为0是受噪声扰动导致的。
fSCL[2] fSCL[1] fSCL[0] sSCL
0 0 0 0
0 0 1 0
0 1 0 0
0 1 1 1
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1
但是我比较疑惑的是 001 和 110 这两组。001说明320ns前、180ns前的SCL为低,40ns前的SCL为高,那么为什么认为此时的SCL为低而不为高呢?同理 110 的情况下为什么认为SCL是高非低呢?这是我一直想不通的地方。作者把这个叫做滤波,我又完全匹配不上计算机控制系统课程中学习的任何离散时间域的滤波算法,迷惑。
通过上面的分析可以知道滤波后得到的sSCL相比实际的SCL会有延迟,不过因为这一路下来我们是以SCL为400kHz来分析,也就是时钟周期为2500ns,这些延迟带来的误差应该是在允许范围内的。
dSCL就没有理解难度了,就是上一个滤波后的SCL的电平。
作者用sSCL和dSCL来干嘛呢?第一个是用来判断起始沿和停止沿。一开始我没懂这个判断有什么意义:起始和结束信号是自己产生的,自己还反过来去检测什么时候起始和结束。但后来想了一下,起始和停止信号被当作起始比特和停止比特划分成了5个状态来实现,当处在这5个状态中的任何一个状态时,是无法对沿进行判断的(SCL和SDA只有00/01/10/11四种状态,无法检测“SCL为高时SDA由高变低”),所以作者才需要在这个状态机之外又重新进行判断,而且还是经过滤波后才判断的。得到起始沿和停止沿就可以对外输出busy信号。
剩下的还有一些信号我不理解,比如scl_sync信号,al信号和sda_chk信号。
五、其他
//TO DO…