為 Arduino 馬達控制設計一個基於穩健正交編碼器 ISR 的程式

什麼是正交編碼器?

正交編碼器是一種機電感測器,用於測量物理旋轉,例如本工程簡介中介紹的馬達軸。然後,相關的微控制器或可程式邏輯控制器(PLC)可以解釋感測器的數據來計算旋轉速度、方向和行進的總角距離。

正交編碼器在許多應用中被普遍使用,因為它們成本低且提供精細的測量解析度。圖 1 所示 Pololu 的 #4754 直流馬達就是代表性範例。它採用非接觸式霍爾效應感測器來提供反應輸出,可直接耦合到圖中的 Arduino Nano Every

以下文章中,我們將展示如何設計一個響應式伺服系統,包括比例積分微分(Proportional Integral Derivative, PID)控制器。強大的正交編碼器介面對於伺服馬達的性能至關重要。

這篇文章是為誰而寫?

本文是為過渡期的業餘愛好者和學生撰寫的。它假設您熟悉 Arduino 微控制器並已成功編寫了幾種您自己的設計。本文將透過一個具有與正交編碼器相關的中等時間要求的應用即時範例引導您進入下一個層級。


圖 1:本文探討了圖中 Pololu #4754 直流馬達和 Arduino Nano Every 微控制器的應用。背景中可以看到 DigilentAnalog Discovery

正交編碼器如何運作?

術語「正交」是指相位相差 90 度的訊號。例如,特斯拉著名感應電動機的交流電由正弦和餘弦波形驅動。正交編碼器以正交波形進行操作。實際上,它使用如圖 2 所示的數字表示。在這裡我們看到正交編碼器的雙數位輸出訊號及其與 SIN 和 COS 波形的關係。觀察當對應的正弦波高於零時每個輸出都處於活動狀態。圖 1 顯示了代表性的正交編碼器。它安裝在馬達的末端,我們可以看到黑色的環,周圍有一系列的磁性元件。我們還可以看到磁環 Arduino 側的霍爾效應感測器。

想了解更多關於正交編碼器的資料,請參閱另一文章「正交編碼器在系統中的工作與訊號變化」。它提供了有關雙數字波形性質的基本資訊。它描述了用於確定旋轉方向的關鍵關係。它還引入了圖 3 所示的狀態機,這在我們後面的討論中很重要。

圖 2:正向(CW)正交訊號,藍色訊號領先綠色訊號。當對應訊號為正時,Q1 和 Q2 感測器啟動。

為什麼我的正交編碼器在高速時無法運作?

馬達驅動的正交編碼器並不是一個簡單的應用。我們很快就會看到,這是一個突破微控制器極限的應用程式。因此,我們必須密切注意時間和微控制器的反應能力。在很多情況下,程式設計是不夠的。相反,我們必須使用基於硬體的解決方案,例如中斷服務程序(Interrupt Service Routine, ISR)。正如將要展示的,ISR 是微控制器硬體和軟體技術的組合,旨在增強微控制器對現實世界事件的回應。

正交編碼器程式失敗的最可能原因是由於錯誤的程式設計技術或微控制器響應緩慢而導致遺失了計數。回想一下,基於正交編碼器的系統透過觀察並回應編碼器雙數字輸出的變化來追蹤位置。如果系統(微控制器和相關程式)速度不夠快,就會錯過計數。其結果是機械系統失控。例如,如果正交編碼器正在監控伺服馬達的位置,系統將失去正確定位對應馬達軸的能力。

本文較後部分將介紹一種確定您的系統是否足夠快的技術。使用示波器,您將了解如何計算用於處理正交編碼器任務的時間百分比。

由馬達驅動時編碼器每轉計數(Counts Per Revolution,CPR)的重要性

建構正交編碼器的方法有很多種。然而,每個編碼器都設計有特定的每轉計數(CPR)。我們可以用一張撲克牌和一個輻條自行車輪來形象化地展示這一點。當車輪轉動時,我們可以將輻條的數量計算為卡片上的刻度。相反,透過了解輻條的數量,我們可以確定車輪的角度位置。

