I2C总线规范详解及其Verilog实现系列文章:
I2C总线规范详解及其Verilog实现01——电路特性与原理 – 徐晓康的博客(myhardware.top)
I2C总线规范详解及其Verilog实现02——通信协议 – 徐晓康的博客(myhardware.top)
I2C总线规范详解及其Verilog实现03——读写过程与时序要求 – 徐晓康的博客(myhardware.top)
I2C总线规范详解及其Verilog实现04——I2C主机Verilog功能模块 – 徐晓康的博客(myhardware.top)
前言
本文实现了几乎全功能的Verilog的I2C主机,可应用于同任意速率,任意地址宽度的I2C从机设备进行通信。
此主机模块还支持页读/页写、从机时钟拉伸、软复位并兼容SCCB协议,但模块未实现时钟同步于仲裁,故不支持多主机。
本文详细说明了模块编码思路和使用注意事项,并展示了功能仿真结果,最后分享了源码和工程。
一. 模块功能
本Verilog功能模块实现了I2C主机的功能,具体功能如下:
-
仅支持7位设备地址(目前市面上还没有10位设备地址的I2C器件,未来可能也不会有) -
支持8/16/17/18位数据地址 -
支持页读/页写 -
支持从机时钟拉伸 -
支持从机软复位 -
支持实时更改I2C总线时钟频率(100kHz、400kHz、1MHz等任意频率可实时更改) -
兼容SCCB协议 -
不支持多主机,即不支持时钟同步和仲裁(99%的情况I2C总线上只会有一个主机)
二. 模块框图
三. 信号接口
3.1 参数列表
参数名 | 说明 |
---|---|
CLK_FREQ_MHZ | 模块工作时钟频率,单位MHz |
FIFO_ADDR_WIDTH | FIFO地址位宽 |
3.2 接口信号列表
信号分组 | 信号名 | 方向 | 说明 |
---|---|---|---|
输入FIFO | i2c_fifo_din[55:0] | input | FIFO输入,输入数据结构见四. 编码思路 |
i2c_fifo_wr_en | input | FIFO写使能 | |
i2c_fifo_full | output | FIFO满指示 | |
I2C读写指示 | i2c_device_addr[6:0] | output | 当前正在操作的从机设备地址 |
i2c_data_addr[15:0] | output | 当前正在操作的从机数据地址 | |
i2c_wdata[7:0] | output | 写入的数据 | |
i2c_wr_data_success | output | 写数据成功,高电平有效 | |
i2c_rdata[7:0] | output | 读出的数据 | |
i2c_rdata_valid | output | 读数据有效指示,高电平有效 | |
I2C物理接口 | i2c_sda_i | input | sda三态接口——输入 |
i2c_sda_o | output | sda三态接口——输出 | |
i2c_sda_oen | output | sda三态接口——输出使能 | |
i2c_scl_i | input | scl三态接口——输入 | |
i2c_scl_o | output | scl三态接口——输出 | |
i2c_scl_oen | output | scl三态接口——输出使能 | |
时钟与复位 | clk | input | 模块工作时钟 |
rstn | input | 模块复位, 低电平有效 |
注意:
-
三态接口需要在最顶层模块中与物理引脚进行连接,连接代码如下:
//~ I2C物理引脚
inout i2c_sda;
inout i2c_scl;
wire i2c_sda_o;
wire i2c_sda_oen;
wire i2c_sda_i = i2c_sda;
assign i2c_sda = i2c_sda_oen ? i2c_sda_o : 1'bz;
wire i2c_scl_o;
wire i2c_scl_oen;
wire i2c_scl_i = scl;
assign i2c_scl = i2c_scl_oen ? i2c_scl_o : 1'b1;
四. 编码思路
-
上层模块使用FIFO接口来完全控制I2C主机的读写。
-
FIFO数据结构: 设备地址 + 读写指示 + 数据地址 + 写数据(读操作忽略此字段) + 控制字节 + fscl控制,也就是:
device_addr(7bit) + rd_or_wr_n(1bit) + data_addr(16bit) + wdata(8bit) + ctrl_byte( 8bit) + clk_freq_div_scl_freq(16bit),共56bit。
-
clk_freq_div_scl_freq表示clk频率与i2c_scl频率的比值, 如100MHz/100kHz = 1000, 新的I2C频率会在下一次传输时生效。
-
ctrl_byte[7:0],控制字节,每一位的含义如下:
[7]表示page_rd_en,页读使能,控制页读是否继续,从机无法知道页的边界,主机通过此控制位来控制页读的继续或停止;
[6]表示page_wr_en,页写使能,同页读控制位;
[5]表示this_slave_data_addr_bytes_minus_1,当前从机数据地址字节数减1,在I2C总线上,可能≤8位数据地址的器件与>8位数据地址的器件共存,此控制位1表示当前从机数据地址为16位或更高,为0表示8位或更低;
[4]表示this_slave_ack_is_necessary,从机是否需要应答,对于SCCB协议,从机不需要应答,通过此控制位,本模块就能兼容SCCB协议;
[3:0]保留。
-
当FIFO非空时,开始一次传输。
-
当一次传输到了最后,对于写操作,写数据完成(接收到从机ACK信号)后,I2C从机当前数据地址+1,同时去读取FIFO,
如果FIFO为空,则STOP;
如果FIFO不为空,判断FIFO中的设备地址和读写指示与前一次是否一致,数据地址与I2C从机当前数据地址是否一致 ,控制字是否支持页写,
如果这三个条件同时满足
,继续写数据;如果不满足上述条件,则STOP。 -
当一次传输到了最后,对于读操作,接收数据完成后,I2C从机当前数据地址+1,同时去读取FIFO,
如果FIFO为空,则STOP;
如果FIFO不为空,判断FIFO中的设备地址和读写指示是否与前一次传输一致,数据地址与I2C从机当前数据地址是否一致 ,控制字是否支持页读,
如果这三个条件同时满足
,则发送ACK,继续读数据;如果不满足上述条件,则STOP。 -
当scl或sda上的低电平持续达到30ms时,进行一次软复位,即发送16个scl时钟,此scl时钟频率固定为10kHz。
-
当模块的rstn复位信号有效时,将对I2C总线进行一次软复位。
-
本模块的scl低电平脉宽固定为17/32个scl周期,设计依据参考:
I2C总线规范详解及其Verilog实现03——时序要求与编程思路——3.2.1 对SCL时钟的时序要求
-
本模块的状态定义设计与参考:
/*
! 写入总结: 开始 -> 设备地址写 -> 写数据地址 -> 写数据1~n -> 停止
! 读出总结:
! 1)开始 -> 设备地址读 -> 读数据1和n -> 停止 (读当前地址)
! 2)开始 -> 设备地址写 -> 写数据地址 -> 开始 -> 设备地址读 -> 读数据1和n -> 停止 (读非当前地址)
*/
//~ 状态定义
localparam IDLE = 11'd1 << 0; // 空闲态, 'h001
localparam START = 11'd1 << 1; // 开始条件, 单次传输开始, 'h002
localparam DEVICE_ADDR_WR = 11'd1 << 2; // 从机设备地址+写, 包括等待从机ACK, 'h004
localparam WR_DATA_ADDR = 11'd1 << 3; // 数据地址, 包括等待从机ACK, 'h008
localparam WR_DATA = 11'd1 << 4; // 写数据, 包括等待从机ACK, 'h010
localparam DEVICE_ADDR_RD = 11'd1 << 5; // 从机设备地址+读, 包括等待从机ACK, 'h020
localparam RD_DATA = 11'd1 << 6; // 读数据, 'h040
localparam RD_DATA_ACK = 11'd1 << 7; // 读数据最后, 应答, 然后继续读, 'h080
localparam RD_DATA_NO_ACK = 11'd1 << 8; // 读数据最后, 不应答, 然后STOP, 'h100
localparam STOP = 11'd1 << 9; // 停止条件, 单次传输结束, 'h200
localparam SOFT_RESET = 11'd1 << 10; // i2c软复位, scl出16个10kHz时钟, 'h400
五. 使用说明
本模块使用FIFO作为数据输入接口,上层模块要读写I2C只需要往FIFO中写数据即可。
FIFO的数据位宽固定为40。在四.编程思路中已经说明了其数据格式,此图给出更直观的数据格式图片:
说明:
-
对于容量 < 1Mbit的设备,设备地址为7bit;对于容量 ≥ 1Mbit的设备,设备地址 < 7bit,低位为数据地址的高位; -
对于容量 ≤ 2Kbit的设备,只需要8位数据地址,应将此8位地址写入bit[23:16]即数据地址的低8位,并设置控制字节[5] = 0,此时bit[31:24]表示的数据地址高8位会被忽略; -
对于读操作,bit[15:8]待写入数据会被忽略; -
控制字节每一位的含义参考 四. 编码思路
。
六. 功能仿真
仿真工具:Vivado 2021.2自带仿真工具。
从Microchip官网下载EEPROM-AT24CM02的Verilog仿真文件——AT24CM02.v,文件有点小问题,我进行了几处修改,将其用作I2C仿真中的从机。
testbench部分代码如下:
//++ 实例化不同容量的EEPROM ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
<span class="hljs-meta-keyword" style="line-height: 26px;">define</span> AT24CM02</span><br><span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;">//
define AT24CM01
//define AT24C512C</span><br><span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;">//
define AT24C64C
//define AT24C02A</span><br><br><span class="hljs-meta" style="color: #999; font-weight: bold; line-height: 26px;">
ifdef AT24C02D
AT24C02D AT24C02D_inst(
.SDA (sda),
.SCL (scl),
.WP (0 )
);
logic [6:0] slave_device_addr = 7'b1010_100;
logic [7:0] ctrl_byte = 8'b1101_0000;
<span class="hljs-meta-keyword" style="line-height: 26px;">endif</span></span><br><br><span class="hljs-meta" style="color: #999; font-weight: bold; line-height: 26px;">
ifdef AT24C64D
AT24C64D AT24C64D_inst(
.SDA (sda),
.SCL (scl),
.WP (0 )
);
logic [6:0] slave_device_addr = 7'b1010_111;
logic [7:0] ctrl_byte = 8'b1111_0000;
<span class="hljs-meta-keyword" style="line-height: 26px;">endif</span></span><br><br><span class="hljs-meta" style="color: #999; font-weight: bold; line-height: 26px;">
ifdef AT24C512C
AT24C512C AT24C512C_inst(
.SDA (sda),
.SCL (scl),
.WP (0 )
);
logic [6:0] slave_device_addr = 7'b1010_011;
logic [7:0] ctrl_byte = 8'b1111_0000;
<span class="hljs-meta-keyword" style="line-height: 26px;">endif</span></span><br><br><span class="hljs-meta" style="color: #999; font-weight: bold; line-height: 26px;">
ifdef AT24CM01
AT24CM01 AT24CM01_inst(
.SDA (sda),
.SCL (scl),
.WP (0 )
);
logic [6:0] slave_device_addr = {6'b1010_00, 1'b1};
logic [7:0] ctrl_byte = 8'b1111_0000;
<span class="hljs-meta-keyword" style="line-height: 26px;">endif</span></span><br><br><span class="hljs-meta" style="color: #999; font-weight: bold; line-height: 26px;">
ifdef AT24CM02
AT24CM02 AT24CM02_inst(
.SDA (sda),
.SCL (scl),
.WP (0 )
);
logic [6:0] slave_device_addr = {6'b1010_00, 1'b0};
logic [7:0] ctrl_byte = 8'b1111_0000;
`endif
//-- 实例化不同容量的EEPROM ------------------------------------------------------------
需要仿真哪个容量的EEPROM就取消哪个`define行的注释,同时还要去AT24CM02.v文件中进行对应修改,这两个文件中的define必须一致,否则仿真会报错。AT24CM02.v中的相关代码如下图所示。
<span class="hljs-meta-keyword" style="line-height: 26px;">define</span> AT24CM02</span><br><span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;">//
define AT24CM01
//define AT24C512C</span><br><span class="hljs-comment" style="color: #998; font-style: italic; line-height: 26px;">//
define AT24C64C
//`define AT24C02A
连续两次写入,给地址16’h0555写入8’hAA,地址16’h0556写入8’hAB。状态010为WR_DATA,从图中可见此状态下写入了两个字节的数据。
连续两次读取,状态080表示主机不应答,此时从机继续输出数据;状态100表示主机应答,从机停止输出数据。可见读出的数据为8’haa和8’hab,和写入数据一致。
七. 上板验证
开发软件:Vivado 2021.2,测试板卡:正点原子-领航者V1-ZYNQ7020,板上的I2C从机为EEPROM-ATMEL-AT24C64(最高fscl为100kHz)与实时时钟_日历-NXP-PCF8563(最高fscl为400kHz)。
顶层文件部分代码如下:
//++ I2C读写 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
localparam [6:0] SLAVE_AT24C64_DEVICE_ADDR = 7'b1010_000; // 适用于EEPROM-ATMEL-AT24C64, 100k
localparam [7:0] AT24C64_CTRL_BYTE = 8'b1111_0000; // 16位地址
localparam [6:0] SLAVE_PCF8563_DEVICE_ADDR = 7'b1010_001; // 适用于实时时钟_日历-NXP-PCF8563, 400k
localparam [7:0] PCF8563_CTRL_BYTE = 8'b1101_0000; //8位地址
localparam RD = 1'b1;
localparam WR = 1'b0;
localparam [7:0] wr_data = 8'hAA;
//~ clk频率与i2c_scl频率的比值, 如100MHz/100kHz = 1000, 新的I2C频率会在下一次传输时生效
localparam [15:0] I2C_CLK_FREQ_DIV_SCL_FREQ_100K = CLK_FREQ_MHZ * 1000 / 100;
localparam [15:0] I2C_CLK_FREQ_DIV_SCL_FREQ_400K = CLK_FREQ_MHZ * 1000 / 400;
reg [3:0] din_cnt;
always @(posedge clk) begin
if (~rstn)
din_cnt <= 'd0;
else
din_cnt <= din_cnt + 1'b1;
end
assign i2c_fifo_wr_en = ~i2c_fifo_full;
always @(posedge clk) begin
if (~rstn)
i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0000, wr_data, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
case (din_cnt)
//~ AT24C64 先写后读
0 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0001, wr_data+8'd0, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
1 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, WR, 16'h0000, wr_data+8'd1, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
2 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, WR, 16'h0001, wr_data+8'd2, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
3 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, WR, 16'h0002, wr_data+8'd3, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
4 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, WR, 16'h0003, wr_data+8'd4, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
5 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0000, wr_data+8'd5, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
6 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0001, wr_data+8'd6, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
7 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0002, wr_data+8'd7, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
8 : i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0003, wr_data+8'd8, AT24C64_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
//~ PCF8563 读取日期和时间
9 : i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0000, wr_data+8'd0, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
10: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0001, wr_data+8'd1, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
11: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0002, wr_data+8'd2, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
12: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0003, wr_data+8'd3, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
13: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0004, wr_data+8'd4, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
14: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0005, wr_data+8'd5, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
15: i2c_fifo_din <= {SLAVE_PCF8563_DEVICE_ADDR, RD, 16'h0006, wr_data+8'd6, PCF8563_CTRL_BYTE
, I2C_CLK_FREQ_DIV_SCL_FREQ_400K};
default: i2c_fifo_din <= {SLAVE_AT24C64_DEVICE_ADDR, RD, 16'h0, wr_data, I2C_CLK_FREQ_DIV_SCL_FREQ_100K};
endcase
end
//-- I2C读写 ------------------------------------------------------------
连接FPGA仿真器,注意调整JTAG频率<模块工作频率的一半
,(当设置模块时钟为10MHz,而JTAG时钟为5MHz时,JTAG很容易断连,暂时不知道什么问题,但对本模块的上板验证影响不大),下载程序,使用Vivado ILA抓取波形。
AT24C64连续写四次的波形如下图所示。可见地址16’h0000 ~ 16’h0003写入了8’hAB ~ 8’hAE。
接着AT24C64连续读四次的波形如下图所示,可见读取数据与写入数据一致。
再然后,PCF8563连续读,地址16’h0000 ~ 16’h0005,读数值分别为16’h08,16’h00,16’hb5,16’h47,16’h01,16’h01。
对比此芯片的寄存器默认值,如下图所示,可见此读取是没问题的。
上板验证正常。
八. 源码与工程分享
源码在Gitee与Github开源,两平台同步。
Gitee:徐晓康/Verilog功能模块–I2C主机与从机
Github:zhengzhideakang/Verilog–I2C
因Git不擅管理非文本文件,故仿真与上板工程文件通过网盘分享。
verilog-function-module–I2C-master-slave Vivado 2021.2工程 20241111.7z
欢迎大家关注我的公众号:徐晓康的博客,回复以下四位数字获取。
0356
建议复制过去不会码错字!
如果本文对你有所帮助,欢迎点赞、转发、收藏、评论让更多人看到,赞赏支持就更好了。
如果对文章内容有疑问,请务必清楚描述问题,留言评论或私信告知我,我看到会回复。
徐晓康的博客持续分享高质量硬件、FPGA与嵌入式知识,软件,工具等内容,欢迎大家关注。