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

news2024/11/14 2:50:47

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.coloradmin.cn/o/1228691.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux socket编程(4):服务端fork之僵尸进程的处理

在上一节利用fork实现服务端与多个客户端建立连接中&#xff0c;我们使用fork函数来实现服务端既可以accept新的客户端连接请求&#xff0c;又可以接收已连接上的客户端发来的消息。但在Linux中&#xff0c;在子进程终止后&#xff0c;父进程需要处理该子进程的终止状&#xff…

LeetCode(29)三数之和【双指针】【中等】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 三数之和 1.题目 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复…

RE2文本匹配实战

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

计算机网络期末复习(知识点)

一、计算机网络体系结构 计算机网络&因特网&#xff1a; 计算机网络定义&#xff1a;将地理位置不同的具有独立功能的多台计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络操作系统&#xff0c;网络关联软件及网络协议的管理和协调下&#xff0c;实…

在 Linux 上搭建 Java Web 项目环境(最简单的进行搭建)

要在 Linux 上安装的程序有 1.JDK (要想运行 java 程序 JDK 是必不可少的) 2.Tomcat &#xff08;HTTP 服务器&#xff0c;是管理 Web 项目的常用工具&#xff09; 3. mysql &#xff08;数据库&#xff09; 一.安装 JDK 博主使用的 Linux 发行版是 centos &#xff0c;cen…

kubenetes-服务发现和负载均衡

一、服务发布 kubenetes把服务发布至集群内部或者外部&#xff0c;服务的三种不同类型&#xff1a; ClusterlPNodePortLoadBalancer ClusterIP是发布至集群内部的一个虚拟IP,通过负载均衡技术转发到不同的pod中。 NodePort解决的是集群外部访问的问题&#xff0c;用户可能不…

【MySQL--->事务】

文章目录 [TOC](文章目录) 一、基本概念二、事务的操作1.设置全局事务隔离级别2.设置事务提交方式3.事务操作 三、事务隔离性1.隔离性概念2 .隔离级别设置 四、MVCC多版本控制2. read view 一、基本概念 事务是由若干条具有逻辑相关性的SQL语句组成的,用来完成某种任务的**逻辑…

java“俄罗斯方块”

首先新建议一个包为Tetris &#xff08;俄罗斯方块&#xff09; 类名也叫做Tetris&#xff1b; 代码运行&#xff1a; package Tetris; import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.aw…

【运维篇】Redis常见运维命令详解

文章目录 1. 前言2. 连接管理命令详解2.1 AUTH命令2.2 PING命令2.3 SELECT命令2.4 QUIT命令 3. 服务器管理命令详解3.1 FLUSHALL命令3.2 SAVE/BGSAVE命令3.3 SHUTDOWN命令 4. 安全管理命令详解4.1 CONFIG命令4.1.1 CONFIG SET命令用法4.1.2 CONFIG GET命令用法 4.2 AUTH命令 5.…

开源更安全? yum源配置/rpm 什么是SSH?

文章目录 1.开放源码有利于系统安全2.yum源配置&#xff0c;这一篇就够了&#xff01;(包括本地&#xff0c;网络&#xff0c;本地共享yum源)3.rpm包是什么4.SSH是什么意思&#xff1f;有什么功能&#xff1f; 1.开放源码有利于系统安全 开放源码有利于系统安全 2.yum源配置…

代码随想录刷题】Day16 二叉树

文章目录 1.【104】二叉树的最大深度&#xff08;优先掌握递归&#xff09;1.1 题目描述1.2 java代码实现 2.【111】二叉树的最小深度&#xff08;优先掌握递归&#xff09;2.1 题目描述2.2 java代码实现 3.【222】完全二叉树的节点个数3.1 题目描述3.2 java代码实现 【104】二…

6 Redis的慢查询配置

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

Java21新增特性

版本介绍 Java 21是Java平台的一个新版本&#xff0c;于2023年9月19日由Oracle公司正式发布。这个版本包含了数千个性能、稳定性和安全性更新&#xff0c;以及几十个新功能和增强。其中&#xff0c;15个增强被赋予了自己的JDK增强提案&#xff08;JEP&#xff09;&#xff0c;…

回归预测 | Matlab实现HPO-ELM猎食者算法优化极限学习机的数据回归预测

回归预测 | Matlab实现HPO-ELM猎食者算法优化极限学习机的数据回归预测 目录 回归预测 | Matlab实现HPO-ELM猎食者算法优化极限学习机的数据回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 Matlab实现HPO-ELM猎食者算法优化极限学习机的数据回归预测&#xff08;…

Linux系统编程 day02 vim、gcc、库的制作与使用

Linux系统编程 day02 vim、gcc、库的制作与使用 01. vim0101. 命令模式下的操作0102. 切换到文本输入模式0103. 末行模式下的操作0104. vim的配置文件 02. gcc03. 库的制作与使用0301. 静态库的制作与使用0302. 动态库(共享库)的制作与使用 01. vim vim是一个编辑器&#xff0…

微服务 Spring Cloud 7,Nacos配置中心的Pull原理,附源码

目录 一、本地配置二、配置中心1、以Nacos为例&#xff1a;2、Pull模式3、也可以通过Nacos实现注册中心 三、配置中心提供了哪些功能四、如何操作配置中心1、配置注册2、配置反注册3、配置查看4、配置变更订阅 五、主流的微服务注册中心有哪些&#xff0c;如何选择&#xff1f;…

在Vue关于ue的computed属性中传递参数

computed的基本用法 computed是Vue实例中一个非常强大的计算属性&#xff0c;它的值主要根据依赖数据而变化。我们可以将一些简单的计算放在computed属性中&#xff0c;从而实现在模板中使用简单的表达式。 但是实际项目中通常有这么一个场景&#xff1a;根据传递不一样的参数值…

pyqt designer的版本问题

之前我的电脑Windows11 python3.12上安装好了pyqt6后&#xff0c;安装不了pyqt6-tools&#xff0c;导致不能使用designer设计师服务。经过摸索&#xff0c;然来只需要安装qt-tools就够了。qt-tools在plugin包里。比如文章顶部的资源包&#xff0c;下载下来直接使用pip安装该whl…

学生邮箱白嫖/免费安装JetBrains全家桶(IDEA/pycharm等) —— 保姆级教程

&#x1f9f8;欢迎来到dream_ready的博客&#xff0c;&#x1f4dc;相信您对博主首页也很感兴趣o (ˉ▽ˉ&#xff1b;) 博主首页&#xff0c;更多redis、java等优质好文以及各种保姆级教程等您挖掘&#xff01; 目录 前言 JetBrains全家桶介绍 申请过程&#xff1a; 获取学…

Appium自动化测试:通过appium的inspector功能无法启动app的原因

在打开appium-desktop程序&#xff0c;点击inspector功能&#xff0c;填写app的配置信息&#xff0c;启动服务提示如下&#xff1a; 报错信息&#xff1a; An unknown server-side error occurred while processing the command. Original error: Cannot start the cc.knowyo…