基於FPGA的目標反射回波檢測算法及其實現(准備篇)
:用Verilog-HDL狀態機控制硬件接口
前段時間,開發了一個簡單的目標反射回波信號識別算法,我會分幾篇文章分享這個基於FPGA的回波識別算法的開發過程和原碼,歡迎大家不吝賜教。“工欲善其事,必先利其器”,調試FPGA上的數字信號處理算法,最直接的辦法是進行行為仿真(前仿)。但有時想通過testbench產生驗證算法所需的特定激勵信號,並不是一件容易的事情。往往導致通過行為仿真驗證/調試FPGA數字信號處理算法的效率低下。
隨着任意信號發生器(Arbitrary Wave Generator)的普及,通過matlab或numpy等算法開發工具產生的激勵信號,可以輕松的通過任意波形發生器變成模擬信號,極大的方便了數字信號處理算法的開發。但想利用任意信號發生器產生的模擬激勵信號,提升算法的開發和調試效率,必須先對這些模擬激勵進行A/D轉換,並通過D/A轉換器顯示算法中各個節點上產生的中間信號。——為了方便算法的開發,我嘗試實現了一組基於PMOD接口的串行A/D、D/A。這些串行芯片的驅動電路則采用Verilog HDL實現的摩爾型狀態機。以下原創內容歡迎網友轉載,但請注明出處:https://www.cnblogs.com/helesheng
很多Xilinx公司的FPGA開發板提供一種稱為PMOD的接口,如下圖所示。
圖1 PMOD接口
PMOD只有8個有效I/O,且采用了低成本的低速接插件,只能驅動低速的串行接口ADC、DAC。但對於FPGA數字信號處理算法的開發,最重要的是能驗證算法行為的邏輯正確性。在沒有高速且可靠的A/D、D/A板卡的情況下,采用PMOD接口和低速串口A/D和D/A轉換器開發驗證算法,不失為一種有效、快捷且廉價的方法。
A/D和D/A轉換器方面,我選擇了Microchip公司廉價的12bits MCP3202和MCP4822:MCP3202在3.3V下的轉換速度為50KSPS,有兩個模擬輸入通道;MCP4822在3.3V下的建立時間為4.5uS,也有兩個模擬輸出通道。兩個芯片的SPI接口的控制方法基本類似,下面先以較為復雜的MCP4822為例,介紹Verilog HDL狀態機流程控制的實現方式。
一、 接口時序
控制MCP4822可以使用標准SPI接口的模式(1,1)和(0,0),對單個DAC通道進行控制時,官方手冊上提供的時序如下圖所示。
圖2 MCP4822單個通道的控制時序
每次SPI傳輸的長度為16bits,包含D/A輸出的數據信息、輸出通道、模擬增益和關斷信息。具體如下表所示。
由於MCP4822有A、B兩個輸出通道,為實現兩個通道同時刷新數據的功能,MCP4822還有一個數據加載引腳LDAC(以下簡稱LD)。當通過SPI口完成D/A轉換數據寄存器的刷新后,還需要在LD上給出一個低電平來將數值同時刷新到MCP4822的兩個模擬輸出通道。
根據上述時序要求,我規划了下圖所示的雙通道輸出時序。
圖3 雙通道D/A輸出時序及控制標志
上圖描述了SPI接口的時鍾SCK、片選CS和數據加載LD信號的時序,而數據引腳MOSI在並未在圖中給出。整個輸出時序包括了對兩個通道的分別賦值及統一的加載等階段,Verilog-HDL狀態機要給出的狀態除了兩個通道的分別發送及數據加載狀態之外還應包括這些狀態之間的間隔狀態。
圖中的START信號是整個電路的觸發信號,它的高電平啟動電路按照事先設定的流程依次產生所需的信號。上層電路只要控制START信號產生的頻率,就可以控制D/A轉換的頻率。而上圖下半部分的PROC_CHA、PROC_INTV1、PROC_CHB、PROC_INTV1和PROC_LD等信號則是摩爾狀態機中標志各個狀態的wire型標志信號。
二、 狀態及其遷移條件
根據上圖所示的時序要求,我設計了如下圖所示的狀態轉換圖。
圖4 雙通道D/A轉換器狀態轉換圖
上圖所示的狀態機共分為:空閑狀態(IDLE_STATE)、發送A通道數據狀態(SEND_CHA_STATE)、兩通道發送間隙狀態(INTV1_STATE)、發送B通道數據狀態(SEND_CHB_STATE)、數據發送完到加載數據之間的間隙狀態(INTV2_STATE)、加載數據狀態(LD_STATE)等共六個狀態。每個狀態都分別對應各自的標志位(即時序圖中的PROC_CHA、PROC_INTV1、PROC_CHB、PROC_INTV1和PROC_LD等標志信號),各個狀態按照各自的順序逐次出現。而狀態切換的標志則是對應的標志位被清零。
根據結構化程序/電路設計的思路,每個狀態中要實現的具體功能不在狀態機模塊中實現,而是在各自對應的Verilog-HDL Module中實現;各模塊則通過各自控制的“標志信號”和狀態機模塊交互。由於狀態機模塊不直接控制輸出,只負責檢測模塊輸出的狀態信號,狀態機顯然屬於“摩爾型”(Moore)。這種思路設計的摩爾型狀態機雖然使電路結構更清晰易懂,但會帶來狀態切換時產生1個時鍾周期的延遲的劣勢。考慮到FPGA的工作頻率可達100MHz以上,而設計的MCP4822的刷新速度只有50KHz左右,所以狀態切換時產生1個時鍾周期的延遲並不會對控制電路性能產生實質性影響。
三、 狀態機程序的設計
根據上述設計思路,采用兩段式摩爾型狀態機設計的Verilog-HDL原碼如下所示。

