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

news2024/12/23 10:29:21

目录

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 通讯设备之间的常用连接方式,具体见下图。

物理层有如下特点:

(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 时,表示主机要对从机进行数据读出操作

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

存储地址

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

2. I2C 读/写操作

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

I2C 单字节写操作:

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

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

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

I2C页写操作:

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

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

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

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


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

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

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

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

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

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

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

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

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

3. I2C 协议整体时序图

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

 数据读写时序图

       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 引脚信息

1.3.2 功能描述

1. 地址信息

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

2. 控制字

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

      控制字的第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 输出线,

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

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

       如上图所示,IIC的SDA数据线 需要传递:1.PCF8591器件地址8’b1001_0000;2.控制字,8’b0XXX_0XXX,及一字节的数据。

4. 模数转换

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

 模数换(读)序列波形:

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


2. 实验

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

最终实验效果如下:

2.1 硬件资源

 PCF8591 实物图

PCF8591 原理图

2.2 模块框图

2.3 程序设计

2.3.1 工程整体框图

2.3.2 I2C驱动模块

1. 模块框图

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

状态机示意图: 

输入输出信号功能简介:

2. 波形图分析:

写数据:

 读数据:

 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 数据生成模块

1. 波形图分析: 

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) 根据需求定义中间变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1015816.html

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

相关文章

来看看Python MetaClass元类详解

MetaClass元类&#xff0c;本质也是一个类&#xff0c;但和普通类的用法不同&#xff0c;它可以对类内部的定义&#xff08;包括类属性和类方法&#xff09;进行动态的修改。可以这么说&#xff0c;使用元类的主要目的就是为了实现在创建类时&#xff0c;能够动态地改变类中定义…

Docker网络学习

文章目录 Docker容器网络1.Docker为什么需要网络管理2. Docker网络简介3. 常见的网络类型4. docker 网络管理命令5.两种网络加入差异6.网络讲解docker Bridge 网络docker Host 网络docker Container 网络docker none 网络 Docker容器网络 1.Docker为什么需要网络管理 容器的网…

Linux启动速度优化方法总结

文章目录 一、启动耗时统计printk timeinitcall_debugbootgraphbootchartgpio示波器 二、内核优化方法kernel压缩方式加载位置内核裁剪预设置lpj数值initcall优化内核initcall_module并行减少pty/tty个数内核module 三、其他优化ubootXIP 四、总结 要对Linux系统启动速度进行优…

Discuz论坛网站标题栏Powered by Discuz!版权信息如何去除或是修改?

当我们搭建好DZ论坛网站后&#xff0c;为了美化网站&#xff0c;想把标题栏的Powered by Discuz&#xff01;去除或是修改&#xff0c;应该如何操作呢&#xff1f;今天飞飞和你分享&#xff0c;在操作前务必把网站源码和数据库都备份到本地或是网盘。 Discuz的版权信息存在两处…

七、安卓手机环境检测软件分享

系列文章目录 第一章 安卓aosp源码编译环境搭建 第二章 手机硬件参数介绍和校验算法 第三章 修改安卓aosp代码更改硬件参数 第四章 编译定制rom并刷机实现硬改(一) 第五章 编译定制rom并刷机实现硬改(二) 第六章 不root不magisk不xposed lsposed frida原生修改定位 第七章 安卓…

生信教程|最大似然系统发育推断

动动发财的小手&#xff0c;点个赞吧&#xff01; 简介 顾名思义&#xff0c;最大似然系统发育推断旨在找到进化模型的参数&#xff0c;以最大化观察手头数据集的可能性。模型参数包括树的拓扑结构及其分支长度&#xff0c;还包括推理中假设的替代模型&#xff08;例如HKY或GTR…

09MyBatisX插件

MyBatisX插件 在真正开发过程中对于一些复杂的SQL和多表联查就需要我们自己去编写代码和SQL语句,这个时候可以使用MyBatisX插件帮助我们简化开发 安装MyBatisX插件: File -> Settings -> Plugins -> 搜索MyBatisx插件搜索安装然后重启IDEA 跳转文件功能 由于一个项…

Linux用户和用户组信息管理

文章目录 用户管理用户密码信息/etc/shadow详解 ⽤户组的管理(切换到root)/etc/group 内容详解用户组的添加用户组的删除用户组的查看用户组的修改 ⽤户组和⽤户的关联 用户管理 ⽤户的管理(/etc/passwd) ⽤户的添加&#xff08;useradd&#xff09; ⽤户的删除&#xff08;us…

