• FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(3)


    FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(1)
    FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(2)
    FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(3)

    工程目的

    以 FPGA 为主机,板载的 E2PROM 为从机,FPGA 通过 IIC 协议对板载的 E2PROM 进行读写控制,所以在模块划分时我们需要一个 IIC 驱动模块和一个 E2PROM 读写模块,两个模块分别命名为 i2c_dri 与 e2Prom_rw;对于读写测试的结果是使用LED 显示结果表示的,既读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁,
    在这里插入图片描述
    E2PROM 读写测试系统框图

    IIC时序图

    在这里插入图片描述
    单次写(字节写)时序

    在这里插入图片描述
    连续写(页写)时序

    在这里插入图片描述
    随机地址读时序
    在这里插入图片描述
    当前地址连续读时序

    在这里插入图片描述
    随机地址连续读时序

    IIC 读写操作方法汇总

    在这里插入图片描述

    正点原子IIC

    实验工程整体框图和模块功能简介,如表下图所示:

    在这里插入图片描述

    在这里插入图片描述

    IIC 驱动模块设计

    首先介绍 IIC 驱动模块的设计,I2C 驱动模块的主要功能是按照 I2C 协议对 E2PROM 存储芯片执行数据读写操作。I2C 驱动模块框图和输入输出端口简介
    在这里插入图片描述
    由图表可知,I2C 驱动模块包括 13 路输入输出信号,其中 7 路输入信号、5 路输出信号,还有一路 sda既可以做输出,也可以做输入。

    ●clk、rst_n 是从顶层例化到 I2C 驱动模块的系统时钟和复位信号;
    ● i2c_exec 是 I2C 触发执行信号,由 e2Prom 读写模块生成并传入,高电平有效;
    ● i2c_rh_wl 是 I2C 读写控制信号,i2c_rh_wl 为 1 表示进行读操作,i2c_rh_wl 为 0 表示进行写操作;
    ● 与 i2c_exec 信号同时传入的还有字地址 i2c_addr[15:0]和待写入字节数据 i2c_data_w[7:0];
    ● 当 I2C 触发执行信号有效,并且 i2c_rh_wl信号为 0,模块执行单次写操作,按照 I2C 器件字地址 i2c_addr,向 E2PROM 对应地址写入数据i2c_data_w;
    ● 当 i2c_rh_wl 为 1,模块执行随机数据读操作,按照 I2C 器件字地址 i2c_addr 读取 E2PROM 对应地址中的数据;
    ● 前文中我们提到,I2C 设备字地址有单字节和双字节两种,为了应对这一情况,我们向模块输入 bit_ctrl 信号,bit_ctrl 信号为字地址位控制信号,是顶层模块定义的参数通过例化传入的 I2C 驱动模块,bit_ctrl 为 1 时表示是双字节字地址,在进行数据读写操作时要写入数据字地址 i2c_addr 的全部 16位,bit_ctrl 为 0 时表示是单节字地址,在进行数据读写操作时只写入数据字地址 i2c_addr 的低 8 位。

    时钟规划

    dri_clk 是本模块的工作时钟,由系统时钟 sys_clk 分频而来,它的时钟频率为串行时钟scl 频率的 4 倍。 I2C 起始信号是在 scl 为高电平时拉低 sda 信号产生的,I2C 停止信号是在 scl 为高电平时,sda 从低电平跳变到高电平产生的,使用 dri_clk 检测该起始信号与结束信号的波形如下图所示:
    在这里插入图片描述
    时钟信号 dri_clk 要传入 e2Prom 读写模块(e2Prom_rw)作为模块的工作时钟;输出给 e2Prom 读写模块(e2Prom_rw)的 I2C 一次操作完成信号 i2c_done,高电平有效,表示 I2C 一次操作完成;
    i2c_data_r[7:0]信号表示自 E2PROM 读出的单字节数据,输出至 e2Prom 读写模块(e2Prom_rw);scl、sda分别是串行时钟信号和串行数据信号,由模块产生传入 E2PROM 存储芯片。

    状态跳转流程

    参照 I2C 设备单次写操作和随机读操作的操作流程,我们绘制 I2C 读/写操作状态转移图如下
    在这里插入图片描述

    单次写操作的波形图如下图所示:

    在这里插入图片描述

    随机读操作的波形图如下图所示:

    在这里插入图片描述
    I2C单次写操作的相关信号、驱动时钟的产生与单次写操作的状态跳转流程。

    ●第一部分:I2C单次写的输入信号说明
    I2C 触发执行信号 i2c_exec,只有该信号被触发,I2C 操作才会进行;I2C 操作被触发后,I2C 读写控制信号 i2c_rh_wl 为 0 时模块才会
    执行单次写操作;bit_ctrl 信号为字地址位控制信号,赋值为 0 时,表示 I2C 设备字地址为单字节,赋值为1 时,表示 I2C 设备字地址为双字节。

    ●第二部分:时钟信号计数器clk_cnt和输出信号i2c_clk的设计与实现
    本实验对E2PROM读写操作的串行时钟scl的频率为250KHz,且只在数据读写操作时时钟信号才有效,其他时刻scl始终保持高电平。若直接使用系统时钟生成串行时钟scl,计数器要设置较大的位宽,声明一个新的计数器clk_cnt对系统时钟sys_clk进行计数,利用计数器clk_cnt生成新的时钟dri_clk。串行时钟scl的时钟频率为250KHz,我们要生成的新时钟dri_clk的频率要是scl的4倍,之所以这样是为了后面更好的生成scl和sda,所以dri_clk的时钟频率为1MHz。经计算,clk_cnt要在0-24内循环计数,每个系统时钟周期自加1;clk_cnt每计完一个周期,dri_clk进行一次取反,最后得到dri_clk为频率1MHz的时钟,本模块中其他信号的生成都以此信号为同步时钟。信号波形图如下。
    在这里插入图片描述

    总结:50M时钟频率经过25计数器后电平信号翻转,得到1M频率。

    在 I2C 总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在 scl 串行时钟的配合下,在 sda 上逐位地串行传送每一位数据。数据位的传输是边沿触发,即在 scl 的上升沿采集数据,在 scl为高电平时数据保持,sda 可以在 scl 为低电平时进行改变,scl 低电平的中间是 i2c 驱动模块时钟(dri_clk)的上升沿,所以主机在写数据的时候在 scl 低电平的中间更新数据,读数据的时候可以在 scl 为高电平的时候寄存数据。

    I2C 数据传输的波形如下图所示:
    在这里插入图片描述

    总结:在1M频率内经过4个节拍,得到250k频率。

    ●第三部分:单次写状态机相关信号波形的设计与实现
    我们使用50MHz系统时钟生成了1MHz时钟i2c_clk,但输出至E2PROM的串行时钟scl的时钟频率为250KHz,为此我们定义了一个I2C驱动时钟i2c_clk的时钟个数计数器cnt,对时钟i2c_clk时钟信号进行计数。单次写操作的每个状态初始时,cnt的值为0,每计数一个周期i2c_clk时钟,cnt自加1,随着cnt计数,依次对串行时钟与串行数据赋值,既传输的指令、地址以及数据,位宽为固定的8位数据。并且申明一个该状态完成信号st_done,st_done高有效,作为状态机状态跳转的触发信号。

    I2C 发送写控制命令
    I2C 发送写控制命令

    状态机状态跳转的各约束条件均已介绍完毕,首先声明状态变量cur_state,结合各约束信号,单次写操作状态机跳转流程如下:
    (1)系统上电后,clk_cnt计数器开始计数,产生I2C驱动时钟dri_clk,并且状态机处于st_idle(空闲状态),接收到I2C触发执行信号i2c_exec后,状态机跳转到st_sladdr(发送写控制命令状态),同时cnt计数器开始计数dri_clk时钟个数;

    (2)在st_sladdr(发送写控制命令状态)状态,保持一个串行时钟周期,期间FPGA向E2PROM存储芯片发送起始信号,既在scl为高电平时拉低sda信号,开始I2C操作,既开始传输7位器件地址+写控制位,写控制命令传输完成会产生一个传输完成信号st_done,该传输完成信号高有效,并且判断接收到字地址控制位信号bit_ctrl,bit_ctrl为1(我们E2PROM器件地址为双字节),状态机跳转到传输写双字节高8位字地址状态(st_addr16);

    (3)在st_addr16状态双字节高8位字地址传输完成后,输出高有效的传输完成信号st_done后,状态机跳转到传输写双字节低8位字地址状态(st_addr8);

    (4)在st_addr8状态双字节低8位字地址传输完成后,输出高有效的传输完成信号st_done后,判断接收到的写标志信号wr_flag,wr_flag为0时,状态机跳转到传输写数据状态(st_data_wr);

    (5)在写数据状态(st_data_wr)传输8位写数据后输出高有效的传输完成信号st_done,此时状态机会跳转到I2C操作结束状态,输出一个I2C单次写操作完成信号i2c_done,i2c_done高有效后状态机再跳转回st_idle(空闲状态)。

    因为数据线 SDA 是双向的,如下图所示,为了避免主机、从机同时操作数据线,可以在 FPGA内部可以使用三态门结构避免此事件发生。sda_dir 表示 I2C 数据方向,为 1 时表示主机(FPGA)输出信号,为 0 时 FPGA 输出高阻态,表示释放控制权。
    在这里插入图片描述
    在 I2C 单次写操作,既每次 FPGA 输出数据时,在进行数据传输之前都需要先将 sda_dir 信号拉高,在数据传输完成后再将 sda_dir 信号拉低,将 SDA 总线的控制权交给从机发送响应数据。

    在空闲状态,接收到 I2C 触发执行信号后进行执行 I2C 操作,并且接收的读写控制信号 i2c_rh_wl 也为低电平时,状态机从空闲状态跳转到发送写命令状态(st_sladdr),并且将接收的 I2C 读写控制信号(i2c_rh_wl)赋值给写标志 wr_flag,将接收的 I2C 字地址寄存为 addr_t,将接收的 i2c 将写数据寄存为data_wr_t,I2C 应答信号 i2c_ack 一直处于应答状态。

    在 st_sladdr 状态,cnt 从 0 开始计数,scl 与 sda 保持默认高电平,cnt 计数为 1 时,scl 为保持高电平,此时将 sda 拉低,代表开始 I2C 操作,cnt 计数加 1,cnt 计数值为 2 时,以连续 4 个 cnt 计数 dri_clk 时钟为一个周期产生串行时钟 scl,用来传输串行数据 sda。在单次写的 st_sladdr 状态,传输的数据主要是器件地址与写控制位,即“10100000”;8bit 数据传输完成,拉高一个周期的数据该次操作完成信号 i2c_done,为下一个状态跳转的标志信号。接下来主机释放 SDA 以使从机应答,即 sda_dir 拉低,sda_out 拉高,接下来从机开始应答,因为我们设计从机一直处于应答状态,只有传输发生错误是从机回发出一个非应答信号,提示数据传输错误,数据重新传输;应答完成后开始切换状态机的状态,所以之后滞后一个周期,状态的状态由上一个状态切换到当前状态,计数器 cnt 清零,开始下一数据传输状态。

    在这里插入图片描述
    I2C 发送写控制命令

    由上面的状态机跳转图可知,写命令传输完成后,根据接收到的字地址控制命令 bit_ctrl 可知,我们下一个进入状态是传输双字节高 8 位字地址状态(st_addr16)。在传输双字节高 8 位字地址的状态(st_addr16),cnt 从 0 开始计数,进入状态后,第一步拉高 sda_dir 信号,切换 SDA 数据方向为 FPGA 输出,然后开始传输 8bit 字地址,因为第一个传入的字地址为“16“”b0000_0000_0000_0000”,所以 st_addr16状态发送的高 8 位字地址位“8’b0000_0000”,通过 sda_out 一个 bit 一个 bit 的传输出去。8bit 数据传输完成,拉高一个周期的数据该次操作完成信号 i2c_done,为下一个状态跳转的标志信号。接下来主机释放SDA 以使从机应答,即 sda_dir 拉低,sda_out 拉高,接下来从机开始应答,因为我们设计从机一直处于应答状态,只有传输发生错误是从机回发出一个非应答信号,提示数据传输错误,数据重新传输;应答完成后开始切换状态机的状态,所以之后滞后一个周期,状态的状态由上一个状态切换到当前状态,计数器cnt 清零,开始下一数据传输状。
    该状态的波形图如下图所示:

    在这里插入图片描述
    I2C 发送双字节高 8 位字地址

    由上面的 2 个状态机跳转图可知,在第 9 个 scl 时钟周期的上升沿,从机开始应答,将 sda 信号拉低,在其下降沿到来后,从机释放了总线,此时的 sda 由外部的上拉电路将其电平拉成高电平。双字节高 8 位字地址传输完成后,我们下一个进入状态是传输双字节低 8 位字地址状态(st_addr8)。传输双字节低 8 位字地址状态(st_addr8)操作与传输双字节高 8 位字地址的状态(st_addr16)操作基本一致,只是传输的数据内容是 addr_t 的低 8 位数据,即“8’b0000_0000”通过 sda_out 传输出去,该状态的波形图如下图所示:

    在这里插入图片描述
    I2C 发送低 8 位字地址

    接下来是进入传输写数据状态(st_data_wr),写数据状态(st_data_wr)操作与传输双字节高 8 位字地址的状态(st_addr16)操作也是基本一致,只是传输的数据内容是 data_wr_t 的低 8 位数据,即8’b0000_0000 通过 sda_out 传输出去。该状态数据传输的波形图如下所示:

    在这里插入图片描述
    I2C 写数据

    接下来是进入停止发送状态即 I2C 操作完成状态(st_stop),该状态数据传输的波形图如下所示:
    在这里插入图片描述
    I2C 写完成

    在 I2C 操作完成状态(st_stop),cnt 从 0 开始计数,首先拉高 sda_dir 信号切换 sda 数据方向位 FPGA主机输出,接下主机在 scl 为高电平时拉低 sda_out,结束本次 I2C 单次写操作,scl 与 sda 都被拉高,即将进入空闲状态,在 I2C 操作完成状态(st_stop)最后一个周期内输出一个单次写完成信号 i2c_done 并且给 cnt 计数器清零后彻底结束本次单次写操作,sda 总线恢复空闲状态。开始下一个字节的写入,直至 256个数据全部写入完成,拉高读写控制信号为读数据状态,重新触发 I2C,再通过随机读将 256 个数据从E2PROM 中读出。由上面的波形图可知,我们在主机发送停止信号后没有马上拉高单次写完成信号i2c_done,是因为 I2C 读写之间需要一点儿间隔时间,这个间隔时间由各个器件的类型决定。因为我们实验设计采用的是随机读,所以在发起读命令之前,我们需要先进行虚写。接下来将展示随机读的波形图如下图 6幅图所示,虚写命令发送的数据传输与波形图 ( I2C 发送写控制命令) 完全一致,只是此时的数据读写控制信号(i2c_rh_wl)在 I2C被触发时同时也被拉高了,I2C 操作从空闲状态跳转到虚写状态,同时写标志信号(wr_flag)也被拉高,开始随机读操作。数据传输过程与单次写命令一致,传输的数据也是 7 位器件地址与写控制位,即“8’b10100000”。
    在这里插入图片描述
    I2C 虚写波形图 1-写命令发送
    虚写 I2C 发送双字节高 8 位字地址的数据传输波形也是与单次写 I2C 发送双字节高 8 位字地址一致,区别是此时读写标志为高的读状态,第一次随机读,传输的双字节高 8 位字地址是“8’b0000_0000”。
    在这里插入图片描述
    I2C 虚写波形图 2- I2C 发送双字节高 8 位字地址
    虚写 I2C 发送双字节低 8 位字地址的数据传输波形与单次写 I2C 发送双字节低 8 位字地址一致,区别是此时读写标志为高的读状态,第一次随机读,传输的双字节低 8 位字地址是“8’b0000_0000”。
    在这里插入图片描述
    I2C 虚写波形图 3- I2C 发送双字节低 8 位字地址

    至此,I2C 的虚写操作已经全部完成,接下来状态机会跳转到发送读命令状态。

    在这里插入图片描述
    I2C 发送读控制命令

    I2C 发送读控制命令的数据传输与波形图 ( I2C 发送写控制命令)非常相似,只是此时的写标志信号(wr_flag)是被拉高的。I2C 发送读控制命令数据传输过程与 I2C 发送写命令一致,只是传输的数据内容有差异,I2C 发送读控制命令传输的数据是 7 位器件地址与读控制位,即“8’b10100001”。
    在这里插入图片描述
    I2C 读数据
    由上面 2 张图可知,在 iic 总线发送重新开始时序后的第 9 个 scl 的上升沿时钟周期,从机响应主机,拉低 sda 的电平,因为后面是读操作,所以在第 9 个 scl 的下降沿从机没有释放总线,故在第 9 个 scl 的下降沿 sda 还是低电平。进入 I2C 读数据时,此时的写标志信号(wr_flag)是被拉高的。cnt 计数器从 0 开始计数,sda 数据方向控制信号 sda_dir 信号为低电平(上个状态结尾切换了从机应答),此时 sda 与 sda_out 信号间为高阻状态,FPGA 开始读取 E2PROM 里面存储的数据,通过 sda_in 信号来获取 sda 信号线上的输入数据。第一次读的数据是“8’b0000_0000”,sda_in 将从 sda 读取的数据逐 bit 赋值给 data_r,然后将最终读取的值赋值给 i2c_data_r 输出模块。成功读取一个字节数据后,拉高一个周期本次读数据完成信号 st_done,下一步进入一次随机读操作的

    在这里插入图片描述
    I2C 读停止状态

    在 I2C 随机读操作完成状态(st_stop),cnt 从 0 开始计数,首先拉高 sda_dir 信号切换 sda 数据方向位FPGA 主机输出,接下来主机在 scl 为高电平时拉低 sda_out,结束本次 I2C 单次写操作,scl 与 sda 都被拉高,即将进入空闲状态,在 I2C 操作完成状态(st_stop)最后一个周期内输出一个单次写完成信号i2c_done 并且给 cnt 计数器清零后彻底结束本次随机读操作,sda 总线恢复空闲状态。开始下一个字节的读出,直至 256 个数据全部读完,本次 E2PROM 读写操作完成。

    I2C 驱动控制模块Verilog代码

    本模块代码主要分为驱动时钟产生模块、I2C 读写模块(使用三段式状态机完成)以及 sda 数据方向控制模块等

    I2C 驱动模块我们命名为 i2c_dri

    odule i2c_dri
    (
    arameter SLAVE_ADDR = 7'b1010000 , //E2PROM 从机地址
    arameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率
    arameter I2C_FREQ = 18'd250_000 		//IIC_SCL 的时钟频率
    
    
    nput clk , //系统时钟
    nput rst_n , //系统复位
    
    //i2c interface
    input i2c_exec , //I2C 触发执行信号
    input bit_ctrl , //字地址位控制(16b/8b)
    input i2c_rh_wl , //I2C 读写控制信号
    input [15:0] i2c_addr , //I2C 器件内地址
    input [ 7:0] i2c_data_w , //I2C 要写的数据
    output reg [ 7:0] i2c_data_r , //I2C 读出的数据
    output reg i2c_done , //I2C 一次操作完成
    output reg i2c_ack , //I2C 应答标志 0:应答 1:未应答
    output reg scl , //I2C 的 SCL 时钟信号
    inout sda , //I2C 的 SDA 信号
    
    //user interface
    output reg dri_clk //驱动 I2C 操作的驱动时钟
    );
    
    //localparam define
    localparam st_idle = 8'b0000_0001; //空闲状态
    localparam st_sladdr = 8'b0000_0010; //发送器件地址(slave address)
    localparam st_addr16 = 8'b0000_0100; //发送 16 位字地址
    localparam st_addr8 = 8'b0000_1000; //发送 8 位字地址
    localparam st_data_wr = 8'b0001_0000; //写数据(8 bit)
    localparam st_addr_rd = 8'b0010_0000; //发送器件地址读
    localparam st_data_rd = 8'b0100_0000; //读数据(8 bit)
    localparam st_stop = 8'b1000_0000; //结束 I2C 操作
    
    //reg define
    reg sda_dir ; //I2C 数据(SDA)方向控制
    reg sda_out ; //SDA 输出信号
    reg st_done ; //状态结束
    reg wr_flag ; //写标志
    reg [ 6:0] cnt ; //计数
    reg [ 7:0] cur_state ; //状态机当前状态
    reg [ 7:0] next_state; //状态机下一状态
    reg [15:0] addr_t ; //字地址寄存
    reg [ 7:0] data_r ; //读取的数据
    reg [ 7:0] data_wr_t ; //I2C 需写的数据的临时寄存
    reg [ 9:0] clk_cnt ; //分频时钟计数
    
    //wire define
    wire sda_in ; //SDA 输入信号
    wire [8:0] clk_divide ; //模块驱动时钟的分频系数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    IIC驱动模块中间变量描述
    在这里插入图片描述

    sda 数据方向控制模块的代码

    assign sda = sda_dir ? sda_out : 1'bz ; //SDA 数据输出或高阻
    assign sda_in = sda ; //SDA 数据输入
    
    • 1
    • 2

    系统时钟是 50Mhz,I2C 的 SCL 时钟是 250KHz,那么系统时钟通过分频得到SCL 时钟的分频系数是 50MHz/250KHz=200。
    再通过计数 dri_clk 的时钟周期得到 SCL 时钟。I2C 驱动时钟 dri_clk 是 SCL 的 4 倍即 250KHz*4=1MHz,系统时钟通过分频得到 dri_clk 时
    钟的分频系数是 clk_divide=50MHz/1MHz=50。

    assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2 ; //模块驱动时钟的分频系数
    
    //生成 I2C 的 SCL 的四倍频率的驱动时钟用于驱动 i2c 的操作
    always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
    dri_clk <= 1'b0;
    clk_cnt <= 10'd0;
    end
    else if(clk_cnt ==(clk_divide[8:1] - 9'd1)) begin
    clk_cnt <= 10'd0;
    dri_clk <= ~dri_clk;
    end
    else
    clk_cnt <= clk_cnt + 10'b1;
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    clk_divide=200/4=50=9’b0_0011_0010。“>>”为右移运算符,每次右移一位,数据的高位补 0,相当于将数据除以 2,右移两位即将数据除以 4。
    “clk_divide[8:1]”是直接丢弃最低位,数据高位补 0,即 clk_divide[8:1]=9’b0_0001_1001=25,实现的运算是将 clk_divide 的值除以 2。

    三段式状态机Verilog代码

    首先我们复习一下三段式状态机的基本格式:
    三段式状态机的基本格式是:
    第一个 always 语句实现同步状态跳转;
    第二个 always 语句采用组合逻辑判断状态转移条件;
    第三个 always 语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
    实现同步状态跳转的代码如下所示:

    //(三段式状态机)同步时序描述状态转移
    always @(posedge dri_clk or negedge rst_n) begin
    if(!rst_n)
    cur_state <= st_idle;
    else
    cur_state <= next_state;
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面代码主要实现同步状态跳转,在系统上电后,状态机的状态(cur_state)处于空闲状态(st_idle),否则将下一个状态赋值给当前状态。

    接下来就是编写三段式状态机的第三段的代码,使用时序逻辑描述状态的输出。

    //组合逻辑判断状态转移条件
    always @(*) begin
    	next_state = st_idle;
    case(cur_state)
    	st_idle: begin //空闲状态
    		if(i2c_exec) begin
    			next_state = st_sladdr;
    		end
    		else
    			next_state = st_idle;
    	end
    	st_sladdr: begin
    		if(st_done) begin
    			if(bit_ctrl) //判断是 16 位还是 8 位字地址
    				next_state = st_addr16;
    				else
    					next_state = st_addr8 ;
    				end
    		else
    			next_state = st_sladdr;
    		end
    	st_addr16: begin //写 16 位字地址
    		if(st_done) begin
    			next_state = st_addr8;
    		end
    		else begin
    			next_state = st_addr16;
    		end
    	end
    	st_addr8: begin //8 位字地址
    		if(st_done) begin
    			if(wr_flag==1'b0) //读写判断
    				next_state = st_data_wr;
    			else
    				next_state = st_addr_rd;
    		end
    			else begin
    				next_state = st_addr8;
    			end
    	end
    	st_data_wr: begin //写数据(8 bit)
    		if(st_done)
    			next_state = st_stop;
    		else
    			next_state = st_data_wr;
    		end
    	st_addr_rd: begin //写地址以进行读数据
    		if(st_done) begin
    			next_state = st_data_rd;
    		end
    		else begin
    			next_state = st_addr_rd;
    		end
    	end
    	st_data_rd: begin //读取数据(8 bit)
    		if(st_done)
    			next_state = st_stop;
    		else
    			next_state = st_data_rd;
    		end
    	st_stop: begin //结束 I2C 操作
    		if(st_done)
    			next_state = st_idle;
    		else
    			next_state = st_stop ;
    		end
    	default: next_state= st_idle;
    	endcase
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    上面代码中的各个状态之间的跳转还有判断条件。

    这里主要简述的是主机发送写命令状态的代码,摘取其中一个状态的输出部分分析如下所示:

    	st_sladdr: begin //写命令(器件地址和写控制位)
    		case(cnt)
    			7'd1 : sda_out <= 1'b0; //开始 I2C
    			7'd3 : scl <= 1'b0;
    			7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
    			7'd5 : scl <= 1'b1;
    			7'd7 : scl <= 1'b0;
    			7'd8 : sda_out <= SLAVE_ADDR[5];
    			7'd9 : scl <= 1'b1;
    			7'd11: scl <= 1'b0;
    			7'd12: sda_out <= SLAVE_ADDR[4];
    			7'd13: scl <= 1'b1;
    			7'd15: scl <= 1'b0;
    			7'd16: sda_out <= SLAVE_ADDR[3];
    			7'd17: scl <= 1'b1;
    			7'd19: scl <= 1'b0;
    			7'd20: sda_out <= SLAVE_ADDR[2];
    			7'd21: scl <= 1'b1;
    			7'd23: scl <= 1'b0;
    			7'd24: sda_out <= SLAVE_ADDR[1];
    			7'd25: scl <= 1'b1;
    			7'd27: scl <= 1'b0;
    			7'd28: sda_out <= SLAVE_ADDR[0];
    			7'd29: scl <= 1'b1;
    			7'd31: scl <= 1'b0;
    			7'd32: sda_out <= 1'b0; //0:写
    			7'd33: scl <= 1'b1;
    			7'd35: scl <= 1'b0;
    			7'd36: begin //主机释放 SDA 以使从机应答
    				sda_dir <= 1'b0;
    				sda_out <= 1'b1;
    		end
    		7'd37: scl <= 1'b1;
    		7'd38: begin //从机应答
    			st_done <= 1'b1;
    			if(sda_in == 1'b1) //高电平表示未应答
    				i2c_ack <= 1'b1; //拉高应答标志位
    		end
    		7'd39: begin
    			scl <= 1'b0;
    			cnt <= 7'b0; //清空计数
    		end
    		default : ;
    	endcase
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    I2C 读写状态的输出的部分代码都有绘制对应的波形图,方便按照波形进行编写代码。

    EEPROM 读写模块Verilog代码

    E2PROM 读写模块主要实现对 I2C 读写过程的控制,包括给出字地址及需要写入该地址中的数据、启动 I2C 读写操作、判断读写数据是否一致等。

    E2PROM 读写模块框图和输入输出端口简介
    在这里插入图片描述

    E2PROM 读写模块端口与功能描述如下表所示:
    在这里插入图片描述
    在这里插入图片描述

    i2c_data_r是从 E2PROM 读出的数据,i2c_done 是一次 I2C 操作完成信号,i2c_ack 是 I2C 应答标志。这三个信号都是由 I2C 驱动模块(i2c_dri.v)输入进来。i2c_rh_wl 是 I2C 读写控制信号,初始值为 0,表示在进行单次写,在写完 256 个数据后,拉高该信号,I2C 开始随机读操作;i2c_exec 是 I2C 触发执行信号,i2c_exe 信号拉高一个一个周期触发一次 I2C 操作;i2c_addr 是 I2C 器件字地址,i2c_data_w 是 I2C 要写的数据,初始值都为 0,随着每次单次写操作完成信号 i2c_done 逐次加 1。rw_done 是 E2PROM 读写测试完成信号,在I2C 读写完成拉高一个周期;rw_result 是 E2PROM 读写测试结果,将读取的数据与写入的数据进行对比,两者一致说明 E2PROM 读写测试成功,将 rw_result 拉为高电平;rw_done 与 rw_result 会传入读写测试结果显示模块(rw_result_led.v)。

    波形图绘制
    e2Prom数据读写模块除了上面描述的输入输出信号,还需要定义一个写延时计数器wait_cnt,用来计数5ms的写延迟时间。因为AT24C64官方手册规定了数据写入芯片的完成时间最大不超过10ms,所以为了保证数据能够正确写入,单次写入数据操作完成后,最好延时10ms的时间。本次实验为了节省数据写入的时间,WR_WAIT_TIME的值设置为5000,即5ms(输入时钟的周期为1us,1us*5000=5ms),实测延时5ms也可以正确写入。这里不建议大家将写入的间隔设置的过于短,否则会导致数据写入失败。另外,E2PROM只有对写操作有时间间隔要求,对读操作没有间隔要求,因此读写测试模块仅对写操作增加时间间隔。

    另外我们还定义一个状态流控制 flow_cnt,用来切换读写控制与生成 I2C 的将写数据。系统上电后,进入 flow_cnt=2’d0 状态,读写控制信号(i2c_rh_wl)为低电平表示可以进行写操作,wait_cnt 计数器从 0开始计数,计数到 5ms 后拉高一个周期 I2C 触发信号(i2c_exec),触发一次 I2C 操作,将i2c_addr=16’b0000_0000_0000_0000 与 i2c_data_w=8’b0000_0000 的数据传入 I2C 驱动模块进行一次单次写操作,I2C 驱动模块单次写完成后输出一个周期的 i2c_done 高电平,此时控制状态流控制信号(flow_cnt)加 1 进入 2’d1 状态。

    在 2’d1 状态,i2c_addr 与 i2c_data_w 数据分别加 1 后又进入 2’d0 状态,wait_cnt 计数器又从 0 开始计数,计数到 5ms 后拉高 I2C 触发信号(i2c_ack),触发一次 I2C 操作,再次将现在的 i2c_addr 与i2c_data_w 数据传入 I2C 驱动模块进行一次单次写操作,如此循环操作,直至传输完成 256 各将写入的数据后,拉高读写控制信号(i2c_rh_wl),表示可以进行读操作,并且控制状态流控制信号(flow_cnt)进入
    2’d2 状态。

    在 2’d2 状态,收到 I2C 触发信号(i2c_exec)后,开始 I2C 随机读操作,并且控制状态流控制信号(flow_cnt)加 1 进入 2’d3 状态。在 2’d3 状态,在随机读完成以后,将接收的随机读到数据(i2c_data_r)与写入的数据进行对比,如果两者不一致或者在 I2C 读写操作中从机非应答,则说明虽然 I2C 读写操作完成了,但是 I2C 读写操作测试失败,此时输出一个周期高电平的 E2PROM 读写测试完成信号(rw_done),此时表示 E2PROM 读写测试结果信号(rw_result)处于低电平表示测试失败。如果随机读到数据(i2c_data_r)与写入的数据对比一致,则输出一个周期高电平的 E2PROM 读写测试完成信号(rw_done)并且拉高 E2PROM 读写测试结果信号(rw_result)表示 E2PROM 读写测试成功。

    e2Prom 数据读写模块的波形图如下图所示:
    在这里插入图片描述e2Prom 数据读写模块的波形图

    根据上面的波形图的设计,我们编写 E2PROM 读写模块的代码如下:

    module e2Prom_rw(
    input clk , //时钟信号
    input rst_n , //复位信号
    
    //i2c interface
    output reg i2c_rh_wl , //I2C 读写控制信号
    output reg i2c_exec , //I2C 触发执行信号
    output reg [15:0] i2c_addr , //I2C 器件内地址
    output reg [ 7:0] i2c_data_w , //I2C 要写的数据
    input [ 7:0] i2c_data_r , //I2C 读出的数据
    input i2c_done , //I2C 一次操作完成
    input i2c_ack , //I2C 应答标志
    
    //user interface
    output reg rw_done , //E2PROM 读写测试完成
    output reg rw_result //E2PROM 读写测试结果 0:失败 1:成功
    );
    //parameter define
    //E2PROM 写数据需要添加间隔时间,读数据则不需要
    parameter WR_WAIT_TIME = 14'd5000; //写入间隔时间
    parameter MAX_BYTE = 16'd256 ; //读写测试的字节个数
    
    //reg define
    reg [1:0] flow_cnt ; //状态流控制
    reg [13:0] wait_cnt ; //延时计数器
    
    //*****************************************************
    //** main code
    //*****************************************************
    
    //E2PROM 读写测试,先写后读,并比较读出的值与写入的值是否一致
    always @(posedge clk or negedge rst_n) begin
    	if(!rst_n) begin
    		flow_cnt <= 2'b0;
    		i2c_rh_wl <= 1'b0;
    		i2c_exec <= 1'b0;
    		i2c_addr <= 16'b0;
    		i2c_data_w <= 8'b0;
    		wait_cnt <= 14'b0;
    		rw_done <= 1'b0;
    		rw_result <= 1'b0; 
    	end
    	else begin
    		i2c_exec <= 1'b0;
    		rw_done <= 1'b0;
    	case(flow_cnt)
    	2'd0 : begin 
    		wait_cnt <= wait_cnt + 14'b1; //延时计数
    		if(wait_cnt == (WR_WAIT_TIME - 14'b1)) begin //E2PROM 写操作延时完成
    			wait_cnt <= 1'b0;
    		if(i2c_addr == MAX_BYTE) begin //256 个字节写入完成
    			i2c_addr <= 16'b0;
    			i2c_rh_wl <= 1'b1;
    			flow_cnt <= 2'd2;
    		end
    			else begin
    			flow_cnt <= flow_cnt + 2'b1;
    			i2c_exec <= 1'b1;
    			end
    		end
    	end
    	2'd1 : begin
    		if(i2c_done == 1'b1) begin //E2PROM 单次写入完成
    			flow_cnt <= 2'd0;
    			i2c_addr <= i2c_addr + 16'b1; //地址 0~255 分别写入
    			i2c_data_w <= i2c_data_w + 8'b1; //数据 0~255
    		end 
    	end
    	2'd2 : begin 
    		flow_cnt <= flow_cnt + 2'b1;
    		i2c_exec <= 1'b1;
    	end 
    	2'd3 : begin
    		if(i2c_done == 1'b1) begin //E2PROM 单次读出完成
    		//读出的值错误或者 I2C 未应答,读写测试失败
    		if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
    			rw_done <= 1'b1;
    			rw_result <= 1'b0;
    		end
    		else if(i2c_addr == (MAX_BYTE - 16'b1)) begin //读写测试成功
    			rw_done <= 1'b1;
    			rw_result <= 1'b1;
    		end 
    		else begin
    			flow_cnt <= 2'd2;
    			i2c_addr <= i2c_addr + 16'b1;
    		end
    		end 
    		end
    	default : ;
    	endcase 
    	end
    end 
    
    endmodule
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    拉高 i2c_exec,拉低 i2c_rh_wl(低电平表
    示写),然后分别向 E2PROM 的地址 0 至地址 255 写入数据 0 至 255,并且在每次写操作之间增加 5ms 的延时。数据全部写完后,发起读操作,即拉高 i2c_exec,拉高 i2c_rh_wl(高电平表示读),然后分别从E2PROM 的地址 0 至地址 255 读出数据,并判断读出的值与写入的值是否一致,如果数据一致并且每次操作 IIC 都有应答信号产生(i2c_ack),E2PROM 的读写测试才正确,否则读写测试失败。

    读写测试完成后,输出 rw_done 信号和 rw_result 信号,rw_done 为 E2PROM 读写测试完成信号,rw_result 为读写测试的结果,0 表示读写失败,1 表示读写正确。

  • 相关阅读:
    【算法刷题day32】Leetcode:122. 买卖股票的最佳时机 II、55. 跳跃游戏、45. 跳跃游戏 II
    LED点灯
    vue内置组件Transition的详解
    【C语言】【strlen函数的使用与模拟实现】
    【毕业季·进击的技术er】青春不散场
    Linux运维工程师面试题集锦
    先睹为快_Mandelbrot集
    如何利用低代码做好系统整合,实现企业统一管理?
    Delphi 开发so库,Delphi 调用SO库
    opencv python debug记录
  • 原文地址:https://blog.csdn.net/weixin_41226265/article/details/134101722