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

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

  • 工程目的
  • IIC时序图
  • IIC 读写操作方法汇总
  • 正点原子IIC
    • 实验工程整体框图和模块功能简介,如表下图所示:
  • IIC 驱动模块设计
  • 时钟规划
  • 状态跳转流程
    • 单次写操作的波形图如下图所示:
    • 随机读操作的波形图如下图所示:
  • I2C 驱动控制模块Verilog代码
  • 三段式状态机Verilog代码
  • EEPROM 读写模块Verilog代码

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 ; //模块驱动时钟的分频系数

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

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

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

系统时钟是 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

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

上面代码主要实现同步状态跳转,在系统上电后,状态机的状态(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

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

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

	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

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

拉高 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 表示读写正确。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/166504.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

程序员带你入门人工智能

随着人工智能技术的飞速发展&#xff0c;越来越多的程序员开始关注并学习人工智能。作为程序员&#xff0c;我们可能会对如何开始了解人工智能感到困惑。今天&#xff0c;我将向大家介绍一些如何通过自学了解人工智能的经验和方法&#xff0c;帮助大家更好地入门这个充满挑战和…

【Java集合】聊聊Hashmap的哈希函数、扩容、树化

哈希函数 hashmap是开发中常用的一个集合&#xff0c;除了一些基本的属性、put、get等流程&#xff0c;本篇文章主要介绍下哈希函数、扩容、树化的一些细节。 而hash函数就是hashmap的重中之重。 static final int hash(Object key) {int h;return (key null) ? 0 : (h key…

YOLOv8改进 | EIoU、SIoU、WIoU、DIoU、FoucsIOU等二十余种损失函数

一、本文介绍 这篇文章介绍了YOLOv8的重大改进&#xff0c;特别是在损失函数方面的创新。它不仅包括了多种IoU损失函数的改进和变体&#xff0c;如SIoU、WIoU、GIoU、DIoU、EIOU、CIoU&#xff0c;还融合了“Focus”思想&#xff0c;创造了一系列新的损失函数。这些组合形式的…

Halcon Solution Guide I basics(1): Guide to Halcon Methods(Halcon解决方案)

文章目录 文章专栏前言文章解读基础解决方案字符串格式化 文章专栏 Halcon开发 前言 今天来看Halcon的第一章内容&#xff0c;Halcon解决方案 文章解读 基础解决方案 Halcon大部分的应用都使用了三种常用的算子应用用于图像的预处理。 Image Acquisition&#xff1a;图像加…

6 Redis的慢查询配置

1、redis的命令执行流程 redis的慢查询只针对步骤3 默认情况下&#xff0c;慢查询的阈值是10ms 在配置文件中进行配置 //这个参数的单位为微秒 //如果将这个值设置为负数&#xff0c;则会禁用慢日志功能 //如果将其设置为0&#xff0c;则会强制记录每个命令 slowlog-log-slow…

[python]python筛选excel表格信息并保存到另一个excel

目录 关键词平台说明背景所需库1.安装相关库2.代码实现sourcetarget1 关键词 python、excel、DBC、openpyxl 平台说明 项目Valuepython版本3.6 背景 从一个excel表中遍历删选信息并保存到另一个excel表 所需库 1.openpyxl &#xff1a;是一个用于读写 Excel 文件的 Pyt…

2023.11.17 关于 Spring Boot 日志文件

目录 日志文件作用 常见的日志框架说明 门面模式 日志的使用 日志的级别 六种级别 日志级别的设置 日志的持久化 使用 Lombok 输出日志 实现原理 普通打印和日志的区别 日志文件作用 记录 错误日志 和 警告日志&#xff08;发现和定位问题&#xff09;记录 用户登录…

牛客::栈的压入、弹出序列

栈的压入、弹出序列 题目 输入两个整数序列&#xff0c;第一个序列表示栈的压入顺序&#xff0c;请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序&#xff0c;序列4,5,3,2,1是该压栈序列对应的一个弹出序列&…

三、LED闪烁

通过LED的闪烁实验&#xff0c;详解Keil MDK中创建mm32单片机的工程的步骤。 1、开发环境 (1)Keil MDK: V5.38.0.0 (2)MCU: mm320163D7P。 2、Keil工程的创建 (1)打开Keil MDK。 (2)点击“Project”→“New μVision Project...”。 (3)选择工程保存地址及工程文件名&…

RE2文本匹配实战

引言 今天我们来实现RE2进行文本匹配&#xff0c;模型实现参考了官方代码https://github.com/alibaba-edu/simple-effective-text-matching-pytorch。 模型实现 RE2模型架构如上图所示。它的输入是两个文本片段&#xff0c;所有组件参数除了预测层和对齐层外都是共享的。上图…

变周期控制思路

举例&#xff1a;热值调节的过程中&#xff0c;调节周期在偏差较小时&#xff0c;可以设置较大些&#xff0c;调节周期在偏差较大时&#xff0c;可以设置较小些。并且在偏差较大时&#xff0c;立刻进入调节&#xff08;计时器清零&#xff09;。 -350<偏差<600&#xff0…

华为麒麟服务器--硬盘问题

记录以下今天处理的服务器&#xff1a; 情况说明&#xff1a;linux 系统&#xff0c;不知道什么原因系统就突然不能用了&#xff08;据说是前段时间断电来着&#xff0c;但是机房有应急电源&#xff09;。 系统环境&#xff1a; 服务器&#xff1a;华为RH2288H V3 服务器 服…

设计模式(二)-创建者模式(2-0)-简单工厂模式

一、简单工厂模式定义 客户端不需要关注创建实例的过程。于是需要通过工厂模式&#xff0c;要把创建对象过程和使用对象进行分离。所以客户端只要使用对象即可&#xff0c;而创建对象过程由一种类来负责&#xff0c;该类称为工厂类。 由于创建实例的方式是在静态方法里实现的…

文件钓鱼-后缀隐藏文件捆绑文件压缩释放技巧

0x00 文件钓鱼 简单说下文件样本钓鱼的目的&#xff0c;为诱导用户安装木马文件&#xff0c;达到控制或者窃取某些信息的目的&#xff0c;抛开邮件的真实性。木马的伪造是一个比较关键的点&#xff0c;下面简要说下三种木马文件伪装的技巧 0x01 水坑攻击与鱼叉攻击的概念 水坑…

VMware——WindowServer2012R2环境mysql5.7.14解压版安装主从复制(图解版)

目录 一、服务器信息二、192.168.132.33主服务器上安装mysql&#xff08;主&#xff09;2.1、环境变量配置2.2、安装2.2.1、修改配置文件内容2.2.2、初始化mysql并指定超级用户密码2.2.3、安装mysql服务2.2.4、启动mysql服务2.2.5、登录用户管理及密码修改2.2.6、开启远程访问 …

AD教程 (十九)PCB板框的评估和层叠设置

AD教程 &#xff08;十九&#xff09;PCB板框的评估和层叠设置 板子越小&#xff0c;层数越少&#xff0c;成本越低 PCB板框评估 器件摆放 CtrlA 选中全部器件点击工具&#xff0c;选择器件摆放&#xff0c;选择在矩形区域排列 框定矩形区域&#xff0c;器件就会摆放在框定的矩…

Unity Meta Quest 一体机开发(七):配置玩家 Hand Grab 功能

文章目录 &#x1f4d5;教程说明&#x1f4d5;玩家物体配置 Hand Grab Interactor⭐添加 Hand Grab Interactor 物体⭐激活 Hand Grab Visual 和 Hand Grab Glow⭐更新 Best Hover Interactor Group &#x1f4d5;配置可抓取物体&#xff08;无抓取手势&#xff09;⭐刚体和碰撞…

sftp 从windows10向linux(centos7)传输文件

前言背景&#xff1a;该示例是需要从windows10向本地linux系统传输一个qt安装文件&#xff0c;不想或者无法安装xftp这些传输工具&#xff0c;直接通过命令传输&#xff1b; 首先保证windows10 ping通linux系统ip&#xff0c;linux ping 通windows10系统&#xff1b; 注意&am…

ps找不到msvcp140.dll怎么办?亲测5个有效的修复方法分享

运行Photoshop时提示找不到MSVCP140.dll&#xff0c;这是因为计算机MSVCP140.dll文件丢失或者损坏。msvcp140.dll是微软Visual C 2015运行库的一部分&#xff0c;它包含了许多用于支持C运行的函数和类。当我们在使用某些程序时&#xff0c;如果这个程序依赖于msvcp140.dll&…

Figma 插件学习(一)

一.插件介绍 插件在文件中运行&#xff0c;执行一个或多个用户操作&#xff0c;并允许用户自定义其体验或创建更高效的工作流程。 插件通过专用插件API与Figma的编辑器交互。还可以利用外部Web API。 1.插件API 插件API支持读写功能&#xff0c;允许查看、创建和修改文件的…
最新文章