FPGA实现IIC驱动模块设计

(附源码)FPGA实现IIC通信,看完你就懂了!!!

1.前言

本次主要学习IIC通信协议的FPGA实现,并使用modelsim仿真,由于手中没有IIC相关的外设,所以本次暂时不进行板载验证,只进行时序逻辑仿真。

2.IIC协议简介

IIC通信协议是一种串行通信总线,但其只有一根数据线,属于半双工通信协议,适用于传输距离较短的场景下使用。IIC由数据线SDA和时钟线SCL组成,数据线既可以用来发送数据,也可以用来接收数据,数据传输速率在标准模式下可以达到100kbit/s,在快速模式下可以达到400kbit/s,在高速模式下可以达到3.4Mbit/s,IIC的数据线和时钟线可以都会接上拉电阻,保证在没有进行数据传输时引脚电平处于高电平状态。

IIC支持多主和主从两种工作方式,其通常工作在主从工作模式,在主从工作模式下,系统只有一个主机,其他期间都是IIC总线的从机,在该工作模式下,主机产生时钟信号并启动数据发送和停止。IIC工作总线时序如下图所示。空闲状态数据线和时钟线均处于高电平,当相进行数据传输时,需要先把SDA数据线拉低,产生一个起始信号,当想停止数据传输时,将SDA数据线拉高,产生一个停止信号,此时再次回到空闲状态,有没有发现看到这里数据传输格式和串口通信有点像呢?不过这个可比串口通信复杂。

IIC通信包括读和写,这两个功能都是在同一条数据线上完成的,通过不同的读写位来控制。IIC通信开始后,会首先发送一个8位数据来选择器件以及读写控制,具体格式如下:

开始位-x6-x5-x4-x3-x2-x1-x0-R/W-ACK应答位

其中x0-x6位器件地址,这跟使用的具体器件有关,R/W为读写控制位,为1时表示主机对从机进行读操作,为0时表示主机对从机进行写操作,每发送一帧8位数据都需要从机应答,相反,进行读操作时,每读完8位数据,需要主机给予相应的应答信号。

IIC单次写时序图如下图所示。首先SDA数据线拉低发送起始信号,SCL时钟开启,SDA数据线首先发送期间地址和写命令,器件地址位变化只能在SCL电平为低电平时变化,因为SCL电平为高电平时从机会从SDA读取数据,当发送完一帧数据之后,从机会发送应答信号,主机接收到应答信号之后会继续发送下一帧数据,下面就是进行“虚写”,之所以称之为“虚写”是因为当我们需要向从机某个地址写数据时,需要先发送该地址,即“虚写”,之后发送要写的地址的高8位和低8位,分贝得到从机应答之后就可以将需要写的数据发送过去,写完之后如果不需要写数据了,就将电平拉高并且停止产生时钟,即发送停止信号,一次写操作完成。

一次写操作可以在指定地址写一个8位数据,当我们需要在连续的地址写多个8位数据时,是不是每次都需要发送器件地址和写地址再发送要写的数据呢?答案是否定的,当我们需要写的地址是连续的时,可以采用连续写的方式,即只发送一次器件地址和起始写地址就可以了,时序图如下图所示。在一次写数据完成之后,不发送停止信号,继续发送数据,这是会自动向开始地址的下一个地址写数据,直到发送停止信号。

同理,IIC读操作也是相同的操作。其时序图如下图所示。读操作同样首先是发送器件地址和要读的地址(此时处于写操作模式),之后重新发送起始信号、器件地址和读信号,就可以在指定的地址进行读数据了,读完一帧8位数据之后,需要主机房应答信号,从机收到应答信号之后才会继续发送数据,结束之后主机发送停止信号结束。同理,连续读同样是主机不发送停止信号继续读下一个地址的数据,直到发送停止信号。

至此,IIC协议时序部分就介绍到这里。

3.程序设计

FPGA程序设计主要在一个iic_driver模块中实现IIC在指定地址读和写一帧8位数据。程序中需要注意一个点就是inout引脚SDA,当读数据的时候读到8位数据需要输出一个应答信号,当写数据的时候,写完8位数据需要等待接收应答信号。inout数据只能通过assign赋值,一般通过三态门实现,当需要发送数据的时候为输出状态,当需要接收数据的时候为高阻态,这具体用法在程序中有体现。仿真发送一个8位数据如下图所示。