Spring boot原理

起步依赖 Maven的传递依赖 自动配置 Springboot的自动配置就是当spring容器启动后&#xff0c;一些配置类、bean对象就自动存入到IOC容器中&#xff0c;不需要我们手动去声明&#xff0c;从而简化了开发&#xff0c;省去了繁琐的配置操作。 自动配置原理&#xff1a; 方案一…

MySQL——一、安装以及配置

MySQL 一、windows下的安装以及配置常规方法二、windows下的安装以及配置简单方法三、Linux下的数据库安装以及配置 一、windows下的安装以及配置常规方法 准备工具&#xff1a; 链接&#xff1a;https://pan.xunlei.com/s/VNeRbKScnTd6MbgZ-jwubY6-A1?pwdtaxz# 这里我准备的…

sync.Once-保证运行期间的某段代码只会执行一次

初入门径 sync.Once提供了保证某个操作只被执行一次的功能,其最常应用于单例模式之下,例如初始化系统配置、保持数据库唯一连接,以及并发访问只需要初始化一次的共享资源。 单例模式有懒汉模式和饿汉模式两种 饿汉模式 顾名思义就是比较饥饿&#xff0c;所以一上来(服务启动时)…

【看好了】如何使用fiddler实现手机抓包,Filters过滤器!

一、Fiddler与其他抓包工具的区别  1、Firebug虽然可以抓包&#xff0c;但是对于分析http请求的详细信息&#xff0c;不够强大。模拟http请求的功能也不够&#xff0c;且firebug常常是需要“无刷新修改”&#xff0c;如果刷新了页面&#xff0c;所有的修改都不会保存&#xff…

CDH集群部署

文章目录 1. 资源准备2. 部署 Mariadb 数据库3. 安装CM服务4. 安装数据节点5. 登录CM系统 1. 资源准备 准备好CDH安装包资源&#xff0c;官方网站下载需要账号&#xff0c;如果没有账号可以去网上到处搜搜。主要涉及到的资源有&#xff1a; cloudera-manager-servercloudera-m…

力扣:100. 相同的树(Python3)

题目&#xff1a; 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣…

Mars3d插件参考开发教程并在相关页面引入

问题场景&#xff1a; 1.在使用Mars3d热力图功能时&#xff0c;提示mars3d.layer.HeatLayer is not a constructor 问题原因: 1.mars3d的热力图插件mars3d-heatmap没有安装引用。 解决方案&#xff1a; 1.参考开发教程&#xff0c;找到相关的插件库&#xff1a;Mars3D 三维…

【LeetCode-简答题】242. 有效的字母异位词

文章目录 题目方法一&#xff1a;数组存放&#xff1a;方法二&#xff1a;哈希存放 题目 方法一&#xff1a;数组存放&#xff1a; class Solution {public boolean isAnagram(String s, String t) {int[] s1 new int[26];int[] t1 new int[26];for(int i 0; i< s.lengt…

【LeetCode-简单题】350. 两个数组的交集 II

文章目录 题目方法一&#xff1a;哈希表方法二&#xff1a;双指针 题目 方法一&#xff1a;哈希表 用哈希表记录第一个数组的每个数和每个数的出现次数再遍历第二个数组&#xff0c;如果哈希表中有这个数&#xff0c;并且次数还不为0&#xff0c;说明是交集元素&#xff0c;加…

RocketMQ实践与原理分析(Docker安装RocketMQ)

前言 QBM之前使用的消息中间件是ActiveMQ&#xff0c;后续需要升级为RocketMQ。 MQ广泛应用于很多业务场景中&#xff0c;主要的作用 异步解耦削峰… 常用MQ中间件对比&#xff0c;参考官方文档&#xff1a;https://rocketmq.apache.org/zh/docs/4.x/introduction/03whatis…

Android查看公钥与MD5

参考&#xff1a;填写App特征信息_备案-阿里云帮助中心 安卓应用获取App特征信息指导 包名、公钥和签名MD5获取方式有多种&#xff0c;本文以使用JadxGUI工具获取为例。 下载JadxGUI工具&#xff1a;GitHub - skylot/jadx: Dex to Java decompiler下载安装完成后&#xff0c;使…

饲料添加剂 微生物 屎肠球菌

声明 本文是学习GB 7300.503-2023 饲料添加剂 第5部分&#xff1a;微生物 屎肠球菌. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件规定了饲料添加剂屎肠球菌的技术要求、采样、检验规则、标签、包装、运输、贮存和保质 期&#xff0…