Arduino Opta PLC 梯形圖邏輯儲存結構

所有可編程邏輯控制器 (PLC) 的起點都是對記憶體結構的理解。這一點至關重要,因為我們編寫的程式是在記憶體單元上進行操作的。即使是最簡單的程序,例如直接將螺絲端子數位輸入的值傳輸到螺絲端子數位輸出,也是記憶體到記憶體的操作。它至少涉及兩個記憶體單元和兩個用於操作這些單元的功能,包括讀取和寫入操作。在梯形圖邏輯 (LL) 中,我們將這些功能分別表示為常開觸點和線圈。請在閱讀本文時牢記這一點。雖然我們看到了觸點和線圈的圖像,但我們並非直接操作硬件,而是在執行記憶體到記憶體的操作。

Arduino 概述

本文將探討全域記憶體和局部記憶體之間的關係。為了精簡內容,我們將討論範圍限定在布林值。

本文假設讀者俱備 Arduino 微控制器(C 語言)程式設計經驗。為了便於理解,我們將首先簡要回顧一些重要的 Arduino 概念,包括循環以及涉及局部變數和全域變數的記憶體空間。掌握這些概念後,您將能夠輕鬆地從 Arduino 過渡到 LL 語言。

您是否準備好利用 Arduino Opta PLC 和 LL 等符合 IEC 61131-3 標準的程式語言來發揮其強大功能?讓我們告別傳統的 Arduino 整合開發環境 (IDE),轉而使用 Arduino PLC IDE

技術提示:本文提供了開始使用梯形圖邏輯對 Arduino Opta 進行程式設計所需的必要背景資訊。由於篇幅較長且內容較為教科書式,敬請見諒。然而,這是使用梯形圖邏輯對 Opta 進行程式設計所必需的背景知識。我衷心希望您能將此頁面加入書籤,並在開始對 Opta PLC 進行程式設計時參考。您會發現,這些概念雖然簡單,但並非顯而易見,而且由於許多 PLC 使用的是較舊的固定記憶體模型,與 Opta 靈活的基於標籤的方法相比,這些概念通常沒有詳細的文件說明。

Arduino 循環

每個 Arduino 程式都包含一個 setup() 函數和一個 loop() 函數。請記住,setup() 函數中的語句在程式啟動時執行一次。loop() 函數中的語句按順序執行,但僅在需要重新開始時執行-loop() 函數的最後一個操作是跳到循環頂部並重新開始(此處解釋過於簡化,另請參閱關鍵字 static)。只要微控制器不遇到阻隔程式碼,此循環每秒就會執行數千次。

技術提示:阻隔程式碼是指導致程式停止運作的一條或多條語句。例如,tone()、delay() 或諸如 while(WaitForEvent) 之類的控制語句。在阻隔程式碼執行期間,微控制器會停止運作。除非實現了中斷等其他機制,否則微控制器將無法回應事件。請注意,PLC 程式碼經過專門設計,可防止出現阻隔情況,從而使 PLC 能夠回應現實世界的事件。對於從 Arduino 過渡到 PLC 的程式設計師來說,阻隔的概念可能是一個困難。

Arduino 輸入/輸出

Arduino 的輸入/輸出 (I/O) 操作使用 digitalRead()digitalWrite() 函數執行。這是一個記憶體到記憶體的資料傳輸範例。 digitalRead() 函數讀取輸入引腳的狀態並將資料傳輸到記憶體位置。 digitalWrite() 函數將布林記憶體位置的值傳送到物理輸出引腳。

Arduino 記憶體作用域

C 語言程式設計中最具挑戰性的方面之一是記憶體作用域的概念。這一點在學習者初次接觸函數時尤其明顯。一個經典的例子是,不恰當地使用同名變量,例如在全域變數、一個或多個函數以及 loop() 循環中使用。例如,程式設計師可能在所有三個位置都使用索引變數 “i”,如 for(int i = 0; i < 10; i++)。編譯器秉持著「你知道自己在做什麼」的理念,會很樂意依照既定規則編譯程式碼。但這對於不了解記憶體作用域規則的初學者來說並不總是有幫助的。產生的程式碼可能難以排查故障。

雖然這些規則不能直接應用於 LL 語言的 PLC,但重述一下這些概念還是很有幫助的:

  • 全域變數:全域變數定義在任何函數之外。在 Arduino 程式中,全域變數通常定義在 setup() 函數之上且外部。全域變數可以被同一 Arduino 程式標籤頁內的所有語句存取(此處解釋較為簡化,另請參閱關鍵字 extern)。

  • 局部變數:局部變數定義在函數內部。它只能被該函數內部的語句存取。局部變數的值可以透過引用或值傳遞給另一個函數。但是,該函數不能直接存取該變數。

Arduino C 語言與梯形圖邏輯的共通之處

使用梯形圖邏輯對 Arduino 和 PLC 進行程式設計有許多相似之處。循環、I/O 操作和記憶體結構等概念對於這兩種裝置的程式設計都至關重要。接下來,我們將探討這兩種語言之間的相似之處。需要注意的是,這種梯形圖邏輯的討論並非普遍適用。每個 PLC 製造商都有其獨特的記憶體處理方式。Arduino 的實作方式是一種複雜的、基於標籤的現代實作。

