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

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

参考代码编写完毕

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢谢~谢先生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值