此篇文章,主要讲述经过视频点拨后自己动手写adc_driver.v代码所遇到的若干问题。


文章目录

  • 1. 开篇,各模块连接关系
  • 2. ADC时序分析
    • 2.1 DIN的变化
    • 2.2 rdata的变化
    • 2.3 case语句
    • 2.4 sclk_edge_cnt的使能信号
  • 3. 轻触开关变琴键开关
  • 4. 被卡住的en_conv(重点)
    • 4.1 adc_driver.v
    • 4.2 adc_driver_tb.v
    • 4.3 错误分析
    • 4.4 改进方法
  • 5. testbench中走不出的h=0
    • 5.1 错误分析
    • 5.2 改进方法
  • 6. In The End

1. 开篇,各模块连接关系


其中User_Ctrl模块可理解为testbench,其给出3位地址信号channel供驱动选择8个信道中的一个;en_conv为外部使能信号;conv_done为ADC将数据全部输出到驱动里,驱动全部接收并产生转换完成信号;同时将接收的数据以寄存器rdata[11:0]的形式输出。

关于信号en_conv的使用技巧,详见"轻触开关变琴键开关"一节。

adc_driver即为产生ADC的驱动信号,并接收ADC转换的数据dout。驱动信号有:片选信号csn,ADC时钟信号sclk,地址信号din;其中地址信号din是将3位并行的channel[2:0]转化成串行的1位信号din。需要说明的是,csnsclkdindout位宽都为1。

2. ADC时序分析

以德州仪器的ADC128S022作为ADC器件,其操作时序图如下:

时序分析是编写对应的驱动必不可少的部分。而我们往往从时序入手,将最为核心的代码写出(地基),之后逆向前推,需要哪些控制信号就一个个添加(砖瓦),直到整个文件完整写出,并能够通过Quartus的分析与综合,此时才能是大功告成(大楼)。

稍作分析不难发现,在sclk的第一个下降沿之前,csn为高,且csn与sclk两信号之间并无特殊的时序要求,故可认为,csn拉低时,sclk也可同时拉低。于是,可在第一个sclk的下降沿之前定义一个状态0(或时刻0),并认为此时csn=1'b1.

反倒是csn与dout之间有tEN的限制,即csn的下降沿之后,dout必须在30ns之内给拉低。而对于adc_driver.v来说,dout是输入,故dout的值只能是adc.v(即ADC这个器件)给出,而不受adc_driver.v的控制,即在设计verilog的过程中可不考虑tEN

对于此时序:

  1. 定义时序图中最开始sclk为高的那一部分的时刻为0;
  2. 定义时序图中sclk曲线上编号为1的时刻为时刻1,此时正好为sclk的第一个下降沿;
  3. 定义时序图中sclk的第一个上升沿为时刻2;
  4. 定义时序图中sclk的第二个下降沿为时刻3;
  5. 定义时序图中sclk的第二个上降沿为时刻4;

于是可得如下时序(或称“状态”)表格:


表格中黄色的信号为输入信号,绿色信号为输出信号。

表格中din信号为X表示无关项,即不论该信号拉高或为低都对ADC这个器件毫无影响;并且直到第三个sclk的下降沿,din才开始变化;当将三位的addr信号传输完毕后,又变成了无关项(三位addr信号正好对应ADC器件上的8个通道)。

表格中dout信号为Z表示高阻态。由于高阻,故此时dout无论为什么都不会传输到adc_driver中,故在时刻0定义其为1也无大碍。

2.1 DIN的变化

din只是在第三、第四、第五个sclk信号的下降沿进行变化,其他时刻其取值不影响后续逻辑,故设定其初始值为din <= 1'b0;在第五个sclk周期之后,其值保持不变(即在case语句中不对din进行赋值操作)。

2.2 rdata的变化

从第五个sclk下降沿开始,每来一个下降沿,输入数据dout就变化一次,故为了保证输入的信号被正确读出,故驱动(adc_driver.v)必须在dout变化之后读出该数据,即在sclk的上升沿读出dout,并采用移位寄存器的写法将dout一个个读出:rdata <= {rdata, dout};

