08-Verilog学习-S_I2C总线协议

IIC协议

首先是比较好的博客与资料:

  1. https://www.cnblogs.com/xiaomeige/p/6509414.html
  2. https://www.cnblogs.com/microxiami/p/8527464.html
  3. I2C总线协议(中文版).pdf
    https://download.csdn.net/download/weixin_43499278/12275402
  4. I2C协议的实现(判断SCL).pdf
    https://download.csdn.net/download/weixin_43499278/12275402
  5. 夏宇闻《Verilog数字系统设计教程》——第16章

本次实践是I2C总线协议的硬件实现,其中本次实践只能用于,这些功能可以后续加入。本次实践I2C总线设计可具体分为以下部分(下述按此框架讲述):

  • 产生SCL
  • 构建输入/输出性SDA
  • 确定I2C总线协议的基础状态,以设计状态机跳转方式
  • 编写I2C单字节读/写状态机,以控制SDA产生相应时序

其核心思想:利用状态机的跳转来控制SDA、SCL产生符合I2C协议的时序,属于时序电路控制组合电路输出。

1、产生SCL

从参考资料2中,可知I2C总线工作的速度分为标准模式(100kbit/s)、快速模式(400kbit/s),因此SCL的速度受到限制。本次实践中,我采用了比I2C工作速度高2倍的时钟作为时钟输入,并且经过时钟二分频后,即可得到满足要求的SCL,具体实现如下(相当于时钟分频器):

/*产生SCL时钟:二分频*/

//下降沿时钟跳转:SCL中点检测SDA的电平
always@(negedge clk or negedge rst_n)
begin
	if(!rst_n)
		SCL <= 1;
	else
		SCL <= ~SCL;
end

重点: 采用clk下降沿进行时钟分频,由此后面模块使用clk上升沿判断时,可达到在SCL电平中点检测SDA的电平状态。

2.构建输入/输出性SDA

SDA是一个可输入/输出类型引脚,因此使用三态门的结构进行构建,同时硬件外部是将SDA线上拉的,所以具体实现方式如下:

/*组合逻辑控制产生电路*/
assign SDA = (Link_SDA)? SDA_reg:1'bz;

3.确定IIC总线协议的基础状态,设计状态机跳转方式

由参考资料1,可以得到I2C总线协议的基础状态为:起始信号、传输数据(控制字节/数据字节)、应答、停止信号。

以下是I2C单字节的读写操作顺序:

  • I2C写操作:

    起始信号->传输写控制字节->从机应答->传输数据字节->从机应答->停止信号

  • I2C读操作:

    起始信号->传输写控制字节->从机应答->传输器件存储字节->从机应答->起始信号->传输读控制字节->读数据->主机非应答->停止信号

由以上的读写顺序,结合I2C总线协议的基础状态,可得到如下的状态机跳转图:
08-Verilog学习-S_I2C总线协议_第1张图片
其中ack状态内包含了判断I2C总线协议读/写的跳转,为此状态机的要点。

4.编写IIC单字节读/写状态机,以控制SDA产生相应时序

此处的状态机,采用三段式状态机书写。其中重点是:

  1. 在trans_data状态中,判断读写,并将wr_flag/rd_flag赋值,用于ack状态内的判断和状态机的跳转判断。
  2. 应I2C的时序要求,在例如起始信号后必须传数据等,代码注释中有标注。
  3. 写/读数据都只能在SCL为低电平时,才能够变化。
  4. 使用任务task完成起始信号、串转并、并转串、停止信号。注意:使用task时须将从状态机复位。

具体硬件代码如下:

module I2C_Timing(
	input clk,		//时钟、复位
	input rst_n,
	
	input I2C_en,	//I2C使能
	
	input wr_en,	//写使能
	input rd_en,	//读使能
	
	inout SDA,
	output reg SCL,
	
	input  [7:0] ctrl_in,		//I2C控制字节输入
	input  [7:0] data_in,		//I2C数据字节输入
	output reg   done,			//测试信号输出
	output reg [7:0] data_out	//I2C数据输出
);

