I2C协议简介 & Verilog实现

I2C协议

  IIC 协议是三种最常用的串行通信协议(I2C,SPI,UART)之一,接口包含 SDA(串行数据线)和 SCL(串行时钟线),均为双向端口。I2C 仅使用两根信号线,极大地减少了连接线的数量,支持多主多从,且具有应答机制,因此在片间通信有较多的应用。

  I2C 主要包括四个状态:起始 START,数据传送 SEND,应答 ACK,停止 STOP。

  • 传输起始

  当 SCL 为高电平,SDA 出现下跳变时,标志着传输的起始。

  • 数据传输

  在传输数据位时,采用大端传输(即先传最高位 MSB),SDA 在SCL 低电平时改变,在 SCL=H 时,必须保持 SDA 稳定

  • 应答

  在传输完 8bit 数据后,Master 须释放 SDA ,Slave 接过 SDA 的控制权,给出应答信号 ACK,当 ACK=L 时,表示本字节数据传输有效。

  • 停止

  当 SCL 为高,SDA 出现上跳变时,标志着传输的结束。

  一次 I2C 传输可以传输多个字节,通常第一个字节为 I2C 设备地址 ADDR(7bit)和读写标志 I2C协议简介 & Verilog实现(1bit)。一个可能的 I2C 例子如下:

Verilog实现

  I2C的时序相对而言较复杂,因此实现方法自然是万能的三段式状态机(状态机大法好,状态机大法万岁!)

SCL/SDA 状态机输出控制

  不同的 I2C 设备可能具有不同的读写序列,因此这里首先实现 Master 与 Slave 的状态机输出的子模块(即三段式状态机的第三段),分别为 I2C_Master_sub、I2C_Slave_sub,顶层模块只需要合理安排状态转移,即可实现各种 I2C 读写时序!

  为了方便地控制 SDA 和 SCL ,Master 将一个 SCL 周期划分为 4 段;Slave 为了检测 SDA 和 SCL 的边沿并及时做出响应,须采用 8 倍以上的时钟。

  • I2C_Master_sub.v
/* 
 * file			: I2C_Master_sub.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-19
 * version		: v1.0
 * description	: I2C master 的 SDA/SCL 控制模块(通过 state)
 */
module I2C_Master_sub(
input				clk,			//4倍SCL

input		[7:0]	wrdat_buf,
output	reg	[7:0]	rddat_tmp,
output	reg			check_ack,		//检查Slave给出的ACK信号,若为NACK,输出一个高电平脉冲

inout				SCL,
inout				SDA,

output	reg			change_state,	//上升沿时 top 模块应执行 state <- next_state
input		[7:0]	state
);

localparam	IDLE		= 8'h01;	//空闲,释放SCL/SDA
localparam	START		= 8'h02;	//起始,SCL=H,SDA=D
localparam	SEND_DATA	= 8'h04;	//发送数据
localparam	GET_DATA	= 8'h08;	//读取数据,释放SDA
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK,释放SDA
localparam	ACK			= 8'h20;	//发出ACK,SDA=L
localparam	NACK		= 8'h40;	//发出NACK,SDA=H
localparam	STOP		= 8'h80;	//停止,SCL=H,SDA=R

reg				SCL_link	= 1'b0;
reg				SDA_link	= 1'b0;

reg				SCL_buf		= 1'b1;	//o_buf
reg				SDA_buf		= 1'b1;

wire			SCL_ibuf;			//i_buf
wire			SDA_ibuf;

reg		[3:0]	bit_cnt		= 4'd15;

//----------------------IO_BUF-----------------------------
//IOBUF fo SCL
IOBUF IOBUF_SCL(
	.O		(SCL_ibuf),		// Buffer output   Buffer的输出,接采集信号
	.IO		(SCL),			// Buffer inout port (connect directly to top-level port)
	.I		(SCL_buf),		// Buffer input   Buffer的输入,接要输出到FPGA外的信号
	.T		(~SCL_link)		// 3-state enable input, high=input, low=output    =1时,O <- IO;=0时,IO <- I
);

//IOBUF fo SDA
IOBUF IOBUF_SDA(
	.O		(SDA_ibuf),
	.IO		(SDA),
	.I		(SDA_buf),
	.T		(~SDA_link)
);

//---------------------clk div-----------------------------
//将一个SCL周期划分为4份,便于逻辑实现
reg 	[1:0]	clk_cnt	= 2'd0;

always @(posedge clk) begin
	clk_cnt		<= clk_cnt + 1'b1;
end

//---------------------SCL_link-----------------------------
always @(posedge clk) begin
	case(state)
	IDLE: begin
		SCL_link	<= 1'b0;
	end
	START, SEND_DATA, GET_DATA, CHECK_ACK, ACK, NACK, STOP: begin
		SCL_link	<= 1'b1;
	end
	default: begin
		SCL_link	<= 1'b0;
	end
	endcase
end

//---------------------SDA_link-----------------------------
always @(posedge clk) begin
	case(state)
	IDLE, GET_DATA, CHECK_ACK: begin
		SDA_link	<= 1'b0;
	end
	START, SEND_DATA, ACK, NACK, STOP: begin
		SDA_link	<= 1'b1;
	end
	default: begin
		SDA_link	<= 1'b0;
	end
	endcase
