当前位置: 首页 > article >正文

EEPROM读写实验——FPGA学习笔记18

一、EEPROM简介

        Electrically Erasable Progammable Read Only Memory:是指带电可擦可编程只读存储器,是一种常用的非易失性存储器(掉电数据不丢失)

EEPROM发展历史

        我们这次实验所用的AT24C64存储容量为64Kbit,内部分成256页,每页32字节,共有8192个字节,且其读写操作都是以字节为基本单位。可以把AT24C64看作一本书,那么这本书有256页,每页有32行,每行有8个字,总共有256*32*8=65536个字,对应着AT24C6464*1024=65536bit。
        读是从起始读到最大,回到起始; 写是写到一页的结束;

24C02:

片内地址寻址:

读写操作:

二、IIC简介

1、简介

        IIC即Inter-Integrated Circuit(集成电路总线),是由Philips半导体公司(现在的NXP半导体公司)在八十年代初设计出来的一种简单、双向、二线制总线标准。

2、 IIC结构示意

3、IIC协议时序

时钟线为低电平可以变化数据,时钟线为高电平不允许变化数据,只能保持。

 4、器件地址

5、字寄存器地址 (24C64)

6、单次写(字节写)

7、连续写(页写)

8、读当前地址:

9、任意地址单次读(随机读)

10、从xxx地址开始连续读

三、试验任务

        本节的实验任务是先向E2PROMAT24C64)的存储器地址0255分别写入数据0~255;写完之后再读取存储器地址0~255中的数据,若读取的值全部正确则LED灯常亮,否则LED灯闪烁。

四、程序设计

 1、整体框图

2、IIC_my

状态机:

代码:

`timescale 1ns / 1ps

module iic_my#(
    parameter integer WMEN_LEN = 8'd1   ,   //写长度,以字节为单位,包含器件地址
    parameter integer RMEN_LEN = 8'd0   ,   //读长度,以字节为单位,不包含器件地址
    parameter integer CLK_DIV  = 16'd499    //IIC时钟分频系数
)
(
input   wire                            I_clk               ,       //系统时钟输入
input   wire                            I_rstn              ,       //系统复位,低电平有效
output  reg                             O_iic_scl = 1'b0    ,       //I2C时钟SCL
inout   wire                            IO_iic_sda          ,       //I2C 数据总线
input   wire    [WMEN_LEN*8-1'b1:0]     I_wr_data           ,       //写数据寄存器,其中WMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
input   wire    [7:0]                   I_wr_cnt            ,       //写数据计数器,代表写了多少个字节
output  reg     [RMEN_LEN*8-1'b1:0]     O_rd_data = 0       ,       //读数据寄存器,其中RMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
input   wire    [7:0]                   I_rd_cnt            ,       //读数据计数器
input   wire                            I_iic_req           ,       //I_iic_req == 1 使能I2C传输
input   wire                            I_iic_mode          ,       //I_iic_mode = 1 随机读   I_iic_mode = 0 读当前寄存器或者页读
output  reg                             O_iic_busy = 1'b0   ,       //I2C控制器忙
output  reg                             O_iic_bus_error     ,       //I2C总线,无法读到正确ACK出错
output  reg                             IO_iic_sda_dg               //数据总线上的数据
);

localparam IDLE     =    4'd0   ;       //I2C 总线空闲状态
localparam START    =    4'd1   ;       //I2C 总线启动
localparam W_WAIT   =    4'd2   ;       //I2C 总线等待写完成
localparam W_ACK    =    4'd3   ;       //I2C 总线等待写WACK
localparam R_WAIT   =    4'd4   ;       //I2C 总线等待读完成 
localparam R_ACK    =    4'd5   ;       //I2C 总线等待读RACK 
localparam STOP1    =    4'd6   ;       //I2C 总线产生停止位
localparam STOP2    =    4'd7   ;       //I2C 总线产生停止位   

localparam SCL_DIV  =   CLK_DIV/2   ;

localparam OFFSET   =   SCL_DIV - SCL_DIV/4 ;   //设置I2C总线的SCL时钟的偏移,以满足SCL和SDA的时序要求,外部的SCL延迟内部的半周期的四分之三

reg         [2:0 ]  IIC_S   =   4'd0    ;   //I2C 状态机
//generate  scl 
reg         [15:0]  clkdiv  =   16'd0   ;   //I2C 时钟分频寄存器
reg                 scl_r   =   1'b1    ;   //I2C控制器的SCL内部时钟
reg                 sda_o 	=   1'b0    ;   //I2C控制器的SDA
reg                 scl_clk =   1'b0    ;   //I2C控制器内部SCL时钟,与外部时钟存在OFFSET参数设置的相位偏移
reg         [7:0]   sda_r   =   8'd0    ;   //发送寄存器
reg         [7:0]   sda_i_r =   8'd0    ;   //接收寄存器
reg         [7:0]   wcnt    =   8'd0    ;   //发送数据计数器,以byte为单位
reg         [7:0]   rcnt    =   8'd0    ;   //接收数据计数器,以byte为单位
reg         [2:0]   bcnt    =   3'd0    ;   //bit计数器
reg                 rd_req  =   1'b0    ;   //读请求,当判断到需要读数据,内部状态机中设置1
wire                sda_i               ;   //sda 输入
wire                scl_offset          ;   //scl 时钟偏移控制





assign  sda_i       = (IO_iic_sda == 1'b0)  ?  1'b0 : 1'b1  ;       //读总线 
assign  IO_iic_sda  = (sda_o == 1'b0)       ?  1'b0 : 1'bz  ;       //写总线,1'bz代表高阻,I2C外部通过上拉电阻,实现总线的高电平

PULLUP PULLUP_inst (.O(iic_sda));//仅对仿真有效,仿真设计iic_sda状态为高阻,模拟上拉电阻 

//scl 时钟分频器
always@(posedge I_clk)begin
    if(clkdiv < SCL_DIV)    
        clkdiv <= clkdiv + 1'b1;
    else begin
        clkdiv <= 16'd0; 
        scl_clk <= !scl_clk;
    end
end

assign  scl_offset  =   (clkdiv == OFFSET)    ;   //设置scl_offset的时间参数

always @(posedge I_clk)begin
    O_iic_scl <=  scl_offset ?  scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
end

//当IIC_S状态机处于,同时空闲状态,设置SCL为高电平,同时也是空闲,停止状态,用于产生起始位和停止位时序,否则寄存scl_clk时钟
always @(*) begin
    if(IIC_S == IDLE || IIC_S == STOP1 || IIC_S == STOP2)
        scl_r <= 1'b1;
    else 
        scl_r <= scl_clk;
end

//当进入IIC_S状态为启动、停止设置sda=0,结合scl产生起始位,或者(IIC_S == R_ACK && (rcnt != I_rd_cnt) sda=0,用于产生读操作的ACK
always @(*) begin
    if(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt)))begin
        sda_o <= 1'b0;
    end
    else if(IIC_S == W_WAIT)begin
        sda_o <= sda_r[7]; 
    end
    else begin
        sda_o <= 1'b1;                          //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK 
    end
end

//I2C数据发送模块,所有的写数据都通过此模块发送
always @(posedge scl_clk) begin
    if(IIC_S == W_ACK || IIC_S == START)begin   //IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_r
        sda_r <= I_wr_data[(wcnt*8) +: 8];      //寄存需要发发送的数据到sda_r
        if( rd_req ) begin
            sda_r <= {I_wr_data[7:1],1'b1};     //对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读
        end
    end
    else if(IIC_S == W_WAIT)begin               //当W_WAT状态,通过移位操作,把数据发送到数据总线
        sda_r <= {sda_r[6:0],1'b1};             //移位操作
    end
    else begin
        sda_r <= sda_r;
    end
end

//I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data
always @(negedge scl_clk)begin
    if(IIC_S == R_WAIT ) begin                  //当IIC_S == R_WAIT ||IIC_S == W_ACK(如果读操作,第1个BIT是W_ACK这个状态读)启动移位操作
        sda_i_r <= {sda_i_r[6:0],sda_i};
    end
    else if(IIC_S == R_ACK) begin               //当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_data
        O_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0];
    end
    else if(IIC_S == IDLE)begin                 //空闲状态,重置sda_i_r
        sda_i_r <= 8'd0;
    end
end

//总线忙状态
always @(posedge scl_clk or negedge I_rstn )begin
	if(I_rstn == 1'b0)begin
    	O_iic_busy <= 1'b0; 
    end
	else begin
    	if((I_iic_req == 1'b1 || rd_req == 1'b1 || O_iic_bus_error))begin//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
    	    O_iic_busy <= 1'b1; 
        end
        else if(IIC_S == IDLE)begin
        	O_iic_busy <= 1'b0;
        end
    end 		
end

//总线错误状态
always @(negedge scl_clk or negedge I_rstn )begin
	if(I_rstn == 1'b0)begin
    	O_iic_bus_error <= 1'b0;
    end  	
	else begin
    	if(IIC_S  == W_ACK && sda_i == 1'b1)begin//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
    	    O_iic_bus_error <= 1'b1; 
        end
        else if(I_iic_req == 0)begin
        	O_iic_bus_error <= 1'b0;
        end
    end 		
end




//状态机
always @(posedge scl_clk or negedge I_rstn) begin
    if (!I_rstn) begin
        wcnt    <=  8'd0    ;
        rcnt    <=  8'd0    ;
        rd_req  <=  1'd0    ;
        IIC_S   <=  IDLE    ;
    end 
    else begin
        case (IIC_S)                        //sda = 1   scl = 1
            IDLE:begin
                if ((I_iic_req == 1'b1)||(rd_req == 1'b1) )begin    //当I_iic_req == 1'b1代表启动传输 当 rd_req == 1'b1 代表读操作需要产生repeated start 重复启动  
                    IIC_S <= START;         //进入总线启动
                end 
                else begin
                    wcnt <= 8'd0;
                    rcnt <= 8'd0;
                end
            end 
            START:begin                     //这个状态,前面的代码,先设置sda = 0,scl_offset参数设置了scl_clk时钟的偏移,之后 scl_clk =0 即scl =0 产生起始位或者重复起始位
                bcnt <= 3'd7;               //设置bcnt的初值
                IIC_S <= W_WAIT;            //进入发送等待
            end         
            W_WAIT:begin                    //等待发送完成,这里发送8bits 数据,写器件地址,写寄存器地址,写数据,都在这个状态完成
                if (bcnt > 3'd0) begin      //如果8bits没发送完,直到发送完
                    bcnt <= bcnt - 1'b1;    //bcnt计数器,每发送1bit减1
                end 
                else begin                  //8bits发送完毕
                    wcnt <= wcnt + 1'b1;    //wcnt计数器,用于记录已经写了多少字节
                    IIC_S <= W_ACK;         //进入W_ACK状态
                end
            end 
            W_ACK:begin                     //等待WACK,此阶段,也判断是否有读操作
                if (wcnt < I_wr_cnt) begin  //判断是否所有数据发送(写)完成
                    bcnt <= 3'd7;           //如果没有写完,重置bcnt
                    IIC_S <= W_WAIT;        //继续回到W_WAIT等待数据发送(写)完成
                end 
                else if(I_rd_cnt > 3'd0)begin
                    if ((rd_req == 1'b0) && (I_iic_mode == 1'b1)) begin
                        rd_req  <=  1'b1  ;     //请求读操作
                        IIC_S   <=  IDLE  ;     //设置状态进入IDLE,根据rd_req的值会重新产生一次为读操作进行的repeated重复start
                    end 
                    else begin
                        IIC_S   <=  R_WAIT  ;   //进入读等待
                        bcnt    <=  3'd7    ;   
                    end
                end
                else begin
                    IIC_S <= STOP1;             //如果所有的发送完成,也没数据需要读,进入停止状态
                end
            end 
            R_WAIT:begin
                rd_req  <=  1'b0        ;       //重置读请求 
                bcnt    <=  bcnt - 1'b1 ;
                if (bcnt == 3'd0) begin
                    rcnt    <=  ( rcnt < I_rd_cnt ) ? ( rcnt + 1'b1 ) : rcnt ; //判断是否还有数据需要读
                    IIC_S   <=  R_ACK ;
                end
            end 
            R_ACK:begin
                bcnt    <=  3'd7 ;                                             //重置读请求bcnt计数器
                IIC_S   <=  ( rcnt < I_rd_cnt ) ? R_WAIT : STOP1    ;           //如果所有数据读完,进入停止状态 
            end 
            STOP1:begin //产生停止位 sda = 0 scl = 1
                rd_req  <=  1'b0    ;   
                IIC_S   <=  STOP2   ;   
            end 
            STOP2:begin //产生停止位  sda = 1 scl = 1
                IIC_S <= IDLE;
            end 
            default:begin                 
                IIC_S <= IDLE;
            end
        endcase
    end
end



endmodule

3、

状态机:

代码

`timescale 1ns / 1ps

