[X]关闭

[米联客-XILINX-H3_CZ08_7100] FPGA基础篇连载-21读写I2C接口EEPROM实验

文档创建者:FPGA课程
浏览次数:315
最后更新:2024-08-30
文档课程分类-AMD-ZYNQ
AMD-ZYNQ: ZYNQ-FPGA部分 » 2_FPGA实验篇(仅旗舰) » 1-FPGA基础入门实验
本帖最后由 FPGA课程 于 2024-8-30 16:47 编辑

​ 软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用 XILINX A7/K7/Z7/ZU/KU 系列 FPGA
实验平台:米联客-MLK-H3-CZ08-7100开发板
板卡获取平台:https://milianke.tmall.com/
登录“米联客”FPGA社区 http://www.uisrc.com 视频课程、答疑解惑!



1概述
前面的课程中,我们学习了I2C总线协议,以及介绍了米联客I2C Master控制器的实现原理、内部状态机、I2C时序产生、外部控制接口。本文开始,后面所涉及的I2C总线相关内容都会使用该控制器实现。本实验使用米联客的uii2c控制器实现对EEPROM的访问。
2EEPROM-24C02介绍
如下图,A0-A2是EEPROM I2C器件地址,SDA和SCL是EEPROM I2C总线SLAVE接口,WP是保护脚,一般接VCC。

94f5dd3d0406442da47a5ef1e5b15a96.jpg
24LXX 器件地址如下图
fb4cea1fe62b4dd3a6e2ff889e1cbc9c.jpg
我们看下24C02的写时序,可以看到,支持单个字节的写,以及多个字节的写。首先发送器件的地址,然后发送需要写EEPROM存储空间的地址,之后就是数据,对于读操作一次可以写1个字节或者多个字节。
写字节操作BYTE WRITE
在起始位产生后,先写器件地址,再写芯片内存地址,再写入数据,最后产生停止位,每写一个字节都要产生ACK位。
bca5d445205b4942977f74ccb406010b.jpg
页写PAGE WRITE
页写和字节写差不多,在字节写的基础上,连续写入数据,最后产生停止位。
298bb39e20294d5fb6dc76d6f1fbe6eb.jpg
我们看下24C02的读时序,可以看到,支持单个字节的读,以及多个字节的读。以下支持3种读的方式:
读当前地址CURRENT ADDRESS READ
只要发送器件地址就能读当前内存地址所指向的地址空间数据,最后的读数据可以不需要发送ACK
4dc7b6d808d249a39db8fd226f8ab15e.jpg
随机读RANDOM READ
需要发送器件地址,然后发送内存地址,之后再发送器件地址并且读取到数据,最后的读数据可以不需要发送ACK。
dd3b63c3f29c48b98772618a5974c747.jpg
连续读SEQUENTIAL READ
可以从第一种和第二种读方式启动后,连续读取,但是需要注意的时候除最后一个读数据,其他的读主机都需要发送ACK。
6d8de83d7f6a48f9b6335d764f5bf999.jpg
I2C起始停止时序
2d2eb87f526342718a63b299bd31d646.jpg
I2C时序参数
b5662b020eae48979db76d122f70d1de.jpg
badf873d4de04ae0be598c2ad623799e.jpg
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总线忙