/*信号及寄存器定义*/
reg Link_SDA;				//Link_SDA=1时,SDA为输出;Link_SDA=0时,SDA为输入

reg SDA_reg;				//I2C的SDA输出寄存器

reg Finish;					//从状态机完成标志位

reg wr_falg;				//写标志位

reg [1:0]rd_falg;			//读标志位

reg [7:0] write_buf;		//数据写入缓冲寄存器

reg [7:0] read_buf;			//数据读出缓冲寄存器

reg [4:0] current_state;	//主状态机当前状态寄存器

reg [4:0] next_state;		//主状态机下一状态寄存器

reg [4:0] start_state;		//task:start状态寄存器

reg [4:0] stop_state;		//task:stop状态寄存器

reg [5:0] P2S_state;		//task:P2S状态寄存器

reg [5:0] S2P_state;		//task:S2P状态寄存器




//-----主状态机状态定义-----
parameter 
			idle		= 5'b0_0000,	//空闲状态
			gen_start  	= 5'b0_0001,	//产生起始信号
			trans_ctrl 	= 5'b0_0011,	//传送控制字节
			trans_data 	= 5'b0_0010,	//传送数据字节
			ack	       	= 5'b0_0110,	//应答
			gen_stop    = 5'b0_0111,	//产生停止信号
			rd_data		= 5'b0_0101;	//读取I2C数据

//-----从状态机状态定义-----
parameter
			start_idle	= 5'b1_0000,	//起始状态
			start_ready = 5'b1_0001,	//拉高SDA
			start_sda	= 5'b1_0011,	//拉低SDA,产生下降沿
			start_stop	= 5'b1_0010;	//停止状态

parameter
			stop_idle	= 5'b1_1000,	//起始状态
			stop_ready 	= 5'b1_1001,	//拉低SDA
			stop_sda	= 5'b1_1011,	//拉高SDA,产生上升沿
			stop_stop	= 5'b1_1010;	//停止状态
			
parameter 
			P2S_bit7	= 6'b10_0000,
			P2S_bit6	= 6'b10_0001,
			P2S_bit5	= 6'b10_0011,
			P2S_bit4	= 6'b10_0010,
			P2S_bit3	= 6'b10_0110,
			P2S_bit2	= 6'b10_0101,
			P2S_bit1	= 6'b10_0100,
			P2S_bit0	= 6'b10_1100,
			P2S_stop	= 6'b10_1101;

parameter 
			S2P_bit7	= 6'b11_0000,
			S2P_bit6	= 6'b11_0001,
			S2P_bit5	= 6'b11_0011,
			S2P_bit4	= 6'b11_0010,
			S2P_bit3	= 6'b11_0110,
			S2P_bit2	= 6'b11_0101,
			S2P_bit1	= 6'b11_0100,
			S2P_bit0	= 6'b11_1100,
			S2P_stop	= 6'b11_1101;

//-----常量定义-----
parameter YES	= 1;
parameter NO	= 0;




/*组合逻辑控制产生电路*/
assign SDA = (Link_SDA)? SDA_reg:1'bz;



/*产生SCL时钟:二分频*/

//下降沿时钟跳转:SCL中点检测SDA的电平
always@(negedge clk or negedge rst_n)
begin
	if(!rst_n)
		SCL <= 1;
	else
		SCL <= ~SCL;
end




/*主状态机:三段式*/

//-----第一段-----
always@(posedge clk or negedge rst_n)
begin
	if(!rst_n)
		current_state <= idle;
	else
		current_state <= next_state;
end	

