Verilog | I2C详解与Verilog实现

一、I2C接口介绍

1.1 简介

​ IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,是一种同步、半双工的通信总线,用于连接微控制器及其外围设备。I2C总线产生于在80年代,最初为音频和视频设备开发,如今主要在服务器管理中使用,其中包括单个组件状态的通信。例如管理员可对各个组件进行查询,以管理系统的配置或掌握组件的功能状态,如电源和系统风扇。可随时监控内存、硬盘、网络、系统温度等多个参数,增加了系统的安全性,方便了管理。IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)。

IIC总线的特点

  • 简单性和有效性。由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。

  • 支持多主控(multimastering), 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控占用IIC总线。

下图是一个嵌入式系统中处理器仅通过2根线的IIC总线控制多个IIC外设的典型应用图:
Verilog | I2C详解与Verilog实现_第1张图片

下图是I2C多master多slave示意图:
Verilog | I2C详解与Verilog实现_第2张图片
若想实现多master多slave效果:

  • 多个master-slave 时钟、数据线连在一起,需要实现信号的“线与”逻辑(所以SDA、SCL 被设计为漏极开路结构,外加上拉电阻实现“线与”)。
  • 需要实现 “时钟同步”和“总线仲裁”,引脚在输出信号的同时还能对引脚上的电平进行检测,检测是否与刚才输出一致,为 “时钟同步”和“总线仲裁”提供硬件基础。
  • I2C在读写时需要带上设备地址,这样不使用多的信号线就可指定特定的slave(而SPI通信需要多的片选线)。

1.2 I2C总线协议详解

​ IIC总线接口是一个标准的双向传输接口,一次数据传输需要主机和从机按照IIC协议的标准进行。I2C总线是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,并且在硬件上都需要接一个上拉电阻到VCC。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。

IIC的写过程(如图):
Verilog | I2C详解与Verilog实现_第3张图片

  1. Master发起START
  2. Master发送I2C 控制字(7bit)和W(写)操作0(1bit),等待ACK
  3. Slave发送ACK
  4. Master发送存储单元 addr(8bit),等待ACK
  5. Slave发送ACK
  6. Master发送data(8bit),即要写入寄存器中的数据,等待ACK
  7. Slave发送ACK
    第6步和第7步可以重复多次,即顺序写多个寄存器
  8. Master发起STOP结束传输

IIC的读过程(如图):
Verilog | I2C详解与Verilog实现_第4张图片

  1. Master执行写过程的1-5步骤
  2. Master发起START
  3. Master发送I2C addr(7bit)和r(读)操作1(1bit),等待ACK
  4. Slave发送ACK
  5. Slave发送data(8bit),即寄存器里的值
  6. Master发送ACK
    第7步和第8步可以重复多次,即顺序读多个寄存器
  7. 当master接收完想要的数据后,由Master发送NACK,告知slave停止发送数据
  8. Master发送STOP结束传输

完整的读写过程如下图:

Verilog | I2C详解与Verilog实现_第5张图片

1.3 I2C的状态

1) 总线空闲状态:
SDA和SCL同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
2) 总线START:
SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
3) 总线STOP:
SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
4 )总线Restart:
SCL为高电平时,SDA由高电平向低电平跳变,本质上也是START信号,用在完整I2C读过程中的读阶段,在首次发送停止信号之前,master通过发送Restart信号,可以转换与当前slave的通信模式(从写模式到读模式),或是切换到与另一个slave通信。
5 )数据阶段:
在IIC总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定。只有在SCL为低电平期间,才允许SDA上的电平改变状态。简单的说就是,数据在SCL下降会被采样,所以SDA需要在SCL为高电平时保持稳定。
6) ACK与NACK信号:
IIC总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

这段话再说细一点:
写阶段,master写了一字节数据,在第9个时钟脉冲期间释放数据线,由slave反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功;
读阶段,master向slave收数据,slave写了一字节数据,在第9个时钟脉冲期间释放数据线,由master反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功。

还有一种特殊情况:当master决定不再接收数据时,应向slave发送NACK信号,高速slave不再发送。
以下情况会导致出现NACK位:

  1. 接收器没有发送机响应的地址,接收端没有任何ACK发送给发送器
  2. 由于接收器正在忙碌处理实时程序导致接无法接收或者发送
    传输过程中,接收机器别不了发送机的数据或命令
  3. 接收器无法接收
  4. master接收完成读取数据后,要发送NACK结束告知slave。
    当master接收到slave的NACK信号后,可以STOP这次传输,也可以重新START。
    所以:NACK并不只是表示字节没有成功接收,也可以表示master告诉slave不再需要发送数据

二、I2C的Verilog简单实现

Verilog | I2C详解与Verilog实现_第6张图片

代码:

`timescale 1ns / 1ps
//
// Module Name: IIC_control
// Tool Versions: Vivado 2018.3
// Description: 
// 
//

module IIC_control #(
	parameter	 DIV_CLK = 'd500,	//系统时钟分频系数
	parameter   WMEN_LEN = 8'd0, 	//写数据帧数
	parameter   RMEN_LEN = 8'd0		//读数据帧数
	)
	(
	input 							sys_clk,	//系统时钟
	input 							sys_rst_n,	//系统复位
	input		 					iic_en,		//iic使能信号
	input		[WMEN_LEN*8-1'b1:0]	w_data,		//iic设备的写入数据,其中w_data[7:0]应该是地址帧
	input		[7:0]				wd_cnt,		//iic写入数据的帧数
	output	reg [RMEN_LEN*8-1'b1:0]	r_data,		//iic读取数据存储
	input		[7:0]				rd_cnt,		//iic读取数据的帧数
	output 		reg					iic_busy,	//iic工作标识,1为忙,2为空闲
	//标准的iic设备总线
	inout							iic_sda,   	//iic总线的双向数据线
	output		reg					iic_scl		//iic总线的时钟线
    );

    parameter	IDLE 		= 4'd0,				//空闲状态
    			START 		= 4'd1,				//开始
				W_WAIT 		= 4'd2,				//写状态
    			W_ACK 		= 4'd3,				//写响应
    			R_WAIT 		= 4'd4,				//读状态
    			R_ACK 		= 4'd5,				//读响应
    			STOP1 		= 4'd6,				//结束1
    			STOP2 		= 4'd7;				//结束2
    reg  [3:0]	iic_s;			//	状态机状态
    reg 		scl_clk;		//分频时钟,通过对其移位获得iic串行时钟信号
    reg 		iic_mode;		// 设置iic数据线状态,1为输出(写),0为输入(读)
    reg [2:0] 	bcnt;			//比特计数
    reg [7:0] 	wcnt;			//写字节计数
    reg [7:0] 	rcnt;			//读字节计数
    reg 		scl_r;			//iic时钟信号寄存,时钟信号的来源包括两部分,一是iic空闲时主动拉高,二是每比特数据传输时拉高
    reg 		sda_r;			//iic数据线寄存,W_ACK时进行响应判断
    reg 		sda_o = 0;		//iic数据线,写数据
    reg [7:0] 	sda_o_r;		//iic数据线,写数据寄存器
    reg [7:0] 	sda_i_r;		//iic数据线,读数据寄存
	reg [$clog2(DIV_CLK):0] clk_cnt;	//分频时钟计数
//	reg					iic_busy;                      
	reg 				rd_en;		//读数据使能信号
    //分频器对系统时钟进行分频处理,产生串行时钟信号
    always @(posedge sys_clk or negedge sys_rst_n)
    begin
    	if(!sys_rst_n)	begin
    		clk_cnt <= 0	;
    		scl_clk <= 0;
		end
    	else if(clk_cnt == (DIV_CLK>>1) -1'b1)begin//保持50%占空比
    		clk_cnt <= 0;
    		scl_clk <= ~scl_clk;
    	end
    	else clk_cnt <= clk_cnt +'b1;
    end
    //
    always @(*)
    begin
    	if(iic_s == IDLE || iic_s == STOP1 || iic_s == STOP2) //主动拉升iic时钟线
    		scl_r <=  1'b1;
		else scl_r <= ~scl_clk;
    end
    //使iic时钟线高电平保持在数据线传输每比特数据的中心
    wire scl_offset = (clk_cnt == DIV_CLK>>2); 				
    always @(posedge sys_clk) iic_scl <= scl_offset ? scl_r : iic_scl;		//产生iic串行时钟
    //iic状态机,首先发送地址帧
    always @(negedge scl_clk or negedge sys_rst_n)			//在scl_clk时钟下降沿改变状态,为了保证scl与sda的时序
    begin
    	if(!sys_rst_n) begin
    		iic_s 	 <= IDLE ;		//状态机复位
    		iic_mode <= 1'b1; 		// 设置iic为输出
    		bcnt 	 <= 3'd7;		//iic每次先发送高位,因此初始化赋值为7,发送最高比特,然后递减
    		wcnt 	 <= 0;			//将已写入数据帧数标识置零
    		rcnt 	 <= 0;			//将已读出数据帧数标识置零
    		iic_busy <= 0;			//iic空闲
    		rd_en 	 <= 0;			//
    	end
    	else 
    		case(iic_s)
				IDLE : begin						// 空闲状态设置scl与sda均为1
					if(iic_en || rd_en)begin
						iic_s 	 <= START;			//接收到使能信号iic开始工作	
						iic_busy <= 1'b1;			//iic忙
						iic_mode <=  1'b1; 			// 设置iic为写入
					end
					else begin
						iic_mode 	<= 1'b1;	 	// 设置iic为输出
						wcnt 		<= 0;			//将已写入数据帧数标识置零
						rcnt 		<= 0;			//将已读出数据帧数标识置零
						rd_en 		<= 1'b0;
						iic_busy 	<= 1'b0;			
    				end
				end
				START: begin					
					bcnt <= 3'd7;					//数据从最高位开始传输
					iic_s <= W_WAIT;				
				end
				W_WAIT:begin				
					if(bcnt >0) 
						bcnt <= bcnt -1'b1;		//计数减一,写入下一位数据
					else begin
						iic_s <= W_ACK;	
						wcnt <=  wcnt +1'b1;	//每帧数据写入完成后,帧计数器加一
						iic_mode <=  1'b0 ; 	// 设置iic为读取,即从机向主机输入
					end
						
				end
				W_ACK:begin	
					if(wcnt < wd_cnt) begin
						iic_s <= W_WAIT;
//					 	wcnt <=  wcnt +1'b1;
					 	bcnt <= 3'd7;
					 	iic_mode <=  1'b1 ; 	// 下一个状态是W_WAIT,因此设置iic为写入,即主机向从机输入
					end		
					else if(rd_cnt>0) begin		//如果rd_cnt大于0表示iic是读模式
						if(rd_en == 1'b0) begin //rd_en==0表明还未写入读控制字,跳转到IDLE状态进行读控制字写入
							rd_en <= 1'b1;
							iic_s <= IDLE;
							iic_mode <=  1'b1 ;
						end
						else 
							iic_s <= R_WAIT ;	//已写入读控制字,开始进行数据读取
						bcnt <= 3'd7;
					end		
					else 
						iic_s <= STOP1;			//写入完成跳转到结束状态
					if(sda_r !== 1'b0)			//如果未接收到从机应答信号,停止发送
						iic_s <= STOP1;
				end
				R_WAIT:begin					//读取数据
					rd_en <= 1'b0;				//置零,及时释放
					if(bcnt > 0) begin
						bcnt <= bcnt -1'b1;
					end
					else begin
						rcnt <=  rcnt +1'b1;	
						iic_s <= R_ACK;			
						iic_mode <=  1'b1 ; 	// 设置iic为写入,输出应答信号
					end					
				end
				R_ACK:begin
					if(rcnt < rd_cnt) begin					 	
					 	bcnt <= 3'd7;
					 	iic_s <= R_WAIT;
					 	iic_mode <=  1'b0 ; // 设置iic为读取
					end
					else 
				    begin
						iic_s <= STOP1;
					end
				end
				STOP1:begin//sda = 0 scl = 1
					iic_s <= STOP2;
				end
				STOP2:begin//sda = 1 scl =1
					iic_s <= IDLE;
				end
				default:
					iic_s <= IDLE;
    		endcase
    end
    //IIC总线的SDA数据线是一个双向IO口,使用三态门
	assign iic_sda = iic_mode ? sda_o: 1'bz;
    //sda输出
    always @(*)
    begin
    	if(!sys_rst_n || iic_s == STOP2)
    	begin			//sda = 1
    		sda_o <=  1'b1;
    	end
    	else if(iic_s == START || iic_s == STOP1 || (iic_s == R_ACK && rcnt != rd_cnt))
    	begin   		//sda = 0 
			sda_o <=  0;
		end
		else if(iic_s == W_WAIT)
		begin			//输出最高位
			sda_o <= sda_o_r[7];			
		end				//其他状态数据线拉高
		else sda_o <= 1'b1;
    end
    always @(negedge scl_clk)
    begin
    	if(iic_s == W_ACK || iic_s == START)begin
    		sda_o_r <= rd_en?({w_data[7:1],1'b1}):(w_data[(wcnt*8)+:8]);
//			sda_o_r <= w_data[(wcnt*8)+:8];
    		/*写、读模式下均存在wcnt=0的时候,此时w_data应该是地址帧,
    		即w_data[7:0]为地址帧,其中w_data[0]为模式,0代表写,1代表读*/
//    		if(rd_en) sda_o_r <= {w_data[7:1],1'b1};
    	end
    	else if(iic_s == W_WAIT)	//移位寄存器
    		sda_o_r <= {sda_o_r[6:0],1'b1};
		else 
			sda_o_r <= sda_o_r;
    end
    //暂存iic数据线的数据,用于检测从机应答
    always @(posedge scl_clk)	sda_r <= iic_sda;
    //读模式,
    always @(posedge scl_clk or negedge sys_rst_n)
    begin
    	if(!sys_rst_n)
    		r_data <= 'b0;
		else if(iic_s == R_ACK)
			r_data[((rcnt-1)*8)+:8] <= sda_i_r;
		else if(iic_s == R_WAIT || iic_s == W_ACK)
			sda_i_r <= {sda_i_r[6:0],iic_sda};
		else
			sda_i_r <= 0;
    end
      
endmodule

参考:
IIC总线解析(包含时钟拉伸,地址扩展,死锁,总线冲突总线仲裁的问题描述)
I2C详解
【接口时序】6、IIC总线的原理与Verilog实现

你可能感兴趣的:(Verilog,fpga开发)