仿真接收一个8位数据如下图所示,可以看到接收完数据之后才更新8位数据。

下面贴一下代码供大家参考一下。

iic_driver.v

/*
	功能:IIC驱动模块设计
	作者:王志川
	时间:2024.7.5
*/
/*
	进度时间戳:
	2024.7.4 晚上实现发送器件地址+起始地址
	2024.7.5早上实现发送1个8位数据,并成功发送停止位。
	2024.7.5下午实现读取指定地址1个8位数据,并成功发送停止位。
*/
module iic_driver(
	input CLK,                      //50MHZ时钟信号
	input RST,                      //复位信号
	inout SDA,                      //IIC数据引脚
	input iic_rw,                   //IIC读写控制信号
	input iic_start,                //IIC起始信号,为1有效
	input [15:0]iic_data_addr,      //IIC要写数据的地址
	input [7:0]iic_data_w,          //IIC要写的数据
	output reg [7:0]iic_data_r,     //IIC读出的数据
	output reg [3:0]read_flag,           //读数据状态标志
	output reg iic_stop,            //IIC停止信号
	output reg write_done,          //IIC一次8位数据写完成标志位
	output reg SCL                  //IIC时钟引脚
);

//IIC时钟信号产生
parameter [8:0]scl_max=9'd250;//标准模式,100kbit/s
//parameter [8:0]scl_max=9'd63;//快速模式,400kbit/s
//parameter [8:0]scl_max=9'd7;//高速模式,3.4Mbit/s
reg [8:0]scl_cnt;//时钟计数周期
reg scl_en = 1'd0;//时钟使能信号
reg [3:0]sda_state;//数据发送状态
reg [3:0]cnt;//发送数据的位数
always@(posedge CLK or negedge RST)
begin
	if(!RST)begin
		SCL <= 1'd1;
		scl_cnt <= 9'd0;
	end
	else if(scl_en == 1'd1 && iic_start == 1'd1 && (iic_rw != 1'd1 || sda_state != 4'd3 || cnt != 4'd0 || scl_cnt != 9'd0))begin
		if(scl_cnt == scl_max-1)begin//计数到最大值
			SCL <= ~SCL;//时钟翻转
			scl_cnt <= 9'd0;//计数清零
		end
		else
			scl_cnt <= scl_cnt + 1'd1;
	end
	else begin
		SCL = 1'd1;
		scl_cnt <= 9'd0;
	end
end