//-----第二段-----
always@(current_state or Finish)
begin
    case(current_state)
        idle: next_state <= gen_start;
        gen_start:
            begin
                if(Finish==1)
                    next_state <= trans_ctrl;	//产生开始信号完成,跳转传送控制字节		
                else
                    next_state <= gen_start;
            end
        trans_ctrl:
            begin
                if(Finish==1)
                    begin	
                        if(rd_en && (rd_falg==2))	//I2C读操作,第二次产生开始信号和传送控制字节完成,
                            next_state <= rd_data;	//开始接收SDA数据
                        else
                            next_state <= ack;		
                    end
                else
                    begin
                        next_state <= trans_ctrl;
                    end
            end
        ack:
            begin
                if(Finish==1)
                    begin
                        if(wr_en && wr_falg)		
                            begin
                                next_state <= gen_stop;		//I2C写操作完成
                            end
                        else if(rd_en && (rd_falg==1))
                            begin
                                next_state <= gen_start;	//I2C读操作,完成控制字节和器件存储字节的传送,
                            end							  //开始产生开始信号,再写如I2C读操作的控制字节
                        else
                            next_state <= trans_data;
                    end
                else
                    begin
                        next_state <= ack;
                    end
            end
        trans_data:
            begin
                if(Finish==1)
                    begin
                        next_state <= ack;
                    end
                else
                    begin
                        next_state <= trans_data;
                    end
            end
        rd_data:
            begin
                if(Finish==1)
                    next_state <= gen_stop;
                else
                    next_state <= rd_data;
            end
        gen_stop:
            begin
                if(Finish==1)
                    begin
                        next_state <= idle;
                    end
                else
                    next_state <= gen_stop;
            end
        default: next_state <= idle;
    endcase
end

//-----第三段-----
always@(posedge clk or negedge rst_n)
begin
	if(!rst_n)
	begin
		Link_SDA <= NO;
		Finish	 <= 0;
		SDA_reg	 <= 0;
		done     <= 0;
		wr_falg	 <= 0;
		rd_falg  <= 0;
		write_buf<= 0;
		read_buf <= 0;
	end	
	else
	begin
		case(current_state)
			idle:
			begin
				Link_SDA    <= YES;
				SDA_reg		<= 1;
				Finish      <= 0;
				write_buf	<= ctrl_in;
				start_state <= start_sda;			//为满足I2C时序,须下一个clk产生开始信号
			end
			gen_start:
			begin
				if(Finish==0)
					start;
				else
				begin
					Finish    <= 0;
					Link_SDA  <= YES;			
					SDA_reg   <= write_buf[7];		//为满足I2C时序,start信号后,必紧跟数据最高位
					P2S_state <= P2S_bit6;
				end
			end
			trans_ctrl:
			begin
				if(Finish==0)
					P2S;
				else
				begin
					done     <= 1;		//测试信号
					
					Finish 	 <= 0;
					Link_SDA <= NO;
					
					if(rd_en && (rd_falg==2))		//I2C读操作,第二次产生开始信号和传送控制字节完成
					begin						   //开始接收SDA数据
						rd_falg     <= 0;
						Link_SDA    <= NO;
						read_buf[7] <= SDA;			//满足I2C时序,start信号后,必紧跟数据最高位
						S2P_state   <= S2P_bit6;
					end
					else
						;
				end
			end
			ack:
			begin
				if((SDA==0)&&(SCL==1))
				begin
					done      <= 0;		//测试信号
					
					Finish    <= 1;
					write_buf <= data_in;	
				end
				else
				begin
					Finish <= 0;
					
					if(wr_en && wr_falg)			//I2C写操作完成
					begin
						wr_falg	   <= 0;
						Link_SDA   <= YES;
						SDA_reg	   <= 0;
						stop_state <= stop_sda;		//为满足I2C时序,须下一个clk产生停止信号
					end
					else if(rd_en && (rd_falg==1))	//I2C读操作,完成控制字节和器件存储字节的传送
					begin
						rd_falg	    <= rd_falg+1;
						write_buf	<= ctrl_in;
						start_state <= start_sda;	//为满足I2C时序,须下一个clk产生开始信号
						Link_SDA    <= YES;
					end
					else
					begin
						Link_SDA  <= YES;
						P2S_state <= P2S_bit6;
						SDA_reg	  <= write_buf[7];	//为满足I2C时序,应答后须立即传送数据
					end
				end
			end
			trans_data:
			begin
				if(Finish==0)
					P2S;
				else
				begin
					done <= 1;		//测试信号
					
					if(wr_en)
						wr_falg <= 1;
					else if(rd_en)
						rd_falg	<= 1;
					else 
					begin
						wr_falg	<= 0;
						rd_falg <= 0;
					end
					
					Finish 	 <= 0;
					Link_SDA <= NO;
				end
			end
			rd_data:
			begin
				if(Finish==0)
					S2P;
				else
				begin
					data_out   <= read_buf;		//I2C读一字节数据完成
					Link_SDA   <= YES;
					Finish 	   <= 0;
					SDA_reg	   <= 1;
					stop_state <= stop_idle;	//I2C读一字节后,主机发送非应答(拉高SDA),并延拍
			end
			gen_stop:
			begin
				if(Finish==0)
					stop;
				else
				begin
					rd_falg	  <= 0;
					wr_falg	  <= 0;
					Finish    <= 0;
					Link_SDA  <= NO;
				end
			end
		default: Link_SDA <= NO;
		endcase
	end
