在嵌入式系統開發中,按鈕和 LED 的控制是基礎且重要的環節。本文將深入探討如何有效地結合這兩者,並運用中斷機制、去彈跳策略和狀態變數等技巧,打造更具彈性且效能更高的嵌入式系統。首先,我們會從 I/O 引腳的組態開始,逐步建立按鈕子系統,並探討如何使用中斷機制回應按鈕事件。接著,將介紹按鈕去彈跳的必要性以及其實作方法,以確保系統的穩定性。此外,我們將引入狀態變數的概念,藉此簡化程式碼邏輯並提升可維護性。最後,將探討如何利用計時器實作精確的 LED 閃爍控制,滿足實時性要求。

組態I/O引腳

要使用I/O引腳作為輸入,需要進行以下步驟:

  1. 將引腳新增到I/O對映標頭檔案中。
  2. 組態引腳為輸入模式,並確認它不屬於其他外設的一部分。
  3. 如果必要,組態內部拉高或拉低電阻。

建立按鈕子系統

為了隱藏系統細節,我們可以建立一個按鈕子系統,使用I/O處理模組。這個子系統可以提供一個簡單的介面,傳回按鈕的狀態。

boolean ButtonPressed();

這個函式傳回true當按鈕被按下時,否則傳回false

實作主函式

主函式可以實作為以下形式:

main:
    初始化LED
    初始化按鈕
    迴圈:
        如果按鈕被按下,關閉LED
        否則切換LED
        等待一段時間
    重複

這個實作會導致LED在按鈕被按下後有一定的延遲才會關閉。為了減少這個延遲,可以使用輪詢(polling)方法不斷檢查按鈕的狀態。

迴圈:
    如果按鈕被按下,關閉LED
    否則:
        如果足夠時間已經過去,
            切換LED
            清除已經過去的時間
    重複

然而,這種方法可能會導致系統變得遲緩。另一種方法是使用中斷(interrupt)來捕捉和處理按鈕按下事件。

迴圈:
    如果按鈕沒有被按下,切換LED

在這種情況下,中斷會在按鈕被按下時觸發,允許系統立即回應按鈕事件。

按鈕按下與LED閃爍控制

在嵌入式系統中,按鈕按下和LED閃爍控制是兩個常見的應用場景。然而,當這兩個系統耦合在一起時,可能會導致系統複雜性增加和維護困難。為瞭解決這個問題,我們可以使用中斷(interrupt)機制來處理按鈕按下事件。

中斷機制

中斷機制允許系統在執行主程式時,暫時停止並執行中斷服務程式(ISR)。ISR負責處理中斷事件,例如按鈕按下或LED閃爍控制。在本例中,我們可以使用中斷機制來捕捉按鈕按下事件,並根據按鈕按下的次數來調整LED閃爍的延遲時間。

按鈕按下事件

當按鈕按下時,系統需要知道按鈕何時被按下和何時被釋放。理想情況下,按鈕訊號應該像圖4-6頂部所示的理想按鈕訊號。如果按鈕訊號符合這個模式,我們可以在訊號上升邊緣(rising edge)觸發中斷,並執行相應的動作。

全域變數與中斷

為了讓主程式能夠知道按鈕按下的事件,我們可以使用全域變數來儲存按鈕按下的狀態。當按鈕按下時,中斷服務程式將設定全域變數為true,主程式則可以檢查這個變數來決定是否需要執行相應的動作。

volatile bool buttonPressed = false;

void interruptServiceRoutine() {
    buttonPressed = true;
}

void main() {
    while (1) {
        if (buttonPressed) {
            // 調整LED閃爍的延遲時間
            buttonPressed = false;
        }
        // 其他程式碼
    }
}

Volatile關鍵字

在C語言中,volatile關鍵字用於告訴編譯器,某個變數或物件的值可能會在任何時候改變,因此編譯器不應該對其進行最佳化。當我們使用全域變數來分享資料給中斷服務程式和主程式時,需要使用volatile關鍵字來確保資料的一致性。

volatile bool buttonPressed = false;

設定中斷

設定一個引腳觸發中斷通常與設定引腳為輸入是分開的。雖然兩者都算是初始化步驟,但我們希望保持中斷設定與現有的初始化函式分開。這樣,您可以為需要中斷組態的引腳儲存複雜性(更多請參考第 5 章)。

