FPGA实现串口与iic控制器总结(2)

       在剖析了《深入浅出玩转FPGA》的串口代码和IIC控制器代码、xilinx官方的xilinx的iic控制器(参见书《FPGACPLD设计工具──Xilinx ISE使用详解》)、《片上系统设计思想与源代码分析》一书中带有wishbone接口的iic控制器后,本文尝试对以上做一些总结,并分析不同的iic控制器的实现区别。

      上一讲,我们分析了串口代码的实现以及testbench的设计,这一讲,我们开始分析不同的iic控制器的实现并总结。 下一讲将继续另2种带有总线接口和更充分实现iic协议的例子,敬请期待。
       
2、IIC控制器

      2.1 特权的iic控制器

       关于IIC的协议就不再赘述了,可以看看网上的一些资料,主要是scl和sda信号线的高低电平的变化的要求和一些约定。实际上的iic是支持共享总线,但是任何时候只能由一个主设备,这样就需要仲裁。但是特权的例子只是模拟了iic的时序接口,做了一个最基本的控制,这点需要明确。他的例程实现的效果是按下键1,iic控制器写入EEPROM芯片AT24CXX,按下键2,读出存储的值并显示在数码管上。

     2个文件和一个顶层文件:

module iic_top(
			clk,rst_n,
			sw1,sw2,
			scl,sda,
			sm_cs1_n,sm_cs2_n,sm_db			//数码管片选
		);	
input clk;		// 50MHz
input rst_n;	//复位信号,低有效
input sw1,sw2;	//按键1、2,(1按下执行写入操作,2按下执行读操作)
output scl;		// 24C02的时钟端口
inout sda;		// 24C02的数据端口
output sm_cs1_n,sm_cs2_n;	//数码管片选信号,低有效
output[6:0] sm_db;	//7段数码管(不包括小数点)
wire[7:0] dis_data;		//在数码管上显示的16进制数
iic_com		iic_com(
				.clk(clk),
				.rst_n(rst_n),
				.sw1(sw1),
				.sw2(sw2),
				.scl(scl),
				.sda(sda),
				.dis_data(dis_data)
				);
led_seg7	led_seg7(
				.clk(clk),
				.rst_n(rst_n),
				.dis_data(dis_data),
				.sm_cs1_n(sm_cs1_n),
				.sm_cs2_n(sm_cs2_n),
				.sm_db(sm_db)	
				);
endmodule	

改顶层模块比较好理解。注意,scl这里仅为out型,即只能做主设备。

FPGA实现串口与iic控制器总结(2)_第1张图片

先看看iic_com实现

输入输出接口:

input clk;		// 50MHz
input rst_n;	//复位信号,低有效
input sw1,sw2;	//按键1、2,(1按下执行写入操作,2按下执行读操作)
output scl;		// 24C02的时钟端口,很明显只是作为主机,并不能接收时钟
inout sda;		// 24C02的数据端口
output[7:0] dis_data;	//数码管显示的数据
输出8位给数码管显示

按键检测:

//--------------------------------------------
		//按键检测
reg sw1_r,sw2_r;	//键值锁存寄存器,每20ms检测一次键值 
reg[19:0] cnt_20ms;	//20ms计数寄存器
//这个的习惯是,计数与判断分成2个always来写
always @ (posedge clk or negedge rst_n)
	if(!rst_n) cnt_20ms <= 20'd0;
	else cnt_20ms <= cnt_20ms+1'b1;	//不断计数