1 module mcp4822_state_machine 2 (clk,rst_n,state_flag,START,PROC_CHA,PROC_INTV1,PROC_CHB,PROC_INTV2,PROC_LD); 3 //這是用於控制MCP4822兩個通道輸出的狀態機 4 input clk; 5 input rst_n; 6 input START; //起始信號,高電平表示開始一次轉換 7 input PROC_CHA; 8 //通道A輸出信號標志位,高電平表示正在輸出通道A數據,低電平表示輸出完成。 9 input PROC_INTV1; 10 //兩通道數據輸出之間的間隔標志位,高電平表示狀態機正在兩通道數據輸出之間的間隔期。 11 input PROC_CHB; 12 //通道B輸出信號標志位,高電平表示正在輸出通道B數據,低電平表示輸出完成。 13 input PROC_INTV2; //數據發送完成到加載數據之間的間隔標志位,高電平表示狀態機正在數據發送和加載信號之間的間隔期。 14 input PROC_LD; //模擬數據加載階段標志位,高電平表示狀態機正處在數據加載階段 15 output[5:0] state_flag;//狀態機表示的狀態 16 parameter IDLE_STATE = 6'B000001; //空閑狀態 17 parameter SEND_CHA_STATE = 6'B000010; //發送通道A數據狀態 18 parameter INTV1_STATE = 6'B000100; //兩個發送之間的間隔狀態 19 parameter SEND_CHB_STATE = 6'B001000; //發送通道B數據狀態 20 parameter INTV2_STATE = 6'B010000; //發送數據和加載模擬信號之間的等待狀態 21 parameter LD_STATE = 6'B100000; //加載模擬信號狀態 22 reg[5:0] cur_state; //當前狀態 23 reg[5:0] next_state; //下一個狀態 24 assign state_flag[5:0] = cur_state[5:0];//用當前狀態作為輸出 25 //兩段式moore狀態機的第一段,時序邏輯控制 26 always @ (posedge clk or negedge rst_n) 27 begin 28 if(!rst_n) 29 cur_state[5:0] <= IDLE_STATE; 30 else 31 cur_state[5:0] <= next_state[5:0]; 32 end 33 //兩段式moore狀態機的第2段,控制下一個狀態的組合邏輯 34 always @ (*) 35 begin 36 case(cur_state) 37 IDLE_STATE: begin 38 if(START) //收到開始信號,則進入發送狀態 39 next_state <= SEND_CHA_STATE; 40 else 41 next_state <= IDLE_STATE; 42 end 43 SEND_CHA_STATE: begin 44 if(!PROC_CHA) //發送通道A標志為0,表示通道A數據發送完成,進入兩通道之間的等待狀態 45 next_state <= INTV1_STATE; 46 else 47 next_state <= SEND_CHA_STATE; 48 end 49 INTV1_STATE: begin 50 if(!PROC_INTV1) //兩通道發送間隔標志為0,發送間隔完成,進入通道B數據發送狀態 51 next_state <= SEND_CHB_STATE; 52 else 53 next_state <= INTV1_STATE; 54 end 55 SEND_CHB_STATE: begin 56 if(!PROC_CHB) //發送通道B標志為0,表示通道A數據發送完成,進入兩通道之間的等待狀態 57 next_state <= INTV2_STATE; 58 else 59 next_state <= SEND_CHB_STATE; 60 end 61 INTV2_STATE: begin 62 if(!PROC_INTV2) //數據發送完成等待加載間隔標志為0,等待間隔完成,模擬數據加載狀態 63 next_state <= LD_STATE; 64 else 65 next_state <= INTV2_STATE; 66 end 67 LD_STATE: begin 68 if(!PROC_LD) //模擬數據加載標志0,數據加載標志完成,空閑狀態 69 next_state <= IDLE_STATE; 70 else 71 next_state <= LD_STATE; 72 end 73 default: begin 74 next_state <= IDLE_STATE; 75 end 76 endcase 77 end 78 endmodule
上述代碼注釋詳細,這里就不在一一解釋了。
四、 SPI接口控制電路
SPI接口電路需要產生SCK、CS和MOSI,共3個接口信號;以及SPI忙的標志信號(例化為PROC_CHA和PROC_CHB)。三個接口信號分別用三個過程語句塊實現,而標志信號則由CS取反產生。具體代碼如下。

