本文隸屬於AVR單片機教程系列。
引子
定時/計數器(簡稱定時器)是單片機編程中至關重要的一部分,再簡單的單片機也會帶有定時器。
也許你會覺得我們已經在delay
函數中接觸過定時器了,然而並不是,它只是軟件地通過“浪費時間”來實現延時。我們接觸定時器在數碼管中,segment_auto
函數可以自動完成動態掃描,好像在main
函數背后又開了一個線程,兩者並行執行一樣。這就用到了定時器中斷。
中斷是一種必要的程序流程控制方法,但這兩講我們先聚焦於利用定時器來輸出波形。
本講中,我們用定時器來輸出一定頻率的方波,讓蜂鳴器發出聲音。
定時/計數器
ATmega324PA提供了3個定時器:定時器0、定時器1、定時器2。其中,定時器0和2都是8位的,定時器1是16位的;定時器1支持輸入捕獲;定時器2有異步支持,即可以獨立於CPU時鍾工作。為了簡單起見,本講以定時器0為例。
定時器0有一個計數寄存器TCNT0
,由CPU時鍾的可配置分頻驅動,每一定時器時鍾周期增加1。
定時器0有4種工作模式:普通模式、CTC模式,還有兩種放到下一講。CTC模式下可以輸出波形,后兩種模式也有對應的波形。波形可以輸出到引腳PB3和PB4上。定時器時鍾、工作模式與波形輸出在寄存器TCCR0A
與TCCR0B
中配置。
在普通模式中,TCNT0
持續增加,在值為255時再加1會溢出變成0,因此以256個定時器時鍾周期為循環周期。這種模式一般用於產生定時器中斷。
在CTC模式中,TCNT0
增加到寄存器OCR0A
的值時,發生比較匹配,此時TCNT0
會被硬件清零,引腳電平可以被翻轉、置低或置高。如果配置為翻轉,則每匹配兩次,引腳輸出一個方波,而每次匹配需要OCR0A
值+1個周期,所以輸出方波的頻率為:\(f_{OC0A} = \frac {f_{clk\_I/O}} {2 \cdot N \cdot (1 + OCR0A)}\),其中,\(f_{clk\_I/O}\)是外設IO時鍾,頻率與CPU時鍾相同;\(N\)表示分頻系數,對於定時器0,可以是1、8、64、256或1024。
以上是對數據手冊部分信息的不完全概括。請參閱數據手冊第15章,以完成作業題。
分頻系數與OCR0A
的值應該根據想要的波形頻率來計算。首先,選擇分頻系數的原則是,在可選的值中選擇最小的。最小的分頻系數1往往是不能選的,因為計算下來OCR0A
的值會超過其可接受的最大值255
(開發板上單片機的CPU頻率是25MHz);如果分頻系數過大,OCR0A
的值會比較小,由於計算出的通常是小數而實際只能取整數,較小的數會產生較大的誤差。
比如,為了輸出1kHz的方波,先計算最小的分頻系數:\(N_{min} = \frac {f_{CPU}} {2 \cdot (1 + OCR0A_{max}) \cdot f_{OC0A}} = \frac {25000000} {2 \cdot 256 \cdot 1000} = 48.83\),因此分頻系數應取64
。再根計算OCR0A
的值:\(OCR0A = \frac {f_{CPU}} {2 \cdot N \cdot f_{OC0A}} - 1 = \frac {25000000} {2 \cdot 64 \cdot 1000} - 1 = 194.31\),所以取OCR0A
為194
。不妨再計算一下實際波形頻率:\(f_{OC0A} = \frac {f_{CPU}} {2 \cdot N \cdot (1 + OCR0A)} = \frac {25000000} {2 \cdot 64 \cdot (1 + 194)} = 1001.6Hz\),只比預期的差3個音分,相當精確。
開發板上一共有4個可以輸出波形的引腳,分別是引腳4~7,在庫中被定義為WAVE_0
到WAVE_3
。要輸出波形,必須先調用wave_mode
以指定輸出何種波形,然后再調用tone_set
輸出一定頻率的方波。
蜂鳴器
蜂鳴器有有源與無源兩種,“源”指的是振盪源。有源蜂鳴器給一定電壓就可以發出一定頻率的聲音,但不能改變;無源蜂鳴器需要方波才能發聲,聲音的頻率與方波的相同,這是可以控制的。開發板上的是壓電式無源蜂鳴器,兩極都接出來了,所以可以同時發出兩個頻率的聲音。如果只需要一個,一般把負極接地,正極接單片機引腳。
到這里你應該暫停一下,試着用tone_set
函數使蜂鳴器發出523Hz的聲音。
假設你已經實現了。程序很短吧?你也許會想當然地認為用tone_set
函數控制蜂鳴器已經足夠方便了,但實踐證明不是的。試試這段代碼:
#include <ee1/delay.h>
#include <ee1/button.h>
#include <ee1/wave.h>
#include <ee1/tone.h>
int main()
{
button_init(PIN_NULL, PIN_NULL);
wave_mode(WAVE_0, WAVE_MODE_TONE);
tone_set(WAVE_0, 523);
delay(1000);
while (1)
{
if (button_down(BUTTON_0))
tone_set(WAVE_0, 523);
else
tone_set(WAVE_0, 0);
delay(10);
}
}
在程序開始時,你會聽到一聲清脆的Do,但是之后按鍵按下時,蜂鳴器的聲音卻沒那么純粹了。這是因為,每次調用tone_set
時,波形都會從新的周期開始,而原來的周期可能只進行到一半,就使波形不是很完美——可別小看這半個周期,你不是聽到這明顯的噪音了嗎?
而buzzer_tone
函數作為進一步的封裝,在設計上避免了這個問題。它把蜂鳴器正在播放的頻率保存起來,如果調用時參數與上次的相同,則不進行任何操作。
我們來實現播放復音的功能。
#include <ee1/delay.h>
#include <ee1/button.h>
#include <ee1/switch.h>
#include <ee1/buzzer.h>
int main()
{
button_init(PIN_2, PIN_3);
switch_init(PIN_NULL, PIN_NULL);
buzzer_init(WAVE_0, WAVE_1);
uint16_t freq[] = {262, 330, 392, 523};
while (1)
{
if (switch_status(SWITCH_0))
{
uint16_t temp[2] = {0};
uint16_t* ptr = temp;
for (uint8_t i = BUTTON_COUNT; i-- && ptr != temp + 2;)
if (button_down(i))
*ptr++ = freq[i];
buzzer_tone(temp[0], temp[1]);
}
else
buzzer_tone(0, 0);
delay(40);
}
}
雖然蜂鳴器的聲音本來就比較刺耳,但和聲還是挺和諧的吧。不信?試試349和494,然后你就會覺得上面這個程序效果其實挺不錯的。
作業
-
當定時器在引腳上輸出波形時,原來的
PORT
和DDR
寄存器還有用嗎? -
閱讀數據手冊,使用寄存器,輸出440Hz的方波。
-
用旋轉編碼器控制蜂鳴器,發出音階中的音符。你可以用計算器或Excel計算好音符頻率,然后直接寫在程序中。