always @ (posedge clk or negedge rst_n)
	if(!rst_n) begin
			sw1_r <= 1'b1;	//键值寄存器复位,没有键盘按下时键值都为1
			sw2_r <= 1'b1;
		end
	else if(cnt_20ms == 20'hfffff) begin			//50mhz,这个时间为10的6次方乘以20ns即为20ms
			sw1_r <= sw1;	//按键1值锁存
			sw2_r <= sw2;	//按键2值锁存
		end
//这个按键锁存并不到位,并没有与的操作
这里并没有像特权按键检测之前那样对键值做一些与的操作,防止被检测到触发了多次,所以并没有什么效果,单此处也并不在意触发的次数的多少。具体的按键消抖可以看特权专门的那一章,其思路是收到的键值效果与20ms前比较。

iic支持的传输速度有多种,此处设置为100k,下面即是对时钟分频的处理,需在产生这么时钟,并区分出上升沿,下降沿等。

//---------------------------------------------
		//分频部分
reg[2:0] cnt;	// cnt=0:scl上升沿,cnt=1:scl高电平中间,cnt=2:scl下降沿,cnt=3:scl低电平中间
reg[8:0] cnt_delay;	//500循环计数,产生iic所需要的时钟
reg scl_r;		//时钟脉冲寄存器
always @ (posedge clk or negedge rst_n)
	if(!rst_n) cnt_delay <= 9'd0;
	else if(cnt_delay == 9'd499) cnt_delay <= 9'd0;	//计数到10us为scl的周期,即100KHz,50MHZ
	else cnt_delay <= cnt_delay+1'b1;	//时钟计数
always @ (posedge clk or negedge rst_n) begin
	if(!rst_n) cnt <= 3'd5;
	else begin
		case (cnt_delay)					//将一个50%的时钟clk划分为4个时间点,画图可知
			9'd124:	cnt <= 3'd1;	//cnt=1:scl高电平中间,用于数据采样
			9'd249:	cnt <= 3'd2;	//cnt=2:scl下降沿
			9'd374:	cnt <= 3'd3;	//cnt=3:scl低电平中间,用于数据变化
			9'd499:	cnt <= 3'd0;	//cnt=0:scl上升沿
			default: cnt <= 3'd5;
			endcase
		end
end
`define SCL_POS	     (cnt==3'd0)    //cnt=0:scl上升沿
`define SCL_HIG      (cnt==3'd1)    //cnt=1:scl高电平中间,用于数据采样
`define SCL_NEG      (cnt==3'd2)    //cnt=2:scl下降沿
`define SCL_LOW      (cnt==3'd3)   //cnt=3:scl低电平中间,用于数据变化
always @ (posedge clk or negedge rst_n)
 if(!rst_n) 
      scl_r <= 1'b0;
 else if(cnt==3'd0) 
       scl_r <= 1'b1;       //scl信号上升沿
      else if(cnt==3'd2)
          scl_r <= 1'b0;    //scl信号下降沿
assign scl = scl_r;                      //产生iic所需要的时钟
500对应50M的时钟正好是100khz,这里的思路是吧一个时钟周期分成4个间距相等的关键的时间节点,自己画图就知道了,后续这些宏定义即代表应该是电平变化的那个阶段。scl根据阶段输出0或1,很好理解。

//---------------------------------------------
		//需要写入24C02的地址和数据				
`define	DEVICE_READ		8'b1010_0001	//被寻址器件地址(读操作)
`define DEVICE_WRITE	8'b1010_0000	//被寻址器件地址(写操作)
`define	WRITE_DATA		8'b0001_0001	//写入EEPROM的数据
`define BYTE_ADDR		8'b0000_0011	//写入/读出EEPROM的地址寄存器	
reg[7:0] db_r;		//在IIC上传送的数据寄存器
reg[7:0] read_data;	//读出EEPROM的数据寄存器

这里是对开发板上的at24c02的实际情况的设定,根据该芯片手册,读写的地址如上。

接下来就是状态机了:

FPGA实现串口与iic控制器总结(2)_第2张图片

//---------------------------------------------
		//读、写时序
parameter 	IDLE 	= 4'd0;		
parameter 	START1 	= 4'd1;				//scl为高电平时的sda表示启动
parameter 	ADD1 	= 4'd2;				//add1表示的写地址位,会延续8个
parameter 	ACK1 	= 4'd3;				//等待应答
parameter 	ADD2 	= 4'd4;
parameter 	ACK2 	= 4'd5;
parameter 	START2 	= 4'd6;
parameter 	ADD3 	= 4'd7;
parameter 	ACK3	= 4'd8;
parameter 	DATA 	= 4'd9;
parameter 	ACK4	= 4'd10;
parameter 	STOP1 	= 4'd11;
parameter 	STOP2 	= 4'd12;

reg[3:0] cstate;	//状态寄存器
reg sda_r;		//输出数据指示寄存器
reg sda_link;	//输出数据sda信号inout方向控制位,1表示从发数据到ee中		
reg[3:0] num;	//计数8位

always @ (posedge clk or negedge rst_n) begin
	if(!rst_n) begin
			cstate <= IDLE;
			sda_r <= 1'b1;
			sda_link <= 1'b0;
			num <= 4'd0;
			read_data <= 8'b0000_0000;
		end
	else 	  
		case (cstate)
			IDLE:	begin
					sda_link <= 1'b1;			//数据线sda为output
					sda_r <= 1'b1;
					if(!sw1_r || !sw2_r) begin	//SW1,SW2键有一个被按下			
						db_r <= `DEVICE_WRITE;	//送器件地址(写操作)
						cstate <= START1;		
						end
					else cstate <= IDLE;	//没有任何键被按下
				end
			START1: begin
					if(`SCL_HIG) begin		//scl为高电平期间
						sda_link <= 1'b1;	//数据线sda为output
						sda_r <= 1'b0;		//拉低数据线sda,产生起始位信号,364行assign输出该信号
						cstate <= ADD1;
						num <= 4'd0;		//num计数清零
						end
					else cstate <= START1; //等待scl高电平中间位置到来
				end
			ADD1:	begin
					if(`SCL_LOW) begin						//scl为低电平期间才可以变化数据
							if(num == 4'd8) begin			//发完8个,不再发了,准备接收应答
									num <= 4'd0;			//num计数清零
									sda_r <= 1'b1;
									sda_link <= 1'b0;		//sda置为高阻态(input)
									cstate <= ACK1;
								end
							else begin
									cstate <= ADD1;
									num <= num+1'b1;
									case (num)
										4'd0: sda_r <= db_r[7];			//高位在前
										4'd1: sda_r <= db_r[6];
										4'd2: sda_r <= db_r[5];
										4'd3: sda_r <= db_r[4];
										4'd4: sda_r <= db_r[3];
										4'd5: sda_r <= db_r[2];
										4'd6: sda_r <= db_r[1];
										4'd7: sda_r <= db_r[0];
										default: ;
										endcase
							//		sda_r <= db_r[4'd7-num];	//送器件地址,从高位开始
								end
						end
			//		else if(`SCL_POS) db_r <= {db_r[6:0],1'b0};	//器件地址左移1bit
					else cstate <= ADD1;
				end
			ACK1:	begin
					if(/*!sda*/`SCL_NEG) begin	//注:24C01/02/04/08/16器件可以不考虑应答位,下降沿表示应答
							cstate <= ADD2;	//从机响应信号
							db_r <= `BYTE_ADDR;	// 要写或读的地址		
						end
					else cstate <= ACK1;		//等待从机响应			//一直等,没有冗余机制
				end
			ADD2:	begin
					if(`SCL_LOW) begin							//下降沿开始数据变化,发送地址
							if(num==4'd8) begin	
									num <= 4'd0;			//num计数清零
									sda_r <= 1'b1;
									sda_link <= 1'b0;		//sda置为高阻态(input)
									cstate <= ACK2;
								end
							else begin
									sda_link <= 1'b1;		//sda作为output
									num <= num+1'b1;
									case (num)
										4'd0: sda_r <= db_r[7];
										4'd1: sda_r <= db_r[6];
										4'd2: sda_r <= db_r[5];
										4'd3: sda_r <= db_r[4];
										4'd4: sda_r <= db_r[3];
										4'd5: sda_r <= db_r[2];
										4'd6: sda_r <= db_r[1];
										4'd7: sda_r <= db_r[0];
										default: ;
										endcase
							//		sda_r <= db_r[4'd7-num];	//送EEPROM地址(高bit开始)		
									cstate <= ADD2;					
								end
						end
			//		else if(`SCL_POS) db_r <= {db_r[6:0],1'b0};	//器件地址左移1bit
					else cstate <= ADD2;				
				end
			ACK2:	begin
					if(/*!sda*/`SCL_NEG) begin		//从机响应信号
						if(!sw1_r) begin			//这个建是写
								cstate <= DATA; 	//写操作,进入data状态
								db_r <= `WRITE_DATA;	//写入的数据							
							end	
						else if(!sw2_r) begin			//读
								db_r <= `DEVICE_READ;	//送器件地址(读操作),特定地址读需要执行该步骤以下操作
								cstate <= START2;		//读操作
							end
						end
					else cstate <= ACK2;	//等待从机响应,一直等着,知道案件按下
				end
			START2: begin	//读操作起始位
					if(`SCL_LOW) begin			//为低的时候等高电平
						sda_link <= 1'b1;	//sda作为output
						sda_r <= 1'b1;		//拉高数据线sda
						cstate <= START2;
						end
					else if(`SCL_HIG) begin	//scl为高电平中间
						sda_r <= 1'b0;		//拉低数据线sda,产生起始位信号
						cstate <= ADD3;
						end	 
					else cstate <= START2;
				end
			ADD3:	begin	//送读操作地址
					if(`SCL_LOW) begin
							if(num==4'd8) begin	
									num <= 4'd0;			//num计数清零
									sda_r <= 1'b1;
									sda_link <= 1'b0;		//sda置为高阻态(input)
									cstate <= ACK3;
								end
							else begin
									num <= num+1'b1;
									case (num)
										4'd0: sda_r <= db_r[7];		//读器件地址
										4'd1: sda_r <= db_r[6];
										4'd2: sda_r <= db_r[5];
										4'd3: sda_r <= db_r[4];
										4'd4: sda_r <= db_r[3];
										4'd5: sda_r <= db_r[2];
										4'd6: sda_r <= db_r[1];
										4'd7: sda_r <= db_r[0];
										default: ;
										endcase									
								//	sda_r <= db_r[4'd7-num];	//送EEPROM地址(高bit开始)		
									cstate <= ADD3;					//等到满足送出8位再来			
								end
						end
				//	else if(`SCL_POS) db_r <= {db_r[6:0],1'b0};	//器件地址左移1bit
					else cstate <= ADD3;				
				end
			ACK3:	begin									//拉低表示应答,其实应该是从机拉低表示应答,这里全部是主机自己根据时间来掐时间认为正确
					if(/*!sda*/`SCL_NEG) begin
							cstate <= DATA;	//从机响应信号
							sda_link <= 1'b0;				//数据线为z,表示准备接受数据
						end
					else cstate <= ACK3; 		//等待从机响应
				end
			DATA:	begin
					if(!sw2_r) begin	 //读操作
							if(num<=4'd7) begin
								cstate <= DATA;
								if(`SCL_HIG) begin	
									num <= num+1'b1;	
									case (num)
										4'd0: read_data[7] <= sda;		//先接受高位,eeprom中读出的数据 
										4'd1: read_data[6] <= sda;  
										4'd2: read_data[5] <= sda; 
										4'd3: read_data[4] <= sda; 
										4'd4: read_data[3] <= sda; 
										4'd5: read_data[2] <= sda; 
										4'd6: read_data[1] <= sda; 
										4'd7: read_data[0] <= sda; 
										default: ;
										endcase																		
					//				read_data[4'd7-num] <= sda;	//读数据(高bit开始)
									end
				//				else if(`SCL_NEG) read_data <= {read_data[6:0],read_data[7]};	//数据循环右移
								end
							else if((`SCL_LOW) && (num==4'd8)) begin
								num <= 4'd0;			//num计数清零
								cstate <= ACK4;
								end
							else cstate <= DATA;
						end
					else if(!sw1_r) begin	//写操作
							sda_link <= 1'b1;	
							if(num<=4'd7) begin
								cstate <= DATA;
								if(`SCL_LOW) begin
									sda_link <= 1'b1;		//数据线sda作为output
									num <= num+1'b1;
									case (num)
										4'd0: sda_r <= db_r[7];
										4'd1: sda_r <= db_r[6];
										4'd2: sda_r <= db_r[5];
										4'd3: sda_r <= db_r[4];
										4'd4: sda_r <= db_r[3];
										4'd5: sda_r <= db_r[2];
										4'd6: sda_r <= db_r[1];
										4'd7: sda_r <= db_r[0];
										default: ;
										endcase									
								//	sda_r <= db_r[4'd7-num];	//写入数据(高bit开始)
									end
			//					else if(`SCL_POS) db_r <= {db_r[6:0],1'b0};	//写入数据左移1bit
							 	end
							else if((`SCL_LOW) && (num==4'd8)) begin
									num <= 4'd0;
									sda_r <= 1'b1;
									sda_link <= 1'b0;		//sda置为高阻态
									cstate <= ACK4;
								end
							else cstate <= DATA;
						end
				end
			ACK4: begin
					if(/*!sda*/`SCL_NEG) begin
//						sda_r <= 1'b1;
						cstate <= STOP1;						
						end
					else cstate <= ACK4;
				end
			STOP1:	begin
					if(`SCL_LOW) begin
							sda_link <= 1'b1;
							sda_r <= 1'b0;
							cstate <= STOP1;
						end
					else if(`SCL_HIG) begin
							sda_r <= 1'b1;	//scl为高时,sda产生上升沿(结束信号)
							cstate <= STOP2;
						end
					else cstate <= STOP1;
				end
			STOP2:	begin
					if(`SCL_LOW) sda_r <= 1'b1;
					else if(cnt_20ms==20'hffff0) cstate <= IDLE;		//防止死循环,又跳到了idle状态
					else cstate <= STOP2;
				end
			default: cstate <= IDLE;
			endcase
end
assign sda = sda_link ? sda_r:1'bz;<span style="white-space:pre">			</span>//准备接收数据时为z状态,表示悬空的三态
assign dis_data = read_data;<span style="white-space:pre">				</span>//eeprom中读出的数据给输出给数码管显示
状态机还是很清晰的,idle状态下等待按键进入状态循环。根据时序,start信号应该是产生一个下降沿,即sda_link(这个是控制输出的)为1,sda_r为0,进入add1状态,否则一直等到那个分频后的iic的clock的高电平然后该控制器主动发出一个起始信号。add1中等到低电平期间发送器件地址(协议规定sda只能在scl为低时变化),直至8位发完,回到高阻态。记住前一个状态中sda_link为1,所以还是对外输出。ack1状态是等待从机应答,协议规定是从机拉低sda(因为sda是被上拉的,如果从机释放后会被上拉电阻拉到1,但是从机有意继续拉低一个时钟周期,则主机可以判断是从机再应答)产生应答,这里我们不考虑从机应答,设想一切正常,则我们只需等到那个时间点,自动跳转到下一个状态。add2则是要写的地址了,与前面类似,等待发完。ack2再次接受应答,还是一样的处理,这个时候要根据按键与否来判断下面进入哪个状态了,1键是写,db_r为要写入的值,2键是读,得从新发器件地址(影响中iic对读有一些与写有不一样的地方)。按照写的流程则进入data状态。按照读的流程,则再来一次start2,add3依旧是器件地址,接下来ack3,这样才进入data状态。读和写2种方式进入的data状态,得判断。若读操作,从sda读数据到寄存器中,注意是每个时钟高电平期间(SCL_HIG),因为iic协议规定高电平期间锁存数据,先读的是高位,ack3中sda_link已经为0了,表示接受数据,为高阻态。若写,先发高位,注意是低电平SCL_LOW,低电平才可数据变化,sda_link需设为1。等待ack4的响应,stop1为产生了一个停止位。延时一段时间20ms,stop2中状态又回到idle。

所以整个的思路还是很清晰的,但是并没有接受实践的从机的应答,也没有冗余机制,通信失败了怎么办,而是主机一直在哪个地方等,可能会卡死,所以这个仅仅算一个iic时序模拟器吧,有点这种感觉。

最后数据传给dis_data到数码管显示。

简单说下数码管显示:

module led_seg7(
			clk,rst_n,
			dis_data,
			sm_cs1_n,sm_cs2_n,sm_db	
		);

input clk;		// 50MHz
input rst_n;	// 复位信号,低有效

input[7:0] dis_data;	//显示数据	
output sm_cs1_n,sm_cs2_n;	//数码管片选信号,低有效
output[6:0] sm_db;	//7段数码管(不包括小数点)

reg[7:0] cnt;
always @ (posedge clk or negedge rst_n)
	if(!rst_n) cnt <= 8'd0;
	else cnt <= cnt+1'b1;			
	
//-------------------------------------------------------------------------------
/*	共阴极 :不带小数点
              ;0,  1,  2,  3,  4, 5,  6,  7,  
      db      3fh,06h,5bh,4fh,66h,6dh,7dh,07h 
              ;8,  9, a,  b,   c,  d,  e,  f , 灭   
      db      7fh,6fh,77h,7ch,39h,5eh,79h,71h,00h   */
parameter	seg0	= 7'h3f,
			seg1	= 7'h06,
			seg2	= 7'h5b,
			seg3	= 7'h4f,
			seg4	= 7'h66,
			seg5	= 7'h6d,
			seg6	= 7'h7d,
			seg7	= 7'h07,
			seg8	= 7'h7f,
			seg9	= 7'h6f,
			sega	= 7'h77,
			segb	= 7'h7c,
			segc	= 7'h39,
			segd	= 7'h5e,
			sege	= 7'h79,
			segf	= 7'h71;

reg[6:0] sm_dbr;		//7段数码管(不包括小数点)
	
wire[3:0] num;		//显示数据
assign num = cnt[7] ? dis_data[7:4] : dis_data[3:0];				//用2个数码管,每个数码管为4位
assign sm_cs1_n = cnt[7];		//数码管1常开			//一下只显示一个数码管,开一个就关另一个,50%到50%的显示比例
assign sm_cs2_n = ~cnt[7];		//数码管2常开
	
always @ (posedge clk)
		case (num)	//NUM值显示在两个数码管上
			4'h0: sm_dbr <= seg0;
			4'h1: sm_dbr <= seg1;
			4'h2: sm_dbr <= seg2;
			4'h3: sm_dbr <= seg3;
			4'h4: sm_dbr <= seg4;
			4'h5: sm_dbr <= seg5;
			4'h6: sm_dbr <= seg6;
			4'h7: sm_dbr <= seg7;
			4'h8: sm_dbr <= seg8;
			4'h9: sm_dbr <= seg9;
			4'ha: sm_dbr <= sega;
			4'hb: sm_dbr <= segb;
			4'hc: sm_dbr <= segc;
			4'hd: sm_dbr <= segd;
			4'he: sm_dbr <= sege;
			4'hf: sm_dbr <= segf;
			default: ;
			endcase

assign sm_db = sm_dbr;
 
endmodule

cnt溢出计数,仅根据最高位是0还是1来实现究竟显示哪个数码管,所以是50%的显示,扫描的时间也可根据cnt与时钟频率来算。比较简单。

总的来说,思路还是比较清晰的,这是一个状态机的例子,与上一讲的串口的实现方式不一样,一个是状态跳转,一个是多个always块中的信号传递来实现控制。这里面也有作者的很多个人的习惯,比如cnt‘的always作者习惯分成2部分来写等等。

但是这只是一个非常粗陋的iic控制器,不接受从机的应答,相对于完整的iic协议来说,非常不完整,并且移植性也不好。我们在学单片机时,知道有的单片机有iic,我们只需在写51代码时,操纵寄存器即可,那么那个部件是怎么实现,显然跟我们这里讲的这种方式是不一样的。

下一讲就来讲讲xilinx官方的一个完整的iic的控制器和or1200中带有wishbone总线接口的iic控制器的实现,相比于这种方式完整得多。


你可能感兴趣的:(FPGA实现串口与iic控制器总结(2))