基于Verilog的IIC接口通信设计(支持多字节地址读写)


一、IIC协议简介

IIC(Inter-Integrated Circuit)协议是一种同步串行通信接口,它采用半双工工作模式,即在同一时间只能进行单向的数据传输。总线由两条信号线组成:一条是数据线SDA(Serial Data Line),用于传输数据;另一条是时钟线SCL(Serial Clock Line),由主设备提供时钟信号,以确保所有连接到总线的设备同步进行数据交换。
在IIC总线上,每个从设备都有一个唯一的地址,主设备通过发送这个地址来选择与其通信的目标设备。由于支持多主控功能,多个具备主控能力的设备可以在同一总线上竞争控制权,并通过硬件仲裁机制避免冲突。在这里插入图片描述
IIC总线四种特点:
① 由时钟线SCL和数据线SDA组成,并且都接上拉电阻,确保总线空闲状态为高电平;
② 总线支持多设备连接,允许多主机存在,每个设备都有一个唯一的地址;
③ 连接到总线上的数目受总线的最大电容400pf限制;
④ 数据传输速率:标准模式100k bit/s、快速模式400k bit/s 、高速模式3.4Mbit/s。

由于其物理接口简单且占用线路少,I2C被广泛应用于嵌入式系统和电子设备中,方便连接各种低速外设,例如传感器、存储器、时钟芯片等。同时,I2C支持多种速度等级,能满足不同应用环境对数据传输速率的需求。

二、时序分析

1、协议时序

IIC通信协议的时序分为开始信号、地址字节、数据字节和停止信号四个阶段。
在这里插入图片描述

  1. 开始信号(Start Signal):
    主设备通过拉低数据线(SDA)时钟线(SCL)仍为高电平来发送开始信号。此时,从设备需准备好接收数据,并等待地址字节的到来。
  2. 地址字节(Address Byte):
    主设备发送一个地址字节到从设备以确定通信对象。地址字节的高七位是设备的地址,最低一位是读写控制位,通常为0表示写操作,1表示读操作。此时,从设备会检查其地址是否与发送的地址字节匹配。
  3. 数据字节(Data Byte):
    主设备和从设备之间的数据传输是通过数据字节来完成的。主设备发送数据字节,从设备接收数据字节。数据的传输是以字节为单位的,每个数据字节传输后都会有一个应答信号。
  4. 停止信号(Stop Signal):
    主设备发送停止信号作为传输的结束标志。停止信号通过将数据线从低电平拉升至高电平实现,此时时钟线仍然保持高电平。

2、实例分析

以下时序案例,在工程实例中比较常见。包括单地址、多地址的读写,以及读写数据的突发时序,本设计代码都可以扩展实现。
1、单字节地址写时序
在这里插入图片描述
2、双字节地址写时序
在这里插入图片描述
3、单字节地址读时序
在这里插入图片描述
4、双字节地址读时序
在这里插入图片描述

三、设计实现

该模块的设计具有以下两个特点:①读写地址可扩展,支持多字节地址实现;②读写数据可扩展,支持多字节地址实现;只需要修改相关代码参数就可以实现。源码下载链接:FPGA-Verilog语言-IIC接口驱动代码

参考代码如下所示:

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2024/03/29 09:43:38
// Design Name: 
// Module Name: iic_comm
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//


module iic_comm#(
	parameter  	SYS_CLOCK = 60_000_000,	//系统时钟频率
	parameter  	SCL_CLOCK = 100_000		//iic时钟频率
	)(
	input                  i_clk			,	//系统时钟
	input                  i_rst_n			,	//系统复位
	
	input                  i_wr_en			,	//写使能
	input                  i_rd_en			,	//读使能
	input          [6:0]   i_dev_addr		,	//器件地址
	input          [15:0]  i_reg_addr		,	//寄存器地址,单字节时从低字节有效
	input          [1:0]   i_reg_addr_num	,	//寄存器地址字节数 1: 1字节,其他:2字节

    input          [7:0]   i_wr_data		,	//写数据
	input          [1:0]   i_wr_data_num	,  	//写数据字节数,默认为1
	
	output  reg            o_rd_data_vaild	,	//读数据有效	
	output  reg    [7:0]   o_rd_data		,	//读数据	
	input          [1:0]   i_rd_data_num	,  	//读数据字节数,默认为1

	output  reg            o_cfg_done 		,	//一次读写操作完成标志
	
	inout 		           io_SDA			,   //IIC--SDA
	output  reg            o_SCL				//IIC--SCL
);

