[FPGA]Verilog利用PWM調製巧妙完成RGB三色彩虹呼吸燈(給簡約的題目以美妙的解答)
概述
實現彩虹呼吸燈
題目就是這麼簡短,但這是目前我碰到的最有意思的一道題目,因為他有無數種解決方法,並且每一種都是那麼高階或者巧妙,比如
- 可以利用3路不同初相的PWM調製訊號驅動三顆RGB燈重疊呼吸
- 利用1路PWM訊號以及狀態機,將一個週期分為3個狀態,分別是[R降G升B滅],[R滅,G降,B升]和[R升,G滅,B降],依次往復實現重疊呼吸
- 將PWM拆分為3段,分別為升,降,滅,在不同時間週期性的輸送給RGB實現重疊呼吸
當然,不只這幾種,還有更高階的方法或者生成語句也可以更加簡練的完成題目,在這裡,我將採取上面羅列的幾種方法的一種折中方案,採取"拆分PWM","三元運算子實現單行條件訊號分配","監視模組內執行情況並以監視訊號作為狀態轉換的觸發條件"來實現彩虹呼吸燈.
題目分析
題目只有七個字:"實現彩虹呼吸燈",其中"呼吸兩個字",已經確定了這個實驗和脈寬調製扯不開干係,另外"彩虹"也說明這個實驗需要很多的色彩,單單靠單色LED是完成不了的,一定需要三色RGB完成,並且只是讓R,G,B三個LED交替呼吸,也達不到"彩虹"的效果,所以需要讓三色燈按照一定的規律重疊呼吸,這裡為了方便,我按照下圖示意的樣式進行程式設計
(抱歉畫工實在欠缺,咳咳)
意思就是在R燈最亮時,G燈開始升,R燈開始降,在G燈最亮時,R燈已滅,B燈開始升,G燈開始降,以此類推.
通過這個圖也可以容易的分成三個情況,用以實現狀態機.
PWM
PWM是個啥?
PWM( Pulse width modulation )就是脈衝寬度調製,是一種通過數字訊號對模擬訊號控制的有效技術.簡單來說,規律的進行脈寬調製,比如將一束方波的佔空比不斷減小,那麼這束方波的有效值也相應的減小,佔空比增大,有效值也增大,藉此來對LED的亮度進行控制,加以週期性的增減,即可實現呼吸燈.
呼吸燈只是PWM的一個具體應用.
PWM咋實現?
在之前的學習早已接觸過PWM調製的實現方法,在這裡直接給出程式碼,可以通過註釋回憶PWM實現過程
module PWM (input CLK ,input FLAG//標誌位,控制輸出的PWM是升還是降(1升0降) ,output STT//監視訊號(脈衝) ,output PWM ); reg[24:0]cnt1; reg[24:0]cnt2; parameter freq=2400;//通過這個freq來控制PWM的週期 reg stt;//監視狀態 always@(posedge CLK) if(cnt2==freq-1)//cnt2滿,則狀態為1(只持續一個時鐘週期) stt<=1'b1; else stt<=1'b0; assign STT=stt; always@(posedge CLK) if(cnt1>=freq-1)//滿則清零 cnt1<=1'b0; else cnt1<=cnt1+1'b1; always@(posedge CLK) if(cnt1==freq-1)//cnt1滿,以cnt1從空到滿為一個週期執行操作 if(FLAG)//升的情況 if(cnt2>=freq-1) cnt2<=1'b0; else cnt2<=cnt2+1'b1;//升 else//降的情況 if(cnt2<=0) cnt2<=freq-1; else cnt2<=cnt2-1'b1;//降 else cnt2<=cnt2; assign PWM=(cnt1<cnt2)?1'b0:1'b1;//PWM的核心,輸出調製好的PWM訊號 endmodule
本程式碼參考此網頁,內有更詳細的圖片和講解
程式碼中的stt
和STT
是監視脈衝,不影響PWM輸出;輸入訊號FLAG
控制PWM輸出訊號是升還是降.二者作用在頂層程式碼處詳細解釋.
頂層模組
PWM很容易實現,需要動腦子的就是如何通過例化模組來實現交替呼吸.下面給出我的演算法.
例化模組
先看程式碼
wire UP;
wire DW;
wire STT0;
wire STT1;
PWM up(CLK,1,STT0,UP);
PWM dw(CLK,0,STT1,DW);
其中up
例化模組中的1
代表FLAG
,在此表示這個up
例化模組是一個"升"模組,即為可以產生控制LED亮度從滅到亮的PWM訊號,dw
例化模組則代表可以產生一個可以控制從亮到暗的PWM訊號.通過這個設計可以將PWM模組的功能拆分,提供兩種模式供主模組靈活呼叫.
程式碼中的UP
和DW
分別為代表亮度升和亮度降的PWM訊號.
狀態分析
這裡不按照文首的那種狀態機思路來寫,而是將RGB三色燈分成3路對待,這裡先以R為例.
對R來說,他的亮滅規律為:升(一單位時間),降(一單位時間),滅(一單位時間).然後可以以此來寫條件語句進行訊號分配,可能第一時間想到的就是直接定義一個分頻,不同時間顯示不同狀態即可,但是這種寫法不利於後期拓展,易讀性和可維護性也稍差,在這裡採用很方便的"三元運算子"解決.先來看這段程式碼
reg[1:0]flag0=2'b00;
always@(posedge STT0)
if(flag0==2'b10)
flag0<=1'b0;
else
flag0<=flag0+1'b1;
這裡定義了一個標誌為flag0
,它是以上文提到過的監視脈衝STT
為觸發進行遞增計數的,STT
是一個在PWM模組內每一個工作週期完成後就輸出一單位時間高電平的監視脈衝,通過這個脈衝可以知道PWM已經工作完一個週期,可以進行下一週期的工作,在頂層程式碼裡則充當了狀態轉移的觸發條件.
再來看這一行程式碼
assign LED[0]=(flag0==2'b00)?UP:((flag0==2'b01)?DW:1'b1);
這一行是實現RGB燈工作狀態的核心程式碼,通過(兩層)三元運算子在一條表示式內就完成了條件賦值.
這條程式碼的意思就是,如果標誌位flag0
是2'b00
,則R亮度升,若不是,則檢測標誌為是否為2'b01
,若是,則R亮度降,如不是,則滅.然後通過上一個程式碼塊中的程式碼可以知道,每一個PWM週期完成後(表現為R已到達最亮或者最暗),狀態發生轉移,標誌為變為下一個狀態,R也就在完成了亮度升之後立刻開始亮度降,巨集觀表現為"呼吸"的狀態.
程式碼整合
上文裡兩個程式碼塊就足以讓一個燈完成一個狀態的工作,這部分程式碼如下
reg[1:0]flag0=2'b00;
always@(posedge STT0)
if(flag0==2'b10)
flag0<=1'b0;
else
flag0<=flag0+1'b1;
assign LED[0]=(flag0==2'b00)?UP:((flag0==2'b01)?DW:1'b1);
reg[1:0]flag1=2'b01;
always@(posedge STT0)
if(flag1==2'b10)
flag1<=1'b0;
else
flag1<=flag1+1'b1;
assign LED[1]=(flag1==2'b00)?UP:((flag1==2'b01)?DW:1'b1);
reg[1:0]flag2=2'b10;
always@(posedge STT0)
if(flag2==2'b10)
flag2<=1'b0;
else
flag2<=flag2+1'b1;
assign LED[2]=(flag2==2'b00)?UP:((flag2==2'b01)?DW:1'b1);
三個燈就相當於將這一段程式碼例化三次,就可以讓三色燈分別進行互相不影響的狀態轉移(呼吸變化),但是我們的目的是讓他們按照文首圖中的規律重疊呼吸,該怎麼實現呢?
這很簡單,很容易想到,三段一樣的程式碼裡都分別有一個獨立的標誌為flag
,他是reg
型別資料,所以可以在定義時給他分配一個初始狀態,這樣就相當於給三個燈設定了不同的初相,在後面工作的時候由於工作週期相同,就會一直保持最開始的相位差,週期性的進行文首圖中的交替呼吸.
至此,彩虹呼吸燈已經完成.
效果
最後的效果圖點此檢視,圖片較大,載入可能比較慢.(因為燈實在是太亮了,就蒙了一層紙來觀察顏色變化)
後話
這篇文章是目前寫過的第二費力的了,其中的程式碼更新了很多很多次,在琢磨更精簡更巧妙的演算法上和修Bug上花了很多的時間和精力,前前後後燒錄上板測試不下50次(不誇張T_T),在本地commit了無數個版本,回滾了無數次,一遍一遍修改,最後才得到了你看到的這些程式碼.我的水平有限,所以就算如此文中的程式碼和講解一定有所缺漏,還請希望大家多多包涵,並指出不足之處,改進這篇文章,來幫助更多的人.
本專案完整程式碼存放在我的Github中,最新版以Github上為準(順路給顆Star唄