end

//---------------------SCL_buf-----------------------------
always @(posedge clk) begin
	case(state)
	IDLE: begin											//1111
		SCL_buf		<= 1'b1;
	end
	START: begin										//1110
		case(clk_cnt)
		2'd0, 2'd1, 2'd2: begin
			SCL_buf		<= 1'b1;
		end
		2'd3: begin
			SCL_buf		<= 1'b0;
		end
		default: ;
		endcase
	end
	STOP: begin											//0111
		case(clk_cnt)
		2'd1, 2'd2, 2'd3: begin
			SCL_buf		<= 1'b1;
		end
		2'd0: begin
			SCL_buf		<= 1'b0;
		end
		default: ;
		endcase
	end
	SEND_DATA, GET_DATA, CHECK_ACK, ACK, NACK: begin	//0110
		case(clk_cnt)
		2'd1, 2'd2: begin
			SCL_buf		<= 1'b1;
		end
		2'd0, 2'd3: begin
			SCL_buf		<= 1'b0;
		end
		default: ;
		endcase
	end
	default: begin										//1111
		SCL_buf		<= 1'b1;
	end
	endcase
end

//---------------------bit_cnt-----------------------------
always @(posedge clk) begin
	case(state)
	SEND_DATA, GET_DATA: begin
		case(clk_cnt)
		2'd2: begin
			bit_cnt		<= bit_cnt - 1'b1;
		end
		default: ;
		endcase
	end
	START, ACK, NACK, CHECK_ACK: begin
		bit_cnt		<= 4'd7;
	end
	default: begin
		bit_cnt		<= 4'd15;
	end
	endcase
end

//--------------------rddat_tmp----------------------------
always @(posedge clk) begin
	case(state)
	GET_DATA: begin
		case(clk_cnt)
		2'd1: begin
			rddat_tmp[bit_cnt]	<= SDA_ibuf;
		end
		default: ;
		endcase
	end
	default: begin
		rddat_tmp	<= rddat_tmp;
	end
	endcase
end

//--------------------check_ack----------------------------
always @(posedge clk) begin
	case(state)
	CHECK_ACK: begin
		case(clk_cnt)
		2'd1: begin
			check_ack	<= SDA_ibuf;
		end
		default: begin
			check_ack	<= check_ack;
		end
		endcase
	end
	default: begin
		check_ack	<= 0;
	end
	endcase
end

//---------------------SDA_buf-----------------------------
always @(posedge clk) begin
	case(state)
	IDLE: begin
		SDA_buf		<= 1'b1;
	end
	START: begin										//1100,从而在SCL=H时,产生SDA=D
		case(clk_cnt)
		2'd0, 2'd1: begin
			SDA_buf		<= 1'b1;
		end
		2'd2, 2'd3: begin
			SDA_buf		<= 1'b0;
		end
		default: ;
		endcase
	end
	SEND_DATA: begin									//在clk_cnt=0给出数据,从而在clk_cnt=1,2时(SCL=H)保持SDA的稳定
		case(clk_cnt)
		2'd0: begin
			SDA_buf		<= wrdat_buf[bit_cnt];
		end
		default: ;
		endcase
	end
	GET_DATA: begin
		SDA_buf		<= 1'b1;
	end
	CHECK_ACK: begin
		SDA_buf		<= 1'b0;
	end
	ACK: begin
		SDA_buf		<= 1'b0;
	end
	NACK: begin
		SDA_buf		<= 1'b1;
	end
	STOP: begin											//0011,从而在SCL=H时,产生SDA=R
		case(clk_cnt)
		2'd0, 2'd1: begin
			SDA_buf		<= 1'b0;
		end
		2'd2, 2'd3: begin
			SDA_buf		<= 1'b1;
		end
		default: ;
		endcase
	end
	default: begin
		SDA_buf		<= 1'b1;
	end
	endcase
end

//-------------------change_state---------------------------
always @(posedge clk) begin
	case(state)
	IDLE, ACK, NACK, CHECK_ACK, STOP: begin
		case(clk_cnt)
		2'd3: begin
			change_state	<= 1'b1;
		end
		default: begin
			change_state	<= 1'b0;
		end
		endcase
	end
	SEND_DATA, GET_DATA: begin
		case(bit_cnt)
		4'd15: begin
			case(clk_cnt)
			2'd3: begin
				change_state	<= 1'b1;
			end
			default: begin
				change_state	<= 1'b0;
			end
			endcase
		end
		default: begin
			change_state	<= 1'b0;
		end
		endcase
	end
	default: begin
		case(clk_cnt)
		2'd3: begin
			change_state	<= 1'b1;
		end
		default: begin
			change_state	<= 1'b0;
		end
		endcase
	end
	endcase
end

endmodule
  • I2C_Slave_sub.v
/* 
 * file			: I2C_Slave_sub.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-19
 * version		: v1.0
 * description	: I2C Slave 的 SDA/SCL 控制模块(通过 state)
 */