設定一個引腳為中斷會在我們的 I/O 子系統中新增三個函式:

  • IOConfigureInterrupt(port, pin, trigger type, trigger state):組態一個引腳為中斷。中斷會在看到某種觸發型別(如邊緣或級別)時觸發。對於邊緣觸發型別,中斷可以在上升或下降邊緣發生。對於級別觸發型別,中斷可以在高或低階別時發生。有些系統還提供了一個回撥引數,即當中斷發生時要呼叫的函式;其他系統會將回撥硬編碼到某個函式名稱,您需要將程式碼放在那裡。
  • IOInterruptEnable(port, pin):啟用與引腳相關聯的中斷。
  • IOInterruptDisable(port, pin):停用與引腳相關聯的中斷。

如果中斷不是每個引腳(可能是每個銀行),則處理器可能為每個銀行有一個通用的 I/O 中斷,在這種情況下,ISR 需要解開哪個引腳引起了中斷。這取決於您的處理器。如果每個 I/O 引腳都可以有自己的中斷,則模組可以更鬆散地耦合。

按鈕去彈跳

許多按鈕不提供清潔的訊號,如理想按鈕訊號所示。相反,它們看起來更像標記為「彈跳數字按鈕訊號」的訊號。如果您在這個訊號上觸發中斷,您的系統可能會浪費處理器週期。按鈕彈跳可能是由於機械或電氣效應。

去彈跳是一種技術,用於消除不想要的邊緣。雖然它可以在硬體或軟體中實作,但我們將關注軟體。見 Jack Ganssle 的網路文章(在第 123 頁的「進一步閱讀」中)關於硬體解決方案。

許多現代按鈕具有非常短的不確定期。按鈕也有資料表;檢視您自己的資料表,看看製造商的建議。要注意,嘗試經驗地確定是否需要去彈跳可能是不夠的,因為不同批次的按鈕可能會有不同的行為。

您仍然想要尋找訊號上升邊緣,即使用者釋放按鈕的時刻。為了避免上升和下降邊緣附近的垃圾,您需要尋找一段相對較長的時間內的一致訊號。這段時間的長度取決於您的按鈕和您想要對使用者做出反應的速度。

為了去彈跳按鈕,請在比您想要對使用者做出反應的速度更快的週期間隔內對引腳進行多次讀取(也稱為樣本)。當有多個連續的一致樣本時,提醒系統其餘部分按鈕已經改變。請參考圖 4-6;目標是讀取足夠的樣本,以便不確定的邏輯級別不會導致虛假按鈕按壓。

您需要三個變數:

  • 引腳的當前原始讀取值
  • 一個計數器,用於確定原始讀取值保持一致的時間
  • 去彈跳按鈕值(由系統使用,包括系統對使用者做出反應所需的時間)

計數器應設定為在合理的時間內對於該按鈕進行去彈跳。如果沒有產品規格,請考慮鍵盤上的按鈕被按下的速度。如果高階打字員可以每分鐘輸入 120 個字,假設每個字平均有五個字元,那麼他們每秒鐘大約會按 10 次按鈕。假設按鈕下降了一半的時間,您需要找到按鈕下降約 50 毫秒的時間。(如果您真的正在製作鍵盤,您可能需要更緊密的公差,因為有更快的打字員。)

對於我們的系統,神話中的按鈕具有虛構的資料表,指出按鈕在按下或釋放時最多會響起 12.5 毫秒。如果目標是對按住 50 毫秒或更長時間的按鈕做出反應,我們可以以 10 毫秒(100 Hz)的速度對按鈕進行取樣,並尋找五個連續樣本。

使用五個連續樣本相當保守。你可能想要調整多久輪詢一次按鈕的級別,以便只需要三個連續樣本來指示按鈕狀態已經改變。

在去彈跳方法中,平衡錯誤的成本(煩惱或災難?)和對使用者做出反應更慢的成本。

在之前的下降邊緣中斷方法中處理按鈕按壓時,按鈕狀態不如狀態改變那樣有趣。為此,我們將新增第四個變數以簡化主迴圈:

  • 讀取按鈕:如果原始讀取值與去彈跳按鈕值相同,則重置計數器;否則,減少計數器,如果計數器為零,則設定去彈跳按鈕值為原始讀取值,並設定已改變為真,然後重置計數器。
  • 主迴圈:如果是讀取按鈕的時間,則讀取按鈕,如果按鈕已改變且按鈕不再被按住,則…

使用狀態變數提高程式碼彈性

