目錄
- 一、知識了解
- 二、子產品設計
- 三、程式實作
- 四、管腳配置及結果展示
上一篇博文:【入門學習一】基于 FPGA 使用 Verilog 實作按鍵點燈代碼及原理講解
功能描述:通過前面一篇學習的按鍵使用,本篇文章進一步使用按鍵,通過點選按鍵後,可以讓蜂鳴器播放不同的曲子
一、知識了解
PWM 技術控制蜂鳴器不同聲響
- 所謂 PWM 就是脈沖寬度調制,本文通過變化輸出的脈沖頻率來使得蜂鳴器發出不同的音符聲音。
- 下表是每個音符所對應的頻率及半周期。
音符 | 對應頻率(Hz) | 時鐘周期數 |
---|---|---|
1 | 523 | 95600 |
2 | 587 | 85150 |
3 | 659 | 75850 |
4 | 698 | 71600 |
5 | 784 | 63750 |
6 | 880 | 56800 |
7 | 988 | 50600 |
- 音符對應的時鐘周期數越多,那麼它的一個周期就越長,從下圖可以看出來,音符從 1 到 7,它的一個周期逐次遞減。
- 是以說,如何讓蜂鳴器發聲,隻需要往蜂鳴器的管腳輸出連續的高低變化電平即可。
- 就拿我這個闆子的蜂鳴器來說,圖中有個 PNP 型三極管,當基極為高電平時截止,也就是高電平的發射極 Q7 無法導通到集電極使蜂鳴器發聲,低電平時導通,此時高電平的發射極 Q7 可以導通到集電極使蜂鳴器發聲。
- 要讓蜂鳴器發出不同的聲音,采用 PWM 原理改變基極的脈沖寬度即可,也就是前面提到的,不同音符的一個周期所對應不同寬度脈沖。
設定每個音符持續時長
- 當輸出一個周期的脈沖給蜂鳴器,它肯定會響,但是有一點,一個周期的脈沖時長隻有幾百毫秒,我們能聽到嗎?當然聽不到,是以需要不斷地重複一個周期的脈沖,使它連續輸出波形長達 1 s 或者 0.5 s,也就是一個音符的持續時長,那麼我們就肯定能聽到了。
-
這裡我以音符 1 為基準,讓它的一個周期重複 250 次,那麼它總時長為 95600 × 250 = 23 , 900 , 000 95600×250=23,900,000 95600×250=23,900,000 個時序周期,其它的音符總時長也為 23 , 900 , 000 23,900,000 23,900,000 個時序周期,是以不難得出
每 個 音 符 的 重 複 次 數 = 23 , 900 , 000 該 音 符 一 個 周 期 時 序 數 每個音符的重複次數=\frac{23,900,000}{該音符一個周期時序數}\\ 每個音符的重複次數=該音符一個周期時序數23,900,000
-
音符1 = 23 , 900 , 000 95600 ≈ 250 ( 次 ) = \frac{23,900,000}{95600}≈250(次) =9560023,900,000≈250(次)
音符2 = 23 , 900 , 000 85150 ≈ 281 ( 次 ) = \frac{23,900,000}{85150}≈281(次) =8515023,900,000≈281(次)
音符3 = 23 , 900 , 000 75850 ≈ 315 ( 次 ) = \frac{23,900,000}{75850}≈315(次) =7585023,900,000≈315(次)
音符4 = 23 , 900 , 000 71600 ≈ 334 ( 次 ) = \frac{23,900,000}{71600}≈334(次) =7160023,900,000≈334(次)
音符5 = 23 , 900 , 000 63750 ≈ 375 ( 次 ) = \frac{23,900,000}{63750}≈375(次) =6375023,900,000≈375(次)
音符6 = 23 , 900 , 000 56800 ≈ 421 ( 次 ) = \frac{23,900,000}{56800}≈421(次) =5680023,900,000≈421(次)
音符7 = 23 , 900 , 000 50600 ≈ 472 ( 次 ) = \frac{23,900,000}{50600}≈472(次) =5060023,900,000≈472(次)
歌譜
- 歌譜如下:
二、子產品設計
- 由于程式比較簡單,是以就隻需要一個蜂鳴器 .v 檔案即可,它也是頂層子產品,再在其中引用按鍵子產品。
- 其中按鍵子產品 key_debounce.v 在前一篇博文中已經貼出了,這裡就不再重複貼出代碼以及講解了。
三、程式實作
蜂鳴器子產品 pwm_buzzer.v
module pwm_buzzer(
input clk , //時鐘輸入
input rst_n , //複位按鍵輸入
input key_in , //按鍵輸入
output reg buzzer //驅動蜂鳴器
);
wire press ; //線,連接配接按鍵标志信号
//引用按鍵子產品
key_debounce u_key_debounce(
.clk (clk ),
.rst_n (rst_n ),
.key (key_in ),
.press (press )
);
//定義音符時序周期數
localparam M0 = 98800,
M1 = 95600,
M2 = 85150,
M3 = 75850,
M4 = 71600,
M5 = 63750,
M6 = 56800,
M7 = 50600;
//信号定義
reg [16:0] cnt0 ; //計數每個音符對應的時序周期
reg [10:0] cnt1 ; //計數每個音符重複次數
reg [5 :0] cnt2 ; //計數曲譜中音符個數
reg [16:0] pre_set ; //預裝載值
wire [16:0] pre_div ; //占空比
reg [10:0] cishu ; //定義不同音符重複不同次數
wire [10:0] cishu_div ; //音符重複次數占空比
reg flag ; //歌曲種類标志:0小星星,1兩隻老虎
reg [5 :0] YINFU ; //定義曲譜中音符個數
//歌曲種類标志位
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
flag <= 1'b0;
end
else if(press) begin
flag <= ~flag;
end
end
//重設音符的個數
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
YINFU <= 48;
else if(flag == 1'b1)
YINFU <= 36;
else
YINFU <= 48;
end
//計數每個音符的周期,也就是表示音符的一個周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt0 <= 0;
end
else if(press)
cnt0 <= 0;
else begin
if(cnt0 == pre_set - 1)
cnt0 <= 0;
else
cnt0 <= cnt0 + 1;
end
end
//計數每個音符重複次數,也就是表示一個音符的響鳴持續時長
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt1 <= 0;
end
else if(press)
cnt1 <= 0;
else begin
if(cnt0 == pre_set - 1)begin
if(cnt1 == cishu)
cnt1 <= 0;
else
cnt1 <= cnt1 + 1;
end
end
end
//計數有多少個音符,也就是曲譜中有共多少個音符
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt2 <= 0;
end
else if(press)
cnt2 <= 0;
else begin
if(cnt1 == cishu && cnt0 == pre_set - 1) begin
if(cnt2 == YINFU - 1) begin
cnt2 <= 0;
end
else
cnt2 <= cnt2 + 1;
end
end
end
//定義音符重複次數
always @(*) begin
case(pre_set)
M0:cishu = 242;
M1:cishu = 250;
M2:cishu = 281;
M3:cishu = 315;
M4:cishu = 334;
M5:cishu = 375;
M6:cishu = 421;
M7:cishu = 472;
endcase
end
//曲譜定義
always @(*) begin
if(flag == 1'b0) begin
case(cnt2) //小星星歌譜
0 : pre_set = M1;
1 : pre_set = M1;
2 : pre_set = M5;
3 : pre_set = M5;
4 : pre_set = M6;
5 : pre_set = M6;
6 : pre_set = M5;
7 : pre_set = M0;
8 : pre_set = M4;
9 : pre_set = M4;
10: pre_set = M3;
11: pre_set = M3;
12: pre_set = M2;
13: pre_set = M2;
14: pre_set = M1;
15: pre_set = M0;
16: pre_set = M5;
17: pre_set = M5;
18: pre_set = M4;
19: pre_set = M4;
20: pre_set = M3;
21: pre_set = M3;
22: pre_set = M2;
23: pre_set = M0;
24: pre_set = M5;
25: pre_set = M5;
26: pre_set = M4;
27: pre_set = M4;
28: pre_set = M3;
29: pre_set = M3;
30: pre_set = M2;
31: pre_set = M0;
32: pre_set = M1;
33: pre_set = M1;
34: pre_set = M5;
35: pre_set = M5;
36: pre_set = M6;
37: pre_set = M6;
38: pre_set = M5;
39: pre_set = M0;
40: pre_set = M4;
41: pre_set = M4;
42: pre_set = M3;
43: pre_set = M3;
44: pre_set = M2;
45: pre_set = M2;
46: pre_set = M1;
47: pre_set = M0;
endcase
end
else begin
case(cnt2) //兩隻老虎歌譜
0 : pre_set = M1;
1 : pre_set = M2;
2 : pre_set = M3;
3 : pre_set = M1;
4 : pre_set = M1;
5 : pre_set = M2;
6 : pre_set = M3;
7 : pre_set = M1;
8 : pre_set = M3;
9 : pre_set = M4;
10: pre_set = M5;
11: pre_set = M0;
12: pre_set = M3;
13: pre_set = M4;
14: pre_set = M5;
15: pre_set = M0;
16: pre_set = M5;
17: pre_set = M6;
18: pre_set = M5;
19: pre_set = M4;
20: pre_set = M3;
21: pre_set = M1;
22: pre_set = M5;
23: pre_set = M6;
24: pre_set = M5;
25: pre_set = M4;
26: pre_set = M3;
27: pre_set = M1;
28: pre_set = M2;
29: pre_set = M5;
30: pre_set = M1;
31: pre_set = M0;
32: pre_set = M2;
33: pre_set = M5;
34: pre_set = M1;
35: pre_set = M0;
endcase
end
end
assign pre_div = pre_set >> 1; //除以2
assign cishu_div = cishu * 4 / 5;
//向蜂鳴器輸出脈沖
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
buzzer <= 1'b1;
end
else if(pre_set != M0) begin
if(cnt1 < cishu_div) begin
if(cnt0 < pre_div) begin
buzzer <= 1'b1;
end
else begin
buzzer <= 1'b0;
end
end
else begin
buzzer <= 1'b1;
end
end
else
buzzer <= 1'b1;
end
endmodule
- 其中要說明的一點是,曲譜中有一個 “-” 的符号,我将它定位 M0,可以在向蜂鳴器輸出波形的時序邏輯中,可以看到當音符為 M0 時,它輸出高電平,也就是讓它此時不發聲。
- 還有一點時,我設定了一個時間占空比為 4/5,也就是前 4/5 發聲,後 1/5 的時間不發聲,如果不設定時間占空比也可以,隻不過它會連續不斷的播放曲譜,感覺就不好聽了,是以這裡設定了個時間占空比。
代碼執行過程:
- 首先設定 flag 的預設值為 0,那麼它最開始會自動開始循環播放小星星。
- 其中,最開始會計數一個音符的周期,直至計滿不同音符所對應的周期數,它就會歸零,重新開始計數。
- 計滿一個周期後,計數周期重複次數的計數器就會 +1,直至計滿不同音符所對應的重複次數,它就會歸零,重新開始計數。
- 然後開始該歌曲的下一個音符。
- 随着 cnt2 的值不斷累加,其所代表的音符類别也在不斷改變,這裡使用一個 case 語句來羅列一整首歌的音符順序。
- 當按下按鍵後,press 脈沖信号傳遞過來,改變 flag 的值,以及重設 cnt0、cnt1、cnt2 的值歸零。
- 當 flag 的值變為 1’b1,那麼 if 條件語句中,就會選擇第二個 case ,也就是第二首歌。
- 其實我這樣講解代碼執行過程并不正确,這樣講隻是便于了解,應該配置時鐘去看語句的執行情況,說明一點,assign 語句是随時鐘變化的,也就是說,時鐘信号變化一次,它就執行一次,品一品這個意思。
四、管腳配置及結果展示
管腳配置
- 要按照自己開發闆的管腳進行配置。
結果展示
- 最後結果是指唱歌發生,是以沒辦法展示出來,這個代碼是我自己手寫的,親測可以成功,按鍵按下後,也可以切歌。
- 如果你燒錄程式後還是無法發聲,那麼就是管腳的配置問題,具體查一下你自己開發闆的管腳原理圖,并配置正确。
- 當然,如果想實作發一個聲,LED 燈閃亮一下,也是很簡單可以做到的,如果有興趣可以做一下。
下一篇博文:【入門學習三】基于 FPGA 使用 Verilog 實作按鍵狀态機代碼及原理詳解