基于FPGA实现 I2C 协议的Master功能

本文主要介绍I2C协议及其Master的实现,两端通信中的主从过程如下:

一、FPGA开发低速通信方式

FPGA开发板使用的低速通信协议主要有UART、I2C、CAN、SPI,根据板卡原理图中协议接口的位置不同,实现方式主要有以下几种:

1.1 、外设位于Zynq的PS侧

当上述协议的IO位于ARM**,原理图中位置可为PS端的MIO或者EMIO。**

****直接使用片内硬核,vitis/eclipse嵌入式开发即可,开发便捷简单。只是PS端专用IO较少。

1.2 、外设位于Zynq的PL侧

当上述协议的IO位于PL**,原理图中位置可选用的PIN很多。实现方式有2中:**

1、RTL编码实现,常用verilog/Systemverilog/VHDL

2、利用片内硬核**,通过EMIO、GPIO方式,vitis/eclipse嵌入式开发即可**

1.3 、外设位于FPGA侧

当上述协议的IO位于纯FPGA**,原理图中位置可选用的PIN很多。实现方式有2中:**

1、RTL编码实现,常用verilog/Systemverilog/VHDL

2、利用软核MicroBlaze或者Nios**,vitis/eclipse嵌入式开发即可**

二、I2C通信

2.1 、I2C总线

I2C 即Inter-Integrated Circuit(集成电路总线)是由Philips 半导体公司设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场景下的主从通信。主机启动总线,并产生时钟用于数据传输,

控制总线的访问状态、产生START和STOP条件。

I2C 总线由数据线SDA 和时钟线SCL 构成通信线路,既可用于发送数据,也可接收数据。在主从间进行双向数据传输,数据的传输速率在标准模式下可达100Kbps,在快速模式下可达400Kbps,在高速模式下可达3.4Mbps。各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,各类I2C器件手册中都有相关规定)识别。

比如某开发板中的I2C 总线物理拓扑结构如下图所示,其中3款示例芯片都是I2C配置接口(所有的I2C外设按照其手册,并在电路上设计成不同地址)。

I2C 器件一般采用开漏结构与总线相连,所以I2C_SCL 和I2C_SDA 均需接上拉电阻;当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低。

2.2 、I2C通信过程

常用的I2C设备,设备地址一般为7位,寄存器/数据地址一般为8bit或16bit,访问的数据为8bit。

以一个7位从设备地址为例,对8位寄存器写入1字节的通信过程如下图所示:

上图的I2C写通信过程如下:

先是总线处于空闲状态,主机发起通信,然后发送从机的器件地址;从机设备收到其匹配地址后提供响应ACK,然后主机发送寄存器地址,匹配的从机提供响应ACK,主机发送写入寄存器的数据,匹配的从机提供响应ACK,最后主机提供一个停止信号,总线进入空闲状态。

2.2.1、空闲状态

总线空闲状态为:SCL一直处于高电平,同时SDA也一直处于高电平。

2.2.2、起始标志

通信开始的标志:主机发起,当SCL为高电平时,SDA从高电平跳变为低电平;总线上的从机设备收到该跳变则知道通信开始。

2.2.3 、从设备寻址

在起始阶段后,主机传送的第一个字节内容为7bit的从设备地址+1bit的读/写标志。

图中R/W=0表示主机发起写操作,R/W=1表示主机发起读操作。

2.2.4 、数据传输阶段(寄存器地址及内容)

数据传输遵循从MSB开始到LSB结束;

数据SDA变化只能在时钟SCL低电平时进行;

在时钟SCL高电平期间数据SDA必须保持稳定;

2.2.5、应答阶段

每传输完一个内容(地址/寄存器/数据)(8位或者16位),对端(主机或从机)在对应的第9(每字节结束)个时钟周期通过SDA线给出反馈。

对应时钟周期内SDA应答内容,为0表示数据成功接收,为1表示接收失败或数据传输结束。

2.2.6、结束标志

通信结束的标志:主机发起,当SCL为高电平时,SDA从低电平跳变为高电平。

2.3、I2C使用事项

在使用I2C外设时需要注意具体设备的存储寄存器地址长度读写(连续/单次)过程。

2.3.1、设备地址

有些设备的地址是固定的,有些设备的地址包含固定部分和可配置部分,对于板卡中总线上挂了多个I2C外设的场景,原理图及开发时要设计为I2C外设的地址互不相同。

2.3.2、寄存器地址

不同器件的寄存器读写性不同,有些内容对于主机而言只能读操作,有些内容对于主机即可进行读也可写操作。