//================================================================================
localparam  SCL_CNT_M = SYS_CLOCK / SCL_CLOCK;

localparam 	IDLE 		= 9'b0_0000_0001;  //独热编码
localparam  WR_START 	= 9'b0_0000_0010;
localparam  WR_CTRL 	= 9'b0_0000_0100;
localparam  WR_REG_ADDR = 9'b0_0000_1000;
localparam  WR_DATA 	= 9'b0_0001_0000;
localparam  RD_START 	= 9'b0_0010_0000;
localparam  RD_CTRL 	= 9'b0_0100_0000;
localparam  RD_DATA 	= 9'b0_1000_0000;
localparam  STOP 		= 9'b1_0000_0000;

//-------------------------------------------------
// 变量声明
//-------------------------------------------------
reg  [15:0]	scl_cnt;		//clk计数器,用于产生scl时钟
reg 		scl_high;		//scl高电平中部标志
reg 		scl_low;		//scl低电平中部标志
reg 		scl_vaild;		//scl有效标志

reg  [8:0]  main_state;	   	//状态寄存器
reg         sda_reg;		//sda输出寄存器
reg 		sda_en;			//sda三态使能
reg 		sda_task_flag;	//串行输出输入任务执行标志位
reg 		w_flag;			//写标志
reg 		r_flag;			//读标志
reg  [7:0]	scl_level_cnt;	//scl高低电平计数器
reg 		ack;			//应答信号
reg  [1:0]	wdata_cnt;		//写数据字节数计数器
reg  [1:0]	rdata_cnt;		//读数据字节数计数器
reg  [1:0]	reg_addr_cnt;	//地址字节数计数器
reg  [7:0]	sda_data_out;	//数据输出buffer
reg  [7:0]	sda_data_in;	//数据输入buffer
wire [7:0]	wr_ctrl_word;	//写控制字
wire [7:0]	rd_ctrl_word;	//读控制字
wire 		rdata_vaild; 	//读数据有效前寄存器

