FPGA开发(2)——IIC通信

1、IIC通信理论知识


I2C 通讯协议(Inter-Integrated Circuit)是由 Philips 公司开发的一种简单、双向二线制同步串行总线,只需要两根线即可在连接于总线上的器件之间传送信息。
I2C 通讯协议和通信接口在很多工程中有广泛的应用,如数据采集领域的串行 AD,图像处理领域的摄像头配置,工业控制领域的 X 射线管配置等等。除此之外,由于 I2C 协议占用引脚特别少,硬件实现简单,可扩展型强,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。

IIC物理层框图如下图所示。
在这里插入图片描述
(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4) 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6) 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
(7) 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

IIC协议层时序图如下图所示。
在这里插入图片描述
(1) 图中标注①表示“总线空闲状态”,在此状态下串口时钟信号 SCL 和串行数据信号 SDA 均保持高电平,此时无 I2C 设备工作。
(2) 图中标注②表示“起始信号”,在 I2C 总线处于“空闲状态”时,SCL 依旧保持高电平时, SDA 出现由高电平转为低电平的下降沿,产生一个起始信号,此时与总线相连的所有 I2C 设备在检测到起始信号后,均跳出空闲状态,等待控制字节的输入。
(3) 图中标注③表示“数据读/写状态”,“数据读/写状态”时序图具体见下图。
在这里插入图片描述
I2C 通讯设备的通讯模式是主从通讯模式,通讯双方有主从之分。
当主机向从机进行指令或数据的写入时,串行数据线 SDA 上的数据在串行时钟 SCL为高电平时写入从机设备,每次只写入一位数据;串行数据线 SDA 中的数据在串行时钟SCL 为低电平时进行数据更新,以保证在 SCL 为高电平时采集到 SDA 数据的稳定状态。
当一个完整字节的指令或数据传输完成,从机设备正确接收到指令或数据后,会通过拉低 SDA 为低电平,向主机设备发送单比特的应答信号,表示数据或指令写入成功。若从机正确应答,可以结束或开始下一字节数据或指令的传输,否则表明数据或指令写入失败,主机就可以决定是否放弃写入或者重新发起写入。
(4) 图中标注④表示“停止信号”,完成数据读写后,串口时钟 SCL 保持高电平,当串口数据信号 SDA 产生一个由低电平转为高电平的上升沿时,产生一个停止信号,I2C 总线跳转回“总线空闲状态”。

IIC器件地址与存储地址
每个IIC通讯的器件都有自己的地址,这在出厂就被设定了,用户无法更改,这个器件地址一般是7位,像0V5640等。这边EEPROM的器件地址是4位,为1010A2A1A0,其中A2A1A0是用户自己根据电平高低设置的,这边开发板都是拉低,所以EEPROM器件地址是1010000,外加写控制0或者读控制1,构成完整的一个字节控制信号。
在这里插入图片描述
存储地址更具寄存器或者存储大小而决定,AT24C64因为有64k的存储空间,需要两个字节的存储地址才可以。
在这里插入图片描述
IIC单字节写操作
对传入从机的控制命令最低位读写控制位写入不同数据值,主机可实现对从机的读/写操作,读写控制位为 0 时,表示主机要对从机进行数据写入操作;读写控制位为 1 时,表示主机要对从机进行数据读出操作。对于 I2C 协议的读/写操作,我们将其分为读操作和写操作两部分进行讲解。
如下图所示分别是单字节存储的时序图,分别画了单字节存储地址和双字节存储地址。
在这里插入图片描述
在这里插入图片描述
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始单字节数据的写入;
(7) 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单字节数据写入完成。

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

2、IIC实现EEPROM读写


本次实验完成对AT24C64这种EEPROM的读写控制,往EEPROM中写入十个数据,之后读取十个数据到数码管进行显示。整个工程主要包括了以下的模块:按键滤波模块、eeprom读写控制模块、eeprom控制模块、数码管显示模块。我们分别对这几个模块进行设计和验证。

