順序控制和狀態機之間的差別

不愿透露姓名的小村村發表於2024-08-29

1. 背景

  • 在PLC一直以來的工程經驗裡面。如果想要做一個順序控制(業務邏輯):
    • 用Graph實現
    • 用Csae..Of分支跳轉語句
    • 用int數的變化切換不同的執行語句
  • 下面一種一種的來看,先看看使用int數切換實現一個順序控制的案例:



    • 在這個案例裡面,程式掃描currrent_step的值的不同,從而選擇執行不同的程式段的內容,根據條件是否滿足則可以做到在一個程式段裡實現往下順序執行,或者往上迴圈回跳直到滿足跳出迴圈的條件。或者定點跳到指定步。這種方法的缺點是:1.可讀性差;2.新增順序控制的時候難以直接在原程式之間差入程式而不引入老程式邏輯的修改;3.在當前步執行的過程中,不知道前序步是從哪兒過來的,因為也許有好幾個前序步在同時指向當前步;4. 本質還是IF判斷,意味著每個迴圈內所有條件都被判斷了一遍。好處就是簡單好寫,順序編寫,只管滿足條件就跳轉,不用過多考慮邏輯判斷的衝突。
  • 如果用Case..Of來寫,情況又會好一點:
    • Step_int用在了Switch語句裡面,指定哪個分支就跳到那個分支,效率提高。
    • 但是缺點依舊,複雜程式步序很多,Step_int在分支語句中被重新賦值,但是不知道前序是哪裡來的,也很難在不去改變Step_int值的情況下插入新的邏輯程式碼
