软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用XILINX A7/K7/Z7/ZU/KU系列FPGA
登录米联客(MiLianKe)FPGA社区-www.uisrc.com观看免费视频课程、在线答疑解惑!
1 概述
我们知道I2C总线具备广泛的用途,比如寄存器的配置,EEPROM的使用,更重要的是I2C总线上可以挂载非常多的外设。 对于一些低速器件的访问非常节省IO资源,由于是标准的总线接口,使用起来非常方便。I2C总线是OC开路,支持双向传输,所以总线上需要上拉电阻,如下图。
2 I2C总线协议
由于这节课讲解的I2C是基于ZYNQ的I2C控制器,实际上可以不需要非常清楚I2C的详细时序,但是作为初学者,如果第一次学习I2C总线的,还是有必要学习下。
I2C协议把传输的消息分为两种类型的帧:
一个地址帧 :用于master指明消息发往哪个slave;
一个或多个数据帧: 由master发往slave的数据(或由slave发往master),每一帧是8-bit的数据。
注:协议要求每次放到SDA上的字节长度必须为8位,并且每个字节后须跟一个ACK位,在下面会讲到。
数据在SCL处于低电平时放到SDA上,并在SCL变为高电平后进行采样。读写数据和SCL上升沿之间的时间间隔是由总线上的设备自己定义的,不同芯片可能有差异。
I2C数据传输的时序图如下:
1、开始条件(start condition):
为了标识传输正式启动,master设备会将SCL置为高电平(当总线空闲时,SDA和SCL都处于高电平状态),然后将SDA拉低,这样,所有slave设备就会知道传输即将开始。如果两个master设备在同一时刻都希望获得总线的所有权,那么谁先将SDA拉低,谁就赢得了总线的控制权。在整个通信期间,可以存在多个start来开启每一次新的通信序列(communication sequence),而无需先放弃总线的控制权,后面会讲到这种机制。
2、地址帧(address frame):
地址帧总是在一次通信的最开始出现。一个7-bit的地址是从最高位(MSB)开始发送的,这个地址后面会紧跟1-bit的操作符,1表示读操作,0表示写操作。
3、应答AC K/NACK
接下来的一个bit是NACK/ACK,当这个帧中前面8bits发送完后,接收端的设备获得SDA控制权,此时接收设备应该在第9个时钟脉冲之前回复一个ACK(将SDA拉低)以表示接收正常,如果接收设备没有将SDA拉低,则说明接收设备可能没有收到数据(如寻址的设备不存在或设备忙)或无法解析收到的消息,如果是这样,则由master来决定如何处理(stop或repeated start condition)。
4、数据帧(data frames):
在地址帧发送之后,就可以开始传输数据了。Master继续产生时钟脉冲,而数据则由master(写操作)或slave(读操作)放到SDA上。每个数据帧8bits,数据帧的数量可以是任意的,直到产生停止条件。每一帧数据传输(即每8-bit)之后,接收方就需要回复一个ACK或NACK(写数据时由slave发送ACK,读数据时由master发送ACK。当master知道自己读完最后一个byte数据时,可发送NACK然后接stop condition)。
5、停止条件(stop condition):
当所有数据都发送完成时,master将产生一个停止条件。停止条件定义为:在SDA置于低电平时,将SCL拉高并保持高电平,然后将SDA拉高。
注意,在正常传输数据过程中,当SCL处于高电平时,SDA上的值不应该变化,防止意外产生一个停止条件。
6、重复开始条件(repeated start condition):
有时master需要在一次通信中进行多次消息交换(例如与不同的slave传输消息,或切换读写操作),并且期间不希望被其他master干扰,这时可以使用"重复开始条件" —— 在一次通信中,master可以产生多次start condition,来完成多次消息交换,最后再产生一个stop condition结束整个通信过程。由于期间没有stop condition,因此master一直占用总线,其他master无法切入。
为了产生一个重复的开始条件,SDA在SCL低电平时拉高,然后SCL拉高。接着master就可以产生一个开始条件继续新的消息传输(按照正常的7-bit/10-bit地址传输时序)。重复开始条件的传输时序如下图所示:
7、时钟拉伸(clock stretching)(了解即可):
有时候,低速slave可能由于上一个请求还没处理完,尚无法继续接收master的后续请求,即master的数据传输速率超过了slave的处理能力。这种情况下,slave可以进行时钟拉伸来要求master暂停传输数据 —— 通常时钟都是由master提供的,slave只是在SDA上放数据或读数据。而时钟拉伸则是slave在master释放SCL后,将SCL主动拉低并保持,此时要求master停止在SCL上产生脉冲以及在SDA上发送数据,直到slave释放SCL(SCL为高电平)。之后,master便可以继续正常的数据传输了。可见时钟拉伸实际上是利用了时钟同步的机制(见下文),只是时钟由slave产生。
如果系统中存在这种低速slave并且slave实现了clock stretching,则master必须实现为能够处理这种情况,实际上大部分slave设备中不包含SCL驱动器的,因此无法拉伸时钟。
所以更完整的I2C数据传输时序图为:
8、10-bit地址空间(了解即可):
上面讲到I2C支持10-bit的设备地址,此时的时序如下图所示:
在10-bit地址的I2C系统中,需要两个帧来传输slave的地址。第一个帧的前5个bit固定为b11110,后接slave地址的高2位,第8位仍然是R/W位,接着是一个ACK位,由于系统中可能有多个10-bit slave设备地址的高2bit相同,因此这个ACK可能由多个slave设备设置。第二个帧紧接着第一帧发送,包含slave地址的低8位(7:0),接着该地址的slave回复一个ACK(或NACK)。
注意,10-bit地址的设备和7-bit地址的设备在一个系统中是可以并存的,因为7-bit地址的高5位不可能是b11110。实际上对于7-bit的从设备地址,合法范围为b0001XXX-b1110XXX,'X'表示任意值,因此该类型地址最多有112个(其他为保留地址[1])。
两个地址帧传输完成后,就开始数据帧的传输了,这和7-bit地址中的数据帧传输过程相同。
9、时钟同步和仲裁(了解即可):
如果两个master都想在同一条空闲总线上传输,此时必须能够使用某种机制来选择将总线控制权交给哪个master,这是通过时钟同步和仲裁来完成的,而被迫让出控制权的master则需要等待总线空闲后再继续传输。在单一master的系统上无需实现时钟同步和仲裁。
10、时钟同步(了解即可):
时钟同步是通过I2C接口和SCL之间的线"与"(wired-AND)来完成的,即如果有多个master同时产生时钟,那么只有所有master都发送高电平时,SCL上才表现为高电平,否则SCL都表现为低电平。
11、总线仲裁(了解即可):
总线仲裁和时钟同步类似,当所有master在SDA上都写1时,SDA的数据才是1,只要有一个master写0,那此时SDA上的数据就是0。一个master每发送一个bit数据,在SCL处于高电平时,就检查看SDA的电平是否和发送的数据一致,如果不一致,这个master便知道自己输掉仲裁,然后停止向SDA写数据。也就是说,如果master一直检查到总线上数据和自己发送的数据一致,则继续传输,这样在仲裁过程中就保证了赢得仲裁的master不会丢失数据。
输掉仲裁的master在检测到自己输了之后也不再产生时钟脉冲,并且要在总线空闲时才能重新传输。
仲裁的过程可能要经过多个bit的发送和检查,实际上两个master如果发送的时序和数据完全一样,则两个master都能正常完成整个的数据传输。
3 EEPROM-24C02介绍
如下图,A0-A2是EEPROM I2C器件地址,SDA和SCL是EEPROM I2C总线SLAVE接口,WP是保护脚,一般接VCC。
24LXX 器件地址如下图
我们看下24C02的写时序,可以看到,支持单个字节的写,以及多个字节的写。首先发送器件的地址,然后发送需要写EEPROM存储空间的地址,之后就是数据,对于读操作一次可以写1个字节或者多个字节。
写字节操作BYTE WRITE
在起始位产生后,先写器件地址,再写芯片内存地址,再写入数据,最后产生停止位,每写一个字节都要产生ACK位。
页写PAGE WRITE
页写和字节写差不多,在字节写的基础上,连续写入数据,最后产生停止位。
我们看下24C02的读时序,可以看到,支持单个字节的读,以及多个字节的读。以下支持3种读的方式:
读当前地址CURRENT ADDRESS READ
只要发送器件地址就能读当前内存地址所指向的地址空间数据,最后的读数据可以不需要发送ACK
随机读RANDOM READ
需要发送器件地址,然后发送内存地址,之后再发送器件地址并且读取到数据,最后的读数据可以不需要发送ACK。
连续读SEQUENTIAL READ
可以从第一种和第二种读方式启动后,连续读取,但是需要注意的时候除最后一个读数据,其他的读主机都需要发送ACK。
I2C起始停止时序
I2C时序参数
4 用户程序设计
4.1 I2C MASTER控制器驱动设计
I2C Master控制器主要包含I2C收发数据状态机,SCL时钟分频器、发送移位模块、接收移位模块、空闲控制忙指示模块。SCL和SDA的输出逻辑和时序通过SCL和I2C状态机控制。
重点介绍关键信号:
IO_sda为I2C双向数据总线
O_scl为I2C时钟
I_wr_cnt写数据字节长度,包含了器件地址,发送I_iic_req前,预设该值
I_rd_cnt读数据字节长度,仅包含读回有效部分,发送I_iic_req前,预设该值
I_wr_data写入的数据
O_rd_data读出的数据,如果是读请求,当O_iic_busy从高变低代表数据读回有效
I_iic_req I2C操作请求,根据I_rd_cnt是否大于0决定是否有读请求
I_iic_mode是否支持随机读写,发送I_iic_req前,预设该值
O_iic_busy总线忙
首先在O_iic_busy=0即I2C总线空闲情况下,设置I_wr_cnt,I_rd_cnt,I_wr_data,并且设置I_iic_req=1,启动I2C传输。当O_iic_busy=1说明I2C控制器开始传输,这时候可以设置I_iic_req=0,结束本次请求,并且等待O_iic_busy=0,当O_iic_busy=0代表本次传
传输结束.如果发送的是读请求(当I_rd_cnt>0),则此时O_rd_data有效可以读走数据。
4.1.1 状态机设计
所有的SDA和SCL控制依据状态机设计。
IDLE:在IDLE状态,当I_iic_req请求有效代表一次全新的传输,进入I2C START启动传输阶段,如果rd_req有效代表目前要进行repeated start,也是进入START状态。
START:在START状态对bcnt进行初始化,设置需要发送的bit数量,因为不管是写操作,还是随机读操作,I2C总线协议要求,都要发送器件地址。因此在START后发送7Bit的器件地址和1bit的读/写位。
W_WAIT:在此阶段发送一个完整的8bit数据,发送移位模块也会在此阶段对数据移位。
W_ACK:对于写操作,SLAVE设备响应ACK,如果还有数据需要些,则回到W_WAIT;如果还要进行读操作,则回到IDLE产生一次Repeated Start;如果已经完成所有数据发送,也没有数据需要读,则进入STOP1
R_WAIT:在此阶段完成8bits数据接收,接收移位模块工作,之后进入R_ACK
R_ACK:响应NACK,如果还有数据需要接收,则再次进入R_WAIT,否则进入STOP1,完成本次传输。
STOP1:产生停止位,SDA=0 SDA=1,进入STOP1
SOTP2:产生停止位,SDA=1 SDA=1,回到IDLE
4.1.2 程序源码
`timescale 1ns / 1ns //仿真刻度/精度
module uii2c# ( parameter integer WMEN_LEN = 8'd0,//写长度,以字节为单位,包含器件地址 parameter integer RMEN_LEN = 8'd0,//读长度,以字节为单位,不包含器件地址 parameter integer CLK_DIV = 16'd499// I2C时钟分频系数 ) ( input wire I_clk,//系统时钟输入 input wire I_rstn,//系统复位,低电平有效 output reg O_iic_scl = 1'b0,//I2C时钟SCL inout wire IO_iic_sda,//I2C 数据总线 input wire [WMEN_LEN*8-1'b1:0]I_wr_data,//写数据寄存器,其中WMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多 input wire [7:0]I_wr_cnt,//写数据计数器,代表写了多少个字节 output reg [RMEN_LEN*8-1'b1:0]O_rd_data = 0,//读数据寄存器,其中RMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多 input wire [7:0]I_rd_cnt,//读数据计数器 input wire I_iic_req,//I_iic_req == 1 使能I2C传输 input wire I_iic_mode,//I_iic_mode = 1 随机读 I_iic_mode = 0 读当前寄存器或者页读 output reg O_iic_busy = 1'b0,//I2C控制器忙 output reg O_iic_bus_error, //I2C总线,无法读到正确ACK出错 output reg IO_iic_sda_dg );
localparam IDLE = 4'd0;//I2C 总线空闲状态 localparam START = 4'd1;//I2C 总线启动 localparam W_WAIT = 4'd2;//I2C 总线等待写完成 localparam W_ACK = 4'd3;//I2C 总线等待写WACK localparam R_WAIT = 4'd4;//I2C 总线等待读完成 localparam R_ACK = 4'd5;//I2C 总线等待读RACK localparam STOP1 = 4'd6;//I2C 总线产生停止位 localparam STOP2 = 4'd7;//I2C 总线产生停止位
localparam SCL_DIV = CLK_DIV/2;
localparam OFFSET = SCL_DIV - SCL_DIV/4;//设置I2C总线的SCL时钟的偏移,以满足SCL和SDA的时序要求,外部的SCL延迟内部的半周期的四分之三
reg [2:0] IIC_S = 4'd0; //I2C 状态机 //generate scl reg [15:0] clkdiv = 16'd0; //I2C 时钟分频寄存器 reg scl_r = 1'b1; //I2C控制器的SCL内部时钟 reg sda_o = 1'b0; //I2C控制器的SDA reg scl_clk = 1'b0; //I2C控制器内部SCL时钟,与外部时钟存在OFFSET参数设置的相位偏移 reg [7:0] sda_r = 8'd0; //发送寄存器 reg [7:0] sda_i_r = 8'd0; //接收寄存器 reg [7:0] wcnt = 8'd0; //发送数据计数器,以byte为单位 reg [7:0] rcnt = 8'd0; //接收数据计数器,以byte为单位 reg [2:0] bcnt = 3'd0; //bit计数器 reg rd_req = 1'b0; //读请求,当判断到需要读数据,内部状态机中设置1 wire sda_i; //sda 输入 wire scl_offset; //scl 时钟偏移控制
//assign sda_i = (IO_iic_sda == 1'b0) ? 1'b0 : 1'b1; //读总线 //assign IO_iic_sda = (sda_o == 1'b0) ? 1'b0 : 1'bz; //写总线,1'bz代表高阻,I2C外部通过上拉电阻,实现总线的高电平
PULLUP PULLUP_inst (.O(iic_sda));
//XILINX I2C 用IOBUF实现控制 IOBUF #( .DRIVE(12), // Specify the output drive strength .IBUF_LOW_PWR("TRUE"), // Low Power - "TRUE", High Performance = "FALSE" .IOSTANDARD("DEFAULT"), // Specify the I/O standard .SLEW("SLOW") // Specify the output slew rate ) IOBUF_inst ( .O(sda_i), // Buffer output .IO(IO_iic_sda), // Buffer inout port (connect directly to top-level port) .I(sda_o), // Buffer input .T(sda_o) // 3-state enable input, high=input, low=output );
//scl 时钟分频器 always@(posedge I_clk) if(clkdiv < SCL_DIV) clkdiv <= clkdiv + 1'b1; else begin clkdiv <= 16'd0; scl_clk <= !scl_clk; end
assign scl_offset = (clkdiv == OFFSET);//设置scl_offset的时间参数 always @(posedge I_clk) O_iic_scl <= scl_offset ? scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
//采集I2C 数据总线sda always @(posedge I_clk) IO_iic_sda_dg <= sda_i;
//当IIC_S状态机处于,同时空闲状态,设置SCL为高电平,同时也是空闲,停止状态,用于产生起始位和停止位时序,否则寄存scl_clk时钟 always @(*) if(IIC_S == IDLE || IIC_S == STOP1 || IIC_S == STOP2) scl_r <= 1'b1; else scl_r <= scl_clk;
//当进入IIC_S状态为启动、停止设置sda=0,结合scl产生起始位,或者(IIC_S == R_ACK && (rcnt != I_rd_cnt) sda=0,用于产生读操作的ACK always @(*) if(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt))) sda_o <= 1'b0; else if(IIC_S == W_WAIT) sda_o <= sda_r[7]; else sda_o <= 1'b1; //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK
//I2C数据发送模块,所有的写数据都通过此模块发送 always @(posedge scl_clk) if(IIC_S == W_ACK || IIC_S == START)begin//IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_r sda_r <= I_wr_data[(wcnt*8) +: 8];//寄存需要发发送的数据到sda_r if( rd_req ) sda_r <= {I_wr_data[7:1],1'b1};//对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读 end else if(IIC_S == W_WAIT)//当W_WAT状态,通过移位操作,把数据发送到数据总线 sda_r <= {sda_r[6:0],1'b1};//移位操作 else sda_r <= sda_r;
//sda data bus read and hold data to O_rd_data register when IIC_S=R_ACK //I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data always @(negedge scl_clk)begin if(IIC_S == R_WAIT ) //当IIC_S == R_WAIT ||IIC_S == W_ACK(如果读操作,第1个BIT是W_ACK这个状态读)启动移位操作 sda_i_r <= {sda_i_r[6:0],sda_i}; else if(IIC_S == R_ACK)//当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_data O_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0]; else if(IIC_S == IDLE)//空闲状态,重置sda_i_r sda_i_r <= 8'd0; end
//总线忙状态 always @(posedge scl_clk or negedge I_rstn )begin if(I_rstn == 1'b0) O_iic_busy <= 1'b0; else begin if((I_iic_req == 1'b1 || rd_req == 1'b1 || O_iic_bus_error))//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态 O_iic_busy <= 1'b1; else if(IIC_S == IDLE) O_iic_busy <= 1'b0; end end
//总线忙状态 always @(negedge scl_clk or negedge I_rstn )begin if(I_rstn == 1'b0) O_iic_bus_error <= 1'b0; else begin if(IIC_S == W_ACK && sda_i == 1'b1)//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态 O_iic_bus_error <= 1'b1; else if(I_iic_req == 0) O_iic_bus_error <= 1'b0; end end
//I2C Master控制器状态机 always @(posedge scl_clk or negedge I_rstn )begin if(I_rstn == 1'b0)begin //异步复位,复位相关寄存器 wcnt <= 8'd0; rcnt <= 8'd0; rd_req <= 1'b0; IIC_S <= IDLE; end else begin case(IIC_S) //sda = 1 scl =1 IDLE:begin//在空闲状态,sda=1 scl=1 if(I_iic_req == 1'b1 || rd_req == 1'b1) //当I_iic_req == 1'b1代表启动传输 当 rd_req == 1'b1 代表读操作需要产生repeated start 重复启动 IIC_S <= START; //进入START状态 else begin wcnt <= 8'd0; //复位计数器 rcnt <= 8'd0; //复位计数器 end end START:begin //这个状态,前面的代码,先设置sda = 0,scl_offset参数设置了scl_clk时钟的偏移,之后 scl_clk =0 即scl =0 产生起始位或者重复起始位 bcnt <= 3'd7; //设置bcnt的初值 IIC_S <= W_WAIT;//进入发送等待 end W_WAIT://等待发送完成,这里发送8bits 数据,写器件地址,写寄存器地址,写数据,都在这个状态完成 begin if(bcnt > 3'd0)//如果8bits没发送完,直到发送完 bcnt <= bcnt - 1'b1; //bcnt计数器,每发送1bit减1 else begin //8bits发送完毕 wcnt <= wcnt + 1'b1; //wcnt计数器,用于记录已经写了多少字节 IIC_S <= W_ACK;//进入W_ACK状态 end end W_ACK://等待WACK,此阶段,也判断是否有读操作 begin if(wcnt < I_wr_cnt)begin //判断是否所有数据发送(写)完成 bcnt <= 3'd7; //如果没有写完,重置bcnt IIC_S <= W_WAIT;//继续回到W_WAIT等待数据发送(写)完成 end else if(I_rd_cnt > 3'd0)begin//I_rd_cnt > 0代表有数据需要读,I_rd_cnt决定了有多少数据需要读 if(rd_req == 1'b0 && I_iic_mode == 1'b1)begin //对于第一次写完器件地址,如果I_iic_mode==1代表支持随机读 rd_req <= 1'b1;//设置rd_req=1,请求读操作 IIC_S <= IDLE; //设置状态进入IDLE,根据rd_req的值会重新产生一次为读操作进行的repeated重复start end else //如果之前已经完成了repeated重复start,那么读操作进入读数据阶段 IIC_S <= R_WAIT;//进入读等待 bcnt <= 3'd7;//设置bcnt的初值 end else //如果所有的发送完成,也没数据需要读,进入停止状态 IIC_S <= STOP1; end R_WAIT://等待读操作完成 begin rd_req <= 1'b0;//重置读请求rd_req=0 bcnt <= bcnt - 1'b1; //bit 计数器 if(bcnt == 3'd0)begin //当8bits数据读完 rcnt <= (rcnt < I_rd_cnt) ? (rcnt + 1'b1) : rcnt;//判断是否还有数据需要读 IIC_S <= R_ACK;//进入R_ACK end end R_ACK://R_ACK状态产生NACK begin bcnt <= 3'd7;//重置读请求bcnt计数器 IIC_S <= (rcnt < I_rd_cnt) ? R_WAIT : STOP1; //如果所有数据读完,进入停止状态 end STOP1:begin//产生停止位 sda = 0 scl = 1 rd_req <= 1'b0; IIC_S <= STOP2; end STOP2://产生停止位 sda = 1 scl = 1 IIC_S <= IDLE; default: IIC_S <= IDLE; endcase end end
endmodule
|
4.1.3 程序分析
以简单写1个字节来说明关键的顺序设计。
所有的控制逻辑以IIC_S状态机的状态,以及内部时钟scl_clk为主要时序来控制。写操作内部同步时序全部以scl_clk的上升沿进行,为了满足数据Tsu和Thd,设计scl延迟于scl_r半周期的四分之三 OFFSET = CLK_DIV - CLK_DIV/4。这样对于SLAVE接收来说具有足够的Tsu和Thd
对于读操作,每个scl_sck的下降沿采集总线,由于scl完成了相位调整,也是非常容易满足Tsu和Thd。
4.2 EEPROM用户读写程序设计
4.2.1 状态机介绍
4.2.2 用户接口程序源码
/*******************************eeprom_test********************* --1.本实验目的用于验证米联客I2C控制器 --2.通过写入数据到EEPROM并且读出比对数据,确认I2C控制器是否工作正常 --3.本实验也演示了,如何使用米联客I2C控制器的信号接口 *********************************************************************/
`timescale 1ns / 1ns//仿真时间间隔/精度
module eeprom_test ( input wire I_sysclk,//系统时钟输入 output wire O_iic_scl,// I2C SCL时钟 inout wire IO_iic_sda,//I2C SDA数据总线 output wire [2:0]O_test_led,//测试LED output wire O_led, output wire O_card_power_en //error LED );
assign O_card_power_en = 1'b1; //子卡上电
localparam SYSCLKHZ = 50_000_000; //定义系统时钟50MHZ localparam T500MS_CNT = (SYSCLKHZ/2-1); //定义每500ms访问一次EEPROM
reg [8 :0] rst_cnt = 9'd0;//延迟复位计数器 reg [25:0] t500ms_cnt = 26'd0;//500ms计数器 reg [19:0] delay_cnt = 20'd0;//eeprom每次读写完后,延迟操作计数器 reg [2 :0] TS_S = 2'd0; // 读写EEPROM状态机 reg iic_req = 1'b0; //i2c总线,读/写请求信号 reg [31:0] wr_data = 32'd0;//写数据寄存器 reg [7 :0] wr_cnt = 8'd0;//写数据计数器 reg [7 :0] rd_cnt = 8'd0;//读数据计数器 wire iic_busy; // i2c总线忙信号标志 wire [31:0] rd_data; // i2c读数据 wire t500ms_en;// 500ms延迟到使能
wire iic_sda_dg; wire iic_bus_error; //i2c总线错误 reg iic_error = 1'b0; //i2c 读出数据有错误 assign O_test_led = rd_data[2:0];//测试LED输出 assign O_led = iic_error;//通过LED显示错误标志 assign t500ms_en = (t500ms_cnt==T500MS_CNT);//500ms 使能信号
//通过内部计数器实现复位 always@(posedge I_sysclk) begin if(!rst_cnt[8]) rst_cnt <= rst_cnt + 1'b1; end
//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败 always@(posedge I_sysclk) begin if(!rst_cnt[8]) delay_cnt <= 0; else if((TS_S == 3'd0 || TS_S == 3'd2 )) delay_cnt <= delay_cnt + 1'b1; else delay_cnt <= 0; end
//每间隔500ms状态机运行一次 always@(posedge I_sysclk) begin if(!rst_cnt[8]) t500ms_cnt <= 0; else if(t500ms_cnt == T500MS_CNT) t500ms_cnt <= 0; else t500ms_cnt <= t500ms_cnt + 1'b1; end
//状态机实现每次写1字节到EEPROM然后再读1字节 always@(posedge I_sysclk) begin if(!rst_cnt[8])begin iic_req <= 1'b0; wr_data <= 32'd0; rd_cnt <= 8'd0; wr_cnt <= 8'd0; iic_error <= 1'b0; TS_S <= 3'd0; end else begin case(TS_S) 0:if(!iic_busy)begin//当总线非忙,可以开始一次I2C数据操作 iic_req <= 1'b1;//请求发送数据 wr_data <= {8'hfe,wr_data[15:8],wr_data[15:8],8'b10100000};//数据寄存器中8'b10100000代表需要写的器件地址,第一个wr_data[15:8]代表了EEPROM内存地址,第二个wr_data[15:8]代表了写入数据 rd_cnt <= 8'd0; //不需要读数据 wr_cnt <= 8'd3; //需要写入3个BYTES数据,包含1个器件地址,1个EEPROM 寄存器地址 1个数据 TS_S <= 3'd1;//进入下一个状态 end 1:if(iic_busy)begin iic_req <= 1'b0; //重置iic_req=0 TS_S <= 3'd2; end 2:if(!iic_busy&&delay_cnt[19])begin //当总线非忙,可以开始一次I2C数据操作,该时间约不能低于500us,否则会导致EEPROM操作失败 iic_req <= 1'b1;//请求接收数据 rd_cnt <= 8'd1; //需要读1个BYTE wr_cnt <= 8'd2; //需要些2个BYTE(1个器件地址8'b10100000,和1个寄存器地址wr_data[15:8])(I2C控制器会自定设置读写标志位) TS_S <= 3'd3; //进入下一个状态 end 3:if(iic_busy)begin iic_req <= 1'b0; //重置iic_req=0 TS_S <= 3'd4; end 4:if(!iic_busy)begin//当总线非忙,代表前面读数据完成 if(wr_data[23:16] != rd_data[7:0])//比对数据是否正确 iic_error <= 1'b1;//如果有错误,设置iic_error=1 else iic_error <= 1'b0;//如果有错误,设置iic_error=0 wr_data[15:8] <= wr_data[15:8] + 1'b1;//wr_data[15:8]+1 地址和数据都加1 TS_S <= 3'd5; end 5:if(t500ms_en)begin//延迟操作后进入下一个状态 TS_S <= 3'd0; end default: TS_S <= 3'd0; endcase end end
// 以下代码为在线逻辑分析仪观察调试部分 reg scl_r = 1'b0; always @(posedge I_sysclk)begin //对O_iic_scl寄存1次 scl_r <= O_iic_scl; end
//产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号 reg [8:0] dg_clk_cnt; wire dg_clk = (dg_clk_cnt==0);//用scl_dg即O_iic_scl的跳变沿作为触发信号 always@(posedge I_sysclk) begin dg_clk_cnt <= dg_clk_cnt+ 1'b1; end
ila_0 ila_debug ( .clk(I_sysclk),//在线逻辑分析仪的时钟 .probe0({rd_data[7:0],wr_data[23:0],TS_S,iic_error,iic_req,scl_r,iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号 );
//例化I2C控制模块 uii2c# ( .WMEN_LEN(4),//最大支持一次写入4BYTE(包含器件地址) .RMEN_LEN(4),//最大支持一次读出4BYTE(包含器件地址) .CLK_DIV(SYSCLKHZ/50000)//100KHZ I2C总线时钟 ) uii2c_inst ( .I_clk(I_sysclk),//系统时钟 .I_rstn(rst_cnt[8]),//系统复位 .O_iic_scl(O_iic_scl),//I2C SCL总线时钟 .IO_iic_sda(IO_iic_sda),//I2C SDA数据总线 .I_wr_data(wr_data),//写数据寄存器 .I_wr_cnt(wr_cnt),//需要写的数据BYTES .O_rd_data(rd_data), //读数据寄存器 .I_rd_cnt(rd_cnt),//需要读的数据BYTES .I_iic_req(iic_req),//I2C控制器请求 .I_iic_mode(1'b1),//读模式 .O_iic_busy(iic_busy),//I2C控制器忙 .O_iic_bus_error(iic_bus_error),//总线错误信号标志 .IO_iic_sda_dg(iic_sda_dg)//debug iic_sda );
endmodule
|
5 FPGA工程
fpga工程的创建过程不再重复
米联客的代码管理规范,在对应的FPGA工程路径下创建uisrc路径,并且创建以下文件夹
01_rtl:放用户编写的rtl代码
02_sim:仿真文件或者工程
03_ip:放使用到的ip文件
04_pin:放fpga的pin脚约束文件或者时序约束文件
05_boot:放编译好的bit或者bin文件(一般为空)
06_doc:放本一些相关文档(一般为空)
6 RTL仿真
6.1仿真激励文件
eeprom仿真模型
`define timeslice 20 module eeprom( input scl, inout sda); reg out_flag; reg [7:0] memory[2047:0]; reg[10:0] address; reg[7:0] memory_buf; reg [7:0] sda_buf; reg [7:0] shift; reg [7:0] addr_byte; reg [7:0] ctrl_byte; reg [1:0] State; integer i;
// ---------------------------------------------- parameter r7=8'b10101111,w7=8'b10101110, r6=8'b10101101,w6=8'b10101100, r5=8'b10101011,w5=8'b10101010, r4=8'b10101001,w4=8'b10101000, r3=8'b10100111,w3=8'b10100110, r2=8'b10100101,w2=8'b10100100, r1=8'b10100011,w1=8'b10100010, r0=8'b10100001,w0=8'b10100000;
//---------------------------------------------------
assign sda= (out_flag == 1)?sda_buf[7]:1'bz; //--------------------寄存器和存储器初始化------------------------------ initial begin addr_byte =0; ctrl_byte =0; out_flag =0; sda_buf =0; State =2'b00; memory_buf =0; address =0; shift =0; for(i=0;i<=2047;i=i+1) memory[i]=0; end
//////--------------启动信号检测-------------- always @(negedge sda) if(scl == 1) begin State=State+1; if(State==2'b11) disable write_to_eeprm; end /////-------------------主状态机----------------------- always @(posedge sda) if(scl == 1) stop_W_R; else begin casex(State) 2'b01: begin read_in; if(ctrl_byte == w7||ctrl_byte == w6|| ctrl_byte == w5 || ctrl_byte == w4 || ctrl_byte == w3 || ctrl_byte == w2 ||ctrl_byte == w1 ||ctrl_byte == w0) begin State = 2'b10; write_to_eeprm; end else State = 2'b00; end
2'b11: read_from_eeprm; default: State=2'b00; endcase end
//--------------操作停止------------------ task stop_W_R; begin
State = 2'b00; addr_byte =0; ctrl_byte =0; out_flag =0; sda_buf =0; end endtask //----------------读进控制字和存储单元地址------------------- task read_in; begin shift_in(ctrl_byte); shift_in(addr_byte); end endtask //-------------EEPROM-------------------- task write_to_eeprm; begin shift_in(memory_buf); address ={ctrl_byte[3:1],addr_byte}; memory[address] = memory_buf; $display("eeprm---memory[%0h]=%0h",address,memory[address]); State= 2'b00; end endtask
//-------------EEPROM读操作_______________________ task read_from_eeprm; begin shift_in(ctrl_byte); if(ctrl_byte == r7 || ctrl_byte == r6 || ctrl_byte == r5 || ctrl_byte == r4 || ctrl_byte == r3 || ctrl_byte == r2 || ctrl_byte == r1 || ctrl_byte == r0) begin address = {ctrl_byte[3:1],addr_byte}; sda_buf =memory [address]; shift_out; State = 2'b00; end end endtask
// ---SDA 数据线上的数据存入寄存器 ,数据在SCL的高电平有效------------------ task shift_in; output[7:0] shift; begin @(posedge scl) shift[7]=sda; @(posedge scl) shift[6]=sda; @(posedge scl) shift[5]=sda; @(posedge scl) shift[4]=sda; @(posedge scl) shift[3]=sda; @(posedge scl) shift[2]=sda; @(posedge scl) shift[1]=sda; @(posedge scl) shift[0]=sda; @(negedge scl) //ACK begin #`timeslice;//模拟芯片的延迟输出ACK out_flag = 1; sda_buf =0; end @(negedge scl)//结束ACK #`timeslice out_flag = 0; end endtask //----------EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化 task shift_out; begin out_flag= 1; for(i=6;i>=0;i=i-1) begin
@(negedge scl); # `timeslice; sda_buf = sda_buf<<1; end @(negedge scl) # `timeslice sda_buf[7]=1; @(negedge scl) # `timeslice out_flag=0; end endtask endmodule |
顶层调用接口仿真代码
`timescale 1ns / 1ns
module eeprom_test_tb; reg I_sysclk = 1'b1; wire O_iic_scl; wire IO_iic_sda;
pullup( IO_iic_sda );
eeprom_test eeprom_test_inst ( .I_sysclk(I_sysclk), .O_iic_scl(O_iic_scl), .IO_iic_sda(IO_iic_sda) );
eeprom eeprom_inst( .scl(O_iic_scl), .sda(IO_iic_sda) );
always begin #10 I_sysclk = ~I_sysclk; end
endmodule |
6.2仿真结果
启动后,右击需要观察的信号,添加到波形窗口,并仿真。
放大观察I2C时序,查看写操作START和ACK位置
放大观察I2C时序,查看写读操作Repeated START
7 在线仿真
设置ila的采样深度为8192,越大观察的数据越多,但是消耗的Bram也越多,设置支持capture control模式
设置需要观察数据的总位宽
注意代码中,通过512分频器产生的信号作为capture信号
// 以下代码为在线逻辑分析仪观察调试部分 reg scl_r = 1'b0; always @(posedge I_sysclk)begin //对O_iic_scl寄存1次 scl_r <= O_iic_scl; end
//产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号 reg [8:0] dg_clk_cnt; wire dg_clk = (dg_clk_cnt==0);//用scl_dg即O_iic_scl的跳变沿作为触发信号 always@(posedge I_sysclk) begin dg_clk_cnt <= dg_clk_cnt+ 1'b1; end
ila_0 ila_debug ( .clk(I_sysclk),//在线逻辑分析仪的时钟 .probe0({rd_data[7:0],wr_data[23:0],TS_S,iic_error,iic_req,scl_r,iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号 ); |
8 下载演示
为了方便观察结果,使用LED观察,每间隔500ms完成一次读写操作
assign O_test_led = rd_data[2:0]; |
下载程序前,先确保FPGA工程已经编译。
8.1 硬件连接
请确保下载器和开发板已经正确连接,并且开发板已经上电(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)
8.2 运行结果
1.通过LED观察I2C的读写结果,可以看到LED规律运行
2.通过ILA在线逻辑分析仪观察
Capure模式必须设置BASIC模式
用500ms作为触发信号
运行后抓到的波形如下
标签:scl,begin,end,05,sda,iic,I2C,EEPROM From: https://www.cnblogs.com/milianke/p/17931234.html