FPGA(八)EEPROM

8.1 EEPROM简介

EEPROM(或E2PROM,Electrically Erasable Programmable read only memory):电可擦除可编程只读存储器。

两种非易失性存储器对比:

  • EEPROM:以字节为单位改写;结构复杂,容量小。
  • Flash:以扇区为单位擦除;结构简单,容量大。

下面是EEPROM的芯片AT24C04原理图:

  • A0,A1,A2:定义该芯片的地址,最多支持8个芯片共用I2C总线,因为这里只有一个芯片,直接全部置0;
  • SCL,SDA:I2C总线;
  • WP:写保护,拉高则无法写,这里始终拉低。

8.2 IIC协议

IIC(或I2C,Inter-Integrated Circuit):集成线路总线。两线式串行总线,支持一主多从通信。半双工通信,由数据线(SDA)时钟线(SCL)构成。有不同的模式 :标准100Kb/s、快速400Kb/s、高速3.4Mb/s。

8.2.1 整体时序

下面是I2C的整体时序:

  • ①:空闲状态,SCL和SDA均拉高;
  • ②:起始信号,主机开始传输数据,在SCL为高时拉低SDA;
  • ③:数据传输状态;
  • ④:停止信号,主机停止传输数据,在SCL为高时拉高SDA。

下面是数据传输的时序:

  • SDA只能在SCL为低电平时改变,在SCL为高电平时必须保持稳定。
  • 发生数据时,先发送8bit数据(8个时钟周期),第8个时钟周期末主机释放SDA。第9个时钟周期从机通过SDA进行应答,拉低表示有效,拉高表示失败,该时钟周期末从机释放SDA让主机继续传输。
  • 最先发送的是字节的MSB(最高位)。

8.2.2 器件地址

每个I2C器件都有一个地址,即前面提到的A0,A1,A2

在传输数据时,第一个字节需要发送器件地址,即下图的绿色部分(7bit),前4位固定为1010,后3位为A0,A1,A2。第8位是读写控制位,表示该轮传输是读(1)还是写(0)。

8.2.3 存储地址

发送完器件地址后需要发送具体读写的地址,即下面的存储地址(字地址)。

有些EEPROM容量比较小,地址一个字节就可以表示,如AT24C02只有2Kb=256B,参加上图单字节地址分布;而大容量EEPROM的地址需要两个字节来表示,如AT24C64需要13位,参加上图双字节地址分布,最前面的3字节不关心。

8.2.4 写时序

写分为单次写(字节写)和连续写(页写)。1页是32字节。

  • 字节写

    先发送器件地址,再发送存储地址,最后发送8bit数据。注意每字节的传输都有应答信号,最后主机要发送停止信号。

  • 页写

    只有部分I2C设备支持页写操作。

    前面和单次写一样,只不过传输完8bit数据后不停止,而是继续发送8bit数据,全部发送完毕后再停止。

    1页只有32B,芯片内部有5位的指针,当指向的地址为31时,下一次指针会重新指向0,即覆盖了页内地址0的数据。因此没必要写超过1页的数据。

8.2.5 读时序

有下面3种模式。

  • 当前地址读

    指在一次读或写操作后发起读操作。由于I2C器件在读写操作后,其内部的地址指针自动加一,因此当前地址读可以读取下一个字地址的数据。

    时序如上,接收完8bit数据后不应答(ACK拉高),发送停止信号。

  • 随机地址读

    在读之前通过哑写(Dummy Write)来改变指针地址。

  • 连续地址读

    即在“当前地址读”或“连续地址读”之后进行应答,直到读完后才不应答和发送停止位。

8.3 程序设计

8.3.1 系统架构

8.3.2 I2C驱动设计

下图是I2C驱动中状态机的状态转换图。从左边的idle状态开始,大致流程为根据bit_ctrl决定发送几位字地址,然后根据wr_flag决定是执行读还是写操作。

8.3.3 三态门说明

sda是inout端口,需要用如下的语句生成三态门,也可以用IOBUF,参见LCD实验

sda_dir控制是input还是output,如果是input则赋值为高阻态,数据通过sda_in接收。

assign sda = sda_dir ? sda_out : 1'bz;
assign sda_in = sda;

8.4 实验代码

8.4.1 顶层模块

module top(
    input               sys_clk    ,      //系统时钟
    input               sys_rst_n  ,      //系统复位
    //eeprom interface
    output              i2c_scl    ,      //eeprom的时钟线scl
    inout               i2c_sda    ,      //eeprom的数据线sda
    //user interface
    output              led               //led显示
);

    //parameter define
    parameter    SLAVE_ADDR = 7'b1010000     ; //器件地址(SLAVE_ADDR)
    parameter    BIT_CTRL   = 1'b1           ; //字地址位控制参数(16b/8b)
    parameter    CLK_FREQ   = 26'd50_000_000 ; //i2c_dri模块的驱动时钟频率 50MHz
    parameter    I2C_FREQ   = 18'd250_000    ; //I2C的SCL时钟频率 250kHz
    parameter    L_TIME     = 17'd125_000    ; //led闪烁时间参数

    //wire define
    wire           dri_clk   ; //I2C操作时钟
    wire           i2c_exec  ; //I2C触发控制
    wire   [15:0]  i2c_addr  ; //I2C操作地址
    wire   [ 7:0]  i2c_data_w; //I2C写入的数据
    wire           i2c_done  ; //I2C操作结束标志
    wire           i2c_ack   ; //I2C应答标志 0:应答 1:未应答
    wire           i2c_rh_wl ; //I2C读写控制
    wire   [ 7:0]  i2c_data_r; //I2C读出的数据
    wire           rw_done   ; //E2PROM读写测试完成
    wire           rw_result ; //E2PROM读写测试结果 0:失败 1:成功 

    //e2prom读写测试模块
    e2prom_rw u_e2prom_rw(
        .clk         (dri_clk   ),  //时钟信号
        .rst_n       (sys_rst_n ),  //复位信号
        //i2c interface
        .i2c_exec    (i2c_exec  ),  //I2C触发执行信号
        .i2c_rh_wl   (i2c_rh_wl ),  //I2C读写控制信号
        .i2c_addr    (i2c_addr  ),  //I2C器件内地址
        .i2c_data_w  (i2c_data_w),  //I2C要写的数据
        .i2c_data_r  (i2c_data_r),  //I2C读出的数据
        .i2c_done    (i2c_done  ),  //I2C一次操作完成
        .i2c_ack     (i2c_ack   ),  //I2C应答标志 
        //user interface
        .rw_done     (rw_done   ),  //E2PROM读写测试完成
        .rw_result   (rw_result )   //E2PROM读写测试结果 0:失败 1:成功
    );

    //i2c驱动模块
    i2c_driver #(
        .SLAVE_ADDR  (SLAVE_ADDR),  //EEPROM从机地址
        .CLK_FREQ    (CLK_FREQ  ),  //模块输入的时钟频率
        .I2C_FREQ    (I2C_FREQ  )   //IIC_SCL的时钟频率
    ) u_i2c_dri(
        .clk         (sys_clk   ),  
        .rst_n       (sys_rst_n ),  
        //i2c interface
        .i2c_exec    (i2c_exec  ),  //I2C触发执行信号
        .bit_ctrl    (BIT_CTRL  ),  //器件地址位控制(16b/8b)
        .i2c_rh_wl   (i2c_rh_wl ),  //I2C读写控制信号
        .i2c_addr    (i2c_addr  ),  //I2C器件内地址
        .i2c_data_w  (i2c_data_w),  //I2C要写的数据
        .i2c_data_r  (i2c_data_r),  //I2C读出的数据
        .i2c_done    (i2c_done  ),  //I2C一次操作完成
        .i2c_ack     (i2c_ack   ),  //I2C应答标志
        .scl         (i2c_scl   ),  //I2C的SCL时钟信号
        .sda         (i2c_sda   ),  //I2C的SDA信号
        //user interface
        .dri_clk     (dri_clk   )   //I2C操作时钟
    );

    //led指示模块
    led_alarm #(.L_TIME(L_TIME  )   //控制led闪烁时间
    ) u_led_alarm(
        .clk         (dri_clk   ),  
        .rst_n       (sys_rst_n ), 
        
        .rw_done     (rw_done   ),  
        .rw_result   (rw_result ),
        .led         (led       )    
    );

endmodule

8.4.2 I2C驱动模块

该模块会生成一个SCL的4倍频率的时钟dri_clk,用于驱动相关控制。为什么4分频?参见下图,这样在dri_clk上升沿时能控制SDA信号变化。

module i2c_driver #(
    parameter SLAVE_ADDR = 7'b1010000,      // EEPROM从机地址
    parameter CLK_FREQ   = 26'd50_000_000,  // 模块输入的时钟频率
    parameter I2C_FREQ   = 18'd250_000      // IIC_SCL的时钟频率
) (                                                            
    input                clk,    
    input                rst_n,   
                                         
    //i2c interface                      
    input                i2c_exec,      // I2C触发执行信号
    input                bit_ctrl,      // 控制存储地址的位数,1:16bit,0:8bit
    input                i2c_rh_wl,     // I2C读写控制信号
    input        [15:0]  i2c_addr,      // I2C器件内地址,如果bit_ctrl=0则只使用低8位
    input        [ 7:0]  i2c_data_w,    // I2C要写的数据
    output logic [ 7:0]  i2c_data_r,    // I2C读出的数据
    output logic         i2c_done,      // I2C一次操作完成
    output logic         i2c_ack,       // I2C应答标志,0:应答 1:未应答
    output logic         scl,           // I2C的SCL时钟信号
    inout                sda,           // I2C的SDA信号
                                       
    //user interface                   
    output logic         dri_clk        // 驱动I2C操作的驱动时钟
);

    // 状态定义
    localparam  ST_IDLE    = 8'b0000_0001;  // 空闲状态
    localparam  ST_SLADDR  = 8'b0000_0010;  // 发送器件地址
    localparam  ST_ADDR16  = 8'b0000_0100;  // 发送16位字地址
    localparam  ST_ADDR8   = 8'b0000_1000;  // 发送8位字地址
    localparam  ST_DATA_WR = 8'b0001_0000;  // 写数据(8 bit)
    localparam  ST_ADDR_RD = 8'b0010_0000;  // 发送器件地址读
    localparam  ST_DATA_RD = 8'b0100_0000;  // 读数据(8 bit)
    localparam  ST_STOP    = 8'b1000_0000;  // 结束I2C操作

    logic         sda_dir;      // I2C数据(SDA)方向控制,1:输出,0:输入
    logic         sda_out;      // SDA输出信号
    logic         st_done;      // 为高表示状态结束,可以转换到下一个状态
    logic         wr_flag;      // 写标志,来自i2c_rh_wl
    logic [ 6:0]  cnt;          // 计数
    logic [ 7:0]  data_r;       // 读取的数据
    logic [15:0]  addr_t;       // 寄存读/写地址,来自i2c_addr
    logic [ 7:0]  data_w_t;     // 寄存写的数据,来自i2c_data_w
    
    logic [ 7:0]  cur_state;    // 状态机当前状态
    logic [ 7:0]  next_state;   // 状态机下一状态

    wire          sda_in;       // SDA输入信号
    wire   [8:0]  clk_divide;   // 模块驱动时钟的分频系数

    // SDA控制
    assign sda = sda_dir ? sda_out : 1'bz;  // SDA数据输出或高阻
    assign sda_in = sda;                    // SDA数据输入

    // 生成I2C的SCL的四倍频率的驱动时钟
    localparam CLK_DIVIDE = CLK_FREQ / I2C_FREQ / 8;
    logic [9:0] clk_cnt;

    always_ff @(posedge clk, negedge rst_n) begin
        if (!rst_n) begin
            dri_clk <=  1'b0;
            clk_cnt <= 10'd0;
        end
        else if (clk_cnt == CLK_DIVIDE - 1'd1) begin
            clk_cnt <= 10'd0;
            dri_clk <= ~dri_clk;
        end
        else
            clk_cnt <= clk_cnt + 1'b1;
    end

    // (三段式状态机)同步时序描述状态转移
    always_ff @(posedge dri_clk, negedge rst_n) begin
        if (!rst_n)
            cur_state <= ST_IDLE;
        else
            cur_state <= next_state;
    end

    // 组合逻辑判断状态转移条件
    always_comb begin
        case (cur_state)
            ST_IDLE: begin
                if (i2c_exec)       next_state = ST_SLADDR;
                else                next_state = ST_IDLE;
            end
            ST_SLADDR: begin
                if (st_done) begin
                    if (bit_ctrl)   next_state = ST_ADDR16;
                    else            next_state = ST_ADDR8;
                end
                else                next_state = ST_SLADDR;
            end
            ST_ADDR16: begin
                if (st_done)        next_state = ST_ADDR8;
                else                next_state = ST_ADDR16;
            end
            ST_ADDR8: begin
                if (st_done) begin
                    if (wr_flag)    next_state = ST_ADDR_RD;
                    else            next_state = ST_DATA_WR;
                end
                else                next_state = ST_ADDR8;
            end
            ST_DATA_WR: begin
                if (st_done)        next_state = ST_STOP;
                else                next_state = ST_DATA_WR;
            end
            ST_ADDR_RD: begin
                if (st_done)        next_state = ST_DATA_RD;
                else                next_state = ST_ADDR_RD;
            end
            ST_DATA_RD: begin
                if (st_done)        next_state = ST_STOP;
                else                next_state = ST_DATA_RD;
            end
            ST_STOP: begin
                if (st_done)        next_state = ST_IDLE;
                else                next_state = ST_STOP ;
            end
            default:                next_state = ST_IDLE;
        endcase
    end

    // 时序电路描述状态输出
    always_ff @(posedge dri_clk, negedge rst_n) begin
        // 复位初始化
        if (!rst_n) begin
            scl        <= 1'b1;
            sda_out    <= 1'b1;  // 数据线复位为高
            sda_dir    <= 1'b1;
            i2c_done   <= 1'b0;
            i2c_ack    <= 1'b0;
            cnt        <= 1'b0;
            st_done    <= 1'b0;
            data_r     <= 1'b0;
            i2c_data_r <= 1'b0;
            wr_flag    <= 1'b0;
            addr_t     <= 1'b0;
            data_w_t   <= 1'b0;
        end
        else begin
            st_done <= 1'b0;
            cnt     <= cnt + 1'b1;
            case (cur_state)
                ST_IDLE: begin
                    scl      <= 1'b1;
                    sda_out  <= 1'b1;
                    sda_dir  <= 1'b1;
                    i2c_done <= 1'b0;
                    cnt      <= 7'b0;
                    if (i2c_exec) begin                   
                        wr_flag  <= i2c_rh_wl;
                        addr_t   <= i2c_addr;
                        data_w_t <= i2c_data_w;
                        i2c_ack  <= 1'b0;                      
                    end                                  
                end                                      
                ST_SLADDR: begin        // 发送器件地址和读写控制位
                    case (cnt)                            
                        7'd1 : sda_out <= 1'b0;             // 在scl为高时拉低sda,开始信号
                        7'd3 : scl <= 1'b0;                 // 拉低scl
                        7'd4 : sda_out <= SLAVE_ADDR[6];    // 在scl为低时发送数据
                        7'd5 : scl <= 1'b1;
                        7'd7 : scl <= 1'b0;
                        7'd8 : sda_out <= SLAVE_ADDR[5];
                        7'd9 : scl <= 1'b1;
                        7'd11: scl <= 1'b0;
                        7'd12: sda_out <= SLAVE_ADDR[4];
                        7'd13: scl <= 1'b1;
                        7'd15: scl <= 1'b0;
                        7'd16: sda_out <= SLAVE_ADDR[3];
                        7'd17: scl <= 1'b1;
                        7'd19: scl <= 1'b0;
                        7'd20: sda_out <= SLAVE_ADDR[2];
                        7'd21: scl <= 1'b1;             
                        7'd23: scl <= 1'b0;             
                        7'd24: sda_out <= SLAVE_ADDR[1];
                        7'd25: scl <= 1'b1;             
                        7'd27: scl <= 1'b0;             
                        7'd28: sda_out <= SLAVE_ADDR[0]; 
                        7'd29: scl <= 1'b1;
                        7'd31: scl <= 1'b0;
                        7'd32: sda_out <= 1'b0;         // 0:写
                        7'd33: scl <= 1'b1;
                        7'd35: scl <= 1'b0;             // 至此8位数据发送完毕
                        7'd36: begin
                            sda_dir <= 1'b0;            // 释放sda
                            sda_out <= 1'b1;
                        end
                        7'd37: scl <= 1'b1;             // 拉高准备接受应答
                        7'd38: begin                    // 接受应答 
                            st_done <= 1'b1;
                            if(sda_in == 1'b1)          // 高电平表示未应答
                                i2c_ack <= 1'b1;        // 拉高应答标志位
                        end  
                        7'd39: begin                     
                            scl <= 1'b0;
                            cnt <= 1'b0;
                        end            
                    endcase                              
                end                                      
                ST_ADDR16: begin                         
                    case(cnt)                            
                        7'd0 : begin                     
                            sda_dir <= 1'b1 ;            
                            sda_out <= addr_t[15];      // 传送字地址
                        end                              
                        7'd1 : scl <= 1'b1;              
                        7'd3 : scl <= 1'b0;              
                        7'd4 : sda_out <= addr_t[14];    
                        7'd5 : scl <= 1'b1;              
                        7'd7 : scl <= 1'b0;              
                        7'd8 : sda_out <= addr_t[13];    
                        7'd9 : scl <= 1'b1;              
                        7'd11: scl <= 1'b0;              
                        7'd12: sda_out <= addr_t[12];    
                        7'd13: scl <= 1'b1;              
                        7'd15: scl <= 1'b0;              
                        7'd16: sda_out <= addr_t[11];    
                        7'd17: scl <= 1'b1;              
                        7'd19: scl <= 1'b0;              
                        7'd20: sda_out <= addr_t[10];    
                        7'd21: scl <= 1'b1;              
                        7'd23: scl <= 1'b0;              
                        7'd24: sda_out <= addr_t[9];     
                        7'd25: scl <= 1'b1;              
                        7'd27: scl <= 1'b0;              
                        7'd28: sda_out <= addr_t[8];     
                        7'd29: scl <= 1'b1;              
                        7'd31: scl <= 1'b0;              
                        7'd32: begin                     
                            sda_dir <= 1'b0;             
                            sda_out <= 1'b1;   
                        end                              
                        7'd33: scl  <= 1'b1;             
                        7'd34: begin            
                            st_done <= 1'b1;   
                            if(sda_in == 1'b1)  
                                i2c_ack <= 1'b1;
                        end        
                        7'd35: begin                     
                            scl <= 1'b0;                 
                            cnt <= 1'b0;                 
                        end
                    endcase                              
                end                                      
                ST_ADDR8: begin                          
                    case(cnt)                            
                        7'd0: begin                      
                        sda_dir <= 1'b1 ;             
                        sda_out <= addr_t[7];
                        end                              
                        7'd1 : scl <= 1'b1;              
                        7'd3 : scl <= 1'b0;              
                        7'd4 : sda_out <= addr_t[6];     
                        7'd5 : scl <= 1'b1;              
                        7'd7 : scl <= 1'b0;              
                        7'd8 : sda_out <= addr_t[5];     
                        7'd9 : scl <= 1'b1;              
                        7'd11: scl <= 1'b0;              
                        7'd12: sda_out <= addr_t[4];     
                        7'd13: scl <= 1'b1;              
                        7'd15: scl <= 1'b0;              
                        7'd16: sda_out <= addr_t[3];     
                        7'd17: scl <= 1'b1;              
                        7'd19: scl <= 1'b0;              
                        7'd20: sda_out <= addr_t[2];     
                        7'd21: scl <= 1'b1;              
                        7'd23: scl <= 1'b0;              
                        7'd24: sda_out <= addr_t[1];     
                        7'd25: scl <= 1'b1;              
                        7'd27: scl <= 1'b0;              
                        7'd28: sda_out <= addr_t[0];     
                        7'd29: scl <= 1'b1;              
                        7'd31: scl <= 1'b0;              
                        7'd32: begin                     
                            sda_dir <= 1'b0;         
                            sda_out <= 1'b1;                    
                        end                              
                        7'd33: scl     <= 1'b1;          
                        7'd34: begin            
                            st_done <= 1'b1;    
                            if(sda_in == 1'b1)  
                                i2c_ack <= 1'b1; 
                        end   
                        7'd35: begin                     
                            scl <= 1'b0;                 
                            cnt <= 1'b0;                 
                        end
                    endcase                              
                end                                      
                ST_DATA_WR: begin       // 写数据(8 bit)
                    case(cnt)                            
                        7'd0: begin                      
                            sda_out <= data_w_t[7];
                            sda_dir <= 1'b1;             
                        end                              
                        7'd1 : scl <= 1'b1;              
                        7'd3 : scl <= 1'b0;              
                        7'd4 : sda_out <= data_w_t[6];  
                        7'd5 : scl <= 1'b1;              
                        7'd7 : scl <= 1'b0;              
                        7'd8 : sda_out <= data_w_t[5];  
                        7'd9 : scl <= 1'b1;              
                        7'd11: scl <= 1'b0;              
                        7'd12: sda_out <= data_w_t[4];  
                        7'd13: scl <= 1'b1;              
                        7'd15: scl <= 1'b0;              
                        7'd16: sda_out <= data_w_t[3];  
                        7'd17: scl <= 1'b1;              
                        7'd19: scl <= 1'b0;              
                        7'd20: sda_out <= data_w_t[2];  
                        7'd21: scl <= 1'b1;              
                        7'd23: scl <= 1'b0;              
                        7'd24: sda_out <= data_w_t[1];  
                        7'd25: scl <= 1'b1;              
                        7'd27: scl <= 1'b0;              
                        7'd28: sda_out <= data_w_t[0];  
                        7'd29: scl <= 1'b1;              
                        7'd31: scl <= 1'b0;              
                        7'd32: begin                     
                            sda_dir <= 1'b0;           
                            sda_out <= 1'b1;                              
                        end                              
                        7'd33: scl <= 1'b1;              
                        7'd34: begin            
                            st_done <= 1'b1;   
                            if(sda_in == 1'b1)  
                                i2c_ack <= 1'b1;
                        end          
                        7'd35: begin                     
                            scl  <= 1'b0;                
                            cnt  <= 1'b0;                
                        end
                    endcase                              
                end                                      
                ST_ADDR_RD: begin       // 写地址以进行读数据
                    case(cnt)                            
                        7'd0 : begin                     
                            sda_dir <= 1'b1;             
                            sda_out <= 1'b1;             
                        end                              
                        7'd1 : scl <= 1'b1;              
                        7'd2 : sda_out <= 1'b0;         // 在scl为高时拉低sda,重新开始
                        7'd3 : scl <= 1'b0;              
                        7'd4 : sda_out <= SLAVE_ADDR[6];// 传送器件地址
                        7'd5 : scl <= 1'b1;              
                        7'd7 : scl <= 1'b0;              
                        7'd8 : sda_out <= SLAVE_ADDR[5]; 
                        7'd9 : scl <= 1'b1;              
                        7'd11: scl <= 1'b0;              
                        7'd12: sda_out <= SLAVE_ADDR[4]; 
                        7'd13: scl <= 1'b1;              
                        7'd15: scl <= 1'b0;              
                        7'd16: sda_out <= SLAVE_ADDR[3]; 
                        7'd17: scl <= 1'b1;              
                        7'd19: scl <= 1'b0;              
                        7'd20: sda_out <= SLAVE_ADDR[2]; 
                        7'd21: scl <= 1'b1;              
                        7'd23: scl <= 1'b0;              
                        7'd24: sda_out <= SLAVE_ADDR[1]; 
                        7'd25: scl <= 1'b1;              
                        7'd27: scl <= 1'b0;              
                        7'd28: sda_out <= SLAVE_ADDR[0]; 
                        7'd29: scl <= 1'b1;              
                        7'd31: scl <= 1'b0;              
                        7'd32: sda_out <= 1'b1;         // 1:读
                        7'd33: scl <= 1'b1;              
                        7'd35: scl <= 1'b0;              
                        7'd36: begin                     
                            sda_dir <= 1'b0;            
                            sda_out <= 1'b1;                    
                        end
                        7'd37: scl     <= 1'b1;
                        7'd38: begin
                            st_done <= 1'b1;     
                            if(sda_in == 1'b1)  
                                i2c_ack <= 1'b1;
                        end   
                        7'd39: begin
                            scl <= 1'b0;
                            cnt <= 1'b0;
                        end
                    endcase
                end
                ST_DATA_RD: begin       // 读取数据(8 bit)
                    case(cnt)
                        7'd0: sda_dir <= 1'b0;
                        7'd1: begin
                            data_r[7] <= sda_in;
                            scl       <= 1'b1;
                        end
                        7'd3: scl  <= 1'b0;
                        7'd5: begin
                            data_r[6] <= sda_in ;
                            scl       <= 1'b1   ;
                        end
                        7'd7: scl  <= 1'b0;
                        7'd9: begin
                            data_r[5] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd11: scl  <= 1'b0;
                        7'd13: begin
                            data_r[4] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd15: scl  <= 1'b0;
                        7'd17: begin
                            data_r[3] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd19: scl  <= 1'b0;
                        7'd21: begin
                            data_r[2] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd23: scl  <= 1'b0;
                        7'd25: begin
                            data_r[1] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd27: scl  <= 1'b0;
                        7'd29: begin
                            data_r[0] <= sda_in;
                            scl       <= 1'b1  ;
                        end
                        7'd31: scl  <= 1'b0;
                        7'd32: begin
                            sda_dir <= 1'b1;             
                            sda_out <= 1'b1;        // 发送非应答
                        end
                        7'd33: scl     <= 1'b1;
                        7'd34: st_done <= 1'b1;
                        7'd35: begin
                            scl <= 1'b0;
                            cnt <= 1'b0;
                            i2c_data_r <= data_r;
                        end
                    endcase
                end
                ST_STOP: begin          // 结束I2C操作
                    case(cnt)
                        7'd0: begin
                            sda_dir <= 1'b1;
                            sda_out <= 1'b0;
                        end
                        7'd1 : scl     <= 1'b1;
                        7'd3 : sda_out <= 1'b1;         // 在scl为高时拉高sda,停止信号
                        7'd15: st_done <= 1'b1;
                        7'd16: begin
                            cnt      <= 1'b0;
                            i2c_done <= 1'b1;           //向上层模块传递I2C结束信号
                        end
                    endcase
                end
            endcase
        end
    end

endmodule

8.4.3 I2C读写模块

控制i2c_driver依次写入和读出一定数量的字节,判断结果是否正确。

module e2prom_rw(
    input                 clk        , //时钟信号
    input                 rst_n      , //复位信号

    //i2c interface
    output   reg          i2c_rh_wl  , //I2C读写控制信号
    output   reg          i2c_exec   , //I2C触发执行信号
    output   reg  [15:0]  i2c_addr   , //I2C器件内地址
    output   reg  [ 7:0]  i2c_data_w , //I2C要写的数据
    input         [ 7:0]  i2c_data_r , //I2C读出的数据
    input                 i2c_done   , //I2C一次操作完成
    input                 i2c_ack    , //I2C应答标志

    //user interface
    output   reg          rw_done    , //E2PROM读写测试完成
    output   reg          rw_result    //E2PROM读写测试结果 0:失败 1:成功
);

    //parameter define
    //EEPROM写数据需要添加间隔时间,读数据则不需要
    parameter      WR_WAIT_TIME = 14'd5000; // 写入间隔时间5ms,时钟是dri_clk是1MHz
    parameter      MAX_BYTE     = 16'd256 ; // 读写测试的字节个数

    //reg define
    reg   [1:0]    flow_cnt  ; //状态流控制
    reg   [13:0]   wait_cnt  ; //延时计数器

    //EEPROM读写测试,先写后读,并比较读出的值与写入的值是否一致
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            flow_cnt   <= 2'b0;
            i2c_rh_wl  <= 1'b0;
            i2c_exec   <= 1'b0;
            i2c_addr   <= 16'b0;
            i2c_data_w <= 8'b0;
            wait_cnt   <= 14'b0;
            rw_done    <= 1'b0;
            rw_result  <= 1'b0;
        end
        else begin
            i2c_exec <= 1'b0;
            rw_done  <= 1'b0;
            case(flow_cnt)
                2'd0 : begin                                  
                    wait_cnt <= wait_cnt + 1'b1;               //延时计数
                    if(wait_cnt == WR_WAIT_TIME - 1'b1) begin  //EEPROM写操作延时完成
                        wait_cnt <= 1'b0;
                        if(i2c_addr == MAX_BYTE) begin         //256个字节写入完成
                            i2c_addr  <= 1'b0;
                            i2c_rh_wl <= 1'b1;
                            flow_cnt  <= 2'd2;
                        end
                        else begin
                            flow_cnt <= flow_cnt + 1'b1;
                            i2c_exec <= 1'b1;
                        end
                    end
                end
                2'd1 : begin
                    if(i2c_done == 1'b1) begin                  //EEPROM单次写入完成
                        flow_cnt   <= 2'd0;
                        i2c_addr   <= i2c_addr + 1'b1;           //地址0~255分别写入
                        i2c_data_w <= i2c_data_w + 1'b1;         //数据0~255
                    end    
                end
                2'd2 : begin                                   
                    flow_cnt <= flow_cnt + 1'b1;
                    i2c_exec <= 1'b1;
                end    
                2'd3 : begin
                    if(i2c_done == 1'b1) begin                 //EEPROM单次读出完成
                        //读出的值错误或者I2C未应答,读写测试失败
                        if((i2c_addr[7:0] != i2c_data_r) || (i2c_ack == 1'b1)) begin
                            rw_done <= 1'b1;
                            rw_result <= 1'b0;
                        end
                        else if(i2c_addr == MAX_BYTE - 1'b1) begin //读写测试成功
                            rw_done   <= 1'b1;
                            rw_result <= 1'b1;
                        end    
                        else begin
                            flow_cnt <= 2'd2;
                            i2c_addr <= i2c_addr + 1'b1;
                        end
                    end                 
                end
                default : ;
            endcase    
        end
    end    

endmodule

8.4.4 LED显示模块

LED灯常亮表示读写测试正确,闪烁表示错误。

module led_alarm #(
    parameter L_TIME = 25'd25_000_000 
) (
    input        clk       ,  //时钟信号
    input        rst_n     ,  //复位信号
                 
    input        rw_done   ,  //E2PROM读写测试完成
    input        rw_result ,  //E2PROM读写测试结果
    output  reg  led
);

    //reg define
    reg          rw_done_flag;    //读写测试完成标志
    reg  [24:0]  led_cnt     ;    //led计数

    //读写测试完成标志
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)
            rw_done_flag <= 1'b0;
        else if(rw_done)
            rw_done_flag <= 1'b1;
    end        

    //错误标志为1时PL_LED0闪烁,否则PL_LED0常亮
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            led_cnt <= 25'd0;
            led <= 1'b0;
        end
        else begin
            if(rw_done_flag) begin
                if(rw_result)                          //读写测试正确
                    led <= 1'b1;                       //led灯常亮
                else begin                             //读写测试错误
                    led_cnt <= led_cnt + 25'd1;
                    if(led_cnt == L_TIME - 1'b1) begin
                        led_cnt <= 25'd0;
                        led <= ~led;                   //led灯闪烁
                    end
                end
            end
            else
                led <= 1'b0;                           //读写测试完成之前,led灯熄灭
        end    
    end

endmodule

8.4.5 I2C驱动测试模块

进行一次写和一次读。

`timescale  1ns/1ns                     //定义仿真时间单位1ns和仿真时间精度为1ns

module  tb_i2c_dri;              

    //parameter  define
    parameter  T = 20;                      //时钟周期为20ns
    parameter  IIC_WR_CYCYLE  = 10_000;
    parameter  SLAVE_ADDR = 7'b1010000 ;    //EEPROM从机地址
    parameter  CLK_FREQ   = 26'd50_000_000; //模块输入的时钟频率
    parameter  I2C_FREQ   = 18'd250_000;    //IIC_SCL的时钟频率

    //reg define
    reg          sys_clk;                   //时钟信号
    reg          sys_rst_n;                 //复位信号
        
    reg          i2c_exec  ;
    reg          bit_ctrl  ;
    reg          i2c_rh_wl ;
    reg   [15:0] i2c_addr  ;
    reg   [7:0]  i2c_data_w;
    reg   [3:0]  flow_cnt  ;
    reg   [13:0] delay_cnt ;

    //wire define
    wire  [7:0]  i2c_data_r;
    wire         i2c_done  ;
    wire         i2c_ack   ;
    wire         scl       ;
    wire         sda       ;
    wire         dri_clk   ;

    //给输入信号初始值
    initial begin
        sys_clk            = 1'b0;
        sys_rst_n          = 1'b0;     //复位
        #(T+1)  sys_rst_n  = 1'b1;     //在第21ns的时候复位信号信号拉高
    end

    //50Mhz的时钟,周期则为1/50Mhz=20ns,所以每10ns,电平取反一次
    always #(T/2) sys_clk = ~sys_clk;

    always @(posedge dri_clk or negedge sys_rst_n) begin
        if(!sys_rst_n) begin
            i2c_exec <= 1'b0;
            bit_ctrl <= 1'b0;
            i2c_rh_wl <= 1'b0;
            i2c_addr <= 1'b0;
            i2c_data_w <= 1'b0;
            flow_cnt <= 1'b0;
            delay_cnt <= 1'b0;
        end
        else begin
            case(flow_cnt)
                'd0 : flow_cnt <= flow_cnt + 1'b1;
                'd1 : begin
                    i2c_exec <= 1'b1;                //拉高触发信号
                    bit_ctrl <= 1'b1;                //地址位选择信号   1: 16位
                    i2c_rh_wl <= 1'b0;               //写操作
                    i2c_addr <= 16'h0555;            //写地址
                    i2c_data_w <= 8'hAA;             //写数据
                    flow_cnt <= flow_cnt + 1'b1;
                end
                'd2 : begin 
                    i2c_exec <= 1'b0;
                    flow_cnt <= flow_cnt + 1'b1;
                end    
                'd3 : begin
                    if(i2c_done)
                        flow_cnt <= flow_cnt + 1'b1;
                end
                'd4 : begin
                    delay_cnt <= delay_cnt + 1'b1;
                    if(delay_cnt == IIC_WR_CYCYLE - 1'b1)
                        flow_cnt <= flow_cnt + 1'b1;
                end
                'd5 : begin
                        i2c_exec <= 1'b1;
                        bit_ctrl <= 1'b1;                   
                        i2c_rh_wl <= 1'b1;           //读操作     
                        i2c_addr <= 16'h0555;
                        i2c_data_w <= 8'hAA;        
                        flow_cnt <= flow_cnt + 1'b1;                    
                end
                'd6 : begin 
                    i2c_exec <= 1'b0;
                    flow_cnt <= flow_cnt + 1'b1;
                end 
                'd7 : begin
                    if(i2c_done)
                        flow_cnt <= flow_cnt + 1'b1;
                end
                default:;
            endcase    
        end
    end

    pullup(sda); // 高阻时上拉

    //例化led模块
    i2c_driver #(
        .SLAVE_ADDR  (SLAVE_ADDR),  //EEPROM从机地址
        .CLK_FREQ    (CLK_FREQ  ),  //模块输入的时钟频率
        .I2C_FREQ    (I2C_FREQ  )   //IIC_SCL的时钟频率
    ) u_i2c_dri(
        .clk          (sys_clk),
        .rst_n        (sys_rst_n), 

        .i2c_exec     (i2c_exec  ), 
        .bit_ctrl     (bit_ctrl  ), 
        .i2c_rh_wl    (i2c_rh_wl ), 
        .i2c_addr     (i2c_addr  ), 
        .i2c_data_w   (i2c_data_w), 
        .i2c_data_r   (i2c_data_r), 
        .i2c_done     (i2c_done  ), 
        .i2c_ack      (i2c_ack   ), 
        .scl          (scl       ), 
        .sda          (sda       ),
        .dri_clk      (dri_clk   )
    );

    EEPROM_AT24C64 u_EEPROM_AT24C64(
        .scl         (scl),
        .sda         (sda)
        );

endmodule

8.4.6 AT24C64仿真模型

`timescale 1ns/1ns
`define timeslice 1250
module EEPROM_AT24C64(
scl,
sda
);
input scl; 
inout sda; 
reg out_flag; 
reg[7:0] memory[8191:0]; 
reg[12:0]address; 
reg[7:0]memory_buf; 
reg[7:0]sda_buf; 
reg[7:0]shift; 
reg[7:0]addr_byte_h; 
reg[7:0]addr_byte_l; 
reg[7:0]ctrl_byte; 
reg[1:0]State;
integer i;
//---------------------------
parameter
r7 = 8'b1010_1111, w7 = 8'b1010_1110, //main7
r6 = 8'b1010_1101, w6 = 8'b1010_1100, //main6
r5 = 8'b1010_1011, w5 = 8'b1010_1010, //main5
r4 = 8'b1010_1001, w4 = 8'b1010_1000, //main4
r3 = 8'b1010_0111, w3 = 8'b1010_0110, //main3
r2 = 8'b1010_0101, w2 = 8'b1010_0100, //main2
r1 = 8'b1010_0011, w1 = 8'b1010_0010, //main1
r0 = 8'b1010_0001, w0 = 8'b1010_0000; //main0
assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;

initial
begin
addr_byte_h = 0;
addr_byte_l = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
State = 2'b00;
memory_buf = 0;
address = 0;
shift = 0;
for(i=0;i<=8191;i=i+1)
memory[i] = 0;
end
always@(negedge sda)
begin
if(scl == 1)
begin
State = State + 1;
if(State == 2'b11)
disable write_to_eeprom;
end
end

always@(posedge sda)
begin
if(scl == 1) 
stop_W_R;
else
begin
casex(State)
2'b01:begin
read_in;
if(ctrl_byte == w7 || ctrl_byte == w6
|| ctrl_byte == w5 || ctrl_byte == w4
|| ctrl_byte == w3 || ctrl_byte == w2
|| ctrl_byte == w1 || ctrl_byte == w0)
begin
State = 2'b10;
write_to_eeprom; 
end
else
State = 2'b00;
end
2'b11:
read_from_eeprom;
default:
State = 2'b00;
endcase
end
end 
task stop_W_R;
begin
State = 2'b00;
addr_byte_h = 0;
addr_byte_l = 0;
ctrl_byte = 0;
out_flag = 0;
sda_buf = 0;
end
endtask

task read_in;
begin
shift_in(ctrl_byte);
shift_in(addr_byte_h);
shift_in(addr_byte_l);
end
endtask

task write_to_eeprom;
begin
shift_in(memory_buf);
address = {addr_byte_h[4:0], addr_byte_l};
memory[address] = memory_buf;
State = 2'b00;
end
endtask

task read_from_eeprom;
begin
shift_in(ctrl_byte);
if(ctrl_byte == r7 || ctrl_byte == w6
|| ctrl_byte == r5 || ctrl_byte == r4
|| ctrl_byte == r3 || ctrl_byte == r2
|| ctrl_byte == r1 || ctrl_byte == r0)
begin
address = {addr_byte_h[4:0], addr_byte_l};
sda_buf = memory[address];
shift_out;
State = 2'b00;
end
end
endtask
task shift_in;
output[7:0]shift;
begin
@(posedge scl) shift[7] = sda;
@(posedge scl) shift[6] = sda;
@(posedge scl) shift[5] = sda;
@(posedge scl) shift[4] = sda;
@(posedge scl) shift[3] = sda;
@(posedge scl) shift[2] = sda;
@(posedge scl) shift[1] = sda;
@(posedge scl) shift[0] = sda;
@(negedge scl)
begin
#(`timeslice);
out_flag = 1;
sda_buf = 0;
end
@(negedge scl)
begin
#(`timeslice-250);
out_flag = 0;
end
end
endtask
task shift_out;
begin
out_flag = 1;
for(i=6; i>=0; i=i-1)
begin
@(negedge scl);
#`timeslice;
sda_buf = sda_buf << 1;
end
@(negedge scl) #`timeslice sda_buf[7] = 1;
@(negedge scl) #`timeslice out_flag = 0;
end
endtask
endmodule

FPGA(八)EEPROM
https://shuusui.site/blog/2026/01/17/fpga-8/
作者
Shuusui
发布于
2026年1月17日
更新于
2026年1月20日
许可协议