(原創)用Verilog實現一個參數化的呼吸燈(Verilog,CPLD/FPGA)


1.Abstract

    觀察到一個有趣的現象,每當把Apple筆記本合上的時候,那個白色的呼吸燈就會反復地由暗漸明,然后又由明漸暗,乍一看就像Apple筆記本在打盹休息一樣,十分可愛!於是突發奇想,要不用Verilog也寫一個吧,資源也不需要太多,一個LED燈就可以了。為了使用方便,可以把它做成參數化的,可以根據時常進行參數調節;深睡、淺睡跟清醒的時候呼吸頻率似乎是不一樣的…

    下面就來分析和實踐一下。

2.Content

  2.1 理論分析

    根據上述描述的現象,仔細分析一下,就是對LED燈亮的占空比設置,畫張圖來表示更直觀一些。

image FIG2.1 LED燈占空比示意圖

    為了表達得更明晰一些,將LED這根信號線用紅色凸顯出來。從左邊往右邊看吧,在RST的復位信號之前是一個不確定的邏輯(前一個狀態,可能是0,也可能是1),所以用黑色塊加深表示,復位后LED輸出低電平(將電路設計為LED信號為1時驅動燈管亮)。過一段時間以后,輸出一個占空比為1%的高電平,再過一段時間,輸出為2%的高電平,依次往后直到輸出為100%的高電平,這樣現象就是燈管逐漸變亮;要是燈管逐漸變暗,直接將邏輯反過來就可以了。

  2.2 整體設計

    縱觀整個燈的變化過程,可以歸結為4個過程,依次為逐漸變亮、亮保持、逐漸變滅、滅保持,依次用S0,S1,S2,S3表示;若有復位信號產生,則所有狀態立刻轉到S0;若沒有復位信號產生,那么就在這個狀態機里邊循環。用狀態轉換表和狀態圖來表示就容易理解得多。

TAB2.1 狀態轉換表

當前狀態

跳轉條件 下一個狀態
S0 RST / !S0_end S0
S0 S0_end S1
S1

RST

S0
S1 !S0_end S1
S1 S0_end S2
S2 RST S0
S2 !S0_end S2
S2 S0_end S3
S3 RST / S3_end S0
S3 !S3_end S3

    用狀態圖來表示。

image FIG2.2 邏輯狀態轉換圖

    雖然轉換表與狀態轉換圖表達的功能都是一樣的,乍一看上去,還是覺得狀態裝換圖更加容易接受一些,紅色表示非正常狀態下的轉換,綠色表示正常循環。

    狀態轉換就設計完了,每一個狀態的時長比較長,都是秒為單位的,下面就是關於怎樣點燈了。燈的亮滅跟狀態有關,每一個狀態下燈的變化是不同的。在S0下,燈是隨着占空比的增加而逐步變亮的,這樣的重復多次,才能達到柔和平緩的效果(只重復一次的話,變換節湊就太明顯了,效果生硬不堪);在S1下,是燈亮的至高點,銜接燈逐步變亮到開始變暗的過度;在S2下,燈開始逐步變滅,和S1一樣,燈隨着占空比的增加而逐步變暗,這樣的過程需要重復多次,達到一個柔和平緩的效果;在S3下,燈完全熄滅,銜接燈逐步變暗到開始逐步變亮的過度。

    展開S0或S2下的一個燈逐步變化的過程,以S0為例。S0從占空比為0逐步變化到100%,對應的每個占空比下也應該分多個循環,進行控制LED燈。這樣看的話,就好像有一個層次結構,主層次占空比是S0逐步從0增加至100%,頻率比較小,次級層次則是對應當前主層次的占空比進行比較,也是從1逐步增加到100,若小於當前占空比,則燈亮,否則,燈滅,它的頻率比較大。分三種層次來看,從整體上看,就是四種狀態的切換,頻率最小;從主層次上的某個占空比看,LED燈亮的程度取決於當前主層次上這個占空比;從次級層次上來看,如果在主層次的占空比內,則亮燈,否則就滅燈。

    整個邏輯描述就完成了,下面根據這個思路開始描述電路。因為對復位沒有太嚴格的要求,所以做成同步時序邏輯電路。

