共通控制程式架構
曹永誠
刊登於電機月刊第 99期 (1999年3月)
本文榮獲第9屆電機月刊金筆獎
前 言
相對於PLC(可程式化邏輯控制器)與Ladder(階梯式語言)﹐PC-Base控制器具有軟體發展彈性高﹑硬體可選擇性多﹑人機介面方便漂亮﹑高附加價值等優點。反過來看﹐PC-Base控制器僅僅是一般用途的汎用型電腦﹐並無控制專用發展環境。因此當我們拿它來開發機臺控制器時﹐所能利用的設計工具就只有汎用型電腦語言與作業系統﹐例如C/C++﹑BASIC或 PASCAL等汎用型語言﹐與DOS﹑MS-Windows﹑UNIX﹑Linux等作業系統。然而控制程式相較於一般程式﹐無論在設計架構﹑設計原則﹑考慮層次上都有相當大的差異﹐但是坊間卻少有關於PC-Base控制器控制程式設計的資料﹐更不用說整機整系統的架構理論。導致每個控制程式設計師在面對新機種的開發上﹐都得一次次的「重新發明輪子」﹐隨著機臺規格﹑動作程序﹑操作流程與「心情」來「從頭開始設計」。如此一來在發展時程上恐怕無法精簡﹑軟體品質很難掌控﹑而且資源無法共享﹑經驗較難累積。因此筆者將提出一套自己所開發且應用於某些領域機種的共通控制程式架構﹐並說明發展該架構的一些心得﹐希望能藉此拋磚引玉﹐並供各位先進參考。共通控制程式架構
PC是一般用途的汎用型電腦﹐並無控制專用硬體﹑作業系統或發展工具﹐因此我們只能拿汎用型作業系統(例如:MS-Windows 95/98/NT)與發展工具(例如:VB﹑VC++﹑BCB﹑Delphi﹑LabWindows/CVI﹑LabView)來發展控制程式﹐而偏偏機臺的控制又是如此的複雜與多變﹐導致控制程式的設計上困難度很高﹐而且各種類型的機臺無論在動作程序上或是操作流程上都有相當大的差異﹐因此不同類型的機臺控制程式幾乎得個案處理(Case by case)。也就是說不可能發展出一組「萬用控制程式」﹐只需要微調部份參數就可以適用於各產業各類型的機臺控制器。因為就算是MIS最單純的AP﹐都很難有共通的萬用程式﹐或是不需要額外編修程式碼直接產生可用程式的產生器﹐更何況機臺控制程式比起MIS來說更是千奇百怪﹑差異甚多。因此若想要設計一套「萬用控制程式」或是「控制程式產生器」將是件很困難的事情。那是不是就代表著控制程式完全不可能有軟體重用性了呢?其實也不是這樣子﹐第一﹑如果每一種機臺都得重頭設計一套全新的控制程式的話﹐那麼投資報酬率恐怕不會太好;而且每一次都得「重新發明輪子」似的「從頭開始設計」﹐發展時程恐怕也無法精簡。第二﹑如果每一種機臺的控制架構都截然不同﹐那麼將會導致軟體品質很難掌控﹐常會因設計群的不同而異;Debug﹑維護與未來擴充困難度增高;而且資源無法共享﹑經驗較難累積。所以折衷的辦法是將機臺的類型予以歸類﹐找出同類機臺共通的特點與運轉流程。然後依照這些共通的特點設計出共通控制架構與可重用元件﹐讓控制程式能透過適當的架構分層化與模組化來簡化系統﹐並讓層次間的介面標準化以提高重用性與軟體品質﹐並讓分工更為清楚。如此或許可以有效的提高軟體品質﹑簡化系統發展﹑降低設計時間與成本。在筆者的工作上﹐所開發的機種大多數是屬於多軸(大多數超過六軸)﹑單軸或雙軸的動作都是點對點運動(Point to Point)或定速動作﹑沒有關節式連接或圓弧運動﹑能自動上下料的全自動機臺。於是針對這類機臺的控制程式開發上﹐發展出一套共通控制程式架構與可重用元件﹐以下將介紹這套共通控制程式的系統架構﹑設計理念﹑設計原則與注意事項﹐以供各位先進參考。
筆者所發展的共通控制程式架構區分為四層(如圖一所示)﹐分別是Device層﹑Function層﹑Command層與MMI層。每一層都是由數支函數或是執行緒所組成的﹐層與層之間定義標準介面(Interface)﹐可以互相交換命令與資料。上層程式可以透過標準介面下達命令給下層程式執行;而下層程式的執行結果或是執行期間的狀態值﹐也可以透過標準介面上傳回去處理﹑儲存或是顯示在人機介面上。如此分層負責﹐層層封包的架構﹐可以設計出高度模組化的控制程式﹐以降低程式出錯的風險﹑增加修改/擴充的彈性與減少因增修而意外出錯的機會。[1]
Device層功能是硬體層的抽象化。實際上控制外界動力源或讀取狀態/資料的是硬體層﹐但是因為硬體控制卡的種類過多﹐同一種硬體控制卡有太多的廠牌與型號可供選配﹐但是使用的方式都不大相同(例如不同廠牌的運動控制卡Motion Card所提供的功能或許是大同小異﹐但是使用介面與方法卻沒有統一的標準)。因此如果讓程式直接控制硬體層的話﹐當硬體層因故更換時就會造成程式修改上極大的困擾。況且這種作法也會導致程式架構不夠模組化﹐造成維護與可重用性的困難。所以最好的方法是訂定一個標準介面(Driver Interface)﹐定義一組標準的命令與回應做為與硬體層溝通的標準介面﹐然後為硬體層的每一種介面卡設計一組Device Driver﹐並讓所有同類型硬體有著相同的標準介面。如此上層程式(Device層以上)就不用再考慮所用的硬體廠牌與型號﹐只要遵循標準介面就可以控制硬體輸出/輸入。而且當硬體層因故更換時﹐上層程式幾乎不需要修改﹐僅需抽換Device Driver即可﹐這樣可以讓程式更模組化﹐方便除錯與增修。
Function層功能是將動作流程抽象化﹐是實際上負責運作機臺控制動作流程的模組。Function層擁有一組標準介面(Func Interface)提供給上層程式使用﹐讓Command層的排程派工程式或MMI層的人機介面程式﹐能透過該介面下達命令給Function Driver。當Function Driver接受命令之後﹐會根據該模組所設計的動作順序﹐透過Driver Interface下達命令給Device Driver﹐再由Device Driver真正輸出硬體層控制動力源運轉;運轉結果先由Device Driver讀回﹐同樣透過Driver Interface回應給原Function Driver「完成」訊號﹐Function Driver收到該「完成」訊號後﹐會依該模組所設定的動作程序繼續下一個動作﹐直到該模組的動作程序結束或卡料故障為止。在整個工作期間﹐Function Driver運轉的狀態會依照需求﹐定時或定點透過Func Interface回報給上層程式﹐供排程﹑顯示或是記錄使用。
Command層功能是做排程派工使用﹐負責協調機臺的各個獨立模組共同運轉。其模組分為二種模式(Mode):Manual Mode﹑Autorun Mode。三種元件:Cmd Process(x) ﹑Autorun Process與Return Process。其中的Cmd Process(x)與Return Process主要是提供手動模式(Manual mode)中MMI層與Function層之間的標準介面﹐讓MMI程式(例如:人機介面程式)可以透過這種機制直接下達命令給Func(x)程式﹐做單步或單功能運轉。其作用在於讓維護工程師在試機﹑維修時能針對需要測試的部份單獨運轉﹐並且透過FuncQueue監看各模組的運轉狀態﹐方便機臺的檢修。而Autorun Process模組主要是提供自動模式(Autorun mode)中各模組的排程派工功能﹐Autorun Process模組會根據事先設計好的排程演算法﹐透過Func Interface自動下達適當的運轉命令給Func(x)程式運轉機臺﹐並且透過FuncQueue讀回運轉狀態﹐再根據排程演算法進行下一步動作程序。
MMI層主要擔任控制系統的對外介面﹐包括人機介面操作流程部分。主要工作項目:負責機臺/物料參數的編修﹑生產資料的記錄與顯示﹑手動/自動模式下將操作人員的命令下達給Command層程式運轉機臺﹑顯示運轉狀態以及故障卡料時的排除程序。
圖一. 系統架構圖
可重用元件
以上述的控制架構﹐各類機臺將擁有類似的控制架構與程式運作流程。因此很多程式碼就可以因此模組化成可重用元件﹐讓各個小組人員能共享設計成果﹑分享可重用元件﹑減少新機種的發展時間與成本﹐以下是建議採用的可重用元件:- Device層的Device Driver要模組化成可重用元件﹐並依需要封裝成COM﹑Active X﹑Device Driver或函式庫等型式。相同類型硬體的Driver有相同的標準介面(Driver Interface)﹐讓上層程式能快速方便的使用及抽換新Driver。
- Function層設計則依共通需求訂定「動作流程語言語法」﹐並提供「解譯器核心(Interpreter Kernel)」﹐用來解譯與執行用「動作流程語言」寫好的巨集程式。這種設計具有較佳的模組化與可重用性﹐設計與修改動作流程會比較快速與方便。
- MMI層比較少具可重用性的模組﹐但是針對常用的MMI元件可以設計成函式庫﹐讓機臺的MMI發展能更加快速與方便。
- Command層沒有可重用元件﹐因為排程派工法則跟機構設計﹑物料物性﹑產業類別與使用者操作習慣有很大的關連。但是排程演算法設計之優劣﹐對機臺的運轉效率與生產產能又有著極大的影響。因此只能依照這些要求個案處理(Case by Case)與最佳化。然而為了要讓控制程式在程式維護﹑程式碼交接與團隊合作(Team Work)上能更加順利﹐最好能建立共通設計規範﹑流程與架構。
Real-Time與作業系統的選用
在深入探討共通控制架構各層元件之前﹐我們先討論一下關於Real-Time的定義與作業系統的選用問題。簡單來說Real-Time區分為Hard Real-Time與Soft Real-Time二種。Hard Real-Time對於反應時間要求嚴格﹐不容許反應時間有超過要求時限的可能性﹐因為如果反應時間超過要求時限﹐機臺就可能會發生不可回復的損壞(例如:加工品質下降﹑工件毀損﹑製程良率下降);而Soft Real-Time對於反應時間不得超過要求時限的需求也很高﹐但是容許很低機率超過時限的情況發生﹐而且當反應時間超過要求時限時﹐機臺並不會發生不可回復的損壞。通常機臺在控制上並不會全部都需要Hard Real-Time處理程序﹐大部份的機臺只有極少數的訊號是真正需要Hard Real-Time處理的﹐其它的訊號就只需要Soft Real-Time處理就足夠。況且Hard Real-Time處理通常需要消耗很高的CPU資源﹐而且需要即時作業系統(RTOS﹐Real-Time OS)或是使用中斷處理程式碼(ISR)來特別處理。因此為求最佳的C/P值與Performance﹐這二種處理程序最好能區分開來﹐分別由不同的執行緒﹑子系統或電腦來處理。對於作業系統(OS)的選用﹐筆者是採用MS-Windows 95/98/NT平臺。因為支援MS-Windows 95/98/NT的軟硬體資源最多﹐而且發展工具與3rd Party函式庫最齊全;Win32 API提供很多開發所需功能﹐而且不像DOS有640K記憶體的限制。但是MS-Windows 95/98/NT乃是針對MIS所設計的﹐對於機臺控制而言﹐MS-Windows 95/98/NT有著Real-Time的問題。對MS-Windows 95/98而言﹐OS所支援的最高精度為55ms;對MS-Windows NT 4.0而言則是10ms﹐而且根據測試經驗這些數據都是常態值而不是保證值。換言之MS-Windows 95/98/NT所支援的是Soft Real-Time處理﹐而並非控制上所需要的Hard Real-Time處理。因此對於控制系統中需要Hard Real-Time處理的部份﹐就必須採用以下二種方式來解決。
- 把需要Hard Real-Time處理的程序(例如:Motion﹑高速A/D)放到硬體子系統中(Subsystem)﹐用另一顆CPU/DSP來處理。例如使用內含CPU/DSP的Motion控制卡(例如:Melec﹑PMAC﹑Parker﹑nuLogic)﹐然後透過I/O Address下達命令給Motion 卡﹐讓Motion卡上的CPU/DSP自動計算並驅動馬達。又例如將Motion控制程序放到PLC子控制器處理﹐然後用通訊方法下達命令給PLC。或是例如高速資料擷取系統﹐在機臺某些狀態下需要透過A/D卡準確的對某個物理量高速取樣﹐就可以安排另一塊CPU卡專門負責A/D取樣﹐並在取樣結束後透過RS422/Ethernet將資料傳回。這些硬體子系統都會有一支Device Driver程式與之對應﹐以提供標準介面供上層程式使用。當上層程式欲下達命令要求Hard Real-Time硬體子系統處理時﹐該Device Driver會透過I/O或通訊等方式將命令與資料送入硬體子系統﹐並動態傳回相關資料。對於上層程式而言﹐完全不需要知道內部處理流程﹐而是把硬體子系統當成「黑盒子」﹐以封裝(模組化)硬體Real-Time處理模組。
- 使用MS-Windows NT 3rd Party Real-Time擴充模組(Real-Time Extension)﹐讓NT內增加一組軟體子系統來處理Hard Real-Time程序。目前市售的Real-Time擴充模組有很多﹐例如:VenturCom Inc.的RTX4.1[2]﹑Imagination Systems Inc.的Hyperkernel[3]與Radisys Corp.的Intime[4]。以VenturCom的RTX 4.1為例﹐RTX透過修改MS-Windows NT的HAL層與RTSS(Real-Time Subsystem)﹐以提供100us的即時精度[5]。因此我們可以把需要Hard Real-Time處理的程序寫成一支RTX執行檔(*.rtss﹐RTX執行檔副檔名是rtss)﹐並且設計一支Device Driver程式與之對應﹐以提供標準介面供上層程式使用。當上層程式欲下達命令要求Hard Real-Time軟體子系統處理時﹐Device Driver會透過IPC(Inter-Process Communication﹐包括:Shared memory﹑Semaphores﹑Mutex objects)將命令與資料送入RTX執行檔中﹐並傳回相關資料。對於上層程式而言﹐完全不需要知道內部處理流程﹐而是把軟體子系統當成「黑盒子」﹐以封裝(模組化)軟體Real-Time處理模組。
Device Driver
Device層位於共通控制程式架構(如圖一所示)的最底層﹐主要功能是硬體層的抽象化﹐是真正接觸硬體控制輸出/輸入的程式。每一種硬體控制卡(器)都有對應的Device Driver程式﹐而每一組Device Driver也都有標準介面(Driver Interface)提供外界使用﹐並隱藏內部處理細節。不管是市售控制卡(例如:I/O卡﹑A/D卡﹑Motion卡)﹑硬體子系統(使用CPU/DSP來處理Hard Real-Time程序)﹑還是軟體子系統(使用Real-Time擴充模組來處理Hard Real-Time程序)﹐只要是相同功能的硬體都有著相同的介面。例如讓內含DSP的PMAC Motion卡的Device Driver﹐與使用沒有CPU的Motion卡卻用RTX處理Hard Real-Time程序的Device Driver標準介面相同﹐可以讓機臺的彈性加大(例如:更換硬體控制卡快速方便)﹑分工清楚(寫Motion卡Driver的工程師與寫機臺動作流程的工程師介面清楚﹑權責分明)。在開始製作Device Driver之前﹐最好對Device Driver的標準介面設計能多加考量。因為一旦介面標準定義清楚之後﹐上層程式的設計工作就可以同步進行。因此如果介面定義不佳或是不夠﹐就會讓發展後期面臨瓶頸﹐因為不良的標準介面會嚴重影響Driver的功能與效率。如果當初定義的規格造成後期的發展受限時﹐那真的會進退二難-改介面也不是﹐不改也不是。因為如果不改介面的話﹐功能又達不到;但是如果更動的話﹐則相關的程式全部得一併更改﹐不但耗時費工﹐而且容易產生意外的Bug。因此與其花很長的時間﹑很多的技巧去設計很棒的Device Driver程式碼﹐卻因進度太趕﹑時間不夠隨便定義介面標準﹐還不如多花點時間來定義較佳的標準介面。但是什麼是較佳的介面標準呢?我想這很難有標準答案﹐介面標準制訂的優劣可以說非常的Case by Case﹐但是就設計原則來看﹐最好能具備下列三點:簡單﹑效率﹑彈性高。
對於Device Driver的標準介面﹐我們以I/O卡為例來說明。在標準介面的程式設計上﹐必須考量到多執行緒(Multi-Thread)下的競速狀態(Race Conditions)與函式重入問題﹐因此建議採用函式方式製作﹐而且函式內的變數都要使用Local變數﹐存取值部份使用Win32 API的Critical Sections(臨界區域)做多執行緒的同步控制。I/O卡的標準介面函式如下:DIO_CreateDriver(…)﹑DIO_DeleteDriver(…)﹑DIO_Set(DO Address, Value)﹑DIO_Get(DI Address, *Value)﹑DIO_Wait(DI Address, Value, Object handle)﹑DIO_KillWait(Object handle)。DIO_CreateDriver(…) 函式是啟動並初始化Device Driver;DIO_DeleteDriver(…) 函式是結束並釋放資源(如果Device Driver程式設計是採用OOPL例如C++﹐則這二個函式可寫成建構子與解構子);DIO_Set()函式為DO點輸出;DIO_Get()函式則是DI點值的讀取;而DIO_Wait()函式與DIO_KillWait()函式是為了要讓Function Driver能更有效率所設計的。
以機臺常用的動力源來說﹐大多數的動力源都是類似ON/OFF元件﹐例如:氣壓缸﹑吸盤﹑感應馬達﹑甚至於是「點對點運動」的步進馬達或伺服馬達。就氣壓缸來說﹐我們常會在活塞的二端各裝置一顆磁簧開關以感測活塞的位置﹐當氣壓缸的活塞推出(或縮回)到達這二點時﹐該點的磁簧開關就會變成True(ON或是”1”)的狀態﹐因此當我們下令要求氣壓缸推出(或縮回)之後﹐接下來上層程式就得苦苦等待﹐並常常去讀取感測器狀態值來得知動作是否完成。對於「點對點運動」的步進馬達或伺服馬達來說﹐狀況也是十分類似﹐當我們把位置﹑速度﹑加速度等資料下達給運動控制卡(有CPU/DSP或是運動控制專用晶片)要求做「點對點運動」之後﹐接下來上層程式就得苦苦等待﹐並常常去讀取運動控制卡狀態值來得知動作是否完成。於是負責動作流程的Function Driver﹐常做的工作就會是「下達一組DO點來驅動某個動力源(例如讓氣壓缸推出)﹐或是下達「點對點運動」命令給某顆伺服馬達﹐然後開始不停的讀取某個感測器(例如磁簧開關)的值﹐或是某塊運動控制卡的狀態值﹐並判斷讀取值是否變成True以得知動作是否完成﹐然後繼續下一個動作」。這種下達命令﹑讀取﹑等待﹑讀取﹑等待……的Busy Waiting動作﹐會讓Function Driver平白無故耗用過多CPU資源。解決這個問題的較佳方法是在Function Driver中Create Manual-Reset Event Object(Win32 API同步控制)﹐然後透過DIO_Wait()函式將該Event的handle傳給I/O Device Driver﹐然後使用WaitForMultipleObjects()(Win32 API)來進入有條件式的Sleep(會釋放CPU執行權)。而Driver在收到這些Event handle後﹐會把它放入內部Event Table﹐並且在每次讀入硬體卡DI值之後﹐就到Event Table去比對是否有條件滿足的Event﹐如果有就激發該Event(Win32 API的SetEvent())﹐再將該Event從Event Table中去除。而這激發狀態的Event就會喚醒(Wake up)Function Driver以進行下一步動作程序。這樣的設計架構可以降低CPU資源的耗用﹐並加快反應時間。
至於DIO_KillWait()函式則是將Event從Event Table中去除。因為Function Driver被喚醒的條件除了動作完成的正常Event之外﹐Timeout(例如:卡料﹑感測器壞掉)或是Alarm Event(例如:緊急停止EM﹑Stop﹑安全門被打開﹑斷水)都會喚醒Function Driver。因此當Function Driver被喚醒之後﹐首要動作就是判斷是被那一種狀況所喚醒。如果是正常Event被激發的話﹐由於Event handle是自動從Event Table中去除﹐因此可以直接進行下一步動作程序;但是如果是Timeout或是Alarm Event﹐則Function Drive就得自行Call DIO_KillWait()函式來去除Event Table中的Event handle﹐再進行異常狀況排除的動作程序。
I/O Device Driver的程式設計部份是用獨立的執行緒定時去檢查是否有需要輸出﹐如果有則輸出Output Port。接著讀取I/O卡DI值﹐將之存入陣列變數(Array)﹐再查詢Event Table比對是否有條件滿足的Event﹐如果有就激發該Event﹐再將Event從Event Table中去除﹐最後讓執行緒Sleep()固定時間。
虛擬裝置 Visual Device Driver
Visual Device類似於一般的Device﹐是位於共通控制程式架構的最底層﹐主要功能是模擬硬體I/O卡做各層各模組間的資料交換或是交握訊號。我們所設計的Visual Device裝置主要有二個元件:VIO與Mutex。VIO的動作與I/O卡非常類似﹐差別在於VIO沒有真正輸出硬體而已。對於Function Driver的動作流程來說﹐有時運作到某一個狀況下﹐必須等待另一個模組也到達某一個狀況才能繼續動作﹐這時候就可以使用VIO來做動作上的交握訊號。例如A模組將X物料搬到B模組的上空等待B模組﹐於是A模組就可以開始進入Sleep並等待某個VIO點變成1(例如VIO(7) == 1);當B模組到達後就將VIO(7)設定為1﹐就可以喚醒A模組﹐當A模組被喚醒首先檢查是Alarm﹑Timeout還是B模組Ready﹐如果是前二者就開始進入異常狀況處理;如果是B模組Ready就開始繼續下一步動作程序。Mutex則提供模組間共用資源或是機構間共用空間的互斥管理。對於Function Driver的動作流程來說﹐有時候二個模組會共用到相同的資源或是空間﹐如果不加以管理﹐當二個模組同時使用到該資源或是空間時﹐將會有「撞機」或是「誤動作」等問題發生。這時候就可以使用Mutex來做資源與空間上的交握訊號。例如Z位置是A模組與B模組所共用的空間﹐當A模組要將X物料搬到Z位置上空之前﹐必須先取得某個Mutex訊號(例如Mutex(5));同樣的當B模組要運動到Z位置也要先取得這個Mutex訊號(Mutex(5))才行﹐如果此時Mutex(5)沒有先被其它模組使用的話﹐則該模組就可以繼續下一步的動作程序;但是如果Mutex(5)已經先被其它模組使用而且還沒有被釋放的話﹐則該模組會被等待設定時間後Timeout出去﹐於是這二個模組就可以做共用資源與空間的「互斥」管理。
VIO介面標準與I/O卡非常類似:VIO_CreateDriver(…)﹑VIO_DeleteDriver(…)﹑VIO_Set(VIO Address, Value)﹑VIO_Get(VIO Address, *Value)﹑VIO_Wait(VIO Address, Value, Object handle)﹑VIO_KillWait(Object handle)。VIO_CreateDriver(…) 函式是啟動並初始化Visual Device Driver;VIO_DeleteDriver(…) 函式是結束並釋放資源;VIO_Set()函式負責VIO點的輸出;VIO_Get()函式則擔任VIO點值的讀取;而VIO_Wait()函式與VIO_KillWait()函式的動作流程與DIO Device Driver的DIO_Wait與DIO_KillWait()類似。
VIO的程式並沒有使用獨立的執行緒來設計﹐因為VIO並沒有真實的硬體I/O卡需要輸出/輸入﹐因此不需要使用執行緒來定時讀取。VIO的VIO_Get()函式會取得VIO程式內虛擬I/O點的變數值(0/1);而VIO_Set()函式則會設定VIO程式內虛擬I/O點的變數值(0/1)﹐並且檢查Event Table表中是否有人正在等待這個VIO點設定為這個值﹐如果有就激發該Event﹐並將Event從Event Table中去除;而VIO_Wait()函式則會先檢查所要等待的VIO虛擬I/O點的值(0/1)是否合乎條件﹐如果有就直接激發Event;如果沒有才將Event handle放入Event Table中。VIO_KillWait()則是將Event從Event Table中去除。
Mutex介面標準也是採用函式方式製作﹐共計使用以下四個函式:Mutex_CreateDriver(…)﹑Mutex _DeleteDriver(…)﹑Mutex_Wait (Index, Timeout)﹑Mutex _Release(Index)。Mutex_CreateDriver(…) 函式是啟動並初始化Visual Device Driver;Mutex_DeleteDriver(…) 函式是結束並釋放資源;Mutex_Wait()函式負責取得Mutex資源;Mutex_Release()函式則是釋放Mutex資源。至於Mutex資源的程式設計是直接使用Win32 API CreateMutex(…)與WaitForMultipleObjects(…)來處理的。
Function Driver
Function層位於共通控制程式架構的中層﹐是以機臺的觀點將動作流程抽象化﹐在處理程序上大多數是屬於Soft Real-Time處理程序。一般來說﹐機臺的動作流程都很複雜而且共通性很低﹐如果想要將這種複雜的動作流程﹐直接設計在Function Driver內﹐勢必導致Function Driver程式設計不易﹑維護困難而且容易隱藏Bug。因此建議在規劃Function Driver階段﹐就先針對機臺的動作流程做簡化的工作:首先將機臺的動作流程﹐依照特性劃分成一個個獨立的模組﹐讓每個模組的動作流程都是順序式;各個模組間卻是彼此獨立﹐可同時運轉而互不干擾。例如IC Test機臺的料盤進料機構﹑空盤搬運機構﹑滿盤出料機構﹑IC進料Arm﹑IC出料Arm與IC測試機構﹐在空間上或是動作上都是獨立而分開的模組﹐除了部份空間會互相干涉之外﹐彼此之間的運轉並無前後順序連動關係。但是單就某一模組來看﹐各組動力源(例如:氣壓缸﹑感應馬達或伺服馬達)的作動次序卻有特定順序。於是我們就可以把各個模組的動作流程單獨分析出來﹐如此一來雖然整機動作仍然十分複雜﹐但是單就個別模組的動作流程來看﹐卻是很簡單的順序控制﹐透過這些模組的順序程序﹐就可以組合出整機所需的複雜動作流程。當我們適當的劃分好機臺的模組之後﹐就可以針對各模組設計個別的Function Driver Func(x)﹐分別負責該模組的運轉程序(順序控制)。Function Driver的程式設計方法區分為二類:無特殊性動作的模組採用「解譯器核心」來解譯執行動作程序。也就是事先開發設計「解譯器核心」可重用元件﹐用來解譯與執行使用「動作流程語言」描述動作流程的「動作流程程式」。這種設計方式快速方便﹐當有新機種的開發需求時﹐只要把新的動作流程用「動作流程語言」語法描述好﹐放入「解譯器核心」元件中就可以直接使用﹐而且試車修改與維護也會比較快速方便。「解譯器核心」可以因應各種不同的需求設計出幾種類型﹐需要時隨時可以應用﹐因此或許是值得推薦的方式。而對於動作特殊的模組﹐則可考慮將動作流程直接編寫在Function Driver內﹐以因應特殊需求。
如同Device Driver的設計﹐在設計Function Driver程式之前﹐必須先定義一組標準介面(Func Interface)提供上層程式使用﹐讓Command層的排程派工程式以及MMI層人機介面能透過該介面下達命令。當Function Driver接受命令之後﹐Function Driver會使用「解譯器核心」開啟「動作流程程式」依序解譯與執行;或是直接執行Function Driver內的程式碼。Function Driver的程式設計是採用多執行緒的架構設計﹐執行緒在完成各項初始化工作並從硬碟中載入「動作流程程式」之後﹐就會釋放CPU執行權進入Sleep狀態﹐等待上層程式下達命令喚醒自己。當上層程式透過Func Interface下達命令後﹐就會喚醒「解譯器核心」執行緒﹐開啟「動作流程程式」開始解譯執行程式的動作流程程序。
對於「動作流程語言」的設計﹐必須因應機臺類別﹑動作特性﹑產業特質與設計人員喜好來定義﹐並無最佳的標準答案﹐不過基本原則就是「簡單」﹑「夠用就好」。也就是說「動作流程語言」並非如同C/C++/Pascal等是汎用型電腦語言﹐最好僅是根據機臺動作流程的描述需求來定義﹐如此定義出來的語法才會簡單﹑實用﹑效率高。筆者所定義的「動作流程語言」語法如圖四所示。「動作流程語言」有二種變數可使用:Global變數﹑Local變數﹐分別是100個長整數型態的變數陣列﹐而二者的差別在於Global變數是所有Function Driver與Command層所共用;而Local變數則是Function Driver所私有。在程式設計上﹐Global變數多用於資料的交換與參數的下達﹐例如Command層排程程式可以將運轉所需參數放入Global變數中﹐供Function Driver抓取。
「動作流程語言」的參數共有三個:OP1﹑OP2﹑OP3﹐並且區分為三種Type:第一種Type將數值直接寫在程式的參數欄位上﹐例如Goto 10﹐在程式的執行上會跳到Label 10的地方;第二種Type是先將數值放入變數中(例如剛剛的10先放入第7號變數中)﹐再將變數編號寫到程式的參數欄位上(例如Goto *7);第三種Type是先將數值放入變數中(例如剛剛的10先放入第7號變數中)﹐再將變數編號放入第二個變數中(例如剛剛的7放入第5號變數中)﹐最後才將第二個變數編號寫到程式的參數欄位上(例如Goto **5)。不過並不是每一種命令的每一個欄位都能使用三種Type﹐而是依照指令的定義決定的。
「動作流程語言」的程式結構並沒有如同現有的高階電腦語言一樣有區塊(Block)的設計﹐而是單純的採用Goto指令來控制程式的執行流程﹐以簡化「解譯器核心」設計上的複雜度。程式的結構區分為六個欄位;Title﹑Condition﹑Command﹑OP1﹑OP2﹑OP3。Title欄位只能是1或是0﹐如果是1代表要把後面Condition欄位反相(True變False﹑False變True);如果是0則不做任何變更。Condition欄位只能是0或是變數編號﹐如果是0代表該行指令一定得被執行;如果是變數編號﹐則將該編號變數的值(0代表False;非0代表True)與Title的內容(0或是1)做運算﹐並依據結果是否是True來決定該行指令是否被執行。Command欄位是程式指令﹐OP1/OP2/OP3欄位是指令參數。
例如要設計如前文的「下達一組DO點來驅動某個動力源(例如讓氣壓缸推出)﹐或是下達「點對點運動」命令給某顆伺服馬達﹐然後開始不停的讀取某個感測器(例如磁簧開關)的值﹐或是某塊運動控制卡的狀態值﹐並判斷讀取值是否變成True以得知動作是否完成﹐然後繼續下一個動作」的動作流程﹐只需要在「動作流程程式」中使用「SetIO△DO點△1」﹐以及「WaitIO△DI點△1△5000」二行指令即可完成。當「解譯器核心」解譯到「SetIO△DO點△1」時﹐就會透過Driver Interface的DIO_Set(DO Address, Value)函式﹐將該DO點設定為ON;接著解譯到「WaitIO△DI點△1」時﹐則會先重置(ResetEvent())Manual-Reset Event Object﹐再透過Driver Interface的DIO_Wait(DO/DI Address, Value, Object handle)函式將Event Object handle傳給IO Device Driver﹐最後使用WaitForMultipleObjects()來進入有條件式的Sleep﹐並設定Timeout時間(5000msec=5秒)。這樣可以輕易的擺脫「下達命令﹑讀取﹑等待﹑讀取﹑等待﹑……」這種Busy Waiting的動作﹐而且在動作流程的設計上更加方便快速。
然而「天下不如意事十之八九」﹐機臺隨時都可能發生各式各樣的異常狀況﹐例如缺料﹑卡料﹑空壓不足﹑安全門被打開﹑EM……等等。因此「解譯器核心」在設計上就必須提供異常狀態機制﹐供「動作流程程式」處理使用。例如當「解譯器核心」在Sleep等待某個感測器狀態時﹐除了Wait正常Event之外﹐還要同時Wait Alarm Event。當「解譯器核心」從「睡眠」中被喚醒之後﹐第一件事就是判斷到底是被那種Event所喚醒(正常Event﹑Alarm Event或是Timeout)。如果是正常Event﹐就先重置(ResetEvent())Event﹐再將99號Local變數設定為0﹐然後解譯下一道指令;如果是Timeout或是Alarm Event﹐就將99號Local變數設定為1(Timeout)或2(Alarm)﹐並使用KillWait(Object handle)函式強迫將Event從Device Driver的Event Table中去除﹐然後解譯下一道指令。相對的「動作流程程式」也必須善用這些異常狀態機制﹐在每一次用「WaitIO△DI點△0/1△Timeout」去等待I/O點或Motion動作之後的下一道指令﹐一定得去判斷99號Local變數的值﹐以確定動作的完成與否﹐並適當的處理各種異常狀況。
MMI層
MMI層位於共通控制程式架構的最上層﹐是機臺對外的標準介面。主要是擔任操作/維護人員的人機介面﹐負責將操作/維護人員的命令下達給下層程式運轉機臺﹐並隨時顯示運轉狀態﹐以及機臺故障卡料時的異常狀況排除。在MMI的設計上要能與產業特質配合﹐例如在半導體前段廠﹐由於機臺運轉參數多﹑工作場所清潔度高﹑操作人員素質佳﹐因此常可見到使用滑鼠或是觸控螢幕(Touch Screen)來操作Windows﹑Menu或Toolbar圖型人機介面;又例如半導體構裝廠﹐就比較常見到使用薄膜按鍵操作功能鍵﹐畫面大量採用文字﹑翻頁的人機介面。至於工作環境比較惡劣的機械廠就更不適合使用滑鼠或觸控螢幕了﹐因為灰塵容易導致滑鼠/鍵盤故障;手髒容易抹黑觸控螢幕導致無法辨視﹐Windows﹑Menu或Toolbar太過花俏﹐操作起來反而緩慢;相對的使用按鈕(Push Button)操作方式﹐配合大而簡單的圖型/文字畫面﹐可以快速而顯目的看到必要訊息﹐外加數量適當的燈號顯示常用狀態﹐這樣操作起來可能會更順暢。因此何為最佳最適當最好用的人機介面﹐還真的是Case by case。MMI的程式基本上有四項:第一﹑機臺參數(Parameter)的編輯﹑顯示﹑儲存﹑修改﹑刪除功能(例如:機臺狀態-燈號表﹑Arm設定);以及物料參數的選取﹑編輯﹑顯示﹑儲存﹑修改﹑刪除功能(例如:Tray長寬值﹑加熱溫度與時間)。第二﹑生產狀況資料的顯示﹑儲存功能﹐包括工程資料與製程資料。工程資料包括生產管理的資料(例如:產能﹑生產時間)以及機臺運轉資料(例如:Alarm歷史資料﹑Log資料﹑MTBF故障發生平均時間﹑Up-Time﹑Down-Time﹑UPH)﹐可以提供生管/維護工程師參考;而製程資料主要是物料製程中所使用或是所發生的物理量值﹐這些數值有些在機臺運轉過程中就被即時分析與預警(例如SPC)﹐有些會透過資料庫儲存供品保/製程工程師分析參考。第三﹑機臺單功能測試(Manual Mode)﹐提供維護工程師直接控制Function Driver做單步或單功能運轉。第四﹑機臺全自動運轉(Autorun Mode)﹐提供操作員全自動運轉機臺﹐並於運轉期間隨時回報各項生產資料﹐方便操作員管理機臺﹑上下料或是確保機臺的正常運轉。這四項功能各別機種差異很大﹐很難設計出可重用元件﹐因此只能就常用功能設計成函式庫﹐以節省新機種的開發時間。例如將「視覺鍵盤」(如同鍵盤的Windows﹐可用滑鼠點選輸入數字或英文)﹑統計圖表(Bar Chart﹑X-Y Chart﹑Time Chart)﹑Alarm歷史資料Panel(Alarm歷史資料的Display﹑Find﹑Print﹑Save﹑Add﹑Delete)寫成函式庫﹐以節省重覆開發成本。
Command層
Command層主要是作排程派工使用﹐負責協調機臺的各個模組共同運轉。Command層分為二個模式:Manual Mode與Autorun Mode;三種元件:Cmd Process(x) ﹑Autorun Process與Return Process。Manual Mode主要是提供手動模式﹐讓操作人員可以透過MMI直接下達命令給Function層或Driver層﹐做單步或單功能運轉的模式。這種模式多半使用於試機與維修﹐讓維護工程師能以手動方式針對需要測試的部份單獨運轉﹐並且隨時回報各模組的I/O﹑溫控﹑Motion等硬體的運轉狀態﹐方便機臺的檢修。如圖二所示﹐當操作人員透過MMI下達命令之後﹐MMI就會直接Call對應的Command層CmdProcess(x)函式﹐並下傳所需參數。當CmdProcess(x)函式被Call之後﹐首先會解開參數並初始化變數﹐然後檢查動作是否允許執行(包括執行該動作的Func(x)是否有空(idle)﹐所需資源與空間(Mutex)是否free)﹐如果不行就直接回應錯誤碼給MMI然後結束函式(如圖二Type1)﹐MMI會把錯誤碼與錯誤說明顯示在畫面上告知操作者。如果允許執行就開始執行命令﹐如果該命令是Set IO點﹑Get IO點或是讀取Func(x)的執行狀態等可以立即執行完畢的命令﹐就直接透過Driver/Func Interface下達命令執行﹐並回應執行狀態給MMI然後結束函式(如圖二Type1)﹐MMI會把執行狀態顯示在畫面上告知操作者;如果是需要長時間執行動作才能完成的命令﹐就必須透過Func Interface Call Func(x)程式(如圖二Type2)﹐並下傳參數然後結束函式(因為每個Func(x)都有獨立的執行緒﹐所以Call Func(x)的動作只是將Func(x)從Idle狀態中Wake up起來﹐並告知執行的Job編號)。當Func(x)被Wake up之後就會開始執行該Job的動作直到動作完成(或無法完成自動取消)﹐再將執行結果送到FuncQueue後自動回到Idle狀態。Command層另外有支由Timer Event定時觸發的函式 - Return Process函式(只有在Manual Mode才會被使用到)﹐定時會去FuncQueue抓取資料然後回應給MMI﹐MMI會把執行結果顯示在畫面上告知操作者。有時候Func(x)在執行過程中發生了可回復式異常﹐必須要求使用者排除後判斷狀況再下達命令Retry或Cancel該Func(x)(如圖二Type3)。例如Arm的吸盤吸不到物料時(真空度不足)就必須告知操作者﹐由操作者判斷是否真的沒料需要Cancel;還是物料沒擺好﹐只要調整一下再重試Retry就可以了。這種可回復式異常狀況的動作流程如下:Command層的Func(x)將Error碼送到FuncQueue﹐然後Set VIO(y) = 0﹐再Wait VIO(y)為1時Wake up﹐最後Sleep進入Idle狀態。接著Return Process會定時去FuncQueue抓取資料﹐並回應Error碼給MMI(如圖二Type3)﹐MMI就用對話窗(Dialog)將Error碼顯示在畫面上﹐並詢問操作者要Retry或Cancel該命令。如果操作者選擇Retry則會Call處理Retry的CmdProcess(x)將VIO(y+1)設為1﹐再將VIO(y)設為1來Wake up Func(x);如果操作者選擇Cancel則會Call處理Cancel的CmdProcess(x)將VIO(y+1)設為0﹐再將VIO(y)設為1來Wake up Func(x)。Func(x)被Wake up之後得先查詢VIO(y+1)的值﹐用以判斷後續的動作該是Retry或是Cancel。當Func(x)完成後續動作(或無法完成自動取消)之後﹐就會將執行結果送到FuncQueue然後自動進入Idle狀態。Return Process再去FuncQueue抓取資料然後回應給MMI﹐MMI就會把執行結果顯示在畫面上告知操作者。
Autorun Mode的運作流程如圖三所示﹐與Manual Mode最大的差異在於Command層Func(x)命令不由MMI直接下達﹐而是由Command層的Autorun Process(Timer Event定時觸發的函式﹐只有在Autorun Mode才會被使用到)來下達。Autorun Process主要是負責自動模式下各模組的排程派工﹐Autorun Process根據事先設計好的排程演算法﹐透過Func Interface自動下達適當的運轉命令給Func(x)運轉機臺﹐並且透過FuncQueue讀回運轉狀態﹐再根據排程演算法進行下一步動作程序。而此時MMI也可以隨時Call Command層的GetTaskStatus()函式來得知各模組的工作狀況﹐並顯示在畫面上供操作者參考。有時候Func(x)在執行過程中發生了可回復式異常﹐必須要求使用者排除後判斷狀況再下達命令Retry或Cancel。對於這種狀況的處理方式與Manual Mode有些類似:首先Func(x)將Error碼送到FuncQueue中﹐然後Set VIO(x) = 0﹐再Wait VIO(x)為1時Wake up﹐最後Sleep進入Idle狀態。而Autorun Process會定時去FuncQueue抓取資料﹐如果資料是Autorun Process無法自行處理的狀況﹐就直接回應Error碼給MMI﹐通知MMI用對話窗方式詢問操作者要Retry或Cancel。如果操作者選擇Retry則會Call處理Retry的CmdProcess(x)將VIO(x+1)設為1﹐再將VIO(x)設為1來Wake up Func(x);如果操作者選擇Cancel則會Call處理Cancel的CmdProcess(x)將VIO(x+1)設為0﹐再將VIO(x)設為1來Wake up Func(x)。Func(x)被Wake up之後得先去查詢VIO(x+1)的值﹐用以判斷後續的動作該是Retry或是Cancel。當Func(x)完成後續動作(或無法完成自動取消)之後﹐就會將執行結果送到FuncQueue中﹐讓Autorun Process去FuncQueue抓取資料﹐進行下一個程序的動作;如果因此造成整個Autorun Mode無法繼續排程運轉﹐則Autorun Process必須盡量完成出料的工作之後回到Manual Mode。
機臺模擬
傳統控制程式的試車﹐必須等待機構組裝﹑配電完成之後才能進行﹐因此控制程式的試車時間就會影響到機臺出機進度。但是控制程式的試車又很難在沒有機臺的情況下測試各種動作流程﹐因此比較好的解決辦法或許是開發機臺模擬軟體﹐用軟體模擬的方式提供初步試車的工具﹐並以視覺介面來觀察動作流程時序﹐或是記錄下所有動作時序供Debug使用。在這套共通控制程式架構中﹐我們是將Driver層的各個Device Driver「偷偷」開個「後門」﹐提供一組「非標準」介面函式來直接讀寫Device Driver程式內部重要變數﹐然後設計一支機臺模擬程式﹐透過「非標準」介面來讀取上層程式下達給Device Driver的命令(例如GetDO(3)或是RunPTP(2, 1000))﹐接著依照機臺機構的設計與元件的使用方式﹐將該發生的動作訊號透過這組「非標準」介面寫回Driver(例如SetDI(7) = 1或是RunPTP Run OK)。例如由「非標準」介面中讀到上層程式下達命令讓第三點DO為1﹐然後查詢「機臺模擬機構表」找出該DO點為某支氣壓缸的伸出控制點﹐接著再由「機臺模擬狀況表」確認該氣壓缸的狀態﹐如果是在後方縮回處﹐則先查詢「機臺模擬機構表」找出該氣壓缸後方Sensor的DI點編號﹐然後透過「非標準」介面強迫把Device Driver內的該DI點設定為OFF。接著查詢「機臺模擬機構表」找出該氣壓缸的工作時間與前方Sensor的DI點編號﹐然後機臺模擬程式Delay該工作時間﹐然後透過「非標準」介面強迫把Device Driver內的該DI點設定為ON。對Device Driver而言﹐根本不知道這個訊號已經被「攔截」出去﹐然後被「偷」放進來﹐它會以為這是外界機構「真實」動作所產生的結果。於是這個結果就會被送回上層﹐達到以軟體模擬機臺﹐提前讓控制程式可以在機構加工中﹑尚未組裝前﹐先做初步的測試﹐以節省部份試車時間。而機臺模擬軟體除了透過「非標準」介面來抓取命令與反應結果之外﹐還可以把動作的時序圖顯示到畫面上﹐或是記錄下來供Debug使用。結 語
隨著時代進步的腳步日漸加快﹑產品生命週期的日漸縮短﹐能最快因應各種需求推出新機臺者﹐贏得定單穫取利潤的機會就會比較高。為了因應這種趨勢﹐機臺的控制程式開發也必須跨出「手工打造」的「工藝品」製作方式﹐也就是控制程式的開發或許要有標準設計規範與流程﹐並且以「Team Work」分工合作取代「英雄主義」一人獨包的方式﹐在控制架構上最好能設計共通架構與層次間的標準介面﹐並累積可重用元件與函式庫。如此當有新機種的開發需求時﹐控制程式將可以很快的被「組裝」出來﹐開發時間短且品質穩定;而且由於標準化的緣故﹐程式交接快速而且後續維護工作容易﹐因此或許是值得參考的方式之一。參考文獻
[1]曹永誠﹐漫談PC-Base控制程式設計(1)(2)(3)﹐工業電機&自動控制裝置與設計雜誌﹐1998/7,8,9[2]http://www.vci.com
[3]http://www.imagination.com/
[4]http://www.radisys.com/
[5]RTX 4.1 User’s Guide and Reference﹐VenturCom Inc.
沒有留言:
張貼留言