学习i2c心得
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
I2C学习心得
我最近刚做完I2C通信协议的编写与调试,下面介绍一下我从一开始理解夏老师的程序,修改程序,直到下板调试整个的学习过程,希望对大家学习I2C有一定的帮助。
一、分析源代码
学习I2C,首先我们要知道I2C是一种串行接口,I2C通信是一种串行通信。
在实际的数字系统中,我们的数据经常是以并行的方式产生及存储的。
而将数据通过进行传输时,通常会是串行地发送和接收的。
比如我们常见的SPI,UART,
I2C,USB,SATA等接口,均是串行接口。
因此,在数字系统中我们经常会遇到需要将串行数据接收下来转为并行数据存储,或者是将并行数据转换成串行数据发送出去的情况。
说白了,就是发送端要将数据排个队,一个一个地往外蹦,接收端接收到了这些数据又要将它们像串糖葫芦一个一个串起来,成为并行的数据。
那么我们要做的I2C协议,其实就是按照I2C总线协议的要求,将本地的数据串行地发送出去,或者将外部的数据串行地接收回来的这么一个过程。
由于以前从未搞过I2C方面的工作,我的第一步是从理解夏老师的程序开始的,通过浏览I2C设计实例,可以知道要设计一个I2C通信程序,我们需要一个主机和一个从机,如下图1所示,它们之间采用I2C协议进行串行通信,设计实例中的signal模块和EEPROM_WR模块是用来模拟主机发送方的,EEPROM_WR模块是一个可综合的EEPROM读写器模型,它通过SCL和SDA两根线与EEPROM器件进行通信,并且SCL与SDA上的信号必须满足I2C通信协议的要求。
EEPROM模块只是EEPROM的行为级模型,signal模块与EEPROM模块都是为了仿真需要而存在的。
先简单的介绍一下I2C总线特征。
只有在总线处于“非忙”状态时,才能开始数据传输。
在数据传输期间,只要时钟线为高电平,数据线都必须保持稳定,否则数据线上的任何变化都被当作“启动”或“停止”信号。
图1是总线状态的定义。
(1)总线非忙状态(A 段):数据线SDA 和时钟线 SCL 都保持高电平。
(2)启动数据传输(B 段):当时钟线(SCL)为高电平状态时,数据线(SDA)由高电平变为低电平的下降沿被认为是“启动”信号。
只有出现“启动”信号后,其它的命令才有效。
(3)停止数据传输(C 段):当时钟线(SCL)为高电平状态时,数据线(SDA)由低电平变为高电平的上升沿被认为是“停止”信号。
随着“停在”信号出现,所有的外部操作都结束。
(4)数据有效(D 段):在出现“启动”信号以后,在时钟线(SCL)为高电平状态时数据线是稳定的,这时数据线的状态就要传送的数据。
数据线(SDA)上的数据的改变必须在时钟线为低电平期间完成,每位数据占用一个时钟脉冲。
每个数传输都是由“启动”信号开始,结束于“停止”信号。
(5)应答信号:每个正在接收数据的从机EEPROM 在接到一个字节的数据后,通常需要发出一个应答信号。
而每个正在发送数据的EEPROM 在发出一个字节的数据后,通常需要接收一个应答信号。
EEPROM 读写控制器必须产生一个与这个应答位相联系的额外的时钟脉冲。
在EEPROM 的读操作中,EEPROM 读写控制器对
EEPROM 完成的最后一个字节产生一个高的应答位,这叫做非应答信号,随后给EEPROM 一个结束信号。
了解完了I2C的总线特征,我们就可以进一步地了解夏老师的I2C设计。
首先介绍核心模块——EEPROM_WR模块,该模块的难点在于对状态机的编写,图2、3分别是AT24C02/4/8/16字节写入帧格式和读指定地址存储单元的数据帧格式,简单分析一下字节写入格式:第1位启动信号,接下来的第2-9位是控制字节写入,其中2-5位是固定的机器码1010,6-8位是页地址,第10位是EEPROM 给出的应答信号0,第11-18位是把存储地址,19位又是EEPROM给出的应答信号0,第20-27位是写入的数据,28位应答信号,29位停止信号。
而
AT24C02/4/8/16的字节读取格式也是大同小异,在写完地址后,又加入了启动信号与控制字节信号,这时,控制字节的第8位变为了1(读取),然后是读取数据,并且在读取完毕后,主机将SDA拉高作为非应答信号。
最后是停止位。
可能有人还是对这种字节写入与读取格式不明白,其实这种格式是别人定好的,我们所需要的就是要让SDA与SCL的信号满足这种格式要求即可,下面我们来看夏老师的EEPROM_WR程序。
图2:24C02/4/8/16字节写入帧格式
图3:24C02/4/8/16读指定地址存储单元的数据帧格式
首先是一堆输入输出、寄存器的定义,我们读程序的时候大可先不看这些,等到后面有需要的时候再回过头来看这些定义,这里需要注意的是SDA与DATA的
类型,都是inout型,SDA我们很容易理解,因为主机和从机都会给SDA线上发信号,比如字节写入格式时,主机先给SDA发了1个8位数据,然后作为应答位,从机要把SDA拉低,表示我已经接受到你的信号了,在应答位时主机是不能操作SDA 线的。
然而DATA设定为inout型,我们可以看到图1,其实在字节写入格式时,signal模块传输给EEPROM_WR模块,作为要写入的数据。
而在字节读取格式时,EEPROM通过SDA给EEPROM_WR传输数据,其实跟DATA是没有关系的,这里可以只将DATA设定为input型,设定为inout型是因为后续的程序会将EEPROM_WR读取到的数据发给signal,与signal当初发送的数据进行比较,检验通信是否正确。
如果将比较数据这程序操作放在EEPROM_WR模块中,就可以将DATA设为input。
提到inout类型,还得再多啰嗦两句,inout,顾名思义,双向口既能作为输入又能作为输出,可以节省管脚,在具体实现上一般是用三态门来实现,图4就是用三态门实现的sda总线的示意图。
图4:sda总线示意图
link_sda和out_flag分别为EEPROM_WR和EEPROM控制三态门输出的开关,当link_sda打开,out_flag关闭时,主机向SDA传数据,从机接受SDA的数据,此时,SDA对于主机来说就是output,对于从机来说就是input。
反之,当
link_sda关闭,out_flag打开时,主机通过SDA从从机读取数据。
当link_sda与out_flag都关闭时,SDA就被置为高阻状态。
下面来看程序中对于SDA和DATA三态门的描述。
assign sda1 = (link_head) ? head_buf[1] : 1'b0;
assign sda2 = (link_write) ? sh8out_buf[7] : 1'b0;
assign sda3 = (link_stop) ? stop_buf[1] : 1'b0;
assign sda4 = (sda1 | sda2 | sda3);
assign SDA = (link_sda) ? sda4 : 1'bz;
assign DATA = (link_read) ? data_from_rm : 8'hzz;
这是程序中应用三态门对双向口的实现,我们可以看到引入了多个开关,link_head、link_write、link_stop、link_sda、link_read,其中link_sda和link_read这两个开关是最主要的,当link_sda =1,即link_sda打开时,将
sda4输出,SDA=sda4。
当link_sda =0时,将SDA置为高阻。
同时sda4也是由三个开关控制的,为sda1、sda2、sda3三个信号相或的结果,这里是将EEPROM_WR 的状态分为了三个基本的状态,分别是启动状态、写入状态以及停止状态,这是根据EEPROM对SDA总线输出的值区分的。
对照图2、图3,其实意思就是在启动的时候给SDA一个下降沿,停止时给SDA一个上升沿,传输数据时给SDA一个有效数据。
引入这些开关是为了看得更加直观。
当然,我们也可以不引入这么多开关,只需要link_sda和link_read,在每次link_sda打开的时候我们都给sda4赋新值即可。
下面我们来看产生串行时钟SCL的程序:
always @(negedge CLK)
if(RESET)
SCL <= 0;
else
SCL <= ~SCL;
这条产生SCL的程序也是十分巧妙的,本文中的程序是用CLK的下降沿来触发SCL时钟,用CLK的上升沿来触发SDA,这样SDA与SCL就相差了半个SCL周期,目的是为了满足I2C总线上的传输协议,具体见图5.
图5:SCL与SDA基本时序关系
从图5中,我们可以看出,启动位占了一个SCL时钟周期,1时刻拉高,2时刻拉低,保证在SCL高电平时SDA有下降沿,这样就是启动信号,同理,如果我们要传输数据时,3时刻将传输的数据赋给SDA,比如要传输的数是1010,3时刻将SDA拉高,直到完成1个SCL周期时再把下一个值0赋给SDA。
这样就是一个SCL 与SDA基本的时序关系,我个人的理解是这样的:首先,要保证图2、3中字节读写格式的每一位都要占一个SCL时钟周期。
其次,每个CLK上升沿时将数据传给SDA,比如1:SDA<=1;2:SDA<=0。
这样才能保证时序的一致性。
接来下我们看到的就是主状态机程序,由图2、3列出的字节读写格式,我们可以将读写操作分为一共11个状态,状态图如下。
图6:读写操作状态转移
我们依次来看到各个状态。
Idle:
begin
link_read <= NO; link_write <= NO;
link_head <= NO; link_stop <= NO; link_sda <= NO; if(WR)
begin
WF <= 1;
main_state <= Ready ; end
else if(RD)
begin
RF <= 1;
main_state <= Ready ; end
else
begin
WF <= 0;
RF <= 0;
main_state <= Idle;
end
end
Idle:主要就是说当我们的读写控制器收到读/写指令时,要跳到Ready状态,准备往SDA上进行操作。
Ready:
begin
link_read <= NO;
link_write <= NO;
link_stop <= NO;
link_head <= YES;
link_sda <= YES;
head_buf[1:0] <= 2'b10;
stop_buf[1:0] <= 2'b01;
head_state <= head_begin;
FF <= 0;
ACK <= 0;
main_state <= Write_start;
end
Ready:一些初始化赋值语句,此时SDA<=head_buf[1],也就是将SDA拉高,准备启动。
Write_start:
if(FF == 0)
shift_head;
else
begin
sh8out_buf[7:0]<= {1'b1,1'b0,1'b1,1'b0,ADDR[10:8],1'b0};
link_head <= NO;
link_write <= YES;
FF <= 0;
sh8out_state <= sh8out_bit6;
main_state <= Ctrl_write;
end
Write_start:首先一开始FF=0,就进入任务shift_head,shift_head任务就相当于图5的时刻1与时刻2的操作,先将sda拉高,再拉低,并且通过if (scl)来保证在scl高电平期间,sda有下降沿。
而在shift_head这个任务里,我们可以发现除了时刻1、2外,还有一个状态head_end,其实我认为head_end 是可以省去的,启动时我们要做的就是两件事:1时刻sda拉高,2时刻sda拉低,进入控制字节写入环节。
并且在由于时刻2中FF置1,所以从来没有进入过head_end这个状态,读者在modelsim仿真中也可以进行验证。
这里还要多说一句的是那个else语句,此时应该图5中的时刻3了,此时sh8out_buf[7]=1,同时link_sda打开,意味着我们已经将sda置1了,这与
sh8out_state<= sh8out_bit6这条语句要结合起来看,要传输的8个数据我们已经给发送了一个了,下面就要从第二个数据开始发,所以下一步要做的是在时刻4发送第二个数。
这也就是为什么sh8out_state不从sh8out_bit7开始读第一个数了。
Ctrl_write:
if(FF ==0)
shift8_out;
else
begin
sh8out_state <= sh8out_bit7;
sh8out_buf[7:0] <= ADDR[7:0];
FF <= 0;
main_state <= Addr_write;
end
Ctrl_write:首先执行任务shift8_out,将控制字依次赋给SDA,8位的字节写完以后,将link_sda关闭一个SCL时钟周期,这个时钟周期是由从机给SDA置0,作为应答位。
将FF置1,跳出任务,执行else语句,准备地址写入的操作,此时我们看到sh8out_state <= sh8out_bit7,意味着下一次的地址写入是从
sh8out_bit7状态开始的,为什么与上一次的sh8out_state<= sh8out_bit6有区别,这是为了满足时序的要求,读者可以按着上面的思路自己画个时序图分析一下。
Addr_write:
if(FF == 0)
shift8_out;
else
begin
FF <= 0;
if(WF)
begin
sh8out_state <= sh8out_bit7;
sh8out_buf[7:0] <= DATA;
main_state <= Data_write;
end
if(RF)
begin
head_buf <= 2'b10;
head_state <= head_begin;
main_state <= Read_start;
end
end
Addr_write:写入地址,并且判断要进行的是写操作还是读操作。
如果是写操作,主状态机跳到Data_write,如果是读操作,主状态机跳到Read_start,并且准备启动信号。
Data_write:
if(FF == 0)
shift8_out;
else
begin
stop_state<= stop_begin;
main_state <= Stop;
link_write <= NO;
FF <= 0;
end
Data_write:写入8位数据,主状态跳到Stop。
Read_start:
if(FF == 0)
shift_head;
else
begin
sh8out_buf <= {1'b1,1'b0,1'b1,1'b0,ADDR[10:8],1'b1}; link_head <= NO;
link_sda <= YES;
link_write <= YES;
FF <= 0;
sh8out_state <= sh8out_bit6; main_state <= Ctrl_read; end
Ctrl_read:
if(FF == 0)
shift8_out;
else
begin
link_sda <= NO;
link_write <= NO;
FF <= 0;
sh8in_state <= sh8in_begin; main_state <= Data_read; end
Data_read:
if(FF == 0)
shift8in;
else
begin
link_stop <= YES; link_sda <= YES; stop_state <= stop_bit; FF <= 0;
main_state <= Stop;。