module eeprom_rt
(
    input   wire            I_sysclk    ,   //系统时钟输入
    output  wire            O_iic_scl   ,   // I2C SCL时钟
    inout   wire            IO_iic_sda  ,   //I2C SDA数据总线
    output  wire    [3:0]   test_led    ,   //测试LED
    output  wire            led             //error LED
);

localparam  SYSCLKHZ    =   100_000_000;
localparam  T500MS_CNT  =   (SYSCLKHZ / 2 - 1) ;

reg     [8 :0] 	    rst_cnt   	 = 9'd0     ;   //延迟复位计数器
reg     [25:0]      t500ms_cnt	 = 26'd0    ;   //500ms计数器
reg     [19:0]      delay_cnt 	 = 20'd0    ;   //eeprom每次读写完后,延迟操作计数器
reg     [2 :0]	    TS_S 	  	 = 2'd0     ;   // 读写EEPROM状态机
reg     		    iic_req 	 = 1'b0     ;   //i2c总线,读/写请求信号
reg     [31:0]	    wr_data   	 = 32'd0    ;   //写数据寄存器
reg     [7 :0]	    wr_cnt 	  	 = 8'd0     ;   //写数据计数器
reg     [7 :0]	    rd_cnt 	  	 = 8'd0     ;   //读数据计数器
wire     		    iic_busy                ;   // i2c总线忙信号标志
wire    [31:0]      rd_data                 ;   // i2c读数据
wire    		    t500ms_en               ;   // 500ms延迟到使能

