程序中延時功能實現原理:軟延時

硬件 API 長什麼樣兒?一文中我們使用了延時函數來保證硬件 API 要求遵守的時序協議。示例:

void write_zero()
{
    DS18B20_DQ = 0; 
    delay_60us();
    DS18B20_DQ = 1;
    // 總線恢復時間
    delay_2us();
}

代碼中的 delay_2us() 和 delay_60us() 都是延時函數並且它們屬於軟延時,用軟件模擬的延時。與之相對的是硬延時,即用定時器等硬件來更精確地設置延時。

軟延時的原理就是讓 CPU 執行一些指令,並且執行這些指令所用的時間等於想要延時的時間,同時執行這些指令不應該影響程序的結果。例如,可以使用空循環:

for (i = 0; i < 1000; i++) {
  // noop
}

現在,問題轉變爲如何計算指令執行時間,比如執行上例的循環到底需要多長時間?要回答這個問題需要弄清楚 CPU 時序,明白 CPU 是怎麼執行指令的。下文使用 8051 單片機舉例,因爲它的實現特別簡單。

CPU 振盪週期(時鐘週期)

驅動 CPU 不停地自動執行下一條指令的關鍵部件是 “時鐘”。時鐘不間斷地產生高低電平的脈衝信號來驅動電路。如下圖所示:

晶體振盪器(簡稱晶振)使用了特殊的石英晶體元件,它在特定條件下能夠產生穩定的週期性振盪信號,經過特殊處理後可以用作時鐘。晶振可以長下面這個樣子:

時鐘頻率是時鐘的一項關鍵指標。它表示 1 秒內時鐘脈衝週期性變化的次數,單位爲赫茲 Hz。時鐘頻率反映了脈衝信號週期性變化的快慢,頻率越高振盪速度越快。它也在一定程序上決定了 CPU 速度的快慢。

拿上圖中的晶振舉例,上面的 12.000 表示頻率爲 12MHz,即 1 秒鐘完成 12, 000, 000 個振盪週期。因此,1 個振盪週期用時爲 (1 秒 / 12, 000, 000),即 1/12 微秒 (μs)。

CPU 機器週期(machine cycle)

機器週期用於衡量計算機指令的執行時間。例如,執行指令 A 需要 1 個機器週期、執行指令 B 需要 2 個機器週期等等。在 51 單片機中,機器週期與振盪週期的關係爲(假設使用了 12MHz 晶振):

接下來,我們需要知道每條指令的執行需要多少個機器週期。方法是查詢芯片廠商的技術文檔,因爲這是由指令的詳細設計和實現所決定的。

示例 1,NOP 空操作指令,需要 1 機器週期,即 1 微秒:

示例 2,DIV 除法指令,需要 4 機器週期,即 4 微秒:

開始編程

延時 1 微秒:

// 注意,這是一個特殊的庫函數!它不表示函數調用!!
// 編譯器會將其編譯爲一個 NOP 彙編指令
_nop_();

延時 2 微秒:

// 注意,這是一個特殊的庫函數!它不表示函數調用!!
// 編譯器會將其編譯爲一個 NOP 彙編指令
_nop_();
_nop_();

延時 4 微秒:

void delay_4us()
{
  // no-op
}
delay_4us();

解釋:這裏我們定義了一個空函數 delay_4us(),然後直接調用這個空函數就相當於延時 4 微秒。因爲,調用函數使用 LCALL 彙編指令,需要 2 個機器週期;從函數返回使用 RET 彙編指令,需要 2 個機器週期;一共 4 個機器週期,即 4 微秒。

延時 5 微秒:

void delay_5us()
{
  _nop_();
}
delay_5us();

延時 100 微秒:

void delay_100us()
{
  unsigned char i;
  _nop_();
  i = 47;
  while (--i);
}
delay_100us();

解釋:此例又增加使用了 MOV 和 減 1 非零轉移指令。如果延時較長,則需要使用某種形式的循環,不可能拷貝 N 個 NOP 指令的。

總結

指令和週期還有很多技術細節本文並沒有描述。軟延時不總是可靠的,原因之一是因爲中斷系統的存在。在軟延時空跑指令的時候,有可能中斷被觸發,這時 CPU 轉而去處理中斷請求;等中斷處理完畢,CPU 纔會回到之前的延時程序,此刻的延時時間早就過去了。因此,更準確的延時 / 定時功能需要使用定時器中斷功能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GNnyy2dghATwQMZskswNFw