1 module mcp4822_spi(send_data16,en,clk,sck,mosi,cs,PROCING); 2 //這個模塊用於產生控制MCP4822所需的SPI信號 3 input[15:0] send_data16;//用SPI口向外發送的數據 4 input clk; 5 input en; //使能信號,只在這個信號為高電平時發送SPI數據;這個信號是由狀態機輸入和本身所屬的狀態編號比較得到,相同時en為1 6 output sck; 7 output mosi; 8 output cs; 9 output PROCING; //處理標志,1表示本模塊正在發送 10 reg[15:0] shift_reg;//移位寄存器,en為0時讀取send_data16輸入的數據,en為0時左移輸出 11 wire clk_int;//移位寄存器的時鍾,en為0時clk做時鍾存儲並行輸入的數據,en為0時SCK為時鍾,移位輸出數據 12 assign clk_int = (en&(!sck)) | ((!en)&clk);//數據是在SCK的下降沿刷新的 13 reg sck; 14 reg[5:0] cnt_sck;//產生SPI時鍾SCK的計數器 15 parameter CNT_SCK_NUM = 10'D5; //5計數后產生一次反轉,100M輸入產生10M輸出 16 reg cs; 17 reg[9:0] cnt_cs;//產生SPI片選信號CS的計數器 18 parameter CNT_CS_NUM = CNT_SCK_NUM*16*2; //CS選中的時間長度為16個SCK周期 19 20 always @ (posedge clk or posedge cs) 21 begin 22 if(cs) 23 begin 24 sck <= 0; 25 cnt_sck[5:0] <= 6'd0; 26 end 27 else begin 28 if(cnt_sck[5:0] == CNT_SCK_NUM-1) 29 begin 30 cnt_sck[5:0] <= 0; 31 sck <= !sck; 32 end 33 else begin 34 cnt_sck[5:0] <= cnt_sck[5:0] + 6'd1; 35 sck <= sck; 36 end 37 end 38 end 39 40 assign PROCING = !cs; 41 always @ (negedge clk or negedge en) 42 begin 43 if(!en) 44 begin 45 cs <= 1'b1; 46 cnt_cs[9:0] <= 10'd0; 47 end 48 else begin 49 if(cnt_cs[9:0] < CNT_CS_NUM) 50 begin 51 cnt_cs[9:0] <= cnt_cs[9:0] + 10'd1; 52 cs <= 1'b0; 53 end 54 else begin 55 cnt_cs[9:0] <= CNT_CS_NUM; 56 cs <= 1'b1; 57 end 58 end 59 end 60 61 assign mosi = shift_reg[15]; 62 always @ (posedge clk_int) 63 begin 64 //由CS控制的數據選擇電路,選擇外部設置信號還是移位信號作為輸入, 65 //即在移位寄存器的D觸發器的輸出端選擇:當cs為1時設置初值,CS為0時移位。 66 shift_reg[0] <= (cs & send_data16[0]); 67 shift_reg[1] <= (cs & send_data16[1]) | ((!cs) & shift_reg[0]); 68 shift_reg[2] <= (cs & send_data16[2]) | ((!cs) & shift_reg[1]); 69 shift_reg[3] <= (cs & send_data16[3]) | ((!cs) & shift_reg[2]); 70 shift_reg[4] <= (cs & send_data16[4]) | ((!cs) & shift_reg[3]); 71 shift_reg[5] <= (cs & send_data16[5]) | ((!cs) & shift_reg[4]); 72 shift_reg[6] <= (cs & send_data16[6]) | ((!cs) & shift_reg[5]); 73 shift_reg[7] <= (cs & send_data16[7]) | ((!cs) & shift_reg[6]); 74 shift_reg[8] <= (cs & send_data16[8]) | ((!cs) & shift_reg[7]); 75 shift_reg[9] <= (cs & send_data16[9]) | ((!cs) & shift_reg[8]); 76 shift_reg[10] <= (cs & send_data16[10]) | ((!cs) & shift_reg[9]); 77 shift_reg[11] <= (cs & send_data16[11]) | ((!cs) & shift_reg[10]); 78 shift_reg[12] <= (cs & send_data16[12]) | ((!cs) & shift_reg[11]); 79 shift_reg[13] <= (cs & send_data16[13]) | ((!cs) & shift_reg[12]); 80 shift_reg[14] <= (cs & send_data16[14]) | ((!cs) & shift_reg[13]); 81 shift_reg[15] <= (cs & send_data16[15]) | ((!cs) & shift_reg[14]); 82 end 83 endmodule
其中產生移位輸出的信號clk_int是由系統時鍾clk和SPI輸出時鍾SCK結合產生的clk_int = (en&(!sck)) | ((!en)&clk);。在en(SPI模塊使能信號)為高電平期間,clk_int是系統時鍾clk,使模塊不停地從並行的數據輸入send_data16讀取發送數據;en一旦變低SPI開始工作,則clk_int切換為SPI輸出時鍾SCK,以便於在SCK的驅動下逐位發送數據。
SPI模塊的忙狀態標志信號PROCING(后被例化為PROC_CHA和PROC_CHB)為CS取反得到,實現SPI模塊和狀態機模塊之間的交互功能。但如果由狀態機輸出的CS信號取反產生,則有可能產生“先有雞還是先有蛋”的矛盾:狀態切換為SPI模塊工作狀態(SEND_CHA_STATE或SEND_CHB_STATE)后en將產生高電平,但作為被en使能的過程賦值語句所賦值的寄存器,CS只能在下一個時鍾周期中才能產生響應。而摩爾型狀態機將在下一個時鍾周期結束時檢測到被PROCING(也就是!CS)仍未變高,從而離開SPI輸出狀態SEND_CHA_STATE或SEND_CHB_STATE。仿真時序如下圖所示。
圖5 狀態切換失敗
從圖中可以看到狀態寄存器state_flag,被切換為SEND_CHA_STATE(編號為0x02)一個時鍾周期后,由於PROCING(即cs取反)信號沒有被置位,所以直接進入了下一個狀態INTV1_STATE(編號為0x04)。我采取的解決辦法,如上述代碼所示:改為在時鍾clk的下降沿切換CS的狀態,即驅動CS的順序執行塊敏感信號變為:always @ (negedge clk or negedge en)。這樣使CS和PROCING信號在en變高的半個clk周期后就被切換為工作狀態,在下一個clk上升沿到來時PROCING已經被切換為了工作狀態,從而保持住了SPI的工作狀態。另外這樣做還是的摩爾型狀態機的狀態切換的延遲時間降低為半個時鍾周期。仿真時序如下圖所示。
圖6 狀態切換正常的時序仿真圖
五、 其他狀態電路模塊
除去兩個SPI工作狀態SEND_CHA_STATE和SEND_CHB_STATE之外,還有兩通道發送間隙狀態(INTV1_STATE)、數據發送完到加載數據之間的間隙狀態(INTV2_STATE)和加載數據狀態(LD_STATE)等三個狀態只要根據MCP4822的數據手冊產生適當時間的延遲即可。Verilog-HDL代碼如下。

1 module INTV_2_CH(en,clk,PROCING); 2 //兩個通道SPI數據加載之間的空閑時間 3 input en; 4 input clk; 5 output PROCING; //高電平表示正在進行延時處理 6 reg PROCING; 7 reg[19:0] cnt; 8 parameter CNT_NUM = 20'D5; //延時時間 9 always @(negedge clk or negedge en) 10 begin 11 if(!en) 12 begin 13 cnt[19:0] <= 20'd0; 14 PROCING <= 1'b0; 15 end 16 else begin 17 if(cnt[19:0] < CNT_NUM) 18 begin 19 PROCING <= 1'b1; 20 cnt[19:0] <= cnt[19:0] + 20'd1; 21 end 22 else begin 23 PROCING <= 1'b0; 24 cnt[19:0] <= cnt[19:0]; 25 end 26 end 27 end 28 endmodule

1 module INTV_LD(en,clk,LD,PROCING); 2 input en; 3 input clk; 4 output PROCING; //高電平表示正在進行延時處理 5 reg PROCING; 6 output LD; //控制MCP4822的模擬加載信號 7 reg LD; 8 reg[19:0] cnt; 9 parameter CNT_NUM = 20'D5; //模擬加載信號脈沖寬度 10 always @(negedge clk or negedge en) 11 begin 12 if(!en) 13 begin 14 cnt[19:0] <= 20'd0; 15 PROCING <= 1'b0; 16 LD <= 1'b1; 17 end 18 else begin 19 if(cnt[19:0] < CNT_NUM) 20 begin 21 PROCING <= 1'b1; 22 cnt[19:0] <= cnt[19:0] + 20'd1; 23 LD <= 1'b0; 24 end 25 else begin 26 PROCING <= 1'b0; 27 cnt[19:0] <= cnt[19:0]; 28 LD <= 1'b1; 29 end 30 end 31 end 32 endmodule
這兩段代碼都注意了狀態標志信號的產生時鍾更應該是clk的下降沿這個問題,從而解決了狀態切換失敗的問題。
六、 頂層驅動模塊
頂層驅動模塊的作用有二:其一,准備D/A輸出的數據;其三,例化所有模塊。具體代碼如下所示。