我們確定軸的角位置的分辨率由輻條的數量決定。例如,如果車輪只有 4 根輻條,我們的角度測量就限制為 90 度。如果車輪有 64 根輻條,我們的解析度現在為 5.625 度。

CPR 是正交編碼器,類似於輻條的數量。它描述了軸的「輻條」和感測元件之間的物理關係。對於我們的 Pololu #4754 馬達,「輻條」由交替的北磁體和南磁體組件組成。

所選的 Pololu 馬達的 CPR 為 64。這並不代表 64 個 N/S 磁鐵組件。我們的車輪和輻條的類比太簡單了。相反,我們預計只會看到 16 個插槽。從圖 2 中,我們觀察到孿生輸出有效地將每個週期分為 4 個獨特的狀態。因此,16 個槽(16 個 N/S 磁體對)足以獲得 64 的 CPR。

正交編碼器分辨率是 CPR 的函數

請注意,64 的 CPR 描述感測器安裝在馬達軸上。另外,請注意 Pololu #4754 是一款齒輪馬達,其傳動比為 70:1。要確定在輸出軸處測量的 CPR,將馬達的 CPR 乘以齒輪比,得到每轉 4480 次計數。

CPR 和旋轉速度決定每秒計數

Pololu 馬達的最後一個也許是最關鍵的指標是空載軸轉速,為 150 RPM。這代表著馬達轉速為 10,500 RPM 或每秒 175 轉。透過 CPR 為 64,我們計算出微控制器必須響應以每秒 11,200 個週期運行的系統。

響應每秒 11,200 次的馬達並非易事。需要仔細的編程以便我們不會錯過任何一個轉換。可能可以透過主 Arduino 循環執行此活動。然而,在主循環內執行任何其他操作都會有問題。例如,執行該專案的下一個自然演進並實現 PID 控制器將非常困難。非阻隔程式碼技術(參考另一文章「Mastering Non-Blocking Arduino Delay By Adapting PLC Techniques」)變得複雜。此外,向串行監視器或 LCD 顯示器發送訊號將帶來真正的挑戰,因為這些操作有時可能比每秒 11,200 次的馬達允許的時間更長。

如何將正交編碼器與中斷服務程序(ISR)集成

傳統的解決方案是使用中斷服務程序(ISR)。中斷是微控制器的決定性屬性之一,它允許微控制器及時回應非同步現實世界事件。

什麼是中斷服務程序(ISR)?

ISR 是專用硬體和軟體技術的組合,允許微控制器近乎即時地響應。為了更好地理解 ISR,讓我們專注於稱為微控制器狀態的概念。無需太過技術性,就可以認識到微控制器是一種運作組合(順序)邏輯的高度專業化的狀態機。回想一下,順序邏輯需要記憶,因為任何未來的動作都取決於機器中保存的狀態(過去的記憶)。有幾個暫存器(記憶體位置)與微控制器的核心密切相關,用於保存狀態。這些包括狀態暫存器、程式計數器和本機工作暫存器。

當發生中斷時,微控制器將快速保存其當前狀態,然後跳到特定事件的軟體。重要的是要認識到這種狀態改變是硬體內建的,導致從主代碼到中斷代碼的快速轉換。這就像你正在讀書時電話鈴響了一樣。您將目前頁面加入書籤,然後跳起接聽電話。打完電話(中斷)後,您可以返回上次中斷的地方。

技術提示:將 ISR 和主循環分別視為前台和後台會很有幫助。ISR 處於前台,具有高優先級,因為它必須處理時間敏感的任務。在這個特定的例子中,ISR 必須毫無差錯地對每秒 11,200 次計數的正交編碼器做出回應,位置將會遺失。其他任務(例如將資料傳送至序列監視器或更新 LCD 顯示器)均在背景處理。它們降低了優先級,並且只要微控制器的上下文被保存,就可以中斷。

