FPGA学习笔记:I2C协议原理及I2C最小单元读写模块设计

一、IC协议原理

1.I2C总线

I2C总线特点

  • 连接简单,只有SDA(串行数据线)、SCL(串行时钟线)两条串行总线
  • I2C总线是一个真正的多主机总线,所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址,但同一时刻只允许有一个主机
  • 有三种传输速率,标准模式100Kbps、快速模式400Kbps、高速模式3.4Mbps。
    在这里插入图片描述

一次完整的数据传输包括起始位、停止位、器件地址、读写标志、存储单元地址、读写传输数据。在主机发送数据后,从机给出ACK应答位。
起始位:SCL高电平时,SDA出现下降沿
停止位:SCL高电平时,SDA出现上升沿
传输数据先写高位,后写低位
SCL高电平时,SDA数据保持稳定;SCL低电平时,SDA数据变化。因此需要设计代码时在SCL高低电平中点读取数据、变化数据

2.单字节数据写时序

1字节地址写时序
在这里插入图片描述
2字节地址写时序
在这里插入图片描述

3.单字节数据读时序

1字节地址读时序
在这里插入图片描述
2字节地址读时序
在这里插入图片描述

4.多字节数据读时序

在这里插入图片描述

二、I2C最小单元读写模块设计

1.设计思路

根据上述时序图,控制字段的7位器件地址和1位读写标志可合并为8位数据,则I2C总线SDA上的数据可归为6类:起始位、读8位数据、写8位数据、检查应答位、产生应答位ACK/NOACK、停止位。设计I2C最小单元模块,实现这6类数据组合的一个字节数据的传输,后续只需要调用该模块即可实现I2C通信。
使用状态机实现上述过程,由上层控制模块调用最小单元模块,输入对应的指令即可实现I2C读写操作。
在这里插入图片描述
如上图所示,I2C最小单元模块将图中红框部分划为一个单元,单元①包括起始位、写8位数据(器件地址+读标志)、检查应答位;单元②包括写8位数据(存储单元地址)、检查应答位;单元③包括写8位数据、检查应答位、停止位。控制模块中只需要告诉最小单元模块需要完成哪些操作,由状态机在具体的操作之间跳转。

2.模块端口说明

端口名称方向说明
Clkinput系统时钟
Rst_ninput复位信号
Cmd [5:0]inputI2C读写控制命令
GoinputI2C读写开始信号
RX_DATA [7:0]outputI2C读取数据
TX_DATA [7:0]inputI2C发送数据
Trans_Doneoutput单次读写操作完成标志
Ack_ooutput应答信号ACK
i2c_sclkoutputI2C时钟SCLK
i2c_sdatinoutI2C数据线SDA

在这里插入图片描述
Cmd为控制模块给出的控制命令组合,包括起始位请求STA、写请求WR、读请求RD、停止位请求STO、应答位请求ACK、无应答请求NACK,如图中的单元①,控制命令组合为STA+WR,单元②控制命令组合为WR,单元③控制命令组合为WR+STO。
在这里插入图片描述
Go为I2C读写开始信号,只需提供一个脉冲信号即可实现1byte数据的传输。
RX_DATA为I2C读取到的8位数据,TX_DATA为I2C写入存储单元的8位数据。
Ack_o为从机应答信号,应答信号为低电平,非应答信号为高电平。
i2c_sdat是I2C数据线SDA,为了实现输入和输出,该信号为inout

3.代码设计

(1)时钟设置

I2C有三种传输速率,标准模式100Kbps、快速模式400Kbps、高速模式3.4Mbps,在代码中定义速度参数SCLK_CLOCK,本文选择速率为400Kbps,根据50Mhz的系统时钟SYS_CLOCK计算出SCLK分频计数器计数最大值SCL_CNT_M。由于I2C在SCLK低电平中点改变数据,高电平中点读取数据,故将其分为4份,在计算时除以4。
div_cnt_en为SCLK分频计数器使能信号,当模块接收到Go I2C读写开始信号时,div_cnt_en置1,完成最小单元1byte数据读写回到IDLE状态时,div_cnt_en清0。

	//系统时钟50Mhz
	parameter SYS_CLOCK = 50_000_000;
	//SCL时钟 标准模式100Khz 快速模式400Khz 高速模式3.4Mhz
	parameter SCL_CLOCK = 400_000;//快速模式
	//计算SCLK时钟计数器最大值
	localparam SCL_CNT_M = SYS_CLOCK/SCL_CLOCK/4 - 1;
	
	reg div_cnt_en;//分频计数器使能
	reg [8:0] div_cnt;
	
	//分频计数器
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		div_cnt <= 8'd0;
	else if(div_cnt_en) begin
		if(div_cnt == SCL_CNT_M)
			div_cnt <= 8'd0;
		else
			div_cnt <= div_cnt + 8'd1;
	end
	else
		div_cnt <= div_cnt;
		
	wire sclk_plus = div_cnt == SCL_CNT_M;//SCLK 1/4脉冲信号

