I²C即Inter-Integrated Circuit(集成电路总线),它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。I²C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据。在主控与被控IC之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,具体可查器件手册)识别。I²C总线物理拓扑结构图如下所示:
图中的IIC_SCL是串行时钟线,IIC_SDA是串行数据线,由于I2C器件一般采用开漏结构与总线相连,所以IIC_SCL和IIC_SDA均需接上拉电阻,也正因此,当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低,即各器件的SDA及SCL都是“线与”关系。IIC总线支持多主和主从两种工作方式,通常工作在主从工作方式,我们的开发板就采用主从工作方式。在主从工作方式中,系统中只有一个主机,其它器件都是具有I2C总线的外围从机。在主从工作方式中,主机启动数据的发送(发出启动信号)并产生时钟信号,数据发送完成后,发出停止信号。I2C总线结构虽然简单,使用两线传输,然而要实现器件间的通信,需要通过控制SCL和SDA的时序,使其满足I2C的总线传输协议,方可实现器件间的数据传输。
一、起始和结束
在I2C器件开始通信(传输数据)之前,串行时钟线SCL和串行数据线SDA线由于上拉的原因处于高电平状态,此时I2C总线处于空闲状态。
如果主机(此处指FPGA)想开始传输数据,只需在SCL为高电平时将SDA线拉低,产生一个起始信号,从机检测到起始信号后,准备接收数据,当数据传输完成,主机只需产生一个停止信号,告诉从机数据传输结束,停止信号的产生是在SCL为高电平时,SDA从低电平跳变到高电平,从机检测到停止信号后,停止接收数据。起始信号之前为空闲状态,起始信号之后到停止信号之前的这一段为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输由双向数据线(SDA)完成。停止信号产生后,总线再次处于空闲状态。
起始:SCL为高时,SDA由高拉低。
停止:SCL为高时,SDA由低拉高。
二、数据传输和应答期
先看看数据是怎么通过两根线传过来的,除了起始和结束比较特殊,中间的数据传输所遵循的规律如下所示:
1.数据传输
SCL为低时,SDA运行变化。
SCL为高时,SDA数据锁存。
2.应答期,SDA总线是三态门
①在第8个时钟周期末,主机释放SDA以使从机应答
②在第9个时钟周期,从机将SDA拉低以应答
③若第9个时钟周期,SCL为高电平时,SDA未被检测到为低电平,视为非应答,表明此次数据传输失败。
④在第9个时钟周期末,从机释放SDA以使主机继续传输数据,如果主机发送停止信号,此次传输结束。
三、器件地址
每个I2C器件都有一个器件地址,有些I2C器件的器件地址是固定的,而有些I2C器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到I2C总线上,例如为了增加系统的EEPROM容量,可能需要多个EEPROM。器件可编程地址位的数量由它可使用的管脚决定,比如EEPROM器件一般会留下3个管脚用于可编程地址位,当硬件电路上分别将这3个管脚连接到GND或VCC时,就可以设置不同的可编程地址。但有些I2C器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟PCF8563的器件地址为固定的7’h51)。所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可。
进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0时表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。对于AT24C64来说,其传输器件地址格式如下图所示。
四、存储器地址(字地址)
一般而言,每个兼容I2C协议的器件,内部总会有可供读写的寄存器或存储器,对于我们本次实验用到的EEPROM存储器,内部就是一系列顺序编址的存储单元。所以,当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入内容。该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量(2^8=256)时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示。
1.单字节的字地址
2.双字节的字地址
五、IIC写
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为“0”即写命令,从机就处于接收数据的状态,此时,主机就开始写数据了。写数据分为单字节写(对于EEPROM而言,称为字节写)和连续写(对于EEPROM而言,称为页写)。
不管单字节写和连续写,都可概括为:start + 器件地址 + 写命令(0) + 字地址 + 数据 + stop
1.单字节写
单字节写:发送完一字节数据后发送结束信号。
2.连续写
连续写:发送完一字节数据后继续发送下一字节数据,最后发送的是结束信号。
六、IIC读
主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为“1”即读命令,主机就处于接收数据的状态,从机从该地址单元输出数据。读数据分为当前地址读、单字节读和连续读。
不管是单字节读还是连续读,都可以概括为:start + 器件地址 + 写命令(0) + 字地址 + start + 器件地址 + 读命令(1) + 接收从机的数据 + 主机非应答(1) + stop
1.单字节读
发送完器件地址和字地址后又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为“0”,也就是写命令,第二次发送器件地址时后面的读写控制位为“1”,也就是读。为什么会有这样奇怪的操作呢?这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次Dummy Write也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置,等从机应答后,就可以按当前地址读的方式读数据了。因此也可以理解为:没有发送数据的单次写操作 + 当前地址的读操作
单字节读:读取完一字节数据后,主机发送非应答信号。
2.连续读
连续读:读取完一字节数据后,主机发送应答信号,读取完最后一个字节数据后,主机发送非应答信号。
七、IIC控制器Verilog代码设计(修改自正点原子FPGA)
//**************************************************************************
// *** 名称 : iic.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2019-08-10
// *** 描述 : IIC控制器
//************************************************************************** module iic
//========================< 参数 >==========================================
#(
parameter DEVICE_ID = 'b1010000 , //器件ID
parameter CLK = 'd50_000_000 , //本模块的时钟频率
parameter SCL = 'd250_000 //输出的SCL时钟频率
)
//========================< 端口 >==========================================
(
input clk , //时钟
input rst_n , //复位,低电平有效
//IIC control ---------------------------------------
input iic_en , //IIC触发信号
input addr16_en , //16位地址使能
input addr8_en , //8位地址使能
input write_en , //IIC写使能
input read_en , //IIC读使能
input [:] iic_addr , //IIC器件内地址
input [ :] iic_data_wr , //IIC要写的数据
//IIC output ----------------------------------------
output reg [ :] iic_data_rd , //IIC读出的数据
output reg iic_done , //IIC一次操作完成
output reg iic_scl , //IIC的SCL时钟信号
inout iic_sda , //IIC的SDA数据信号
//dri_clk -------------------------------------------
output reg iic_dri_clk //驱动IIC操作的驱动时钟,1Mhz
);
//========================< 参数 >==========================================
localparam IDLE = 'b0000_0001 ; //空闲状态
localparam DEVICE = 'b0000_0010 ; //写器件地址
localparam ADDR_16 = 'b0000_0100 ; //写字地址高8位
localparam ADDR_8 = 'b0000_1000 ; //写字地址低8位
localparam DATA_WR = 'b0001_0000 ; //写数据
localparam DEVICE_RD = 'b0010_0000 ; //虚写器件地址
localparam DATA_RD = 'b0100_0000 ; //读数据
localparam STOP = 'b1000_0000 ; //结束
//========================< 信号 >==========================================
reg sda_dir ; //IIC数据(SDA)方向控制
reg sda_out ; //SDA输出信号
wire sda_in ; //SDA输入信号
reg state_done ; //状态结束
reg [ :] cnt ; //计数
reg [ :] state_c ; //状态机当前状态
reg [ :] state_n ; //状态机下一状态
reg [:] iic_addr_t ; //地址
reg [ :] iic_data_rd_t ; //读取的数据
reg [ :] iic_data_wr_t ; //IIC需写的数据的临时寄存
reg [ :] clk_cnt ; //分频时钟计数
wire [ :] clk_divide ; //模块驱动时钟的分频系数 //==========================================================================
//== sda控制
//==========================================================================
assign iic_sda = sda_dir ? sda_out : 'bz; //SDA数据输出或高阻
assign sda_in = iic_sda ; //SDA数据输入 //==========================================================================
//== 生成SCL的4倍时钟来驱动后面IIC的操作,生成1Mhz的iic_dri_clk
//==========================================================================
assign clk_divide = (CLK/SCL) >> ; // >>3即除以8 always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
iic_dri_clk <= 'b1;
clk_cnt <= 'd0;
end
else if(clk_cnt == clk_divide - 'd1) begin
clk_cnt <= 'd0;
iic_dri_clk <= ~iic_dri_clk;
end
else
clk_cnt <= clk_cnt + 'b1;
end //==========================================================================
//== 状态机
//==========================================================================
always @(posedge iic_dri_clk or negedge rst_n) begin
if(!rst_n)
state_c <= IDLE;
else
state_c <= state_n;
end always @(*) begin
case(state_c)
IDLE: begin //空闲状态
if(iic_en) begin
state_n = DEVICE;
end
else
state_n = IDLE;
end
DEVICE: begin //写器件ID
if(state_done) begin
if(addr16_en)
state_n = ADDR_16;
else if(addr8_en)
state_n = ADDR_8 ;
end
else
state_n = DEVICE;
end
ADDR_16: begin //写地址高8位
if(state_done)
state_n = ADDR_8;
else
state_n = ADDR_16;
end
ADDR_8: begin //写地址低8位
if(state_done) begin
if(write_en)
state_n = DATA_WR;
else if(read_en)
state_n = DEVICE_RD;
end
else
state_n = ADDR_8;
end
DATA_WR: begin //写数据
if(state_done)
state_n = STOP;
else
state_n = DATA_WR;
end
DEVICE_RD: begin //虚写器件ID
if(state_done)
state_n = DATA_RD;
else
state_n = DEVICE_RD;
end
DATA_RD: begin //读数据
if(state_done)
state_n = STOP;
else
state_n = DATA_RD;
end
STOP: begin //结束
if(state_done)
state_n = IDLE;
else
state_n = STOP ;
end
default:state_n= IDLE;
endcase
end //==========================================================================
//== 设计各路信号
//==========================================================================
always @(posedge iic_dri_clk or negedge rst_n) begin
if(!rst_n) begin
iic_scl <= 'b1;
sda_out <= 'b1;
sda_dir <= 'b1;
iic_done <= 'b0;
cnt <= 'b0;
state_done <= 'b0;
iic_addr_t <= 'b0;
iic_data_rd <= 'b0;
iic_data_rd_t <= 'b0;
iic_data_wr_t <= 'b0;
end
else begin
state_done <= 'b0 ;
cnt <= cnt +'b1 ;
case(state_c)
//--------------------------------------------------- 空闲状态
IDLE: begin
iic_scl <= 'b1;
sda_out <= 'b1;
sda_dir <= 'b1;
iic_done <= 'b0;
cnt <= 'b0;
if(iic_en) begin
iic_addr_t <= iic_addr;
iic_data_wr_t <= iic_data_wr;
end
end
//--------------------------------------------------- 写器件ID
DEVICE: begin
case(cnt)
'd1 : sda_out <= 1'b0;
'd3 : iic_scl <= 1'b0;
'd4 : sda_out <= DEVICE_ID[6];
'd5 : iic_scl <= 1'b1;
'd7 : iic_scl <= 1'b0;
'd8 : sda_out <= DEVICE_ID[5];
'd9 : iic_scl <= 1'b1;
'd11: iic_scl <= 1'b0;
'd12: sda_out <= DEVICE_ID[4];
'd13: iic_scl <= 1'b1;
'd15: iic_scl <= 1'b0;
'd16: sda_out <= DEVICE_ID[3];
'd17: iic_scl <= 1'b1;
'd19: iic_scl <= 1'b0;
'd20: sda_out <= DEVICE_ID[2];
'd21: iic_scl <= 1'b1;
'd23: iic_scl <= 1'b0;
'd24: sda_out <= DEVICE_ID[1];
'd25: iic_scl <= 1'b1;
'd27: iic_scl <= 1'b0;
'd28: sda_out <= DEVICE_ID[0];
'd29: iic_scl <= 1'b1;
'd31: iic_scl <= 1'b0;
'd32: sda_out <= 1'b0; //0:写
'd33: iic_scl <= 1'b1;
'd35: iic_scl <= 1'b0;
'd36: begin
sda_dir <= 'b0; //从机应答
sda_out <= 'b1;
end
'd37: iic_scl <= 1'b1;
'd38: state_done <= 1'b1; //状态结束
'd39: begin
iic_scl <= 'b0;
cnt <= 'b0;
end
default : ;
endcase
end
//--------------------------------------------------- 写字地址高8位
ADDR_16: begin
case(cnt)
'd0 : begin
sda_dir <= 'b1 ;
sda_out <= iic_addr_t[];
end
'd1 : iic_scl <= 1'b1;
'd3 : iic_scl <= 1'b0;
'd4 : sda_out <= iic_addr_t[14];
'd5 : iic_scl <= 1'b1;
'd7 : iic_scl <= 1'b0;
'd8 : sda_out <= iic_addr_t[13];
'd9 : iic_scl <= 1'b1;
'd11: iic_scl <= 1'b0;
'd12: sda_out <= iic_addr_t[12];
'd13: iic_scl <= 1'b1;
'd15: iic_scl <= 1'b0;
'd16: sda_out <= iic_addr_t[11];
'd17: iic_scl <= 1'b1;
'd19: iic_scl <= 1'b0;
'd20: sda_out <= iic_addr_t[10];
'd21: iic_scl <= 1'b1;
'd23: iic_scl <= 1'b0;
'd24: sda_out <= iic_addr_t[9];
'd25: iic_scl <= 1'b1;
'd27: iic_scl <= 1'b0;
'd28: sda_out <= iic_addr_t[8];
'd29: iic_scl <= 1'b1;
'd31: iic_scl <= 1'b0;
'd32: begin
sda_dir <= 'b0; //从机应答
sda_out <= 'b1;
end
'd33: iic_scl <= 1'b1;
'd34: state_done <= 1'b1; //状态结束
'd35: begin
iic_scl <= 'b0;
cnt <= 'b0;
end
default : ;
endcase
end
//--------------------------------------------------- 写字地址低8位
ADDR_8: begin
case(cnt)
'd0: begin
sda_dir <= 'b1 ;
sda_out <= iic_addr_t[];
end
'd1 : iic_scl <= 1'b1;
'd3 : iic_scl <= 1'b0;
'd4 : sda_out <= iic_addr_t[6];
'd5 : iic_scl <= 1'b1;
'd7 : iic_scl <= 1'b0;
'd8 : sda_out <= iic_addr_t[5];
'd9 : iic_scl <= 1'b1;
'd11: iic_scl <= 1'b0;
'd12: sda_out <= iic_addr_t[4];
'd13: iic_scl <= 1'b1;
'd15: iic_scl <= 1'b0;
'd16: sda_out <= iic_addr_t[3];
'd17: iic_scl <= 1'b1;
'd19: iic_scl <= 1'b0;
'd20: sda_out <= iic_addr_t[2];
'd21: iic_scl <= 1'b1;
'd23: iic_scl <= 1'b0;
'd24: sda_out <= iic_addr_t[1];
'd25: iic_scl <= 1'b1;
'd27: iic_scl <= 1'b0;
'd28: sda_out <= iic_addr_t[0];
'd29: iic_scl <= 1'b1;
'd31: iic_scl <= 1'b0;
'd32: begin
sda_dir <= 'b0; //从机应答
sda_out <= 'b1;
end
'd33: iic_scl <= 1'b1;
'd34: state_done <= 1'b1; //状态结束
'd35: begin
iic_scl <= 'b0;
cnt <= 'b0;
end
default : ;
endcase
end
//--------------------------------------------------- 写数据
DATA_WR: begin
case(cnt)
'd0: begin
sda_out <= iic_data_wr_t[];
sda_dir <= 'b1;
end
'd1 : iic_scl <= 1'b1;
'd3 : iic_scl <= 1'b0;
'd4 : sda_out <= iic_data_wr_t[6];
'd5 : iic_scl <= 1'b1;
'd7 : iic_scl <= 1'b0;
'd8 : sda_out <= iic_data_wr_t[5];
'd9 : iic_scl <= 1'b1;
'd11: iic_scl <= 1'b0;
'd12: sda_out <= iic_data_wr_t[4];
'd13: iic_scl <= 1'b1;
'd15: iic_scl <= 1'b0;
'd16: sda_out <= iic_data_wr_t[3];
'd17: iic_scl <= 1'b1;
'd19: iic_scl <= 1'b0;
'd20: sda_out <= iic_data_wr_t[2];
'd21: iic_scl <= 1'b1;
'd23: iic_scl <= 1'b0;
'd24: sda_out <= iic_data_wr_t[1];
'd25: iic_scl <= 1'b1;
'd27: iic_scl <= 1'b0;
'd28: sda_out <= iic_data_wr_t[0];
'd29: iic_scl <= 1'b1;
'd31: iic_scl <= 1'b0;
'd32: begin
sda_dir <= 'b0; //从机应答
sda_out <= 'b1;
end
'd33: iic_scl <= 1'b1;
'd34: state_done <= 1'b1; //状态结束
'd35: begin
iic_scl <= 'b0;
cnt <= 'b0;
end
default : ;
endcase
end
//--------------------------------------------------- 虚写器件ID
DEVICE_RD: begin
case(cnt)
'd0 : begin
sda_dir <= 'b1;
sda_out <= 'b1;
end
'd1 : iic_scl <= 1'b1;
'd2 : sda_out <= 1'b0; //重新开始
'd3 : iic_scl <= 1'b0;
'd4 : sda_out <= DEVICE_ID[6];
'd5 : iic_scl <= 1'b1;
'd7 : iic_scl <= 1'b0;
'd8 : sda_out <= DEVICE_ID[5];
'd9 : iic_scl <= 1'b1;
'd11: iic_scl <= 1'b0;
'd12: sda_out <= DEVICE_ID[4];
'd13: iic_scl <= 1'b1;
'd15: iic_scl <= 1'b0;
'd16: sda_out <= DEVICE_ID[3];
'd17: iic_scl <= 1'b1;
'd19: iic_scl <= 1'b0;
'd20: sda_out <= DEVICE_ID[2];
'd21: iic_scl <= 1'b1;
'd23: iic_scl <= 1'b0;
'd24: sda_out <= DEVICE_ID[1];
'd25: iic_scl <= 1'b1;
'd27: iic_scl <= 1'b0;
'd28: sda_out <= DEVICE_ID[0];
'd29: iic_scl <= 1'b1;
'd31: iic_scl <= 1'b0;
'd32: sda_out <= 1'b1; //1:读
'd33: iic_scl <= 1'b1;
'd35: iic_scl <= 1'b0;
'd36: begin
sda_dir <= 'b0; //从机应答
sda_out <= 'b1;
end
'd37: iic_scl <= 1'b1;
'd38: state_done <= 1'b1; //状态结束
'd39: begin
iic_scl <= 'b0;
cnt <= 'b0;
end
default : ;
endcase
end
//--------------------------------------------------- 读数据
DATA_RD: begin
case(cnt)
'd0 : sda_dir <= 1'b0;
'd1 : begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd3 : iic_scl <= 1'b0;
'd5 : begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd7 : iic_scl <= 1'b0;
'd9 : begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd11: iic_scl <= 1'b0;
'd13: begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd15: iic_scl <= 1'b0;
'd17: begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd19: iic_scl <= 1'b0;
'd21: begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd23: iic_scl <= 1'b0;
'd25: begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1;
end
'd27: iic_scl <= 1'b0;
'd29: begin
iic_data_rd_t[] <= sda_in;
iic_scl <= 'b1 ;
end
'd31: iic_scl <= 1'b0;
'd32: begin
sda_dir <= 'b1; //非应答
sda_out <= 'b1;
end
'd33: iic_scl <= 1'b1;
'd34: state_done <= 1'b1; //状态结束
'd35: begin
iic_scl <= 'b0;
cnt <= 'b0;
iic_data_rd <= iic_data_rd_t;
end
default : ;
endcase
end
//--------------------------------------------------- 结束
STOP: begin
case(cnt)
'd0 : begin
sda_dir <= 'b1;
sda_out <= 'b0;
end
'd1 : iic_scl <= 1'b1;
'd3 : sda_out <= 1'b1;
'd15: state_done <= 1'b1; //状态结束
'd16: begin
cnt <= 'b0;
iic_done <= 'b1; //IIC配置完成
end
default : ;
endcase
end
endcase
end
end endmodule
参考资料:
[1]正点原子FPGA教程
[2]小梅哥FPGA教程