本文主要介绍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
往期 精彩****回顾
FPGA 40G/50G Ethernet Subsystem核的使用
FPGA光通信系列4 --- 基于64b/66b编码的自定义协议
FPGA光通信系列3 --- 基于8b/10b编码的自定义协议应用
FPGA光通信系列2------Aurora 64B/66B的使用
JESD204B的使用系列------3、DAC的应用(AD9164 9.6GSPS)