wire                IO_iic_sda_dg           ;
wire                iic_bus_error           ;   //i2c总线错误
reg                 iic_error = 1'b0        ;   //i2c 读出数据有错误

assign  test_led    =   rd_data[3:0]            ;//测试LED输出
assign  led         =   iic_error               ;//通过LED显示错误标志
assign  t500ms_en   =   (t500ms_cnt==T500MS_CNT);//500ms 使能信号

//系统复位计数器
always @(posedge I_sysclk ) begin
    if (!rst_cnt[8]) begin
        rst_cnt <= rst_cnt + 1'b1;
    end
end

//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败
always@(posedge I_sysclk) begin
    if(!rst_cnt[8])begin
        delay_cnt <= 0;
    end
    else if((TS_S == 3'd0 || TS_S == 3'd2 )) begin
        delay_cnt <= delay_cnt + 1'b1;
    end
    else begin 
        delay_cnt <= 0;
    end
end

//每间隔500ms状态机运行一次
always@(posedge I_sysclk) begin
    if(!rst_cnt[8])begin
        t500ms_cnt <= 0;
    end
    else if(t500ms_cnt == T500MS_CNT) begin
        t500ms_cnt <= 0;
    end
    else begin 
        t500ms_cnt <= t500ms_cnt + 1'b1;
    end
end

