FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(2)
- 工程目的
- I2C 单字节写操作
- I2C 随机读操作
- EEPROM 字节读写整体框图
- 模块功能简介
- I2C 驱动模块
- 模块框图
- 跨时钟域处理
- 状态转移图
- 波形分析
- 单字节写操作局部波形图(一)
- 单字节写操作局部波形图(二)
- 第一部分:输入信号说明
- i2c_clk 信号波形图
- 第二部分:时钟信号计数器 cnt_clk 和输出信号 i2c_clk 的设计与实现
- 第三部分:状态机相关信号波形的设计与实现
- 下面开始随机读操作部分的讲解。
- 数据收发模块
FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(1)
FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(2)
FPGA_IIC代码-正点原子 野火 小梅哥 特权同学对比写法(3)
工程目的
使用按键控制数据写入或读出 EEPROM。使用写控制按键向 EEPROM 中写入数据 1-10 共 10 字节数据,使用读控制按键读出之前写入到 EEPROM 的数据,并将读出的数据在数码管上显示出来。
I2C 单字节写操作
单字节写操作时序图(单字节存储地址)
单字节写操作时序图(2 字节存储地址)
参照时序图,列出单字节写操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;
(7) 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单字节数据写入完成。
I2C 随机读操作
随机读操作时序图(单字节存储地址)
随机读操作时序图(2 字节存储地址)
参照时序图,列出页写时序操作流程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;
EEPROM 字节读写整体框图
模块功能简介
I2C 驱动模块
模块框图
I2C 驱动模块的主要功能是按照 I2C 协议对 EERPROM 存储芯片执行数据读写操作。
I2C 驱动模块框图和输入输出端口简介
wr_en、rd_en 为写使能信号,由数据收发模块生成并传入,高电平有效;
i2c_start 信号为单字节数据读/写开始信号, i2c_start 信号同时传入的还有数据存储地址 byte_addr 和待写入字节数据wr_data;
写使能 wr_en 和 i2c_start 信号同时有效,模块执行单字节数据写操作,按照数据存储地址 byte_addr,向 EEPROM 对应地址写入数据 wr_data;
读使能信号 rd_en 和i2c_start 信号同时有效,模块执行单字节数据读操作,按照数据存储地址 byte_addr 读取EEPROM 对应地址中的数据;
I2C 设备存储地址有单字节和 2 字节两种,为了应对这一情况,我们向模块输入 addr_num 信号,当信号为低电平时,表示 I2C 设备存储地址为单字节,在进行数据读写操作时只写入数据存储地址 byte_addr 的低 8 位;当信号为高电平时,表示 I2C 设备存储地址为 2 字节,在进行数据读写操作时要写入数据存储地址 byte_addr 的全部 16 位。
为了后面的通用性,我们这是使用了单字节操作,如果想实现数据的连续读写,可以持续拉高读写使能信号,同时要保证开始信号的使能,这样就能够实现数据连续读取或写入。
跨时钟域处理
输出信号中,i2c_clk 是本模块的工作时钟,由系统时钟 sys_clk 分频而来,它的时钟频率为串行时钟 i2c_scl 频率的 4 倍,时钟信号 i2c_clk 要传入数据收发模块(i2c_rw_data)作为模块的工作时钟;输出给数据收发模块(i2c_rw_data)的单字节数据读/写结束信号i2c_end,高电平有效,表示一次单字节数据读/写操作完成;rd_data 信号表示自 EEPROM读出的单字节单字节数据,输出至数据收发模块(i2c_rw_data);i2c_scl、i2c_sda 分别是串行时钟信号和串行数据信号,由模块产生传入 EEPROM 存储芯片。
状态转移图
参照 I2C 设备单字节写操作和随机读操作的操作流程
系统上电后,状态机处于 IDLE(初始状态),接收到有效的单字节数据读/写开始信号i2c_start 后,状态机跳转到 START_1(起始状态);FPGA 向 EEPROM 存储芯片发送起始信号;随后状态机跳转到 SEND_D_ADDR(发送器件地址状态),在此状态下向 EEPROM 存储芯片写入控制指令,控制指令高 7 位为器件地址,最低位为读写控制字,写入“0”,表示执行写操作;控制指令写入完毕后,状态机跳转ACK_1(应答状态)。
在 ACK_1(应答状态)状态下,要根据存储地址字节数进行不同状态的跳转。当 FPGA接收到 EEPROM 回 传 的 应 答 信 号 且 存 储 地 址 字 节 为 2 字 节 , 状 态 机 跳 转 到SEND_B_ADDR_H(发送高字节地址状态),将存储地址的高 8 位写入 EEPROM,写入完成后,状态机跳转到 ACK_2(应答状态);FPGA 接收到应答信号后,状态机跳转到SEND_B_ADDR_L(发送低字节地址状态);当 FPGA 接收到 EEPROM 回传的应答信号且存储地址字节为单字节,状态机状态机直接跳转到SEND_B_ADDR_L(发送低字节地址状态);在此状态低 8 位存储地址或单字节存储地址写入完成后,状态机跳转到 ACK_3(应答状态)。
在 ACK_3(应答状态)状态下,要根据读/写使能信号做不同的状态跳转。当 FPGA 接收到应答信号且写使能信号有效,状态机跳转到 WR_DATA(写数据状态);在写数据状态,向 EEPROM 写入单字节数据后,状态机跳转到 ACK_4(应答状态);待 FPGA 接收到有效应答信号后,状态机跳转到 STOP(停止状态);当 FPGA 接收到应答信号且读使能信号有效,状态机跳转到 START_2(起始状态);再次向EEPROM 写入起始信号,状态跳转到SEND_RD_ADDR(发送读控制状态);再次向 EEPROM 写入控制字节,高 7 位器件地址不变,读写控制位写入“1”,表示进行读操作,控制字节写入完毕后,状态机跳转到ACK_5(应答状态);待 FPGA 接收到有效应答信号后,状态机跳转到 RD_DATA(读数据状态);在 RD_DATA(读数据状态)状态,EEPROM 向 FPGA 发送存储地址对应存储单元下的单字节数据,待数据读取完成户,状态机跳转到 N_ACK(无应答状态),在此状态下向EEPROM 写入一个时钟的高电平,表示数据读取完成,随后状态机跳转到 STOP(停止状态)。在 STOP(停止状态)状态,FPGA 向 EEPROM 发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回 IDLE(初始状态),等待下一次单字节数据读/写开始信号i2c_start。
波形分析
单字节写操作局部波形图(一)
单字节写操作局部波形图(二)
第一部分:输入信号说明
本模块的输入信号有 8 路,其中 7 路信号与单字节写操作有关。系统时钟信号 sys_clk和复位信号 sys_rst_n 不必多说,这是模块正常工作必不可少的;写使能信号 wr_en、 单字节数据读/写开始信号 i2c_start,只有在两信号同时有效时,模块才会执行单字节数据写操作,若 wr_en 有效时,i2c_start 信号 n 次有效输入,可以实现 n 个字节的连续写操作;addr_num 信号为存储地址字节数标志信号,赋值为 0 时,表示 I2C 设备存储地址为单字节,赋值为 1 时,表示 2C 设备存储地址为 2 字节,本实验使用的 EEPROM 存储芯片的存储地址位 2 字节,此信号恒为高电平;信号 byte_addr 为存储地址;wr_data 表示要写入该地址的单字节数据。
i2c_clk 信号波形图
第二部分:时钟信号计数器 cnt_clk 和输出信号 i2c_clk 的设计与实现
串行时钟 scl 的时钟频率为 250KHz,我们要生成的新时钟 i2c_clk 的频率要是 scl 的 4倍,之所以这样是为了后面更好的生成 scl 和 sda,所以 i2c_clk 的时钟频率为 1MHz。经计算,cnt_clk 要在 0-24 内循环计数,每个系统时钟周期自加 1;cnt_clk 每计完一个周期,i2c_clk 进行一次取反,最后得到 i2c_clk 为频率 1MHz 的时钟,本模块中其他信号的生成都以此信号为同步时钟。
第三部分:状态机相关信号波形的设计与实现
前文理论部分提到,输出至 EEPROM 的串行时钟 scl 与串行数据 sda 只有在进行数据读写操作时有效,其他时刻始终保持高电平。由前文状态机相关讲解可知,除 IDLE(初始状态)状态之外的其他状态均属于数据读写操作的有效部分,所以声明一个使能信号cnt_i2c_clk_en,在除 IDLE(初始状态)状态之外的其他状态保持有效高电平,作为 I2C 数据读写操作使能信号。
我们使用 50MHz 系统时钟生成了 1MHz 时钟 i2c_clk,但输出至 EEPROM 的串行时钟scl 的时钟频率为 250KHz,我们声明时钟信号计数器 cnt_i2c_clk,作为分频计数器,对时钟 i2c_clk 时钟信号进行计数,初值为 0,计数范围为 0-3,计数时钟为 i2c_clk 时钟,每个时钟周期自加 1,实现时钟 i2c_clk 信号的 4 分频,生成串行时钟 scl。同时计数器cnt_i2c_clk 也可作为生成串行数据 sda 的约束条件,以及状态机跳转条件。
计数器 cnt_i2c_clk 循环计数一个周期,对应串行时钟 scl 的 1 个时钟周期以及串行数据 sda 的 1 位数据保持时间,进行数据读写操作时,传输的指令、地址以及数据,位宽为固定的 8 位数据,我们声明一个比特计数器 cnt_bit,对计数器 cnt_i2c_clk 的计数周期进行计数,可以辅助串行数据 sda 的生成,同时作为状态机状态跳转的约束条件。
输出的串行数据 sda 作为一个双向端口,主机通过它向从机发送控制指令、地址以及数据,接收从机回传的应答信号和读取数据。回传给主机的应答信号是实现状态机跳转的条件之一。声明信号 sda_in 作为串行数据 sda 缓存,声明 ack 信号作为应答信号,ack 信号只在状态机处于各应答状态时由 sda_in 信号赋值,此时为从机回传的应答信号,其他状态时钟保持高电平。
状态机状态跳转的各约束条件均已介绍完毕,声明状态变量 state,结合各约束信号,单字节写操作状态机跳转流程如下:
系统上电后,状态机处于 IDLE(初始状态),接收到有效的单字节数据读/写开始信号i2c_start 后,状态机跳转到 START_1(起始状态),同时使能信号cnt_i2c_clk_en 拉高、计数器 cnt_i2c_clk、cnt_bit 开始计数,开始数据读写操作;
在 START_1(起始状态)状态保持一个串行时钟周期,期间 FPGA 向 EEPROM 存储芯片发送起始信号,一个时钟周期过后,计数器 cnt_ i2c_clk 完成一个周期计数,计数器 cnt_i2c_clk 计数到最大值 3,状态机跳转到 SEND_D_ADDR(发送器件地址状态);
计数器 cnt_i2c_clk、cnt_bit 同时归 0,重新计数,计数器 cnt_i2c_clk 每计完一个周期,cnt_bit 自加 1,当计数器 cnt_i2c_clk 完成 8 个计数周期后,cnt_bit 计数到 7,实现 8 个比特计数,器件 FPGA 按照时序向 EEPROM 存储芯片写入控制指令,控制指令高 7 位为器件地址,最低位为读写控制字,写入“0”,表示执行写操作。当计数器 cnt_ i2c_clk 计数到最大值 3、cnt_bit 计数到 7,两计数器同时归 0,状态机跳转到转到 ACK_1(应答状态);
在 ACK_1(应答状态)状态下,计数器 cnt_i2c_clk、cnt_bit 重新计数,当计数器 cnt_i2c_clk 计 数 到 最 大 值 3 , 且 应 答 信 号 ack 为 有 效 的 低电平,状态机跳转到SEND_B_ADDR_H(发送高字节地址状态),两计数器清 0;此状态下,FPGA 将存储地址的高 8 位按时序写入 EEPROM,当计数器 cnt_ i2c_clk 计数到 3、cnt_bit 计数到 7,状态机跳转到 ACK_2(应答状态), 两计数器清 0;
ACK_2 状态下,当计数器 cnt_ i2c_clk 计数到 3,且应答信号 ack 为有效的低电平,状态机跳转到 SEND_B_ADDR_L(发送低字节地址状态) ,两计数器清 0;在此状态下,低 8 位存储地址按时序写入 EEPROM,计数器 cnt_ i2c_clk 计数到 3、cnt_bit 计数到 7,状态机跳转到 ACK_3(应答状态);
在 ACK_3(应答状态)状态下,当 cnt_ i2c_clk 计数 3、应答信号 ack 有效,且写使能信号 wr_en 有效,状态机跳转到 WR_DATA(写数据状态);
在写数据状态,按时序向 EEPROM 写入单字节数据,计数器 cnt_ i2c_clk 计数到 3、cnt_bit 计数到 7,状态机跳转到 ACK_4(应答状态);
在 ACK_4(应答状态)状态下,当 cnt_ i2c_clk 计数 3、应答信号 ack 有效,状态机跳转到 STOP(停止状态)状态;
在 STOP(停止状态)状态,FPGA 向 EEPROM 发送停止信号,一次单字节数据读/写操作完成,随后状态机跳回 IDLE(初始状态),等待下一次单字节数据读/写开始信号i2c_start。
状态机相关信号波形如下
状态机相关信号波形图
状态机相关信号波形图
第四部分:输出串行时钟 i2c_scl、串行数据信号 i2c_sda 及相关信号的波形设计与实现
串口数据 sda 端口作为一个双向端口,在单字节读取操作中,主机只在除应答状态之外的其他状态拥有它的控制权,在应答状态下主机只能接收由从机通过 sda 传入的应答信号。声明使能信号 sda_en,只在除应答状态之外的其他状态赋值为有效的高电平,sda_en有效时,主机拥有对 sda 的控制权。
声明 i2c_sda_reg 作为输出 i2c_sda 信号的数据缓存,在 sda_en 有效时,将 i2c_sda_reg的值赋值给输出串口数据 i2c_sda,sda_en 无效时,输出串口数据 i2c_sda 为高阻态,主机放弃其控制权,接收其传入的应答信号。
i2c_sda_reg 在使能信号 sda_en 无效时始终保持高电平,在使能 sda_en 有效时,在状态机对应状态下,以计数器 cnt_ i2c_clk、cnt_bit 为约束条件,对应写入起始信号、控制指令、存储地址、写入数据、停止信号。
对于输出的串行时钟 i2c_clk,由 I2C 通讯协议可知,I2C 设备只在串行时钟为高电平时进行数据采集,在串行时钟低电平时实现串行数据更新。我们使用计数器 cnt_ i2c_clk、cnt_bit 以及状态变量 state 为约束条件,结合 I2C 通讯协议,生成满足时序要求的输出串行时钟i2c_clk。
输出串行时钟 i2c_scl、串行数据信号 i2c_sda 及相关信号的波形图如下。
i2c_scl、i2c_sda 及相关信号波形图
i2c_scl、i2c_sda 及相关信号波形图
单字节写操作部分涉及的各信号波形的设计与实现讲解完毕,下面开始随机读操作部分的讲解。单字节写操作和随机读操作所涉及的各信号大体相同,在随机读操作,我们只讲解差别较大之处,两操作相同或相似之处不再说明,读者可回顾单字节写操作部分的介绍。
下面开始随机读操作部分的讲解。
状态机相关信号波形图
状态机相关信号波形图
状态机相关信号波形图
i2c_scl、i2c_sda、rd_data 及相关信号波形图
i2c_scl、i2c_sda、rd_data 及相关信号波形图
i2c_scl、i2c_sda、rd_data 及相关信号波形图
驱动模块参考代码(i2c_ctrl.v)
parameter CNT_CLK_MAX = (SYS_CLK_FREQ/SCL_FREQ) >> 2’d3 ;
解释: 50000000/250000=200, 200 / (222)200 / (222)
`timescale 1ns/1ns
module i2c_ctrl
#(
parameter DEVICE_ADDR = 7'b1010_000 , //i2c设备地址
parameter SYS_CLK_FREQ = 26'd50_000_000 , //输入系统时钟频率
parameter SCL_FREQ = 18'd250_000 //i2c设备scl时钟频率
)
(
input wire sys_clk , //输入系统时钟,50MHz
input wire sys_rst_n , //输入复位信号,低电平有效
input wire wr_en , //输入写使能信号
input wire rd_en , //输入读使能信号
input wire i2c_start , //输入i2c触发信号
input wire addr_num , //输入i2c字节地址字节数
input wire [15:0] byte_addr , //输入i2c字节地址
input wire [7:0] wr_data , //输入i2c设备数据
output reg i2c_clk , //i2c驱动时钟
output reg i2c_end , //i2c一次读/写操作完成
output reg [7:0] rd_data , //输出i2c设备读取数据
output reg i2c_scl , //输出至i2c设备的串行时钟信号scl
inout wire i2c_sda //输出至i2c设备的串行数据信号sda
);
//************************************************************************//
//******************** Parameter and Internal Signal *********************//
//************************************************************************//
// parameter define
parameter CNT_CLK_MAX = (SYS_CLK_FREQ/SCL_FREQ) >> 2'd3 ; //cnt_clk计数器计数最大值
parameter CNT_START_MAX = 8'd100; //cnt_start计数器计数最大值
parameter IDLE = 4'd00, //初始状态
START_1 = 4'd01, //开始状态1
SEND_D_ADDR = 4'd02, //设备地址写入状态 + 控制写
ACK_1 = 4'd03, //应答状态1
SEND_B_ADDR_H = 4'd04, //字节地址高八位写入状态
ACK_2 = 4'd05, //应答状态2
SEND_B_ADDR_L = 4'd06, //字节地址低八位写入状态
ACK_3 = 4'd07, //应答状态3
WR_DATA = 4'd08, //写数据状态
ACK_4 = 4'd09, //应答状态4
START_2 = 4'd10, //开始状态2
SEND_RD_ADDR = 4'd11, //设备地址写入状态 + 控制读
ACK_5 = 4'd12, //应答状态5
RD_DATA = 4'd13, //读数据状态
N_ACK = 4'd14, //非应答状态
STOP = 4'd15; //结束状态
// wire define
wire sda_in ; //sda输入数据寄存
wire sda_en ; //sda数据写入使能信号
// reg define
reg [7:0] cnt_clk ; //系统时钟计数器,控制生成clk_i2c时钟信号
reg [3:0] state ; //状态机状态
reg cnt_i2c_clk_en ; //cnt_i2c_clk计数器使能信号
reg [1:0] cnt_i2c_clk ; //clk_i2c时钟计数器,控制生成cnt_bit信号
reg [2:0] cnt_bit ; //sda比特计数器
reg ack ; //应答信号
reg i2c_sda_reg ; //sda数据缓存
reg [7:0] rd_data_reg ; //自i2c设备读出数据
//************************************************************************//
//******************************* Main Code ******************************//
//************************************************************************//
// cnt_clk:系统时钟计数器,控制生成clk_i2c时钟信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_clk <= 8'd0;
else if(cnt_clk == CNT_CLK_MAX - 1'b1)
cnt_clk <= 8'd0;
else
cnt_clk <= cnt_clk + 1'b1;
// i2c_clk:i2c驱动时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
i2c_clk <= 1'b1;
else if(cnt_clk == CNT_CLK_MAX - 1'b1)
i2c_clk <= ~i2c_clk;
// cnt_i2c_clk_en:cnt_i2c_clk计数器使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_i2c_clk_en <= 1'b0;
else if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
cnt_i2c_clk_en <= 1'b0;
else if(i2c_start == 1'b1)
cnt_i2c_clk_en <= 1'b1;
// cnt_i2c_clk:i2c_clk时钟计数器,控制生成cnt_bit信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_i2c_clk <= 2'd0;
else if(cnt_i2c_clk_en == 1'b1)
cnt_i2c_clk <= cnt_i2c_clk + 1'b1;
// cnt_bit:sda比特计数器
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_bit <= 3'd0;
else if((state == IDLE) || (state == START_1) || (state == START_2)
|| (state == ACK_1) || (state == ACK_2) || (state == ACK_3)
|| (state == ACK_4) || (state == ACK_5) || (state == N_ACK))
cnt_bit <= 3'd0;
else if((cnt_bit == 3'd7) && (cnt_i2c_clk == 2'd3))
cnt_bit <= 3'd0;
else if((cnt_i2c_clk == 2'd3) && (state != IDLE))
cnt_bit <= cnt_bit + 1'b1;
// state:状态机状态跳转
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
state <= IDLE;
else case(state)
IDLE:
if(i2c_start == 1'b1)
state <= START_1;
else
state <= state;
START_1:
if(cnt_i2c_clk == 3)
state <= SEND_D_ADDR;
else
state <= state;
SEND_D_ADDR:
if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
state <= ACK_1;
else
state <= state;
ACK_1:
if((cnt_i2c_clk == 3) && (ack == 1'b0))
begin
if(addr_num == 1'b1)
state <= SEND_B_ADDR_H;
else
state <= SEND_B_ADDR_L;
end
else
state <= state;
SEND_B_ADDR_H:
if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
state <= ACK_2;
else
state <= state;
ACK_2:
if((cnt_i2c_clk == 3) && (ack == 1'b0))
state <= SEND_B_ADDR_L;
else
state <= state;
SEND_B_ADDR_L:
if((cnt_bit == 3'd7) && (cnt_i2c_clk == 3))
state <= ACK_3;
else
state <= state;
ACK_3:
if((cnt_i2c_clk == 3) && (ack == 1'b0))
begin
if(wr_en == 1'b1)
state <= WR_DATA;
else if(rd_en == 1'b1)
state <= START_2;
else
state <= state;
end
else
state <= state;
WR_DATA:
if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
state <= ACK_4;
else
state <= state;
ACK_4:
if((cnt_i2c_clk == 3) && (ack == 1'b0))
state <= STOP;
else
state <= state;
START_2:
if(cnt_i2c_clk == 3)
state <= SEND_RD_ADDR;
else
state <= state;
SEND_RD_ADDR:
if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
state <= ACK_5;
else
state <= state;
ACK_5:
if((cnt_i2c_clk == 3) && (ack == 1'b0))
state <= RD_DATA;
else
state <= state;
RD_DATA:
if((cnt_bit == 3'd7) &&(cnt_i2c_clk == 3))
state <= N_ACK;
else
state <= state;
N_ACK:
if(cnt_i2c_clk == 3)
state <= STOP;
else
state <= state;
STOP:
if((cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
state <= IDLE;
else
state <= state;
default: state <= IDLE;
endcase
// ack:应答信号
always@(*)
case (state)
IDLE,START_1,SEND_D_ADDR,SEND_B_ADDR_H,SEND_B_ADDR_L,
WR_DATA,START_2,SEND_RD_ADDR,RD_DATA,N_ACK:
ack <= 1'b1;
ACK_1,ACK_2,ACK_3,ACK_4,ACK_5:
if(cnt_i2c_clk == 2'd0)
ack <= sda_in;
else
ack <= ack;
default: ack <= 1'b1;
endcase
// i2c_scl:输出至i2c设备的串行时钟信号scl
always@(*)
case (state)
IDLE:
i2c_scl <= 1'b1;
START_1:
if(cnt_i2c_clk == 2'd3)
i2c_scl <= 1'b0;
else
i2c_scl <= 1'b1;
SEND_D_ADDR,ACK_1,SEND_B_ADDR_H,ACK_2,SEND_B_ADDR_L,
ACK_3,WR_DATA,ACK_4,START_2,SEND_RD_ADDR,ACK_5,RD_DATA,N_ACK:
if((cnt_i2c_clk == 2'd1) || (cnt_i2c_clk == 2'd2))
i2c_scl <= 1'b1;
else
i2c_scl <= 1'b0;
STOP:
if((cnt_bit == 3'd0) &&(cnt_i2c_clk == 2'd0))
i2c_scl <= 1'b0;
else
i2c_scl <= 1'b1;
default: i2c_scl <= 1'b1;
endcase
// i2c_sda_reg:sda数据缓存
always@(*)
case (state)
IDLE:
begin
i2c_sda_reg <= 1'b1;
rd_data_reg <= 8'd0;
end
START_1:
if(cnt_i2c_clk <= 2'd0)
i2c_sda_reg <= 1'b1;
else
i2c_sda_reg <= 1'b0;
SEND_D_ADDR:
if(cnt_bit <= 3'd6)
i2c_sda_reg <= DEVICE_ADDR[6 - cnt_bit];
else
i2c_sda_reg <= 1'b0;
ACK_1:
i2c_sda_reg <= 1'b1;
SEND_B_ADDR_H:
i2c_sda_reg <= byte_addr[15 - cnt_bit];
ACK_2:
i2c_sda_reg <= 1'b1;
SEND_B_ADDR_L:
i2c_sda_reg <= byte_addr[7 - cnt_bit];
ACK_3:
i2c_sda_reg <= 1'b1;
WR_DATA:
i2c_sda_reg <= wr_data[7 - cnt_bit];
ACK_4:
i2c_sda_reg <= 1'b1;
START_2:
if(cnt_i2c_clk <= 2'd1)
i2c_sda_reg <= 1'b1;
else
i2c_sda_reg <= 1'b0;
SEND_RD_ADDR:
if(cnt_bit <= 3'd6)
i2c_sda_reg <= DEVICE_ADDR[6 - cnt_bit];
else
i2c_sda_reg <= 1'b1;
ACK_5:
i2c_sda_reg <= 1'b1;
RD_DATA:
if(cnt_i2c_clk == 2'd2)
rd_data_reg[7 - cnt_bit] <= sda_in;
else
rd_data_reg <= rd_data_reg;
N_ACK:
i2c_sda_reg <= 1'b1;
STOP:
if((cnt_bit == 3'd0) && (cnt_i2c_clk < 2'd3))
i2c_sda_reg <= 1'b0;
else
i2c_sda_reg <= 1'b1;
default:
begin
i2c_sda_reg <= 1'b1;
rd_data_reg <= rd_data_reg;
end
endcase
// rd_data:自i2c设备读出数据
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_data <= 8'd0;
else if((state == RD_DATA) && (cnt_bit == 3'd7) && (cnt_i2c_clk == 2'd3))
rd_data <= rd_data_reg;
// i2c_end:一次读/写结束信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
i2c_end <= 1'b0;
else if((state == STOP) && (cnt_bit == 3'd3) &&(cnt_i2c_clk == 3))
i2c_end <= 1'b1;
else
i2c_end <= 1'b0;
// sda_in:sda输入数据寄存
assign sda_in = i2c_sda;
// sda_en:sda数据写入使能信号
assign sda_en = ((state == RD_DATA) || (state == ACK_1) || (state == ACK_2)
|| (state == ACK_3) || (state == ACK_4) || (state == ACK_5))
? 1'b0 : 1'b1;
// i2c_sda:输出至i2c设备的串行数据信号sda
assign i2c_sda = (sda_en == 1'b1) ? i2c_sda_reg : 1'bz;
endmodule
数据收发模块
数据收发模块的主要功能是:为 I2C 驱动模块提供读/写数据存储地址、待写入数据以及作为 EEPROM 读出数据缓存,待数据读取完成后将读出数据发送给数码管显示模块进行 数据显示。数据收发模块框图及模块输入输出端口简介
I2C 数据收发模块输入输出信号简介
输入信号中,有 2 路时钟信号和 1 路复位信号,sys_clk 为系统时钟信号,在数据收发模块中用于采集读/写触发信号 read 和 write,2 路触发信号均由外部按键输出,经消抖处理后传入本模块,消抖模块使用的时钟信号为与 sys_clk 相同的系统时钟,所以读/写触发信号的采集要使用系统时钟;i2c_clk 为模块工作时钟,由 I2C 驱动模块生成并传入,是存储地址、读/写数据以及使能信号的同步时钟,因为 I2C 模块的工作时钟为 i2c_clk 时钟信号,两模块工作时钟相同,不会出现时钟不同引起时序问题;复位信号 sys_rst_n,低电平有效,不必多说;i2c_end 为单字节数据读/写接数信号,由 I2C 驱动模块产生并传入,告知数据生成模块单字节数据读/写操作完成。若连续读/写多字节数据,此信号可作为存储地址、写数据的更新标志;rd_data 为 I2C 驱动模块传入的数据信号,表示由 EEPROM 读出的字节数据。
输出信号中, rd_en、wr_en 分别为读写使能信号,生成后传入 I2C 驱动模块,作为I2C 驱动模块读/写操作的判断标志;i2c_start 是单字节数据读/写开始信号,作为 I2C 驱动模块单字节读/写操作开始的标志信号;byte_addr 为读写数据存储地址;wr_data 为待写入EEPROM 的字节数据;fifo_rd_data 为自 EEPROM 读出的字节数据,要发送到数码换显示模块在数码管显示出来。
跨时钟域处理
写有效信号 wr_valid 拉高后,工作时钟 i2c_clk 上升沿采集到 wr_valid 高电平,拉高写使能信号 wr_en,告知 I2C 驱动模块接下来要进行数据写操作。在此次实验我们要连续写入 10 字节数据,所以写使能信号 wr_en 要保持 10 次数据写操作的有效时间,在这一时间段我们要输出 10 次有效的 i2c_start 信号,在接收到第 10 次 i2c_end 信号后,表示 10 字节数据均已写入完成,将写使能信号 rw_en 拉低,完成 10 字节数据的连续写入。
要实现这一操作我们需要声明 2 个变量,声明字节计数器 wr_i2c_data_num 对已写入字节进行计数;由数据手册可知,两次相邻的读/写操作之间需要一定的时间间隔,以保证上一次读/写操作完成,所以声明计数器 cnt_start,对相邻读/写操作时间间隔进行计数。
采集到写有效信号 wr_valid 为高电平,拉高写使能信号 wr_en,计数器 cnt_wait、wr_i2c_data_num 均由 0 开始计数,每一个工作时钟周期 cnt_wait 自加 1,计数到最大值1499,i2c_start 保持一个工作时钟的高电平,同时 cnt_wait 归 0,重新开始计数;I2C 驱动模块接收到有效的 i2c_start 信号后,向 EEPROM 写入单字节数据,传回 i2c_end 信号,表示一次单字节写操作完毕,计数器 wr_i2c_data_num 加 1;计数器 cnt_start 完成 10 次循环计数,i2c_start 拉高 10 次,在接收到第 10 次有效的 i2c_end 信号后,表示连续 10 字节数据写入完毕,将写使能信号 wr_en 拉低,写操作完毕。相关信号波形如下。
第二部分:输出存储地址 byte_addr、写数据 wr_data 信号波形的设计与实现既然是对 EEPROM 中兴写数据操作,存储地址和写数据必不可少,在本从实验中,向EEPROM 中 10 个连续存储存储单元写入 10 字节数据。对输出存储地址 byte_addr,赋值初始存储地址,当 i2c_end 信号有效时,地址加 1,待 10 字节数据均写入完毕,再次赋值初始从从地址;对于写数据 wr_data 处理方式相同,先赋值写数据初值,当 i2c_end 信号有效时,写数据加 1 ,待 10 字节数据均写入完毕,在此赋值写数据初值。两输出信号波形如下。
数据收发模块写操作部分介绍完毕,接下来介绍一下读操作部分各信号波形。
与写操作部分相同,外部按键传入的读触发信号经消抖处理后传入本模块,该信号只保持一个有效时钟,且同步时钟为系统时钟 sys_clk,模块工作时钟 i2c_clk 很难采集到该触发信号。我们需要延长该读使能触发信号的有效时间,使模块工作时钟 i2c_clk 可以采集到该触发信号。处理方式和写操作方式相同,声明计数器 cnt_rd 和读有效信号 rd_valid 两信号,延长读触发信号 read 有效时间,使 i2c_clk 时钟能采集到该读触发信号。具体方法参照写操作部分相关介绍,计数器 cnt_rd 和读有效信号 rd_valid 波形图如下。
对于读使能信号的处理方式也与写操作方式相同,工作时钟 i2c_clk 上升沿采集到有效rd_valid 信号,拉高读使能信号 rd_en,告知 I2C 驱动模块接下来要进行数据读操作。
声明字节计数器 rd_i2c_data_num 对已读出字节进行计数;使用之前声明的计数器cnt_start,对相邻读/写操作时间间隔进行计数。
采集到读有效信号 rd_valid 为高电平,拉高读使能信号 rd_en,计数器 cnt_wait、rd_i2c_data_num 均由 0 开始计数,每一个工作时钟周期 cnt_wait 自加 1,计数到最大值1499,i2c_start 保持一个工作时钟的高电平,同时 cnt_wait 归 0,重新开始计数;I2C 驱动模块接收到有效的 i2c_start 信号后,自 EEPROM 读出单字节数据,传回 i2c_end 信号,表示一次单字节写操作完毕,计数器 rd_i2c_data_num 加 1;计数器 cnt_start 完成 10 次循环计数,i2c_start 拉高 10 次,在接收到第 10 次有效的 i2c_end 信号后,表示连续 10 字节数据写入完毕,将读使能信号 rd_en 拉低,读操作完毕。相关信号波形如下。
既然是数据读操作,自然有读出数据传入本模块,一次读操作连续读出 10 字节数据,先将读取的 10 字节数据暂存到内部例化的 FIFO 中,以传回的 i2c_end 结束信号为写使能,在 i2c_clk 时钟同步下将读出数据写入 FIFO 中。同时我们将 FIFO 的数据计数器引出,方便后续数据发送阶段的操作。相关信号波形图如下。
对于存储地址信号 byte_addr 的讲解,读者参阅写操作部分相关介绍,此处不再赘述,接下来开始数据发送部分各信号波形的讲解。
等到读取的 10 字节均写入 FIFO 中,FIFO 数据计数器 data_num 显示为 10,表示 FIFO中存有 10 字节读出数据。此时拉高 FIFO 读有效信号 fifo_rd_valid,只有信号 fifo_rd_valid为有效高电平,对 FIFO 的读操作才有效;fifo_rd_valid 有效时,计数器 cnt_wait 开始循环
计数,声明此计数器的目的是计数字节数据读出时间间隔,间隔越长,每字节数据在数码管显示时间越长,方面现象观察;当计数器 cnt_wait 计数到最大值时,归 0 重新计数,FIFO 读使能信号信号 fifo_rd_en 拉高一个时钟周期,自 FIFO 读出一个字节数据,由fifo_rd_data 将数据传出给数码管显示模块,读出字节计数器 rd_data_num 加 1;等到 10 字节数据均读取并传出后,fifo_rd_valid 信号拉低,数据发送操作完成。相关信号波形如下。
数据收发模块参考代码(i2c_rd_data.v)
module i2c_rw_data
(
input wire sys_clk , //输入系统时钟,频率 50MHz
input wire i2c_clk , //输入 i2c 驱动时钟,频率 1MHz
input wire sys_rst_n , //输入复位信号,低有效
input wire write , //输入写触发信号
input wire read , //输入读触发信号
input wire i2c_end , //一次 i2c 读/写结束信号
input wire [7:0] rd_data , //输入自 i2c 设备读出的数据
output reg wr_en , //输出写使能信号
output reg rd_en , //输出读使能信号
output reg i2c_start , //输出 i2c 读/写触发信号
output reg [15:0] byte_addr , //输出 i2c 设备读/写地址
output reg [7:0] wr_data , //输出写入 i2c 设备的数据
output wire [7:0] fifo_rd_data //输出自 fifo 中读出的数据
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
// parameter define
parameter DATA_NUM = 8'd10 ,//读/写操作读出或写入的数据个数
CNT_START_MAX = 11'd1500 ,//cnt_start 计数器计数最大值
CNT_WR_RD_MAX = 8'd200 ,//cnt_wr/cnt_rd 计数器计数最大值
CNT_WAIT_MAX = 28'd500_000 ;//cnt_wait 计数器计数最大值
// wire define
wire [7:0] data_num ; //fifo 中数据个数
// reg define
reg [7:0] cnt_wr ; //写触发有效信号保持时间计数器
reg write_valid ; //写触发有效信号
reg [7:0] cnt_rd ; //读触发有效信号保持时间计数器
reg read_valid ; //读触发有效信号
reg [10:0] cnt_start ; //单字节数据读/写时间间隔计数
reg [7:0] wr_i2c_data_num ; //写入 i2c 设备的数据个数
reg [7:0] rd_i2c_data_num ; //读出 i2c 设备的数据个数
reg fifo_rd_valid ; //fifo 读有效信号
reg [27:0] cnt_wait ; //fifo 读使能信号间时间间隔计数
reg fifo_rd_en ; //fifo 读使能信号
reg [7:0] rd_data_num ; //读出 fifo 数据个数
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//cnt_wr:写触发有效信号保持时间计数器,计数写触发有效信号保持时钟周期数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wr <= 8'd0;
else if(write_valid == 1'b0)
cnt_wr <= 8'd0;
else if(write_valid == 1'b1)
cnt_wr <= cnt_wr + 1'b1;
//write_valid:写触发有效信号
//由于写触发信号保持时间为一个系统时钟周期(20ns),
//不能被 i2c 驱动时钟 i2c_scl 正确采集,延长写触发信号生成写触发有效信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
write_valid <= 1'b0;
else if(cnt_wr == (CNT_WR_RD_MAX - 1'b1))
write_valid <= 1'b0;
else if(write == 1'b1)
write_valid <= 1'b1;
//cnt_rd:读触发有效信号保持时间计数器,计数读触发有效信号保持时钟周期数
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_rd <= 8'd0;
else if(read_valid == 1'b0)
cnt_rd <= 8'd0;
else if(read_valid == 1'b1)
cnt_rd <= cnt_rd + 1'b1;
//read_valid:读触发有效信号
//由于读触发信号保持时间为一个系统时钟周期(20ns),
//不能被 i2c 驱动时钟 i2c_scl 正确采集,延长读触发信号生成读触发有效信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
read_valid <= 1'b0;
else if(cnt_rd == (CNT_WR_RD_MAX - 1'b1))
read_valid <= 1'b0;
else if(read == 1'b1)
read_valid <= 1'b1;
//cnt_start:单字节数据读/写操作时间间隔计数
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_start <= 11'd0;
else if((wr_en == 1'b0) && (rd_en == 1'b0))
cnt_start <= 11'd0;
else if(cnt_start == (CNT_START_MAX - 1'b1))
cnt_start <= 11'd0;
else if((wr_en == 1'b1) || (rd_en == 1'b1))
cnt_start <= cnt_start + 1'b1;
//i2c_start:i2c 读/写触发信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
i2c_start <= 1'b0;
else if((cnt_start == (CNT_START_MAX - 1'b1)))
i2c_start <= 1'b1;
else
i2c_start <= 1'b0;
//wr_en:输出写使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_en <= 1'b0;
else if((wr_i2c_data_num == DATA_NUM - 1)
&& (i2c_end == 1'b1) && (wr_en == 1'b1))
wr_en <= 1'b0;
else if(write_valid == 1'b1)
wr_en <= 1'b1;
//wr_i2c_data_num:写入 i2c 设备的数据个数
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_i2c_data_num <= 8'd0;
else if(wr_en == 1'b0)
wr_i2c_data_num <= 8'd0;
else if((wr_en == 1'b1) && (i2c_end == 1'b1))
wr_i2c_data_num <= wr_i2c_data_num + 1'b1;
//rd_en:输出读使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_en <= 1'b0;
else if((rd_i2c_data_num == DATA_NUM - 1)
&& (i2c_end == 1'b1) && (rd_en == 1'b1))
rd_en <= 1'b0;
else if(read_valid == 1'b1)
rd_en <= 1'b1;
//rd_i2c_data_num:写入 i2c 设备的数据个数
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_i2c_data_num <= 8'd0;
else if(rd_en == 1'b0)
rd_i2c_data_num <= 8'd0;
else if((rd_en == 1'b1) && (i2c_end == 1'b1))
rd_i2c_data_num <= rd_i2c_data_num + 1'b1;
//byte_addr:输出读/写地址
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
byte_addr <= 16'h00_5A;
else if((wr_en == 1'b0) && (rd_en == 1'b0))
byte_addr <= 16'h00_5A;
else if(((wr_en == 1'b1) || (rd_en == 1'b1)) && (i2c_end == 1'b1))
byte_addr <= byte_addr + 1'b1;
//wr_data:输出待写入 i2c 设备数据
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_data <= 8'hA5;
else if(wr_en == 1'b0)
wr_data <= 8'hA5;
else if((wr_en == 1'b1) && (i2c_end == 1'b1))
wr_data <= wr_data + 1'b1;
//fifo_rd_valid:fifo 读有效信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
fifo_rd_valid <= 1'b0;
else if((rd_data_num == DATA_NUM)
&& (cnt_wait == (CNT_WAIT_MAX - 1'b1)))
fifo_rd_valid <= 1'b0;
else if(data_num == DATA_NUM)
fifo_rd_valid <= 1'b1;
//cnt_wait:fifo 读使能信号间时间间隔计数,计数两 fifo 读使能间的时间间隔
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_wait <= 28'd0;
else if(fifo_rd_valid == 1'b0)
cnt_wait <= 28'd0;
else if(cnt_wait == (CNT_WAIT_MAX - 1'b1))
cnt_wait <= 28'd0;
else if(fifo_rd_valid == 1'b1)
cnt_wait <= cnt_wait + 1'b1;
//fifo_rd_en:fifo 读使能信号
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
fifo_rd_en <= 1'b0;
else if((cnt_wait == (CNT_WAIT_MAX - 1'b1))
&& (rd_data_num < DATA_NUM))
fifo_rd_en <= 1'b1;
else
fifo_rd_en <= 1'b0;
//rd_data_num:自 fifo 中读出数据个数计数
always@(posedge i2c_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_data_num <= 8'd0;
else if(fifo_rd_valid == 1'b0)
rd_data_num <= 8'd0;
else if(fifo_rd_en == 1'b1)
rd_data_num <= rd_data_num + 1'b1;
//****************************************************************//
//************************* Instantiation ************************//
//****************************************************************//
//------------- fifo_read_inst -------------
fifo_data fifo_read_inst
(
.clock (i2c_clk ), //输入时钟信号,频率 1MHz,1bit
.data (rd_data ), //输入写入数据,1bit
.rdreq (fifo_rd_en ), //输入数据读请求,1bit
.wrreq (i2c_end && rd_en ), //输入数据写请求,1bit
.q (fifo_rd_data ), //输出读出数据,1bit
.usedw (data_num ) //输出 fifo 内数据个数,1bit
);
endmodule
参考代码编写完毕