//step
CASE #step_int OF
   
        //init start
    20:
       
        IF "enable_cycle" THEN
            
            #step_int := 21;
            
        END_IF;

        //cycle buffer
    21:
            "recordsData".buffer := "random_data".buffer[#random_cycle].buffer;
            #random_cycle := #random_cycle + 1;
            #step_int := 22;
            
            //cycle count
        22:
            IF #random_cycle < 4 THEN
                #step_int := 30;
            ELSIF #random_cycle = 4 THEN
                #random_cycle := 0;
                #step_int := 30;
            END_IF;
                   
        //way of ascii send to plc300
    30:
        "AsciiSendBuffer".buffer := "recordsData".buffer;
        "_cm1241data".P_SEND.Req := true;
        IF "_cm1241data".P_SEND.Done THEN
            #step_int := 40;
        END_IF;
        
        //tcp inform to 300plc-->inform ascii send finish
    40:
        "_cm1241data".P_SEND.Req := false;
        #step_int := 50;
        
        //tcp inform to 1200plc-->inform ascii rcv finish
    50:
        IF "_cm1241data".P_RCV.Ndr THEN
            #step_int := 60;
        END_IF;
        
        //wait for the feedback/compare 
    60:
        #my_time := T#200ms;
        #timeline.enable := true;
        IF #timeline.time_down AND NOT #impulse_array[1] THEN
            
            "recordsData".records.total_count := "recordsData".records.total_count + 1;
            IF "AsciiRcvBuffer".buffer <> "recordsData".buffer THEN
                "recordsData".records.result := 'N';
                #TmpInt := RD_LOC_T("recordsData".records.timestamp);
                IF "recordsData".records.error_count < 1024 THEN
                    "recordsData".records.table["recordsData".records.error_count].total_count := "recordsData".records.total_count;
                    "recordsData".records.table["recordsData".records.error_count].errror_count := "recordsData".records.error_count;
                    "recordsData".records.table["recordsData".records.error_count].time_stamp := "recordsData".records.timestamp;
                    "recordsData".records.error_count := "recordsData".records.error_count + 1;
                    #step_int := 70;
                ELSE
                    "recordsData".records.error_carry := "recordsData".records.error_carry + 1;
                    "recordsData".records.error_count := "recordsData".records.error_count := 0;
                    "recordsData".records.table["recordsData".records.error_count].total_count := "recordsData".records.total_count;
                    "recordsData".records.table["recordsData".records.error_count].errror_count := "recordsData".records.error_count;
                    "recordsData".records.table["recordsData".records.error_count].time_stamp := "recordsData".records.timestamp;
                    "recordsData".records.error_count := "recordsData".records.error_count + 1;
                    #step_int := 70;
                END_IF;
                
            ELSE
                "recordsData".records.result := 'Y';
                #TmpInt := RD_LOC_T("recordsData".records.timestamp);
                #step_int := 70;
            END_IF;
            
        END_IF;
        #impulse_array[1] := #timeline.time_down;
        
        //data clean
    70:
        #timeline.enable := FALSE;
        #step_int := 71;
        
        //rcv rst
    71:
        "_cm1241data".P_RST.REQ := true;
        #step_int := 72;
        
        
        //rst ok
    72:
        #rst_ton(IN:=(#step_int=72),
                 PT:=T#500ms);
        
        IF "_cm1241data".P_RST.DONE OR #rst_ton.Q THEN
            "_cm1241data".P_RST.REQ := false;
            #step_int := 80;
        END_IF;
        
        //next turn 
    80:
        RESET_TIMER(#rst_ton);
        #step_int := 21;
        
    END_CASE;
    
//execute
#Random_Instance(udintORchar:=true,
                 Out_char_random=>#generate_char);

#alternate_time_Instance(In_switchtime:=#my_time,
                         In_startAlternate:=#timeline.enable,
                         Out_switchtime_up=>#timeline.time_up,
                         Out_switchtime_Down=>#timeline.time_down);
  • 在這種Cse..Of的情況下,有方法可以做到記錄跳轉前序:
//1. 把Step_Int改成一個Current_Step,一個nextStep
//2. case依靠nextStep轉換,步序轉換賦值給currentStep
//3. 新建一個方法,方法在每個掃描週期都會被呼叫,方法作用是當發現currentStep和nextStep值不同時,把新的currentStep的值重新賦值給nextStep
//4. 這種方法還可以靈活的在轉換間隔加上延時以控制延時時間,或者加入一些暫停,取消,終止,手動等等很多步序控制的新功能。
  • 西門子PLC其實提供了更好的做順序控制的方法,如圖:
    • 這是Graph,他有Step,也有Trans
    • 使用者不用再去考慮分支轉換的問題,只要在當前步,滿足條件就可去到下一步或者指定步。
    • 有很多狀態點可以知道程式的現在位置,和上一個位置,以及下一個位置
    • 狀態圖形化,直觀。

2. 什麼是狀態機

  • 上面提到的都是關於順序控制的,也就是說上面的控制一定是逐過程的。當新寫一段Graph程式的時候,設計者通常考慮的都是:初始化狀態是什麼, 第一步要幹什麼,滿足第一步條件之後要跳到哪一步去,下一步又要幹嘛,結束和恢復狀態條件是什麼。這種更是一種邊做邊寫的過程。步序從頭到位以滿足使用需要。
  • 狀態機和順控有一些簡單的相似點,但是他們更多的是不同點。比如西門子的Graph就可以理解為一種狀態機,這是一種“從進到出的”順序,它一定有前序,每一步的執行或多或少是和一些前序步和前置條件繫結的。
  • 但是狀態機的思想是:它更少的去關注過程(條件),更多的關注狀態,每一種獨立的行為或者動作就是一個狀態。這個時候可以儘量的把有限的狀態給統計起來,一個狀態做一個方法。當寫程式之前,首先畫一畫狀態圖,考慮當前狀態要進入的條件,或者當前狀態要出去的條件,注意的是,這種情況下我們其實僅僅在關注當前狀態的前後條件。然後我們用Switch..Case把所有狀態列舉出來。透過一個閒時狀態統一管理狀態的切換,每一個狀態執行完畢之後都回到閒時狀態,由它來決定下一步怎麼走。
  • 和順控不一樣的地方出來了,順口大部分是一步一步走,狀態機無所謂上一步,它只會在Idle中去呼叫分支。對於一些事件觸發(按鈕事件),按下則觸發響應的狀態。
  • 如果狀態機要做一個需要前後判斷的狀態動作,則可以在Idle狀態裡面去判斷當前狀態的標誌位,下一個狀態沒有進行的標誌位,如果標誌位滿足邏輯則進入。
  • 狀態機最重要的是狀態圖,程式的修改要及時修改狀態圖。
  • 狀態機的擴充套件僅僅只需要擴充套件一個新的狀態,按照新的狀態圖改變即可
  • 有一些複雜的場景,狀態機的狀態表示甚至可以用一個二維陣列的表示
  • 一個狀態機的全週期,其實就可以理解為一個產品的全部功能。

相關文章