sclk_plus为SCLK 1/4脉冲信号,分频计数器计数到最大值时,该信号置1,表示SCLK边沿或高低电平中点,用于在状态机中实现相应操作。sclk_plus四个为一组,在代码中用计数器cnt记录sclk_plus脉冲,脉冲和SCLK对应关系如下图所示,所有状态下的SCLK信号连接起来,构成I2C完整的SCLK时钟。在状态机中,cnt为0时是SCLK低电平中点,为2时是SCLK高电平中点,1、3为SCLK边沿。
在这里插入图片描述

(2)I2C SDA信号三态开漏模式代码实现

I2C总线要求SDA是开漏输出,低电平时,通过给总线赋值为0实现,高电平时总线为高阻态,通过外部上拉电阻实现高电平。
在这里插入图片描述
为了避免在I2C读取从机数据时,总线i2c_sda受到I2C输出信号i2c_sda_od的影响,在代码中添加i2c_sda_oe使能信号控制i2c_sda_od的输出。
i2c_sda作为输出端口,i2c_sda_oe使能打开,i2c_sda_od的值传输到i2c_sda_id和i2c_sda;i2c_sda作为输入端口,i2c_sda_oe使能关闭,i2c_sda的值传输到i2c_sda_id。SDA三态开漏模式实现代码如下:

	reg i2c_sda_oe,i2c_sda_od;//输出使能,输出寄存器
	assign i2c_sdat = i2c_sda_oe?(i2c_sda_od?1'bz:1'b0):1'bz;

(3)状态转换

使用状态机完成I2C最小单元模块设计,包含以下7个状态:
IDLE:空闲状态,等待单次传输需要执行的内容
GEN_STA:产生起始位
WR_DATA:写8位数据
RD_DATA:读8位数据
CHECK_ACK:检查应答位
GEN_ACK:产生ACK/NOACK
GEN_STO:产生停止位

状态转换图如下:
在这里插入图片描述
根据前面I2C的读写时序图,无论是读数据还是写数据,都需要先写器件地址和存储单元地址,所以发送起始位后一定是写数据,数据写入从机后,一定会产生应答信号,故跳入CHECK_ACK检查应答位。检查完应答位后,可能继续读写数据,回到空闲状态,也可能是读数据完毕发送停止位。
Cmd为I2C读写操作命令的组合,在状态机中根据对应命令跳入指定状态。命令及状态机定义如下:

	localparam
		STA =  6'b000001,	//起始位请求
		WR =   6'b000010,	//写请求
		RD =   6'b000100,	//读请求
		STO =  6'b001000,	//停止位请求
		ACK =  6'b010000,	//应答位请求
		NACK = 6'b100000;	//无应答请求
		
	reg [7:0] state;
	localparam 
		IDLE = 		8'b00000001,//空闲状态,等待单次传输需要执行的内容
		GEN_STA = 	8'b00000010,//产生起始位
		WR_DATA = 	8'b00000100,//写8位数据
		RD_DATA = 	8'b00001000,//读8位数据
		CHECK_ACK = 8'b00010000,//检查应答位
		GEN_ACK = 	8'b00100000,//产生ACK/NOACK
		GEN_STO = 	8'b01000000;//产生停止位

(4)空闲状态

当检测到Go信号为高电平时,开始I2C数据传输,未检测到Go信号则在IDLE信号等待。
Cmd和上面定义的I2C命令按位作与运算,若为1,则跳入对应状态GEN_STA、WR_DATA、RD_DATA。

	IDLE:begin
		Trans_Done <= 1'b0;
		div_cnt_en <= 1'b0;
		i2c_sda_oe <= 1'b1;
		i2c_sda_od <= 1'b1;
		Ack_o <= 1'b0;
		if(Go)begin
			div_cnt_en <= 1'b1;
			if(Cmd & STA)
				state <= GEN_STA;
			else if(Cmd & WR)
				state <= WR_DATA;
			else if(Cmd & RD)
				state <= RD_DATA;
			else
				state <= IDLE;
		end
		else
			state <= IDLE;
	end

(5)产生起始位

前面提到sclk_plus为SCLK 1/4脉冲信号,表示SCLK边沿或高低电平中点,cnt用于计数sclk_plus,cnt为0时是SCLK低电平中点,为2时是SCLK高电平中点。I2C起始位定义为在SCL高电平时SDA出现下降沿,根据前面的cnt和SCLK的关系图cnt为0、1、2时SCLK为高电平,cnt为3时SCLK为低电平。cnt为1时,将SDA输出使能i2c_sda_oe打开,i2c_sda_od输出高电平。cnt为2时,将i2c_sda_od拉低,输出低电平。cnt计满四个数跳入下一状态。

	GEN_STA:begin
		if(sclk_plus)begin
			case(cnt)
				0:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= 1'b1; end
				1:i2c_sclk <= 1'b1;
				2:begin i2c_sda_od <= 1'b0;i2c_sclk <= 1'b1; end
				3:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b0; end
				default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd3)begin
				cnt <= 5'd0;
				state <= WR_DATA;//起始位结束后一定会写7位地址位
			end
			else
				cnt <= cnt + 1'b1;
		end
	end

(6)写8位数据

8位数据需要传输8个SCLK时钟,则sclk_plus脉冲计数器cnt计数32次。在SCLK低电平中点改变数据,即cnt为0、4、8、12……时将TX_DATA放入i2c_sda_od

	WR_DATA:begin
		if(sclk_plus)begin
			case(cnt)
			0,4,8 ,12,16,20,24,28:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= TX_DATA[7-cnt[4:2]];end
			1,5,9 ,13,17,21,25,29:i2c_sclk <= 1'b1;
			2,6,10,14,18,22,26,30:i2c_sclk <= 1'b1;
			3,7,11,15,19,23,27,31:i2c_sclk <= 1'b0;
			default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd31)begin
				cnt <= 5'd0;
				state <= CHECK_ACK;//写数据结束后检查应答位
			end
			else
				cnt <= cnt +1'b1;
		end
	end

(7)读8位数据

读数据和写数据类似,sclk_plus脉冲计数器cnt计数32次。在高电平中点2、6、10、14……读取数据,采用移位的方法,每次将i2c_sdat放入RX_DATA最低位

	RD_DATA:begin
		if(sclk_plus)begin
			case(cnt)
			0,4,8 ,12,16,20,24,28:begin i2c_sda_oe <= 1'b0; i2c_sclk <= 1'b0;end
			1,5,9 ,13,17,21,25,29:i2c_sclk <= 1'b1;
			2,6,10,14,18,22,26,30:begin RX_DATA <= {RX_DATA[6:0],i2c_sdat}; i2c_sclk <= 1'b1;end
			3,7,11,15,19,23,27,31:i2c_sclk <= 1'b0;
			default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd31)begin
				cnt <= 5'd0;
				state <= GEN_ACK;//读数据结束后产生应答位
			end
			else
				cnt <= cnt +1'b1;
		end
	end

(8)检查应答位

写8数据后检查从机返回来的应答位。cnt为0时关闭SDA输出使能,在SCLK高电平中点cnt为2时,读取i2c_sdat数据存入Ack_o。若下一状态为发送停止位STO,则跳入下一状态;否则本次I2C最小单元全部发送完毕,跳回IDLE状态等待下一命令,并将单次读写操作完成标志Trans_Done置1

	CHECK_ACK:begin
		if(sclk_plus)begin
			case(cnt)
			0:begin i2c_sclk <= 1'b0; i2c_sda_oe <= 1'b0;end//关闭SDA输出使能
			1:i2c_sclk <= 1'b1;
			2:begin Ack_o <= i2c_sdat;i2c_sclk <= 1'b1; end
			3:i2c_sclk <= 1'b0;
			default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd3)begin
				cnt <= 5'd0;
				if(Cmd & STO)
					state <= GEN_STO;
				else begin
					state <= IDLE;
					Trans_Done <= 1'b1;
				end
			end
			else
				cnt <= cnt + 1'b1;
		end
	end

