在I2C发起读/写过程之前,主设备会先发起I2C的启动(START), 从设备的地址匹配(从设备地址写),I2C的读/写控制以及从设备应答检测的等一系列过程。由于无论是I2C读还是I2C写,该部分的操作过程都是必须的,因此把该部分内容单独作为一个模块进行设计,便于代码的重用。时序如图1:

图1
1.I2C启动部分主设备程序设计
-
接口要求:
- 复位信号,系统时钟,系统时钟分频(由于I2C不同速度匹配)
- input rst,
- input sys_clk,
- input clk_en, clk_en_half,
- 从设备地址、启动信号、读/写标志(即该段程序之后的I2C 总线行为
- input [7:0] data_in, // {dev_addr[6:0], 1’b0/1’b1}
- input [1:0] op_cmd, // 2’b00 means START + dev_addr + R/W
- input op_cmd_en, // op_cmd enable , sys_clk single pulse
- I2C 总线的内部信号:由于inout信号一般只在端口使用,因此在FPGA的内部逻辑(内部模块)将会把inout(双向口)变换成input, output类型进行传递
- input sda_i,
- output sda_o,
- output sda_t,
- input scl_i,
- output scl_o,
- output scl_t,
- 握手信号:op_cmd_done 操作完成、从设备应答。当op_cmd_done产生,从设备应答后(dev_ack)同时送给上层模块。op_cmd_done 保持一个sys_clk 时钟宽度。
- output op_cmd_done, // operation finished
- output reg dev_ack//从设备是否有应答
- 复位信号,系统时钟,系统时钟分频(由于I2C不同速度匹配)
-
总体接口如下:
module i2c_addr(
input sys_clk,
input [1:0] op_cmd,
input op_cmd_en,
output reg op_cmd_done,
output reg dev_ack,
input [7:0] data_in,
input clk_en,
input clk_en_half,
output reg sda_t,
output sda_o,
input sda_i,
output reg scl_t,
output scl_o,
input scl_i,
input rst
);
-
主体程序设计
该部分程序实现的主体内容如下:初始化,在IDLE状态下检测启动请求(op_cmd = 2’b00),对从设备的地址与读写控制信号移位进行串行化,等待从设备应答。串行化期间对SCL,SDA的要求是,在SCL为高电平期间SDA保持不变(start除外),SDA的任何变化都在SCL为低电平的时段发生。程序的流程图如下:
图2
-
程序代码如下:
`timescale 1ns / 1ps
module i2c_addr(
input sys_clk,
input [1:0] op_cmd,
input op_cmd_en,
output reg op_cmd_done,
output reg dev_ack,
input [7:0] data_in,
input clk_en,
input clk_en_half,
output reg sda_t,
output sda_o,
input sda_i,
output reg scl_t,
output scl_o,
input scl_i,
input rst
);
/*
op_cmd[1:0] : 00 send start + address + wr/rd (data_in)
01 send data
10 receive data (data_out)
11 send stop flag
*/
assign scl_o = 1'b0;
assign sda_o = 1'b0;
reg [7:0] i2c_st;
reg [2:0] sft_count;
reg [7:0] i2c_wr_data; //将需要写的数据存入本地寄存器中
always@(posedge sys_clk or posedge rst)
if(rst)
begin
op_cmd_done <= 0;
dev_ack <= 0;
scl_t <= 1'b1;
sda_t <= 1'b1;
sft_count <= 7;
i2c_wr_data <= 0;
i2c_st <= 0;
end
else
case(i2c_st)
0:
begin
sft_count <= 7;
op_cmd_done <= 1'b0;
dev_ack <= 0;
scl_t <= scl_i;
sda_t <= sda_i;
i2c_wr_data <= data_in;
if({op_cmd_en,op_cmd[1:0]} == 3'b100)
i2c_st <= 1; // start flag
end
1: //start flag
begin
if(clk_en)
begin
scl_t <= 1'b1;
sda_t <= 1'b1;
i2c_st <= 2;
end
end
2:
begin
scl_t <= 1'b1;
if(clk_en)
begin
sda_t <= 1'b0;
i2c_st <= 3;
end
end
3:
begin
if(clk_en)
begin
scl_t <= 1'b0;
i2c_st <= 4;
end
end
4: //write
begin
if(clk_en)
begin
scl_t <= 1'b0;
i2c_st <= 5;
end
end
5:
begin
if(clk_en_half)
sda_t <= i2c_wr_data[sft_count];
if(clk_en)
begin
scl_t <= 1'b1;
sft_count <= sft_count - 1;
i2c_st <= 4;
if( sft_count == 0 )
i2c_st <= 6;
end
end
6:
begin
if(clk_en)
begin
scl_t <= 1'b0;
i2c_st <= 7;
end
end
7: //read ack/nack from device
begin
if( clk_en_half )
sda_t <= 1'b1;
if(clk_en)
begin
scl_t <= 1'b1;
dev_ack <= sda_i;
i2c_st <= 8;
end
end
8:
begin
if(clk_en)
begin
scl_t <= 1'b0;
op_cmd_done <= 1'b1;
i2c_st <= 9;
end
end
9:
begin
op_cmd_done <= 1'b0;
if(clk_en)
begin
scl_t <= 1'b1;
i2c_st <= 0;
end
end
default:i2c_st <= 0;
endcase
endmodulefsfsf
- iic_phy 模块代码
`timescale 1ns / 1ps
module iic_phy(
input sys_clk,
input [1:0] op_cmd,
input op_cmd_en,
output reg op_cmd_done,
output reg dev_ack,
input master_ack,
input [7:0] data_in,
output [7:0] data_out,
input clk_en,
input clk_en_half,
output reg sda_t,
output sda_o,
input sda_i,
output reg scl_t,
output scl_o,
input scl_i,
input rst
);
/*
op_cmd[1:0] : 00 send start + address + wr/rd (data_in)
01 send data
10 receive data (data_out)
11 send stop flag
*/
wire dev_ack_addr;
wire dev_ack_wr;
always @(*)
begin
case(op_cmd)
2'b00: dev_ack = dev_ack_addr;
2'b01: dev_ack = dev_ack_wr;
default: ;
endcase
end
wire op_cmd_done_addr;
wire op_cmd_done_wr;
wire op_cmd_done_read;
wire op_cmd_done_stop;
always @(*)
begin
case(op_cmd)
2'b00: op_cmd_done = op_cmd_done_addr;
2'b01: op_cmd_done = op_cmd_done_wr;
2'b10: op_cmd_done = op_cmd_done_read;
2'b11: op_cmd_done = op_cmd_done_stop;
endcase
end
wire sda_t_addr;
wire sda_t_wr;
wire sda_t_read;
wire sda_t_stop;
wire scl_t_addr;
wire scl_t_wr;
wire scl_t_read;
wire scl_t_stop;
always @(*)
begin
case(op_cmd)
2'b00: begin sda_t = sda_t_addr; scl_t = scl_t_addr; end
2'b01: begin sda_t = sda_t_wr; scl_t = scl_t_wr; end
2'b10: begin sda_t = sda_t_read; scl_t = scl_t_read; end
2'b11: begin sda_t = sda_t_stop; scl_t = scl_t_stop; end
endcase
end
i2c_addr i2c_addr_inst
(
.sys_clk (sys_clk),
.op_cmd (op_cmd),
.op_cmd_en (op_cmd_en),
.op_cmd_done (op_cmd_done_addr),
.dev_ack (dev_ack_addr),
.data_in (data_in),
.clk_en (clk_en),
.clk_en_half (clk_en_half),
.sda_t (sda_t_addr),
.sda_i (sda_i),
.scl_t (scl_t_addr),
.scl_i (scl_i),
.rst (rst)
);
assign sda_o = 1'b0;
assign scl_o = 1'b0;
endmodule
- 将设计的模块例化,并组装到 iic_engine的模块中,这样就可以使用SCl_en模块,充分利用以设计的成果,逐步组装成程序代码及接口如下:
`timescale 1ns / 1ps
module iic_engine #
(
parameter DIV_ADDR = 32'hFFFF_0001,
parameter DEV_ADDR = 7'b101_0000,
parameter CLK_F = 100_000_000,
parameter SPEED = 100_000
)
(
input sys_clk,
inout SDA,
inout SCL,
input [31:0] div_addr,
input [31:0] div_in,
input div_en,
input div_rd,
output [31:0] div_dout,
input [1:0] op_cmd,
input op_cmd_en,
output op_cmd_done,
output dev_ack,
input master_ack,
input [7:0] data_in,
output [7:0] data_out,
output i2c_error,
input rst
);
wire sda_i;
wire sda_o;
wire sda_t;
wire scl_o;
wire scl_t;
wire scl_i;
assign SDA = sda_t ? 1'bz: sda_o;
assign sda_i = SDA;
assign SCL = scl_t ? 1'bz: scl_o;
assign scl_i = SCL;
wire clk_en;
wire clk_en_half;
//=========================== i2c clock baudtick
scl_en #
(
.DIV_ADDR ( DIV_ADDR ),
.CLK_F ( CLK_F ),
.SPEED ( SPEED )
)
scl_en_inst
(
.sys_clk ( sys_clk ),
.div_addr ( div_addr ),
.div_in ( div_in ),
.div_en ( div_en ),
.clk_en ( clk_en ),
.div_rd ( div_rd ),
.div_dout ( div_dout ),
.clk_en_half ( clk_en_half ),
.rst ( rst )
);
//===============================iic phy
iic_phy iic_phy_inst(
.sys_clk (sys_clk),
.op_cmd ( op_cmd ),
.op_cmd_en ( op_cmd_en ),
.op_cmd_done ( op_cmd_done ),
.dev_ack ( dev_ack ),
.master_ack ( master_ack ),
.data_in ( data_in ),
.data_out ( data_out ),
.clk_en ( clk_en ),
.clk_en_half ( clk_en_half ),
.sda_t ( sda_t ),
.sda_o ( sda_o ),
.sda_i ( sda_i ),
.scl_t ( scl_t ),
.scl_o ( scl_o ),
.scl_i ( scl_i ),
.rst ( rst )
);
endmodule
-
仿真程序设计:
`timescale 1ns / 1ps
module tb_dev_addr();
parameter PERIOD = 10 ;
parameter FREQUANCY = 100000000; //100M
parameter SPEED = 100000; // 100K
parameter SPEED1 = 400000; //400K
parameter DEV_ADDR = 7'b101_0001;
reg inclk;
always #( PERIOD / 2 ) inclk = ~inclk; //133M
wire sys_clk = inclk;
reg rst; //
reg [ 31: 0 ] div_in; //for baudrate register
wire [ 31: 0 ] div_addr = 32'hFFFF_0001;
reg div_en = 0;
reg [1:0] op_cmd = 0;
reg op_cmd_en = 0;
wire op_cmd_done;
wire dev_ack;
reg master_ack = 0;
reg [7:0] data_in = 0;
wire [7:0] data_out;
reg i2c_error = 0;
initial
begin
rst = 1'b1;
inclk = 0;
#50 rst = 1'b0;
end
reg [3:0] st = 0;
reg [31:0] cnt = 0;
always@( posedge sys_clk or posedge rst )
if ( rst )
begin
i2c_error <= 0;
cnt <= 0;
st <= 0;
op_cmd_en <= 1'b0;
op_cmd <= 0;
end
else
begin
case ( st )
0:
begin
i2c_error <= 0;
op_cmd <= 2'b00;
op_cmd_en <= 0;
if(cnt == 5000)
begin
cnt <= 0;
st <= 1;
end
else cnt <= cnt + 1;
end
1:
begin
data_in <= {DEV_ADDR, 1'b0};
op_cmd <= 2'b00;
op_cmd_en <= 1;
st <= 2;
end
2:
begin
op_cmd_en <= 0;
if(op_cmd_done)
begin
if(dev_ack)
i2c_error <= 1;
st <= 3;
end
end
3:
begin
st <= 3;
end
default:
st <= 0;
endcase
end
wire SDA;
wire SCL;
iic_engine
#(
.CLK_F ( FREQUANCY ) , //default with 100Mhz
.SPEED ( SPEED ) //default speed 100k
)
iic_engine_inst
(
.sys_clk ( sys_clk ),
.div_addr ( div_addr ),
.div_in ( div_in ),
.div_en ( div_en ),
.op_cmd ( op_cmd ),
.op_cmd_en ( op_cmd_en ),
.op_cmd_done( op_cmd_done ),
.dev_ack ( dev_ack ),
.master_ack ( master_ack ),
.data_in ( data_in ),
.data_out ( data_out ),
.SDA ( SDA ),
.SCL ( SCL ),
.rst ( rst )
);
PULLUP SDA_inst ( .O(SDA) );
PULLUP SCL_inst ( .O(SCL) );
AT24C02D #
(.SLAVE_ADDRESS(DEV_ADDR))
AT24C02D_inst
(
.SDA (SDA),
.SCL (SCL),
.WP (0)
);
endmodule
仿真程序中使用at24c02D IP ,这个IP 是由厂家提供的,这里我们只是使用这个I2C EEPROM ip 核。 IP核具体信息,可以参考文章https://www.microchip.com/en-us/product/AT24C02D
图3
注意,厂家提供的verilog IP ,目前只能在vivado下或者是questaSim 下仿真。 在quartus下或者在modelsim,questaSim_starter下不能仿真。(有些仿真语法不支持)。
在仿真工程中,我们主要学习三态门如何在仿真程序中仿真。
-
仿真结果
仿真波形如图4
图4
从图4可以看出在写阶段串行了1010_001(地址)及0(写控制)。24c02 iic设备应答。在I2C PHY层 Verilog程序结构化设计中介绍过op_cmd 定义:op_cmd[1:0] = 2’b00;代表start + write + dev_ack。 op_cmd[1:0] 和op_cmd_en 同时由仿真程序发出,当iic_phy完成相关操作后,返回op_cmd_done 信号, 代表当前操作命令(op_cmd)完成。