此外,不同的外设其寄存器地址长度也不相同,与其存储空间大小有关;比如常见EEPROM的为8bit寄存器地址,也有13bit的寄存器地址。对于地址寄存器地址超过8bit的,在寻址阶段要用16个SCL周期传输地址,并在每字节后穿插应答阶段。

对于8bit寄存器地址:

对于多字节寄存器地址:

2.3.3、I2C单字节写

对于I2C单字节写时序如下图:

(1)主机产生并发送起始信号到从机,随后发送器件地址,读写控制位设置为低电平,表示对从机进行写数据操作。

(2)从机接收到写控制指令后,回传应答信号,如果从机没有应答则会输出I2C 通信错误信号,如果主机接收到应答信号,就开始寄存器地址的写入。根据器件类型(寄存器地址长度),若为双字节地址,先向从机写入高8 位地址,且高位在前低位在后;待接收到从机回传的应答信号,再写入低8 位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4);若为单字节地址跳转到步骤(3);

(3)按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4);

(4)字地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;

(5)单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单次写(字节写)完成。

2.3.4、I2C连续写

对于多字节的连续写时序如下,以寄存器地址为起始,按顺序写入N字节数据:

(1)主机产生并发送起始信号到从机,随后发送器件地址 ,读写控制位设置为低电 平,表示对从机进行写数据操作。

(2)从机接收到写控制指令后,回传应答信号;主机开始传输寄存器地址,若为双字节地址,先向从机写入高8位地址,且高位在前低位在后;待接收到从机 回传的应答信号,再写入低8位地址,且高位在前低位在后,双字节字地址写入完成后执行步骤(4);若 为单字节地址跳转到步骤(3);

(3)按高位在前低位在后的顺序写入单字节存储地址,单字节字地址写入完成后执行步骤(4);

(4)地址写入完成,主机接收到从机回传的应答信号后,开始第一个单字节数据的写入;

(5)数据写入完成,主机接收到从机回传应答信号后,开始下一个单字节数据的写入;

(6)直到所有数据写入完成,主机接收到从机回传应答信号后,执行步骤(7)。若所有数据未完成写 入,跳回到步骤(5);

(7)主机向从机发送停止信号,连续写操作完成。

2.3.5、I2C读过程

对于I2C单字节读时序如下图:

读过程与写过程类似,需要注意R/W不同,应答不同,以及多了ReStart过程。上图中灰色区域为主机发送内容白框内容为从机发送内容****

(1) 发送起始信号(Start)和设备地址: 主设备首先发送起始信号来开始通信,然后发送目标设备的地址和写命令。

(2) 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

(3) 寄存器地址: 然后发送需要读取的目标设备的寄存器地址。(单字节、双字节地址)

(4) 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

(5) 设备地址: 主机发送设备地址和读命令。

(6) 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

(7) 接收数据:主设备向从设备发送数据请求后,从设备会开始发送数据帧,主设备接收从设备发送的数据。

(8) 发送应答(ACK)或非应答(NACK): 主设备在接收每个数据字节后,会向从设备发送一个应答信号(ACK)或非应答信号(NACK),以指示是否要继续接收数据。如果主设备准备好继续接收数据,则发送应答信号(ACK);如果主设备不想继续接收数据(例如,数据传输完成),则发送非应答信号(NACK)。

(9) 重复步骤(7)和步骤(8): 主设备会重复步骤(7)和步骤(8),直到接收到所有需要的数据为止。

(10)发送停止信号(Stop): 当所有数据都被接收完毕后,主设备发送停止信号来结束通信。

2.3.6、当前地址的I2C读过程

上章节为标准的指定地址的寄存器读过程;此处的"当前地址"指的是上次读或写操作期间访问的最后一个地址,此读操作完成后寄存器内部地址自动加1。

此类读操作过程得以简化为:

开始 -> 设备地址读 -> 读数据1和n -> 停止

三、FPGA实现I2C Master

前几年的项目中用到了器件LTC2990 I2** C接口实现测量4路电压、电流、温度**),编码了I2C Msater模块,设计了如下状态机:****

复制代码
localparam [3:0] IDLE        = 4'd0;
localparam [3:0] START       = 4'd1;
localparam [3:0] RESTART     = 4'd2;
localparam [3:0] ADDR_SLAVE  = 4'd3;
localparam [3:0] ADDR_REG    = 4'd4;                 
localparam [3:0] READ        = 4'd5;
localparam [3:0] WRITE       = 4'd6;
localparam [3:0] NEXT_WRITE  = 4'd7;
localparam [3:0] ACK_RX      = 4'd8;
localparam [3:0] ACK_TX      = 4'd9;
localparam [3:0] STOP        = 4'hA;
localparam [3:0] RELEASE     = 4'hB;
code-snippet__js 复制代码