(9)产生ACK/NOACK

在读多字节数据的时序图中,单次读完8位数据后,向从机发送ACK低电平表示继续读数据,发送NOACK高电平表示不再读数据。在cnt为0时,打开SDA输出使能,在i2c_sda_od产生应答位和非应答位。在本状态判断是否需要发送停止位,若需要发送,则跳入GEN_STO,否则返回IDLE状态,并将单次读写操作完成标志Trans_Done置1。

	GEN_ACK:begin
		if(sclk_plus)begin
			case(cnt)
			0:begin
				i2c_sda_oe <= 1'b1;
				if(Cmd & ACK)
					i2c_sda_od <= 1'b0;//产生应答位
				else if(Cmd & NACK)
					i2c_sda_od <= 1'b1;//产生非应答位
				else i2c_sda_od <= 1'b1;
				i2c_sclk <= 1'b0;
			end
			1:i2c_sclk <= 1'b1;
			2:i2c_sclk <= 1'b1;
			3:i2c_sclk <= 1'b0;
			default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd3)begin
				cnt <= 5'd0;
				if(Cmd & STO)
					state <= GEN_STO;
				else begin
					state <= IDLE;
					Trans_Done <= 1'b1;
				end
			end
			else
				cnt <= cnt + 1'b1;
		end
	end

(10)产生停止位