end




/*任务定义*/

//-----产生起始信号-----
task start;
	case(start_state)
		start_idle:
		begin
			Finish		<= 0;
			Link_SDA	<= YES;
			SDA_reg  	<= 1;
			start_state <= start_sda;
		end
		start_ready:
		begin
			Link_SDA 	<= YES;
			start_state <= start_sda;
		end
		start_sda:
		begin
			if(SCL)
			begin
				Link_SDA  	<= YES;
				SDA_reg   	<= 0;
				Finish 	  	<= 1;
				start_state <= 5'b1_1111;	//起始信号产生完成,设置为无效态
			end
			else
			begin
				start_state<= start_sda;
			end
		end
		default:
		begin
			start_state <= 5'b1_1111;		//无效态
		end
	endcase

endtask

//-----产生停止信号-----
task stop;
	case(stop_state)
		stop_idle:
		begin
			Finish		<= 0;
			Link_SDA	<= YES;
			SDA_reg  	<= 1;
			stop_state  <= stop_ready;
		end
		stop_ready:
		begin
			Link_SDA	<= YES;
			SDA_reg		<= 0;
			stop_state  <= stop_sda;
		end
		stop_sda:
		begin
			if(SCL)
			begin
				Link_SDA <= YES;
				SDA_reg  <= 1;
				Finish 	 <= 1;
				stop_state <= 5'b1_1111;
			end
			else
			begin
				stop_state<= stop_sda;
			end
		end
		default:
		begin
			stop_state  <= 5'b1_1111;		//无效态
		end
	endcase
endtask

//-----并行数据转串行数据-----
task P2S;
	case(P2S_state)
		P2S_bit6:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[6];
				P2S_state  <= P2S_bit5;
			end
			else
			begin
				P2S_state  <= P2S_bit6;
			end
		end
		P2S_bit5:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[5];
				P2S_state  <= P2S_bit4;
			end
			else
			begin
				P2S_state  <= P2S_bit5;
			end
		end
		P2S_bit4:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[4];
				P2S_state  <= P2S_bit3;
			end
			else
			begin
				P2S_state  <= P2S_bit4;
			end
		end
		P2S_bit3:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[3];
				P2S_state  <= P2S_bit2;
			end
			else
			begin
				P2S_state  <= P2S_bit3;
			end
		end
		P2S_bit2:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[2];
				P2S_state  <= P2S_bit1;
			end
			else
			begin
				P2S_state  <= P2S_bit2;
			end
		end
		P2S_bit1:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[1];
				P2S_state  <= P2S_bit0;
			end
			else
			begin
				P2S_state  <= P2S_bit1;
			end
		end
		P2S_bit0:
		begin
			if(!SCL)
			begin
				Link_SDA   <= YES;
				SDA_reg    <= write_buf[0];
				P2S_state  <= P2S_stop;
			end
			else
			begin
				P2S_state  <= P2S_bit0;
			end
		end
		P2S_stop:
		begin
			Finish <= 1;
			if(!SCL)
			begin
				Link_SDA   <= NO;
				P2S_state  <= 6'b11_1111;	//完成传输,设置为无效态
			end
			else
			begin
				P2S_state <= P2S_stop;
			end
		end
	
		default:
		begin
			P2S_state  <= 6'b11_1111;		//设置为无效态
		end
	endcase