/* -----------------------------------------------
    Module Name:        breath
    Module Function:    參數化的呼吸燈
    Module Input:        CLK  時鍾,
                        RST  復位
    Module Output:        LED  LED燈
    Module Reference:    None
    Note:                參數設定
    #######################################
    CLK_50M     時鍾設定,默認是50M Hz
    UP_TIME     上升時間,單位為秒 
    HD_TIME        最亮保持時間,單位為秒
    H2_TIME        熄滅保持時間,單位為秒
    DN_TIME        下降時間,單位為秒
    #######################################
   ---------------------------------------------*/
module breath(LED,CLK,RST);
output reg LED = 0;

input CLK;
input RST;

// 時鍾參數化
parameter CLK_50M = 28'd50_000_000;

// 時間參數化
parameter UP_TIME = 1;
parameter HD_TIME = 1;
parameter H2_TIME = 1;
parameter DN_TIME = 3;

parameter CNT_MAIN_UP_FULL = CLK_50M / 200 * UP_TIME;      // 1/2 時間百分比
parameter CNT_MAIN_DN_FULL = CLK_50M / 200 * DN_TIME;     
parameter CNT_SUB_UP_FULL  = CNT_MAIN_UP_FULL / 10000;     // 占空比內分為100個子份
parameter CNT_SUB_DN_FULL  = CNT_MAIN_DN_FULL / 10000;     
parameter CNT_HD_FULL = CLK_50M * HD_TIME;                 // 保持時間
parameter CNT_H2_FULL = CLK_50M * H2_TIME;

// 狀態機  上升-高亮保持-下降-熄滅保持-上升
//          S0 -> S1  ->  S2 ->  S3  -> S0
parameter     S0 = 2'd0,
            S1 = 2'd1,
            S2 = 2'd2,
            S3 = 2'd3;

// 偽時鍾
reg main_clk = 0;
reg sub_clk = 0;

// 計數器
reg [27:0] cnt_main_clk = 28'd0;
reg [27:0] cnt_sub_clk = 28'd0;

reg [27:0] cnt_hold = 28'd0;

reg [7:0] cnt_main = 8'd0;
reg [7:0] cnt_sub = 8'd0;

reg [1:0] status = 2'd0;

// 狀態結束標志
reg up_end = 1'b0;
reg hd_end = 1'b0;
reg h2_end = 1'b0;
reg dn_end = 1'b0;

// 記錄上一個時鍾狀態
reg last_main_clk = 1'b0;
reg last_sub_clk = 1'b0;

