[FPGA系列] I2C项目实战总结
一、基本概念
I2C 通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线, 只需要两根线即可在连接于总线上的器件之间传送信息。
I2C 通讯设备之间的常用连接方式,如图所示。

它的物理层 有如下特点:
(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间访问。
(4) 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6) 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
(7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。
想要使用FPGA进行I2C通信,我们首先要搞清楚它的协议时序,如下图所示。

I2C协议整体时序大致可以分为4部分:
①空闲状态 :在此状态下串口时钟信号 SCL 和串行数据信号 SDA 均保持高电平,此时无 I2C 设备工作。
②起始状态 :SCL 依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有 I2C 设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。
③数据读/写状态 :在该状态,可以进行数据读写操作,包括写入从设备地址、数据寄存地址、写入数据、读取数据...,具体时序如图所示。

可以看到,在SCL为高电平时,SDA上的数据要保持不变,在SCL为低电平时,SDA上的数据可以发生变化,每次传送数据 8 bit为一组,每传送一组后等待响应位(一般为从设备提供),然后传送下一组,再等响应位,等待全部传送完成后进入停止状态。
④停止状态 :一次读/写完成后,进入停止状态,在SCL为高电平时,SDA产生一个上升沿,I2C总线转回空闲状态。
二、I2C读/写操作
前面了解了I2C协议的整体时序,下面详细讲解具体的读写操作。
首先讲解 I2C 写操作,由于一次写入数据量的不同, I2C 的写操作可分为单字节写操作和页写操作,在这里我们主要讲解单字节写操作。

整个阶段我们可以分为三部分:
①发送从设备地址(7bit)+写标志位(1bit)。
②发送数据寄存地址,如果数据寄存地址为单字节的话,就只有8bit,双字节的话为16bit,先发送高8位,再发送低8位。
③发送要写入的数据8bit,高位在前,低位在后。
I2C读操作可以分为随机读操作和顺序读操作,这里我们主要讲解随机读操作,它的操作时序与写时序很相似,如图所示。

整个阶段我们可以分为四部分(或者说是2+2):
①发送从设备地址(7bit)+写标志位(1bit)。
②发送数据寄存地址,如果数据寄存地址为单字节的话,就只有8bit,双字节的话为16bit,先发送高8位,再发送低8位。
③ 发送从设备地址(7bit)+读标志位(1bit)。
④读取数据8bit。
可以看到,读操作前两部分和写操作是完全相同的,需要注意 的有:
(1) 写操作有一个起始位和一个停止位,而读操作有两个起始位和一个停止位;
(2) 在读操作读取数据完成后,等待的响应位并不是从设备提供(从设备产生低电平),而是主设备主动拉高,然后进入停止状态。
三、思路整理
根据I2C协议的操作时序,我们可以使用状态机来很好地完成,如图所示。

除了必要的 i2c_scl 和 i2c_sda 端口,我们产生一个 i2c_clk 时钟信号,该信号时钟频率为i2c_scl 的4倍,这样我们就可以很方便的对 SCL 线和 SDA 线的时序进行控制。
编写 i2c_ctrl 模块来进行时序控制,如图所示。

其端口说明如图所示。

四、Verilog程序编写
module i2c_ctrl
#(
parameter DEVICE_ADDR = 7'b1010_000 , //i2c设备地址
parameter SYS_CLK_FREQ = 26'd50_000_000 , //输入系统时钟频率
parameter SCL_FREQ = 18'd250_000 //i2c设备scl时钟频率
)
(
input wire clk , //输入系统时钟50MHZ
input wire rst , //复位,低电平有效
input wire i2c_start , //输入i2c触发信号
input wire wr_en , //输入写使能信号
input wire rd_en , //输入读使能信号
input wire [15:0] byte_addr , //输入i2c字节地址
input wire [7:0] wr_data , //输入i2c设备数据
input wire addr_num , //输入i2c字节地址字节数,0-单字节 1-两字节
output reg i2c_scl , //i2c时钟线
inout wire i2c_sda , //i2c数据线
output reg i2c_clk , //i2c驱动时钟
output wire i2c_end , //i2c一次读/写操作完成
output reg [7:0] rd_data //i2c读取数据
);
localparam I2C_CLK_CNT = (SYS_CLK_FREQ/SCL_FREQ) >> 2'd3; //i2c_clk时钟频率为i2c_scl的4倍
localparam IDLE = 4'd00, //空闲状态
START_1 = 4'd01, //i2c启动,准备写
SEND_D_ADDR = 4'D02, //发送写设备地址
ACK_1 = 4'D03, //等待响应
SEND_B_ADDR_H = 4'D04, //发送字节地址高8位
ACK_2 = 4'D05, //等待响应
SEND_B_ADDR_L = 4'D06, //发送字节地址低8位
ACK_3 = 4'D07, //等待响应
WR_DATA = 4'D08, //发送设备数据
ACK_4 = 4'D09, //等待响应
START_2 = 4'D10, //i2c启动,准备读
SEND_RD_ADDR = 4'D11, //发送读设备地址
ACK_5 = 4'D12, //等待响应
RD_DATA = 4'D13, //读取设备数据
N_ACK = 4'D14, //主设备拉高响应
STOP = 4'D15; //i2c停止
reg [3:0] state,next_state ;
reg [7:0] i2c_clk_cnt ;
reg [1:0] i2c_clk_num ;
wire clk_num_flag ;
reg [3:0] bit_cnt ;
reg ack ;
reg i2c_sda_out ;
reg [7:0] data_out ;
//i2c_clk_cnt:i2c工作频率计数
always@(posedge clk or negedge rst)
if(~rst)
i2c_clk_cnt <= 8'b0;
else if(i2c_clk_cnt == I2C_CLK_CNT - 1'd1)
i2c_clk_cnt <= 8'b0;
else
i2c_clk_cnt <= i2c_clk_cnt + 1'b1;
//i2c_clk:i2c工作频率
always@(posedge clk or negedge rst)
if(~rst)
i2c_clk <= 1'b0;
else if(i2c_clk_cnt == I2C_CLK_CNT - 1'd1)
i2c_clk <= ~i2c_clk;
//clk_num_flag:i2c_clk_num周期计数使能标志
assign clk_num_flag = (state == IDLE)?1'b0:1'b1;
//i2c_clk_num:i2c_clk周期计数
always@(posedge i2c_clk or negedge rst)
if(~rst)
i2c_clk_num <= 2'b0;
else if(i2c_clk_num == 2'd3)
i2c_clk_num <= 2'b0;
else if(clk_num_flag == 1'b1)
i2c_clk_num <= i2c_clk_num + 1'b1;
else
i2c_clk_num <= 2'b0;
//bit_cnt:bit计数
always@(posedge i2c_clk or negedge rst)
if(~rst)
bit_cnt <= 4'b0;
else if(bit_cnt == 4'd15)
bit_cnt <= 4'b0;
else if(state == SEND_D_ADDR || state == SEND_B_ADDR_H ||
state == SEND_B_ADDR_L || state == WR_DATA ||
state == SEND_RD_ADDR || state == RD_DATA)
if(i2c_clk_num == 2'd3)
bit_cnt <= bit_cnt + 1'b1;
else
bit_cnt <= bit_cnt;
else
bit_cnt <= 4'b0;
//state:现态转移
always@(posedge i2c_clk or negedge rst)
if(~rst)
state <= IDLE;
else
state <= next_state;
//next_state:次态改变
always@(*)
case(state)
IDLE : next_state = (i2c_start == 1'b1)?START_1:IDLE; //空闲状态
START_1 : next_state = (i2c_clk_num == 2'd3)?SEND_D_ADDR:START_1; //i2c启动,准备写
SEND_D_ADDR : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_1:SEND_D_ADDR; //发送写设备地址
ACK_1 : if((i2c_clk_num == 'd3) && (ack == 'd0)) //等待响应
if(addr_num == 'd1)
next_state = SEND_B_ADDR_H;
else
next_state = SEND_B_ADDR_L;
else
next_state = ACK_1;
SEND_B_ADDR_H : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_2:SEND_B_ADDR_H; //发送字节地址高8位
ACK_2 : next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?SEND_B_ADDR_L:ACK_2; //等待响应
SEND_B_ADDR_L : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_3:SEND_B_ADDR_L; //发送字节地址低8位
ACK_3 : if((i2c_clk_num == 'd3) && (ack == 'd0)) //等待响应
if(wr_en == 'b1)
next_state = WR_DATA;
else if(rd_en == 'b1)
next_state = START_2;
else
next_state = ACK_3;
else
next_state = ACK_3;
WR_DATA : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_4:WR_DATA; //发送设备数据
ACK_4 : next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?STOP:ACK_4; //等待响应
START_2 : next_state = (i2c_clk_num == 2'd3)?SEND_RD_ADDR:START_2; //i2c启动,准备读
SEND_RD_ADDR : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?ACK_5:SEND_RD_ADDR; //发送读设备地址
ACK_5 : next_state = ((i2c_clk_num == 'd3)&&(ack == 'd0))?RD_DATA:ACK_5; //等待响应
RD_DATA : next_state = ((bit_cnt == 'd7)&&(i2c_clk_num == 'd3))?N_ACK:RD_DATA; //读取设备数据
N_ACK : next_state = (i2c_clk_num == 'd3)?STOP:N_ACK; //主设备拉高响应
STOP : next_state = (i2c_clk_num == 'd3)?IDLE:STOP; //i2c停止
default : next_state = IDLE;
endcase
//data_out:输出数据缓存
always@(*)
case(state)
SEND_D_ADDR : data_out = {DEVICE_ADDR|3'b011,1'b0};
SEND_B_ADDR_H : data_out = byte_addr[15:8];
SEND_B_ADDR_L : data_out = byte_addr[7:0];
WR_DATA : data_out = wr_data;
SEND_RD_ADDR : data_out = {DEVICE_ADDR|3'b011,1'b1};
default : data_out = 8'b1;
endcase
//i2c_sda_out:i2c输出
always@(*)
case(state)
IDLE : i2c_sda_out <= 1'b1; //空闲状态
START_1 : i2c_sda_out <= (i2c_clk_num <= 'd1)?1'b1:1'b0; //i2c启动,准备写
SEND_D_ADDR : i2c_sda_out <= data_out['d7-bit_cnt]; //发送写设备地址
SEND_B_ADDR_H : i2c_sda_out <= data_out['d7-bit_cnt]; //发送字节地址高8位
SEND_B_ADDR_L : i2c_sda_out <= data_out['d7-bit_cnt]; //发送字节地址低8位
WR_DATA : i2c_sda_out <= data_out['d7-bit_cnt]; //发送设备数据
START_2 : i2c_sda_out <= (i2c_clk_num <= 'd1)?1'b1:1'b0; //i2c启动,准备读
SEND_RD_ADDR : i2c_sda_out <= data_out['d7-bit_cnt]; //发送读设备地址
N_ACK : i2c_sda_out <= 1'b1; //主设备拉高响应
STOP : i2c_sda_out <= (i2c_clk_num >='d2)?1'b1:1'b0; //i2c停止
default : i2c_sda_out <= 1'b1;
endcase
//ack:响应信号
always@(posedge i2c_clk or negedge rst)
if(~rst)
ack <= 1'b1;
else if(state == ACK_1 || state == ACK_2 || state == ACK_3
|| state == ACK_4 || state == ACK_5)
ack <= i2c_sda;
else
ack <= 1'b1;
//i2c_sda
assign i2c_sda = (state == ACK_1 || state == ACK_2 || state == ACK_3 || state == ACK_4 ||
state == ACK_5 || state == RD_DATA)?1'bz:i2c_sda_out;
//i2c_scl
always@(posedge i2c_clk or negedge rst)
if(~rst)
i2c_scl <= 1'b1;
else if(state != IDLE && state != STOP)
if(i2c_clk_num == 'd0 || i2c_clk_num == 'd1)
i2c_scl <= 1'b1;
else
i2c_scl <= 1'b0;
else
i2c_scl <= 1'b1;
//rd_data:i2c读取数据输出
always@(posedge i2c_clk or negedge rst)
if(~rst)
rd_data <= 8'b0;
else if(i2c_clk_num == 'd1 && state == RD_DATA)
rd_data['d7-bit_cnt] <= i2c_sda;
else
rd_data <= rd_data;
//i2c_end:一次读/写完成
assign i2c_end = (state == STOP)?1'b1:1'b0;
endmodule
五、Testbench编写
`timescale 1ns/1ns
3. module tb_i2c_ctrl
();
reg clk ; //输入系统时钟50MHZ
reg rst ; //复位,低电平有效
reg i2c_start ; //输入i2c触发信号
reg wr_en ; //输入写使能信号
reg rd_en ; //输入读使能信号
reg [15:0] byte_addr ; //输入i2c字节地址
reg [7:0] wr_data ; //输入i2c设备数据
reg addr_num ; //输入i2c字节地址字节数,0-单字节 1-两字节
wire i2c_scl ; //i2c时钟线
tri i2c_sda ; //i2c数据线
wire i2c_clk ; //i2c驱动时钟
wire i2c_end ; //i2c一次读/写操作完成
wire [7:0] rd_data ; //i2c读取数据
reg i2c_sda_in ;
initial
begin
clk <= 1'b1;
rst <= 1'b0;
i2c_start <= 1'b0;
wr_en <= 1'b0;
rd_en <= 1'b0;
byte_addr <= 16'b0;
wr_data <= 8'b0;
addr_num <= 1'b0;
i2c_sda_in <= 1'bz;
#40;
rst<= 1'b1;
#40;
i2c_start <= 1'b1;
wr_en <= 1'b1;
byte_addr <= 16'b1000_0100_0010_0001;
wr_data <= 8'b1010_1010;
addr_num <= 1'b1;
#600;
i2c_start <= 1'b0;
#35860; //SEND_D_ADDR
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#32000; //SEND_B_ADDR_H
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#32000; //SEND_B_ADDR_L
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#32000; //WR_DATA
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz; //STOP
#32000;
#500;
i2c_start <= 1'b1;
wr_en <= 1'b0;
rd_en <= 1'b1;
#600;
i2c_start <= 1'b0;
#35900; //SEND_D_ADDR
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#32000; //SEND_B_ADDR_H
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#32000; //SEND_B_ADDR_L
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'bz;
#36000; //SEND_RD_ADDR
i2c_sda_in <= 1'b0;
#4000;
i2c_sda_in <= 1'b0;
#4000; //RD_DATA
i2c_sda_in <= 1'b1;
#4000; //RD_DATA
i2c_sda_in <= 1'b0;
#4000; //RD_DATA
i2c_sda_in <= 1'b1;
#4000; //RD_DATA
i2c_sda_in <= 1'b0;
#4000; //RD_DATA
i2c_sda_in <= 1'b1;
#4000; //RD_DATA
i2c_sda_in <= 1'b0;
#4000; //RD_DATA
i2c_sda_in <= 1'b1;
#4000; //RD_DATA
i2c_sda_in <= 1'bz;
#4000; //N_ACK
//STOP
end
always #10 clk = ~clk;
assign i2c_sda = i2c_sda_in;
i2c_ctrl
#(
7'b1010_000 , //i2c设备地址
26'd50_000_000 , //输入系统时钟频率
18'd250_000 //i2c设备scl时钟频率
)
i2c_ctrl_inst
(
clk , //输入系统时钟50MHZ
rst , //复位,低电平有效
i2c_start , //输入i2c触发信号
wr_en , //输入写使能信号
rd_en , //输入读使能信号
byte_addr , //输入i2c字节地址
wr_data , //输入i2c设备数据
addr_num , //输入i2c字节地址字节数,0-单字节 1-两字节
i2c_scl , //i2c时钟线
i2c_sda , //i2c数据线
i2c_clk , //i2c驱动时钟
i2c_end , //i2c一次读/写操作完成
rd_data //i2c读取数据
);
endmodule
六、Modelsim仿真
在仿真中,我们的仿真顺序是先写操作,后读操作。
仿真中,写操作的一些参数:
设备地址+写标志位 = 1010_0110
byte_addr = 0x8421 ---> 1000_0100_0010_0001
wr_data = 0xaa ---> 1010_1010
读操作的参数:
设备地址+读标志位 = 1010_0111
i2c_sda = 0101_0101 ---> 0x55
仿真结果如图所示。

放大来看,写操作如图所示。

读操作如图所示。

从仿真效果来看,与我们之前设想的一样,实验成功。
参考资料:《FPGA Verilog开发实战指南——基于Altera EP4CE10》