2.3 case语句

由于已知每个时刻(或状态)对应的操作,而时刻(或状态)可用位宽为7的计数器sclk_edge_cnt表示(其实6位已经足够),并按照其数值的变化做出相应操作。

将采集的地址信号存放到寄存器reg [2:0] channel_r中,并得出遍历的代码:

         case(sclk_edge_cnt)7'd0  : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0;         end7'd1  : begin csn <= 1'b0; sclk <= 1'b0;                       end7'd2  : begin              sclk <= 1'b1;                      end7'd3  : begin              sclk <= 1'b0;                      end7'd4  : begin              sclk <= 1'b1;                      end7'd5  : begin              sclk <= 1'b0; din <= channel_r[2]; end7'd6  : begin              sclk <= 1'b1;                      end7'd7  : begin              sclk <= 1'b0; din <= channel_r[1]; end7'd8  : begin              sclk <= 1'b1;                      end7'd9  : begin              sclk <= 1'b0; din <= channel_r[0]; end7'd10, 7'd12, 7'd14, 7'd16, 7'd18, 7'd20, 7'd22, 7'd24, 7'd26, 7'd28, 7'd30, 7'd32: begin  sclk  <= 1'b1; rdata <= {rdata[10:0], dout}; end7'd11, 7'd13, 7'd15, 7'd17, 7'd19, 7'd21,7'd23, 7'd25, 7'd27, 7'd29, 7'd31:begin sclk <= 1'b0; end7'd33 : begin sclk <= 1'b1; enddefault : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; endendcase

2.4 sclk_edge_cnt的使能信号

根据参考手册可知ADC的sclk频率为0.8~3.2MHz,且根据case语句可得知计数器sclk_edge_cnt每变化一次即为sclk的半个周期,也就是说,计数器每变化两次才是sclk的一个完整周期。故可得知,控制计数器的使能信号使能两次即为sclk的一个周期,且该使能信号为sclk频率的两倍。

假设sclk频率为1MHz,1000ns为一个周期。
那么
在0ns时使能信号有效,计数器变化一次,sclk拉高(或拉低);
在500ns时使能信号再次有效,计数器再变化一次,sclk拉低(或为高);
故使能信号和计数器的频率为sclk信号的两倍

对于该两倍频的关系,可参考文章关于小梅哥74HC595驱动设计的思考

而在设计adc_driver.v时,采用频率为50MHz的全局时钟clk_50M,故需要对计数器的使能信号进行分频。现定义计数器的使能信号为sclk2x,'2x’表示sclk的两倍关系,代码如下(其中信号en为整个系统的使能信号):

parameter CNT_NUM = 7'd26;   //即sclk为clk_50M的26分频
reg [6:0] div_cnt;           //分频寄存器
wire      sclk2x ;//generate sclk2x
assign sclk2x = (div_cnt == (CNT_NUM/2 - 1));//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) beginif (!rstn)div_cnt <= 7'b0;else if (en) beginif (div_cnt == CNT_NUM/2 - 1)div_cnt <= 7'b0;elsediv_cnt <= div_cnt + 1'b1;endelsediv_cnt <= 7'b0;
end//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) sclk_edge_cnt <= 7'b0;else if (en) beginif (sclk2x) beginif (sclk_edge_cnt == 7'd33)sclk_edge_cnt <= 7'b0;elsesclk_edge_cnt <= sclk_edge_cnt + 1'b1;endelsesclk_edge_cnt <= sclk_edge_cnt;endelsesclk_edge_cnt <= 7'b0;
end

至此,ADC的时序分析结束,剩下的即为各种控制信号的产生了。

3. 轻触开关变琴键开关

对于整个系统的使能信号en,虽然在testbench中可以写为

initial begin
en = 1'b0;
#200 en = 1'b1;
//然后比如5000ns之后执行完毕,再将en拉低
#5000 en = 1'b0;
...
...