在前面的範例中,我們使用了一個簡單的迴圈來控制 LED 的切換。但是,這種方法有其侷限性,尤其是在面對多個 LED 時。為了提高程式碼的彈性和可擴充套件性,我們可以使用狀態變數來儲存目前要切換的 LED。

狀態變數的優點

使用狀態變數可以帶來幾個優點:

  • 減少程式碼複製: 不需要為每個 LED 複製一份程式碼。
  • 提高可擴充套件性: 只需修改狀態變數即可增加或減少 LED 的數量。
  • 降低複雜度: 程式碼變得更加簡潔和易於維護。

實作狀態變數

以下是使用狀態變數的範例程式碼:

// 狀態變數,儲存目前要切換的 LED
int currentLed = 0;

// LED 切換函式
void toggleLed(int led) {
    // 根據 led 引數切換對應的 LED
    switch (led) {
        case 0:
            // 切換藍色 LED
            break;
        case 1:
            // 切換紅色 LED
            break;
        case 2:
            // 切換黃色 LED
            break;
    }
}

// 主迴圈
while (1) {
    // 讀取按鈕狀態
    if (buttonChanged &&!buttonPressed) {
        // 按鈕釋放時,切換到下一個 LED
        currentLed = (currentLed + 1) % 3;
        buttonChanged = false;
    }

    // 切換目前的 LED
    if (timeToToggleLed) {
        toggleLed(currentLed);
    }
}

在這個範例中,我們使用了一個 currentLed 狀態變數來儲存目前要切換的 LED。當按鈕被按下時,我們切換到下一個 LED。toggleLed 函式根據 currentLed 引數切換對應的 LED。

圖表翻譯:
  flowchart TD
    A[開始] --> B[讀取按鈕狀態]
    B --> C{按鈕釋放時}
    C -->|是| D[切換到下一個 LED]
    C -->|否| E[繼續執行]
    D --> E
    E --> F{時間到達切換 LED 時間}
    F -->|是| G[切換目前的 LED]
    F -->|否| A
    G --> A

這個流程圖描述了程式碼的執行流程。當按鈕被按下時,程式碼會切換到下一個 LED。當時間到達切換 LED 時間時,程式碼會切換目前的 LED。

使用計時器

使用按鈕來改變LED閃爍速度很有幫助,但行銷團隊發現了閃爍速率的不確定性問題。行銷團隊希望使用按鈕來迴圈切換一系列精確的閃爍速率:6.5次/秒(Hz)、8.5 Hz和10 Hz。

這個要求看似簡單,但這是第一次需要進行時間精確控制。之前,系統可以處理按鈕和一般性的LED閃爍,但現在系統需要在實時處理LED。達到“精確”的程度取決於系統的引數,主要取決於處理器輸入時鐘的準確性和精確度。

計時器元件

從原理上講,計時器是一個簡單的計數器,透過計數來衡量時間。主時鐘越確定,計時器就越精確。計時器在背景中獨立運作,不會減慢程式碼的執行速度。技術上來說,它們是在微控制器中的矽門中運作的。

要設定計時器的頻率,您需要確定時鐘輸入。這可能是您的處理器時鐘(也稱為系統時鐘或主時鐘),或是來自其他子系統的不同時鐘(例如,許多處理器都有一個外圍時鐘)。

系統統計

當嵌入式系統工程師與其他工程師討論系統統計時,我們傾向於使用一個簡寫,包括廠商、處理器(及其核心)、每個指令中的位數和系統時鐘速度。早些時候,在本章中,我提供了STM32F103xx、MSP430和ATtiny處理器系列的暫存器示例。具有這些處理器的系統可能具有以下統計:

  • STM32F103(Cortex-M3),32位,72 MHz
  • 德州儀器MSP430 G2201,16位,16 MHz
  • Atmel ATtiny45,8位,4 MHz

最後一個數字是處理器時鐘,描述了處理器每秒可以處理的指令數。實際效能可能會更慢,如果您的記憶體存取跟不上,或者如果您可以使用處理器功能來繞過開銷,則可能更快。系統時鐘與板上的振盪器(如果您有一個)不同。感謝內部PLL電路,您的處理器速度可能比板上的振盪器更快。PLL代表相位鎖定迴路,是一種處理器可以將較慢的時鐘(即較慢的振盪器)乘以一定係數來獲得更快的時鐘(即處理器時鐘)的方法。

許多小型微控制器使用內部RC振盪器作為其時鐘源。雖然這使得硬體設計師的生活更容易,但其準確性卻留下了很多改進的空間。隨著時間的推移,可能會積累相當大的漂移,這可能會導致通訊和某些實時應用中的錯誤。

