基于FPGA的I2C协议------以EEPROM为例
一、I2C硬件层
1、I2C为双线总线接口,仅有SCL(时钟线)、SDA(数据线)两根线。
2、其中两根线均为开漏输出, 均无输出高电平的能力,需要外界上拉电阻来输出高电平,SCL、SDA在空闲状态为高阻态。
3、在一个I2C通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
4、传输速率标准模式下可以达到100kb/s,快速模式下可以达到400kb/s,高速模式下可达 3.4Mbit/s。
【图为I2C硬件原理图】
二、I2C协议简介
1、在时钟(SCL)为高电平的时候,数据总线(SDA)必须保持稳定,所以数据总线(SDA),在时钟(SCL)为低电平的时候才能改变。
2、当SCL为高电平时,SDA产生下降沿为起始状态。
3、当SCL为高电平时,SDA产生上升沿为停止状态,同时转为空闲状态。
4、当一个完整字节的指令或数据传输完成,从机设备正确接收到指令或数据后,会通过拉低SDA为低电平,向主机设备发送单比特的应答信号,表示数据或指令写入成功。若从机正确应答,可以结束或开始下一字节数据或指令的传输,否则表明数据或指令写入失败,主机就可以决定是否放弃写入或者重新发起写入。
5、每个I2C 器件都有一个器件地址,有的器件地址在出厂时地址就设置好了,用户不可以更改(例如 OV7670 器件地址为固定的 0x42),有的确定了几位,剩下几位由硬件确定(比如常见的I2C 接口的 EEPROM 存储器,留有 3 个控地址的引脚,由用户自己在硬件设计时确定)。(本帖以器件地址为A0H为例。)
6、IIC读操作
单字节写操作时序图 (单字节存储地址 )
单字节写操作时序图 (双字节存储地址 )
7、IIC写操作
关于时序的详细解释资料很多,这里就不过多解释
三、程序讲解
1.程序目标
本程序使用EEPROM单字节写和读实现连续写入六个数据并读出。
按下按键1进行写,按下按键2进行读。
RTL视图如下:
2.状态机图示
有点丑
3.代码讲解
为了提高可移植性,将I2C控制协议单独做了一个模块。
开发平台:Vivado 2018.3
仿真平台:Modusim SE-64 10.5
开发平台:ALINX AX7Z020
模块列表如下
module i2c_ctrl
#(
parameter SLAVE_ADDR = 7'b1010000 , //EEPROM从机地址
parameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率
parameter I2C_FREQ = 18'd250_000 //IIC_SCL的时钟频率ss
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire i2c_wr_en ,//写操作使能端口
input wire i2c_rd_en ,//读操作使能端口
input wire i2c_start , //产生一个高脉冲进行I2C开始
input wire [1:0] addr_num , //存储地址字节选择,1为单字节,2为双个字节
input wire [15:0] byte_addr ,//读或者写字节地址
input wire [7:0] i2c_data_w ,//预写入的数据
output reg [7:0] i2c_data_r ,//读出的数据
output reg i2c_done ,//读写操作完成信号
output reg i2c_clk ,//由于IIC写于运行速率过慢,故先分频为1MHz进行处理
output reg i2c_ack ,//若读写失败产生的非应答信号,将其拉高
output reg i2c_scl ,//I2C_SCL信号
inout wire i2c_sda //I2C_SDA信号
);
注:
由于SDA需要进行输入输出,且要求连接到总线上的输出端必须是开漏输出结构,给不了高电平,所以总线上所有的高电平应该是由上拉电阻上拉达到效果的,而不是由主机直接给总线赋值 1就能实现,所以我们在写这个逻辑的时候也应该遵循这个标准,当总线上要输出低电平的时候,我们就直接给总线赋值 0,要输出高电平的时候,只能将总线设置成高阻态,这样再由外部上拉电阻来上拉成高电平。
用三态门很好实现。
assign i2c_sda = (sda_en)?(sda_out?1'bZ:1'b0):1'bZ;
assign sda_in = i2c_sda;
//sda_en为1时为输入,0为输出状态
IIC控制代码较长只给出状态机第二段的写法(文末附全部代码链接)
其中用clk_bit进行时序计数器,建议结合仿真一起查看
always@(posedge i2c_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0) begin
i2c_scl <= 1'b1;
sda_out <= 1'b1;
sda_en <= 1'b1;
st_done <= 1'b0;
clk_bit <= 8'd0;
i2c_ack <= 1'd0;
i2c_done <= 1'b0;
addr_num_r <= 2'b0;
i2c_wr_en_r <= 1'd0;
i2c_rd_en_r <= 1'd0;
i2c_data_r <= 1'b0;
i2c_data_w_r <= 8'b0;
byte_addr_r <= 16'b0;
i2c_read_data_r <= 8'b0;
end
else begin
st_done <= 1'b0;
case(state)
IDLE: begin
sda_out <= 1'b1;
i2c_done <= 1'b0;
clk_bit <= 8'd0;
if(i2c_start == 1'b1) begin
addr_num_r <= addr_num;
i2c_wr_en_r <= i2c_wr_en;
i2c_rd_en_r <= i2c_rd_en;
i2c_data_w_r <= i2c_data_w;
byte_addr_r <= byte_addr;
end
end
START: begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
sda_out <= 1'b0;
st_done <= 1'b1;
end
1 :clk_bit <= 1'b0;
endcase
end
SEND_SLADDR_W: begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0,4,8,12,16,20,24,28,32 :i2c_scl <= 1'b0;
2,6,10,14,18,22,26,30,34 :i2c_scl <= 1'b1;
1 :sda_out <= SLAVE_ADDR[6];
5 :sda_out <= SLAVE_ADDR[5];
9 :sda_out <= SLAVE_ADDR[4];
13 :sda_out <= SLAVE_ADDR[3];
17 :sda_out <= SLAVE_ADDR[2];
21 :sda_out <= SLAVE_ADDR[1];
25 :sda_out <= SLAVE_ADDR[0];
29 :sda_out <= 0;
33 :sda_en <=1'b0;
35 :st_done <= 1'b1;
36 :begin
i2c_scl <= 1'b0;
if(sda_in == 1'b1) begin
i2c_ack <= 1'b1;
end
clk_bit <= 1'b0;
end
default : ;
endcase
end
SEND_ADDR16: begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
sda_en <=1'b1;
sda_out <= byte_addr_r[15];
end
3,7,11,15,19,23,27,31 : i2c_scl <= 1'b0;
1,5,9,13,17,21,25,29,33 : i2c_scl <= 1'b1;
4 :sda_out <= byte_addr_r[14];
8 :sda_out <= byte_addr_r[13];
12 :sda_out <= byte_addr_r[12];
16 :sda_out <= byte_addr_r[11];
20 :sda_out <= byte_addr_r[10];
24 :sda_out <= byte_addr_r[9];
28 :sda_out <= byte_addr_r[8];
32 :sda_en <=1'b0;
34 :st_done <= 1'b1;
35 :begin
i2c_scl <= 1'b0;
if(sda_in == 1'b1) begin
i2c_ack <= 1'b1;
end
clk_bit <= 1'b0;
end
default : ;
endcase
end
SEND_ADDR8:begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
sda_en <=1'b1;
sda_out <= byte_addr_r[7];
end
3,7,11,15,19,23,27,31 : i2c_scl <= 1'b0;
1,5,9,13,17,21,25,29,33 : i2c_scl <= 1'b1;
4 :sda_out <= byte_addr_r[6];
8 :sda_out <= byte_addr_r[5];
12 :sda_out <= byte_addr_r[4];
16 :sda_out <= byte_addr_r[3];
20 :sda_out <= byte_addr_r[2];
24 :sda_out <= byte_addr_r[1];
28 :sda_out <= byte_addr_r[0];
32 :sda_en <=1'b0;
34 :st_done <= 1'b1;
35 :begin
i2c_scl <= 1'b0;
if(sda_in == 1'b1) begin
i2c_ack <= 1'b1;
end
clk_bit <= 1'b0;
end
default : ;
endcase
end
SEND_DATA:begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
sda_en <=1'b1;
sda_out <= i2c_data_w_r[7];
end
3,7,11,15,19,23,27,31 : i2c_scl <= 1'b0;
1,5,9,13,17,21,25,29,33 : i2c_scl <= 1'b1;
4 :sda_out <= i2c_data_w_r[6];
8 :sda_out <= i2c_data_w_r[5];
12 :sda_out <= i2c_data_w_r[4];
16 :sda_out <= i2c_data_w_r[3];
20 :sda_out <= i2c_data_w_r[2];
24 :sda_out <= i2c_data_w_r[1];
28 :sda_out <= i2c_data_w_r[0];
32 :sda_en <=1'b0;
34 :st_done <= 1'b1;
35 :begin
i2c_scl <= 1'b0;
if(sda_in == 1'b1) begin
i2c_ack <= 1'b1;
end
clk_bit <= 1'b0;
end
default : ;
endcase
end
STOP:begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
sda_en <=1'b1;
sda_out <= 1'b0;
end
1 :i2c_scl <= 1'b1;
2 :sda_out <= 1'b1;
8 :st_done <= 1'b1;
9 :begin
clk_bit <= 1'b0;
i2c_done <= 1'b1;
end
default : ;
endcase
end
START_2: begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :sda_en <=1'b1;
1 :begin
sda_out <= 1'b1;
i2c_scl <= 1'b1;
st_done <= 1'b1;
end
2 :begin
sda_out <= 1'b0;
clk_bit <= 1'b0;
end
endcase
end
SEND_SLADDR_R:begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :begin
i2c_scl <= 1'b0;
sda_en <=1'b1;
end
4,8,12,16,20,24,28,32 :i2c_scl <= 1'b0;
2,6,10,14,18,22,26,30,34 :i2c_scl <= 1'b1;
1 :sda_out <= SLAVE_ADDR[6];
5 :sda_out <= SLAVE_ADDR[5];
9 :sda_out <= SLAVE_ADDR[4];
13 :sda_out <= SLAVE_ADDR[3];
17 :sda_out <= SLAVE_ADDR[2];
21 :sda_out <= SLAVE_ADDR[1];
25 :sda_out <= SLAVE_ADDR[0];
29 :sda_out <= 1;
33 :sda_en <=1'b0;
35 :st_done <= 1'b1;
36 :begin
i2c_scl <= 1'b0;
if(sda_in == 1'b1) begin
i2c_ack <= 1'b1;
end
clk_bit <= 1'b0;
end
default : ;
endcase
end
READ_DATA:begin
clk_bit <= clk_bit + 1'b1;
case(clk_bit)
0 :sda_en <=1'b0;
3,7,11,15,19,23,27,31 :i2c_scl <= 1'b0;
1,5,9,13,17,21,25,29,33 :i2c_scl <= 1'b1;
2 :i2c_read_data_r[7]<= sda_in;
6 :i2c_read_data_r[6]<= sda_in;
10 :i2c_read_data_r[5]<= sda_in;
14 :i2c_read_data_r[4]<= sda_in;
18 :i2c_read_data_r[3]<= sda_in;
22 :i2c_read_data_r[2]<= sda_in;
26 :i2c_read_data_r[1]<= sda_in;
30 :i2c_read_data_r[0]<= sda_in;
32 :sda_en <= 1'b1;
33 :sda_out <= 1'b1;
34 :st_done <= 1'b1;
35 :begin
i2c_scl <= 1'b0;
clk_bit <= 1'b0;
i2c_data_r <= i2c_read_data_r;
end
default : ;
endcase
end
endcase
end
end
仿真如下
EEPROM_AT24C64 为原子哥提供的仿真模型
module tb_i2c_ctrl();
reg sys_clk;
reg sys_rst_n;
reg i2c_wr_en ;
reg i2c_rd_en ;
reg i2c_start ;
reg [1:0] addr_num ;
reg [15:0] byte_addr ;
reg [7:0] i2c_data_w;
wire [7:0] i2c_data_r ;
wire i2c_done ;
wire i2c_clk ;
wire i2c_ack ;
wire i2c_scl ;
wire i2c_sda ;
pullup(i2c_sda);
//给输入信号初始值
initial
begin
sys_clk = 1'b0;
sys_rst_n = 1'b0; //复位
addr_num <= 2'd2;
i2c_wr_en <= 1'b1;
i2c_rd_en <= 1'b0;
byte_addr <= 16'hA5A5;
i2c_data_w <= 8'hAA;
#21;
sys_rst_n = 1'b1; //在第21ns的时候复位信号信号拉高
i2c_start <= 1'b1;
#1000;
i2c_start <= 1'b0;
#500000;
i2c_wr_en <= 1'b0;
i2c_rd_en <= 1'b1;
byte_addr <= 16'hA5A5;
#1000;
i2c_start <= 1'b1;
#1000;
i2c_start <= 1'b0;
#500000;
i2c_wr_en <= 1'b1;
i2c_rd_en <= 1'b0;
byte_addr <= 16'h5AA5;
i2c_data_w <= 8'h55;
i2c_start <= 1'b1;
#1000;
i2c_start <= 1'b0;
#500000;
i2c_wr_en <= 1'b0;
i2c_rd_en <= 1'b1;
byte_addr <= 16'h5AA5;
i2c_start <= 1'b1;
#1000;
i2c_start <= 1'b0;
#500000;
i2c_wr_en <= 1'b0;
i2c_rd_en <= 1'b1;
byte_addr <= 16'hA5A5;
i2c_start <= 1'b1;
#1000;
i2c_start <= 1'b0;
end
//50Mhz的时钟,周期则为1/50Mhz=20ns,所以每10ns,电平取反一次
always #10 sys_clk = ~sys_clk;
i2c_ctrl
#(
.SLAVE_ADDR(7'b1010000 ) , //EEPROM从机地址
.CLK_FREQ (26'd50_000_000 ) , //模块输入的时钟频率
.I2C_FREQ (18'd250_000 ) //IIC_SCL的时钟频率ss
)
i2c_ctrl_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.i2c_wr_en (i2c_wr_en ),
.i2c_rd_en (i2c_rd_en ),
.i2c_start (i2c_start ),
.addr_num (addr_num ),
.byte_addr (byte_addr ),
.i2c_data_w (i2c_data_w),
.i2c_data_r (i2c_data_r),
.i2c_done (i2c_done ),
.i2c_clk (i2c_clk ),
.i2c_ack (i2c_ack ),
.i2c_scl (i2c_scl ),
.i2c_sda (i2c_sda )
);
EEPROM_AT24C64 u_EEPROM_AT24C64(
.scl (i2c_scl ),
.sda (i2c_sda )
);
endmodule
顶层EEPROM的读写就很轻松了,这里仅给出仿真结果(文末附全部文件)
读写的数据一致,仿真通过。
黑金的EEPROM竟然接在了PS段,上不了板了,呜呜呜
总结
文件链接在这里,嘿咻
提取码:4ik6
IIC协议在FPGA中算是比较难实现的协议了.
孩子写了好久,呜呜呜。
文中I2C控制的代码参考了原子哥的程序(还得是原子哥)。
协议方面个人认为小梅哥的线下班讲的很棒。
在写代码之前一定要理清协议的原理,最好是画一下波形图,写代码的时候如果觉得还是有点乱可以边仿真变写代码,个人认为这是比较快的方法。>o<
此工程可以应用于实际项目,如果认为文章写的不错请点赞支持。
FPGA初学者,欢迎交流鸭>o<