但是这种方法太原始,当仿真量过大,或要进行多次仿真,还必须使得每个使能信号en间隔得当,繁琐冗余。小梅哥介绍一种新方法,即轻触开关转琴键开关(这个方法又是从周立功那边而来…)。所谓轻触开关,即为一个周期的脉冲信号;琴键开关即为电平信号(因为钢琴琴键按下去才能发出声音,一直按一直有声,不按则没声,类似电平信号)。定义轻触开关为en_conv(conv意为convert),琴键开关即之前的en信号:

//adc_driver.v中的写法:
always @ (posedge clk_50M or negedge rstn) beginif (!rstn)en <= 1'b0;else if (en_conv)en <= 1'b1;else if (conv_done)en <= 1'b0;elseen <= en;
end

其中conv_donerdata读完了所有的dout之后,产生转换完成信号,输出给User_Ctrl模块。此法可避免在testbench中写出之前的冗余代码,并能精简成如下形式:

`define p 20    //定义clk_50M的周期参数p
initial begin...en_conv = 1'b1;#(`p) en_conv = 1'b0;
...
end

即在testbech中只需将en_conv这个信号拉高一个周期(即为脉冲信号),信号enconv_done有效之前一直为高,直到转化完成,en拉低。若想进行多次转换,只需将en_conv置于一个for循环,并给出相应的控制信号即可。

4. 被卡住的en_conv(重点)

4.1 adc_driver.v

总的adc_driver.v代码如下:

set nu
module adc_driver(clk_50M     ,rstn        ,channel     ,en_conv     ,conv_done   , conv_done_r,csn         ,sclk        ,din         ,dout        ,data
);input         clk_50M ;
input       rstn      ;
input [2:0] channel ;
input       en_conv ;
input       dout    ;output      conv_done   ;   reg conv_done;
output      csn         ;   reg csn;
output      sclk        ;   reg sclk;
output      din         ;   reg din;
output[11:0]data        ;   reg [11:0] data;reg         en                ;
reg [6 :0]  div_cnt       ;
reg [2 :0]  channel_r     ;
reg [6 :0]  sclk_edge_cnt ;
reg [11:0]  rdata         ;
parameter CNT_NUM = 6'd26;//en_conv to en
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) en <= 1'b0;else if (en_conv)en <= 1'b1;else if (conv_done)en <= 1'b0;elseen <= en;
end//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) beginif (!rstn)div_cnt <= 7'b0;else if (en) beginif (div_cnt == CNT_NUM/2 - 1)div_cnt <= 7'b0;elsediv_cnt <= div_cnt + 1'b1;endelsediv_cnt <= 7'b0;
end//generate sclk2x
wire sclk2x = (div_cnt == CNT_NUM/2-1);//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) sclk_edge_cnt <= 7'b0;else if (en) beginif (sclk2x) beginif (sclk_edge_cnt == 7'd33)sclk_edge_cnt <= 7'b0;elsesclk_edge_cnt <= sclk_edge_cnt + 1'b1;endelsesclk_edge_cnt <= sclk_edge_cnt;endelsesclk_edge_cnt <= 7'b0;
end     //channel
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) channel_r <= 3'b0;else if (en_conv)channel_r <= channel;elsechannel_r <= channel_r;
end//conv_done and data[11:0];
always @ (posedge clk_50M or negedge rstn) beginif (!rstn){data, conv_done} <= 13'b0;else if (en && sclk2x && sclk_edge_cnt == 7'd33) begindata      <= rdata;conv_done <= 1'b1 ;endelse begindata      <= data;conv_done <= 1'b0;end
end//csn, sclk, din, dout
always @ (posedge clk_50M or negedge rstn) beginif (!rstn) begincsn  <= 1'b1 ;sclk <= 1'b1 ;din  <= 1'b0 ;rdata<= 12'b0;endelse if (en) beginif (sclk2x) begincase(sclk_edge_cnt)7'd0  : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0;         end7'd1  : begin csn <= 1'b0; sclk <= 1'b0;                    end7'd2  : begin              sclk <= 1'b1;                      end7'd3  : begin              sclk <= 1'b0;                      end7'd4  : begin              sclk <= 1'b1;                      end7'd5  : begin              sclk <= 1'b0; din <= channel_r[2]; end7'd6  : begin              sclk <= 1'b1;                      end7'd7  : begin              sclk <= 1'b0; din <= channel_r[1]; end7'd8  : begin              sclk <= 1'b1;                      end7'd9  : begin              sclk <= 1'b0; din <= channel_r[0]; end7'd10, 7'd12, 7'd14, 7'd16, 7'd18, 7'd20, 7'd22, 7'd24, 7'd26, 7'd28, 7'd30, 7'd32: begin  sclk  <= 1'b1; rdata <= {rdata[10:0], dout}; end7'd11, 7'd13, 7'd15, 7'd17, 7'd19, 7'd21,7'd23, 7'd25, 7'd27, 7'd29, 7'd31:begin sclk <= 1'b0; end7'd33 : begin sclk <= 1'b1; end default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; endendcaseendendelse begincsn  <= 1'b1 ;sclk <= 1'b1 ;din  <= 1'b0 ;rdata<= 12'b0;end
endendmodule