停止位是在SCL高电平时SDA出现上升沿。和起始位类似,cnt为0时SCLK为低电平,cnt为1、2、3时SCLK为高电平。cnt为1时,将SDA输出使能i2c_sda_oe打开,i2c_sda_od输出低电平。cnt为2时,将i2c_sda_od拉低,输出高电平

	GEN_STO:begin
		if(sclk_plus)begin
			case(cnt)
				0:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= 1'b0; end
				1:i2c_sclk <= 1'b1;
				2:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
				3:i2c_sclk <= 1'b1;//空闲状态时SCLK为高电平
				default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
			endcase
			if(cnt == 5'd3)begin
				cnt <= 5'd0;
				Trans_Done <= 1'b1;//单次读写操作完成
				state <= IDLE;//停止位结束后回到空闲状态
			end
			else
				cnt <= cnt + 1'b1;
		end
	end

I2C最小单元发送模块源代码

module i2c_bit_shift(
	input Clk,							//系统时钟
	input Rst_n,						//复位信号
	
	input [5:0] Cmd,					//I2C读写控制命令
	input Go,							//I2C读写开始信号
	output reg [7:0] RX_DATA,			//I2C读取数据
	input [7:0] TX_DATA,				//I2C发送数据
	
	output reg Trans_Done,				//单次读写操作完成标志
	output reg Ack_o,					//应答信号ACK
	output reg i2c_sclk,				//I2C时钟SCLK
	inout i2c_sdat						//I2C数据线SDA
);
	
	//系统时钟50Mhz
	parameter SYS_CLOCK = 50_000_000;
	//SCL时钟 标准模式100Khz 快速模式400Khz 高速模式3.4Mhz
	parameter SCL_CLOCK = 400_000;//快速模式
	//计算SCLK时钟计数器最大值
	//由于在低电平中点改变数据,高电平中点读取数据,故将其分为4份
	localparam SCL_CNT_M = SYS_CLOCK/SCL_CLOCK/4 - 1;
	
	reg i2c_sda_oe,i2c_sda_od;//输出使能,输出寄存器
	assign i2c_sdat = i2c_sda_oe?(i2c_sda_od?1'bz:1'b0):1'bz;
	
	localparam
		STA =  6'b000001,	//起始位请求
		WR =   6'b000010,	//写请求
		RD =   6'b000100,	//读请求
		STO =  6'b001000,	//停止位请求
		ACK =  6'b010000,	//应答位请求
		NACK = 6'b100000;	//无应答请求
		
	reg [7:0] state;
	localparam 
		IDLE = 		8'b00000001,//空闲状态,等待单次传输需要执行的内容
		GEN_STA = 	8'b00000010,//产生起始位
		WR_DATA = 	8'b00000100,//写8位数据
		RD_DATA = 	8'b00001000,//读8位数据
		CHECK_ACK = 8'b00010000,//检查应答位
		GEN_ACK = 	8'b00100000,//产生ACK/NOACK
		GEN_STO = 	8'b01000000;//产生停止位

		
	reg div_cnt_en;//分频计数器使能
	reg [8:0] div_cnt;
	
	//分频计数器
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n)
		div_cnt <= 8'd0;
	else if(div_cnt_en) begin
		if(div_cnt == SCL_CNT_M)
			div_cnt <= 8'd0;
		else
			div_cnt <= div_cnt + 8'd1;
	end
	else
		div_cnt <= div_cnt;
		
	wire sclk_plus = div_cnt == SCL_CNT_M;//SCLK 1/4脉冲信号
	
	
	reg [4:0] cnt;// 1/4 SCLK计数器
	
	//I2C单次传输状态机
	always@(posedge Clk or negedge Rst_n)
	if(!Rst_n) begin
		Trans_Done <= 1'b0;
		Ack_o <= 1'b0;			//应答信号为低电平,非应答信号为高电平
		i2c_sclk <= 1'b1;		//空闲状态时SCLK为高电平
		RX_DATA <= 8'd0;
		div_cnt_en <= 1'b0;
		i2c_sda_oe <= 1'b1;
		i2c_sda_od <= 1'b1;	//空闲状态时SDA为高电平
		cnt <= 5'd0;
		state <= IDLE;
	end
	else begin
		case(state)
			IDLE:begin
				Trans_Done <= 1'b0;
				div_cnt_en <= 1'b0;
				i2c_sda_oe <= 1'b1;
				i2c_sda_od <= 1'b1;
				Ack_o <= 1'b0;
				if(Go)begin
					div_cnt_en <= 1'b1;
					if(Cmd & STA)
						state <= GEN_STA;
					else if(Cmd & WR)
						state <= WR_DATA;
					else if(Cmd & RD)
						state <= RD_DATA;
					else
						state <= IDLE;
				end
				else
					state <= IDLE;
			end
	
			GEN_STA:begin
				if(sclk_plus)begin
					case(cnt)
						0:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= 1'b1; end
						1:i2c_sclk <= 1'b1;
						2:begin i2c_sda_od <= 1'b0;i2c_sclk <= 1'b1; end
						3:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b0; end
						default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd3)begin
						cnt <= 5'd0;
						state <= WR_DATA;//起始位结束后一定会写7位地址位
					end
					else
						cnt <= cnt + 1'b1;
				end
			end
			
			WR_DATA:begin
				if(sclk_plus)begin
					case(cnt)
					0,4,8 ,12,16,20,24,28:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= TX_DATA[7-cnt[4:2]];end
					1,5,9 ,13,17,21,25,29:i2c_sclk <= 1'b1;
					2,6,10,14,18,22,26,30:i2c_sclk <= 1'b1;
					3,7,11,15,19,23,27,31:i2c_sclk <= 1'b0;
					default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd31)begin
						cnt <= 5'd0;
						state <= CHECK_ACK;//写数据结束后检查应答位
					end
					else
						cnt <= cnt +1'b1;
				end
			end
			
			RD_DATA:begin
				if(sclk_plus)begin
					case(cnt)
					0,4,8 ,12,16,20,24,28:begin i2c_sda_oe <= 1'b0; i2c_sclk <= 1'b0;end
					1,5,9 ,13,17,21,25,29:i2c_sclk <= 1'b1;
					2,6,10,14,18,22,26,30:begin RX_DATA <= {RX_DATA[6:0],i2c_sdat}; i2c_sclk <= 1'b1;end
					3,7,11,15,19,23,27,31:i2c_sclk <= 1'b0;
					default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd31)begin
						cnt <= 5'd0;
						state <= GEN_ACK;//读数据结束后产生应答位
					end
					else
						cnt <= cnt +1'b1;
				end
			end
			
			CHECK_ACK:begin
				if(sclk_plus)begin
					case(cnt)
					0:begin i2c_sclk <= 1'b0; i2c_sda_oe <= 1'b0;end//关闭SDA输出使能
					1:i2c_sclk <= 1'b1;
					2:begin Ack_o <= i2c_sdat;i2c_sclk <= 1'b1; end
					3:i2c_sclk <= 1'b0;
					default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd3)begin
						cnt <= 5'd0;
						if(Cmd & STO)
							state <= GEN_STO;
						else begin
							state <= IDLE;
							Trans_Done <= 1'b1;
						end
					end
					else
						cnt <= cnt + 1'b1;
				end
			end
	
			GEN_ACK:begin
				if(sclk_plus)begin
					case(cnt)
					0:begin
						i2c_sda_oe <= 1'b1;
						if(Cmd & ACK)
							i2c_sda_od <= 1'b0;//产生应答位
						else if(Cmd & NACK)
							i2c_sda_od <= 1'b1;//产生非应答位
						else i2c_sda_od <= 1'b1;
						i2c_sclk <= 1'b0;
					end
					1:i2c_sclk <= 1'b1;
					2:i2c_sclk <= 1'b1;
					3:i2c_sclk <= 1'b0;
					default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd3)begin
						cnt <= 5'd0;
						if(Cmd & STO)
							state <= GEN_STO;
						else begin
							state <= IDLE;
							Trans_Done <= 1'b1;
						end
					end
					else
						cnt <= cnt + 1'b1;
				end
			end
			
			GEN_STO:begin
				if(sclk_plus)begin
					case(cnt)
						0:begin i2c_sda_oe <= 1'b1;i2c_sda_od <= 1'b0; end
						1:i2c_sclk <= 1'b1;
						2:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
						3:i2c_sclk <= 1'b1;//空闲状态时SCLK为高电平
						default:begin i2c_sda_od <= 1'b1;i2c_sclk <= 1'b1; end
					endcase
					if(cnt == 5'd3)begin
						cnt <= 5'd0;
						Trans_Done <= 1'b1;//单次读写操作完成
						state <= IDLE;//停止位结束后回到空闲状态
					end
					else
						cnt <= cnt + 1'b1;
				end
			end
	
			default:state <= IDLE;
		endcase
	end
endmodule

三、I2C最小单元发送模块仿真

1.将单次I2C读写代码封装

为了简化仿真文件,保持其简洁性和可读性,将单次读写操作封装成task任务I2C_WR和I2C_RD,在仿真时只需要将Cmd和待发送数据TX_DATA传入任务即可。

	task I2C_WR;
		input [5:0] cmd;
		input [7:0] wr_DATA;
		begin
			Cmd = cmd;
			TX_DATA = wr_DATA;
			Go = 1'b1;#20;
			Go = 1'b0;#20;
			wait(Trans_Done);
			#200;
		end
	endtask
	
	task I2C_RD;
		input [5:0] cmd;
		begin	
			Cmd = cmd;
			Go = 1'b1;#20;
			Go = 1'b0;#20;
			wait(Trans_Done);
			#200;
		end
	endtask

2.仿真文件

采用EEPROM仿真模型M24LC04B对本模块进行仿真。将数据8’h76写入存储单元地址8’h3B中。

`timescale 1ns/1ns
module i2c_bit_shift_tb();
	reg Clk,Rst_n,Go;
	reg [5:0] Cmd;
	reg [7:0] TX_DATA;
	wire [7:0] RX_DATA;
	wire Trans_Done;
	wire Ack_o;	
	wire i2c_sclk;
	wire i2c_sdat;	
	
	pullup(i2c_sdat); //模拟外部上拉电阻
	
	localparam
		STA =  6'b000001,	//起始位请求
		WR = 	 6'b000010,	//写请求
		RD = 	 6'b000100,	//读请求
		STO =  6'b001000,	//停止位请求
		ACK =  6'b010000,	//应答位请求
		NACK = 6'b100000;	//无应答请求

	i2c_bit_shift i2c_bit_shift(
		.Clk(Clk),					//系统时钟
		.Rst_n(Rst_n),				//复位信号
		.Cmd(Cmd),					//I2C读写控制命令
		.Go(Go),						//I2C读写开始信号
		.RX_DATA(RX_DATA),		//I2C读取数据
		.TX_DATA(TX_DATA),		//I2C发送数据
		.Trans_Done(Trans_Done),//单次读写操作完成标志
		.Ack_o(Ack_o),				//应答信号ACK
		.i2c_sclk(i2c_sclk),		//I2C时钟SCLK
		.i2c_sdat(i2c_sdat)		//I2C数据线SDA
	);

	M24LC04B M24LC04B(
		.A0(0), 
		.A1(0), 
		.A2(0), 
		.WP(0),//写保护引脚,高电平不允许写,低电平允许写
		.SDA(i2c_sdat), 
		.SCL(i2c_sclk), 
		.RESET(~Rst_n)
	);

	initial Clk = 1'b1;
	always #10 Clk = ~Clk;

	initial begin
		Rst_n = 1'b0;
		TX_DATA = 8'd0;
		Go = 1'b0;
		Cmd = 6'd0;
		#201;
		
		Rst_n = 1'b1;
		#20;
		//写数据步骤:起始位->发送器件地址->ACK->
		//发送存储单元地址->ACK->写入DATA->ACK->停止位
		I2C_WR(STA|WR,8'hA0&8'hfe);//写数据,发送器件地址
		I2C_WR(WR,8'h3B);//发送存储单元地址
		I2C_WR(WR|STO,8'h76);//发送存储数据
		
		#20000;
		
		I2C_WR(STA|WR,8'hA0&8'hfe);//写数据,发送器件地址
		I2C_WR(WR,8'h3B);//发送存储单元地址
		I2C_WR(STA|WR,8'hA0|8'd1);//读数据,发送器件地址
		I2C_RD(RD|NACK|STO);//读取存储数据
		
		#20000;
		$stop;	
	end
	
	task I2C_WR;
		input [5:0] cmd;
		input [7:0] wr_DATA;
		begin
			Cmd = cmd;
			TX_DATA = wr_DATA;
			Go = 1'b1;#20;
			Go = 1'b0;#20;
			wait(Trans_Done);
			#200;
		end
	endtask
	
	task I2C_RD;
		input [5:0] cmd;
		begin	
			Cmd = cmd;
			Go = 1'b1;#20;
			Go = 1'b0;#20;
			wait(Trans_Done);
			#200;
		end
	endtask
endmodule

3.仿真结果

在这里插入图片描述
从仿真结果可以看出,写入EEPROM存储单元8’h3B的数据,和从中读取的数据一致,均为8’h76。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FPGA(Field-Programmable Gate Array)是一种可编程的逻辑器件,可以用来实现各种数字电路。I2C(Inter-Integrated Circuit)是一种串行通信协议,常用于连接集成电路芯片之间的通信。 在FPGA中实现I2C的单次读写操作,你需要完成以下几个步骤: 1. 配置I2C控制器:首先,你需要在FPGA中实现一个I2C控制器,该控制器负责管理I2C总线的时序和通信协议。你可以使用硬件描述语言(如VHDL或Verilog)来编写控制器的代码,定义I2C总线的时钟频率、地址、数据等。 2. 发送起始条件:在进行I2C通信时,首先需要发送起始条件。起始条件是一个高电平到低电平的跳变,表示通信的开始。你可以通过在FPGA控制I2C总线的时钟和数据线来实现起始条件的发送。 3. 发送设备地址和读/写位:接下来,你需要发送要访问的设备地址和读/写位。设备地址是目标设备在I2C总线上的唯一标识符,读/写位用于指示是读取数据还是写入数据。 4. 读写数据:根据你的需求,你可以选择进行读取操作或写入操作。对于读取操作,你需要等待目标设备发送数据,并通过I2C总线接收数据。对于写入操作,你需要将要写入的数据发送到目标设备。 5. 发送停止条件:完成数据的读写后,你需要发送停止条件。停止条件是一个低电平到高电平的跳变,表示通信的结束。通过控制I2C总线的时钟和数据线,你可以实现停止条件的发送。 需要注意的是,具体实现方法可能会因硬件平台和使用的开发工具而有所不同。你可以参考FPGA厂商提供的资料和示例代码来帮助你完成这些步骤。另外,还可以使用一些开源的IP核(如OpenCores提供的I2C IP核)来简化开发过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值