草庐IT

Verilog | I2C详解与Verilog实现

初雪白了头 2023-11-29 原文

一、I2C接口介绍

1.1 简介

​ IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,是一种同步、半双工的通信总线,用于连接微控制器及其外围设备。I2C总线产生于在80年代,最初为音频和视频设备开发,如今主要在服务器管理中使用,其中包括单个组件状态的通信。例如管理员可对各个组件进行查询,以管理系统的配置或掌握组件的功能状态,如电源和系统风扇。可随时监控内存、硬盘、网络、系统温度等多个参数,增加了系统的安全性,方便了管理。IIC数据传输速率有标准模式(100 kbps)、快速模式(400 kbps)和高速模式(3.4 Mbps),另外一些变种实现了低速模式(10 kbps)和快速+模式(1 Mbps)。

IIC总线的特点

  • 简单性和有效性。由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。

  • 支持多主控(multimastering), 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控占用IIC总线。

下图是一个嵌入式系统中处理器仅通过2根线的IIC总线控制多个IIC外设的典型应用图:

下图是I2C多master多slave示意图:

若想实现多master多slave效果:

  • 多个master-slave 时钟、数据线连在一起,需要实现信号的“线与”逻辑(所以SDA、SCL 被设计为漏极开路结构,外加上拉电阻实现“线与”)。
  • 需要实现 “时钟同步”和“总线仲裁”,引脚在输出信号的同时还能对引脚上的电平进行检测,检测是否与刚才输出一致,为 “时钟同步”和“总线仲裁”提供硬件基础。
  • I2C在读写时需要带上设备地址,这样不使用多的信号线就可指定特定的slave(而SPI通信需要多的片选线)。

1.2 I2C总线协议详解

​ IIC总线接口是一个标准的双向传输接口,一次数据传输需要主机和从机按照IIC协议的标准进行。I2C总线是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,并且在硬件上都需要接一个上拉电阻到VCC。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。

IIC的写过程(如图):

  1. Master发起START
  2. Master发送I2C 控制字(7bit)和W(写)操作0(1bit),等待ACK
  3. Slave发送ACK
  4. Master发送存储单元 addr(8bit),等待ACK
  5. Slave发送ACK
  6. Master发送data(8bit),即要写入寄存器中的数据,等待ACK
  7. Slave发送ACK
    第6步和第7步可以重复多次,即顺序写多个寄存器
  8. Master发起STOP结束传输

IIC的读过程(如图):

  1. Master执行写过程的1-5步骤
  2. Master发起START
  3. Master发送I2C addr(7bit)和r(读)操作1(1bit),等待ACK
  4. Slave发送ACK
  5. Slave发送data(8bit),即寄存器里的值
  6. Master发送ACK
    第7步和第8步可以重复多次,即顺序读多个寄存器
  7. 当master接收完想要的数据后,由Master发送NACK,告知slave停止发送数据
  8. Master发送STOP结束传输

完整的读写过程如下图:

1.3 I2C的状态

1) 总线空闲状态:
SDA和SCL同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
2) 总线START:
SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
3) 总线STOP:
SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
4 )总线Restart:
SCL为高电平时,SDA由高电平向低电平跳变,本质上也是START信号,用在完整I2C读过程中的读阶段,在首次发送停止信号之前,master通过发送Restart信号,可以转换与当前slave的通信模式(从写模式到读模式),或是切换到与另一个slave通信。
5 )数据阶段:
在IIC总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定。只有在SCL为低电平期间,才允许SDA上的电平改变状态。简单的说就是,数据在SCL下降会被采样,所以SDA需要在SCL为高电平时保持稳定。
6) ACK与NACK信号:
IIC总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在第9个时钟脉冲期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

这段话再说细一点:
写阶段,master写了一字节数据,在第9个时钟脉冲期间释放数据线,由slave反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功;
读阶段,master向slave收数据,slave写了一字节数据,在第9个时钟脉冲期间释放数据线,由master反馈应答信号,ACK(低)表示数据成功接收,NACK(高)表示该字节没有接收成功。