c093205497f346d79d56213da6b50257.jpg
请求一次I2C传输的控制时序如下:
bee66bc67fd3464fb0acf1da61366d19.jpg
首先在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 状态机介绍
937cd207e07c4f56a1088eb60b8bae75.jpg
3.2.2 用户接口程序源码
  1. /*******************************eeprom_test*********************
  2. --1.本实验目的用于验证米联客I2C控制器
  3. --2.通过写入数据到EEPROM并且读出比对数据,确认I2C控制器是否工作正常
  4. --3.本实验也演示了,如何使用米联客I2C控制器的信号接口
  5. *********************************************************************/
  6. `timescale 1ns / 1ns//仿真时间间隔/精度

  7. module eeprom_test
  8. (
  9. input  wire I_sysclk_p,
  10. input  wire I_sysclk_n,//系统时钟输入
  11. output wire O_iic_scl,// I2C SCL时钟
  12. inout  wire IO_iic_sda,//I2C SDA数据总线
  13. output wire [3:0]O_test_led,//测试LED
  14. output wire O_led //error LED
  15. );
  16. //localparam SYSCLKHZ     =  100_000_00; //仿真用系统时钟
  17. localparam SYSCLKHZ     =  100_000_000; //定义系统时钟100MHZ
  18. localparam T500MS_CNT   = (SYSCLKHZ/2-1); //定义每500ms访问一次EEPROM
  19. wire I_clk;
  20. IBUFGDS CLK_U(
  21. .I(I_sysclk_p),
  22. .IB(I_sysclk_n),
  23. .O(I_clk)
  24. );
  25. reg [8 :0]  rst_cnt      = 9'd0;//延迟复位计数器
  26. reg [25:0]  t500ms_cnt   = 26'd0;//500ms计数器
  27. reg [19:0]  delay_cnt    = 20'd0;//eeprom每次读写完后,延迟操作计数器
  28. reg [2 :0]  TS_S         = 2'd0; // 读写EEPROM状态机
  29. reg         iic_req      = 1'b0; //i2c总线,读/写请求信号
  30. reg [31:0]  wr_data      = 32'd0;//写数据寄存器
  31. reg [7 :0]  wr_cnt       = 8'd0;//写数据计数器
  32. reg [7 :0]  rd_cnt       = 8'd0;//读数据计数器
  33. wire        iic_busy; // i2c总线忙信号标志
  34. wire [31:0] rd_data;  // i2c读数据
  35. wire        t500ms_en;// 500ms延迟到使能
  36. wire iic_sda_dg;
  37. wire iic_bus_error;  //i2c总线错误
  38. reg iic_error = 1'b0; //i2c 读出数据有错误
  39. assign O_test_led = rd_data[3:0];//测试LED输出
  40. assign O_led = iic_error;//通过LED显示错误标志
  41. assign t500ms_en = (t500ms_cnt==T500MS_CNT);//500ms 使能信号      
  42. //通过内部计数器实现复位
  43. always@(posedge I_clk) begin
  44.     if(!rst_cnt[8])
  45.         rst_cnt <= rst_cnt + 1'b1;
  46. end

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

  56. //每间隔500ms状态机运行一次
  57. always@(posedge I_clk) begin
  58.     if(!rst_cnt[8])
  59.         t500ms_cnt <= 0;
  60.     else if(t500ms_cnt == T500MS_CNT)
  61.         t500ms_cnt <= 0;
  62.     else
  63.         t500ms_cnt <= t500ms_cnt + 1'b1;
  64. end

  65. //状态机实现每次写1字节到EEPROM然后再读1字节
  66. always@(posedge I_clk) begin
  67.     if(!rst_cnt[8])begin
  68.         iic_req   <= 1'b0;
  69.         wr_data   <= 32'd0;
  70.         rd_cnt    <= 8'd0;
  71.         wr_cnt    <= 8'd0;
  72.         iic_error <= 1'b0;
  73.         TS_S      <= 3'd0;   
  74.     end
  75.     else begin
  76.         case(TS_S)
  77.         0:if(!iic_busy)begin//当总线非忙,可以开始一次I2C数据操作
  78.             iic_req <= 1'b1;//请求发送数据
  79.             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]代表了写入数据
  80.             rd_cnt  <= 8'd0; //不需要读数据
  81.             wr_cnt  <= 8'd3; //需要写入3个BYTES数据,包含1个器件地址,1个EEPROM 寄存器地址 1个数据   
  82.             TS_S     <= 3'd1;//进入下一个状态      
  83.         end
  84.         1:if(iic_busy)begin
  85.             iic_req  <= 1'b0; //重置iic_req=0
  86.             TS_S     <= 3'd2;
  87.         end
  88.         2:if(!iic_busy&&delay_cnt[19])begin //当总线非忙,可以开始一次I2C数据操作,该时间约不能低于500us,否则会导致EEPROM操作失败
  89.             iic_req  <= 1'b1;//请求接收数据
  90.             rd_cnt  <= 8'd1; //需要读1个BYTE
  91.             wr_cnt  <= 8'd2; //需要些2个BYTE(1个器件地址8'b10100000,和1个寄存器地址wr_data[15:8])(I2C控制器会自定设置读写标志位)
  92.             TS_S    <= 3'd3;  //进入下一个状态
  93.         end     
  94.         3:if(iic_busy)begin
  95.             iic_req  <= 1'b0; //重置iic_req=0
  96.             TS_S     <= 3'd4;
  97.         end   
  98.         4:if(!iic_busy)begin//当总线非忙,代表前面读数据完成
  99.             if(wr_data[23:16] != rd_data[7:0])//比对数据是否正确
  100.                 iic_error <= 1'b1;//如果有错误,设置iic_error=1
  101.             else
  102.                 iic_error <= 1'b0;//如果有错误,设置iic_error=0
  103.                 wr_data[15:8] <= wr_data[15:8] + 1'b1;//wr_data[15:8]+1 地址和数据都加1
  104.             TS_S    <= 3'd5;
  105.         end
  106.         5:if(t500ms_en)begin//延迟操作后进入下一个状态
  107.             TS_S    <= 3'd0;
  108.         end
  109.         default:
  110.             TS_S    <= 3'd0;
  111.     endcase
  112.    end
  113. end

  114. // 以下代码为在线逻辑分析仪观察调试部分
  115. reg scl_r = 1'b0;
  116. always @(posedge I_clk)begin //对O_iic_scl寄存1次
  117. scl_r <= O_iic_scl;
  118. end
  119. //产生一个触发时钟,这个时钟是系统时钟的512倍分频,这样抓取总线的时候,可以看到更多I2C的有效信号
  120. reg [8:0] dg_clk_cnt;
  121. wire dg_clk = (dg_clk_cnt==0);//用scl_dg即O_iic_scl的跳变沿作为触发信号
  122. always@(posedge I_clk) begin
  123.     dg_clk_cnt <= dg_clk_cnt+ 1'b1;
  124. end

  125. ila_0 ila_debug (
  126.     .clk(I_clk),//在线逻辑分析仪的时钟
  127.     .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}) // 需要观察的调试信号
  128. );
  129. //例化I2C控制模块
  130. uii2c#
  131. (
  132. .WMEN_LEN(4),//最大支持一次写入4BYTE(包含器件地址)
  133. .RMEN_LEN(4),//最大支持一次读出4BYTE(包含器件地址)
  134. .CLK_DIV(SYSCLKHZ/100000)//100KHZ I2C总线时钟
  135. )
  136. uii2c_inst
  137. (
  138. .I_clk(I_clk),//系统时钟
  139. .I_rstn(rst_cnt[8]),//系统复位
  140. .O_iic_scl(O_iic_scl),//I2C SCL总线时钟
  141. .IO_iic_sda(IO_iic_sda),//I2C SDA数据总线
  142. .I_wr_data(wr_data),//写数据寄存器
  143. .I_wr_cnt(wr_cnt),//需要写的数据BYTES
  144. .O_rd_data(rd_data), //读数据寄存器
  145. .I_rd_cnt(rd_cnt),//需要读的数据BYTES
  146. .I_iic_req(iic_req),//I2C控制器请求
  147. .I_iic_mode(1'b1),//读模式
  148. .O_iic_busy(iic_busy),//I2C控制器忙
  149. .O_iic_bus_error(iic_bus_error),//总线错误信号标志
  150. .IO_iic_sda_dg(iic_sda_dg)//debug IO_iic_sda
  151. );  
  152. endmodule
复制代码


4FPGA工程
fpga工程的创建过程不再重复
0131045670df42528ac92b130491c007.jpg
米联客的代码管理规范,在对应的FPGA工程路径下创建uisrc路径,并且创建以下文件夹
01_rtl:放用户编写的rtl代码
02_sim:仿真文件或者工程
03_ip:放使用到的ip文件
04_pin:放fpga的pin脚约束文件或者时序约束文件
05_boot:放编译好的bit或者bin文件(一般为空)
06_doc:放本一些相关文档(一般为空)
57d04d380e754afbae26e9b739bb0188.jpg

5 RTL仿真5.1仿真激励文件
eeprom仿真模型
  1. `define timeslice 20
  2. module eeprom(
  3. input scl,
  4. inout sda);
  5. reg out_flag;
  6. reg [7:0] memory[2047:0];
  7. reg[10:0] address;
  8. reg[7:0] memory_buf;
  9. reg [7:0] sda_buf;
  10. reg [7:0] shift;
  11. reg [7:0] addr_byte;
  12. reg [7:0] ctrl_byte;
  13. reg [1:0] State;
  14. integer i;

  15. // ----------------------------------------------
  16. parameter r7=8'b10101111,w7=8'b10101110,
  17.           r6=8'b10101101,w6=8'b10101100,
  18.              r5=8'b10101011,w5=8'b10101010,
  19.              r4=8'b10101001,w4=8'b10101000,
  20.              r3=8'b10100111,w3=8'b10100110,
  21.           r2=8'b10100101,w2=8'b10100100,
  22.              r1=8'b10100011,w1=8'b10100010,
  23.              r0=8'b10100001,w0=8'b10100000;
  24.             
  25. //---------------------------------------------------

  26. assign sda= (out_flag == 1)?sda_buf[7]:1'bz;
  27. //--------------------寄存器和存储器初始化------------------------------
  28. initial
  29. begin
  30. addr_byte   =0;
  31. ctrl_byte   =0;
  32. out_flag    =0;
  33. sda_buf     =0;
  34. State       =2'b00;
  35. memory_buf  =0;
  36. address     =0;
  37. shift       =0;
  38. for(i=0;i<=2047;i=i+1)
  39. memory[i]=0;
  40. end

  41. //////--------------启动信号检测--------------
  42. always @(negedge sda)
  43.             if(scl == 1)
  44.              begin
  45.                 State=State+1;
  46.                 if(State==2'b11)
  47.                  disable write_to_eeprm;
  48.              end               
  49. /////-------------------主状态机-----------------------
  50. always @(posedge sda)
  51.                 if(scl == 1)
  52.                 stop_W_R;
  53.                 else
  54.                 begin
  55.                 casex(State)  
  56.                 2'b01:
  57.                 begin
  58.                 read_in;
  59.                     if(ctrl_byte == w7||ctrl_byte == w6|| ctrl_byte == w5
  60.                     || ctrl_byte == w4 || ctrl_byte == w3 || ctrl_byte == w2 ||ctrl_byte == w1 ||ctrl_byte == w0)
  61.                      begin
  62.                         State = 2'b10;
  63.                         write_to_eeprm;
  64.                      end
  65.                      else
  66.                         State = 2'b00;
  67.                 end
  68.                      
  69.                 2'b11:
  70.                      read_from_eeprm;
  71.                      default:
  72.                            State=2'b00;
  73.                      endcase
  74.                      end

  75.                
  76. //--------------操作停止------------------
  77. task stop_W_R;
  78.        begin
  79.          
  80.          State = 2'b00;
  81.          addr_byte  =0;
  82.          ctrl_byte  =0;
  83.          out_flag   =0;
  84.          sda_buf   =0;
  85.          end
  86.     endtask
  87. //----------------读进控制字和存储单元地址-------------------
  88.     task read_in;
  89.     begin
  90.     shift_in(ctrl_byte);
  91.     shift_in(addr_byte);
  92.     end
  93.     endtask
  94.     //-------------EEPROM--------------------
  95.     task write_to_eeprm;
  96.     begin
  97.     shift_in(memory_buf);
  98.     address    ={ctrl_byte[3:1],addr_byte};
  99.     memory[address]  = memory_buf;
  100.     $display("eeprm---memory[%0h]=%0h",address,memory[address]);
  101.     State= 2'b00;
  102.     end
  103.     endtask
  104.    
  105.    
  106.     //-------------EEPROM读操作_______________________
  107.     task read_from_eeprm;
  108.     begin
  109.     shift_in(ctrl_byte);
  110.     if(ctrl_byte == r7 || ctrl_byte == r6 || ctrl_byte == r5 || ctrl_byte == r4 || ctrl_byte == r3 || ctrl_byte == r2
  111.         || ctrl_byte == r1 || ctrl_byte == r0)
  112.          begin
  113.          address = {ctrl_byte[3:1],addr_byte};
  114.          sda_buf =memory [address];
  115.          shift_out;
  116.          State = 2'b00;
  117.     end
  118.     end
  119.     endtask
  120.    
  121.     // ---SDA 数据线上的数据存入寄存器 ,数据在SCL的高电平有效------------------
  122.     task shift_in;
  123.     output[7:0] shift;
  124.     begin
  125.     @(posedge scl) shift[7]=sda;
  126.     @(posedge scl) shift[6]=sda;
  127.     @(posedge scl) shift[5]=sda;
  128.     @(posedge scl) shift[4]=sda;
  129.     @(posedge scl) shift[3]=sda;
  130.     @(posedge scl) shift[2]=sda;
  131.     @(posedge scl) shift[1]=sda;
  132.     @(posedge scl) shift[0]=sda;
  133.     @(negedge scl) //ACK
  134.     begin
  135.     #`timeslice;//模拟芯片的延迟输出ACK
  136.     out_flag = 1;
  137.     sda_buf  =0;
  138.     end
  139.     @(negedge scl)//结束ACK
  140.     #`timeslice out_flag  = 0;
  141.     end
  142.     endtask
  143.     //----------EEPROM存储器中的数据通过SDA数据线输出,数据在SCL低电平时变化
  144.    task shift_out;
  145.     begin
  146.     out_flag= 1;
  147.     for(i=6;i>=0;i=i-1)
  148.     begin
  149.    
  150.     @(negedge scl);
  151.     # `timeslice;
  152.     sda_buf = sda_buf<<1;
  153.     end
  154.    @(negedge scl) # `timeslice sda_buf[7]=1;
  155.     @(negedge scl) # `timeslice out_flag=0;
  156.     end
  157.     endtask
  158.     endmodule
复制代码

顶层调用接口仿真代码
  1. `timescale 1ns / 1ns

  2. module eeprom_test_tb;
  3. reg I_sysclk_p = 1'b0;
  4. reg I_sysclk_n = 1'b1;
  5. wire O_iic_scl;
  6. wire IO_iic_sda;

  7. pullup( IO_iic_sda );

  8. eeprom_test eeprom_test_inst
  9. (
  10. .I_sysclk_p(I_sysclk_p),
  11. .I_sysclk_n(I_sysclk_n),
  12. .O_iic_scl(O_iic_scl),
  13. .IO_iic_sda(IO_iic_sda)
  14. );
  15.    
  16. eeprom eeprom_inst(
  17. .scl(O_iic_scl),
  18. .sda(IO_iic_sda)
  19. );   

  20. always
  21.     begin
  22.         #5 I_sysclk_p = ~I_sysclk_p;
  23.         #5 I_sysclk_n = ~I_sysclk_n;
  24.     end

  25. endmodule
复制代码

5.2仿真结果
启动后,右击需要观察的信号,添加到波形窗口,并仿真。
972d5fe7c8ba4b87b4f1a1cb018f700c.jpg
放大观察I2C时序,查看写操作START和ACK位置
a626374e94a849efbbc7abf985bcb4a2.jpg
放大观察I2C时序,查看写读操作Repeated START
737ec3097b5746efa91171be7ccc6552.jpg
6在线仿真
设置ila的采样深度为8192,越大观察的数据越多,但是消耗的Bram也越多,设置支持capture control模式
00f624b5f217406babc0d5691ab2bc80.jpg
设置需要观察数据的总位宽
c310ab4404914c06837ae717ab17769e.jpg
注意代码中,通过512分频器产生的信号作为capture信号
  1. // 以下代码为在线逻辑分析仪观察调试部分
  2. reg scl_r = 1'b0;
  3. always @(posedge I_clk)begin //对O_iic_scl寄存1次
  4. scl_r <= O_iic_scl;
  5. end

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

  12. ila_0 ila_debug (
  13.    .clk(I_clk),//在线逻辑分析仪的时钟
  14.    .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}) // 需要观察的调试信号
  15. );
复制代码

7下载演示
为了方便观察结果,使用LED观察,每间隔500ms完成一次读写操作
  1. assign O_test_led = rd_data[3:0];//测试LED输出
复制代码

下载程序前,先确保FPGA工程已经编译。
7.1硬件连接
(该教程为通用型教程,教程中仅展示一款示例开发板的连接方式,具体连接方式以所购买的开发板型号以及结合配套代码管脚约束为准。)
请确保下载器和开发板已经正确连接,另外需要把核心板上的2P模式开关设置到JTAG模式,即ON ON,并且开发板已经上电。(注意JTAG端子不支持热插拔,而USB接口支持,所以在不通电的情况下接通好JTAG后,再插入USB到电脑,之后再上电,以免造成JTAG IO损坏)
a1cd1c1e8c5f4a4788a705961954f824.jpg
7.2运行结果
(该教程为通用型教程,教程中仅展示一款示例开发板的上板现象,具体现象以所购买的开发板型号以及配套代码上板现象为准。)
1.通过LED观察I2C的读写结果,可以看到LED规律运行
2.通过ILA在线逻辑分析仪观察
Capure模式必须设置BASIC模式
9bda0a3694c54f8f80ef498478353fb2.jpg
用500ms作为触发信号
d07f2a8cb29841e894e402371e307ea3.jpg

运行后抓到的波形如下

fc1110cda8bc47d4b99113dd6032ea66.jpg

3ee5a49e65664a999f91edab85ac63d1.jpg




您需要登录后才可以回帖 登录 | 立即注册

本版积分规则