【代码更新】IIC协议建模——读写EEPROM
发布日期:2021-05-19 04:00:03 浏览次数:15 分类:博客文章

本文共 24462 字,大约阅读时间需要 81 分钟。

  案例采用明德扬设计思想完成。IIC协议是非常常用的接口协议,在电子类岗位招聘要求中经常出现它的身影。关于IIC协议这里只做简要介绍,详细信息请自行百度或查阅相关Datasheet,网上资料非常多。该篇博文主要讲如何使用verilog来描述IIC协议,以读写EEPROM为例带领大家了解下明德扬四段式状态机规范和优势,另外还有一些自己在设计过程中总结的经验技巧。

  IIC协议时序格式以Datasheet中时序图的形式供大家参考。IIC协议有一条时钟线SCL和一条双线数据总线SDA。SDA在SCL高电平时保持稳定,否则视为开始或结束条件。

  发送端发送1byte数据后,接收端在下一个SCL高电平期间拉低总线表示应答,即接收数据成功。

  以下分别是器件地址为1字节的EEPROM的单字节写和读操作,需要注意的是DEVICE ADDRESS段中前四位固定是4'b1010,后三位根据EEPROM地址信号电平决定(本次实验地址信号引脚均接地,因此后三位为000),最后一位是读写标志位,低电平写。

  好了,有了以上五张时序图我们便知道要干什么了,就是实现这些时序嘛!对于这种串行时序,时间有先后且操作差异较大的要用状态机实现。每种类型操作定义在一个状态中,状态内部需要多个操作则配合计数器实现。整体设计思路如下:先构造时钟信号SCL,这里频率定义为200KHz,而系统时钟有频率为200MHz差分晶振提供,显然需要用到分频计数器。由于SCL高电平期间数据要保持稳定,所以我们在分频计数器计数到1/4处拉高SCL,3/4处拉低SCL,这样做的好处是在结束计数时正好处于SCL低电平中间点,此处作为数据变化的时刻再合适不过。

  有了时钟信号,下一步就是通过不同的状态实现SDA信号线满足上述时序要求。我们先来划分状态(实际上时序图中都给我们标识好了),很明显综合考虑写和读两种时序,状态应定义为:初始状态、开始、写控制、响应1、写地址、响应2、写数据、响应3、重新开始、读控制、响应4、读数据、不响应、停止。这里写控制和读控制即是DEVICE ADDRESS阶段,唯一的区别在于读写标志位不同。能看出以上状态划分包括写流程分支和读流程分支,可以根据指令用一个标志位加以区分。

  定义状态参数并采用独热码进行编码:

  IIC协议中每次SCL高电平期间视为一次操作,因此为了让每个状态都有整数个SCL周期(完整分频计数周期),对每个状态进行比特计数,写控制、地址、写数据、读控制、读数据阶段计数周期是8,其他为1。另外为保证代码的“健壮性”,也就是即使发送1byte数据后没有响应也不至于挂死在等待响应阶段,设定在每次等待响应阶段若响应才进入下一操作,否则回到初始状态。由此得到状态转移图(只包括主要流程,转移条件及未响应回到IDLE状态未画出):

  至此所有的设计工作都已经完成,接下来就是根据上述分析编写代码。在编写代码之前简要介绍明德扬四段式状态机的设计思想和代码规范:四段式状态机实质上是在三段式状态机基础上单独提出状态转移条件定义的结构。目的是让设计者一个时间段只专注于一件事情,也就是说当设计状态机的时候先把状态转移流程确定,而条件用不同的信号名代替,等状态转移流程确定后再定义转移条件。这样做的另一个好处是作为条件的信号名可以很方便的在后续时序逻辑中使用。其中用于代替条件的信号名要遵循类似如下格式:<state_c>2<state_n>。<>处用状态名代替。

整体代码如下:

1 `timescale 1ns / 1ps  2   3 module i2c_interface#(parameter SCL_CYC = 1000)//200KHz  4 (  5     input clk,  6     input rst_n,  7       8     //用户侧接口  9     input write_en,//写指令 10     input read_en, //读指令 11     input [7:0]share_addr,    //读写复用地址 12     input [7:0] wri_data,//代写入数据 13     input wri_data_vld, 14      15     output reg busy,//总线忙信号 16     output reg [7:0] rd_data,//读回数据 17     output reg rd_data_vld, 18      19     //仿真用接口 20     output reg [13:0] state_c, 21      22     //eeprom侧接口 23     output reg scl, //时钟 24     input sda_in, 25     output reg sda_en, 26     output reg sda_reg 27      28     ); 29      30     reg [11:0] div_cnt; 31     reg high_middle,low_middle; 32     reg [3:0] bit_cnt; 33     reg [3:0] N; 34     //(*keep = "true"*)reg [13:0] state_c; 35     reg [13:0] state_n; 36     reg [7:0] wri_byte; 37     reg rd_flag; 38     reg [7:0] rd_buf; 39     reg [13:0] state_c_tmp; 40     reg [7:0] device_addr_wr_shift; 41      42     wire add_bit_cnt,end_bit_cnt; 43     wire add_div_cnt,end_div_cnt; 44     wire idle2start,start2wri_ctrl,wri_ctrl2ack1,ack12addr,addr2ack2,ack22wri_data; 45     wire wri_data2ack3,ack32stop,ack22re_start,re_start2rd_ctrl,rd_ctrl2ack4; 46     wire ack42rd_data,rd_data2nack,nack2stop,stop2idle,ack2idle; 47     reg ack_valid,ack_invalid; 48     wire [2:0] cs; 49     wire wri_vld; 50     wire [7:0] device_addr_rd,device_addr_wr; 51     wire [7:0] word_addr; 52     wire ack_state; 53      54     //状态编码 55     localparam IDLE     = 14'b00_0000_0000_0001,//1 56                START    = 14'b00_0000_0000_0010,//2 57                WRI_CTRL = 14'b00_0000_0000_0100,//4 58                ACK1     = 14'b00_0000_0000_1000,//8 59                ADDR     = 14'b00_0000_0001_0000,//10 60                ACK2     = 14'b00_0000_0010_0000,//20 61                WRI_DATA = 14'b00_0000_0100_0000,//40 62                ACK3     = 14'b00_0000_1000_0000,//80 63                RE_START = 14'b00_0001_0000_0000,//100 64                RD_CTRL  = 14'b00_0010_0000_0000,//200 65                ACK4     = 14'b00_0100_0000_0000,//400 66                RD_DATA  = 14'b00_1000_0000_0000,//800 67                NACK     = 14'b01_0000_0000_0000,//1000 68                STOP     = 14'b10_0000_0000_0000;//2000 69      70     //分频计数器 在响应操作直到完成或退出到IDLE中间都计数 71     always@(posedge clk or negedge rst_n)begin 72         if(!rst_n) 73             div_cnt <= 0; 74         else if(add_div_cnt)begin 75             if(end_div_cnt) 76                 div_cnt <= 0; 77             else  78                 div_cnt <= div_cnt + 1'b1; 79         end 80         else  81             div_cnt <= 0; 82     end 83      84     assign add_div_cnt = busy == 1; 85     assign end_div_cnt = add_div_cnt && div_cnt == SCL_CYC - 1; 86      87     //比特计数器 88     always@(posedge clk or negedge rst_n)begin 89         if(!rst_n) 90             bit_cnt <= 0; 91         else if(add_bit_cnt)begin 92             if(end_bit_cnt) 93                 bit_cnt <= 0; 94             else  95                 bit_cnt <= bit_cnt + 1'b1; 96         end 97     end 98      99     assign add_bit_cnt = end_div_cnt;100     assign end_bit_cnt = add_bit_cnt && bit_cnt == N - 1;101     102     always@(*)begin103         case(state_c)104             WRI_CTRL:N = 8;105             ADDR:N = 8;106             WRI_DATA:N = 8;107             RD_CTRL:N = 8;108             RD_DATA:N = 8;109             default:N = 1;110         endcase111     end112     113     //---------------------iic时序四段式状态机部分-------------------------114     115     //时序逻辑描述状态转移116     always@(posedge clk or negedge rst_n)begin117         if(!rst_n)118             state_c <= IDLE;119         else 120             state_c <= state_n;121     end122     123     //组合逻辑描述状态转移条件124     always@(*)begin125         case(state_c)126             IDLE:begin       //空闲状态127                 if(idle2start)128                     state_n = START;129                 else 130                     state_n = state_c;131             end132             133             START:begin    //产生开始条件 即SCL高电平期间SDA拉低134                 if(start2wri_ctrl)135                     state_n = WRI_CTRL;136                 else 137                     state_n = state_c;138             end139             140             WRI_CTRL:begin  //写器件地址和写标志位141                 if(wri_ctrl2ack1)142                     state_n = ACK1;143                 else 144                     state_n = state_c;145             end146             147             ACK1:begin   //等待响应148                 if(ack12addr)149                     state_n = ADDR;150                 else if(ack2idle)151                     state_n = IDLE;152                 else 153                     state_n = state_c;154             end155             156             ADDR:begin  //写存储单元地址157                 if(addr2ack2)158                     state_n = ACK2;159                 else 160                     state_n = state_c;161             end162             163             ACK2:begin   //等待响应2164                 if(ack22wri_data)   //写操作165                     state_n = WRI_DATA;166                 else if(ack22re_start)//读操作167                     state_n = RE_START;168                 else if(ack2idle)169                     state_n = IDLE;170                 else 171                     state_n = state_c;172             end173             174             WRI_DATA:begin   //写数据 8bit175                 if(wri_data2ack3)176                     state_n = ACK3;177                 else 178                     state_n = state_c;179             end180             181             ACK3:begin   //等待响应3182                 if(ack32stop)183                     state_n = STOP;184                 else if(ack2idle)185                     state_n = IDLE;186                 else 187                     state_n = state_c;188             end189             190             RE_START:begin  //若为读操作在响应2后再次构造开始条件191                 if(re_start2rd_ctrl)192                     state_n = RD_CTRL;193                 else 194                     state_n = state_c;195             end196             197             RD_CTRL:begin   //写入存储单元地址和读标志位198                 if(rd_ctrl2ack4)199                     state_n = ACK4;200                 else 201                     state_n = state_c;202             end203             204             ACK4:begin  //等待响应4205                 if(ack42rd_data)206                     state_n = RD_DATA;207                 else if(ack2idle)208                     state_n = IDLE;209                 else 210                     state_n = state_c;211             end212             213             RD_DATA:begin  //读数据 8bit214                 if(rd_data2nack)215                     state_n = NACK;216                 else 217                     state_n = state_c;218             end219             220             NACK:begin  //不响应 无操作即可221                 if(nack2stop)222                     state_n = STOP;223                 else 224                     state_n = state_c;225             end226             227             STOP:begin  //构造停止条件228                 if(stop2idle)229                     state_n = IDLE;230                 else 231                     state_n = state_c;232             end233             234             default:235                 state_n = IDLE;236         endcase237     end238     239     //连续赋值语句定义状态转移条件240     assign idle2start       = state_c  == IDLE     && (write_en || read_en);241     assign start2wri_ctrl   = state_c  == START    && end_bit_cnt;  242     assign wri_ctrl2ack1    = state_c  == WRI_CTRL && end_bit_cnt;243     assign ack12addr        = state_c  == ACK1     && ack_valid && end_bit_cnt;244     assign addr2ack2        = state_c  == ADDR     && end_bit_cnt;245     assign ack22wri_data    = state_c  == ACK2     && ack_valid && !rd_flag && end_bit_cnt;246     assign wri_data2ack3    = state_c  == WRI_DATA && end_bit_cnt;247     assign ack32stop        = state_c  == ACK3     && ack_valid && end_bit_cnt;248     assign ack22re_start    = state_c  == ACK2     && ack_valid && rd_flag && end_bit_cnt;249     assign re_start2rd_ctrl = state_c  == RE_START && end_bit_cnt;250     assign rd_ctrl2ack4     = state_c  == RD_CTRL  && end_bit_cnt;251     assign ack42rd_data     = state_c  == ACK4     && ack_valid && end_bit_cnt;252     assign rd_data2nack     = state_c  == RD_DATA  && end_bit_cnt;253     assign nack2stop        = state_c  == NACK     && end_bit_cnt;254     assign stop2idle        = state_c  == STOP     && end_bit_cnt;255     assign ack2idle         = ack_state && ack_invalid;256     257 258     259     always@(posedge clk or negedge rst_n)begin260         if(!rst_n)261             ack_valid <= 0;262         else if(ack12addr || ack22wri_data || ack32stop || ack22re_start || ack42rd_data || ack2idle)263             ack_valid <= 0;264         else if(ack_state && high_middle && !sda_en && !sda_in)265             ack_valid <= 1;266     end267     268     assign ack_state = state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4;269     270     always@(posedge clk or negedge rst_n)begin271         if(!rst_n)272             ack_invalid <= 0;273         else if(state_c == NACK && high_middle && !sda_en && sda_in)274             ack_invalid <= 1;275         else if(end_bit_cnt)276             ack_invalid <= 0;277     end278     279     //时序逻辑描述状态输出280     281     //scl时钟信号282     always@(posedge clk or negedge rst_n)begin283         if(!rst_n)284             scl <= 0;285         else if(add_div_cnt && div_cnt == SCL_CYC/4 - 1)286             scl <= 1;287         else if(add_div_cnt && div_cnt == SCL_CYC/4 + SCL_CYC/2 - 1)288             scl <= 0;289     end290     291     //找到scl高低电平中间点292     always@(posedge clk or negedge rst_n)begin293         if(!rst_n)294             high_middle <= 0;295         else if(add_div_cnt && div_cnt == SCL_CYC/2 - 1)296             high_middle <= 1;297         else 298             high_middle <= 0;299     end300     301     //三态门输出使能302     always@(posedge clk or negedge rst_n)begin303         if(!rst_n)304             sda_en <= 1;305         else if(idle2start || ack12addr || ack22wri_data || ack32stop || ack22re_start || nack2stop|| rd_data2nack)306             sda_en <= 1;307         else if(wri_ctrl2ack1 || addr2ack2 || wri_data2ack3 || rd_ctrl2ack4 || ack2idle || stop2idle)308             sda_en <= 0;309     end310     311     //数据总线输出寄存器312     always@(posedge clk or negedge rst_n)begin313         if(!rst_n)314             sda_reg <= 1;315         else if(idle2start)316             sda_reg <= 1;317         else if((state_c == START || state_c == RE_START) && high_middle)318             sda_reg <= 0;319         else if(state_c == WRI_CTRL)320             sda_reg <= device_addr_wr[7-bit_cnt];321         else if(state_c == ADDR)322             sda_reg <= word_addr[7 - bit_cnt];323         else if(state_c == WRI_DATA)324             sda_reg <= wri_data[7 - bit_cnt];325         else if(state_c == STOP && high_middle)326             sda_reg <= 1;327         else if(ack22re_start)328             sda_reg <= 1;329         else if(state_c == RE_START && high_middle)330             sda_reg <= 0;331         else if(state_c == RD_CTRL)332             sda_reg <= device_addr_rd[7- bit_cnt];333         else if(ack_state)334             sda_reg <= 0;335         else if(rd_data2nack)336             sda_reg <= 1;337         else if(nack2stop)338             sda_reg <= 0;339     end340     341     assign device_addr_wr = {4'b1010,cs,1'b0};342     assign cs             = 3'b000;343     assign word_addr      = share_addr;344     assign device_addr_rd = {4'b1010,cs,1'b1};345     346     //读取数据缓存347     always@(posedge clk or negedge rst_n)begin348         if(!rst_n)349             rd_buf <= 0;350         else if(state_c == RD_DATA && high_middle)351             rd_buf <= {rd_buf[6:0],sda_in};352     end353     354     //读数据有效指示355     always@(posedge clk or negedge rst_n)begin356         if(!rst_n)357             rd_data_vld <= 0;358         else if(rd_data2nack)359             rd_data_vld <= 1;360         else 361             rd_data_vld <= 0;362     end363     364     //读数据输出365     always@(posedge clk or negedge rst_n)begin366         if(!rst_n)367             rd_data <= 0;368         else 369             rd_data <= rd_buf;370     end371     372     //读标志位373     always@(posedge clk or negedge rst_n)begin374         if(!rst_n)375             rd_flag <= 0;376         else if(read_en)377             rd_flag <= 1;378         else if(rd_flag && (stop2idle || state_c == IDLE))379             rd_flag <= 0;380     end381     382     //总线忙信号383     always@(posedge clk or negedge rst_n)begin384         if(!rst_n)385             busy <= 0;386         else if(write_en || read_en)387             busy <= 1;388         else if(busy == 1 &&(stop2idle || state_c == IDLE))389             busy <= 0;390     end391     392 endmodule
i2c_interface.v

   可以看出状态机部分依次分为:时序逻辑描述状态转移,组合逻辑描述状态转移条件,连续赋值定义状态转移条件以及时序逻辑描述状态相关输出。并且至始至终使用state_c和state_n两个信号表示现态和次态,使逻辑更加清晰。接口部分为了方便仿真和调试,加入状态信号state_c。这里涉及到一个双向端口sda,用三个信号:输出使能sda_en,输出寄存器sda_reg和输入缓存sda_in表示。在顶层模块中使用这三个信号通过三态门的形式给出,关于三态门的使用细节和仿真方式稍后讲述。

  先设计其他模块和顶层模块,之后对顶层模块进行仿真测试,这时观察各个模块中信号数值分析排查问题。有了时序接口模块,在正确无误情况下,已经可以实现对EEPROM的读写操作。现在明确设计目的,我们要实现EEPROM的一字节数据读写,因此可以通过按键发送指令向EEPROM中某地址中写入任意一个数据,之后用另一个按键发送读指令将刚写入地址中数据读出的方式验证读写操作是否正常工作。编写控制模块(控制模块仅实现IIC总线空闲时才响应操作,实际上用按键方式犹豫时间间隔较长,不会出现多个指令抢占总线的情况,这里设计控制模块是为了适应其他场合或功能扩展用途)

1 `timescale 1ns / 1ps 2  3 module iic_ctrl( 4     input clk, 5     input rst_n, 6     input local_rd, 7     input local_wr, 8      9     input iic_busy,10     output reg com_rd,11     output reg com_wr12     );13     14     wire ready;15     16     assign ready = !iic_busy;17     18     //写命令19     always@(posedge clk or negedge rst_n)begin20         if(!rst_n)21             com_wr <= 0;22         else if(local_wr && ready)//iic总线空闲时才响应操作23             com_wr <= 1;24         else 25             com_wr <= 0;26     end27     28     //读命令29     always@(posedge clk or negedge rst_n)begin30         if(!rst_n)31             com_rd <= 0;32         else if(local_rd && ready)33             com_rd <= 1;34         else 35             com_rd <= 0;36     end37     38     39 endmodule

   剩下只需加入按键消抖模块,并把按键消抖模块,控制模块还有时序接口模块都例化在顶层文件中即可。按键消抖模块在之前的博文中有讲述,这里使用计数器配合状态标志位的方式实现。需要说明的是多个按键使用一个按键消抖模块的设计方式:只需将信号位宽定义为可变参数。