endtask

//-----串行数据转并行数据-----
task S2P;
	case(S2P_state)
		S2P_bit6:
		begin
			done <= 0;
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[6] <= SDA;
				S2P_state   <= S2P_bit5;
			end
			else
			begin
				S2P_state  <= S2P_bit6;
			end
		end
		S2P_bit5:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[5] <= SDA;
				S2P_state   <= S2P_bit4;
			end
			else
			begin
				S2P_state  <= S2P_bit5;
			end
		end
		S2P_bit4:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[4] <= SDA;
				S2P_state   <= S2P_bit3;
			end
			else
			begin
				S2P_state  <= S2P_bit4;
			end
		end
		S2P_bit3:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[3] <= SDA;
				S2P_state   <= S2P_bit2;
			end
			else
			begin
				S2P_state  <= S2P_bit3;
			end
		end
		S2P_bit2:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[2] <= SDA;
				S2P_state   <= S2P_bit1;
			end
			else
			begin
				S2P_state  <= S2P_bit2;
			end
		end
		S2P_bit1:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[1] <= SDA;
				S2P_state   <= S2P_bit0;
			end
			else
			begin
				S2P_state  <= S2P_bit1;
			end
		end
		S2P_bit0:
		begin
			if(!SCL)
			begin
				Link_SDA    <= NO;
				read_buf[0] <= SDA;
				S2P_state   <= S2P_stop;
			end
			else
			begin
				S2P_state  <= S2P_bit0;
			end
		end
		S2P_stop:
		begin
			Finish <= 1;
			if(!SCL)
			begin
				Link_SDA   <= NO;
				S2P_state  <= 6'b11_1111;	//完成传输,设置为无效态
			end
			else
			begin
				S2P_state <= S2P_stop;
			end
		end
	
		default:
		begin
			S2P_state  <= 6'b11_1111;		//设置为无效态
		end
	endcase
endtask

endmodule

5.仿真时序图

为了方便仿真,我在硬件代码中加入了done这个测试信号,用于在仿真时,给予应答信号(拉低SDA)

1. I2C写仿真
08-Verilog学习-S_I2C总线协议_第2张图片

2.I2C读仿真
08-Verilog学习-S_I2C总线协议_第3张图片
3.仿真代码

module I2C_Timing_tb;
	reg clk;
	reg rst_n;
	reg I2C_en;
	reg wr_en;
	reg rd_en;
	
	reg  uSDA;
	wire SDA;
	
	wire SCL;
	
	reg  [7:0] ctrl_in;
	reg  [7:0] data_in;
	wire [7:0] data_out;
	wire done;
	
	pullup(SDA);

//inout型仿真要求
assign SDA = uSDA?1'b0:1'bz;

//reg define
reg uuSDA;

I2C_Timing uI2C_Timing(
	.clk(clk),			//时钟、复位
	.rst_n(rst_n),
	
	.I2C_en(I2C_en),	//I2C使能
	.wr_en(wr_en),		//写使能
	.rd_en(rd_en),		//读使能
	
	.SDA(SDA),
	.SCL(SCL),
	
	.ctrl_in(ctrl_in),
	.data_in(data_in),
	.done(done),
	.data_out(data_out)
);


always #50 clk = ~clk;

always@(clk)
begin
	if(done)
		uSDA <= 1;
	else
		uSDA <= 0;
end

initial
begin
	#100
	clk 	= 0;
	rst_n 	= 1;
	uSDA	= 0;
	I2C_en  = 0;
	ctrl_in = 8'b1000_0001;	
	data_in = 8'b1000_1001;
	#150
	rst_n	= 0;
	#50
	rst_n	= 1;
	#100
	wr_en   = 0;
	rd_en	= 1;
	#100
	I2C_en  = 1;
end

endmodule

6.可改进的地方

  1. 再写Top,把data、I2C_en、wr_en、rd_en接口留给用户即可。
  2. 在ack状态中,须再加入若无应答,则直接结束,并报告给用户。
  3. 增加多字节读写的功能。

你可能感兴趣的:(Verilog项目实践)