4.2 adc_driver_tb.v

以小梅哥编写的testbench加持,为节省仿真时间,设定clk的周期为2ns。

`timescale 1ns/1ns
`define p 2
`define sin_data "./sin_12bit.txt"module adc_driver_tb;reg         clk_50M     ;reg         rstn        ;reg [2:0]   channel     ;reg         en_conv     ;reg         dout        ;wire        conv_done   ;wire        csn         ;wire        sclk        ;wire        din         ;wire [11:0] data        ;adc_driver driver(.clk_50M   (clk_50M  ),.rstn      (rstn     ),.channel   (channel  ),.en_conv   (en_conv  ),.dout      (dout     ),.conv_done (conv_done),    .csn       (csn      ),.sclk      (sclk     ),.din       (din      ),.data      (data     )
);reg [11:0] memory[4095:0];
reg [11:0] addr          ;           initial $readmemh(`sin_data, memory);initial         clk_50M = 1'b0    ;
always #(`p/2)  clk_50M = ~clk_50M;integer h;initial beginrstn    = 1'b0;channel = 3'b0;en_conv = 1'b0;dout    = 1'b0;addr    = 12'b0;#(`p * 10);rstn    = 1'b1;channel = 3'b101;for (h=0; h<3; h=h+1) beginfor(addr=0; addr<4095; addr=addr+1) beginen_conv = 1'b1;#(`p * 1);en_conv = 1'b0;gene_dout(memory[addr]);@ (posedge conv_done);#(`p * 5);endend#(`p * 100);$stop;
endreg [4:0] cnt;task gene_dout(input [15:0] vdata);begincnt = 5'b0;wait (!csn);while (cnt <= 5'd15) begin@(negedge sclk) dout = vdata[15 - cnt];cnt  = cnt + 1'b1     ; endend endtaskendmodule

经过Quartus II 的分析与综合后,利用Modelsim软件进行仿真,将几个重要的信号拉出来,结果如下:

4.3 错误分析

可见:

  1. sclk_edge_cnt == 7'd33变为7'd0之后保持0不变;
  2. sclk2x信号最后一个脉冲之后,一直拉低;
  3. conv_done拉高一个周期后,en_conv在5个周期后并不拉高,导致en信号之后一直为0,不再拉高,导致整个adc_driver.v不再工作;
  4. task中的cnt信号一直为15,task卡在了while循环中。

上面四个现象,稍微好入手的是task中的cnt信号的变化。稍作分析可知,由于在cnt == 5'd15时,task在等待negedge sclk,然而由于conv_done的拉高,en信号拉低,导致csn信号为高,继而导致task在执行while的过程中直接跳到上一句wait (!csn)语句中。此后由于csn信号为高,task一直被卡住,即for循环一直卡在语句gene_dout中,无法继续执行下一句@(posedge conv_done),故addr在仿真期间一直为0,无法加1。

4.4 改进方法