always @(posedge CLK)
begin
    if(!RST)        // 復位
        begin
            cnt_main_clk <= 28'd0;
            cnt_sub_clk <= 28'd0;
            cnt_hold <= 28'd0;
            
            main_clk <= 1'b0;
            sub_clk <= 1'b0;
            
            cnt_main <= 8'd0;
            cnt_sub  <= 8'd0;
            
            status <= 2'd0;
            
            LED <= 1'b0;
            
            up_end <= 1'b0;
            hd_end <= 1'b0;
            h2_end <= 1'b0;
            dn_end <= 1'b0;
        end
    else    begin
                // 當前狀態確定
                case(status)
                S0:    begin  // 上升狀態
                        if(cnt_main_clk > CNT_MAIN_UP_FULL - 1) // 產生主時鍾
                            begin
                                cnt_main_clk <= 28'd0;
                                main_clk <= ~main_clk;
                            end
                        else begin cnt_main_clk <= cnt_main_clk + 1; end
                        
                        if(cnt_sub_clk >  CNT_SUB_UP_FULL - 1) // 產生子時鍾
                            begin
                                cnt_sub_clk <= 28'd0;
                                sub_clk <= ~ sub_clk;
                            end
                        else begin cnt_sub_clk <= cnt_sub_clk + 1; end
                    end
                S1:    begin // 高亮保持
                        if(cnt_hold > CNT_HD_FULL - 1)
                            begin
                                cnt_hold <= 28'd0;
                                hd_end <= 1'b1;
                            end
                        else begin cnt_hold <= cnt_hold + 1; end
                    end
                S2:    begin // 下降
                    if(cnt_main_clk > CNT_MAIN_DN_FULL - 1)    // 產生主時鍾
                            begin
                                cnt_main_clk <= 28'd0;
                                main_clk <= ~main_clk;
                            end
                    else begin cnt_main_clk <= cnt_main_clk + 1; end
                        
                    if(cnt_sub_clk >  CNT_SUB_DN_FULL - 1)    // 產生子時鍾
                        begin
                                cnt_sub_clk <= 28'd0;
                                sub_clk <= ~ sub_clk;
                        end
                    else begin cnt_sub_clk <= cnt_sub_clk + 1; end
                    end    
                S3:    begin  // 熄滅保持
                        if(cnt_hold > CNT_H2_FULL - 1)
                            begin
                                cnt_hold <= 28'd0;
                                h2_end <= 1'b1;
                            end
                        else begin cnt_hold <= cnt_hold + 1; end
                    end            
                endcase
                
                // 主沿處理
                if(last_main_clk != main_clk)  
                begin
                    last_main_clk <= main_clk;
                    if(main_clk) //模擬上升沿
                    begin
                        cnt_main <= cnt_main + 1;
                        if(cnt_main > 8'd99)
                            begin
                                cnt_main <= 8'd0;
                                if(status == S0) up_end <= 1'b1;
                                if(status == S2) dn_end <= 1'b1;
                            end                        
                    end
                end
                
                // 子沿處理
                if(last_sub_clk != sub_clk)
                begin
                    last_sub_clk <= sub_clk;
                    if(sub_clk)    // 模擬上升沿
                    begin
                        if(cnt_sub == 8'd99)
                            begin
                                cnt_sub <= 8'd0;
                                LED <= 1'b1;
                            end
                        else
                            begin
                              cnt_sub <= cnt_sub + 1;
                              
                                if(status == S0)
                                    begin
                                        if(cnt_sub < cnt_main) 
                                          LED <= 1'b1;    //邏輯正
                                        else    LED <= 1'b0;
                                    end
            
                                if(status == S2)
                                    begin
                                        if(cnt_sub < cnt_main)
                                          LED <= 1'b0;    //邏輯負
                                        else    LED <= 1'b1;
                                    end
                            end
                    end
                end
            
                // 狀態機切換
                if(up_end) begin 
                          up_end <= 1'b0;
                          cnt_main_clk <= 28'd0;
                          cnt_sub_clk <= 28'd0;
                          status <= S1;
                          LED <= 1'b1;
                        end
                if(hd_end) begin 
                           hd_end <= 1'b0;
                           cnt_hold <= 28'd0;
                           status <= S2;
                           end
                if(dn_end) begin
                             dn_end <= 1'b0;
                             cnt_main_clk <= 28'd0;
                             cnt_sub_clk <= 28'd0;
                             status <= S3;
                             LED <= 1'b0;
                           end
                if(h2_end) begin
                             h2_end <= 1'b0;
                             cnt_hold <= 28'd0;
                             status <= S0;
                           end
            end
end

endmodule

    源碼看起來挺多的,主要分成三大部分,時鍾產生、沿檢測和數據處理、狀態切換。設計的是上升和下降時間分割成100份,更符合占空比設計,每一個占空比下循環檢測100次,達到平緩柔和切換的效果。看看生成出來的邏輯網表,生成的RTL視圖下的邏輯網表很大,最后以附件的方式完整給出,這里就截取局部的看一下吧。

image FIG2.3綜合出的電路邏輯網表

    紅色的線是指CLK時鍾,狀態機輸入均與觸發器連接,保證電路是沿觸發操作。中間的黃色部分是一個狀態機,表示的是上述理論設計中狀態的切換,打開看一下。

image FIG2.4 綜合后的狀態轉換圖

    注意觀察下紅色線,這樣的狀態是在預先的設計中沒有的,但是狀態轉換圖將所有狀態都列出來了,可以說是對手工設計的一種補充吧,為確定設計的狀態機是正確的,一般都會生成出一個與TAB2.1相似的表格,可以對照檢驗一下。

image FIG2.5 計算機生成的電路轉換表

  2.3 電路驗證

    驗證電路最重要的方法還是功能驗證和時序邏輯驗證,一般時序邏輯驗證要求比較高一點,在功能驗證成功而時序驗證不成功的情況下,可以采用換更高速率的器件或者降低時鍾頻率,最為關鍵的是功能驗證需要正確。

    因為測試的時間要求很長,軟件自帶的仿真工具受限,所以采用modelsim來輔助驗證,在測試之前,首先要寫好測試程序。

module breath_tb;
  
reg CLK = 1'b0;
reg RST = 1'b1;

wire LED;

initial begin
      CLK = 1'b0;
  #30 RST = 1'b0;
  #50 RST = 1'b1;
  #50000000 $stop;
end

always #10 CLK = ~CLK;

breath M0 (.LED(LED), .CLK(CLK), .RST(RST));

endmodule

    加入測試波形以后的仿真結果。

image FIG2.6 測試仿真圖

    限於屏幕,測試仿真圖只能截取部分,兩根紅色的標尺表明了狀態從S0轉換到S1再到S2的分界線,在S0下(最左邊),LED的頻率變化逐步變高(第一個信號),S1時是高亮保持,LED信號為高電平,S2下,LED的頻率變化逐步變小(可能看得不太明顯),再截取局部看看。

imageFIG2.7 測試仿真圖局部

    S0下占空比是100的標尺,在sub_clk的上升沿,次級層次占空比加1,滿100后變為0重新開始計數。

    邏輯驗證正確以后,最后將生成的數據燒錄到FPGA芯片上,觀察板子上LED的現象。

    視頻地址:http://v.youku.com/v_show/id_XODI5NjA3MjA4.html

2.4 小結

    電路設計和驗證做完了,可以說是達到了相應的效果,LED燈慢慢地呼吸起來,跟開始描述的一樣,像是打盹睡覺一般,十分可愛!

    覺得唯一的不足就是描述的程序沒有模塊化,整段就在一個always塊內,看起來特別龐雜。檢測時鍾邊沿用的是偽上升沿。嘗試過將這些模塊分開開來,但是綜合和適配的時候產生了問題,時序電路的一個寄存器不能再多個塊內進行賦值操作,受於水平有限吧,暫時還沒有想到更好的方法來替代,希望有經驗的朋友能夠指點一二,提前先謝謝了!

3.Conclusion

     雖然只是一個呼吸燈小程序,用文字的方式把它們記錄下來確實花了不少功夫;把設計從草稿紙上有條理地搬到電子版上來,多個軟件都需要熟練操作。值得肯定的是,用電子版的方式記錄這些,既可以理清邏輯思維,也方便以后查看和完善,很是方便。

4.Platform

    1. Quartus II Version 9.1 Build 222

    2. ModelSim SE PLUS 6.5

    3. TimeGen 3.1

    4.MicroSoft Office Visio Professional 2003 SP3

5.Reference

     [1] Verilog 數字系統設計教程 夏宇聞

6. Attachment

    呼吸燈RTL視圖文件:http://files.cnblogs.com/hechengfei/%E5%91%BC%E5%90%B8%E7%81%AFRTL%E8%A7%86%E5%9B%BE.pdf

7.Notes

    附加一點說明吧,其實很不願意用寫程序這個詞兒來說寫硬件電路,盡管編程跟描述電路的前期這個用文本寫相應的語句過程是一樣的,某些朋友就會立即嚴正地指出,這樣說是完全不對的、不科學、甚至錯誤!首先我想非常感謝這些朋友,嚴格上來說,用程序這個字眼確實有些不對,不科學,感謝你們的細心批評指正,不過也容我稍微說明一下,這里使用程序字眼,是指廣泛意義上程序,是使用文本編輯器書寫語句、進行語法檢查等這樣通用的過程,后期邏輯綜合和邏輯驗證抽象起來看也是一個結果生成和驗證的過程,它們的過程類似,所以用了程序這個字眼。懇請這些朋友能接受我的這樣理解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM