基于 I2C 协议的 AD实验(附代码)

目录

1. 理论学习

1.1 AD介绍

1.2 I2C 简介

1.2.1 I2C物理层

1.2.2 I2C协议层

1.3 PCF8591芯片简介

1.3.1 引脚信息

1.3.2 功能描述

2. 实验

2.1 硬件资源

2.2 模块框图

2.3 程序设计

2.3.1 工程整体框图

2.3.2 I2C驱动模块

1. 模块框图

2. 波形图分析:

写数据:

 3. RTL代码设计

2.3.3 数据生成模块

1. 波形图分析: 

2. RTL代码编写

2.3.4 串口接收模块


1. 理论学习

1.1 AD介绍

      模/数转换器即 A/D 转换器,或简称 ADC(Analog to Digital Conver),通常是指一个将模拟信号转变为数字信号的电子元件或电路。常见的模/数转换器将经过与标准量比较处理后的模拟量转换为以二进制数值表示的离散信号。 真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。
      模拟信号与数字信号的转换过程一般分为四个步骤:采样、保持、量化、编码。前两个步骤在采样-保持电路中完成,后两步则在 ADC 芯片中完成。
       常用的 ADC 可分为积分型、逐次逼近型、并行比较型/串并行型、Σ -Δ调制型、电容阵列逐次比较型以及压频变换型。
       ADC 的主要技术指标包括:分辨率、转换速率、量化误差、满刻度误差、线性度。
分辨率:   ADC的分辨率是指使输出数字量变化一个相邻数码所需输入模拟电压的变化量。常用二进制的位数表示,例如12位ADC的分辨率就是12位。
转换速率:转换速率是指完成一次从模拟转换到数字的 AD 转换所需要的时间的倒数。积分型AD 的转换时间是毫秒级属低速 AD,逐次比较型 AD 是微秒级属中速 AD,全并行/串并行型 AD 可达到纳秒级。
量化误差:ADC把模拟量变为数字量,用数字量近似表示模拟量,这个过程称为量化。量化误差是ADC的有限位数对模拟量进行量化而引起的误差。
满刻度误差:  满刻度误差又称为增益误差。ADC的满刻度误差是指满刻度输出数码所对应的实际输入电压与理想输入电压之差。
线性度:   线性度有时又称为非线性度,它是指转换器实际的转换特性与理想直线的最大偏差。


1.2 I2C 简介

       I2C 通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线, 只需要两根线即可在连接于总线上的器件之间传送信息。I2C 通讯协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD, 图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。


1.2.1 I2C物理层

        I2C 通讯设备之间的常用连接方式,具体见下图。

基于 I2C 协议的 AD实验(附代码)_第1张图片

物理层有如下特点:

(1)支持多设备的总线。在一个I2C通讯总线中,可以连接多个I2C通讯设备,支持多主机及多个从机通讯。
(2)一个I2C总线由两根总线组成。一条双向串行数据线(SDA):用于传递数据,一条串行时钟(SCL):同步主从机数据的收发。
(3)主机通过从机地址进行访问。
(4)在I2C设备空闲时,给I/O确定的高电平(硬件上设计上拉电阻),防止误启动I2C。
(5)当多主机同时使用总线时,利用仲裁方式决定哪个设备占用总线。
(6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。

1.2.2 I2C协议层

1. I2C 设备器件地址与存储地址

器件地址

       每个 I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成。可编程的部分需用户在硬件设计时设置。PCF8591 为例,其器件地址为 1001 加 3 位的可编程地址,通过将芯片的 A0、A1、 A2 这 3 个引脚分别连接到 VCC 或 GND 来实现器件地址低 3位的设置,若 3 个引脚均连接到 VCC,则设置后的器件地址为 1010_111;若 3 个引脚均连接到 GND,则设置后的器件地址为 1010_000。
      在 I2C 主从设备通讯时,主机在发送了起始信号后,接着会向从机发送控制命令。控制命令长度为 1 个字节,它的高 7 位为上文讲解的 I2C 设备的器件地址,最低位为读写控制位。读写控制位为 0 时,表示主机要对从机进行数据写入操作;读写控制位为 1 时,表示主机要对从机进行数据读出操作

基于 I2C 协议的 AD实验(附代码)_第2张图片

       主机会将控制命令直接发送到串行数据线 SDA上,与主机硬件相连的从机设备都会接收到主机发送的控制命令。所有从机设备在接收到主机发送的控制命令后会与自身器件地址做对比;若两者地址相同,该从机设备会回应一个应答信号告知主机设备,主机设备接收到应答信号后,主从设备建立通讯连接,两者可进行数据通讯。

存储地址

       每一个支持 I2C 通讯协议的设备器件,内部都会包含一些可进行读/写操作的寄存器或存储器,例如OV7725、 OV5640 摄像头(它们使用的是与 I2C 协议极为相似的 SCCB 协议),他们内部包含一些需要进行读/写配置的寄存器,只有向对应寄存器写入正确参数,摄像头才能被正确使用。写配置的寄存器,只有向对应寄存器写入正确参数,摄像头才能被正确使用。由于 I2C 设备要配置寄存器的多少或存储容量的大小的不同,存储地址根据位宽分为单字节和 2 字节两种。

2. I2C 读/写操作

      I2C 写操作,对传入从机的控制命令最低位即读写控制位写入不同数据值,主机可实现对从机的读/写操作,读写控制位为 0 时,表示主机要对从机进行数据写入操作;读写控制位为 1 时,表示主机要对从机进行数据读出操作。由于一次写入数据量的不同, I2C 的写操作可分为单字节写操作和页写操作。

I2C 单字节写操作:

基于 I2C 协议的 AD实验(附代码)_第3张图片

 单字节写操作时序图(单字节存储地址)

基于 I2C 协议的 AD实验(附代码)_第4张图片

 单字节写操作时序图(2字节存储地址)

参照时序图,列出单字节写操作流程如下:
(1)主机产生并发送起始信号给从机,然后将控制命令写入从机设备,其中读写控制位设置0,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;
(7)主机产生并发送起始信号给从机,然后将控制命令写入从机设备,其中读写控制位设置0,控制命令的写入高位在前低位在后;

I2C页写操作:

     页写操作中,主机一次可向从机写入多字节数据。

基于 I2C 协议的 AD实验(附代码)_第5张图片

页写操作时序图(单字节存储地址)

基于 I2C 协议的 AD实验(附代码)_第6张图片

页写操作时序图(2字节存储地址)

       2字节写操作流程与单字节类似,这里不过的说明。所有 I2C 设备均支持单字节数据写入操作,但只有部分 I2C 设备支持页写操作;且支持页写操作的设备,一次页写操作写入的字节数不能超过设备单页包含的存储单元数。


I2C 随机读操作(单字节数据的读取):

基于 I2C 协议的 AD实验(附代码)_第7张图片

随机读操作时序图(单字节存储地址)

基于 I2C 协议的 AD实验(附代码)_第8张图片

随机读操作时序图(2字节存储地址)

单字读操作流程如下:(先写再读)

(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号
(7) 主机向从机发送控制命令, 读写控制位设置为高电平,表示对从机进行数据读操作;
(8) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(9) 数据接收完成后,主机产生一个时钟的高电平无应答信号;
(10) 主机向从机发送停止信号,单字节读操作完成。

I2C 顺序读操作:
     I2C 顺序读操作就是对寄存器或存储单元数据的顺序读取。假如要读取 n 字节连续数据,只需写入要读取第一个字节数据的存储地址,就可以实现连续 n 字节数据的顺序读取。

基于 I2C 协议的 AD实验(附代码)_第9张图片

 顺序读操作时序图(单字节存储地址)

基于 I2C 协议的 AD实验(附代码)_第10张图片

  顺序读操作时序图(2字节存储地址)

      2字节读操作流程与单字节类似,这里再的说明。

3. I2C 协议整体时序图

基于 I2C 协议的 AD实验(附代码)_第11张图片

       由图可知, I2C 协议整体时序图分为 4 个部分,图中标注的①②③④表示 I2C 协议的 4个状态,分别为“总线空闲状态”、“起始信号”、“数据读/写状态”和“停止信号”,针对这 4 个状态,做一下详细介绍。
       (1) 图中标注①表示“总线空闲状态”,在此状态下串口时钟信号 SCL 和串行数据信号 SDA 均保持高电平,此时无 I2C 设备工作。
       (2) 图中标注②表示“起始信号” ,在 I2C 总线处于“空闲状态”时, SCL 依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有 I2C 设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。
       (3) 图中标注③表示“数据读/写状态” , “数据读/写状态” 时序图具体见下图。

基于 I2C 协议的 AD实验(附代码)_第12张图片

 数据读写时序图

       I2C 通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分。
       当主机向从机进行指令或数据的写入时,串行数据线 SDA 上的数据在串行时钟 SCL为高电平时写入从机设备,每次只写入一位数据;串行数据线 SDA 中的数据在串行时钟SCL 为低电平时进行数据更新,以保证在 SCL 为高电平时采集到 SDA 数据的稳定状态。
       当一个完整字节的指令或数据传输完成,从机设备正确接收到指令或数据后,会通过拉低SDA 为低电平,向主机设备发送单比特的应答信号,表示数据或指令写入成功。 若从机正确应答,可以结束或开始下一字节数据或指令的传输,否则表明数据或指令写入失败,主机就可以决定是否放弃写入或者重新发起写入。
       (4) 图中标注④表示“停止信号” ,完成数据读写后,串口时钟 SCL 保持高电平,当串口数据信号 SDA 产生一个由低电平转为高电平的上升沿时, 产生一个停止信号, I2C 总线跳转回“总线空闲状态”。


1.3 PCF8591芯片简介

        PCF8591 是一款单片集成、 单独供电、低功耗 8 位 CMOS 数据采集设备,具有四个模拟输入,一个模拟输出和一个串行 I2C 总线接口。 三个地址引脚 A0, A1 和 A2 用于硬件地址编码,最多可支持 8 个设备同时连接到 I2C 总线。芯片的输入输出地址、控制信号和数据信息通过两线双向 I2C 总线串行传输。

1.3.1 引脚信息

基于 I2C 协议的 AD实验(附代码)_第13张图片

1.3.2 功能描述

1. 地址信息

       与其他 I2C 通讯设备相同, FPGA 通过向设备发送有效地址,可以激活 I2C 总线系统中的每个 PCF8591 设备。地址由固定部分和可编程部分组成,在 I2C 总线协议中,地址始终作为起始条件之后的第一个字节发送, 地址字节的最后一位是读/写位,它设置后续数据传输的方向。

基于 I2C 协议的 AD实验(附代码)_第14张图片

2. 控制字

       发送到 PCF8591 设备的第二个字节存储在其控制寄存器中,是控制设备功能的必需字节。

基于 I2C 协议的 AD实验(附代码)_第15张图片

      控制字的第6位用于使能模拟输出(高有效),第5和第4位用于模拟输入编程。当第5位和 第4位为00时,AIN0至AIN3为四个单端输入;为01时AIN0至AIN3为三个差分输入;为10时AIN0 至AIN3为两单端一差分输入;为11时AIN0至AIN3为两差分输入。我们这次实验使用的是四个单 端输入(00)。
      第2位为自增标志位,高有效。如果自增标志位置1,每次AD转换后通道号将自 动增加。如果选择一个不存在的输入通道将导致分配到最高可用的通道号,此时如果自增标志 有效,那么下一个被选择的通道将总是通道0。第1位和第0位用于选择通道号:00为通道0;01 为通道1;10为通道2;11为通道3。第7位和第3位是预留未来使用的,必须设置为0。上电复位 后,控制寄存器所有位为0。

3. 数模转换

       发送到 PCF8591 器件的第三个字节存储在 DAC 数据寄存器中,并使用片上数模转换器转换为相应的模拟电压。数模转换器由电阻分压器链组成,该电阻分压器链通过 256 个抽头和选择开关连接到外部基准电压。抽头解码器将这些抽头之一切换到 DAC 输出线,

基于 I2C 协议的 AD实验(附代码)_第16张图片

       模拟输出电压由自动归零的单位增益放大器缓冲。设置控制寄存器的模拟输出使能标志可打开或关闭此缓冲放大器。 在激活状态下,输出电压将保持到发送另一个数据字节为止。模拟输出 AOUT 的输出电压的公式,如图 48-5 所示;数模转换序列波形,如下图。

基于 I2C 协议的 AD实验(附代码)_第17张图片

 数模转换(写)序列波形:

基于 I2C 协议的 AD实验(附代码)_第18张图片       如上图所示,IIC的SDA数据线 需要传递:1.PCF8591器件地址8’b1001_0000;2.控制字,8’b0XXX_0XXX,及一字节的数据。

4. 模数转换

        模数转换器使用逐次逼近转换技术。 在模数转换周期中,暂时使用片上数模转换器和高增益比较器。向 PCF8591 器件发送有效的读取模式地址后,始终执行模数转换周期。模数转换周期在应答时钟脉冲的后沿触发,并在传输前一转换结果的同时执行,具体见下图。

 模数换(读)序列波形:
基于 I2C 协议的 AD实验(附代码)_第19张图片

       如上图所示,IIC的SDA数据线仅需要传递:1.PCF8591器件地址8’b1001_0001,及一字节的数据。


2. 实验

      实验目标:   使用板载 AD/DA 芯片 PCF8591 测量自电位器输入的模拟信号转为数字信号的数据,并在电脑上显示。

最终实验效果如下:

基于 I2C 协议的 AD实验(附代码)_第20张图片

2.1 硬件资源

基于 I2C 协议的 AD实验(附代码)_第21张图片

 PCF8591 实物图

基于 I2C 协议的 AD实验(附代码)_第22张图片

PCF8591 原理图

2.2 模块框图

基于 I2C 协议的 AD实验(附代码)_第23张图片

2.3 程序设计

2.3.1 工程整体框图

基于 I2C 协议的 AD实验(附代码)_第24张图片

2.3.2 I2C驱动模块

1. 模块框图

     I2C 驱动模块的功能是按照 I2C 协议对PCF8591芯片执行数据读写操作。

基于 I2C 协议的 AD实验(附代码)_第25张图片

状态机示意图: 

基于 I2C 协议的 AD实验(附代码)_第26张图片

输入输出信号功能简介:

基于 I2C 协议的 AD实验(附代码)_第27张图片

2. 波形图分析:

写数据:

基于 I2C 协议的 AD实验(附代码)_第28张图片

基于 I2C 协议的 AD实验(附代码)_第29张图片

 读数据:

基于 I2C 协议的 AD实验(附代码)_第30张图片

基于 I2C 协议的 AD实验(附代码)_第31张图片

 3. RTL代码设计

`timescale  1ns/1ns

module  i2c_ctrl
#(
    parameter   DEVICE_ADDR     =   7'b1010_000     ,   //i2c设备地址
    parameter   SYS_CLK_FREQ    =   26'd50_000_000  ,   //输入系统时钟频率     50MHZ
    parameter   SCL_FREQ        =   18'd250_000         //i2c设备scl时钟频率   250khz
)
(
    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 define
localparam  CNT_CLK_MAX     =   (SYS_CLK_FREQ/SCL_FREQ) >> 2'd3   ;   //cnt_clk计数器计数最大值  (SYS_CLK_FREQ/SCL_FREQ) = 200,  25 = 200/(2^3)

localparam  CNT_START_MAX   =   8'd100; //cnt_start计数器计数最大值

localparam  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设备读出数据


// 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)  //0-24
        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 <= 2'd3 || cnt_i2c_clk_en == 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'b;
            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'd)
                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

2.3.3 数据生成模块

基于 I2C 协议的 AD实验(附代码)_第32张图片

1. 波形图分析: 

基于 I2C 协议的 AD实验(附代码)_第33张图片

2. RTL代码编写

`timescale  1ns/1ns

module  pcf8591_ad
(
    input   wire            sys_clk     ,  
    input   wire            sys_rst_n   ,  
    input   wire            i2c_end     ,   //i2c设备一次读/写操作完成
    input   wire    [7:0]   rd_data     ,   //输出i2c设备读取数据

    output  reg             rd_en       ,   //输入i2c设备读使能信号
    output  reg             i2c_start   ,   //输入i2c设备触发信号
    output  reg     [15:0]  byte_addr   ,   //输入i2c设备字节地址
    output  wire    [7:0]   po_data     ,        
    output  wire             po_flag         
);

parameter   CTRL_DATA   =   8'b0100_0000;   //AD/DA控制字
parameter   CNT_WAIT_MAX=   18'd6_9999  ;   //采样间隔计数最大值
parameter   IDLE        =   3'b001,
            AD_START    =   3'b010,
            AD_CMD      =   3'b100;

//wire  define
//wire    [31:0]  data_reg/* synthesis keep */;   //数码管待显示数据缓存

//reg   define
reg     [17:0]  cnt_wait;   //采样间隔计数器
reg     [4:0]   state   ;   //状态机状态变量
reg     [7:0]   ad_data ;   //AD数据


assign  po_flag = i2c_end ;

//cnt_wait:采样间隔计数器
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_wait    <=  18'd0;
    else    if(state == IDLE)
        if(cnt_wait == CNT_WAIT_MAX)
            cnt_wait    <=  18'd0;
        else
            cnt_wait    <=  cnt_wait + 18'd1;
    else
        cnt_wait    <=  18'd0;

//state:状态机状态变量
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        state   <=  IDLE;
    else
        case(state)
            IDLE:
                if(cnt_wait == CNT_WAIT_MAX)
                    state   <=  AD_START;
                else
                    state   <=  IDLE;
            AD_START:
                state   <=  AD_CMD;
            AD_CMD:
                if(i2c_end == 1'b1)
                    state   <=  IDLE;
                else
                    state   <=  AD_CMD;
            default:state   <=  IDLE;
        endcase

//i2c_start:输入i2c设备触发信号
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        i2c_start   <=  1'b0;
    else    if(state == AD_START)
        i2c_start   <=  1'b1;
    else
        i2c_start   <=  1'b0;

//rd_en:输入i2c设备读使能信号
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        rd_en   <=  1'b0;
    else    if(state == AD_CMD)
        rd_en   <=  1'b1;
    else
        rd_en   <=  1'b0;

//byte_addr:输入i2c设备字节地址
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        byte_addr   <=  16'b0;
    else
        byte_addr   <=  CTRL_DATA;

//ad_data:AD数据
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        ad_data <=  8'b0;
    else    if(i2c_end == 1'b1) //(state == AD_CMD) && (i2c_end == 1'b1))
        ad_data <=  rd_data;

//data_reg:数码管待显示数据缓存
//assign  data_reg = ((ad_data * 3300) >> 4'd8);

//po_data:数码管待显示数据
assign  po_data = ad_data[7:0];

endmodule

2.3.4 串口接收模块

前面文章详细讲解过,这里不在复述。

总结: 

(1) 根据I2C时序将读写过程,使用状态机进行描述。(2) 根据需求定义中间变量。

你可能感兴趣的:(FPGA_拓展练习,fpga开发,嵌入式硬件)