本文学习的主要是使用FPGA作为主机实现I2C协议,用来和EEPROM芯片AT24C64通信。
简介
I2C 即 Inter-Integrated Circuit(集成电路总线),是由 Philips 半导体公司(现在的 NXP 半导体公司)在 八十年代初设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短 的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。
AT24C64 存储容量为 64Kbit,内部分成 256 页,每页 32 字节,共有 8192 个字节,且其读写操作都是以字节为基本单位。可 以把 AT24C64 看作一本书,那么这本书有 256 页,每页有 32 行,每行有 8 个字,总共有 256*32*8=65536 个字,对应着 AT24C64 的 64*1024=65536 个 bit。
AT24C64 具有高可靠性,可对所存数据保存 100 年,并可多次擦写,擦写次数达一百万次。
I2C时序
数据传输过程中SDA必须在SCL高电平的时候保持高电平,在SCL上升沿前拉高SDA,SCL下降沿后拉低SDA,时序图如下所示:

除了数据信号,I2C还有开始信号和结束信号,SDA和SCL空闲状态处于高电平,开始信号需要SDA在SCL高电平期间拉低,结束信号需要SDA在SCL高电平期间拉高,时序图如下所示:

还有一个比较重要的信号是ACK响应信号,他在写入8bit信号之后拉低一个SCL时钟,也就是在第9bit的时候从机需要拉低SDA,时序图如下所示:

AT24C64读写时序
字节写的逻辑
开始位;
写入设备地址和读写控制位;
从机响应;
写入数据地址高字节
从机响应;
写入数据地址低字节;
从机响应;
写入一字节数据;
从机响应;
停止位;
时序图如下所示:

页写的逻辑
开始位;
写入设备地址和读写控制位;
从机响应;
写入数据地址高字节
从机响应;
写入数据地址低字节;
从机响应;
写入第1个字节数据;
从机响应;
写入第2个字节数据;
从机响应;
写入第n个字节数据;
从机响应;
停止位;
时序图如下所示:

随机读逻辑
开始位;
写入设备地址和读写控制位;
从机响应;
写入数据地址高字节;
从机响应;
写入数据地址低字节;
开始位;
写入设备地址和读写控制位;
从机响应;
读取1个字节数据;
从机响应;
停止位;
时序图如下所示:

顺序读逻辑
开始位;
写入设备地址和读写控制位;
从机响应;
写入数据地址高字节;
从机响应;
写入数据地址低字节;
开始位;
写入设备地址和读写控制位;
从机响应;
读取第1个字节数据;
从机响应;
读取第2个字节数据;
从机响应;
读取第n个字节数据;
从机响应;
停止位;
时序图如下所示:


