Advertisement

[FPGA系列] I2C项目实战总结

阅读量:

一、基本概念

I2C 通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线, 只需要两根线即可在连接于总线上的器件之间传送信息。
I2C 通讯设备之间的常用连接方式,如图所示。

它的物理层 有如下特点:

(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。

(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间访问。

(4) 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

(5) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

(6) 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。

(7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

想要使用FPGA进行I2C通信,我们首先要搞清楚它的协议时序,如下图所示。

I2C协议整体时序大致可以分为4部分:

空闲状态 :在此状态下串口时钟信号 SCL 和串行数据信号 SDA 均保持高电平,此时无 I2C 设备工作。

起始状态 :SCL 依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有 I2C 设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。

数据读/写状态 :在该状态,可以进行数据读写操作,包括写入从设备地址、数据寄存地址、写入数据、读取数据...,具体时序如图所示。

可以看到,在SCL为高电平时,SDA上的数据要保持不变,在SCL为低电平时,SDA上的数据可以发生变化,每次传送数据 8 bit为一组,每传送一组后等待响应位(一般为从设备提供),然后传送下一组,再等响应位,等待全部传送完成后进入停止状态。

停止状态 :一次读/写完成后,进入停止状态,在SCL为高电平时,SDA产生一个上升沿,I2C总线转回空闲状态。

二、I2C读/写操作

前面了解了I2C协议的整体时序,下面详细讲解具体的读写操作。

首先讲解 I2C 写操作,由于一次写入数据量的不同, I2C 的写操作可分为单字节写操作和页写操作,在这里我们主要讲解单字节写操作。

整个阶段我们可以分为三部分:

①发送从设备地址(7bit)+写标志位(1bit)。

②发送数据寄存地址,如果数据寄存地址为单字节的话,就只有8bit,双字节的话为16bit,先发送高8位,再发送低8位。

③发送要写入的数据8bit,高位在前,低位在后。

I2C读操作可以分为随机读操作和顺序读操作,这里我们主要讲解随机读操作,它的操作时序与写时序很相似,如图所示。

整个阶段我们可以分为四部分(或者说是2+2):

①发送从设备地址(7bit)+写标志位(1bit)。

②发送数据寄存地址,如果数据寄存地址为单字节的话,就只有8bit,双字节的话为16bit,先发送高8位,再发送低8位。

③ 发送从设备地址(7bit)+读标志位(1bit)。

④读取数据8bit。

可以看到,读操作前两部分和写操作是完全相同的,需要注意 的有:

(1) 写操作有一个起始位和一个停止位,而读操作有两个起始位和一个停止位;

(2) 在读操作读取数据完成后,等待的响应位并不是从设备提供(从设备产生低电平),而是主设备主动拉高,然后进入停止状态。

三、思路整理

根据I2C协议的操作时序,我们可以使用状态机来很好地完成,如图所示。

除了必要的 i2c_scl 和 i2c_sda 端口,我们产生一个 i2c_clk 时钟信号,该信号时钟频率为i2c_scl 的4倍,这样我们就可以很方便的对 SCL 线和 SDA 线的时序进行控制。

编写 i2c_ctrl 模块来进行时序控制,如图所示。

其端口说明如图所示。

四、Verilog程序编写

复制代码
 module i2c_ctrl

    
 #(
    
 	parameter	DEVICE_ADDR 	= 	7'b1010_000		,	//i2c设备地址
    
 	parameter	SYS_CLK_FREQ	=	26'd50_000_000	,	//输入系统时钟频率
    
 	parameter	SCL_FREQ		=	18'd250_000			//i2c设备scl时钟频率
    
 )
    
 (
    
 	input wire 			clk			,	//输入系统时钟50MHZ
    
 	input wire 			rst			,	//复位,低电平有效
    
 	input wire 			i2c_start	,	//输入i2c触发信号
    
 	input wire 			wr_en		,	//输入写使能信号
    
 	input wire 			rd_en		,	//输入读使能信号
    
 	input wire [15:0]   byte_addr	,	//输入i2c字节地址
    
 	input wire [7:0] 	wr_data		,	//输入i2c设备数据
    
 	input wire 			addr_num	,	//输入i2c字节地址字节数,0-单字节 1-两字节
    
  
    
 	output reg 			i2c_scl		,	//i2c时钟线
    
 	inout  wire 		i2c_sda		,	//i2c数据线
    
 	output reg 			i2c_clk		,	//i2c驱动时钟
    
 	output wire 		i2c_end		,	//i2c一次读/写操作完成
    
 	output reg [7:0] 	rd_data			//i2c读取数据
    
 );
    
  
    
 localparam	I2C_CLK_CNT	=	(SYS_CLK_FREQ/SCL_FREQ) >> 2'd3;	//i2c_clk时钟频率为i2c_scl的4倍
    
  
    
 localparam	IDLE			=	4'd00,	//空闲状态
    
 			START_1			=	4'd01,	//i2c启动,准备写
    
 			SEND_D_ADDR		=	4'D02,	//发送写设备地址
    
 			ACK_1			=	4'D03,	//等待响应
    
 			SEND_B_ADDR_H	=	4'D04,	//发送字节地址高8位
    
 			ACK_2			=	4'D05,	//等待响应
    
 			SEND_B_ADDR_L	=	4'D06,	//发送字节地址低8位
    
 			ACK_3			=	4'D07,	//等待响应
    
 			WR_DATA			=	4'D08,	//发送设备数据
    
 			ACK_4			=	4'D09,	//等待响应
    
 			START_2			=	4'D10,	//i2c启动,准备读
    
 			SEND_RD_ADDR	=	4'D11,	//发送读设备地址
    
 			ACK_5			=	4'D12,	//等待响应
    
 			RD_DATA			=	4'D13,	//读取设备数据
    
 			N_ACK			=	4'D14,	//主设备拉高响应
    
 			STOP			=	4'D15;	//i2c停止
    
  
    
 reg	[3:0]	state,next_state	;
    
 reg	[7:0]   i2c_clk_cnt			;
    
 reg	[1:0]   i2c_clk_num			;
    
 wire		clk_num_flag		;
    
 reg [3:0]	bit_cnt				;
    
 reg			ack					;
    
 reg			i2c_sda_out			;
    
 reg	[7:0]   data_out			;
    
  
    
  
    
 //i2c_clk_cnt:i2c工作频率计数
    
 always@(posedge clk or negedge rst)
    
 	if(~rst)
    
 		i2c_clk_cnt <= 8'b0;
    
 	else if(i2c_clk_cnt == I2C_CLK_CNT - 1'd1)
    
 		i2c_clk_cnt <= 8'b0;
    
 	else
    
 		i2c_clk_cnt <= i2c_clk_cnt + 1'b1;
    
 		
    
 //i2c_clk:i2c工作频率
    
 always@(posedge clk or negedge rst)
    
 	if(~rst)
    
 		i2c_clk <= 1'b0;
    
 	else if(i2c_clk_cnt == I2C_CLK_CNT - 1'd1)
    
 		i2c_clk <= ~i2c_clk;
    
  
    
 //clk_num_flag:i2c_clk_num周期计数使能标志
    
 assign clk_num_flag = (state == IDLE)?1'b0:1'b1;
    
 		
    
 //i2c_clk_num:i2c_clk周期计数
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		i2c_clk_num <= 2'b0;
    
 	else if(i2c_clk_num == 2'd3)
    
 		i2c_clk_num <= 2'b0;
    
 	else if(clk_num_flag == 1'b1)
    
 		i2c_clk_num <= i2c_clk_num + 1'b1;
    
 	else
    
 		i2c_clk_num <= 2'b0;
    
  
    
 //bit_cnt:bit计数
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		bit_cnt <= 4'b0;
    
 	else if(bit_cnt == 4'd15)
    
 		bit_cnt <= 4'b0;
    
 	else if(state == SEND_D_ADDR || state == SEND_B_ADDR_H ||
    
 				state == SEND_B_ADDR_L || state == WR_DATA ||
    
 				state == SEND_RD_ADDR || state == RD_DATA)
    
 				if(i2c_clk_num == 2'd3)
    
 					bit_cnt <= bit_cnt + 1'b1;
    
 				else
    
 					bit_cnt <= bit_cnt;
    
 	else
    
 		bit_cnt <= 4'b0;
    
 		
    
 //state:现态转移
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		state <= IDLE;
    
 	else
    
 		state <= next_state;
    
  
    
 //next_state:次态改变 
    
 always@(*)
    
 	case(state)	
    
 		IDLE			: next_state = (i2c_start == 1'b1)?START_1:IDLE;							//空闲状态
    
 		
    
 		START_1			: next_state = (i2c_clk_num == 2'd3)?SEND_D_ADDR:START_1;							//i2c启动,准备写
    
 		
    
 		SEND_D_ADDR		: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_1:SEND_D_ADDR;	//发送写设备地址
    
 		
    
 		ACK_1			: if((i2c_clk_num == 'd3) && (ack == 'd0))												//等待响应
    
 								if(addr_num == 'd1)
    
 									next_state = SEND_B_ADDR_H;
    
 								else
    
 									next_state = SEND_B_ADDR_L;
    
 								else
    
 									next_state = ACK_1;		
    
 									
    
 		SEND_B_ADDR_H	: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_2:SEND_B_ADDR_H;	//发送字节地址高8位
    
 		
    
 		ACK_2			: next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?SEND_B_ADDR_L:ACK_2;		//等待响应
    
 		
    
 		SEND_B_ADDR_L	: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_3:SEND_B_ADDR_L;	//发送字节地址低8位
    
 		
    
 		ACK_3			: if((i2c_clk_num == 'd3) && (ack == 'd0))												//等待响应
    
 								if(wr_en == 'b1)
    
 									next_state = WR_DATA;
    
 								else if(rd_en == 'b1)
    
 									next_state = START_2;
    
 								else
    
 									next_state = ACK_3;
    
 								else
    
 									next_state = ACK_3;									
    
 									
    
 		WR_DATA			: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_4:WR_DATA;			//发送设备数据
    
 		
    
 		ACK_4			: next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?STOP:ACK_4;					//等待响应
    
 		
    
 		START_2			: next_state = (i2c_clk_num == 2'd3)?SEND_RD_ADDR:START_2;							//i2c启动,准备读
    
 		
    
 		SEND_RD_ADDR	: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_5:SEND_RD_ADDR;	//发送读设备地址
    
 		
    
 		ACK_5			: next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?RD_DATA:ACK_5;				//等待响应
    
 		
    
 		RD_DATA			: next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?N_ACK:RD_DATA;			//读取设备数据
    
 		
    
 		N_ACK			: next_state = (i2c_clk_num == 'd3)?STOP:N_ACK;										//主设备拉高响应
    
 		
    
 		STOP			: next_state = (i2c_clk_num == 'd3)?IDLE:STOP;											//i2c停止		
    
 		
    
 		default			: next_state = IDLE;
    
 	endcase
    
  
    
 //data_out:输出数据缓存
    
 always@(*)
    
 	case(state)
    
 		SEND_D_ADDR		: data_out = {DEVICE_ADDR|3'b011,1'b0};
    
 		SEND_B_ADDR_H	: data_out = byte_addr[15:8];
    
 		SEND_B_ADDR_L	: data_out = byte_addr[7:0];
    
 		WR_DATA			: data_out = wr_data;
    
 		SEND_RD_ADDR	: data_out = {DEVICE_ADDR|3'b011,1'b1};
    
 		default			: data_out = 8'b1;
    
 	endcase
    
  
    
 //i2c_sda_out:i2c输出
    
 always@(*)
    
 	case(state)
    
 		IDLE			: i2c_sda_out <= 1'b1;										//空闲状态	
    
 		START_1			: i2c_sda_out <= (i2c_clk_num <= 'd1)?1'b1:1'b0;	//i2c启动,准备写
    
 		SEND_D_ADDR		: i2c_sda_out <= data_out['d7-bit_cnt];				//发送写设备地址									
    
 		SEND_B_ADDR_H	: i2c_sda_out <= data_out['d7-bit_cnt];				//发送字节地址高8位										
    
 		SEND_B_ADDR_L	: i2c_sda_out <= data_out['d7-bit_cnt];				//发送字节地址低8位			
    
 		WR_DATA			: i2c_sda_out <= data_out['d7-bit_cnt];				//发送设备数据	
    
 		
    
 		START_2			: i2c_sda_out <= (i2c_clk_num <= 'd1)?1'b1:1'b0;	//i2c启动,准备读	
    
 		SEND_RD_ADDR	: i2c_sda_out <= data_out['d7-bit_cnt];				//发送读设备地址
    
 		N_ACK			: i2c_sda_out <= 1'b1;										//主设备拉高响应
    
 		STOP			: i2c_sda_out <= (i2c_clk_num >='d2)?1'b1:1'b0;		//i2c停止		
    
 		default			: i2c_sda_out <= 1'b1;
    
 	endcase
    
 		
    
 		
    
 //ack:响应信号
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		ack <= 1'b1;
    
 	else if(state == ACK_1 || state == ACK_2 || state == ACK_3 
    
 			|| state == ACK_4 || state == ACK_5)
    
 		ack <= i2c_sda;
    
 	else
    
 		ack <= 1'b1;
    
 		
    
 //i2c_sda
    
 assign i2c_sda = (state == ACK_1 || state == ACK_2 || state == ACK_3 || state == ACK_4 ||
    
 						state == ACK_5 || state == RD_DATA)?1'bz:i2c_sda_out;
    
  
    
 //i2c_scl
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		i2c_scl <= 1'b1;
    
 	else if(state != IDLE && state != STOP)
    
 		if(i2c_clk_num == 'd0 || i2c_clk_num == 'd1)
    
 			i2c_scl <= 1'b1;
    
 		else
    
 			i2c_scl <= 1'b0;
    
 	else
    
 		i2c_scl <= 1'b1;
    
 						
    
 //rd_data:i2c读取数据输出
    
 always@(posedge i2c_clk or negedge rst)
    
 	if(~rst)
    
 		rd_data <= 8'b0;
    
 	else if(i2c_clk_num == 'd1 && state == RD_DATA)
    
 		rd_data['d7-bit_cnt] <= i2c_sda;
    
 	else
    
 		rd_data <= rd_data;
    
  
    
 //i2c_end:一次读/写完成
    
 assign i2c_end = (state == STOP)?1'b1:1'b0;
    
 	
    
 endmodule

五、Testbench编写

复制代码
 `timescale 1ns/1ns

    
   3. module tb_i2c_ctrl
    
 ();
    
  
    
 reg			clk			;	//输入系统时钟50MHZ
    
 reg			rst			;	//复位,低电平有效
    
 reg			i2c_start	;	//输入i2c触发信号
    
 reg			wr_en		;	//输入写使能信号
    
 reg 		rd_en		;	//输入读使能信号
    
 reg [15:0] 	byte_addr	;	//输入i2c字节地址
    
 reg [7:0] 	wr_data		;	//输入i2c设备数据
    
 reg 		addr_num	;	//输入i2c字节地址字节数,0-单字节 1-两字节
    
 	
    
 wire 		i2c_scl		;	//i2c时钟线
    
 tri 		i2c_sda		;	//i2c数据线
    
 wire 		i2c_clk		;	//i2c驱动时钟
    
 wire 		i2c_end		;	//i2c一次读/写操作完成
    
 wire [7:0] 	rd_data		;	//i2c读取数据
    
  
    
 reg			i2c_sda_in	;
    
  
    
 initial
    
 	begin
    
 		clk <= 1'b1;
    
 		rst <= 1'b0;
    
 		i2c_start <= 1'b0;
    
 		wr_en <= 1'b0;
    
 		rd_en <= 1'b0;
    
 		byte_addr <= 16'b0;
    
 		wr_data <= 8'b0;
    
 		addr_num <= 1'b0;
    
 		i2c_sda_in <= 1'bz;
    
 		#40;
    
 		rst<= 1'b1;
    
 		#40;
    
 		i2c_start <= 1'b1;
    
 		wr_en <= 1'b1;
    
 		byte_addr <= 16'b1000_0100_0010_0001;
    
 		wr_data <= 8'b1010_1010;
    
 		addr_num <= 1'b1;
    
 		#600;
    
 		i2c_start <= 1'b0;
    
 		#35860;					//SEND_D_ADDR
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#32000;					//SEND_B_ADDR_H
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#32000;					//SEND_B_ADDR_L
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#32000;					//WR_DATA
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;	    //STOP
    
 		
    
 		#32000;
    
 		#500;
    
 		
    
 		i2c_start <= 1'b1;
    
 		wr_en	<= 1'b0;
    
 		rd_en <= 1'b1;
    
 		#600;						
    
 		i2c_start <= 1'b0;
    
 		
    
 		#35900;					//SEND_D_ADDR
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#32000;					//SEND_B_ADDR_H
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#32000;					//SEND_B_ADDR_L
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		i2c_sda_in <= 1'bz;
    
 		#36000;					//SEND_RD_ADDR
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;
    
 		
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b1;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b1;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b1;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b0;
    
 		#4000;					//RD_DATA
    
 		i2c_sda_in <= 1'b1;
    
 		#4000;					//RD_DATA
    
 		
    
 		i2c_sda_in <= 1'bz;
    
 		#4000;					//N_ACK
    
 								//STOP
    
 	end
    
  
    
 always #10 clk = ~clk;
    
 assign i2c_sda = i2c_sda_in;
    
  
    
 i2c_ctrl
    
 #(
    
 	7'b1010_000		,	//i2c设备地址
    
 	26'd50_000_000	,	//输入系统时钟频率
    
 	18'd250_000			//i2c设备scl时钟频率
    
 )
    
 i2c_ctrl_inst
    
 (
    
 	clk			,	//输入系统时钟50MHZ
    
 	rst			,	//复位,低电平有效
    
 	i2c_start	,	//输入i2c触发信号
    
 	wr_en		,	//输入写使能信号
    
 	rd_en		,	//输入读使能信号
    
 	byte_addr	,	//输入i2c字节地址
    
 	wr_data		,	//输入i2c设备数据
    
 	addr_num	,	//输入i2c字节地址字节数,0-单字节 1-两字节
    
 		
    
 	i2c_scl		,	//i2c时钟线
    
 	i2c_sda		,	//i2c数据线
    
 	i2c_clk		,	//i2c驱动时钟
    
 	i2c_end		,	//i2c一次读/写操作完成
    
 	rd_data			//i2c读取数据
    
 );
    
  
    
 endmodule

六、Modelsim仿真

在仿真中,我们的仿真顺序是先写操作,后读操作。

仿真中,写操作的一些参数:

设备地址+写标志位 = 1010_0110

byte_addr = 0x8421 ---> 1000_0100_0010_0001

wr_data = 0xaa ---> 1010_1010

读操作的参数:

设备地址+读标志位 = 1010_0111

i2c_sda = 0101_0101 ---> 0x55

仿真结果如图所示。

放大来看,写操作如图所示。

读操作如图所示。

从仿真效果来看,与我们之前设想的一样,实验成功。

参考资料:《FPGA Verilog开发实战指南——基于Altera EP4CE10》

全部评论 (0)

还没有任何评论哟~