例如,ATtiny45具有最高4 MHz的處理器時鐘。我們希望LED能夠以10 Hz閃爍,這意味著需要以20 Hz中斷(一次中斷開啟一次中斷關閉)。這意味著我們需要一個200,000的除法。ATtiny45是一個8位處理器;它有兩個8位計時器和一個16位計時器。沒有任何計時器能夠計數到那麼高(見第111頁的“系統統計”側欄)。然而,晶片設計師們認識到了這個問題,並給了我們另一個工具:預分頻暫存器,它可以將時鐘分割,使計數器以較慢的速率遞增。

許多計時器是零基的,而不是一基的,因此對於一個能夠將時鐘分割為N的預分頻,你需要在預分頻暫存器中放置一個1。整個計時器東西已經夠複雜了,不需要再進行上述的數學計算。請查閱您的處理器手冊,以檢視哪些計時器暫存器是零基的。

預分頻暫存器的效果如圖4-8所示。系統時鐘規律地切換。具有預分頻值2,預分頻時鐘(我們計時器子系統的輸入)以系統時鐘速度的一半切換。計數器遞增。當計數器與比較暫存器(在圖中設定為3)相等時,處理器會注意到這一點。當它匹配時,它可能會繼續遞增或重置,取決於處理器和組態設定。

在回到ATtiny45上的計時器之前,請注意使計時器工作所需的暫存器通常由以下組成:

  • 計時器計數器 它儲存了計時器自上次重置以來的變化值(即刻數)。
  • 比較暫存器(或捕捉比較暫存器或匹配暫存器) 當計時器計數器等於此暫存器時,採取某些操作。每個計時器可能有多個比較暫存器。
  • 動作暫存器(或自動過載暫存器) 此暫存器設定當計時器和比較暫存器相同時要採取的動作。(對於某些計時器,這些動作也可在計時器溢位時使用,即計數器達到最大值。)可以組態四種可能的動作(一次或多次發生):
    • 中斷
    • 停止或繼續計數
    • 過載計數器
    • 將輸出引腳設定為高、低、切換或無改變

圖表翻譯:

  graph LR
    A[系統時鐘] --> B[預分頻暫存器]
    B --> C[預分頻時鐘]
    C --> D[計數器]
    D --> E[比較暫存器]
    E --> F[動作暫存器]
    F --> G[動作]

此圖表描述了系統時鐘、預分頻暫存器、預分頻時鐘、計數器、比較暫存器、動作暫存器和動作之間的關係。它展示瞭如何使用預分頻暫存器來控制計數器的遞增速率,以及如何使用比較暫存器和動作暫存器來觸發特定動作。

嵌入式系統對於精確時間控制的需求日益增長,從簡單的LED控制到複雜的實時系統,都離不開精確的計時機制。本文深入探討瞭如何利用中斷和計時器來實作精確的LED閃爍控制,並分析了按鈕去彈跳、狀態變數應用以及計時器原理等關鍵技術。

透過引入中斷機制,系統可以更有效地回應外部事件,例如按鈕按下,避免了輪詢方式造成的系統延遲和資源浪費。volatile關鍵字的運用,確保了中斷服務程式與主程式之間資料的一致性,是嵌入式系統開發中不可或缺的技巧。此外,狀態變數的引入,提升了程式碼的可擴充套件性和可維護性,有效降低了系統複雜度,展現了良好的軟體設計實踐。

然而,單純的中斷機制並不能滿足精確時間控制的需求。為此,本文進一步探討了計時器的應用。透過組態計時器的頻率和預分頻值,可以實作精確的定時中斷,從而精準控制LED的閃爍頻率。同時,文章也指出了系統時鐘精確度對於計時器精確性的影響,以及不同處理器架構下計時器組態的差異。目前,雖然計時器能滿足大部分應用場景,但對於極高精確度要求的系統,仍需探索更精確的時鐘源和計時方案。

玄貓認為,深入理解中斷、計時器以及軟體設計原則,對於構建高效、可靠的嵌入式系統至關重要。未來,隨著物聯網和邊緣計算的快速發展,對嵌入式系統的實時性和精確性要求將會更高,相關技術的發展和應用也將迎來新的突破。開發者應持續關注計時器技術的演進,例如更高精確度、更低功耗的計時器設計,以及與其他硬體和軟體元件的整合方案,以滿足未來嵌入式系統的發展需求。