跳至正文

I2C总线规范详解及其Verilog实现04——I2C主机Verilog功能模块

标签:

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主机的功能,具体功能如下:

  1. 仅支持7位设备地址(目前市面上还没有10位设备地址的I2C器件,未来可能也不会有)
  2. 支持8/16/17/18位数据地址
  3. 支持页读/页写
  4. 支持从机时钟拉伸
  5. 支持从机软复位
  6. 支持实时更改I2C总线时钟频率(100kHz、400kHz、1MHz等任意频率可实时更改)
  7. 兼容SCCB协议
  8. 不支持多主机,即不支持时钟同步和仲裁(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 模块复位, 低电平有效

注意:

  1. 三态接口需要在最顶层模块中与物理引脚进行连接,连接代码如下:

    //~ 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;

四. 编码思路

  1. 上层模块使用FIFO接口来完全控制I2C主机的读写。

  2. 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。

  3. clk_freq_div_scl_freq表示clk频率与i2c_scl频率的比值, 如100MHz/100kHz = 1000, 新的I2C频率会在下一次传输时生效。

  4. 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]保留。

  5. 当FIFO非空时,开始一次传输。

  6. 当一次传输到了最后,对于写操作,写数据完成(接收到从机ACK信号)后,I2C从机当前数据地址+1,同时去读取FIFO,

    如果FIFO为空,则STOP;

    如果FIFO不为空,判断FIFO中的设备地址和读写指示与前一次是否一致,数据地址与I2C从机当前数据地址是否一致 ,控制字是否支持页写,如果这三个条件同时满足,继续写数据;如果不满足上述条件,则STOP。

  7. 当一次传输到了最后,对于读操作,接收数据完成后,I2C从机当前数据地址+1,同时去读取FIFO,

    如果FIFO为空,则STOP;

    如果FIFO不为空,判断FIFO中的设备地址和读写指示是否与前一次传输一致,数据地址与I2C从机当前数据地址是否一致 ,控制字是否支持页读,如果这三个条件同时满足,则发送ACK,继续读数据;如果不满足上述条件,则STOP。

  8. 当scl或sda上的低电平持续达到30ms时,进行一次软复位,即发送16个scl时钟,此scl时钟频率固定为10kHz。

  9. 当模块的rstn复位信号有效时,将对I2C总线进行一次软复位。

  10. 本模块的scl低电平脉宽固定为17/32个scl周期,设计依据参考:I2C总线规范详解及其Verilog实现03——时序要求与编程思路——3.2.1 对SCL时钟的时序要求

  11. 本模块的状态定义设计与参考:

    /*
    ! 写入总结: 开始 -> 设备地址写 -> 写数据地址 -> 写数据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。在四.编程思路中已经说明了其数据格式,此图给出更直观的数据格式图片:

说明:

  1. 对于容量 < 1Mbit的设备,设备地址为7bit;对于容量 ≥ 1Mbit的设备,设备地址 < 7bit,低位为数据地址的高位;
  2. 对于容量 ≤ 2Kbit的设备,只需要8位数据地址,应将此8位地址写入bit[23:16]即数据地址的低8位,并设置控制字节[5] = 0,此时bit[31:24]表示的数据地址高8位会被忽略;
  3. 对于读操作,bit[15:8]待写入数据会被忽略;
  4. 控制字节每一位的含义参考四. 编码思路

六. 功能仿真

仿真工具: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_001'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_001'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与嵌入式知识,软件,工具等内容,欢迎大家关注。

0 0 投票数
文章评分
订阅评论
提醒
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x