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)和读写标志 R / W ‾ \rm{R/\overline W} R/W(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 确认信号,也会自行终止读取进程,结果如下

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/3060.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Django 实现瀑布流

需求分析 现在是 "图片为王"的时代&#xff0c;在浏览一些网站时&#xff0c;经常会看到类似于这种满屏都是图片。图片大小不一&#xff0c;却按空间排列&#xff0c;就这是瀑布流布局。 以瀑布流形式布局&#xff0c;从数据库中取出图片每次取出等量&#xff08;7 …

Educational Codeforces Round 145 (Rated for Div. 2) (A~E)

Problem - B - Codeforces 思路&#xff1a; 我们选择长度后&#xff0c;其特定长度会构成一个正方形&#xff0c;因为点与点距离大于1&#xff0c;所以偶数的正方形里面只能包含偶数的正方形&#xff0c;奇数的包含奇数。计算每个长度容纳最大点数&#xff1a; 发现cnt[0]1,…

WPF毛笔字实现过程

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Python中生产者消费者模型

Python生产者消费者模型 一、消费模式 生产者消费者模式 是Controlnet网络中特有的一种传输数据的模式。用于两个CPU之间传输数据&#xff0c;即使是不同类型同一厂家的CPU也可以通过设置来使用。 二、传输原理 类似与点对点传送&#xff0c;又略有不同&#xff0c;一个生产…

能把爬虫讲的这么透彻的,没有20年功夫还真不行【0基础也能看懂】

前言 可以说很多人学编程&#xff0c;不玩点爬虫确实少了很多意思&#xff0c;不管是业余、接私活还是职业爬虫&#xff0c;爬虫世界确实挺精彩的。 今天来给大家浅谈一下爬虫&#xff0c;目的是让准备学爬虫或者刚开始起步的小伙伴们&#xff0c;对爬虫有一个更深更全的认知…

chatGPT爆火,什么时候中国能有自己的“ChatGPT“

目录 引言 一、ChatGPT爆火 二、中国何时能有自己的"ChatGPT" 三、为什么openai可以做出chatGPT? 四、结论 引言 随着人工智能技术的不断发展&#xff0c;自然语言处理技术也逐渐成为了研究的热点之一。其中&#xff0c;ChatGPT作为一项领先的自然语言处理技术…

【软件测试】基础知识第一篇

文章目录一. 什么是软件测试二. 测试和调试的区别三. 什么是测试用例四. 软件的生命周期五. 软件测试的生命周期一. 什么是软件测试 软件测试就是验证软件产品特性是否满足用户的需求。 那需求又是什么呢&#xff1f;在多数软件公司&#xff0c;会有两种需求&#xff0c;一种…

【vue3】小小入门介绍

⭐【前言】 首先&#xff0c;恭喜你打开了一个系统化的学习专栏&#xff0c;在这个vue专栏中&#xff0c;大家可以根据博主发布文章的时间顺序进行一个学习。博主vue专栏指南在这&#xff1a;vue专栏的学习指南 &#x1f973;博主&#xff1a;初映CY的前说(前端领域) &#x1f…

python自动发送邮件,qq邮箱、网易邮箱自动发送和回复

在python中&#xff0c;我们可以用程序来实现向别人的邮箱自动发送一封邮件&#xff0c;甚至可以定时&#xff0c;如每天8点钟准时给某人发送一封邮件。今天&#xff0c;我们就来学习一下&#xff0c;如何向qq邮箱&#xff0c;网易邮箱等发送邮件。 一、获取邮箱的SMTP授权码。…

new动态内库管理库学习

new文件是动态内存管理库的一部分&#xff0c;特别提供低层内存管理特性。 它包括bad_alloc, bad_array_new_length&#xff0c;nothrow_t&#xff0c;align_val_t类nothrow常量&#xff0c;以及函数 operator newoperator new[],operator deleteoperator delete[],get_new_han…

微信小程序登录注册页面

// login.js // 获取应用实例 var app getApp() var api require("../../utils/api.js")Page({data: {motto: zhenbei V1.0.0,userInfo: {},hasUserInfo: false,disabled: true,btnstate: default,username: ,password: ,canIUse: wx.canIUse(button.open-type.get…

Python实现人脸识别检测, 对美女主播照片进行评分排名

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 素材、视频、代码、插件安装教程我都准备好了&#xff0c;直接在文末名片自取就可点击此处跳转 开发环境: Python 3.8 Pycharm 2021.2 模块使用&#xff1a; requests >>> pip install requests tqdm >…

如何利用WDM波分复用技术来扩展光纤容量?

文章导读&#xff1a; 如何利用WDM来扩展光纤容量&#xff1f; 什么是Mux合波和Demux分波&#xff1f; CWDM, DWDM, OADM 了解WDM的常用波段 WDM技术&#xff1a;TFF和AWG WDM-PON应用于接入网 WDM网络拓扑在5G传输中的应用 网络提供商一直面临着如何应对不断扩大的带宽需求&a…

【Pytorch】利用PyTorch实现图像识别

本文参加新星计划人工智能(Pytorch)赛道&#xff1a;https://bbs.csdn.net/topics/613989052 这是目录使用torchvision库的datasets类加载常用的数据集或自定义数据集使用torchvision库进行数据增强和变换&#xff0c;自定义自己的图像分类数据集并使用torchvision库加载它们使…

3月最新!AIGC公司生态地图;开发者实用ChatGPT工具清单;上手必会的SD绘图教程;字幕组全自动化流程大公开 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 『光年之外诚邀产品经理加入』古典产品经理的复兴&#xff01; 光年之外创始人王慧文在社交平台发帖&#xff0c;公布联合创始人团队基…

【C语言初阶】循环语句

文章目录&#x1f490;专栏导读&#x1f490;文章导读&#x1f337;什么是循环&#x1f337;while循环&#x1f337;do while循环&#x1f337;for循环&#x1f337;循环结构中的break与continue&#x1f33a;break&#x1f33a;continue&#x1f337;goto语句&#x1f490;专栏…

5G将在五方面彻底改变制造业

想象一下这样一个未来&#xff0c;智能机器人通过在工厂车间重新配置自己&#xff0c;从多条生产线上组装产品。安全无人机处理着从监视入侵者到确认员工停车等繁琐的任务。自动驾驶汽车不仅可以在建筑物之间运输零部件&#xff0c;还可以在全国各地运输。工厂检查可以在千里之…

java基于SSH框架的超市管理系统mvc

目 录 1、引言 4 1.1 研究现状 4 1.2 主要研究的目的及内容 5 1.3 研究方法及设计思路 5 1.3.1 研究方法 5 1.3.2 设计思路 6 2、应用需求分析与可行性分析 6 2.1 应用需求分析 7 2.2 运行需求分析 8 2.3 其他需求分析 8 2.4 可行性分析 8 2.…

SpringBoot实战(十三)集成 Admin

目录一、简介二、搭建 springboot-admin 管理服务1.Maven 依赖2.application.yml3.添加 EnableAdminServer4.启动服务&#xff0c;查看页面三、搭建 springboot-admin-client 客户端服务1.Maven 依赖2.application.yml3.启动服务&#xff0c;查看页面四、搭配 Eureka 使用1.搭建…

二叉树的顺序存储与手撕数据结构—堆

TIPS树的话是一种非线性的数据结构&#xff0c;他实际上就是具有一定层次关系的数据集合&#xff0c;并且在树形结构当中&#xff0c;子树之间不能有任何的交集&#xff0c;否则就不是树形结构。然后对于树而言的话&#xff0c;在实际应用当中并不是特别多&#xff0c;在实际应…
最新文章