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

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


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;         end
                7'd1  : begin csn <= 1'b0; sclk <= 1'b0;             	      end
                7'd2  : begin              sclk <= 1'b1;                      end
                7'd3  : begin              sclk <= 1'b0;                      end
                7'd4  : begin              sclk <= 1'b1;                      end
                7'd5  : begin              sclk <= 1'b0; din <= channel_r[2]; end
                7'd6  : begin              sclk <= 1'b1;                      end
                7'd7  : begin              sclk <= 1'b0; din <= channel_r[1]; end
                7'd8  : begin              sclk <= 1'b1;                      end
                7'd9  : begin              sclk <= 1'b0; din <= channel_r[0]; end
                
                7'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}; end
        
                7'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; end
               
				7'd33 : 
				begin sclk <= 1'b1; end
			
                default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; end
            endcase
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) begin
    if (!rstn)
        div_cnt <= 7'b0;
    else if (en) begin
        if (div_cnt == CNT_NUM/2 - 1)
            div_cnt <= 7'b0;
        else
            div_cnt <= div_cnt + 1'b1;
    end
    else
        div_cnt <= 7'b0;
end

//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) 
        sclk_edge_cnt <= 7'b0;
    else if (en) begin
        if (sclk2x) begin
		      if (sclk_edge_cnt == 7'd33)
					sclk_edge_cnt <= 7'b0;
			  else
					sclk_edge_cnt <= sclk_edge_cnt + 1'b1;
	    end
		else
		    sclk_edge_cnt <= sclk_edge_cnt;
	 end
	 else
	     sclk_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) begin
	if (!rstn)
		en <= 1'b0;
	else if (en_conv)
		en <= 1'b1;
	else if (conv_done)
		en <= 1'b0;
	else
		en <= 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) begin
    if (!rstn) 
        en <= 1'b0;
    else if (en_conv)
        en <= 1'b1;
    else if (conv_done)
        en <= 1'b0;
    else
        en <= en;
end

//divide the clk_50M
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn)
        div_cnt <= 7'b0;
    else if (en) begin
        if (div_cnt == CNT_NUM/2 - 1)
            div_cnt <= 7'b0;
        else
            div_cnt <= div_cnt + 1'b1;
    end
    else
        div_cnt <= 7'b0;
end

//generate sclk2x
wire sclk2x = (div_cnt == CNT_NUM/2-1);

//sclk_edge_cnt
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) 
        sclk_edge_cnt <= 7'b0;
    else if (en) begin
        if (sclk2x) begin
		      if (sclk_edge_cnt == 7'd33)
					sclk_edge_cnt <= 7'b0;
			  else
					sclk_edge_cnt <= sclk_edge_cnt + 1'b1;
			end
			else
			    sclk_edge_cnt <= sclk_edge_cnt;
			end
	 else
	     sclk_edge_cnt <= 7'b0;
end     

//channel
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) 
        channel_r <= 3'b0;
    else if (en_conv)
        channel_r <= channel;
    else
        channel_r <= channel_r;
end


//conv_done and data[11:0];
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn)
       {data, conv_done} <= 13'b0;
   else if (en && sclk2x && sclk_edge_cnt == 7'd33) begin
       data      <= rdata;
       conv_done <= 1'b1 ;
   end
   else begin
       data      <= data;
       conv_done <= 1'b0;
   end
end

//csn, sclk, din, dout
always @ (posedge clk_50M or negedge rstn) begin
    if (!rstn) begin
       csn  <= 1'b1 ;
       sclk <= 1'b1 ;
       din  <= 1'b0 ;
       rdata<= 12'b0;
   end
   else if (en) begin
       if (sclk2x) begin
           case(sclk_edge_cnt)
                7'd0  : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0;         end
                7'd1  : begin csn <= 1'b0; sclk <= 1'b0;             	      end
                7'd2  : begin              sclk <= 1'b1;                      end
                7'd3  : begin              sclk <= 1'b0;                      end
                7'd4  : begin              sclk <= 1'b1;                      end
                7'd5  : begin              sclk <= 1'b0; din <= channel_r[2]; end
                7'd6  : begin              sclk <= 1'b1;                      end
                7'd7  : begin              sclk <= 1'b0; din <= channel_r[1]; end
                7'd8  : begin              sclk <= 1'b1;                      end
                7'd9  : begin              sclk <= 1'b0; din <= channel_r[0]; end
                
                7'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}; end
        
                7'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; end
                
				7'd33 : begin sclk <= 1'b1; end 

                default : begin csn <= 1'b1; sclk <= 1'b1; din <= 1'b0; rdata <= 12'b0; end
            endcase

				end
    end
	 
    else begin
        csn  <= 1'b1 ;
        sclk <= 1'b1 ;
        din  <= 1'b0 ;
        rdata<= 12'b0;
    end
end

endmodule
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 begin
    rstn    = 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) begin
        for(addr=0; addr<4095; addr=addr+1) begin
            en_conv = 1'b1;
            #(`p * 1);
            en_conv = 1'b0;
            gene_dout(memory[addr]);
			@ (posedge conv_done);
            #(`p * 5);
        end
    end
	 
	 #(`p * 100);
	 $stop;
end

    reg [4:0] cnt;
    task gene_dout(input [15:0] vdata);
        begin
			cnt = 5'b0;
            wait (!csn);
            while (cnt <= 5'd15) begin
                @(negedge sclk) 
                dout = vdata[15 - cnt];
                cnt  = cnt + 1'b1     ; 
            end
        end 
    endtask
    
endmodule

经过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 ... end
	7'd33 : begin sclk <= 1'b0; end
	7'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) begin
        for(addr=0; addr<4095; addr=addr+1) begin
            en_conv = 1'b1;
            #(`p * 1);
            en_conv = 1'b0;
            gene_dout(memory[addr]);
			@ (posedge conv_done);
            #(`p * 5);
        end
    end
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等内容等待进行。然而就这一半的内容也让我花了足足两周多一点的时间进行考虑。

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

  • 14
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值