3.1 、状态机设计

该器件的寄存器地址为8bit,标准的单次读写状态机如下:

3.2 、相关源码

端口信息:

状态机相关代码:

复制代码
case (state)
            IDLE  :      begin
                             if ((i_req_trans == 1'b1)&&(busy == 1'b0)) begin
                                 busy           <= 1'b1;
                                 state          <= START;
                                 next_state     <= ADDR_SLAVE;
                                 
                                 addr_i2c_rw    <= i_addr_i2c_rw;
                                 rw             <= i_rw;
                                 data_w         <= i_data_w;
                                 byte_num       <= i_byte_num;
                                 
                                 scl_en         <= 1'b0;
                                 sda_temp       <= 1'b1;
                                 sda_rw_en      <= 1'b1;
                                 
                                 nack           <= 1'b0;
                                 byte_sent      <= 1'b0;
                                 read_reg_addr_sent_flag <= 1'b0;
                                 num_byte_sent  <= 8'd0;                        
                             end
                             else begin
                                 scl_en         <= 1'b0;
                                 sda_temp       <= 1'b1;
                                 sda_rw_en      <= 1'b1;
                             end
                         end
                         
            START :      begin
                             if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b1) && (clk_i2c_cnt == start_setup)) begin
                                 sda_temp       <= 1'b0;                            //set start bit for negedge of clock, and toggle for the clock to begin
                                 byte_sr        <= {addr_i2c_rw[7:1], 1'b0};        //Don't need to check read or write, will always have write in a read request as well
                                 sda_rw_en      <= 1'b1;
                                
                                 state          <= ADDR_SLAVE;
                             end
                             
                             scl_en         <= 1'b1;
                         end
                         
            RESTART  :   begin
                             if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0)) begin
                                 sda_rw_en      <= 1'b1;
                             end
                             
                             if((clk_i2c_reg2 == 1'b0) && (clk_i2c_reg1 == 1'b1)) begin
                                 scl_high       <= 1'b1;
                             end
                             
                             if(scl_high == 1'b1) begin
                                 if(clk_i2c_cnt == start_setup) begin   //Must wait minimum setup time
                                    scl_high       <= 1'b0;
                                    sda_temp       <= 1'b0;
                                    sda_rw_en      <= 1'b1;
                                    state          <= ADDR_SLAVE;
                                    byte_sr        <= {addr_i2c_rw[7:1], rw};
                                 end
                                 else begin
                                    sda_temp       <= 1'b1;
                                 end
                             end
                             else if (sda_rw_en == 1'b0) begin
                                 sda_temp       <= 1'b0;
                             end
                             else if ((8'd2 <= clk_i2c_cnt) && (clk_i2c_cnt <= 8'd100)) begin
                                 sda_temp       <= 1'b0;
                             end
                             else begin
                                 sda_temp       <= 1'b1;
                             end
                         end

            ADDR_SLAVE : begin
                             //When scl has fallen, we can change sda 
                             if((byte_sent == 1'b1) && (cnt[0] == 1'b1)) begin    //8bits
                                 byte_sent      <= 1'b0;               //deassert the flag
                                 state          <= ACK_RX;             //await for nack_ack
                                 next_state     <= read_reg_addr_sent_flag ? READ : ADDR_REG;    //Check to see if sub addr was sent, we ony reach this state again if doing a read
                                 
                                 
                                 byte_sr        <= i_reg_addr;
                                 
                                 sda_temp       <= 1'bz; 
                                 sda_rw_en      <= 1'b0;
                                 cnt            <= 3'd0;
                             end
                             else begin
                                 sda_rw_en      <= 1'b1;
                             
                                if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0)) begin
                                    scl_low       <= 1'b1;
                                end
                                
                                if(scl_low == 1'b1) begin
                                    if(clk_i2c_cnt == data_hold) begin
                                       {byte_sent, cnt}  <= {byte_sent, cnt} + 1'b1;  //incr cnt, with overflow being caught (due to overflow, no need to set cnt to 0)
                                       sda_temp       <= byte_sr[7];                  //send MSB
                                       byte_sr        <= {byte_sr[6:0], 1'b0};        //shift out MSB
                                       scl_low        <= 1'b0;
                                    end
                                end
                             end
                         end
            
			ADDR_REG : begin
                             //When scl has fallen, we can change sda 
                             if((byte_sent == 1'b1) && (cnt[0] == 1'b1)) begin    //8bits
                                 state          <= ACK_RX;                 //await for nack_ack
                                 next_state     <= rw ? RESTART : WRITE;   //move to appropriate state
                                 
								 byte_sr <= rw ? byte_sr : data_w;   //if write, want to setup the data to write to device
                                 read_reg_addr_sent_flag <= 1'b1;    //For dictating state of machine
								 
								 cnt            <= 3'd0;
								 byte_sent      <= 1'b0;
								 
								 sda_temp       <= 1'bz; 
                                 sda_rw_en      <= 1'b0;
                             end
                             else begin
                             
                                if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0)) begin
                                    scl_low       <= 1'b1;
                                end
                                
                                if(scl_low == 1'b1) begin
                                    if(clk_i2c_cnt == data_hold) begin
                                       {byte_sent, cnt}  <= {byte_sent, cnt} + 1'b1;  //incr cnt, with overflow being caught (due to overflow, no need to set cnt to 0)
                                       sda_temp       <= byte_sr[7];                  //send MSB
									   sda_rw_en      <= 1'b1;
                                       byte_sr        <= {byte_sr[6:0], 1'b0};        //shift out MSB
                                       scl_low        <= 1'b0;
                                    end
                                end
                             end
                         end
			
			READ :       begin
                             //When scl has fallen, we can change sda 
                             if(byte_sent == 1'b1) begin    //8bits
                                 byte_sent      <= 1'b0;
								 data_read      <= data_read_temp;
								 valid_out      <= 1'b1;
								 
								 state          <= ACK_TX;                 //Send ack
                                 next_state     <= (num_byte_sent == byte_num-1) ? STOP : READ;      //Have we read all bytes?
								 
								 ack_temp       <= (num_byte_sent == byte_num-1);
								 
								 num_byte_sent  <= num_byte_sent + 1'b1;  //Incr number of bytes read
                                 ack_in_prog    <= 1'b1; 
                                 sda_rw_en      <= 1'b0;
                             end
                             else begin
                                 sda_rw_en      <= 1'b0;
								 
                                if((clk_i2c_reg2 == 1'b0) && (clk_i2c_reg1 == 1'b1)) begin
                                    scl_high      <= 1'b1;
                                end
                                
                                if(scl_high == 1'b1) begin
                                    if(clk_i2c_cnt == read_setup) begin
                                       valid_out      <= 1'b0;
									   
									   {byte_sent, cnt}  <= cnt + 1'b1; 
									   data_read_temp <= {data_read_temp[6:0], sda_reg2};
                 
                                       scl_high       <= 1'b0;
                                    end
                                end
                             end
                         end
			
			WRITE :      begin
                             if((byte_sent == 1'b1) && (cnt[0] == 1'b1)) begin    //8bits
                                 cnt            <= 3'd0;
								 byte_sent      <= 1'b0;
								 
								 state          <= ACK_RX;
								 next_state     <= (num_byte_sent == byte_num-1) ? STOP : NEXT_WRITE;
								 
								 sda_temp       <= 1'bz;
								 sda_rw_en      <= 1'b0;
								 num_byte_sent  <= num_byte_sent + 1'b1;
								 next_data      <= 1'b1;
                             end
                             else begin
								 
                                if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0)) begin
                                    scl_low       <= 1'b1;
                                end
                                
                                if(scl_low == 1'b1) begin
                                    if(clk_i2c_cnt == data_hold) begin
                                       {byte_sent, cnt}  <= {byte_sent, cnt} + 1'b1;
									   sda_temp       <= byte_sr[7];
									   byte_sr        <= {byte_sr[6:0], 1'b0};      
                                       scl_low        <= 1'b0;
									   sda_rw_en      <= 1'b1;
                                    end
                                end
                             end
                         end
						 
			NEXT_WRITE : begin
                             if (next_data == 1'b1) begin
							     req_data_chunk <= 1'b1;
                                 next_data      <= 1'b0;
							 end
                             else begin
								 state          <= WRITE;
                                 byte_sr        <= i_data_w;
                             end
                         end			 
			
			ACK_RX :     begin
                             sda_rw_en      <= 1'b0;
							 if((clk_i2c_reg2 == 1'b0) && (clk_i2c_reg1 == 1'b1)) begin
                                 scl_high       <= 1'b1;
                             end
							 
							 if (scl_high == 1'b1) begin
							     if(clk_i2c_cnt == start_setup) begin
                                     scl_high        <= 1'b0;

                                     if (sda_reg2 == 1'b0) begin
                                         state         <= next_state;
                                     end
                                     else begin
                                         nack          <= 1'b1;
										 busy          <= 1'b0;
										 sda_temp      <= 1'b1;
										 scl_en        <= 1'b0;
										 state         <= IDLE;
                                     end 									 
                                end
                             end
                         end
						 
			ACK_TX :     begin
							 if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0)) begin
                                 scl_low        <= 1'b1;
                             end
							 
							 if (scl_low == 1'b1) begin
							     if(clk_i2c_cnt == data_hold) begin
                                     scl_low         <= 1'b0;

                                     if (ack_in_prog == 1'b1) begin
                                         sda_temp      <= ack_temp;
										 ack_in_prog   <= 1'b0;
										 sda_rw_en     <= 1'b1;
                                     end
                                     else begin
                                         sda_temp      <= {next_state == STOP} ? 1'b1 : 1'bz;
										 sda_rw_en     <= {next_state == STOP} ? 1'b1 : 1'b0;
										 state         <= next_state;
										 en_end        <= {next_state == STOP} ? 1'b1 : en_end;
                                     end 									 
                                end
                             end
                         end
						 
			STOP :       begin
							 if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b0) && (rw == 1'b0)) begin
                                 sda_temp       <= 1'b0;
								 en_end         <= 1'b1;
                             end
							 
							 if((clk_i2c_reg2 == 1'b1) && (clk_i2c_reg1 == 1'b1) && (en_end == 1'b1)) begin
                                 sda_temp       <= 1'b0;
								 en_end         <= 1'b0;
								 scl_high       <= 1'b1;
                             end
							 
							 if (scl_high == 1'b1) begin
							     if(clk_i2c_cnt == stop_setup) begin
                                     scl_high        <= 1'b0;
									 sda_temp        <= 1'b1;
									 state           <=RELEASE;
									 sda_rw_en       <= 1'b1;								 
                                 end
                             end
							 else begin
							     sda_temp        <= 1'b0;
							 end
                         end
			
			RELEASE :    begin
							 if(clk_i2c_cnt == clk_div - 3) begin
							     scl_en         <= 1'b0;
								 state          <= IDLE;
                                 sda_temp       <= 1'b1;
								 sda_rw_en      <= 1'b1;
								 busy           <= 1'b0;
                             end
                         end

            default :    state    <= IDLE;
        
        endcase

往期 精彩****回顾

基于7系列FPGA实现万兆网通信

FPGA实现千兆网UDP协议(含ARP、ICMP)

FPGA 40G/50G Ethernet Subsystem核的使用

基于FPGA开发应用SATA硬盘

基于FPGA实现NVMe硬盘的读写功能

FPGA实现SD卡内文件的读写功能(FAT32文件系统)

FPGA光通信系列4 --- 基于64b/66b编码的自定义协议

FPGA光通信系列3 --- 基于8b/10b编码的自定义协议应用

FPGA光通信系列2------Aurora 64B/66B的使用

FPGA实现Aurora光通信应用

JESD204B的使用系列------3、DAC的应用(AD9164 9.6GSPS)

JESD204B的使用系列------2、协议及ADC的应用(AD9689)

JESD 204B的使用系列---1、时钟芯片的应用

相关推荐
Saniffer_SH1 天前
【每日一题】一台可编程的PCIe 6.0主机 + 一套自动化CTS验证平台 + 一个轻量级链路分析系统
运维·服务器·测试工具·fpga开发·自动化·计算机外设·硬件架构
952361 天前
计算机组成原理 - 主存储器
单片机·嵌入式硬件·学习·fpga开发
简简单单做算法1 天前
【第2章>第1节】基于FPGA的图像放大和插值处理概述
计算机视觉·fpga开发·双线性插值·线性插值·图像放大·均值插值·最邻近插值
木心术11 天前
OpenClaw FPGA资源利用率优化深度指南
人工智能·fpga开发
发光的沙子1 天前
FPGA----zynq 7000与zynqMP内存区域保留方法
fpga开发
minglie11 天前
c和hdl对偶关系
fpga开发
verse_armour1 天前
【FPGA】在PYNQ开发板上搭建卷积神经网络实现交通标志识别
fpga开发
Aaron15882 天前
RFSOC+VU13P/VU9P+GPU通用一体化硬件平台
人工智能·算法·fpga开发·硬件架构·硬件工程·信息与通信·基带工程
XINVRY-FPGA2 天前
XC7VX485T-2FFG1157I Xilinx Virtex-7 FPGA
arm开发·嵌入式硬件·fpga开发·硬件工程·fpga
鄙人菜鸡2 天前
Xilinx IP Aurora 8B/10B 多级光纤串联复位时序
fpga开发