保持 ISR 簡短非常重要,這樣微控制器才能將大部分時間花在後台任務上。例如,當馬達以最大速度運轉時,微控制器在 ISR 中花費的時間不到 5%。如果時間較長,後台任務將因 CPU 週期不足而導致運作緩慢。在極端情況下,ISR 可能無法在下一次變更發生之前完成對正交編碼器變更的服務。在這種情況下,系統將失去位置,並且沒有剩餘的週期來執行後台任務。

正交編碼器 ISR

我們對 Arduino 正交編碼器的實作從這兩行程式碼開始:

attachInterrupt(digitalPinToInterrupt(QUAD1_A_PIN), ISR_QUAD1, CHANGE);
attachInterrupt(digitalPinToInterrupt(QUAD1_B_PIN), ISR_QUAD1, CHANGE);

請注意,QUAD1_A_PIN 或 QUAD_B_PIN 的任何變更都會中斷。也要注意,兩個事件都呼叫相同的 ISR_QUAD1 程式碼。

具有錯誤檢測功能的基於狀態的正交編碼器 ISR

如果您還沒有這樣做,建議參考另一篇介紹正交編碼器的文章「正交編碼器在系統中的工作與訊號變化」。它描述了有限狀態機(FSM),如圖 3 所示。閱讀文章時,請注意正交編碼器的輸出如何具有固定模式 00 → 01 → 11 → 10 → 00 用於正向(順時針)旋轉和相關的 00 → 10 → 11 → 01 → 00 用於反向(逆時針)操作模式。這反映在圖 3 上方的順時針狀態轉換和內部的逆時針轉換。

請注意,某些轉變被視為故障。例如,如果系統處於狀態 00,而正交輸出突然轉變為 11,則表示出現了問題。該計數被視為損壞,並且系統進入故障狀態。為了清楚起見,圖 3 中未顯示返回自我循環。

程式碼的一部分如下所示。程式碼直接遵循圖 3,並顯示了從 00(9 點鐘)狀態開始的可能狀態轉換。系統可以將 CW 模式移至狀態 01。它可以將 CCW 移動到狀態 10。它可以停留在目前狀態(循環至自身)。最後,如果正交輸入從 00 跳變為 11,系統將進入故障狀態。請注意,4 位元組 quad_1_cnt 根據有效狀態轉換而遞增或遞減。

以下是 Arduino 程式碼:
QUAD.ino (7.3 KB)

