21 读写I2C接口EEPROM实验

发布时间 2023-12-29 17:12:44作者: 米联客(milianke)

软件版本:VIVADO2021.1

操作系统:WIN10 64bit

硬件平台:适用XILINX A7/K7/Z7/ZU/KU系列FPGA

登录米联客(MiLianKe)FPGA社区-www.uisrc.com观看免费视频课程、在线答疑解惑!

1 概述

前面的课程中,我们学习了I2C总线协议,以及介绍了米联客I2C Master控制器的实现原理、内部状态机、I2C时序产生、外部控制接口。本文开始,后面所涉及的I2C总线相关内容都会使用该控制器实现。本实验使用米联客的uii2c控制器实现对EEPROM的访问。

2 EEPROM-24C02介绍

如下图,A0-A2是EEPROM I2C器件地址,SDA和SCL是EEPROM I2C总线SLAVE接口,WP是保护脚,一般接VCC。

 

24LXX 器件地址如下图

我们看下24C02的写时序,可以看到,支持单个字节的写,以及多个字节的写。首先发送器件的地址,然后发送需要写EEPROM存储空间的地址,之后就是数据,对于读操作一次可以写1个字节或者多个字节。

写字节操作BYTE WRITE

在起始位产生后,先写器件地址,再写芯片内存地址,再写入数据,最后产生停止位,每写一个字节都要产生ACK位。

页写PAGE WRITE

页写和字节写差不多,在字节写的基础上,连续写入数据,最后产生停止位。

我们看下24C02的读时序,可以看到,支持单个字节的读,以及多个字节的读。以下支持3种读的方式:

读当前地址CURRENT ADDRESS READ

只要发送器件地址就能读当前内存地址所指向的地址空间数据,最后的读数据可以不需要发送ACK

随机读RANDOM READ

需要发送器件地址,然后发送内存地址,之后再发送器件地址并且读取到数据,最后的读数据可以不需要发送ACK。

连续读SEQUENTIAL READ

可以从第一种和第二种读方式启动后,连续读取,但是需要注意的时候除最后一个读数据,其他的读主机都需要发送ACK。

I2C起始停止时序

I2C时序参数

3 用户程序设计

3.1 用户接口时序

先温习下前面课程内容中关于I2C控制器的功能模块可以接口信号:

IO_sda为I2C双向数据总线

O_scl为I2C时钟

I_wr_cnt写数据字节长度,包含了器件地址,发送I_iic_req前,预设该值

I_rd_cnt读数据字节长度,仅包含读回有效部分,发送I_iic_req前,预设该值

I_wr_data写入的数据

O_rd_data读出的数据,如果是读请求,当O_iic_busy从高变低代表数据读回有效

I_iic_req I2C操作请求,根据I_rd_cnt是否大于0决定是否有读请求

I_iic_mode是否支持随机读写,发送I_iic_req前,预设该值

O_iic_busy总线忙

 

请求一次I2C传输的控制时序如下:

首先在O_iic_busy=0即I2C总线空闲情况下,设置I_wr_cnt,I_rd_cnt,I_wr_data,并且设置I_iic_req=1,启动I2C传输。当O_iic_busy=1说明I2C控制器开始传输,这时候可以设置I_iic_req=0,结束本次请求,并且等待O_iic_busy=0,当O_iic_busy=0代表本次传

传输结束.如果发送的是读请求(当I_rd_cnt>0),则此时O_rd_data有效可以读走数据。

3.2 RTC用户读写程序设计

3.2.1 状态机介绍

3.2.2 用户接口程序源码

/*******************************eeprom_test*********************

--1.本实验目的用于验证米联客I2C控制器

--2.通过写入数据到EEPROM并且读出比对数据,确认I2C控制器是否工作正常

--3.本实验也演示了,如何使用米联客I2C控制器的信号接口

*********************************************************************/