由于在执行while语句的过程中被强制跳出,故需要将csn信号再往后延至少半个周期(此周期为sclk的周期,不是clk_50M的周期)。在case语句中,将sclk_edge_cnt的情况稍加更改:

case (sclk_edge_cnt)7'd32 : begin ... end7'd33 : begin sclk <= 1'b0; end7'd34 : begin sclk <= 1'b1; end...
endcase

上述语句为了后延csn信号半个sclk周期,增加了7'd34这个情况,同时为了匹配sclk信号,将7'd33对应的操作改为sclk <= 1'b0;同时,将相应的控制信号(即adc_driver.v的第71行和第99行)变为sclk_edge_cnt == 7'd34,仿真波形如下:
由于增加一个状态7'd34,且其操作是sclk <= 1'b0,故正好能够再执行while循环一次,dout得到赋值,同时cnt也能够再累加一次变为5'd16。而cnt == 5'd16时,由于csn为低,故能够保证while循环的结束,进而保证了整个task的结束。task结束后,等待conv_done的上升沿,5个周期后继续for循环。

读出的data输入如下:

5. testbench中走不出的h=0

对于for循环

reg [11:0] memory[4095:0];
reg [11:0] addr
integer h;for (h=0; h<3; h=h+1) beginfor(addr=0; addr<4095; addr=addr+1) beginen_conv = 1'b1;#(`p * 1);en_conv = 1'b0;gene_dout(memory[addr]);@ (posedge conv_done);#(`p * 5);endend

5.1 错误分析

然而在定义addr时分明是定义为reg [11:0] addr;,同时memroy也有4096个深度,而在testbench的for循环中,设定了addr < 4095,即读取的数据最多读取到memory[4094],而无法读取出memory[4095]这个数。

基于以上分析,将testbench的for循环设置为addr <= 4095,再次仿真,发现仿真根本停不下来,并且参数h一直为h = 32'h0
经分析发现,当addr = 4095时,能够读出memory[4095]这个数据;同时,下一个addr变为addr = 0,但是,addr跳变的同时h也应该跳变为1,可惜并没有。这就是为什么仿真永远停不下来的原因了:第二个for循环能够被正常执行完毕,但是并没有触发第一个for循环的累加条件,故h永远保持0,永远小于3,第一个for循环永远不会结束。

5.2 改进方法

整个逻辑似乎都没有错误,那么到底错在哪里呢?
reg [11:0] addr;
12位的addr可表示0~4095个数。然而,在第二个for循环中:

  1. addr = 4094时,循环正常工作,addr = addr + 1,即为 12'd4095
  2. addr = 4095时,循环正常工作,addr = addr + 1,注意,由于位宽的限制,addr自动变为0,而最高位的进位被丢掉,所以导致此时addr = 12'd0,满足addr <= 4095的条件,第二个for循环得以继续续执行,永远不会跳出第二个for循环来执行第一个for循环,故参数h永远不会改变。

改进:将addr的位宽扩展一位,变成reg [12:0] addr即可让所有代码正常运行。

在task中,while循环内的条件分明为cnt <= 15,但又将cnt定义为5位位宽也是同理。如果定义为4位位宽,那么cnt不论如何累加,其值永远小于等于15,task永远无法执行完毕。

6. In The End

ADC的视频教程目前只看了一半,还有ISSP等内容等待进行。然而就这一半的内容也让我花了足足两周多一点的时间进行考虑。

由此生成的这篇文章,是为两周来的小结。