在这里插入图片描述
在这里插入图片描述
按键滤波模块
按键消抖模块主要作用就是接收外部按键按下,之后延时20ms后,判断按键按下信息,之后给出标志位信号。其框图和时序图如下图所示。
在这里插入图片描述
在这里插入图片描述
直接给出其源代码如下图,因为模块比较简单,这边就不在进行仿真。

module key_control(
    input clk,
    input rst_n,
    input key_in,
    output reg key_flag
);

parameter  CNT_20ms = 20'd999_999 ;

reg [19:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt<=20'd0;
    end
    else if(key_in==1'b0 && cnt<CNT_20ms)begin
      cnt<=cnt+1'b1;
    end
    else if(key_in==1'b1)begin
      cnt<=1'b0;
    end
    else cnt=cnt;
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      key_flag<=1'b0;
    end
    else if(cnt==20'd999_998)begin
      key_flag<=1'b1;
    end
    else key_flag<=1'b0;
end

endmodule

数码管显示模块
数码管显示模块主要就是接收从eeprom读出的数据,送到fifo中进行缓存,之后在数码管显示。主要包含了时钟、复位、数据、数码管位选、数码管信号这些端口。
在这里插入图片描述
数码管的代码如下图所示,这边也不进行仿真,只做一些讲解。cnt进行周期的计数,计数到4999后输出一个flag标志位高电平信号。6位的位选信号收到flag标志位信号后进行移位,对数码管进行动态刷新。
将接收到的数据赋值给number,之后将number数据对应赋给8位的数码管seg。

module smg #(
  parameter  W = 4'd8
)
(
    input clk,
    input rst_n,
    input [W-1:0] data,
    output reg[5:0] sel,
    output reg[7:0] seg 
);

reg  [3:0] number;
wire [3:0] data0;
wire [3:0] data1;

assign data0=data[3:0];
assign data1=data[7:4];

reg [12:0] cnt;
reg flag;

always @(posedge clk or negedge rst_n) begin
  if(~rst_n)begin
    cnt<=13'd0;
  end
  else if(cnt==13'd4999)begin
    cnt<=13'd0;
  end
  else cnt<=cnt+1'b1;
end

always @(posedge clk or negedge rst_n) begin
  if(~rst_n)begin
    flag<=1'b0;
  end
  else if(cnt==13'd4999)begin
    flag<=1'b1;
  end
  else flag<=1'b0;
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      sel<=6'b111_110;
    end
    else if(flag==1)begin
      sel<={sel[4:0],sel[5]};
    end
    else sel<=sel;
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      number<=4'd0;
    end
    else begin
      case(sel)
        6'b111_110:number<=data0;
        6'b111_101:number<=data1;
        6'b111_011:number<=4'd0;
        6'b110_111:number<=4'd0;
        6'b101_111:number<=4'd0;
        6'b011_111:number<=4'd0;
        default:number<=4'd0;
      endcase
    end
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      seg<=8'd0;
    end
    else begin
      case(number)
        4'd0:seg<=8'b1100_0000;
        4'd1:seg<=8'b1111_1001;
        4'd2:seg<=8'b1010_0100;
        4'd3:seg<=8'b1011_0000;
        4'd4:seg<=8'b1001_1001;
        4'd5:seg<=8'b1001_0010;
        4'd6:seg<=8'b1000_0010;
        4'd7:seg<=8'b1111_1000;
        4'd8:seg<=8'b1000_0000;
        4'd9:seg<=8'b1001_0000;
        4'd10:seg<=8'b1000_1000;
        4'd11:seg<=8'b1000_0011;
        4'd12:seg<=8'b1100_0110;
        4'd13:seg<=8'b1010_0001;   
        4'd14:seg<=8'b1000_0110;
        4'd15:seg<=8'b1000_1110; 
        default:seg<=8'b1100_0000;
      endcase
    end
end


endmodule 

IIC控制模块
该模块主要包含了8个输入信号,5个输出信号。
在这里插入图片描述
在这里插入图片描述
IIC读写操作设计到流程控制,这边利用状态机来控制整个流程,状态装换图如下图所示。
在这里插入图片描述
给出IIC控制模块读写操作的时序图如下图所示。首先是写操作控制流程图。具体流程:1、cnt_clk进行0-24的循环计数,实现对50M时钟的50分频,得到1MHz的时钟信号,就是i2c_clk。2、cnt_i2c_clk_en信号是时钟计数控制的使能信号,为下面控制SCL和SDA做准备,在接收到i2c_start信号后拉高使能信号。当状态机状态为STOP并且cnt_i2c_clk等于3,并且cnt_bit等于3时拉低使能信号。3、cnt_i2c_clk在cnt_i2c_clk_en使能信号拉高时进行计数,计数到3后清零,反复进行。4、cnt_bit是对写入的位数进行计数,具体操作可以看书序图。5、state是状态机的状态,这部分跳转比较复杂,可以对照代码和时序图查看。6、ACK是应答信号,在发送完一个字节数据时会接收到一位的低电平信号,在ACK1-5时进行判断,其他情况都是高电平。7、iic_sda_reg和rd_data_reg主要是对信号进行缓存,这部分可以对照波形图和代码查看。8、最后完成sda和scl信号的代码编写。
在这里插入图片描述
在这里插入图片描述
最后的iic控制模块代码如下图所示。

module i2c_control
#(
    parameter  DEVICE_ADDR = 7'b1010_000,
    parameter  SYS_FREQ=26'd50_000_000,
    parameter  I2C_FREQ=18'd250_000
)
(
    input clk,
    input rst_n,
    input wr_en,
    input rd_en,
    input i2c_start,
    input addr_num,
    input [15:0] byte_addr,
    input [7:0] wr_data,

    output reg i2c_clk,
    output reg i2c_end,
    output reg [7:0] rd_data,
    output reg i2c_scl,
    inout wire i2c_sda
);

localparam IDLE=4'd0;
localparam START_1=4'd1;
localparam SEND_D_ADDR=4'd2;
localparam ACK_1=4'd3;
localparam SEND_B_ADDR_H=4'd4;
localparam ACK_2=4'd5;
localparam SEND_B_ADDR_L=4'd6;
localparam ACK_3=4'd7;
localparam WR_DATA=4'd8;
localparam ACK_4=4'd9;
localparam STOP=4'd10;
localparam START_2=4'd11;
localparam SEND_RD_ADDR=4'd12;
localparam ACK_5=4'd13;
localparam RD_DATA=4'd14;
localparam N_ACK=4'd15;

reg [7:0] cnt_clk;
reg cnt_i2c_clk_en;
reg [3:0] state;
reg [1:0] cnt_i2c_clk;
reg [2:0] cnt_bit;
reg ack;
reg i2c_sda_reg;
reg [7:0] rd_data_reg;

wire sda_en;
wire sda_in;

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_clk<=8'd0;
    end
    else if(cnt_clk==((SYS_FREQ/I2C_FREQ)>>2'd3)-1'b1)begin
      cnt_clk<=8'd0;
    end
    else cnt_clk<=cnt_clk+1'b1;
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      i2c_clk<=1'b0;
    end
    else if(cnt_clk==((SYS_FREQ/I2C_FREQ)>>2'd3)-1'b1)begin
      i2c_clk<=~i2c_clk;
    end
    else i2c_clk<=i2c_clk;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_i2c_clk_en<=1'b0;
    end
    else if(i2c_start==1'b1)begin
      cnt_i2c_clk_en<=1'b1;
    end
    else if(state==STOP && cnt_i2c_clk==2'd3 && cnt_bit==3'd3)begin
      cnt_i2c_clk_en<=1'b0;
    end 
    else cnt_i2c_clk_en<=cnt_i2c_clk_en;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_i2c_clk<=2'd0;
    end
    else if(cnt_i2c_clk_en==1'b1)begin
      if(cnt_i2c_clk==2'd3)begin
        cnt_i2c_clk<=2'd0;
      end
      else cnt_i2c_clk<=cnt_i2c_clk+1'b1;
    end
    else cnt_i2c_clk<=2'd0;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_bit<=3'd0;
    end
    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)begin
      cnt_bit<=3'd0;
    end 
    else if(cnt_bit==3'd7 && cnt_i2c_clk==2'd3)begin
      cnt_bit<=3'd0;
    end
    else if(cnt_i2c_clk==3'd3)begin
      cnt_bit<=cnt_bit+1'b1;
    end
end

always@(posedge i2c_clk or negedge rst_n)begin
    if(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
end

always@(*)begin
    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,STOP:
            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
end

always@(*)begin
    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
end

always@(*)begin
    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[3'd7 - 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
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
        rd_data<=8'd0;
    end
    else if(state==RD_DATA && cnt_bit==3'd7 && cnt_i2c_clk==3'd3)begin
        rd_data<=rd_data_reg;
    end
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      i2c_end<=1'b0;
    end
    else if(state==STOP && cnt_i2c_clk==2'd3 && cnt_bit==3'd3)begin
      i2c_end<=1'b1;
    end
    else i2c_end<=1'b0;
end

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;

assign  i2c_sda = (sda_en == 1'b1) ? i2c_sda_reg : 1'bz;
assign  sda_in = i2c_sda;

endmodule

编写完控制代码后对其进行modelsim仿真控制。仿真激励文件如下图所示,以及仿真结果图如下图所示,对照仿真结果和画的时序图,测试结果是没问题的。蓝色线是高阻态,可以看做是应答信号。

`timescale  1ns / 1ps

module tb_i2c_control;

// i2c_control Parameters
parameter PERIOD         = 10            ;
parameter DEVICE_ADDR    = 7'b1010_000   ;
parameter SYS_FREQ       = 26'd50_000_000;
parameter I2C_FREQ       = 18'd250_000   ;

// i2c_control Inputs
reg   clk                                  = 0 ;
reg   rst_n                                = 0 ;
reg   wr_en                                = 1 ;
reg   rd_en                                = 0 ;
reg   i2c_start                            = 0 ;
reg   addr_num                             = 1 ;
reg   [15:0]  byte_addr                    = 16'h005a ;
reg   [7:0]  wr_data                       = 8'haa ;

// i2c_control Outputs
wire  i2c_clk                              ;
wire  i2c_end                              ;
wire  [7:0]  rd_data                       ;
wire  i2c_scl                              ;

// i2c_control Bidirs
wire  i2c_sda                              ;


initial
begin
    forever #(PERIOD/2)  clk=~clk;
end

initial
begin
    #(PERIOD*2) rst_n  =  1;
end

initial
begin
    #(PERIOD*10)
    i2c_start=1;
    #(PERIOD*100)
    i2c_start=0;
end

i2c_control #(
    .DEVICE_ADDR   ( DEVICE_ADDR   ),
    .SYS_FREQ      ( SYS_FREQ      ),
    .I2C_FREQ      ( I2C_FREQ      )
)
 u_i2c_control (
    .clk                     ( clk               ),
    .rst_n                   ( rst_n             ),
    .wr_en                   ( wr_en             ),
    .rd_en                   ( rd_en             ),
    .i2c_start               ( i2c_start         ),
    .addr_num                ( addr_num          ),
    .byte_addr               ( byte_addr  [15:0] ),
    .wr_data                 ( wr_data    [7:0]  ),

    .i2c_clk                 ( i2c_clk           ),
    .i2c_end                 ( i2c_end           ),
    .rd_data                 ( rd_data    [7:0]  ),
    .i2c_scl                 ( i2c_scl           ),

    .i2c_sda                 ( i2c_sda           )
);

initial
begin

end

endmodule

在这里插入图片描述
IIC读写控制模块
IIC读写控制模块主要包括了7个输入信号和6个输出信号。
在这里插入图片描述
在这里插入图片描述
下图所示是IIC读写控制模块的具体时序图。
1、cnt_wr和cnr_rd分别是写和读的计数器,其主要作用是模块接收到,read和write信号后,延时一段时间,输出一个较长时间的写和读控制信号write_vaild和read_vaild。这边加延迟是考虑到按键模块使用的时钟是50M,而读I2C控制是1M的时钟,所以延时一段时间使得1M时钟可以读取到写标志和读标志。
2、wr_en和rd_en分别是写使能和读使能,写使能和读使能在接收到标志信号高电平时拉高。写使能在i2c_end等于1且wr_i2c_data_num等于9时拉低,表示写操作结束,读操作也是类似的。
3、cnt_start是计数模块,控制每一位读取的控制间隔,这边让他在读使能有效或者写使能有效时进行累加至4999后清零。
4、wr_i2c_data_num写入的数据计数,当写使能有效并且i2c_end等于1时,代表写入一个字节完成,计数加1。
5、i2c_start是和i2c_end相对应,代表了一个字节信号开始传输。当写使能或者读使能有效并且cnt_start计数到4999时拉高,其他时间为0。
6、wr_data和byte_addr分别是要写入的数据和地址,这个和wr_i2c_data_num是对应的。用组合逻辑控制实现亦可。
7、fifo_rd_vaild是读fifo的使能信号。当data_num计数为10时,信号拉高,当data_num等于0并且cnt_wait计数到最大值时信号拉低,读结束。
8、cnt_wait是在读取fifo过程中的延时,其主要作用是让fifo读的慢一些,能看出在数码管显示的变化。
9、fifo_rd_en是读fifo的标志位,在拉高时进行读。
10、详细介绍可以看野火的文档。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后根据上面的时序图可以编写verilog代码。

module i2c_rw_control(
    input clk,
    input rst_n,
    input i2c_clk,
    input write,
    input read,
    input i2c_end,
    input [7:0] rd_data,
    
    output reg wr_en,
    output reg rd_en,
    output reg i2c_start,
    output reg [15:0] byte_addr,
    output reg [7:0] wr_data,
    output wire [7:0] fifo_rd_data
);

parameter   DATA_NUM        =   8'd10       ,
            CNT_START_MAX   =   13'd5000    , 
            CNT_WR_RD_MAX   =   8'd200      ,
            CNT_WAIT_MAX    =   28'd500_000 ;

reg [7:0] cnt_wr;
reg [7:0] cnt_rd;
reg write_vaild;
reg read_vaild;
reg fifo_rd_vaild;
reg [7:0] wr_i2c_data_num;
reg [7:0] rd_i2c_data_num;
reg [12:0] cnt_start;
wire [7:0] data_num;
reg [27:0] cnt_wait;
reg fifo_rd_en;
reg [7:0] rd_data_num;


//写操作
/*
always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_wr<=8'd0;
    end
    else if(write==1'b1 || cnt_wr!=8'd0)begin
      cnt_wr<=cnt_wr+1'b1;
    end
    else if(cnt_wr==CNT_WR_RD_MAX)begin
      cnt_wr<=8'd0;
    end
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      write_vaild<=1'b0;
    end
    else if(cnt_wr>8'd0 && cnt_wr<CNT_WR_RD_MAX)begin
      write_vaild<=1'b1;
    end
    else write_vaild<=1'b0;
end
*/

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_wr<=8'd0;
    end
    else if(write_vaild==1'b1)begin
      cnt_wr<=cnt_wr+1'b1;
    end
    else if(write_vaild==1'b0)begin
      cnt_wr<=8'd0;
    end
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      write_vaild<=1'b0;
    end
    else if(cnt_wr == (CNT_WR_RD_MAX-1'b1))begin
      write_vaild<=1'b0;
    end
    else if(write==1'b1)begin
      write_vaild<=1'b1;
    end
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      wr_en<=1'b0;
    end
    else if(write_vaild==1'b1)begin
      wr_en<=1'b1;
    end
    else if(i2c_end==1'b1 && wr_i2c_data_num==DATA_NUM-1'b1 && wr_en==1'b1)begin
      wr_en<=1'b0;
    end
    else wr_en<=wr_en;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_start<=13'd0;
    end
    else if(wr_en==1'b1 || rd_en==1'b1)begin
      if(cnt_start==CNT_START_MAX-1'b1)begin
        cnt_start<=13'd0;
      end
      else cnt_start<=cnt_start+1'b1;
    end
    else cnt_start<=13'd0;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      wr_i2c_data_num<=8'd0;
    end
    else if(wr_en==1'b1)begin
        if(i2c_end==1'b1)begin
          wr_i2c_data_num<=wr_i2c_data_num+1'b1;
        end
    end
    else wr_i2c_data_num<=8'd0;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      i2c_start<=1'b0;
    end
    else if(rd_en==1'b1 || wr_en==1'b1)begin
      if(cnt_start==CNT_START_MAX-1'b1)begin
        i2c_start<=1'b1;
    end
    else i2c_start<=1'b0;
    end
    else i2c_start<=1'b0;
end

always @(*) begin
  if(wr_en==1'b1)begin
    case(wr_i2c_data_num)
    8'd0:wr_data<=8'ha5;
    8'd1:wr_data<=8'ha6;
    8'd2:wr_data<=8'ha7;
    8'd3:wr_data<=8'ha8;
    8'd4:wr_data<=8'ha9;
    8'd5:wr_data<=8'haa;
    8'd6:wr_data<=8'hab;
    8'd7:wr_data<=8'hac;
    8'd8:wr_data<=8'had;
    8'd9:wr_data<=8'hae; 
    default:wr_data<=8'ha5;
  endcase
  end
  else wr_data<=8'ha5;
end

always @(*) begin
  if(wr_en==1'b1)begin
    case(wr_i2c_data_num)
      8'd0:byte_addr<=16'h005a;
      8'd1:byte_addr<=16'h005b;
      8'd2:byte_addr<=16'h005c;
      8'd3:byte_addr<=16'h005d;
      8'd4:byte_addr<=16'h005e;
      8'd5:byte_addr<=16'h005f;
      8'd6:byte_addr<=16'h0060;
      8'd7:byte_addr<=16'h0061;
      8'd8:byte_addr<=16'h0062;
      8'd9:byte_addr<=16'h0063;
      default:byte_addr<=16'h005a;
    endcase
  end
  else if(rd_en==1'b1)begin
    case(rd_i2c_data_num)
      8'd0:byte_addr<=16'h005a;
      8'd1:byte_addr<=16'h005b;
      8'd2:byte_addr<=16'h005c;
      8'd3:byte_addr<=16'h005d;
      8'd4:byte_addr<=16'h005e;
      8'd5:byte_addr<=16'h005f;
      8'd6:byte_addr<=16'h0060;
      8'd7:byte_addr<=16'h0061;
      8'd8:byte_addr<=16'h0062;
      8'd9:byte_addr<=16'h0063;
      default:byte_addr<=16'h005a;
    endcase
  end
  else byte_addr<=16'h005a;
end

//读操作
/*
always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_rd<=8'd0;
    end
    else if(read==1'b1 || cnt_rd!=8'd0)begin
      cnt_rd<=cnt_rd+1'b1;
    end
    else if(cnt_rd==CNT_WR_RD_MAX)begin
      cnt_rd<=8'd0;
    end
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      read_vaild<=1'b0;
    end
    else if(cnt_rd>8'd0 && cnt_rd<CNT_WR_RD_MAX)begin
      read_vaild<=1'b1;
    end
    else read_vaild<=1'b0;
end
*/

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      cnt_rd<=8'd0;
    end
    else if(read_vaild==1'b1)begin
      cnt_rd<=cnt_rd+1'b1;
    end
    else if(read_vaild==1'b0)begin
      cnt_rd<=8'd0;
    end
end

always @(posedge clk or negedge rst_n) begin
    if(~rst_n)begin
      read_vaild<=1'b0;
    end
    else if(cnt_rd == (CNT_WR_RD_MAX-1'b1))begin
      read_vaild<=1'b0;
    end
    else if(read==1'b1)begin
      read_vaild<=1'b1;
    end
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      rd_en<=1'b0;
    end
    else if(read_vaild==1'b1)begin
      rd_en<=1'b1;
    end
    else if(i2c_end==1'b1 && rd_i2c_data_num==DATA_NUM-1'b1 && rd_en==1'b1)begin
      rd_en<=1'b0;
    end
    else rd_en<=rd_en;
end

always @(posedge i2c_clk or negedge rst_n) begin
    if(~rst_n)begin
      rd_i2c_data_num<=8'd0;
    end
    else if(rd_en==1'b1)begin
        if(i2c_end==1'b1)begin
          rd_i2c_data_num<=rd_i2c_data_num+1'b1;
        end
    end
    else rd_i2c_data_num<=8'd0;
end

/*
always @(posedge i2c_clk or negedge rst_n) begin
  if(~rst_n)begin
    fifo_rd_vaild<=1'b0;
  end
  else if(data_num==DATA_NUM-1'b1 && i2c_end==1'b1)begin
    fifo_rd_vaild<=1'b1;
  end
  else if(data_num==8'd0 && cnt_wait==CNT_WAIT_MAX-1'b1)begin
    fifo_rd_vaild<=1'b0;
  end
  else fifo_rd_vaild<=fifo_rd_vaild;
end
*/
always @(posedge i2c_clk or negedge rst_n) begin
  if(~rst_n)begin
    fifo_rd_vaild<=1'b0;
  end
  else if(data_num==DATA_NUM)begin
    fifo_rd_vaild<=1'b1;
  end
  else if(data_num==8'd0 && cnt_wait==CNT_WAIT_MAX-1'b1)begin
    fifo_rd_vaild<=1'b0;
  end
end

always @(posedge i2c_clk or negedge rst_n) begin
  if(~rst_n)begin
    cnt_wait<=28'd0;
  end
  else if(fifo_rd_vaild==1'b1)begin
    if(cnt_wait==CNT_WAIT_MAX-1'b1)begin
      cnt_wait<=28'd0;
    end
    else cnt_wait<=cnt_wait+1'b1;
  end
  else cnt_wait<=28'd0;
end

always @(posedge i2c_clk or negedge rst_n) begin
  if(~rst_n)begin
    fifo_rd_en<=1'b0;
  end
  else if(cnt_wait==CNT_WAIT_MAX-1'b1 && rd_data_num<DATA_NUM)begin
    fifo_rd_en<=1'b1;
  end
  else fifo_rd_en<=1'b0;
end

always @(posedge i2c_clk or negedge rst_n) begin
  if(~rst_n)begin
    rd_data_num<=8'd0;
  end
  else if(fifo_rd_vaild==1'b1)begin
    if(fifo_rd_en==1'b1)begin
      rd_data_num<=rd_data_num+1'b1;
    end
    else rd_data_num<=rd_data_num;
  end
  else rd_data_num<=8'd0;
end

i2c_fifo u_i2c_fifo(
    .clock ( i2c_clk ),
    .data  ( rd_data  ),
    .rdreq ( fifo_rd_en && fifo_rd_vaild ),
    .wrreq ( rd_en &&  i2c_end),
    .q     ( fifo_rd_data     ),
    .usedw ( data_num )
);

endmodule

不在对这个模块进行仿真验证,直接编写顶层模块来例化上面四个模块,顶层模块代码如下图所示。

module i2c_eeprom(
    input clk,
    input rst_n,
    input key_wr,
    input key_rd,
    
    output scl,
    inout sda,

    output wire [5:0] sel,
    output wire [7:0] seg
);

wire write;
wire read;
wire i2c_clk;
wire i2c_end;
wire [7:0] rd_data;
wire wr_en;
wire rd_en;
wire i2c_start;
wire [15:0] byte_addr;
wire [7:0] wr_data;
wire [7:0] fifo_rd_data;

key_control u_key_control_wr(
    .clk    ( clk    ),
    .rst_n  ( rst_n  ),
    .key_in ( key_wr ),
    .key_flag  ( write  )
);

key_control u_key_control_rd(
    .clk    ( clk    ),
    .rst_n  ( rst_n  ),
    .key_in ( key_rd ),
    .key_flag  ( read  )
);

i2c_rw_control u_i2c_rw_control(
    .clk       ( clk       ),
    .rst_n     ( rst_n     ),
    .i2c_clk   ( i2c_clk   ),
    .write     ( write     ),
    .read      ( read      ),
    .i2c_end   ( i2c_end   ),
    .rd_data   ( rd_data   ),
    .wr_en     ( wr_en     ),
    .rd_en     ( rd_en     ),
    .i2c_start ( i2c_start ),
    .byte_addr ( byte_addr ),
    .wr_data   ( wr_data   ),
    .fifo_rd_data  ( fifo_rd_data  )
);

i2c_control#(
    .DEVICE_ADDR             ( 7'b1010_000 ),
    .SYS_FREQ                ( 26'd50_000_000 ),
    .I2C_FREQ                ( 18'd250_000 )
)u_i2c_control(
    .clk                     ( clk                     ),
    .rst_n                   ( rst_n                   ),
    .wr_en                   ( wr_en                   ),
    .rd_en                   ( rd_en                   ),
    .i2c_start               ( i2c_start               ),
    .addr_num                ( 1'b1                    ),
    .byte_addr               ( byte_addr               ),
    .wr_data                 ( wr_data                 ),
    .i2c_clk                 ( i2c_clk                 ),
    .i2c_end                 ( i2c_end                 ),
    .rd_data                 ( rd_data                 ),
    .i2c_scl                 ( scl                 ),
    .i2c_sda                 ( sda                 )
);

smg#(
    .W     ( 4'd8 )
)u_smg(
    .clk   ( clk   ),
    .rst_n ( rst_n ),
    .data  ( fifo_rd_data  ),
    .sel   ( sel   ),
    .seg   ( seg   )
);

endmodule

编写测试的testbench,testbench如下图所示。这边要说明的是,仿真文件需要AT24C64的文件,可以在野火官网找到,以及添加altera_mf文件。仿真中也改变了一些设置。将CNT_WAIT_MAX改为1000,CNT_START_MAX改为1500,是为了让仿真更快一些。

`timescale  1ns / 1ps

module tb_i2c_eeprom;

// i2c_eeprom Parameters
parameter PERIOD  = 20;


// i2c_eeprom Inputs
reg   clk                                  = 0 ;
reg   rst_n                                = 0 ;
reg   key_wr                               = 0 ;
reg   key_rd                               = 0 ;

// i2c_eeprom Outputs
wire  scl                                  ;
wire  [5:0]  sel                           ;
wire  [7:0]  seg                           ;

// i2c_eeprom Bidirs
wire  sda                                  ;


initial
begin
    forever #(PERIOD/2)  clk=~clk;
end

initial
begin
    #(PERIOD*2) rst_n  =  1;
end

i2c_eeprom  u_i2c_eeprom (
    .clk                     ( clk           ),
    .rst_n                   ( rst_n         ),
    .key_wr                  ( key_wr        ),
    .key_rd                  ( key_rd        ),

    .scl                     ( scl           ),
    .sel                     ( sel     [5:0] ),
    .seg                     ( seg     [7:0] ),

    .sda                     ( sda           )
);

M24LC64  M24lc64_inst
(
    .A0     (1'b0       ),  //器件地址
    .A1     (1'b0       ),  //器件地址
    .A2     (1'b0       ),  //器件地址
    .WP     (1'b0       ),  //写保护信号,高电平有效
    .RESET  (~rst_n     ),  //复位信号,高电平有效

    .SDA    (sda        ),  //串行数据
    .SCL    (scl        )   //串行时钟
);

initial
  begin
    key_wr  <=  1'b1  ;
    key_rd  <=  1'b1  ;
    #(PERIOD*100)
    key_wr  <=  1'b0  ;
    key_rd  <=  1'b1  ;
    #(PERIOD*200)
    key_wr  <=  1'b1  ;
    key_rd  <=  1'b1  ;
    #(PERIOD*200_000*15)
    key_wr  <=  1'b1  ;
    key_rd  <=  1'b0  ;
    #(PERIOD*200)
    key_wr  <=  1'b1  ;
    key_rd  <=  1'b1  ;
    #(PERIOD*200_000*15)
    $stop;
  end

endmodule

仿真的结果图如下图所示,结果也是没有问题的。可以进行正常的读写操作,至于AT24C64那边读出的数据都是高阻态,我看了官方仿真结果也是这样的,可能给的仿真文件也有问题。
在这里插入图片描述

3、上板验证

可以进行正常的数据写入以及读取操作。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 13
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

树叶~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值