關於小梅哥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等內容等待進行。然而就這一半的內容也讓我花了足足兩週多一點的時間進行考慮。

由此生成的這篇文章,是爲兩週來的小結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章