关于小梅哥ADC128S022驱动设计的思考相关推荐

  1. 关于小梅哥74HC595驱动设计的思考

    文章目录 源码 RTL testbench 仿真结果 分析 改进 关于74HC595芯片的规格参数以及时序图可参考德州仪器CD74HC595或其他博客,在此不多做分析. 现如今,小梅哥用两片74HC5 ...

  2. 【小梅哥FPGA进阶教程】第九章 基于串口猎人软件的串口示波器

    九.基于串口猎人软件的串口示波器 1.实验介绍 本实验,为芯航线开发板的综合实验,该实验利用芯航线开发板上的ADC.独立按键.UART等外设,搭建了一个具备丰富功能的数据采集卡,芯航线开发板负责进行数 ...

  3. 小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计

    小梅哥FPGA:基于线性序列机的TLC5620型DAC驱动设计 目标:学会使用线性序列机的思想设计常见的有串行执行特征的时序逻辑 实验现象:在QuartusⅡ软件中,使用ISSP工具,输入希望输出的电 ...

  4. DDD 领域驱动设计-三个问题思考实体和值对象(续)

    上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...

  5. 06-BCD计数器设计与应用——小梅哥FPGA设计思想与验证方法视频教程配套文档

    芯航线--普利斯队长精心奉献 实验目的:1.掌握BCD码的原理.分类以及优缺点 2.设计一个多位的8421码计数器并进行验证 3.学会基本的错误定位以及修改能力 实验平台:无 实验原理: BCD码(B ...

  6. 外设驱动库开发笔记54:外设库驱动设计改进的思考

      不知不觉中我们已经发布了五十多篇外设驱动的文章.前段时间有一位网友提出了一些非常中肯的建议,这也让我们开始考虑怎么优化驱动程序设计的问题.在这一篇中我们将来讨论这一问题. 1.问题分析   首先我 ...

  7. 小梅哥FPGA视频教程学习总结(持续学习中……)

    首先附上小梅哥FPGA视频教程链接:https://www.bilibili.com/video/BV1va411c7Dz?p=2&spm_id_from=pageDriver 小梅哥yyds ...

  8. 小梅哥FPGA学习笔记——开发流程及仿真示例

    开发流程及仿真示例 FPGA整体设计开发流程 1. 设计定义 2. 设计输入(Quartus II) 3. 分析和综合(Quartus II) 4. 功能仿真(modelsim-altera/mode ...

  9. 小梅哥FPGA学习笔记

    小梅哥FPGA学习笔记 一.38译码器 功能: 译码器其任一时刻的稳态输出,仅仅与该时刻的输入变量的取值有关,它是一种多输入多输出的组合逻辑电路,负责将二进制代码翻译为特定的对象(如逻辑电平等).38 ...

最新文章

  1. 0x56. 动态规划 - 状态压缩DP(习题详解 × 7)
  2. 黄峥辞职,拼多多何去何从?
  3. linux c 打印错误信息error errno perror和strerror的区别
  4. 【深度学习】CUDA 和 TensorRT 博客搜集
  5. 希望今年能看懂和写出这样的Swift代码
  6. FallbackFactory启动的时候抛出异常
  7. 远程ykvm 插件移值java_Centos7 命令行下kvm安装windows,linux
  8. MPA是什么意思?一MPA简介和MPA地位
  9. IOT(6)---MQTT和CoAP
  10. 计算机视觉领域最好用的开源图像标注工具
  11. WPF应用基础篇---TreeView
  12. iOS开发-OC语言 (七)继承、多态、类别
  13. git tag 使用方法(打标签、发布及回滚)
  14. Required request body is missing:public java.util.List错误
  15. 风变编程-python(基础语法-第1关)
  16. java中加号_java中四则运算中的加号
  17. Win系统 - 你知道 insert 键的隐藏功能吗?
  18. 量化交易服务器系统选择,量化交易该选取什么云服务器
  19. 《卸甲笔记》-单行函数对比之三
  20. 2022-2028年中国三元锂电池行业市场运营格局及前景战略分析报告

热门文章

  1. 基于springboot对接芯烨云无线打印机(非云打印机)
  2. 如何通过云效Projex让项目协作任务不再千篇一律
  3. UNABLE TO READ CONSUMER IDENTITY
  4. 全路段感知、云控平台……京台高速到底有多智慧?
  5. C语言答疑合集(一)
  6. Oracle中有dateadd吗,SQL Server 中add函数到 oracle date add的操作
  7. Java、JSP网上报名系统
  8. django-外键和表关系
  9. mysql单表1000万条_mysql单表千万条数据测试
  10. Unity-URP学习笔记(三)赛璐珞阴影