讀者可能熟悉 Arduino IDE 中的 delay( ) 函數。這是一個簡單的功能,提供適用於 Arduino 系列微控制器中所有成員的阻隔延遲。當您過渡到裸機微控制器程式設計時,您可能會發現自己正在尋找類似的程式碼。不幸的是,您不太可能在 8051 的「標準庫」中找到這樣的延遲函數。
在本文中,我們將簡要探討硬體延遲方法,然後使用一組嚴格定義的假設提出 8051 Busy Bee 解決方案。
關於 89051 似乎缺少 delay() 函數的解釋
這種看似缺失 delay() 函數的情況,其主要原因或許在於彈性。需要理解的是,Arduino 程式碼掌控著底層微控制器的大部分功能;時脈頻率和專用計時器等參數都已預先設定並隱藏在後台。這種隱藏的統一性帶來了簡單的程式設計體驗,確保了 delay() 函數在所有平台上都能表現一致。若不採用這種方式,Arduino 程式設計師必須深入了解硬體架構及週邊的工作原理,而這違背了 Arduino 旨在降低門檻、方便初學者及愛好者使用的初衷。
在典型的 8051 開發環境中,情況大不相同。這裡沒有任何隱藏的底層細節,通常需要你親自配置所有必要的周邊。你需要自行管理時脈系統,既可以選擇高速外部時脈、內部時脈或 32.768kHz 晶振,也可以選擇深度睡眠模式——僅保留看門狗定時器用於定期喚醒微控制器。上述任何一種選擇,都可能導致像 delay() 這樣的函數無法如預期運行,甚至徹底失效。
請找一份 Busy Bee 參考手冊,我們這就開始吧。
技術提示:「阻隔」(blocking)一詞表示微控制器的主程式在整個延時期間處於阻塞狀態(即不執行任何操作)。對於短時間的延時或簡單任務,這種方式通常是可以接受的,但在某些情況下可能會導致無法接受的運行表現。例如,當阻隔式延遲正在進行時,微控制器將無法回應按鍵操作。解決此問題的替代方案包括使用中斷或非阻隔式延遲。
建構延時程序的方法多種多樣。
首先,我們需要認識到,在微控制器中實現延時功能有多種途徑。以下列舉了幾種常見方法:
-
編寫精細的彙編程式碼,利用 NOP( 不作業指令)來實現。在這種方式下,程式設計師需要根據每個彙編指令的特性來計算微控制器的時脈週期。
-
使用具有向量中斷功能的硬體定時器。與延遲相關的操作既可以直接嵌入到中斷服務程序(ISR)中,也可以利用該中斷來維護系統時間(類似於 Arduino 的
millis()函數)。 -
不使用中斷的自由運行(free-running)硬體定時器。在這種模式下,硬體定時器持續運作。這類似於觀察掛鐘並進行模 60 運算:假設當前時間是 50s,若需要 20s 的延時,只需等待秒針指向 10s 刻度即可。在8051單晶片中,根據所選定時器類型的不同,此方案對應的模數可能是 256 或 65536。操作的“速度”取決於系統時鐘和定時器預分頻配置。
-
不使用中斷的受控硬體定時器。採用此方法時,使用者程式會先停止定時器,將其預設為特定值,啟動定時器,然後等待溢位發生。
每種方案各有利弊。具體選擇在很大程度上取決於專案的特定需求、程式設計師的技能水平、未來的可維護性以及可用的硬體資源。例如,如果專案需要穩定的「心跳」訊號,那麼採用向量中斷的硬體計時器就是一個非常理想的方案。這種高精度的週期性定時非常適合用於比例積分微分控制器(Proportional Integral Derivative Controller)或利用數位類比轉換器(DAC)產生波形。
關注阻隔式延遲
本文將介紹一種不使用中斷、僅利用硬體定時器實現的解決方案。回想一下,微控制器的硬體定時器通常就像時鐘的秒針一樣進行遞增計數。本範例開發所使用的 Silicon Labs EFM8BB1 8051 配備了多種定時器,其中包括四個與標準 8051 相容的 16-bit 通用硬體定時器。
此 16-bit 定時器執行模 2^{16} 運算。就像時脈從 0 計數到 60 一樣,該 16-bit 計時器會從 0 計數到 65535,隨後回零(溢位)。這種溢位事件具有特殊性,因為硬體會自動置位中斷標誌。圖 1 中的簡化框圖展示了這個操作過程。
圖 1:Busy Bee 定時器 #2 的簡化框圖。
技術提示:週邊裝置的中斷標誌不會自動觸發中斷;只有當對應的中斷啟用位元在中斷啟用暫存器或擴充中斷啟用暫存器中被置位時,才會觸發中斷。
此中斷標誌對於本文附帶程式碼中所述的阻塞式延遲至關重要。此延時功能的具體實作步驟如下:
-
配置定時器,設定適當的預分頻系數(如 1:1)和工作模式(如 16-bit 自動重裝模式);同時使用
sbit關鍵字為運作控制位元和中斷標誌位元定義別名。這些配置操作在while(1)主循環之前執行一次。 -
關閉定時器並清除中斷標誌。
-
向定時器載入所需的延時計數值。
-
置位定時器的運轉控制位元(啟動定時器)。
-
進入
while迴圈進行空轉(不執行任何操作),直到中斷標誌置位。此操作會阻塞main()函數中的所有其他程式碼,直到標誌被置位。
技術提示:原始 8051 採用了獨特的架構,支援對暫存器進行位元級操作。這意味著無需使用遮罩來選取暫存器中的特定位,從而產生高效率的程式碼。這項功能透過 Keil C51 的
sbit組譯語句來實現。EFM8BB1 等衍生型號保留了這項特性。然而,與原始 8051 相比,這些新產品包含更多的周邊裝置及相應的特殊功能暫存器(SFR)。遺憾的是,並非所有暫存器都支援位元尋址。只有位址以 0x0 或 0x8 結尾的暫存器才能使用這種便捷高效率的
sbit運算。仔細查閱參考手冊可知,位址為 0xC8 的 TMR2CN0 暫存器支援位元尋址。
與阻隔延遲相關的呼叫及數學運算開銷
在結束本節之前,我們需要考慮呼叫函數和計算重載值所帶來的開銷。例如,下面這段簡短的程式碼就存在一個嚴重問題:
tmr_load = -((n_us * 49) >> 1);
此方法試圖透過一種巧妙的方式來處理「每微秒 24.5 個時脈週期」這一數值。為了處理那個「半個」週期(即 0.5),它沒有使用浮點數(float),而是先乘以 49,再透過兩次右移運算除以 2——即分別處理高位和低位(包含進位)。
最後一步是從 2^{16} 中減去重裝載值(reload value)。其簡便做法是對結果取負。換言之,在模 65536 的運算環境下,計算 65536 – x 與計算 0 – x 的結果相同,因為 65536 等同於 0(模組 65536)。這就像時鐘計時一樣,其中 60 等同於 0(模樓 60)。
在下一行程式碼中,我們將 100 加到 tmr_load 變數上。這是一種粗略的方法,用於補償函數呼叫開銷以及計算 tmr_load 所需的時間。請記住,Busy Bee 晶片沒有硬體乘法器,因此利用 8-bit ALU 執行 8-bit 與 16-bit 的乘法運算需要耗費時間。
這種函數開銷帶來的一個限制是無法實現極短的延遲。由於開銷約為 4us,因此無法實現短於此時間的延遲。如果需要此類短延時,則應使用前述包含 NOP 操作的手寫彙編程式碼。
實際運轉結果如圖 2 所示:可以看到接腳電平先保持高電平 5us,隨後變成低電平 5us,最後再次變成高電位。針對相關的 blk_ms_delay 函數也獲得了類似的結果——經 Digilent Analog Discovery 測量,呼叫 blk_ms_delay(2000) 時的實際耗時與設定值的偏差在 0.01ms 以內。您是否認為該阻隔型函數提供了合理的效能表現?
圖 2:針對設定為 5µs 導通與 5µs 關斷的實際訊號進行的示波器測量。
結語
如前所述,隨附的程式碼高度依賴 Busy Bee 晶片的時脈、預分頻及定時器配置。如果實際應用場景與程式碼開發時的預設條件不符,您需要相應地修改程式碼。
例如,您可能會為了節能而改用頻率較低的振盪器,或利用特定技巧讓微控制器進入深度睡眠模式。又或者,您可能希望微控制器能以最快速度喚醒,迅速執行某項操作後再重新進入睡眠狀態。
正是這種靈活性,使得微控制器「裸機」程式設計比您以往接觸過的高階程式設計更具挑戰性;但也正是這種靈活性,成為了充分釋放硬體效能的關鍵。
歡迎在下方留言,分享您的意見或建議。
/***************************************************************************************************
*
* This code was developed on the EFM8BB1 featuring a 24.5 MHz internal clock.
*
* Note that the TMR2CN0 SFR is located at 0xC8. Since it ends in 0x08, it is one of the
* bit-addressable memory locations. This is convenient as it allows the Keil C51
* compiler to use the sbit construct.
*
* Be sure to include these statements:
*
* sbit TR2 = TMR2CN0^2; // Timer 2 run control
* sbit TF2 = TMR2CN0^7; // TMR2 16-bit overflow on the 0xFFFF to 0x0000 transition.
*
* Also, don't forget to configure timer 2:
*
* TMR2CN0 = 0x00; // default for timer 2: clear overload flag, 16-bit, timer off
* CKCON0 |= CKCON0_T2ML__SYSCLK; // use system clock
*
*/
/***************************************************************************************************
*
* @brief Microsecond blocking delay based on T2
*
* Given a 24.5 MHz system clock, this function provides a delay between 5 and 1000 us.
* The actual max value without an overflow error is 1337. However, it's easier and
* less error prone to remember 5 to 1000.
*
* Note that this function cannot be used for single us delays as it takes longer than that to
* perform the function calls and math to calculate the appropriate timer delay.
*
* An empirical correction is added to account for the calling overhead and delay calculations.
*
* This function assumes a 24.5 MHz system clock. It also assumes a 1:1 pre-scale or Timer 2.
* For fast computation:
* 1) multiply n_us by 49
* 2) divide by 2 using a shift right
*
* @param n_us Identifies the number of microseconds to delay.
*
* @warning There are no guard rails for overflow.
*
* @warning Delays less than 5 us will be extended to 5 us.
*
* @warning For improved performance replace the reload calculation with a lookup table.
*
*/
void blk_us_delay (uint16_t n_us){
uint16_t tmr_load;
if (n_us < 5){ // Extend small delays to 5 us
n_us = 5;
}
tmr_load = -((n_us * 49) >> 1);
tmr_load += 100; // Estimate accounting or the calling overhead
// and the machine cycles to perform the previous
// 8 by 16-bit multiplication.
TR2 = 0; // Stop timer
TF2 = 0; // Clear timer overflow flag
TMR2L = tmr_load;
TMR2H = tmr_load >> 8;
TR2 = 1; // Start timer
while (!TF2); // Wait for the overflow
TR2 = 0; // Stop timer to save power
}
/***************************************************************************************************
*
* @brief Millisecond blocking delay based on blk_us_delay which in turn is based on T2.
*
* Provides a delay between 1 and 65,534 ms.
*
* @param n_ms Identifies the number of milliseconds to delay.
*
* @warning For improved accuracy be sure to use a hardware timer with a large pre-scale value.
*
*/
void blk_ms_delay (uint16_t n_ms){
uint16_t i;
for(i = 0; i < n_ms; i++){
blk_us_delay (1000);
}
}

