uisrc 发表于 2023-12-29 19:11:47

2-3-21 FPGA读写I2C接口EEPROM实验

软件版本: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(inputwire I_sysclk,//系统时钟输入output wire O_iic_scl,// I2C SCL时钟inoutwire IO_iic_sda,//I2C SDA数据总线output wire O_test_led,//测试LEDoutput 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; //定义系统时钟50MHZlocalparam T500MS_CNT   = (SYSCLKHZ/2-1); //定义每500ms访问一次EEPROM
reg rst_cnt      = 9'd0;//延迟复位计数器reg t500ms_cnt   = 26'd0;//500ms计数器reg delay_cnt    = 20'd0;//eeprom每次读写完后,延迟操作计数器reg TS_S         = 2'd0; // 读写EEPROM状态机reg         iic_req      = 1'b0; //i2c总线,读/写请求信号reg wr_data      = 32'd0;//写数据寄存器reg wr_cnt       = 8'd0;//写数据计数器reg rd_cnt       = 8'd0;//读数据计数器wire      iic_busy; // i2c总线忙信号标志wire 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;//测试LED输出assign O_led = iic_error;//通过LED显示错误标志assign t500ms_en = (t500ms_cnt==T500MS_CNT);//500ms 使能信号
//通过内部计数器实现复位always@(posedge I_sysclk) begin    if(!rst_cnt)      rst_cnt <= rst_cnt + 1'b1;end//I2C总线延迟间隔操作,该时间约不能低于500us,否则会导致EEPROM操作失败always@(posedge I_sysclk) begin    if(!rst_cnt)      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)      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)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,wr_data,8'b10100000};//数据寄存器中8'b10100000代表需要写的器件地址,第一个wr_data代表了EEPROM内存地址,第二个wr_data代表了写入数据            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)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)(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 != rd_data)//比对数据是否正确                iic_error <= 1'b1;//如果有错误,设置iic_error=1            else                iic_error <= 1'b0;//如果有错误,设置iic_error=0                wr_data <= wr_data + 1'b1;//wr_data+1 地址和数据都加1            TS_S    <= 3'd5;      end      5:if(t500ms_en)begin//延迟操作后进入下一个状态            TS_S    <= 3'd0;      end      default:            TS_S    <= 3'd0;    endcase   endend
// 以下代码为在线逻辑分析仪观察调试部分reg scl_r = 1'b0;always @(posedge I_sysclk)begin //对O_iic_scl寄存1次 scl_r <= O_iic_scl;end
//产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号reg 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,wr_data,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),//系统复位.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 memory;reg address;reg memory_buf;reg sda_buf;reg shift;reg addr_byte;reg ctrl_byte;reg 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:1'bz;//--------------------寄存器和存储器初始化------------------------------initialbeginaddr_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=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,addr_byte};    memory= memory_buf;    $display("eeprm---memory[%0h]=%0h",address,memory);    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,addr_byte};         sda_buf =memory ;         shift_out;         State = 2'b00;    end    end    endtask
    // ---SDA 数据线上的数据存入寄存器 ,数据在SCL的高电平有效------------------    task shift_in;    output shift;    begin    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(posedge scl) shift=sda;    @(negedge scl) //ACK    begin    #`timeslice;//模拟芯片的延迟输出ACK    out_flag = 1;    sda_buf=0;    end    @(negedge scl)//结束ACK    #`timeslice out_flag= 0;    endendtask
    //----------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=1;    @(negedge scl) # `timeslice out_flag=0;    endendtask

    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 START6 在线仿真设置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 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,wr_data,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;


下载程序前,先确保FPGA工程已经编译。7.1 硬件连接(该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)请确保下载器和开发板已经正确连接,并且开发板已经上电。(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)
7.2 运行结果1.通过LED观察I2C的读写结果,可以看到LED规律运行2.通过ILA在线逻辑分析仪观察Capure模式必须设置BASIC模式用500ms作为触发信号运行后抓到的波形如下
页: [1]
查看完整版本: 2-3-21 FPGA读写I2C接口EEPROM实验