module I2C_Slave_sub(
input				clk,			//SCL的8倍以上

input		[7:0]	wrdat_buf,
output	reg	[7:0]	rddat_tmp,
output	reg			check_ack,		//检查Master给出的ACK信号,若为NACK,输出一个高电平脉冲

inout				SCL,
inout				SDA,

output	reg			change_state,	//上升沿时 top 模块应执行 state <- next_state
input		[7:0]	state,

output	reg			busy
);

localparam	IDLE		= 8'h01;	//空闲
localparam	START		= 8'h02;	//起始,检测到SCL=H,SDA=D,
localparam	SEND_DATA	= 8'h04;	//Slave发送数据,接管SDA控制权
localparam	GET_DATA	= 8'h08;	//Slave读取数据
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK
localparam	ACK			= 8'h20;	//发出ACK,SDA=L,接管SDA控制权
localparam	NACK		= 8'h40;	//发出NACK,SDA=H,接管SDA控制权
localparam	STOP		= 8'h80;	//停止,检测到SCL=H,SDA=R
//不实现Clock Stretching功能,因此Slave从不试图接管SCL
//除注释注明的状态外,不获取SDA控制权

wire			SCL_link;
reg				SDA_link	= 1'b0;

reg				SCL_buf		= 1'b1;	//o_buf
reg				SDA_buf		= 1'b1;

wire			SCL_ibuf;			//i_buf
wire			SDA_ibuf;

assign	SCL_link	= 1'b0;

//----------------------IO_BUF-----------------------------
//IOBUF fo SCL
IOBUF IOBUF_SCL(
	.O		(SCL_ibuf),		//Buffer的输出,接采集信号
	.IO		(SCL),
	.I		(SCL_buf),		//Buffer的输入,接要输出到FPGA外的信号
	.T		(~SCL_link)		//=1时,O <- IO;=0时,IO <- I
);

//IOBUF fo SDA
IOBUF IOBUF_SDA(
	.O		(SDA_ibuf),
	.IO		(SDA),
	.I		(SDA_buf),
	.T		(~SDA_link)
);

//--------------------------busy----------------------------
reg				busy_d0;
reg				busy_d1;
wire			busy_pe;
wire			busy_ne;

always @(SDA_ibuf) begin
	if(~SDA_ibuf & SCL_ibuf) begin			// SCL=H,SDA=D,接收起始
		busy	<= 1'b1;
	end
	else if(SDA_ibuf & SCL_ibuf) begin		// SCL=H,SDA=R,接收结束
		busy	<= 1'b0;
	end
	else begin
		busy	<= busy;
	end
end

always @(posedge clk) begin
	busy_d0		<= busy;
	busy_d1		<= busy_d0;
end

assign busy_pe	= busy_d0 & (~busy_d1);
assign busy_ne	= (~busy_d0) & busy_d1;

//--------------------------edge----------------------------
reg				SDA_d0;
reg				SDA_d1;
wire			SDA_pe;
wire			SDA_ne;

reg				SCL_d0;
reg				SCL_d1;
wire			SCL_pe;
wire			SCL_ne;

always @(posedge clk) begin
	SDA_d0		<= SDA_ibuf;
	SDA_d1		<= SDA_d0;

	SCL_d0		<= SCL_ibuf;
	SCL_d1		<= SCL_d0;
end

assign SDA_pe	= SDA_d0 & (~SDA_d1);
assign SDA_ne	= (~SDA_d0) & SDA_d1;

assign SCL_pe	= SCL_d0 & (~SCL_d1);
assign SCL_ne	= (~SCL_d0) & SCL_d1;

//-----------------------SCL_cnt----------------------------
reg		[3:0]	SCL_cnt;	//计算当前是第几个SCL_pe