switch (quad_1_state) {

case 0b00:

    if (BA == 0b00) {
        ;
    } else if (BA == 0b01) {
        quad_1_state = 0b01;
        quad_1_cnt++;
    } else if (BA == 0b10) {
        quad_1_state = 0b10;
        quad_1_cnt--;
    } else{
        quad_1_state = 0xFF;
    }
    break;

圖 3:正交編碼器訊號的狀態圖表示。氣泡代表狀態,線上的數字代表感測器值。為了清楚起見,保持狀態循環已被消除。

從 ISR 到主循環的原子資料傳輸

此時,ISR 會忠實地複製正交編碼器所測量的馬達軸位置。我們可以宣布勝利並結束這篇文章。然而,我們缺乏 ISR 和主循環之間的基本協調步驟。

回想一下,Arduino Nano Every 是一台 8bit 機器。在原子層級上它對位元組寬度變數進行操作。通常,C 編譯器會使用諸如 int32_t(long 類型)之類的關鍵字來隱藏這個不可分割的事實。然而,我們必須認識到,一個簡單的操作,例如 quad_1_cnt++; 需要至少四個離散的 ATMega4809 指令。第一個操作將文字 1 新增到最低位元組。接下來是三條帶進位的加法指令,將加法運算傳遞到所有位元組。

技術提示:術語「原子」指的是不可分割的事物。在微控制器中,原子性與微控制器的本機位寬相關。例如,8bit 機器對位元組寬度變數進行操作,而 32bit 機器對 4bit 組原子元素進行操作。在程式的非同步部分之間傳輸非原子變數時會出現複雜情況。本工程簡介中包含的主要範例涉及使用 8bit 機器在 ISR 和主循環程式碼之間傳輸 32bit 值。在傳輸過程中需要仔細同步以保持資料完整性。我們可以使用原子這個詞來表示整個 32bit 變數必須小心地作為單一原子單元傳輸。

再說了,通常我們並不關心這些事情。但是,我們必須認識到主循環和 ISR 是非同步運作的;指令之間缺乏自然的協調。回想一下,我們的目的是將正交編碼器計數傳輸到主循環。如果沒有適當的協調,星星的排列就會不整齊,主循環就會在複製過程的中途被 ISR 打斷。結果變數將會被破壞。

這種原子安全規定就像讀取時鐘一樣。假設我們以 ISR 維護的實際時間 29sec 開始。讓主線代碼複製 10 位數字。然後讓 ISR 中斷該行程。假設實際時間現在是 30sec。當 ISR 返回主代碼時,第二位數字被捕獲為 0。因此,主代碼認為實際時間是 20,而實際上,實際時間是 30sec。請注意,此錯誤具有高度間歇性。

對即時系統使用標誌和郵箱同步

為了防止錯誤,我們可以停用全域中斷。然而,這在即時系統中是非常不可取的,因為我們面臨錯過事件的風險。相反,我們將重點放在涉及標誌(基本信號量)和郵箱的解決方案。該標誌是一種資訊請求。郵箱是用於原子傳輸的受保護的記憶體位置。

結果很簡單,主代碼將透過標誌向 ISR 請求資料。對於我們的 8bit 機器,該標誌是一個單一易失性全域變數。原子(不可分割)標誌由主循環設定。 ISR 識別出該標誌已設定。它透過將資料放入適當的易失性全域記憶體位置來做出回應。然後 ISR 清除該標誌。

同時,主代碼進入while 循環等待標誌清除。我們看到,在以下函數中,有 1 個延遲,等待 quad_1_flag 清除。請注意,我們新增了超時功能以防止阻隔程式碼。

int32_t get_quad_1(uint32_t timeoutms) {
    int32_t last_known_position;
    int32_t startMillis = millis();

    if (!quad_1_flag) { // Retrieve the latest data if no request is pending
        last_known_position = quad_1_mailbox;
    }

    quad_1_flag = 1; // Request new data. This line is redundant if the flag is already set from a previous iteration.
    while (quad_1_flag) {
        if (millis() - startMillis > timeoutms) {
            return last_known_position;
        }
    }

    return quad_1_mailbox;
}

仔細分析 get_quad_1 函數可以發現一種解決 ISR 和主線程式碼非同步特性的方法。請注意,當主線程式碼需要更新時,無法保證會呼叫 ISR。該函數不會等待 ISR,而是會逾時並傳回最後已知的位置。當馬達轉動非常緩慢時,這可以防止阻隔程式碼。

技術提示:好的編譯器會檢查您的程式碼並採取捷徑。根據設置,透過展開循環並將小函數直接插入程式碼中,產生的機器碼可能會更短(使用的 ROM 更少)或更快。對於 ATMega4809 等微控制器,編譯器還可以透過使用本機工作暫存器而不是存取 RAM 來最佳化程式碼。無論哪種方式,微控制器上運行的機器碼都是原始程式的影子。

編譯器並不特別了解 ISR 和主循環之間共享的變數。它可能採用變數最佳化的捷徑,從而破壞程式兩個部分之間的關聯。我們必須將共享變數宣告為易變的以防止編譯器最佳化。

ISR 與主循環之間的協同時間調度

在上一節中,我們探討了一種從 ISR 到主循環程式碼的可靠資料傳輸方法。在得出結論之前,我們應該考慮 ISR 的時間面。

回想一下,ISR 是一種中斷。這是一種硬體方法,用於保存前後狀態並切換到特定的程式碼片段(例如本文中介紹的 ISR_QUAD1 例程)。如果不考慮微控制器在 ISR 中花費的時間百分比,那麼有關 ISR 的討論就不完整。之前,我們將 ISR 描述為前台進程,並將主循環描述為後台進程。 ISR 是高優先級的前台進程,而常規、低優先事件則被降級到後台。

我們必須認識到微控制器的運作受架構和時脈速度的支配。架構(位元寬和指令類型)決定了特定操作所需的機器週期數,而時脈速度決定了完成這些操作的速度。

無論如何,微控制器是一種受限資源。可以執行的操作非常有限。當我們考慮到該資源在 ISR 和主循環之間共享時,這一點變得尤為重要。 ISR 必須與主循環配合。

如何測量 ISR 時間和百分比

如前所述,ISR 必須很短,否則它將使主線程式碼循環匱乏。更糟的是,較長的 ISR 可能會消耗所有時脈週期,從而阻止主程式碼運行,或者在正交編碼器的情況下,導致錯過轉換並導致計數混亂。

可以使用示波器或邏輯分析儀來分析 ISR 的時序。圖 4 中的範例顯示了 Pololu #4754 在 13VDC 下空載運行的波形。此影像中有三個波形,包括正交編碼器的 A 和 B 輸出以及與 ISR 所花費時間相對應的脈衝。請注意,任何 A 或 B 正交訊號轉換後都會立即進入 ISR。

Time_ISR 脈衝易於程式設計。只需在進入 ISR 時將 I/O 引腳設為高電平,然後在退出之前立即清除它。該結果對 ISR 提供了合理的估計。在此範例中,ISR 大約在 3.8us 內完成。假設 ISR 呼叫之間的時間間隔為 85us(每秒近 12k 個脈衝),則 ISR 消耗了總 CPU 週期的約 5%。這應該算是一個簡短的 ISR,能夠很好地利用微控制器的資源。請注意,這是最壞的情況,馬達運轉電壓高出額定電壓 1VDC。

圖 4:顯示正交編碼器 A 和 B 訊號的波形以及顯示在 ISR 中所花費時間的脈衝。請注意,每次 A 或 B 變更時都會輸入 ISR。

技術提示:架構和時脈速度之間存在權衡。ARM Cortex 是一種精簡指令集電腦(RISC)架構,就是一個很好的例子。RISC 位於 ARM 名稱中,因為它代表高級 RISC 機器(ARM)。這些微控制器採用簡單的指令集來提高速度,從而打造出精簡的矽片。與其遠親的豐富命令相比,RISC 機器必須為給定操作完成更多的指令 - 但它們以較高的時鐘速度完成。換句話說,微控制器不僅僅具有時脈速度。

ISR 中直接連接埠操作的重要性

Arduino 團隊在抽象複雜的微控制器暫存器方面做得非常出色,其結果是易於使用的設備成為了微控制器的絕佳介紹。不幸的是,Arduino 速度不快,因為任何已提供的函數例如 digitalWrite() 都必須經過抽象層才能產生特定的機器碼。當我們在 ISR 的時間限制內運作時,這種時間相關的抽象就變得非常重要。

我們可以透過使用裸機編程來減少 ISR 時間。我們失去了所有的可移植性和基於 Arduino 的簡單性,但我們贏得了時間。例如,考慮這行專門為 ATmega4809 編寫的程式碼:

  BA = VPORTD.IN & 0x03;

它假設正交編碼器輸入直接連接到 4809 連接埠 D 的下兩個引腳。此單一語句讀取連接埠(1 個時脈週期),然後隱蔽除了下兩個位元之外的所有位元(1 個時脈週期)。結果非常快 – 接近 4809 的最佳解決方案。

對範例程式碼的審查表明,執行了幾個直接的 ATmega4809 操作。這是將 ISR 時間減少到總週期 5% 的重要步驟。如果沒有這個微控制器特定的操作,ISR 就會消耗掉總時間的近 20%。

技術提示:與微控制器的特殊功能暫存器(Special Function Register ,SFR)進行低階互動的裸機程式設計是嵌入式程式設計師必備的技能。它需要深入探索微控制器的資料表並了解微控制器架構、記憶體結構和硬體週邊設備。回想一下,Arduino 抽象化了這些細節,提供了一個易於使用的程式設計環境,並且在所有 Arduino 系列成員中保持一致。同時,Arduino IDE 確實允許直接存取本說明中演示的 SFR。這為初學者提供了一種探索微控制器的好方法,而無需完全使用專用的 IDE。然而,在您的教育過程中的某個階段,您應該轉向完整的 IDE,例如用於 ATMega4809 的 MPLAB。這是擴大人才儲備的必要步驟。您會發現,深入研究特定的微控制器將使您更好地理解所有微控制器及其對響應式即時編程的影響。

總結

將高速馬達驅動的正交編碼器與微控制器連接起來並不是一個簡單的應用。如我們所見,高速運轉的馬達每 85us(約 12kHz)需要微控制器注意一次。如果不使用微控制器的專用中斷向量和上下文保存,這將是一項極其困難的任務。這會使通訊、人機介面甚至製程控制等後台任務變得複雜。

我們也看到,ISR 和主程式之間需要仔細協調。這有兩種形式,包括變數的仔細原子傳輸以及時序分析。雖然存在多種安全傳輸資料的方法,但我們將討論限制在基本的標誌和郵箱同步上。至於時間安排,我們探索了 ISR 處於活動狀態時的簡單 I/O 引腳設定。我們簡要探討了使用直接連接埠存取(而不是 Arduino 抽象)來縮短 ISR 程式碼的方法。

雖然本工程簡介確實為強大的高速正交編碼器提供了完整的解決方案,但它只是觸及了表面。欲獲取更多資料信息,請回答本說明末尾的問題。我們鼓勵您下載該檔案並試驗您自己的正交編碼器。另外請繼續關注,因為我會在不久的將來發佈相關的 PID 控製文章。

請訂閱連結查看相關的 Arduino 教育內容

問題

  1. 定義術語阻隔以及為什麼它對於讀取正交編碼器等快速訊號如此有害。

  2. 與術語「裸機」相比,「Arduino 抽取」是什麼意思?

  3. 為什麼附加的程式碼「不可移植」?

  4. 定義本文中應用的原子術語。

  5. 描述標誌和郵箱同步的操作。

  6. 描述與微控制器相關的狀態概念。

  7. 描述前景和背景的概念。

  8. 確定最小化 ISR 時脈週期數的技術。

  9. ISR 如何能讓主循環程式碼從屬?

批判性思考問題

  1. 描述保持 ISR 簡短的重要性。

  2. 描述最小化 ISR 週期數的技術。

  3. 定義與微控制器相關的直接記憶體存取(DMA)。將 DMA 與傳統 ISR 進行比較。 DMA 可以用於該正交編碼器應用嗎?

  4. 越來越多的現代微控制器開始配備整合式 FPGA 結構進行銷售。如何在正交編碼器應用中使用 VHDL 和 Verilog 技術?

  5. 8051 微控制器是一種古老但廣泛使用的架構。研究並描述獨特的 8051 基於硬體的前後狀態切換機制。

  6. Arduino Nano Every 是一個 8bit 微控制器。當使用 32bit 微控制器時,您對原子性的理解如何改變?

  7. 有時,您會遇到時間關鍵型程式碼段,該程式碼段會受到停用然後啟用全域(硬體)中斷機制的程式碼的限制。這是個好主意嗎?在一個為即時操作而設計的系統中會發生什麼?

  8. 即時作業系統允許先進的微控制器同時處理多項任務。描述術語「上下文(context)」在作業系統中的應用。

  9. 描述即時作業系統中訊號量和互斥量的用途。