//状态机实现每次写1字节到EEPROM然后再读1字节
always @(posedge I_sysclk ) begin
    if (!rst_cnt[8]) begin
        iic_req     <=  1'b0    ;
        wr_data     <=  32'd0   ;
        rd_cnt      <=  8'd0    ;
        wr_cnt      <=  8'd0    ;
        iic_error   <=  1'b0    ;
        TS_S        <=  3'd0    ;
    end 
    else begin
        case (TS_S)
            0:begin     
                if (!iic_busy) begin        //当总线不忙,可以开启一次IIC数据操作
                    iic_req <=  1'b1    ;   //请求发送数据
                                            //数据寄存器中8'b10100000代表需要写的器件地址,第一个wr_data[15:8]代表了EEPROM内存地址,第二个wr_data[15:8]代表了写入数据   
                    wr_data <= {8'hfe,wr_data[15:8],wr_data[15:8],8'b10100000}; 
                    rd_cnt  <=  8'd0    ;   //不需要读数据
                    wr_cnt  <=  8'd3    ;   //需要写入3个BYTES数据,包含一个器件地址,1个EEPROM寄存器地址,一个数据
                    TS_S    <=  3'd1    ;        
                end
            end
            1:begin
                if (iic_busy == 1'b1) begin
                    iic_req <=  1'b0    ;
                    TS_S    <=  3'd2    ;
                end 
            end
            2:begin
                if ((!iic_busy) && (delay_cnt[19] == 1'b1)) begin//当总线非忙,可以开始一次I2C数据操作,该时间约不能低于500us,否则会导致EEPROM操作失败
                    iic_req <=  1'b1    ;   //请求接收数据
                    rd_cnt  <=  8'd1    ;   //需要读取一个字节
                    wr_cnt  <=  8'd2    ;   //需要写两个byte  一个器件地址8’d1010_0000 一个寄存器地址 wr_data[15:8] (I2C控制器会自定设置读写标志位)
                    TS_S    <=  3'd3    ;    
                end 
            end
            3:begin
                if (iic_busy == 1'b1) begin
                    iic_req <=  1'b0    ;
                    TS_S    <=  3'd4    ;
                end
            end
            4:begin
                if (!iic_busy) begin        //当总线非忙,代表前面读数据完成       
                    if (wr_data[23:16] != rd_data[7:0]) begin   //对比数据是否正确
                        iic_error   <=  1'b1    ;
                    end 
                    else begin
                        iic_error   <=  1'b0    ;
                    end
                    wr_data[15:8]   <=  wr_data[15:8] + 1'b1    ;   //地址和数据都加1
                    TS_S            <=  3'd5    ;
                end
            end
            5:begin
                if(t500ms_en == 1'b1)begin
                    TS_S    <=  3'd0;
                end
            end
            default: TS_S   <=  3'd0;
        endcase
    end
end

// 以下代码为在线逻辑分析仪观察调试部分
reg scl_r = 1'b0;
always @(posedge I_sysclk)begin //对O_iic_scl寄存1次
 scl_r <= O_iic_scl; 
end

//产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号
reg [8:0] dg_clk_cnt;
wire dg_clk = (dg_clk_cnt==0);//用scl_dg即O_iic_scl的跳变沿作为触发信号
always@(posedge I_sysclk) begin
    dg_clk_cnt <= dg_clk_cnt+ 1'b1;
end

ila_0 ila_debug (
	.clk(I_sysclk),//在线逻辑分析仪的时钟
	.probe0({rd_data[7:0],wr_data[23:0],TS_S,iic_error,iic_req,scl_r,IO_iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号
);

//例化I2C控制模块
iic_my#
(
.WMEN_LEN   (4              )   ,   //最大支持一次写入4BYTE(包含器件地址)
.RMEN_LEN   (4              )   ,   //最大支持一次读出4BYTE(包含器件地址)
.CLK_DIV    (SYSCLKHZ/100000)       //100KHZ I2C总线时钟
)
u_iic_my
(
    .I_clk              (I_sysclk       )   ,   //系统时钟
    .I_rstn             (rst_cnt[8]     )   ,   //系统复位
    .O_iic_scl          (O_iic_scl      )   ,   //I2C SCL总线时钟
    .IO_iic_sda         (IO_iic_sda     )   ,   //I2C SDA数据总线
    .I_wr_data          (wr_data        )   ,   //写数据寄存器
    .I_wr_cnt           (wr_cnt         )   ,   //需要写的数据BYTES
    .O_rd_data          (rd_data        )   ,    //读数据寄存器
    .I_rd_cnt           (rd_cnt         )   ,   //需要读的数据BYTES
    .I_iic_req          (iic_req        )   ,   //I2C控制器请求
    .I_iic_mode         (1'b1           )   ,   //读模式
    .O_iic_busy         (iic_busy       )   ,   //I2C控制器忙
    .O_iic_bus_error    (iic_bus_error  )   ,   //总线错误信号标志
    .IO_iic_sda_dg      (IO_iic_sda_dg  )       //debug IO_iic_sda
); 

endmodule

五、仿真验证

发送器件地址:

发送片内地址:

随机读:

六、上板验证


http://www.kler.cn/news/335624.html

相关文章:

  • 【Conda】Conda命令详解:高效更新与环境管理指南
  • 安装jdk安装开发环境与maven
  • 【postman】本地接口文件
  • State of ChatGPT ---- ChatGPT的技术综述
  • 二叉树基本概念讲解
  • uniapp超全user-agent判断 包括微信开发工具 hbuilder mac windows 安卓ios端及本地识别
  • Task与 async 和await关键字使用和探讨
  • 【Unity】Luban学习笔记
  • 【Kubernetes】常见面试题汇总(四十九)
  • Tomcat监控与调优:比Tomcat Manager更加强大的Psi-Probe
  • AI学习记录 - L2正则化详细解释(权重衰减)
  • MySQL基础之函数
  • 【PCL】Ubuntu22.04 安装 PCL 库
  • 时序逻辑-延一拍/打一拍?
  • Chrome无法拖入加载.crx扩展文件(以IDM为例)
  • 【力扣 | SQL题 | 每日四题】力扣613, 579, 578, 580, 585
  • C# 表达式与运算符
  • (十八)、登陆 k8s 的 kubernetes-dashboard 更多可视化工具
  • 关于CSS 案例_新闻内容展示
  • Ubuntu 22.04.4 LTS更换下载源