//数据发送计时产生
parameter [8:0]sda_max=9'd500;//标准模式,100kbit/s
//parameter [8:0]sda_max=9'd125;//快速模式,400kbit/s
//parameter [8:0]sda_max=9'd15;//高速模式,3.4Mbit/s
reg [8:0]sda_cnt;//时钟计数周期
always@(posedge CLK or negedge RST)
begin
	if(!RST)begin 
		sda_cnt <= 9'd0;
		cnt <= 4'd0;
		sda_state <= 4'd0;
		iic_stop <= 1'd0;
		//last_cnt <= 4'd0;
	end
	//写命令
	else if(iic_start == 1'd1 && iic_rw == 1'd0)begin
		if(sda_cnt == sda_max-1)begin//,由于本次时发送最大值,不是时钟电平转换最大值,所以计数值是scl的两倍,计数到最大值
				sda_cnt <= 9'd0;//计数清零
				if(cnt == 4'd9 && sda_state == 4'd0 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
					sda_state <= 4'd1;
					cnt <= 4'd0;
					//last_cnt <= 4'd0;
				end
				else if(cnt == 4'd8 && sda_state == 4'd1 && SDA == 1'd0)begin//要写的地址高位完成,并且接收到应答
					sda_state <= 4'd2;
					cnt <= 4'd0;
					//last_cnt <= 4'd0;
				end
				else if(cnt == 4'd8 && sda_state == 4'd2 && SDA == 1'd0)begin//要写的地址低位完成,并且接收到应答
					cnt <= 4'd0;
					sda_state <= 4'd3;
					//last_cnt <= 4'd0;
				end//虚写操作完成
				else if(cnt == 4'd9 && sda_state == 4'd3 && SDA == 1'd0)begin//该地址8位数据发送完毕,需要发送停止信号
					cnt <= 4'd0;
					iic_stop <= 1'd1;
					//last_cnt <= 4'd0;
				end
				else
					cnt <= cnt + 1'd1;
			end
//			else if(sda_cnt == sda_max-2)begin
//				last_cnt <= cnt + 1'd1;
//				sda_cnt <= sda_cnt + 1'd1;
//			end
			else
				sda_cnt <= sda_cnt + 1'd1;
	end
	//读命令
	else if(iic_start == 1'd1 && iic_rw == 1'd1)begin
		if(sda_cnt == sda_max-1)begin//,由于本次时发送最大值,不是时钟电平转换最大值,所以计数值是scl的两倍,计数到最大值
				sda_cnt <= 9'd0;//计数清零
				if(cnt == 4'd9 && sda_state == 4'd0 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
					sda_state <= 4'd1;
					cnt <= 4'd0;
					//last_cnt <= 4'd0;
				end
				else if(cnt == 4'd8 && sda_state == 4'd1 && SDA == 1'd0)begin//写地址高位完成,并且接收到应答
					sda_state <= 4'd2;
					cnt <= 4'd0;
					//last_cnt <= 4'd0;
				end
				else if(cnt == 4'd8 && sda_state == 4'd2 && SDA == 1'd0)begin//写地址低位完成,并且接收到应答
					cnt <= 4'd0;
					sda_state <= 4'd3;
					//last_cnt <= 4'd0;
				end//虚写操作完成
				else if(cnt == 4'd10 && sda_state == 4'd3 && SDA == 1'd0)begin//器件写地址完成,并且接收到应答
					sda_state <= 4'd4;
					cnt <= 4'd0;
					//last_cnt <= 4'd0;
				end
				else if(cnt == 4'd9 && sda_state == 4'd4)begin//读取八位数据完成,发送停止信号
					cnt <= 4'd0;
					iic_stop <= 1'd1;
					//last_cnt <= 4'd0;
				end
				else
					cnt <= cnt + 1'd1;
			end
//			else if(sda_cnt == sda_max-2)begin
//				last_cnt <= cnt + 1'd1;
//				sda_cnt <= sda_cnt + 1'd1;
//			end
			else
				sda_cnt <= sda_cnt + 1'd1;
	end
end
always@(*)begin
	//接收写的应答
	if(iic_rw == 1'd0)begin
		if(cnt == 4'd9 && sda_state == 4'd0)//器件写地址完成
			write_done <= 1'd1;
		else if(cnt == 4'd8 && sda_state == 4'd1)//写地址高位完成
			write_done <= 1'd1;
		else if(cnt == 4'd8 && sda_state == 4'd2)//写地址低位完成
			write_done <= 1'd1;//虚写操作完成
		else if(cnt == 4'd8 && sda_state == 4'd3)//发送八位数据完成
			write_done <= 1'd1;//发送八位数据完成
		else
			write_done <= 1'd0;
	end
	//接收写的应答和发送读的应答
	else if(iic_rw == 1'd1)begin
		if(cnt == 4'd9 && sda_state == 4'd0)//器件写地址完成
			write_done <= 1'd1;
		else if(cnt == 4'd8 && sda_state == 4'd1)//写地址高位完成
			write_done <= 1'd1;
		else if(cnt == 4'd8 && sda_state == 4'd2)//写地址低位完成
			write_done <= 1'd1;//虚写操作完成
		else if(cnt == 4'd10 && sda_state == 4'd3)//发送其实信号和器件地址
			write_done <= 1'd1;
		else if(cnt != 4'd8 && cnt != 4'd9 && sda_state == 4'd4)
			write_done <= 1'd1;
		else
			write_done <= 1'd0;
	end
end

always@(posedge CLK or negedge RST)
begin
	if(!RST)
		read_flag <= 4'd0;
	else if(iic_rw == 1'd1 && cnt != 4'd8 && cnt != 4'd9 && sda_state == 4'd4)
		read_flag <= cnt + 1'd1;
	else
		read_flag <= 4'd0;
end


//由于发送数据只能是在scl低电平的时候发送,所以这两个信号的时钟需要进行错位
//即起始信号开始,当起始信号的低电平计数到一半时,开始计数scl时钟。
always@(posedge CLK or negedge RST)
begin
	if(!RST)
		scl_en <= 1'd0;
	else if(sda_cnt == sda_max/2 - scl_max/2)
		scl_en <= 1'd1;
end

//数据发送状态机
reg sda_data;//发送数据暂存
reg [7:0]r_iic_data_r;//接收数据暂存
reg dif_sda;//每一位数据暂存
always@(posedge CLK or negedge RST)
begin
	if(!RST)begin
		sda_data <= 1'd1;//默认时刻为高电平
		r_iic_data_r <= 8'd0;
	end
	else if(iic_stop == 1'd0 && iic_rw == 1'd0)begin//写命令
		case(sda_state)//根据不同的状态发送不同的数据
			4'd0:begin//发送起始信号和器件地址
				case(cnt)
					4'd0:sda_data <= 1'd0;//起始位
					4'd1:sda_data <= 1'd1;
					4'd2:sda_data <= 1'd0;
					4'd3:sda_data <= 1'd1;
					4'd4:sda_data <= 1'd0;
					4'd5:sda_data <= 1'd0;
					4'd6:sda_data <= 1'd0;
					4'd7:sda_data <= 1'd0;
					4'd8:sda_data <= iic_rw;//0表示写
					4'd9:sda_data <= iic_data_addr[15];//下一位数据提前写进去
				endcase
			end
			4'd1:begin//发送地址高位
				case(cnt)
					4'd0:sda_data <= iic_data_addr[15];
					4'd1:sda_data <= iic_data_addr[14];
					4'd2:sda_data <= iic_data_addr[13];
					4'd3:sda_data <= iic_data_addr[12];
					4'd4:sda_data <= iic_data_addr[11];
					4'd5:sda_data <= iic_data_addr[10];
					4'd6:sda_data <= iic_data_addr[9];
					4'd7:sda_data <= iic_data_addr[8];
					4'd8:sda_data <= iic_data_addr[7];//下一位数据提前写进去
				endcase
			end
			4'd2:begin//发送地址低位
				case(cnt)
					4'd0:sda_data <= iic_data_addr[7];
					4'd1:sda_data <= iic_data_addr[6];
					4'd2:sda_data <= iic_data_addr[5];
					4'd3:sda_data <= iic_data_addr[4];
					4'd4:sda_data <= iic_data_addr[3];
					4'd5:sda_data <= iic_data_addr[2];
					4'd6:sda_data <= iic_data_addr[1];
					4'd7:sda_data <= iic_data_addr[0];
					4'd8:sda_data <= iic_data_w[7];//下一位数据提前写进去
				endcase
			end
			4'd3:begin//发送八位数据
				case(cnt)
					4'd0:sda_data <= iic_data_w[7];
					4'd1:sda_data <= iic_data_w[6];
					4'd2:sda_data <= iic_data_w[5];
					4'd3:sda_data <= iic_data_w[4];
					4'd4:sda_data <= iic_data_w[3];
					4'd5:sda_data <= iic_data_w[2];
					4'd6:sda_data <= iic_data_w[1];
					4'd7:sda_data <= iic_data_w[0];
					4'd8:sda_data <= 1'd0;//下一位提前写进去
					4'd9:sda_data <= 1'd0;//停止信号
				endcase
			end
		endcase
	end
	else if(iic_stop == 1'd0 && iic_rw == 1'd1)begin//读命令
		case(sda_state)//根据不同的状态发送不同的数据
			4'd0:begin//发送起始信号和器件地址
				case(cnt)
					4'd0:sda_data <= 1'd0;//起始位
					4'd1:sda_data <= 1'd1;
					4'd2:sda_data <= 1'd0;
					4'd3:sda_data <= 1'd1;
					4'd4:sda_data <= 1'd0;
					4'd5:sda_data <= 1'd0;
					4'd6:sda_data <= 1'd0;
					4'd7:sda_data <= 1'd0;
					4'd8:sda_data <= 1'd0;//0表示写
					4'd9:sda_data <= iic_data_addr[15];//下一位数据提前写进去
				endcase
			end
			4'd1:begin//发送地址高位
				case(cnt)
					4'd0:sda_data <= iic_data_addr[15];
					4'd1:sda_data <= iic_data_addr[14];
					4'd2:sda_data <= iic_data_addr[13];
					4'd3:sda_data <= iic_data_addr[12];
					4'd4:sda_data <= iic_data_addr[11];
					4'd5:sda_data <= iic_data_addr[10];
					4'd6:sda_data <= iic_data_addr[9];
					4'd7:sda_data <= iic_data_addr[8];
					4'd8:sda_data <= iic_data_addr[7];//下一位数据提前写进去
				endcase
			end
			4'd2:begin//发送地址低位
				case(cnt)
					4'd0:sda_data <= iic_data_addr[7];
					4'd1:sda_data <= iic_data_addr[6];
					4'd2:sda_data <= iic_data_addr[5];
					4'd3:sda_data <= iic_data_addr[4];
					4'd4:sda_data <= iic_data_addr[3];
					4'd5:sda_data <= iic_data_addr[2];
					4'd6:sda_data <= iic_data_addr[1];
					4'd7:sda_data <= iic_data_addr[0];
					4'd8:sda_data <= 1'd0;//下一位数据提前写进去
				endcase
			end
			4'd3:begin//发送起始信号和器件地址
				case(cnt)
					4'd0:sda_data <= 1'd1;//先拉高
					4'd1:sda_data <= 1'd0;//起始位
					4'd2:sda_data <= 1'd0;
					4'd3:sda_data <= 1'd1;
					4'd4:sda_data <= 1'd0;
					4'd5:sda_data <= 1'd1;
					4'd6:sda_data <= 1'd0;
					4'd7:sda_data <= 1'd0;
					4'd8:sda_data <= 1'd0;
					4'd9:sda_data <= iic_rw;
				endcase
			end
			4'd4:begin//接收数据
				case(cnt)
					4'd0:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[7] <= dif_sda;
						end
					4'd1:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[6] <= dif_sda;
						end
					4'd2:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[5] <= dif_sda;
						end
					4'd3:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[4] <= dif_sda;
						end
					4'd4:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[3] <= dif_sda;
						end
					4'd5:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[2] <= dif_sda;
						end
					4'd6:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[1] <= dif_sda;
						end
					4'd7:
						if(sda_cnt == sda_max/2)begin 
							r_iic_data_r[0] <= dif_sda;
						end
					4'd8:sda_data <= 1'd1;//发送应答信号
					4'd9:sda_data <= 1'd0;//发送停止信号
				endcase
			end
		endcase
	end
	else if(iic_stop == 1'd1)
		sda_data <= 1'd1;//保持高电平
end

always@(*)begin
	dif_sda <= SDA;
end

always@(*)begin
	if(cnt == 4'd8 && sda_state == 4'd4)
		iic_data_r <= r_iic_data_r;
end

//inout类型的数据使用三态门来实现,1'bz为高阻态
//inout只能使用assign赋值
assign SDA = write_done ? 1'bz : sda_data;

endmodule

iic_driver_tb.v

/*
	功能:IIC驱动模块设计
	作者:王志川
	时间:2024.7.5
*/
`timescale 1ns/1ps
module iic_driver_tb();

reg clk;
reg rst;
reg iic_rw;
reg iic_start;
reg [15:0]iic_data_addr;
reg [7:0]iic_data_w;
wire [7:0]iic_data_r;
wire [3:0]read_flag;
wire iic_stop;
wire write_done;
wire sda;
wire scl;

iic_driver iic_driver(
	.CLK(clk),                      //50MHZ时钟信号
	.RST(rst),                      //复位信号
	.SDA(sda),                      //IIC数据引脚
	.iic_rw(iic_rw),                //IIC读写控制信号,1为读,0为写
	.iic_start(iic_start),          //IIC起始信号,为1有效
	.iic_data_addr(iic_data_addr),  //IIC要写数据的地址
	.iic_data_w(iic_data_w),        //IIC要写的数据
	.iic_data_r(iic_data_r),        //IIC读出的数据
	.read_flag(read_flag),          //读数据状态标志
	.iic_stop(iic_stop),            //IIC停止信号
	.write_done(write_done),        //IIC一次8位数据写完成标志位
	.SCL(scl)                       //IIC时钟引脚
);

initial clk=1;
always #10 clk=!clk;

always@(*)begin
	if(iic_stop == 1'd1)
		iic_start <= 1'd0;
end

reg data;//应答数据
reg [7:0]r_data = 8'b1111_0000;//要发送的数据
always@(*)begin
	if(iic_rw == 1'd1 && read_flag != 4'd0)
		data = r_data[4'd8 - read_flag];
	else
		data = 1'b0;
end

//inout类型的数据使用三态门来实现
assign sda = write_done? data : 1'bz;

initial begin
	rst=0;
	iic_start=0;
	iic_rw=0;//读写切换
	iic_data_addr = 16'b0110_0001_0110_0001;
	iic_data_w = 8'b0101_1001;
	#201;
	rst=1;
	iic_start=1;
	#5000_000;
	$stop;
end

endmodule

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值