梯形圖邏輯循環

程式掃描是 PLC 的基本程式結構。從程式設計理念上講,可以將其視為一個循環,使 PLC 能夠及時回應現實世界的事件。圖 1 所示的時鐘提供了一個很好的類比。在 12 點鐘位置,時鐘開始執行後台維護任務。在 3 點鐘位置,時鐘讀取螺絲端子的輸入並將其傳輸到記憶體。描述記憶體到記憶體操作的程式在 6 點鐘位置執行。最後,在 9 點鐘方向,布林記憶體內容被傳送到 PLC 的螺絲端子輸出。

這個循環每秒執行約 1000 次。避免阻塞代碼至關重要,因為任何阻塞代碼都會阻止 PLC 響應現實世界的事件。PLC 的定時器很好地說明了這一點。它們被設計成非阻塞的。例如,考慮一個 5s 延時定時器 (TON)。在程式掃描數千次後,該定時器才會被判定為真。定時器始終不會中斷程式掃描。

技術提示:PLC 的看門狗定時器與程式掃描密切相關。作為正常的 12 點鐘例行維護程序的一部分,PLC 會「撫摸」硬體看門狗。如果循環時間過長,看門狗會變得焦躁不安,被喚醒,並將 PLC 置於硬故障(停止運行)狀態。與普遍認知相反,可以在梯形圖邏輯中執行類似於 while() 或 for() 的耗時循環;例如,遍歷數組。這些 6 點鐘方向的循環可能會導致整個程式掃描速度減慢,從而喚醒看門狗。

圖 1:此時鐘類比代表了 PLC 程式掃描期間執行的操作。

梯形圖邏輯 I/O

本月初,我曾與人討論過用於 PLC 程式設計的類似 Arduino 的程式語言。我第一次聽到 「PLC 風格」這個詞,它指的是在編程循環中處理 I/O。這個概念與圖 1 所示的時鐘密切相關。

程式碼結構在循環開始時捕獲所有輸入。然後執行各種記憶體到記憶體的操作,前提是影子暫存器在掃描期間保持穩定。在循環結束時(用我們的類比來說,大約是 9 點鐘位置),輸出被更新。這與在程式中處理 I/O 的傳統做法截然不同。

識別這種「PLC 風格」的另一種方法是使用「影子暫存器」的概念。在 3 點鐘位置,螺絲端子輸入傳送到影子寄存器,然後由 6 點鐘位置的代碼使用這些寄存器。稍後在 9 點鐘方向,輸出影子暫存器被傳送到螺絲端子輸出。

技術提示: 影子暫存器標識一個儲存微控制器輸入映像的記憶體位置。它也可以解釋為儲存即將輸出的記憶體位置。在這兩種情況下,您的程式碼操作的是影子暫存器,而不是直接讀寫硬體。這與圖 1 直接相關,圖 1 中輸入訊號被傳送到 3 點鐘位置的影子暫存器。然後,您的 PLC 程式與影子寄存器交互,而不是與螺絲端子上的實際資訊交互。實際上,在下一次程式掃描之前,PLC 對任何輸入變更一無所知。

梯形圖邏輯記憶體示波器

我們終於來到了梯形圖邏輯討論的核心部分。雖然花了一些時間才走到這一步,但首先必須描述循環結構以及用於處理螺絲端子 I/O 的方法。掌握了這些知識後,我們可以探討如何讀取輸入、處理,然後將結果傳遞給輸出。本文的其餘部分將展示 Opta LL 記憶體示波器與傳統 C 程式的密切聯繫。

基於標籤的結構

Arduino Opta 使用基於標籤的記憶體。在許多方面,這只是您在傳統 Arduino 中已經做的事情的延續。您可以像往常一樣自由命名變數並定義它們的類型。定義一個布林值甚至一個陣列來定義 float 類型是很容易的。

技術提示:基於標籤的 PLC 記憶體提供的自由度並不普遍。大量 PLC 具有固定的儲存位置,其中有 N 個布林類型或 M 個可用浮點數。程式設計師可以自由使用編號的記憶體位置或別名。在任何時候,程式設計師都痛苦地意識到記憶體位置。這個較舊的、也是唯一一個變數位置導致了一種平面記憶體結構,其中每個記憶體位置都是全域的。與基於標籤的結構相比,程式設計師不關心變數的位置,而是專注於名稱和類型。

標籤名稱

就像傳統的 Arduino 一樣,在一定範圍內,您可以自由選擇變數名稱。作為一名聰明的程式設計師,您會利用這一點,為程式碼添加描述性標籤。這將極大地增強程式碼的自文件性。例如,標籤名 gxStartPB 可以清楚地標識啟動按鈕的影子暫存器。在這個例子中,我們使用了 匈牙利命名法(Hungarian notation)。前置 g 表示變數是全域變數。前綴 x 表示布林值。