1 module MCP4822(clk100m,rst_n,start,dac_data_a,dac_data_b,cs,mosi,sck,ld); 2 //驅動MCP4822的頂層模塊 3 input clk100m; 4 input rst_n; 5 input start;//高電平表示DA轉換起始的信號 6 input [11:0] dac_data_a;//DA的A通道轉換數據 7 input [11:0] dac_data_b;//DA的B通道轉換數據 8 output cs; 9 output mosi; 10 output sck; 11 output ld; 12 wire [15:0] data_cha;//DA的A通道SPI輸入的數據,含有通道控制的4個位的數據 13 wire [15:0] data_chb;//DA的B通道SPI輸入的數據,含有通道控制的4個位的數據 14 wire clk; 15 assign data_cha[15:0]={1'b0,1'b0,1'b1,1'b1,dac_data_a[11:0]};//這是MCP4822輸出數據的格式,通道A,無關位,增益為1,不關斷 16 assign data_chb[15:0]={1'b1,1'b0,1'b1,1'b1,dac_data_b[11:0]};//通道B,無關位,增益為1,不關斷 17 18 wire[5:0] state_flag;//狀態機輸出的狀態線 19 parameter IDLE_STATE = 6'B000001; //空閑狀態 20 parameter SEND_CHA_STATE = 6'B000010; //發送通道A數據狀態 21 parameter INTV1_STATE = 6'B000100; //兩個發送之間的間隔狀態 22 parameter SEND_CHB_STATE = 6'B001000; //發送通道B數據狀態 23 parameter INTV2_STATE = 6'B010000; //發送數據和加載模擬信號之間的等待狀態 24 parameter LD_STATE = 6'B100000; //加載模擬信號狀態 25 wire PROC_CHA; //通道A輸出信號標志位,高電平表示正在輸出通道A數據,低電平表示輸出完成。 26 wire PROC_INTV1; //兩通道數據輸出之間的間隔標志位,高電平表示狀態機正在兩通道數據輸出之間的間隔期。 27 wire PROC_CHB; //通道B輸出信號標志位,高電平表示正在輸出通道B數據,低電平表示輸出完成。 28 wire PROC_INTV2; //數據發送完成到加載數據之間的間隔標志位,高電平表示狀態機正在數據發送和加載信號之間的間隔期。 29 wire PROC_LD; //模擬數據加載階段標志位,高電平表示狀態機正處在數據加載階段 30 wire cs_cha; 31 wire mosi_cha; 32 wire sck_cha; 33 wire cs_chb; 34 wire mosi_chb; 35 wire sck_chb; 36 assign cs = (state_flag[5:0]==SEND_CHA_STATE)? cs_cha : cs_chb; 37 assign mosi = (state_flag[5:0]==SEND_CHA_STATE)? mosi_cha : mosi_chb; 38 assign sck = (state_flag[5:0]==SEND_CHA_STATE)? sck_cha : sck_chb; 39 assign clk=clk100m; 40 41 mcp4822_state_machine i_state_machine(//狀態機例化模塊 42 .clk(clk), 43 .rst_n(rst_n), 44 .state_flag(state_flag), 45 .START(start), 46 .PROC_CHA(PROC_CHA), 47 .PROC_INTV1(PROC_INTV1), 48 .PROC_CHB(PROC_CHB), 49 .PROC_INTV2(PROC_INTV2), 50 .PROC_LD(PROC_LD) 51 ); 52 53 mcp4822_spi i_mcp4822_spi_cha(//A通道SPI控制模塊的例化 54 .send_data16(data_cha), 55 .en((state_flag[5:0]==SEND_CHA_STATE)), 56 .clk(clk), 57 .sck(sck_cha), 58 .mosi(mosi_cha), 59 .cs(cs_cha), 60 .PROCING(PROC_CHA) 61 ); 62 63 mcp4822_spi i_mcp4822_spi_chb(//B通道SPI控制模塊的例化 64 .send_data16(data_chb), 65 .en((state_flag[5:0]==SEND_CHB_STATE)), 66 .clk(clk), 67 .sck(sck_chb), 68 .mosi(mosi_chb), 69 .cs(cs_chb), 70 .PROCING(PROC_CHB) 71 ); 72 73 INTV_2_CH i_intv_between_ch(//兩個通道SPI通信之間的間隙模塊例化 74 .en((state_flag[5:0]==INTV1_STATE)), 75 .clk(clk), 76 .PROCING(PROC_INTV1) 77 ); 78 INTV_2_CH i_intv_behind_ch(//兩個通道SPI通信完成后到模擬數據加載之間的間隙模塊例化 79 .en((state_flag[5:0]==INTV2_STATE)), 80 .clk(clk), 81 .PROCING(PROC_INTV2) 82 ); 83 84 INTV_LD i_intv_ld(//模擬數據加載模塊例化 85 .en((state_flag[5:0]==LD_STATE)), 86 .clk(clk), 87 .LD(ld), 88 .PROCING(PROC_LD) 89 ); 90 91 endmodule
其中,SPI通信所需的16bits數據,除了12bits的D/A數據外,還有四個bits的配置信息,可由MCP4822數據手冊查詢其具體含義。實現代碼為:
assign data_cha[15:0]={1'b0,1'b0,1'b1,1'b1,dac_data_a[11:0]};//這是MCP4822輸出數據的格式,通道A,無關位,增益為1,不關斷
assign data_chb[15:0]={1'b1,1'b0,1'b1,1'b1,dac_data_b[11:0]};//通道B,無關位
在START信號觸發下,上述MCP4822電路的整體仿真情況如下圖所示,完全滿足數據手冊的要求。
圖7 MCP4822驅動模塊整體仿真時序圖
七、 A/D轉換器MCP3202的接口電路設計
MCP3202是SPI接口的雙通道A/D轉換器,用狀態機控制其工作的方式與MCP4822相近,在這里就不在贅述了。