还有一种特殊情况:当master决定不再接收数据时,应向slave发送NACK信号,高速slave不再发送。
以下情况会导致出现NACK位:

  1. 接收器没有发送机响应的地址,接收端没有任何ACK发送给发送器
  2. 由于接收器正在忙碌处理实时程序导致接无法接收或者发送
    传输过程中,接收机器别不了发送机的数据或命令
  3. 接收器无法接收
  4. master接收完成读取数据后,要发送NACK结束告知slave。
    当master接收到slave的NACK信号后,可以STOP这次传输,也可以重新START。
    所以:NACK并不只是表示字节没有成功接收,也可以表示master告诉slave不再需要发送数据

二、I2C的Verilog简单实现

代码:

`timescale 1ns / 1ps
//
// Module Name: IIC_control
// Tool Versions: Vivado 2018.3
// Description: 
// 
//

module IIC_control #(
	parameter	 DIV_CLK = 'd500,	//系统时钟分频系数
	parameter   WMEN_LEN = 8'd0, 	//写数据帧数
	parameter   RMEN_LEN = 8'd0		//读数据帧数
	)
	(
	input 							sys_clk,	//系统时钟
	input 							sys_rst_n,	//系统复位
	input		 					iic_en,		//iic使能信号
	input		[WMEN_LEN*8-1'b1:0]	w_data,		//iic设备的写入数据,其中w_data[7:0]应该是地址帧
	input		[7:0]				wd_cnt,		//iic写入数据的帧数
	output	reg [RMEN_LEN*8-1'b1:0]	r_data,		//iic读取数据存储
	input		[7:0]				rd_cnt,		//iic读取数据的帧数
	output 		reg					iic_busy,	//iic工作标识,1为忙,2为空闲
	//标准的iic设备总线
	inout							iic_sda,   	//iic总线的双向数据线
	output		reg					iic_scl		//iic总线的时钟线
    );

    parameter	IDLE 		= 4'd0,				//空闲状态
    			START 		= 4'd1,				//开始
				W_WAIT 		= 4'd2,				//写状态
    			W_ACK 		= 4'd3,				//写响应
    			R_WAIT 		= 4'd4,				//读状态
    			R_ACK 		= 4'd5,				//读响应
    			STOP1 		= 4'd6,				//结束1
    			STOP2 		= 4'd7;				//结束2
    reg  [3:0]	iic_s;			//	状态机状态
    reg 		scl_clk;		//分频时钟,通过对其移位获得iic串行时钟信号
    reg 		iic_mode;		// 设置iic数据线状态,1为输出(写),0为输入(读)
    reg [2:0] 	bcnt;			//比特计数
    reg [7:0] 	wcnt;			//写字节计数
    reg [7:0] 	rcnt;			//读字节计数
    reg 		scl_r;			//iic时钟信号寄存,时钟信号的来源包括两部分,一是iic空闲时主动拉高,二是每比特数据传输时拉高
    reg 		sda_r;			//iic数据线寄存,W_ACK时进行响应判断
    reg 		sda_o = 0;		//iic数据线,写数据
    reg [7:0] 	sda_o_r;		//iic数据线,写数据寄存器
    reg [7:0] 	sda_i_r;		//iic数据线,读数据寄存
	reg [$clog2(DIV_CLK):0] clk_cnt;	//分频时钟计数
//	reg					iic_busy;                      
	reg 				rd_en;		//读数据使能信号
    //分频器对系统时钟进行分频处理,产生串行时钟信号
    always @(posedge sys_clk or negedge sys_rst_n)
    begin
    	if(!sys_rst_n)	begin
    		clk_cnt <= 0	;
    		scl_clk <= 0;
		end
    	else if(clk_cnt == (DIV_CLK>>1) -1'b1)begin//保持50%占空比
    		clk_cnt <= 0;
    		scl_clk <= ~scl_clk;
    	end
    	else clk_cnt <= clk_cnt +'b1;
    end
    //
    always @(*)
    begin
    	if(iic_s == IDLE || iic_s == STOP1 || iic_s == STOP2) //主动拉升iic时钟线
    		scl_r <=  1'b1;
		else scl_r <= ~scl_clk;
    end
    //使iic时钟线高电平保持在数据线传输每比特数据的中心
    wire scl_offset = (clk_cnt == DIV_CLK>>2); 				
    always @(posedge sys_clk) iic_scl <= scl_offset ? scl_r : iic_scl;		//产生iic串行时钟
    //iic状态机,首先发送地址帧
    always @(negedge scl_clk or negedge sys_rst_n)			//在scl_clk时钟下降沿改变状态,为了保证scl与sda的时序
    begin
    	if(!sys_rst_n) begin
    		iic_s 	 <= IDLE ;		//状态机复位
    		iic_mode <= 1'b1; 		// 设置iic为输出
    		bcnt 	 <= 3'd7;		//iic每次先发送高位,因此初始化赋值为7,发送最高比特,然后递减
    		wcnt 	 <= 0;			//将已写入数据帧数标识置零
    		rcnt 	 <= 0;			//将已读出数据帧数标识置零
    		iic_busy <= 0;			//iic空闲
    		rd_en 	 <= 0;			//
    	end
    	else 
    		case(iic_s)
				IDLE : begin						// 空闲状态设置scl与sda均为1
					if(iic_en || rd_en)begin
						iic_s 	 <= START;			//接收到使能信号iic开始工作	
						iic_busy <= 1'b1;			//iic忙
						iic_mode <=  1'b1; 			// 设置iic为写入
					end
					else begin
						iic_mode 	<= 1'b1;	 	// 设置iic为输出
						wcnt 		<= 0;			//将已写入数据帧数标识置零
						rcnt 		<= 0;			//将已读出数据帧数标识置零
						rd_en 		<= 1'b0;
						iic_busy 	<= 1'b0;			
    				end
				end
				START: begin					
					bcnt <= 3'd7;					//数据从最高位开始传输
					iic_s <= W_WAIT;				
				end
				W_WAIT:begin				
					if(bcnt >0) 
						bcnt <= bcnt -1'b1;		//计数减一,写入下一位数据
					else begin
						iic_s <= W_ACK;	
						wcnt <=  wcnt +1'b1;	//每帧数据写入完成后,帧计数器加一
						iic_mode <=  1'b0 ; 	// 设置iic为读取,即从机向主机输入
					end
						
				end
				W_ACK:begin	
					if(wcnt < wd_cnt) begin
						iic_s <= W_WAIT;
//					 	wcnt <=  wcnt +1'b1;
					 	bcnt <= 3'd7;
					 	iic_mode <=  1'b1 ; 	// 下一个状态是W_WAIT,因此设置iic为写入,即主机向从机输入
					end		
					else if(rd_cnt>0) begin		//如果rd_cnt大于0表示iic是读模式
						if(rd_en == 1'b0) begin //rd_en==0表明还未写入读控制字,跳转到IDLE状态进行读控制字写入
							rd_en <= 1'b1;
							iic_s <= IDLE;
							iic_mode <=  1'b1 ;
						end
						else 
							iic_s <= R_WAIT ;	//已写入读控制字,开始进行数据读取
						bcnt <= 3'd7;
					end		
					else 
						iic_s <= STOP1;			//写入完成跳转到结束状态
					if(sda_r !== 1'b0)			//如果未接收到从机应答信号,停止发送
						iic_s <= STOP1;
				end
				R_WAIT:begin					//读取数据
					rd_en <= 1'b0;				//置零,及时释放
					if(bcnt > 0) begin
						bcnt <= bcnt -1'b1;
					end
					else begin
						rcnt <=  rcnt +1'b1;	
						iic_s <= R_ACK;			
						iic_mode <=  1'b1 ; 	// 设置iic为写入,输出应答信号
					end					
				end
				R_ACK:begin
					if(rcnt < rd_cnt) begin					 	
					 	bcnt <= 3'd7;
					 	iic_s <= R_WAIT;
					 	iic_mode <=  1'b0 ; // 设置iic为读取
					end
					else 
				    begin
						iic_s <= STOP1;
					end
				end
				STOP1:begin//sda = 0 scl = 1
					iic_s <= STOP2;
				end
				STOP2:begin//sda = 1 scl =1
					iic_s <= IDLE;
				end
				default:
					iic_s <= IDLE;
    		endcase
    end
    //IIC总线的SDA数据线是一个双向IO口,使用三态门
	assign iic_sda = iic_mode ? sda_o: 1'bz;
    //sda输出
    always @(*)
    begin
    	if(!sys_rst_n || iic_s == STOP2)
    	begin			//sda = 1
    		sda_o <=  1'b1;
    	end
    	else if(iic_s == START || iic_s == STOP1 || (iic_s == R_ACK && rcnt != rd_cnt))
    	begin   		//sda = 0 
			sda_o <=  0;
		end
		else if(iic_s == W_WAIT)
		begin			//输出最高位
			sda_o <= sda_o_r[7];			
		end				//其他状态数据线拉高
		else sda_o <= 1'b1;
    end
    always @(negedge scl_clk)
    begin
    	if(iic_s == W_ACK || iic_s == START)begin
    		sda_o_r <= rd_en?({w_data[7:1],1'b1}):(w_data[(wcnt*8)+:8]);
//			sda_o_r <= w_data[(wcnt*8)+:8];
    		/*写、读模式下均存在wcnt=0的时候,此时w_data应该是地址帧,
    		即w_data[7:0]为地址帧,其中w_data[0]为模式,0代表写,1代表读*/
//    		if(rd_en) sda_o_r <= {w_data[7:1],1'b1};
    	end
    	else if(iic_s == W_WAIT)	//移位寄存器
    		sda_o_r <= {sda_o_r[6:0],1'b1};
		else 
			sda_o_r <= sda_o_r;
    end
    //暂存iic数据线的数据,用于检测从机应答
    always @(posedge scl_clk)	sda_r <= iic_sda;
    //读模式,
    always @(posedge scl_clk or negedge sys_rst_n)
    begin
    	if(!sys_rst_n)
    		r_data <= 'b0;
		else if(iic_s == R_ACK)
			r_data[((rcnt-1)*8)+:8] <= sda_i_r;
		else if(iic_s == R_WAIT || iic_s == W_ACK)
			sda_i_r <= {sda_i_r[6:0],iic_sda};
		else
			sda_i_r <= 0;
    end
      
endmodule

参考:
IIC总线解析(包含时钟拉伸,地址扩展,死锁,总线冲突总线仲裁的问题描述)
I2C详解
【接口时序】6、IIC总线的原理与Verilog实现

有关Verilog | I2C详解与Verilog实现的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  5. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  6. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  7. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

  8. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

  9. ruby-on-rails - 使用 Ruby 正确处理 Stripe 错误和异常以实现一次性收费 - 2

    我查看了Stripedocumentationonerrors,但我仍然无法正确处理/重定向这些错误。基本上无论发生什么,我都希望他们返回到edit操作(通过edit_profile_path)并向他们显示一条消息(无论成功与否)。我在edit操作上有一个表单,它可以POST到update操作。使用有效的信用卡可以正常工作(费用在Stripe仪表板中)。我正在使用Stripe.js。classExtrasController5000,#amountincents:currency=>"usd",:card=>token,:description=>current_user.email)

  10. ruby - Ruby 1.8 的 Shellwords.shellescape 实现 - 2

    虽然1.8.7的构建我似乎有一个向后移植的Shellwords::shellescape版本,但我知道该方法是1.9的一个特性,在1.8的早期版本中绝对不支持.有谁知道我在哪里可以找到(以Gem形式或仅作为片段)针对Ruby转义的Bourne-shell命令的强大独立实现? 最佳答案 您也可以从shellwords.rb中复制您想要的内容。在Ruby的颠覆存储库的主干中(即GPLv2'd):defshellescape(str)#Anemptyargumentwillbeskipped,soreturnemptyquotes.ret

随机推荐