`timescale 1ns / 1ns//仿真时间间隔/精度

 

module eeprom_test

(

input  wire I_sysclk,//系统时钟输入

output wire O_iic_scl,// I2C SCL时钟

inout  wire IO_iic_sda,//I2C SDA数据总线

output wire [3:0]O_test_led,//测试LED

output wire O_led,

output wire O_card_power_en //error LED

);

 

assign O_card_power_en = 1'b1; //子卡上电  

 

//localparam SYSCLKHZ     =  50_000_00; //仿真用系统时钟

localparam SYSCLKHZ     =  50_000_000; //定义系统时钟50MHZ

localparam T500MS_CNT   = (SYSCLKHZ/2-1); //定义每500ms访问一次EEPROM

 

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 iic_sda_dg;

wire iic_bus_error;  //i2c总线错误

reg iic_error = 1'b0; //i2c 读出数据有错误

assign O_test_led = rd_data[3:0];//测试LED输出

assign O_led = iic_error;//通过LED显示错误标志

assign t500ms_en = (t500ms_cnt==T500MS_CNT);//500ms 使能信号

                 

//通过内部计数器实现复位

always@(posedge I_sysclk) begin

    if(!rst_cnt[8])

        rst_cnt <= rst_cnt + 1'b1;

end

//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败

always@(posedge I_sysclk) begin

    if(!rst_cnt[8])

        delay_cnt <= 0;

    else if((TS_S == 3'd0 || TS_S == 3'd2 ))

        delay_cnt <= delay_cnt + 1'b1;

    else

        delay_cnt <= 0;

End

 

//每间隔500ms状态机运行一次

always@(posedge I_sysclk) begin

    if(!rst_cnt[8])

        t500ms_cnt <= 0;

    else if(t500ms_cnt == T500MS_CNT)

        t500ms_cnt <= 0;

    else

        t500ms_cnt <= t500ms_cnt + 1'b1;

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:if(!iic_busy)begin//当总线非忙,可以开始一次I2C数据操作

            iic_req <= 1'b1;//请求发送数据

            wr_data <= {8'hfe,wr_data[15:8],wr_data[15:8],8'b10100000};//数据寄存器中8'b10100000代表需要写的器件地址,第一个wr_data[15:8]代表了EEPROM内存地址,第二个wr_data[15:8]代表了写入数据

            rd_cnt  <= 8'd0; //不需要读数据

            wr_cnt  <= 8'd3; //需要写入3个BYTES数据,包含1个器件地址,1个EEPROM 寄存器地址 1个数据  

            TS_S     <= 3'd1;//进入下一个状态      

        end

        1:if(iic_busy)begin

            iic_req  <= 1'b0; //重置iic_req=0

            TS_S     <= 3'd2;

        end

        2:if(!iic_busy&&delay_cnt[19])begin //当总线非忙,可以开始一次I2C数据操作,该时间约不能低于500us,否则会导致EEPROM操作失败

            iic_req  <= 1'b1;//请求接收数据

            rd_cnt  <= 8'd1; //需要读1个BYTE

            wr_cnt  <= 8'd2; //需要些2个BYTE(1个器件地址8'b10100000,和1个寄存器地址wr_data[15:8])(I2C控制器会自定设置读写标志位)

            TS_S    <= 3'd3;  //进入下一个状态

        end    

        3:if(iic_busy)begin

            iic_req  <= 1'b0; //重置iic_req=0

            TS_S     <= 3'd4;

        end    

        4:if(!iic_busy)begin//当总线非忙,代表前面读数据完成

            if(wr_data[23:16] != rd_data[7:0])//比对数据是否正确

                iic_error <= 1'b1;//如果有错误,设置iic_error=1

            else

                iic_error <= 1'b0;//如果有错误,设置iic_error=0

                wr_data[15:8] <= wr_data[15:8] + 1'b1;//wr_data[15:8]+1 地址和数据都加1

            TS_S    <= 3'd5;

        end

        5:if(t500ms_en)begin//延迟操作后进入下一个状态

            TS_S    <= 3'd0;

        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,iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号

);

 

//例化I2C控制模块

uii2c#

(

.WMEN_LEN(4),//最大支持一次写入4BYTE(包含器件地址)

.RMEN_LEN(4),//最大支持一次读出4BYTE(包含器件地址)

.CLK_DIV(SYSCLKHZ/50000)//100KHZ I2C总线时钟

)

uii2c_inst

(

.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(iic_sda_dg)//debug iic_sda

);

     

endmodule

4 FPGA工程

fpga工程的创建过程不再重复

米联客的代码管理规范,在对应的FPGA工程路径下创建uisrc路径,并且创建以下文件夹

01_rtl:放用户编写的rtl代码

02_sim:仿真文件或者工程

03_ip:放使用到的ip文件

04_pin:放fpga的pin脚约束文件或者时序约束文件

05_boot:放编译好的bit或者bin文件(一般为空)

06_doc:放本一些相关文档(一般为空)

5 RTL仿真

5.1仿真激励文件

eeprom仿真模型

`define timeslice 20

 

module eeprom(

input scl,

inout sda);

reg out_flag;

reg [7:0] memory[2047:0];

reg[10:0] address;

reg[7:0] memory_buf;

reg [7:0] sda_buf;

reg [7:0] shift;

reg [7:0] addr_byte;

reg [7:0] ctrl_byte;

reg [1:0] State;

integer i;

 

// ----------------------------------------------