always @(posedge clk) begin
	if(busy_pe) begin
		SCL_cnt		<= 4'd0;
	end
	else if(SCL_ne && SCL_cnt==4'd9) begin
		SCL_cnt		<= 4'd0;
	end
	else if(SCL_pe) begin
		SCL_cnt		<= SCL_cnt + 1'b1;
	end
	else begin
		SCL_cnt		<= SCL_cnt;
	end
end

//---------------------change_state--------------------------
always @(posedge clk) begin
	case(state)
	IDLE: begin
		if(busy_pe) begin
			change_state	<= 1'b1;
		end
		else begin
			change_state	<= 1'b0;
		end
	end
	START: begin
		if(SCL_ne) begin
			change_state	<= 1'b1;
		end
		else begin
			change_state	<= 1'b0;
		end
	end
	SEND_DATA, GET_DATA: begin
		if(SCL_ne && SCL_cnt==4'd8) begin
			change_state	<= 1'b1;
		end
		else begin
			change_state	<= 1'b0;
		end
	end
	ACK, NACK, CHECK_ACK: begin
		if(SCL_ne) begin
			change_state	<= 1'b1;
		end
		else begin
			change_state	<= 1'b0;
		end
	end
	STOP: begin
		if(busy_ne) begin
			change_state	<= 1'b1;
		end
		else begin
			change_state	<= 1'b0;
		end
	end
	default: begin
		change_state	<= 1'b0;
	end
	endcase
end

//-----------------------SDA_link----------------------------
always @(posedge clk) begin
	case(state)
	SEND_DATA, ACK, NACK: begin
		SDA_link	<= 1'b1;
	end
	default: begin
		SDA_link	<= 1'b0;
	end
	endcase
end

//----------------------check_ack----------------------------
always @(posedge clk) begin
	case(state)
	CHECK_ACK: begin
		if(SCL_pe) begin
			check_ack	<= SDA_ibuf;
		end
		else begin
			check_ack	<= 1'b0;
		end
	end
	default: begin
		check_ack	<= 1'b0;
	end
	endcase
end

//----------------------rddat_tmp----------------------------
always @(posedge clk) begin
	case(state)
	GET_DATA: begin
		if(SCL_pe) begin
			rddat_tmp[7 - SCL_cnt]	<= SDA_ibuf;
		end
		else ;
	end
	default: ;
	endcase
end

//-----------------------SDA_buf-----------------------------
always @(posedge clk) begin
	case(state)
	SEND_DATA: begin
		if(SCL_ne || change_state) begin
			SDA_buf		<= wrdat_buf[7 - SCL_cnt];
		end
		else begin
			SDA_buf		<= SDA_buf;
		end
	end
	ACK: begin
		SDA_buf		<= 1'b0;
	end
	NACK: begin
		SDA_buf		<= 1'b1;
	end
	default: begin
		SDA_buf		<= 1'b1;
	end
	endcase
end

endmodule

Master 读/写子模块

  基于 Master_sub 状态机输出控制子模块,分别搭建 Master 读/写控制子模块例程如下(这里实现的是比较常规的 I2C 读写时序,要实现更加具体的读写时序可参考该例程自行实现

  • I2C_Master_Write.v
/* 
 * file			: I2C_Master_Write.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-20
 * version		: v1.0
 * description	: I2C写功能
 * Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
 */
module I2C_Master_Write(
input				clk,			//4倍SCL

input				wr_en,			//上升沿有效
output	reg			wrdat_req,		//上升沿驱动上层模块给出wrdat
input		[7:0]	wrdat,

output				busy,
output				check_ack,		//检查Slave给出的ACK信号,若为NACK,输出一个高电平脉冲

inout				SCL,
inout				SDA
);
// S {ADDR,RW_W} A DATA A ... DATA A P

parameter	ADDR		= 7'h11;    //I2C设备地址
parameter	WR_DATA_LEN	= 16'd1;    //写的数据个数

localparam	RW_W		= 1'b0;
localparam	RW_R		= 1'b1;

//---------------------I2C Master State Define----------------------
localparam	IDLE		= 8'h01;	//空闲,释放SCL/SDA
localparam	START		= 8'h02;	//起始,SCL=H,SDA=D
localparam	SEND_DATA	= 8'h04;	//发送数据
localparam	GET_DATA	= 8'h08;	//读取数据,释放SDA
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK,释放SDA
localparam	ACK			= 8'h20;	//发出ACK,SDA=L
localparam	NACK		= 8'h40;	//发出NACK,SDA=H
localparam	STOP		= 8'h80;	//停止,SCL=H,SDA=R

//------------------------------------------------------------------
reg		[7:0]	state		= IDLE;
reg		[7:0]	next_state	= IDLE;

reg				start_flag	= 1'b0;

wire			change_state;

reg		[7:0]	wrdat_buf	= 8'd0;
reg		[15:0]	data_cnt	= 16'd0;

//------------------------start_flag--------------------------------
reg		wr_en_d0;
reg		wr_en_d1;
wire	wr_en_pe;

always @(posedge clk) begin
	wr_en_d0	<= wr_en;
	wr_en_d1	<= wr_en_d0;
end

assign	wr_en_pe	= wr_en_d0 & (~wr_en_d1);
assign	busy 		= (state == IDLE)? 1'b0 : 1'b1;

always @(posedge clk) begin
	if(wr_en_pe && ~busy) begin
		start_flag	<= 1'b1;
	end
	else if(state == START) begin
		start_flag	<= 1'b0;
	end
	else begin
		start_flag	<= start_flag;
	end
end

//-------------------------State Machine----------------------------
always @(posedge change_state) begin
	state		<= next_state;
end

//状态转移
always @(*) begin
	case(state)
	IDLE: begin
		if(start_flag) begin
			next_state	<= START;
		end
		else begin
			next_state	<= IDLE;
		end
	end
	START: begin
		next_state	<= SEND_DATA;
	end
	SEND_DATA: begin
		next_state	<= CHECK_ACK;
	end
	CHECK_ACK: begin
		if(data_cnt == 1 && check_ack) begin		//第一个CHECK检测到NACK,STOP
			next_state	<= STOP;
		end
		else begin
			if(data_cnt > WR_DATA_LEN) begin
				next_state	<= STOP;
			end
			else begin
				next_state	<= SEND_DATA;
			end
		end
	end
	STOP: begin
		next_state	<= IDLE;
	end
	default: begin
		next_state	<= IDLE;
	end
	endcase
end

//三段式状态机第三段,I2C Master sub
I2C_Master_sub I2C_Master_sub_inst(
	.clk			(clk),

	.wrdat_buf		(wrdat_buf),
	.rddat_tmp		(),
	.check_ack		(check_ack),

	.SCL			(SCL),
	.SDA			(SDA),

	.change_state	(change_state),
	.state			(state)
);

// -----data_req-----
always @(*) begin
	case(state)
	CHECK_ACK: begin
		if(data_cnt <= WR_DATA_LEN) begin
			wrdat_req	<= 1'b1;
		end
		else begin
			wrdat_req	<= 1'b0;
		end
	end
	default: begin
		wrdat_req	<= 1'b0;
	end
	endcase
end

// -----data_cnt-----
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		data_cnt	<= 16'd0;
	end
	SEND_DATA: begin
		data_cnt	<= data_cnt + 1'b1;
	end
	default: begin
		data_cnt	<= data_cnt;
	end
	endcase
end

// -----wrdat_buf-----
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		wrdat_buf	<= 8'd0;
	end
	START: begin
		wrdat_buf	<= {ADDR, RW_W};
	end
	CHECK_ACK: begin
		wrdat_buf	<= wrdat;
	end
	default: begin
		wrdat_buf	<= wrdat_buf;
	end
	endcase
end

endmodule
  • I2C_Master_Read.v
/* 
 * file			: I2C_Master_Read.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-20
 * version		: v1.0
 * description	: I2C读功能
 * Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
 */
module I2C_Master_Read(
input				clk,			//4倍SCL

input				rd_en,			//上升沿有效
output	reg			rddat_vaild,
output	reg	[7:0]	rddat,

output				busy,
output              check_ack,

inout				SCL,
inout				SDA
);
// S {ADDR,RW_R} A DATA A ... DATA A P

parameter	ADDR		= 7'h11;    //I2C设备地址
parameter	RD_DATA_LEN	= 16'd1;    //读的数据个数

localparam	RW_W		= 1'b0;
localparam	RW_R		= 1'b1;

//---------------------I2C Master State Define----------------------
localparam	IDLE		= 8'h01;	//空闲,释放SCL/SDA
localparam	START		= 8'h02;	//起始,SCL=H,SDA=D
localparam	SEND_DATA	= 8'h04;	//发送数据
localparam	GET_DATA	= 8'h08;	//读取数据,释放SDA
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK,释放SDA
localparam	ACK			= 8'h20;	//发出ACK,SDA=L
localparam	NACK		= 8'h40;	//发出NACK,SDA=H
localparam	STOP		= 8'h80;	//停止,SCL=H,SDA=R

//------------------------------------------------------------------
reg		[7:0]	state		= IDLE;
reg		[7:0]	next_state	= IDLE;

reg				start_flag	= 1'b0;

wire			change_state;

reg		[7:0]	wrdat_buf	= 8'd0;
wire	[7:0]	rddat_tmp;
reg		[15:0]	data_cnt	= 16'd0;

//------------------------start_flag--------------------------------
reg		rd_en_d0;
reg		rd_en_d1;
wire	rd_en_pe;

always @(posedge clk) begin
	rd_en_d0	<= rd_en;
	rd_en_d1	<= rd_en_d0;
end

assign	rd_en_pe	= rd_en_d0 & (~rd_en_d1);
assign	busy 		= (state == IDLE)? 1'b0 : 1'b1;

always @(posedge clk) begin
	if(rd_en_pe && ~busy) begin
		start_flag	<= 1'b1;
	end
	else if(state == START) begin
		start_flag	<= 1'b0;
	end
	else begin
		start_flag	<= start_flag;
	end
end

//-------------------------State Machine----------------------------
always @(posedge change_state) begin
	state		<= next_state;
end

//状态转移
always @(*) begin
	case(state)
	IDLE: begin
		if(start_flag) begin
			next_state	<= START;
		end
		else begin
			next_state	<= IDLE;
		end
	end
	START: begin
		next_state	<= SEND_DATA;
	end
	SEND_DATA: begin
		next_state	<= CHECK_ACK;
	end
	CHECK_ACK: begin
		if(check_ack) begin		//检测到NACK,STOP
			next_state	<= STOP;
		end
		else begin
			next_state	<= GET_DATA;
		end
	end
	GET_DATA: begin
		next_state	<= ACK;
	end
	ACK: begin
		if(data_cnt >= RD_DATA_LEN) begin
			next_state	<= STOP;
		end
		else begin
			next_state	<= GET_DATA;
		end
	end
	STOP: begin
		next_state	<= IDLE;
	end
	default: begin
		next_state	<= IDLE;
	end
    endcase
end

//三段式状态机第三段,I2C Master sub
I2C_Master_sub I2C_Master_sub_inst(
	.clk			(clk),

	.wrdat_buf		(wrdat_buf),
	.rddat_tmp		(rddat_tmp),
	.check_ack		(check_ack),

	.SCL			(SCL),
	.SDA			(SDA),

	.change_state	(change_state),
	.state			(state)
);

// -----data_valid-----
always @(*) begin
	case(state)
	ACK: begin
		rddat_vaild	<= 1'b1;
	end
	default: begin
		rddat_vaild	<= 1'b0;
	end
	endcase
end

// -----data_cnt-----
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		data_cnt	<= 16'd0;
	end
	GET_DATA: begin
		data_cnt	<= data_cnt + 1'b1;
	end
	default: begin
		data_cnt	<= data_cnt;
	end
	endcase
end

// -----rddat-----
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		rddat	<= rddat;
	end
	GET_DATA: begin
		rddat	<= rddat_tmp;
	end
	default: begin
		rddat	<= rddat;
	end
	endcase
end

// ---wrdat_buf---
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		wrdat_buf	<= 8'd0;
	end
	START: begin
		wrdat_buf	<= {ADDR, RW_R};
	end
	CHECK_ACK: begin
		wrdat_buf	<= 8'd0;
	end
	default: begin
		wrdat_buf	<= wrdat_buf;
	end
	endcase
end

endmodule

Slave 读/写子模块

  同样,基于 Slave_sub 设计 Slave 的读/写控制子模块例程如下

  • I2C_Slave_Receive.v
/* 
 * file			: I2C_Slave_Receive.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-21
 * version		: v1.0
 * description	: 作为Slave<接收>数据
 * Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
 */
module I2C_Slave_Receive(
input				clk,			//SCL的8倍以上

output	reg			rddat_vaild,	//下降沿有效
output	reg	[7:0]	rddat,

output				busy,

inout				SCL,
inout				SDA
);
// S {ADDR,RW_W} A DATA A ... DATA A P		-- 本机地址
// S {ADDR,RW_W} NA                  P		-- 非本机地址

parameter	ADDR				= 7'h11;    //I2C设备地址
parameter	RECEIVE_DATA_LEN	= 16'd1;    //接收的数据个数

localparam	RW_W		= 1'b0;
localparam	RW_R		= 1'b1;

//---------------------I2C Slave State Define----------------------
localparam	IDLE		= 8'h01;	//空闲
localparam	START		= 8'h02;	//起始,检测到SCL=H,SDA=D,
localparam	SEND_DATA	= 8'h04;	//Slave发送数据,接管SDA控制权
localparam	GET_DATA	= 8'h08;	//Slave读取数据
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK
localparam	ACK			= 8'h20;	//发出ACK,SDA=L,接管SDA控制权
localparam	NACK		= 8'h40;	//发出NACK,SDA=H,接管SDA控制权
localparam	STOP		= 8'h80;	//停止,检测到SCL=H,SDA=R

//------------------------------------------------------------------
reg		[7:0]	state		= IDLE;
reg		[7:0]	next_state	= IDLE;

wire			change_state;

wire	[7:0]	rddat_tmp;
reg		[15:0]	data_cnt	= 16'd0;

reg				isMe		= 1'b0;	//是否为本机地址

//-------------------------State Machine----------------------------
always @(posedge change_state or negedge busy) begin
	if(~busy) begin
		state		<= IDLE;
	end
	else begin
		state		<= next_state;
	end
end

//状态转移
always @(*) begin
	if(busy) begin
		case(state)
		IDLE: begin
			next_state	<= START;
		end
		START: begin
			next_state	<= GET_DATA;
		end
		GET_DATA: begin
			if(isMe) begin
				next_state	<= ACK;
			end
			else begin
				next_state	<= NACK;
			end
		end
		ACK: begin
			if(data_cnt > RECEIVE_DATA_LEN) begin
				next_state	<= STOP;
			end
			else begin
				next_state	<= GET_DATA;
			end
		end
		NACK: begin
			next_state		<= STOP;
		end
		STOP: begin
			next_state		<= IDLE;
		end
		default: begin
			next_state		<= IDLE;
		end
		endcase
	end
	else begin
		next_state		<= START;
	end
end

//三段式状态机第三段,I2C Slave sub
I2C_Slave_sub I2C_Slave_sub_inst(
	.clk			(clk),

	.wrdat_buf		(),
	.rddat_tmp		(rddat_tmp),
	.check_ack		(),

	.SCL			(SCL),
	.SDA			(SDA),

	.change_state	(change_state),
	.state			(state),

	.busy			(busy)
);

// ---rddat_vaild---
always @(*) begin
	case(state)
	IDLE: begin
		rddat_vaild		<= 1'b0;
	end
	ACK: begin
		if(data_cnt>1) begin
			rddat_vaild		<= 1'b1;
		end
		else begin
			rddat_vaild		<= 1'b0;
		end
	end
	default: begin
		rddat_vaild		<= 1'b0;
	end
	endcase
end

// ---rddat---
always @(posedge change_state) begin
	case(state)
	GET_DATA: begin
		if(data_cnt>0) begin
			rddat		<= rddat_tmp;
		end
		else begin
			rddat		<= 8'd0;
		end
	end
	default: begin
		rddat		<= rddat;
	end
	endcase
end

// ---data_cnt---
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		data_cnt	<= 16'd0;
	end
	GET_DATA: begin
		data_cnt	<= data_cnt + 1'b1;
	end
	default: begin
		data_cnt	<= data_cnt;
	end
	endcase
end

// ---isMe---
always @(*) begin
	case(state)
	IDLE: begin
		isMe	<= 1'b0;
	end
	GET_DATA: begin
		if(data_cnt==0) begin
			if(rddat_tmp=={ADDR, RW_W}) begin	//地址=本机,且RW=W,启动Slave接收进程
				isMe	<= 1'b1;
			end
			else begin
				isMe	<= isMe;
			end
		end
		else begin
			isMe	<= isMe;
		end
	end
	default: begin
		isMe	<= isMe;
	end
	endcase
end

endmodule
  • I2C_Slave_Send.v
/* 
 * file			: I2C_Slave_Send.v
 * author		: 今朝无言
 * Lab			: WHU-EIS-LMSWE
 * date			: 2023-03-21
 * version		: v1.0
 * description	: 作为Slave<发送>数据
 * Copyright © 2023 WHU-EIS-LMSWE, All Rights Reserved.
 */
module I2C_Slave_Send(
input				clk,			//SCL的8倍以上

output	reg			wrdat_req,
input		[7:0]	wrdat,

output				busy,

inout				SCL,
inout				SDA
);
// S {ADDR,RW_R} A DATA A ... DATA A P		-- 本机地址
// S {ADDR,RW_R} NA                  P		-- 非本机地址

parameter	ADDR			= 7'h11;    //I2C设备地址
parameter	SEND_DATA_LEN	= 16'd1;    //发送的数据个数

localparam	RW_W		= 1'b0;
localparam	RW_R		= 1'b1;

//---------------------I2C Slave State Define----------------------
localparam	IDLE		= 8'h01;	//空闲
localparam	START		= 8'h02;	//起始,检测到SCL=H,SDA=D,
localparam	SEND_DATA	= 8'h04;	//Slave发送数据,接管SDA控制权
localparam	GET_DATA	= 8'h08;	//Slave读取数据
localparam	CHECK_ACK	= 8'h10;	//检查SDA的ACK/NACK
localparam	ACK			= 8'h20;	//发出ACK,SDA=L,接管SDA控制权
localparam	NACK		= 8'h40;	//发出NACK,SDA=H,接管SDA控制权
localparam	STOP		= 8'h80;	//停止,检测到SCL=H,SDA=R

//------------------------------------------------------------------
reg		[7:0]	state		= IDLE;
reg		[7:0]	next_state	= IDLE;

wire			change_state;

wire	[7:0]	rddat_tmp;

reg		[15:0]	data_cnt	= 16'd0;

reg				isMe		= 1'b0;	//是否为本机地址

//-------------------------State Machine----------------------------
always @(posedge change_state or negedge busy) begin
	if(~busy) begin
		state		<= IDLE;
	end
	else begin
		state		<= next_state;
	end
end

//状态转移
always @(*) begin
	if(busy) begin
		case(state)
		IDLE: begin
			next_state		<= START;
		end
		START: begin
			next_state		<= GET_DATA;
		end
		GET_DATA: begin
			if(isMe) begin
				next_state	<= ACK;
			end
			else begin
				next_state	<= NACK;
			end
		end
		ACK: begin
			next_state		<= SEND_DATA;
		end
		NACK: begin
			next_state		<= STOP;
		end
		SEND_DATA: begin
			next_state		<= CHECK_ACK;
		end
		CHECK_ACK: begin
			if(data_cnt > SEND_DATA_LEN) begin
				next_state	<= STOP;
			end
			else begin
				next_state	<= SEND_DATA;
			end
		end
		STOP: begin
			next_state		<= IDLE;
		end
		default: begin
			next_state		<= IDLE;
		end
		endcase
	end
	else begin
		next_state			<= START;
	end
end

//三段式状态机第三段,I2C Slave sub
I2C_Slave_sub I2C_Slave_sub_inst(
	.clk			(clk),

	.wrdat_buf		(wrdat),
	.rddat_tmp		(rddat_tmp),
	.check_ack		(check_ack),

	.SCL			(SCL),
	.SDA			(SDA),

	.change_state	(change_state),
	.state			(state),

	.busy			(busy)
);

// ---wrdat_req---
always @(*) begin
	case(state)
	ACK, CHECK_ACK: begin
		if(data_cnt <= SEND_DATA_LEN) begin
			wrdat_req	<= 1'b1;
		end
		else begin
			wrdat_req	<= 1'b0;
		end
	end
	default: begin
		wrdat_req	<= 1'b0;
	end
	endcase
end

// ---data_cnt---
always @(posedge change_state) begin
	case(state)
	IDLE: begin
		data_cnt	<= 16'd0;
	end
	GET_DATA, SEND_DATA: begin
		data_cnt	<= data_cnt + 1'b1;
	end
	default: begin
		data_cnt	<= data_cnt;
	end
	endcase
end

// ---isMe---
always @(*) begin
	case(state)
	IDLE: begin
		isMe	<= 1'b0;
	end
	GET_DATA: begin
		if(data_cnt==0) begin
			if(rddat_tmp=={ADDR, RW_R}) begin	//地址=本机,且RW=R,启动Slave发送进程
				isMe	<= 1'b1;
			end
			else begin
				isMe	<= isMe;
			end
		end
		else begin
			isMe	<= isMe;
		end
	end
	default: begin
		isMe	<= isMe;
	end
	endcase
end

endmodule

Test Bench & 测试结果

Master写 & Slave接收

  • I2C_Master_w_Slave_r_tb.v
`timescale 1ns/100ps

module I2C_Master_w_Slave_r_tb();
//测试Master写、Slave接收

reg		clk_100M	= 1'b1;
always #5 begin
	clk_100M	<= ~clk_100M;
end

reg		clk_50M	= 1'b1;
always #10 begin
	clk_50M	<= ~clk_50M;
end

wire			SCL;
wire			SDA;

pullup(SCL);
pullup(SDA);

//-------------------Master-----------------------
reg				wr_en;
wire			wrdat_req;
reg		[7:0]	wrdat	= 8'd0;

wire			busy;
wire			check_ack;

I2C_Master_Write #(
	.ADDR			(7'h44),
	.WR_DATA_LEN	(16'd4))
I2C_Master_Write_inst(
	.clk			(clk_50M),

	.wr_en			(wr_en),
	.wrdat_req		(wrdat_req),
	.wrdat			(wrdat),

	.busy			(busy),
	.check_ack		(check_ack),

	.SCL			(SCL),
	.SDA			(SDA)
);

always @(posedge wrdat_req) begin
	wrdat	<= wrdat + 1'b1;
end

//-------------------Slave-----------------------
wire			rddat_vaild;
wire	[7:0]	rddat;
wire			S_busy;

I2C_Slave_Receive #(
	.ADDR				(7'h44),
	.RECEIVE_DATA_LEN	(16'd4))
I2C_Slave_Receive_inst(
	.clk			(clk_100M),

	.rddat_vaild	(rddat_vaild),
	.rddat			(rddat),

	.busy			(S_busy),

	.SCL			(SCL),
	.SDA			(SDA)
);

//---------------------test-------------------------
initial begin
	wr_en		<= 1'b0;

	#100;
	wr_en		<= 1'b1;
	#100;
	wr_en		<= 1'b0;

	wait(busy);
	wait(~busy);
	
	#100;
	wr_en		<= 1'b1;
	#100;
	wr_en		<= 1'b0;

	wait(busy);
	wait(~busy);

	#200;
	$stop;
end

endmodule

  设置 Master 写设备地址与 Slave 设备地址相同,单次 I2C 通信发送/接收 4 个数据,结果如下

若设置两者地址不同,Master 会检测到 NACK 信号,从而直接终止通信,结果如下

由于例程编写考虑并不全面,因此这里检查到 NACK 时仍进行了数据请求(但没进行数据发送),在实际系统设计中读者应自行修正

Master读 & Slave发送

  • I2C_Master_r_Slave_s_tb.v
`timescale 1ns/100ps

module I2C_Master_r_Slave_s_tb();
//测试Maste读、Slave发送

reg		clk_100M	= 1'b1;
always #5 begin
	clk_100M	<= ~clk_100M;
end

reg		clk_50M	= 1'b1;
always #10 begin
	clk_50M	<= ~clk_50M;
end

wire			SCL;
wire			SDA;

pullup(SCL);
pullup(SDA);

//-------------------Master-----------------------
reg				rd_en		= 1'b0;
wire			rddat_vaild;
wire	[7:0]	rddat;

wire			busy;
wire			check_ack;

I2C_Master_Read #(
	.ADDR			(7'h44),
	.RD_DATA_LEN	(16'd4))
I2C_Master_Read_inst(
	.clk			(clk_50M),

	.rd_en			(rd_en),
	.rddat_vaild	(rddat_vaild),
	.rddat			(rddat),

	.busy			(busy),
	.check_ack		(check_ack),

	.SCL			(SCL),
	.SDA			(SDA)
);

//-------------------Slave-----------------------
wire			wrdat_req;
reg		[7:0]	wrdat		= 8'd0;

wire			S_busy;

I2C_Slave_Send #(
	.ADDR			(7'h44),
	.SEND_DATA_LEN	(16'd4))
I2C_Slave_Send_inst(
	.clk			(clk_100M),

	.wrdat_req		(wrdat_req),
	.wrdat			(wrdat),

	.busy			(S_busy),

	.SCL			(SCL),
	.SDA			(SDA)
);

always @(posedge wrdat_req) begin
	wrdat	<= wrdat + 1'b1;
end

//---------------------test-------------------------
initial begin
	rd_en		<= 1'b0;

	#100;
	rd_en		<= 1'b1;
	#100;
	rd_en		<= 1'b0;

	wait(busy);
	wait(~busy);
	
	#100;
	rd_en		<= 1'b1;
	#100;
	rd_en		<= 1'b0;

	wait(busy);
	wait(~busy);

	#200;
	$stop;
end

endmodule

 设置 Master 读设备地址与 Slave 设备地址相同,单次 I2C 通信读取 4 个数据,结果如下

若两者地址不同,Slave 会自行挂起,直到 I2C 总线释放后自动回到 IDLE 状态,而 Master 由于没有收到指定设备的 ACK 确认信号,也会自行终止读取进程,结果如下

版权声明:本文为博主作者:今朝无言原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/qq_43557686/article/details/129699245

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年1月11日
下一篇 2024年1月11日

相关推荐