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 驱动模块的设计,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 为高电平的时候寄存数据。
总结:在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高有效,作为状态机状态跳转的触发信号。
状态机状态跳转的各约束条件均已介绍完毕,首先声明状态变量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 清零,开始下一数据传输状态。
由上面的状态机跳转图可知,写命令传输完成后,根据接收到的字地址控制命令 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 清零,开始下一数据传输状。
该状态的波形图如下图所示:
由上面的 2 个状态机跳转图可知,在第 9 个 scl 时钟周期的上升沿,从机开始应答,将 sda 信号拉低,在其下降沿到来后,从机释放了总线,此时的 sda 由外部的上拉电路将其电平拉成高电平。双字节高 8 位字地址传输完成后,我们下一个进入状态是传输双字节低 8 位字地址状态(st_addr8)。传输双字节低 8 位字地址状态(st_addr8)操作与传输双字节高 8 位字地址的状态(st_addr16)操作基本一致,只是传输的数据内容是 addr_t 的低 8 位数据,即“8’b0000_0000”通过 sda_out 传输出去,该状态的波形图如下图所示:
接下来是进入传输写数据状态(st_data_wr),写数据状态(st_data_wr)操作与传输双字节高 8 位字地址的状态(st_addr16)操作也是基本一致,只是传输的数据内容是 data_wr_t 的低 8 位数据,即8’b0000_0000 通过 sda_out 传输出去。该状态数据传输的波形图如下所示:
接下来是进入停止发送状态即 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 发送写控制命令)非常相似,只是此时的写标志信号(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 随机读操作完成状态(st_stop),cnt 从 0 开始计数,首先拉高 sda_dir 信号切换 sda 数据方向位FPGA 主机输出,接下来主机在 scl 为高电平时拉低 sda_out,结束本次 I2C 单次写操作,scl 与 sda 都被拉高,即将进入空闲状态,在 I2C 操作完成状态(st_stop)最后一个周期内输出一个单次写完成信号i2c_done 并且给 cnt 计数器清零后彻底结束本次随机读操作,sda 总线恢复空闲状态。开始下一个字节的读出,直至 256 个数据全部读完,本次 E2PROM 读写操作完成。
本模块代码主要分为驱动时钟产生模块、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 ; //模块驱动时钟的分频系数
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。
首先我们复习一下三段式状态机的基本格式:
三段式状态机的基本格式是:
第一个 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 读写状态的输出的部分代码都有绘制对应的波形图,方便按照波形进行编写代码。
E2PROM 读写模块主要实现对 I2C 读写过程的控制,包括给出字地址及需要写入该地址中的数据、启动 I2C 读写操作、判断读写数据是否一致等。
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 表示读写正确。