assign wr_ctrl_word = {i_dev_addr,1'b0};
assign rd_ctrl_word = {i_dev_addr,1'b1};

//----------------------------------------------------
// iic时钟信号生成
//----------------------------------------------------
/* iic 非空闲状态产生 scl_vaild */
always @(posedge i_clk) begin
	if(!i_rst_n)
		scl_vaild <= 1'b0;
	else begin
		if(i_wr_en | i_rd_en)
			scl_vaild <= 1'b1;
		else if(o_cfg_done)
			scl_vaild <= 1'b0;
	end
end
	
/* o_SCL 计数器*/
always @(posedge i_clk) begin
	if(!i_rst_n)
		scl_cnt <= 16'd0;
	else begin
		if(scl_vaild) begin
			if(scl_cnt == SCL_CNT_M-1'b1)
				scl_cnt <= 16'd0;
			else
				scl_cnt <= scl_cnt + 16'd1;
		end
		else
			scl_cnt <= 16'd0;
	end
end
	
/* o_SCL 时钟产生*/
always @(posedge i_clk) begin
	if(!i_rst_n)
		o_SCL <= 1'b1;
	else begin
		if(scl_cnt == SCL_CNT_M >> 1) 
			o_SCL <= 1'b0;
		else if(scl_cnt == 16'd0)
			o_SCL <= 1'b1;
	end
end

/*o_SCL 高低电平中部标志*/
always @(posedge i_clk) begin
	if(!i_rst_n) begin
		scl_high <= 1'b0;
		scl_low  <= 1'b0;
	end
	else begin
		if(scl_cnt == (SCL_CNT_M >> 2))
			scl_high <= 1'b1;
		else
			scl_high <= 1'b0;
			
		if(scl_cnt == ((SCL_CNT_M >> 1) + (SCL_CNT_M >> 2)))
			scl_low <= 1'b1;
		else
			scl_low <= 1'b0;			
	end
end

//----------------------------------------------------
// iic主程序状态机
//----------------------------------------------------
always @(posedge i_clk) begin
	if(!i_rst_n) begin
		main_state 		<= IDLE;
		sda_reg 	  	<= 1'b1;	
		w_flag 			<= 1'b0;
		r_flag 			<= 1'b0;
		o_cfg_done 		<= 1'b0;
		reg_addr_cnt 	<= 2'd1;
		wdata_cnt 		<= 2'd1;
		rdata_cnt 		<= 2'd1;
	end
	else begin		
		case(main_state)
			IDLE: begin
				sda_reg   	 <= 1'b1;	
				w_flag    	 <= 1'b0;
				r_flag 	 	 <= 1'b0;
				o_cfg_done 	 <= 1'b0;
				reg_addr_cnt <= 2'd1;
				wdata_cnt 	 <= 2'd1;
				rdata_cnt 	 <= 2'd1;
				
				if(i_wr_en) begin 
					main_state <= WR_START;
					w_flag     <= 1'b1;
				end	
				else if(i_rd_en) begin
					main_state <= WR_START; 
					r_flag     <= 1'b1;
				end
			end
			//------------------- iic起始信号 ---------------------------			
			WR_START: begin
				if(scl_high) begin
					main_state <= WR_START;
					sda_reg    <= 1'b0;
				end
				else if(scl_low) begin
					main_state    <= WR_CTRL;
					sda_data_out  <= wr_ctrl_word;	// 准备要发送的控制字
					sda_task_flag <= 1'b0; 			// 开始串行传输任务
				end
			end
			//---------------- 写设备地址、寄存器地址 -------------------
			WR_CTRL: begin
				if(sda_task_flag == 1'b0) // 发送数据
					send_8bit_data;
				else begin	              // 等待响应
					if(ack == 1'b1) begin // 收到响应
						if(scl_low) begin // 准备发送的寄存器地址数据
							main_state 	  <= WR_REG_ADDR;// 转换到寄存器地址
							sda_task_flag <= 1'b0;
							if(i_reg_addr_num == 2'b1)
								sda_data_out <= i_reg_addr[7:0];
							else
								sda_data_out <= i_reg_addr[15:8];//如果寄存器地址为2个字节,要保证先发的最高位
						end
					end
					else // 未收到响应
						main_state <= IDLE;
				end
			end			
			WR_REG_ADDR: begin
				if(sda_task_flag == 1'b0)
					send_8bit_data;
				else begin
					if(ack == 1'b1) begin //收到响应
						if(reg_addr_cnt == i_reg_addr_num) begin // 寄存器地址数据发送完成
							if(w_flag && scl_low) begin
								main_state    <= WR_DATA;       //状态转移
								sda_data_out  <= i_wr_data[7:0];//数据准备
								sda_task_flag <= 1'b0;
								reg_addr_cnt  <= 2'd1;
							end
							else if(r_flag && scl_low) begin
								main_state    <= RD_START; 
								sda_reg       <= 1'b1; //sda拉高
							end
						end
						else begin					// 寄存器地址数据没有发送完成
							if(scl_low) begin
								main_state    <= WR_REG_ADDR;
								reg_addr_cnt  <= reg_addr_cnt + 2'd1;
								sda_data_out  <= i_reg_addr[7:0]; // 准备低8位寄存器地址
								sda_task_flag <= 1'b0;
							end
						end		
					end
					else	// 未收到响应
						main_state <= IDLE;					
				end
			end		
			//----------------- 写iic数据 ---------------------------------
			WR_DATA: begin
				if(sda_task_flag == 1'b0)
					send_8bit_data;
				else begin
					if(ack == 1'b1) begin // 收到响应
						if(wdata_cnt == i_wr_data_num) begin //发送完成
							if(scl_low) begin
								main_state <= STOP;
								sda_reg    <= 1'b0;
								wdata_cnt  <= 2'd1;
							end
						end
						else begin //未发送完成
							if(scl_low) begin
								main_state    <= WR_DATA;
								sda_data_out  <= i_wr_data[7:0];
								wdata_cnt     <= wdata_cnt + 2'd1;
								sda_task_flag <= 1'b0;
							end
						end
					end
					else // 未收到响应
						main_state <= IDLE;
				end
			end
			//----------------- 读iic数据 ---------------------------------
			RD_START: begin
				if(scl_high) begin
					main_state    <= RD_START;
					sda_reg       <= 1'b0;
				end
				else if(scl_low) begin
					main_state    <= RD_CTRL;
					sda_data_out  <= rd_ctrl_word;	// 准备要发送的控制字
					sda_task_flag <= 1'b0; 			// 开始串行传输任务
				end
			end			
			RD_CTRL: begin
				if(sda_task_flag == 1'b0)  // 发送数据
					send_8bit_data;
				else begin	// 等待响应
					if(ack == 1'b1) begin  // 收到响应
						if(scl_low) begin  // 准备发送的寄存器地址数据
							main_state    <= RD_DATA; // 转换到寄存器地址
							sda_task_flag <= 1'b0;
						end
					end
					else // 未收到响应
						main_state <= IDLE;
				end
			end			
			RD_DATA: begin
				if(sda_task_flag == 1'b0)
					receive_8bit_data;
				else begin
					if(rdata_cnt == i_rd_data_num) begin  // 接收完成
						sda_reg <= 1'b1;  //发送 ACK,不读了
						if(scl_low) begin
							main_state <= STOP;
							sda_reg    <= 1'b0;
						end
					end
					else begin
						sda_reg <= 1'b0; // 发送NACK,继续读下一个字节
						if(scl_low) begin
							rdata_cnt     <= rdata_cnt + 2'd1;
							sda_task_flag <= 1'b0;
						end
					end
				end
			end
			//------------------- iic停止信号 ---------------------------
			STOP: begin
				if(scl_high) begin
					sda_reg    <= 1'b1;
					main_state <= IDLE;
					o_cfg_done <= 1'b1;
				end
			end
			
			default: main_state <= IDLE;
		endcase
	end
	
end

//----------------------------------------------------
// 串行数据任务,收/发8bit数据
//----------------------------------------------------
/*发送接收数据时 o_SCL 时钟计数*/
always @(posedge i_clk) begin
	if(!i_rst_n)
		scl_level_cnt <= 8'd0;
    else begin
		//这几个状态需要执行数据发送接收任务
		if(main_state == WR_CTRL || main_state == WR_REG_ADDR || main_state == WR_DATA ||
		   main_state == RD_CTRL || main_state == RD_DATA) begin  
			if(scl_low | scl_high) begin
				if(scl_level_cnt == 8'd17)
					scl_level_cnt <= 8'd0;
				else
					scl_level_cnt <= scl_level_cnt + 8'd1;
			end
		end
		else
			scl_level_cnt <= 8'd0;
	end
end

/*数据接收对发送的响应标志位*/
always @(posedge i_clk) begin
	if(!i_rst_n)
		ack <= 1'b0;
	else begin
		if((scl_level_cnt == 8'd16) && scl_high && (io_SDA === 1'd0))
			ack <= 1'b1;
		else if((scl_level_cnt == 8'd17) && scl_low)
			ack <= 1'b0;
	end
end

/* 输出串行数据任务 */
task send_8bit_data;
	if(scl_high && (scl_level_cnt == 8'd16)) //8bit data send o_cfg_done
		sda_task_flag <= 1'b1;
	else if(scl_level_cnt < 8'd17) begin
		sda_reg <= sda_data_out[7];
		if(scl_low)
			sda_data_out <= {sda_data_out[6:0],1'b0};
	end
endtask
	
/* 接收串行数据任务 */ 
task receive_8bit_data;
	if(scl_low && (scl_level_cnt == 8'd15))
		sda_task_flag <= 1'b1;
	else if(scl_level_cnt < 8'd15) begin
		if(scl_high)
			sda_data_in <= {sda_data_in[6:0],io_SDA};
	end
endtask

//----------------------------------------------------
// SDA三态门控制输出
//----------------------------------------------------						
/*io_SDA 三态门输出*/
assign io_SDA = sda_en ? sda_reg : 1'bz; 

always @(*) begin
	case(main_state)
		IDLE: sda_en <= 1'b0;  //输入
			
		WR_START,RD_START,STOP: sda_en <= 1'b1;  //输出
			
		WR_CTRL,WR_REG_ADDR,WR_DATA,RD_CTRL: begin
			if(scl_level_cnt < 8'd16)
				sda_en <= 1'b1;
			else
				sda_en <= 1'b0;
		end		
		RD_DATA: begin
			if(scl_level_cnt < 8'd16)
				sda_en <= 1'b0;
			else	
				sda_en <= 1'b1;
		end
		default: sda_en <= 1'b0;
	endcase
end

//----------------------------------------------------
// 读有效数据
//----------------------------------------------------						
/*读出数据有效标志位*/
assign rdata_vaild = (main_state == RD_DATA) && (scl_level_cnt == 8'd15) && scl_low;

/*读出的有效数据*/
always @(posedge i_clk) begin
	if(!i_rst_n) begin
		o_rd_data_vaild <= 1'b0;
		o_rd_data       <= 8'd0;
	end
	else begin
		if(rdata_vaild) begin
			o_rd_data_vaild <= 1'b1;
			o_rd_data       <= sda_data_in;
		end
		else
			o_rd_data_vaild <= 1'b0;
	end
end



endmodule

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
下面是一个简单的Verilog代码示例,用于实现IIC接口: module iic_interface ( input clk, //时钟 input rst, //复位 input sda, //数据线 inout scl, //时钟线 output reg ack //应答信号 ); reg [7:0] i2c_address; //I2C设备地址 reg [7:0] i2c_data; //I2C数据 //I2C状态 parameter IDLE = 2'b00; parameter START = 2'b01; parameter ADDR = 2'b10; parameter DATA = 2'b11; reg [1:0] state; //当前状态 reg bit read; //标志 reg [7:0] count; //计数器 assign scl = state[1] ? ~scl : 1'b1; //时钟线控制 always @(posedge clk) begin if (rst) begin //复位 state <= IDLE; read <= 1'b0; count <= 8'd0; ack <= 1'b0; end else begin case (state) IDLE: begin //空闲状态 if (~sda && ~scl) begin //START信号 state <= START; count <= 8'd0; i2c_address <= 8'd0; i2c_data <= 8'd0; read <= 1'b0; ack <= 1'b0; end end START: begin //发送START信号 if (count < 8'd1) begin scl <= 1'b0; count <= count + 1'b1; end else begin scl <= 1'b1; state <= ADDR; count <= 8'd0; end end ADDR: begin //发送地址 if (count < 8'd7) begin scl <= 1'b0; i2c_address <= {i2c_address[6:0], read}; count <= count + 1'b1; end else if (count == 8'd7) begin //发送标志 scl <= 1'b0; i2c_address <= {i2c_address[6:0], read}; count <= count + 1'b1; end else if (count == 8'd8) begin //取ACK信号 scl <= 1'b1; state <= DATA; count <= 8'd0; end end DATA: begin //发送/接收数据 if (count < 8'd7) begin //发送/接收数据位 scl <= 1'b0; i2c_data <= {i2c_data[6:0], sda}; count <= count + 1'b1; end else if (count == 8'd7) begin //发送/接收最后一位数据 scl <= 1'b0; i2c_data <= {i2c_data[6:0], sda}; count <= count + 1'b1; end else if (count == 8'd8) begin //取ACK信号 scl <= 1'b1; ack <= ~sda; if (read) begin //取数据 i2c_data <= {i2c_data[6:0], sda}; end count <= 8'd0; state <= IDLE; end endcase end end endmodule 这段代码实现了一个简单的IIC接口支持IIC设备的操作。在实际应用中,需要根据具体的设备和系统要求进行修改和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值