verilog i2c 通用控制模块

        突然发现好久没写文章了,今天就写一篇关于i2c的通用控制模块。

i2c协议保护起始,数据传输,ACK或NACK,和传输终止信号。以下是对应的时序图:

verilog i2c 通用控制模块_第1张图片

 在SCL为高的情况下,SDA由高跳到低,这是起始信号,之后在时钟为低电平时更新数据,在高电平时数据保持稳定,每一次传输8bit数据之后是ACK信号,在受到ACK信号后可以选择结束通信或者继续传输数据,这是基本的i2c协议。

而eeprom的i2c有些许不一样,所以在设计时应考虑加入一些冗余以兼容eeprom的读写。

eeprom的读操作如下图:

verilog i2c 通用控制模块_第2张图片

在向SDA写入设备地址和设备内的存贮地址时,待ACK信号后不是继续传输地址,而是发出起始信号,在接受读操作时需要回复NACK信号(ACK信号是接受方在SCL为高电平时写入低电平数据,NACK,则相反),之后便是终止信号。

基于上述内容,设计如下状态机:

 

verilog i2c 通用控制模块_第3张图片

 

这里WAIT_READ和WAIT_WRITE是为满足时序,实现传输8bit后给外部模块相应的信号,WAIT状态将接受外部的信号以确定下一步的动作是继续传输数据还是终止传输,内部保留超时机制,确保状态机不会被卡死,并且存在跳到START的可能,主要是为了兼容eeprom的读操作而特意设计的,W_ACK,R_ACK是为区分上一步的操作是都还是写,WRITE和READ是读写状态,在需要回复ACK信号时,如果回复NACK信号会跳转到STOP状态以结束通信,整个状态跳转机流程基本介绍完毕,以下是对应的实现代码:


module i2c_ctrl(clk,rst_n,din,w_next,r_next,t_done,ena,restart,i2c_scl,i2c_sdl,done,tc,dout,sdl_ena);
	input clk;
	input rst_n;
	input [7:0]din;
	input w_next;
	input r_next;
	input t_done;	
	input ena;
	input restart;

	inout i2c_sdl;
	
	output i2c_scl;
	output reg done;
	output reg tc;
	output reg [7:0]dout;
	
	reg [10:0]state;
	reg [1:0]scl_cnt;
	reg [7:0]wait_cnt;
	reg [3:0]bit_cnt;
	reg [7:0]d_in_tmp;	
	reg sdl_data;
	output reg sdl_ena;
	
	parameter WAIT_TIMEOUT	= 100;


	localparam	IDLE		= 11'b000_0000_0001 ,			
				START		= 11'b000_0000_0010 ,
				WRITE		= 11'b000_0000_0100 ,
				W_ACK		= 11'b000_0000_1000 ,
				R_ACK		= 11'b000_0001_0000 ,
				WAIT_WRITE	= 11'b000_0010_0000 ,
				WAIT_READ	= 11'b000_0100_0000 ,	
				WAIT		= 11'b000_1000_0000 ,
				READ		= 11'b001_0000_0000 ,
				NACK		= 11'b010_0000_0000 ,
				STOP		= 11'b100_0000_0000 ;

	assign i2c_sdl=(sdl_ena)?sdl_data:1'bz;
	assign i2c_scl=(scl_cnt>2'd1)?1'b1:1'b0;

	always @(posedge clk,negedge rst_n) begin
		if(!rst_n)
			scl_cnt <= 0;
		else if(state ==IDLE )
			scl_cnt <= 2'd2 ;		//keep i2c_scl in high when state is IDLE 
		else if( state !=WAIT && state!=WAIT_READ)
			scl_cnt <= scl_cnt +1'b1;
		else
			scl_cnt <= 0;
	end
	
	always @(posedge clk,negedge rst_n) begin
		if(!rst_n)
			wait_cnt <= 0;
		else if(state == WAIT)
			wait_cnt <= wait_cnt +1'b1;
		else
			wait_cnt <= 0;
	end

	always @(posedge clk,negedge rst_n) begin
		if(!rst_n)	
			bit_cnt <= 0;
		else if(state ==READ ||state ==WRITE)
			begin
				if(scl_cnt == 2'd2)
					bit_cnt <= bit_cnt +1'b1;
			end
		else 
			bit_cnt <= 0;
	end

	/**************state change************************/
	always @(posedge clk,negedge rst_n) begin
		if(!rst_n )
			state <= IDLE ;
		else case(state)
			IDLE		:	if(ena)
								state <= START ;
									
			START		:	if(scl_cnt== 2'd3)
								state <= WRITE ;
				
			WRITE		:	if(bit_cnt==4'd8 && scl_cnt == 2'd3)
								state <= W_ACK ;
				
			W_ACK		:	if(scl_cnt==2'd2)
								begin
									if(i2c_sdl == 1'b0)
										state <= WAIT_WRITE ;
									else
										state <= STOP ;
								end
			R_ACK		:	if(bit_cnt==2'd3)
								state <= WAIT ;
								
			WAIT_WRITE	:	state <= WAIT ;
			
			WAIT		:	if(t_done || wait_cnt== WAIT_TIMEOUT)
								state <= STOP ;
							else if(restart )
								state <= START ;
							else if(w_next)
								state <=WRITE ;
							else if(r_next)
								state <= READ ;
							else 
								state <=WAIT ;
				
			READ		:	if(bit_cnt ==4'd8 && scl_cnt==2'd3)
								state <= WAIT_READ ;
				
			WAIT_READ	:	if(r_next)
								state <= R_ACK;
							else
								state <= NACK ;
			
			NACK		:	if(scl_cnt == 2'd3)
								state <= STOP	;
		    
			STOP		:	if(scl_cnt == 2'd3)
								state <= IDLE ;
		
			default		:	state <= IDLE ;
		endcase
	end

	/***************state output*************************/
	always @(posedge clk,negedge rst_n) begin
		if(!rst_n )
			begin
				d_in_tmp <= 0;
				sdl_ena <= 1'b1;
				sdl_data <= 1'b1;
				tc <= 1'b0;
				dout <= 8'b0;
				done <= 1'b0;
			end
		else case(state)
			IDLE		:	begin
								sdl_ena <= 1'b1;
								sdl_data <= 1'b1;
								if(ena)
									d_in_tmp <= din;
							end
						
			START		:	begin
								sdl_ena <= 1'b1;
								if(scl_cnt < 2'd2)
									sdl_data <= 1'b1;
								else
									sdl_data <= 1'b0;
							end
							
			WRITE		:	begin
								sdl_ena <= 1'b1;
								if(scl_cnt == 2'd0)
									begin
										sdl_data <= d_in_tmp[7];
										d_in_tmp <= {d_in_tmp[6:0],1'b0};
									end
							end		
							
		    W_ACK		:	if(scl_cnt < 2'd2)
								sdl_ena <= 1'b0;
							else
								begin
									done <=1'b1;
									sdl_ena <= 1'b0;
								end
		    			
		    R_ACK		:	if(scl_cnt <= 2'd2)
								begin
									sdl_ena <= 1'b1;
									sdl_data <= 1'b0;
								end
							else
								sdl_ena <= 1'b0;

		    WAIT		:	begin
								if(w_next)
									d_in_tmp <= din;
								done <= 1'b0;
							end
		    			
		    READ		:	begin
								sdl_ena <= 1'b0;
								if(scl_cnt==2'd2)
									dout <= {dout[6:0],i2c_sdl};
								if(bit_cnt == 4'd8 && scl_cnt == 2'd2)
									done <= 1'b1;
								else	
									done <= 1'b0;
							end

		    WAIT_READ	:	;
			
			WAIT_WRITE	:	done <= 1'b0;
								
		    NACK		:	begin
								sdl_ena <= 1'b1;
								sdl_data <= 1'b1;
							end
		    			
		    STOP		:	begin
								sdl_ena <= 1'b1;
								if(scl_cnt <2'd2)
									sdl_data <= 1'b0;
								else 
									begin
										if(scl_cnt == 2'd2)
											tc <= 1'b1;
										else
											tc <= 1'b0;
										sdl_data <= 1'b1;
									end
								end
			default		: 	;
		endcase
	end

endmodule 

这里对模块进行简要的说明,clk使用1MHz的信号,并对其进行4分频以产生SCL时钟,w_next,r_next是外部模块在接受到内部模块发出8bit传输完成done信号后,控制模块是否继续写入数据还是继续读数据,t_done信号为是否接受通信的控制信号,信号优先级高于读写信号,restart信号就是为了兼容eeprom读写而特地设计的,done信号是8bit传输完成信号,tc为传输完成信号,对应于一次传输任务完成,外部模块可以根据此信号进行下一次传输,dout是读到的数据,在done为高时有效,din是外部输入数据。

以下是对应的测试代码,使用System Veilog 编写的:

module i2c_ctrl_tb(i2c_sdl);



	reg clk;
	reg rst_n;
	reg [7:0]din;
	reg w_next;
	reg r_next;
	reg t_done;	
	reg ena;
	reg restart;

	inout i2c_sdl;
	
	wire i2c_scl;
	wire done;
	wire tc;
	wire [7:0]dout;
	wire sdl_ena;

	i2c_ctrl inst(.*);

	initial clk=0;
	
	always #10 clk=~clk;
	byte cnt;
	initial begin
		rst_n =1'b0;
		din=8'h3d;
		ena = 1'b1;
		w_next = 1'b0;
		r_next = 1'b0;
		restart = 1'b0;
		t_done = 1'b0;
		#20 rst_n=1'b1;
		

	
	end
	
	assign i2c_sdl=(~sdl_ena)?1'b0:1'bz;

	always @(posedge clk, negedge rst_n)begin
		if(!rst_n)
			cnt <= 0;
		else if(done)
			begin
				cnt <= cnt+1'b1;
				w_next <= 1'b1;
				if (cnt==0)
					din <= 8'h27;
				else if(cnt==1)
					din <= 8'h31;
				else if(cnt ==2)
					din <= 8'h4f;
				else
					begin
						din <= 0;
						w_next <= 1'b0;
						t_done <= 1'b1;
						ena <= 1'b0;
					end
							
			end
	end
endmodule 

注意,这个测试代码只对写操作进行测试,读没有测试,超时机制也没有测试,eeprom的读所要求的跳转restart也没有测试,主要是太麻烦了,一个人没有这么多的时间。

图片来源于野火fpga书籍,

状态机截图于quartus 软件。        

                                                                                                                                               完

                                                                                                                                        2021/11/06

你可能感兴趣的:(verilog,systemverilog)