此篇文章,主要講述經過視頻點撥後自己動手寫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
。需要說明的是,csn
、sclk
、din
、dout
位寬都爲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。
對於此時序:
- 定義時序圖中最開始sclk爲高的那一部分的時刻爲0;
- 定義時序圖中sclk曲線上編號爲1的時刻爲時刻1,此時正好爲sclk的第一個下降沿;
- 定義時序圖中sclk的第一個上升沿爲時刻2;
- 定義時序圖中sclk的第二個下降沿爲時刻3;
- 定義時序圖中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_done
是rdata
讀完了所有的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
這個信號拉高一個週期(即爲脈衝信號),信號en
在conv_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 錯誤分析
可見:
- 當
sclk_edge_cnt == 7'd33
變爲7'd0
之後保持0不變; - 當
sclk2x
信號最後一個脈衝之後,一直拉低; conv_done
拉高一個週期後,en_conv
在5個週期後並不拉高,導致en
信號之後一直爲0,不再拉高,導致整個adc_driver.v不再工作;- 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循環中:
- 當
addr = 4094
時,循環正常工作,addr = addr + 1
,即爲12'd4095
; - 當
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等內容等待進行。然而就這一半的內容也讓我花了足足兩週多一點的時間進行考慮。
由此生成的這篇文章,是爲兩週來的小結。