源码
驱动文件i2c_driver.v,对EEPROM进行读写,只使用了字节写和随机读。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2025/04/02 15:02:54
// Design Name:
// Module Name: i2c_driver
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module i2c_driver(
input i_clk ,
input i_rst ,
input i_start ,
input i_dir ,//0:write, 1:read
input [6:0] i_device_addr ,
input [15:0] i_data_addr ,
input [7:0] i_data ,
output o_req ,
output o_data_valid ,
output [7:0] o_data ,
output o_i2c_ack ,
output o_i2c_done ,
// input sda_in ,
// output reg sda_out ,
// output reg [3:0] bit_cnt,
output i2c_scl ,
inout i2c_sda
);
parameter DIV_CNT = 4;
reg i2c_dri_clk =1'b0;
reg [3:0] i2c_dri_cnt =4'd0;
reg i2c_dri_rst ;
always@(posedge i_clk)begin
i2c_dri_rst <= i_rst;
if(i2c_dri_cnt>=(DIV_CNT-1))begin
i2c_dri_clk <= ~i2c_dri_clk;
i2c_dri_cnt <= 4'd0;
end
else begin
i2c_dri_cnt <= i2c_dri_cnt + 1'b1;
end
end
reg scl ;
reg sda_dir ;
wire sda_in ;
reg sda_out ;
reg [7:0]rec_data;
assign sda_in = (sda_dir==1'b1)?i2c_sda:1'b1;
assign i2c_sda = (sda_dir==1'b0)?sda_out:1'bz;
assign i2c_scl = scl;
reg [3:0]state;
reg [7:0]device_addr;
localparam IDLE = 4'd0;
localparam START = 4'd1;
localparam WRITE_DEVICE_ADDR = 4'd2;
localparam WRITE_DATA_HIGH_ADDR = 4'd3;
localparam WRITE_DATA_LOW_ADDR = 4'd4;
localparam WRITE_DATA = 4'd5;
localparam RESTART = 4'd6;
localparam READ_DEVICE_ADDR = 4'd7;
localparam READ_DATA = 4'd8;
localparam STOP = 4'd14;
localparam DONE = 4'd15;
reg [3:0]clk_cnt;
reg [3:0]bit_cnt;
reg start_d0,start_d1;
wire start_pdge;
always@(posedge i2c_dri_clk)begin
start_d0 <= i_start;
start_d1 <= start_d0;
end
assign start_pdge = (!start_d1)&start_d0;
reg ack_sig;
always@(*)begin
case(state)
IDLE :device_addr = {i_device_addr, i_dir};
START :device_addr = {i_device_addr, 1'b0};
WRITE_DEVICE_ADDR :device_addr = {i_device_addr, 1'b0};
READ_DEVICE_ADDR :device_addr = {i_device_addr, 1'b1};
DONE :device_addr = {i_device_addr, i_dir};
default :device_addr = {i_device_addr, i_dir};
endcase
end
always @(posedge i2c_dri_clk) begin
if(i2c_dri_rst)begin
state <= 4'd0;
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b0;
ack_sig <= 1'b0;
end
else begin
case(state)
IDLE :begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b0;
ack_sig <= 1'b0;
if(start_pdge)begin
state <= START;
end
else begin
state <= IDLE;
end
end
START :begin
sda_out <= 1'b0;
sda_dir <= 1'b0;
if(sda_out == 1'b0)begin
scl <= 1'b0;
state <= WRITE_DEVICE_ADDR;
end
else begin
state <= START;
end
end
WRITE_DEVICE_ADDR :begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd0)&&(bit_cnt < 4'd8))
sda_out <= device_addr[7-bit_cnt];
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd8))begin
sda_out <= 1'b0;
end
if((bit_cnt == 4'd8)&&(sda_in == 1'b0))begin
ack_sig <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
ack_sig <= 1'b0;
end
if((bit_cnt == 4'd8)&&(clk_cnt == 4'd0))begin
sda_dir <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= WRITE_DATA_HIGH_ADDR;
end
else begin
state <= IDLE;
end
end
else begin
state <= WRITE_DEVICE_ADDR;
end
end
WRITE_DATA_HIGH_ADDR:begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd0)&&(bit_cnt < 4'd8))
sda_out <= i_data_addr[15-bit_cnt];
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd8))begin
sda_out <= 1'b0;
end
if((bit_cnt == 4'd8)&&(sda_in == 1'b0))begin
ack_sig <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
ack_sig <= 1'b0;
end
if((bit_cnt == 4'd8)&&(clk_cnt == 4'd0))begin
sda_dir <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= WRITE_DATA_LOW_ADDR;
end
else begin
state <= IDLE;
end
end
else begin
state <= WRITE_DATA_HIGH_ADDR;
end
end
WRITE_DATA_LOW_ADDR :begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd0)&&(bit_cnt < 4'd8))
sda_out <= i_data_addr[7-bit_cnt];
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd8))begin
sda_out <= 1'b0;
end
if((bit_cnt == 4'd8)&&(sda_in == 1'b0))begin
ack_sig <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
ack_sig <= 1'b0;
end
if((bit_cnt == 4'd8)&&(clk_cnt == 4'd0))begin
sda_dir <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= (i_dir==1'b1)?RESTART:WRITE_DATA;
end
else begin
state <= IDLE;
end
end
else begin
state <= WRITE_DATA_LOW_ADDR;
end
end
WRITE_DATA :begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd0)&&(bit_cnt < 4'd8))
sda_out <= i_data[7-bit_cnt];
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd8))begin
sda_out <= 1'b0;
end
if((bit_cnt == 4'd8)&&(sda_in == 1'b0))begin
ack_sig <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
ack_sig <= 1'b0;
end
if((bit_cnt == 4'd8)&&(clk_cnt == 4'd0))begin
sda_dir <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= STOP;
end
else begin
state <= IDLE;
end
end
else begin
state <= WRITE_DATA;
end
end
RESTART :begin
sda_dir <= 1'b0;
ack_sig <= 1'b0;
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if(clk_cnt == 4'd0)
sda_out <= 1'b1;
else if(clk_cnt == 4'd2)
sda_out <= 1'b0;
if(clk_cnt == 4'd3)begin
state <= READ_DEVICE_ADDR;
end
else begin
state <= RESTART;
end
end
READ_DEVICE_ADDR :begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd0)&&(bit_cnt < 4'd8))
sda_out <= device_addr[7-bit_cnt];
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd8))begin
sda_out <= 1'b0;
end
if((bit_cnt == 4'd8)&&(sda_in == 1'b0))begin
ack_sig <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
ack_sig <= 1'b0;
end
if((bit_cnt == 4'd8)&&(clk_cnt == 4'd0))begin
sda_dir <= 1'b1;
end
else if((clk_cnt == 4'd0)&&(bit_cnt == 4'd0))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= READ_DATA;
sda_dir <= 1'b1;
end
else begin
state <= IDLE;
end
end
else begin
state <= READ_DEVICE_ADDR;
end
end
READ_DATA :begin
if(clk_cnt == 4'd1)
scl <= 1'b1;
else if(clk_cnt == 4'd3)
scl <= 1'b0;
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
sda_dir <= 1'b0;
end
if((clk_cnt == 4'd3)&&(bit_cnt <= 4'd7))begin
rec_data[7-bit_cnt] <= sda_in;
end
if((clk_cnt == 4'd3)&&(bit_cnt == 4'd8))begin
if(ack_sig == 1'b1)begin
state <= STOP;
end
else begin
state <= IDLE;
end
end
else begin
state <= READ_DATA;
end
end
STOP :begin
sda_dir <= 1'b0;
ack_sig <= 1'b0;
// if(clk_cnt == 4'd0)begin
// end
if(clk_cnt == 4'd1)begin
scl <= 1'b1;
end
if(clk_cnt == 4'd2)begin
sda_out <= 1'b1;
end
if(clk_cnt == 4'd3)begin
state <= DONE;
end
else begin
state <= STOP;
end
end
DONE :begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b0;
ack_sig <= 1'b0;
state <= IDLE;
end
endcase
end
end
always@(posedge i2c_dri_clk)begin
if(i2c_dri_rst)begin
clk_cnt <= 4'd0;
bit_cnt <= 4'd0;
end
else begin
case(state)
IDLE:begin
clk_cnt <= 4'd0;
bit_cnt <= 4'd0;
end
START:begin
clk_cnt <= 4'd0;
bit_cnt <= 4'd0;
end
WRITE_DEVICE_ADDR:begin
if(clk_cnt>= 4'd3)begin
clk_cnt <= 4'd0;
if(bit_cnt>= 4'd8)
bit_cnt <= 4'd0;
else
bit_cnt <= bit_cnt + 1'b1;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
WRITE_DATA_HIGH_ADDR, WRITE_DATA_LOW_ADDR, WRITE_DATA, READ_DEVICE_ADDR, READ_DATA:begin
if(clk_cnt>= 4'd3)begin
clk_cnt <= 4'd0;
if(bit_cnt>= 4'd8)
bit_cnt <= 4'd0;
else
bit_cnt <= bit_cnt + 1'b1;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
RESTART :begin
if(clk_cnt>= 4'd3)begin
clk_cnt <= 4'd0;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
STOP :begin
if(clk_cnt>= 4'd3)begin
clk_cnt <= 4'd0;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
end
end
DONE :begin
clk_cnt <= 4'd0;
bit_cnt <= 4'd0;
end
endcase
end
end
ila_0 u_ila_0 (
.clk(i_clk), // input wire clk
.probe0({
state,
sda_in ,
scl,
bit_cnt,
clk_cnt,
rec_data
}) // input wire [127:0] probe0
);
endmodule
仿真文件i2c_dri_tb.v,对驱动进行仿真。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2025/04/07 17:24:44
// Design Name:
// Module Name: i2c_dri_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module i2c_dri_tb(
);
reg clk ;
reg rst ;
reg start ;
reg dir ;//0:write, 1:read
reg [6:0] device_addr ;
reg [15:0] data_addr ;
reg [7:0] data ;
wire o_req ;
wire o_data_valid ;
wire [7:0] o_data ;
wire o_i2c_ack ;
wire o_i2c_done ;
wire i2c_scl ;
wire i2c_sda ;
wire [3:0] bit_cnt;
wire sda_in;
wire sda_out;
initial clk = 0;
always#5 clk = ~clk;
initial begin
rst = 1;
start = 0;
#1000;
rst = 0;
#1000;
start = 1;
#100;
start = 0;
end
assign sda_in = (bit_cnt==4'd8)?1'b0:1'b1;
i2c_driver u_i2c_driver(
.i_clk (clk),
.i_rst (rst),
.i_start (start),
.i_dir (1),//0:write, 1:read
.i_device_addr (7'h13),
.i_data_addr (16'h0506),
.i_data (8'hC6),
.o_req (o_req),
.o_data_valid (o_data_valid),
.o_data (o_data),
.o_i2c_ack (o_i2c_ack),
.o_i2c_done (o_i2c_done),
.sda_in (sda_in),
.sda_out (sda_out),
.bit_cnt (bit_cnt),
.i2c_scl (),
.i2c_sda ()
);
endmodule
顶层文件I2C_TOP.v,用于下板测试,手动控制读写,需要添加xdc文件。
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2025/04/08 21:40:50
// Design Name:
// Module Name: I2C_TOP
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module I2C_TOP(
input sys_clk,
input sys_rst_n,
output iic_scl,
inout iic_sda
);
wire clk;
wire start;
wire rw_ctrl;
wire [15:0]data_addr;
wire [7:0]data;
clk_wiz u_clk_wiz(
// Clock out ports
.clk_out1 (clk ), // output clk_out1
// Status and control signals
.resetn (sys_rst_n ), // input resetn
.locked (locked ), // output locked
// Clock in ports
.clk_in1 (sys_clk ) // input clk_in1
);
vio_0 u_vio_0 (
.clk (sys_clk ), // input wire clk
.probe_out0 (start ), // output wire [0 : 0] probe_out0
.probe_out1 (rw_ctrl),
.probe_out2 (data_addr),
.probe_out3 (data)
);
ila_0 u1_ila_0 (
.clk(sys_clk), // input wire clk
.probe0({
sys_rst_n,
locked,
clk
}) // input wire [127:0] probe0
);
i2c_driver u_i2c_driver(
.i_clk (clk),
.i_rst (!locked),
.i_start (start),
.i_dir (rw_ctrl),//0:write, 1:read
.i_device_addr (7'b1010000),
.i_data_addr (data_addr),
.i_data (data),
.o_req (o_req),
.o_data_valid (o_data_valid),
.o_data (o_data),
.o_i2c_ack (o_i2c_ack),
.o_i2c_done (o_i2c_done),
// .sda_in (),
// .sda_out (),
// .bit_cnt (bit_cnt),
.i2c_scl (iic_scl),
.i2c_sda (iic_sda)
);
endmodule
下板测试
使用vio写入03之后读该地址的数据,读取成功。