有些人可能會質疑為什麼要如此在意變數名。他們的想法沒錯。不過,我認為我們現在又陷入了一種誤解,那就是我們試圖像專家一樣編寫程式碼,而實際上我們應該專注於學習。

舉個例子,想想你是如何透過拼讀法學習閱讀的。要唸出單字 “cat”,先指向你的肩膀,發出 “c”(或“k”)的音,然後指向你的手肘,強調 “a” 的音,最後指向你的手,發出結尾的 “t” 的音。任何有經驗的讀者都不需要執行這些複雜的步驟。同樣,也有充分的理由認為,經驗豐富的程式設計師不需要匈牙利命名法。

然而,我的經驗是,添加匈牙利命名法前置可以迫使學習者思考變數的類型和作用域。這有助於避免一些難以發現的錯誤,例如變數名稱重複或變數位置錯誤。此外,在你日後的程式設計學習中,對變數名稱的精確關注將使你更容易建立基於 PLC 的函數。

要了解更多關於匈牙利命名法和 PLC 程式設計指南的信息,我建議你閱讀 PLCopen 組織編寫的這份編碼指南文件

標籤作用域

與 C 語言程式設計類似,Opta 梯形圖邏輯實作也具有局部記憶體和全域記憶體。全域標籤可供 Opta 內的所有程式組織單元 (POU) 使用。局部變數的作用域僅限於定義它的 POU。圖 2 展示了這種關係,局部變數封裝在各自的 POU 中。

技術提示:POU 是 PLC 中的一段程式碼。在 IEC 61131-3 環境中,PLC 程式由多個 POU 組成。一個密切相關且經常被混淆的概念是 PLC 功能塊 (FB) 或使用者自訂功能塊 (UDFB)。所有功能都是 POU,因為它們有助於組織複雜的程序。但是,並非所有 POU 都是功能。例如,假設我們寫了一個名為 prgCtrl 的 10 階梯形圖程式。這個 POU 是一個程序,但不是一個功能。為了更好地理解這種細微差別,我們採用函數的傳統定義:函數是指在程式內部被呼叫的函數。根據定義,我們的 POU prgCtrl 不會被任何程式明確呼叫——它不是一個函數。但是,它可以呼叫諸如絕對值之類的函數。

當有多個 POU 時,情況就變得複雜了。由於 POU 不是函數,我們需要一種方法在 POU 之間傳遞訊息。例如,考慮圖 2 所示的結構。這是一個包含 prgInMap、prgCtrl 和 prgOutMap 的簡單三程式解決方案。 prgControl POU 包含 PLC 的邏輯,而映射 POU 用於將影子暫存器傳輸到 PLC 的螺絲端子或從螺絲端子傳輸資料。由於這些不是函數,因此傳輸資料的唯一方法是使用全域變數。為了更好地說明,圖 3 展示了 Arduino PLC IDE 中 prgCtrl 的局部變數視圖。

技術提示:詢問任何一位電腦程式設計師,他們都會告訴你避免使用全域變數。他們甚至可能會講述他們或他們認識的人花費數小時甚至數天時間排查一個隱藏很深的 bug 的故事。這是個明智的建議。我們應該盡可能減少全域變數的數量,只在絕對必要時才使用它們。一般來說,它們應該只用於在實體單元 (POU) 之間傳遞 I/O 指令。隨著我們學習編寫函數塊 (FB) 和用戶自訂函數塊 (UDFB) 的程序,這一點會變得越來越容易。

圖 2:全域變數對所有 POU 可用,而局部變數的作用域僅限於定義它們的 POU。

圖 3:此圖顯示了與 prgCtrl 關聯的局部變數。

結論

本文到此結束。我們透過比較 Arduino Opta 和眾所周知的 Arduino C 程式設計結構,定義了 Arduino Opta 的記憶體結構。我們了解到 Opta 使用基於標籤的記憶體分配方式,每個標籤都使用資料類型和描述性名稱進行實例化。PLC 使用循環運行,在後台執行維護操作,並將螺絲端子 I/O 資料傳輸到影子寄存器。最後,我們了解到變數的作用域可以是局部的,也可以是全域的。此外,我們也建議學習者使用匈牙利命名法來幫助組織程序。

:books: 繼續探索更多工業控制系統資訊

如果您覺得本次討論對您有幫助,您可能還想了解以下內容:

:world_map: DigiKey 導航

:japanese_symbol_for_beginner: 相關基礎文章

  • 我再次鼓勵大家閱讀 PLCOpen 組織提供的 PLC 程式設計指南。這與閱讀 GNU 編碼標準等文件同樣重要。這些文件將幫助您了解程式的組裝方式以及使您的程式碼更具可讀性和可維護性的最佳實踐。

  • 也許將來我們可以繼續使用梯形邏輯來發展程式。在撰寫本文時,Opta PLC 初學者對 DigiKey 技術論壇上的一篇文章感興趣。

  • 本文提供了一種有用的技術,可讓您使用狀態和紅色、綠色和藍色 LED 指示燈來查看 Opta I/O 的狀態。這是重要的故障排除幫助,可幫助您區分硬體和軟體問題。