parameter r7=8'b10101111,w7=8'b10101110,

          r6=8'b10101101,w6=8'b10101100,

             r5=8'b10101011,w5=8'b10101010,

             r4=8'b10101001,w4=8'b10101000,

             r3=8'b10100111,w3=8'b10100110,

          r2=8'b10100101,w2=8'b10100100,

             r1=8'b10100011,w1=8'b10100010,

             r0=8'b10100001,w0=8'b10100000;

               

//---------------------------------------------------

 

assign sda= (out_flag == 1)?sda_buf[7]:1'bz;

//--------------------寄存器和存储器初始化------------------------------

initial

begin

addr_byte   =0;

ctrl_byte   =0;

out_flag    =0;

sda_buf     =0;

State       =2'b00;

memory_buf  =0;

address     =0;

shift       =0;

for(i=0;i<=2047;i=i+1)

memory[i]=0;

end

 

//////--------------启动信号检测--------------

always @(negedge sda)

            if(scl == 1)

             begin

                State=State+1;

                if(State==2'b11)

                 disable write_to_eeprm;

             end                

 /////-------------------主状态机-----------------------

always @(posedge sda)

                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_eeprm;

                     end

                     else

                        State = 2'b00;

                end

                       

                2'b11:

                     read_from_eeprm;

                     default:

                           State=2'b00;

                     endcase

                     end

 

                 

//--------------操作停止------------------

task stop_W_R;

       begin

           

         State = 2'b00;

         addr_byte  =0;

         ctrl_byte  =0;

         out_flag   =0;

         sda_buf   =0;

         end

    endtask

//----------------读进控制字和存储单元地址-------------------

    task read_in;

    begin

    shift_in(ctrl_byte);

    shift_in(addr_byte);

    end

    endtask

    //-------------EEPROM--------------------

    task write_to_eeprm;

    begin

    shift_in(memory_buf);

    address    ={ctrl_byte[3:1],addr_byte};

    memory[address]  = memory_buf;

    $display("eeprm---memory[%0h]=%0h",address,memory[address]);

    State= 2'b00;

    end

    endtask

     

     

    //-------------EEPROM读操作_______________________

    task read_from_eeprm;

    begin

    shift_in(ctrl_byte);

    if(ctrl_byte == r7 || ctrl_byte == r6 || ctrl_byte == r5 || ctrl_byte == r4 || ctrl_byte == r3 || ctrl_byte == r2

        || ctrl_byte == r1 || ctrl_byte == r0)

         begin

         address = {ctrl_byte[3:1],addr_byte};

         sda_buf =memory [address];

         shift_out;

         State = 2'b00;

    end

    end

    endtask

     

    // ---SDA 数据线上的数据存入寄存器 ,数据在SCL的高电平有效------------------

    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) //ACK

    begin

    #`timeslice;//模拟芯片的延迟输出ACK

    out_flag = 1;

    sda_buf  =0;

    end

    @(negedge scl)//结束ACK

    #`timeslice out_flag  = 0;

    end

endtask

 

    //----------EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化

   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

 

顶层调用接口仿真代码

`timescale 1ns / 1ns

 

module eeprom_test_tb;

reg I_sysclk = 1'b1;

wire O_iic_scl;

wire IO_iic_sda;

 

pullup( IO_iic_sda );

 

eeprom_test eeprom_test_inst

(

.I_sysclk(I_sysclk),

.O_iic_scl(O_iic_scl),

.IO_iic_sda(IO_iic_sda)

);

     

eeprom eeprom_inst(

.scl(O_iic_scl),

.sda(IO_iic_sda)

);    

 

always

    begin

        #10 I_sysclk = ~I_sysclk;

    end

   

endmodule

5.2仿真结果

启动后,右击需要观察的信号,添加到波形窗口,并仿真。

放大观察I2C时序,查看写操作START和ACK位置

放大观察I2C时序,查看写读操作Repeated START

6 在线仿真

设置ila的采样深度为8192,越大观察的数据越多,但是消耗的Bram也越多,设置支持capture control模式

设置需要观察数据的总位宽

注意代码中,通过512分频器产生的信号作为capture信号

// 以下代码为在线逻辑分析仪观察调试部分

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,iic_sda_dg,iic_bus_error,dg_clk,t500ms_en}) // 需要观察的调试信号

);

7 下载演示

为了方便观察结果,使用LED观察,每间隔500ms完成一次读写操作

assign O_test_led = rd_data[3:0];

下载程序前,先确保FPGA工程已经编译。

7.1 硬件连接

(该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)

请确保下载器和开发板已经正确连接,并且开发板已经上电。(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)

 

7.2 运行结果

1.通过LED观察I2C的读写结果,可以看到LED规律运行

2.通过ILA在线逻辑分析仪观察

Capure模式必须设置BASIC模式

用500ms作为触发信号

运行后抓到的波形如下