1 `timescale 1ns / 1ps 2  3 module key_filter 4 #(parameter DATA_W    = 24, 5             KEY_W     = 2, 6             TIME_20MS = 4_000_000) 7 ( 8    input clk    , 9    input rst_n  ,10    input [KEY_W-1 :0] key_in ,    //按键 按下为低电平11    output reg [KEY_W-1 :0] key_vld 12 );13 14     reg [DATA_W-1:0] cnt;15     reg flag;16     reg [KEY_W-1 :0] key_in_ff1;17     reg [KEY_W-1 :0] key_in_ff0;18 19     wire add_cnt,end_cnt;20     21     //延时计数器22     always  @(posedge clk or negedge rst_n)begin23         if(rst_n==1'b0)24             cnt <= 0;25         else if(add_cnt)begin26             if(end_cnt)27                 cnt <= 0;28             else29                 cnt <= cnt + 1'b1;30         end31         else32             cnt <= 0;33     end34     //按下状态才计数,松手清零35     assign add_cnt = flag == 1'b0 && (key_in_ff1 != 2'b11); 36     assign end_cnt = add_cnt && cnt == TIME_20MS - 1;37     38     //计数标志位,0有效 为了只计数一个周期39     always  @(posedge clk or negedge rst_n)begin 40         if(rst_n==1'b0)begin41             flag <= 1'b0;42         end43         else if(end_cnt)begin44             flag <= 1'b1;45         end46         else if(key_in_ff1 == 2'b11)begin//松手重新清零47             flag <= 1'b0;48         end49     end50     51     //同步处理52     always  @(posedge clk or negedge rst_n)begin 53         if(rst_n==1'b0)begin54             key_in_ff0 <= 0;55             key_in_ff1 <= 0;56         end57         else begin58             key_in_ff0 <= key_in    ;59             key_in_ff1 <= key_in_ff0;60         end61     end62 63     //输出有效64     always  @(posedge clk or negedge rst_n)begin 65         if(rst_n==1'b0)begin66             key_vld <= 0;67         end68         else if(end_cnt)begin69             key_vld <= ~key_in_ff1;70         end71         else begin72             key_vld <= 0;73         end74     end75     76 endmodule

 顶层模块例化子模块:

1 `timescale 1ns / 1ps  2   3 module eeprom_top(  4       5     input sys_clk_p,  6     input sys_clk_n,  7     input rst_n,  8     input [1:0] key,  9     //仿真接口 10     output sda_en, 11     output [13:0] state_c, 12      13     //EEPROM接口 14     output scl, 15     inout sda 16     ); 17      18     wire sys_clk_ibufg; 19     (*keep = "true"*)wire busy; 20     (*keep = "true"*)wire read,write; 21     wire [7:0] rd_data; 22     wire rd_data_vld; 23     (*keep = "true"*)wire sda_reg,sda_in; 24     (*keep = "true"*)wire [1:0] key_vld; 25     //(*keep = "true"*)wire sda_en; 26     //(*keep = "true"*)wire [13:0] state_c; 27     wire [39:0] probe0; 28      29     IBUFGDS # 30     ( 31     .DIFF_TERM ("FALSE"), 32     .IBUF_LOW_PWR ("FALSE") 33     ) 34     u_ibufg_sys_clk 35     ( 36     .I (sys_clk_p),     //差分时钟的正端输入,需要和顶层模块的端口直接连接 37     .IB (sys_clk_n),    // 差分时钟的负端输入,需要和顶层模块的端口直接连接 38     .O (sys_clk_ibufg)  //时钟缓冲输出 39     ); 40      41      42     key_filter 43     #(.DATA_W(24), 44       .KEY_W(2), 45       .TIME_20MS(4_000_000)) 46     key_filter 47     ( 48        .clk (sys_clk_ibufg)   , 49        .rst_n(rst_n)  , 50        .key_in (key),    //按键 按下为低电平 51        .key_vld(key_vld)  52     ); 53      54     iic_ctrl iic_ctrl( 55     .clk(sys_clk_ibufg), 56     .rst_n(rst_n), 57     .local_wr(key_vld[1]), 58     .local_rd(key_vld[0]), 59      60     .iic_busy(busy), 61     .com_rd(read), 62     .com_wr(write) 63     ); 64      65     iic_interface 66     #(.SCL_CYC(1000)) 67     iic_interface( 68     .clk(sys_clk_ibufg), 69     .rst_n(rst_n), 70      71     //用户侧接口 72     .write_en(write),  //写指令 73     .read_en(read),    //读指令 74     .share_addr(8'h15),//读写复用地址 75     .wri_data(8'h32),  //待写入数据 76     .wri_data_vld(1'b1), 77     .busy(busy),       //总线忙信号 78     .rd_data(rd_data), //读回数据 79     .rd_data_vld(rd_data_vld), 80     //仿真接口 81     .state_c(state_c), 82     //eeprom侧接口 83     .scl(scl), //时钟 84     .sda_in(sda_in), 85     .sda_en(sda_en), 86     .sda_reg(sda_reg) 87     ); 88      89     //三态门 90     assign sda    = sda_en ? sda_reg : 1'bz; 91     assign sda_in = sda; 92      93     ila_0 ila_0 ( 94     .clk(sys_clk_ibufg), // input wire clk 95     .probe0(probe0) // input wire [39:0] probe0 96 ); 97  98     assign probe0[13:0] = state_c; //14bit 99     assign probe0[14] = busy;100     assign probe0[15] = scl;101     assign probe0[16] = sda_en;102     assign probe0[17] = sda_reg;103     assign probe0[18] = sda_in;104     assign probe0[19] = write;105     assign probe0[20] = read;106     assign probe0[39:21] = 0;107     108 endmodule

  看一下软件分析出的原理图结构(ILA IP核是之后添加的):

  此处详细说明下双向端口使用:顶层模块中建立三态门结构,在输出使能有效时作为输出端口,无效是呈现高阻态,此时作为输入端口,由sda_in信号读取数值。那双向端口如何仿真呢?很简单,在测试文件中也构造一个三态门结构,而输出使能信号为设计中输出使能信号的相反值,这样在设计中该端口呈现高阻态时,正好在测试文件中相应端口作为输出的阶段。可以注意到我在顶层模块中加入了两个仿真接口:state_c和sda_en,方便在测试文件中找到给出响应的位置。测试文件如下:

1 `timescale 1ns / 1ps  2   3 module eeprom_top_tb;  4       5     reg sys_clk_p,sys_clk_n;  6     reg rst_n;  7     reg [1:0] key;  8       9     wire scl; 10     wire sda; 11     wire sda_en;//高电平时待测试文件为输出 12      13     reg [15:0] myrand; 14     reg sda_tb_out; 15     wire [13:0] state_c; 16      17     eeprom_top eeprom_top( 18     .sys_clk_p(sys_clk_p), 19     .sys_clk_n(sys_clk_n), 20     .rst_n(rst_n), 21     .key(key), 22     .sda_en(sda_en), 23     .state_c(state_c), 24     .scl(scl), 25     .sda(sda) 26     ); 27      28     assign sda = (!sda_en) ? sda_tb_out : 1'bz; 29      30     parameter CYC = 5, 31               RST_TIME = 2; 32      33     defparam eeprom_top.key_filter.TIME_20MS = 200; 34      35     initial begin 36         sys_clk_p = 0; 37         forever #(CYC/2) sys_clk_p = ~sys_clk_p; 38     end 39      40     initial begin 41         sys_clk_n = 1; 42         forever #(CYC/2) sys_clk_n = ~sys_clk_n; 43     end 44      45     localparam IDLE     = 14'b00_0000_0000_0001, 46                START    = 14'b00_0000_0000_0010, 47                WRI_CTRL = 14'b00_0000_0000_0100, 48                ACK1     = 14'b00_0000_0000_1000, 49                ADDR     = 14'b00_0000_0001_0000, 50                ACK2     = 14'b00_0000_0010_0000, 51                WRI_DATA = 14'b00_0000_0100_0000, 52                ACK3     = 14'b00_0000_1000_0000, 53                RE_START = 14'b00_0001_0000_0000, 54                RD_CTRL  = 14'b00_0010_0000_0000, 55                ACK4     = 14'b00_0100_0000_0000, 56                RD_DATA  = 14'b00_1000_0000_0000, 57                NACK     = 14'b01_0000_0000_0000, 58                STOP     = 14'b10_0000_0000_0000; 59      60     initial begin 61         rst_n = 1; 62         #1; 63         rst_n = 0; 64         #(CYC*RST_TIME); 65         rst_n = 1; 66     end 67      68     initial begin 69         #1; 70         key = 2'b11; 71         #(CYC*RST_TIME); 72         #(CYC*10); 73          74         press_key_wr; 75         #120_000; 76         press_key_rd; 77         #80_000; 78         $stop; 79     end 80      81     //构造响应条件 82     always@(*)begin 83         if(state_c == ACK1 || state_c == ACK2 || state_c == ACK3 || state_c == ACK4) 84             sda_tb_out <= 0; 85         else  86             sda_tb_out <= 1; 87     end 88      89     task press_key_wr; 90     begin 91         repeat(20)begin//模拟抖动过程 92             myrand = {$random}%400; 93             #myrand key[1] = ~key[1]; 94         end 95         key[1] = 0; 96         #3000; 97         repeat(20)begin 98             myrand = {$random}%400; 99             #myrand key[1] = ~key[1];100         end101         key[1] = 1;102         #3000;103     end104     endtask105     106     task press_key_rd;107     begin108         repeat(20)begin//模拟抖动过程109             myrand = {$random}%400;110             #myrand key[0] = ~key[0];111         end112         key[0] = 0;113         #3000;114         repeat(20)begin115             myrand = {$random}%400;116             #myrand key[0] = ~key[0];117         end118         key[0] = 1;119         #3000;120     end121     endtask122     123 endmodule

  我的开发板使用差分晶振作为系统时钟,在测试文件中也要以差分信号的形式给出时钟。与单端时钟唯一的区别在于给出两个初始值不同周期相同的时钟信号。其中为了找到响应位置,引入状态编码,并在需要给出响应的时刻拉低总线。运行行为仿真:

整体结构:

写操作:

读操作:

  读写操作过程中状态转移、比特计数器、sda 、scl这些核心信号数据正常,仿真通过。实际上这是设计过程中遇到些小问题,修改代码后的结果。下一步要在线调试了,这里是本篇博文最后一个重点要说明的内容。以往我会使用添加属性的方式(*mark_debug = "true"*)标志要观察的信号,再在综合后使用debug设置向导引入调试IP核。经过实验发现调试核的引入是通过添加约束的方式实现的,而且当要观察别的信号时该约束部分必须改动否则报错,所以这里使用IP核例化调试探测流程,直接在IP catalog中生成ILA IP核。这里有一个小技巧:生成IP核是只使用一个探针信号,并把位宽设置的较大,且使用OOC方式。在例化IP核后使用这个信号的不同位宽部分连接需要在线观察的信号。这样可以避免在反复综合、布局布线的过程中重新编译ILA IP核部分,节约时间。

  打开硬件管理器,下载bit流后自动打开调试界面。设置触发条件观察波形,这里可以很方便的利用状态信号的不同状态设置触发条件。

写操作:

 读操作:

  写入数据定义为8'h32,读取bit依次是0011_0010,即为32,说明正确将写入数据读出。大家可以在本次实验基础上扩展,比如实现页写模式,或是使用串口来发送读写指令并读回数据等。经过本次博文,掌握了IIC协议的四段式状态机实现方式,双向端口的三态门结构及仿真方法,并能够灵活运用ILA IP核进行在线调试。希望大家和我一样收获很多。欢迎交流~

上一篇:寄存器自动化配置通用案例
下一篇:串口完整项目之串口收发字符串

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2025年04月26日 07时36分05秒