Komunikacja pomiędzy sterownikiem PLC a centralą Satel Integra 64 odbywała się u mnie do niedawna w oparciu o wyjścia przekaźnikowe centrali (np. CA-64 O-R) które zwierały obwody podłączone do wejść cyfrowych (DI) sterownika PLC. Wszystko działało doskonale, jednakże ze względu na ograniczoną liczbę żył przewodów pociągniętych pomiędzy centralą alarmu a szafą sterowniczą, rozbudowa o sygnalizowanie kolejnych czujek/zdarzeń była niemożliwa.
Alternatywnym rozwiązaniem, które działa u mnie od kilku dni, jest komunikacja po RS232. Ze strony hardware’u konieczny był moduł WAGO 750-650/003-000 oraz Satel INT-RS. Poza tym ściągnąłem bibliotekę Serial_Interface_01.lib (do pobrania ze strony Wago) i instrukcję programowania dostępną na stronie Satel’a. Otrzymałem też od Wago przykładowy program, na bazie którego oparłem cały kod. Za całą pomoc panom z WAGO serdecznie dziękuję!
Co do konfiguracji – po stronie INT-RS – tryb pracy kontrolowany mikroprzełącznikiem ustawiony na integrację z innym oprogramowaniem (switch 5 ON). Po stronie 750-650/003 – Baudrate: 19200, Data frame: 8 databits, Stopbits: 1, Data bytes: 5, RTS/CTS: Disable…
Przypominam, że poniższy kod jest dziełem amatora i może być niezgody ze sztuką. Jest jednak przetestowany i na razie działa:
W definicji zmiennych:
VAR (*interface definitions*) CommContext : SERIAL_INTERFACE; Init : BOOL; Error : BYTE; IsOpen : BOOL; Send : BOOL:=TRUE; (*send launch, reset on send end*) RMessage : typRING_BUFFER; (*received message*) Message : POINTER TO BYTE; MessageLEN : BYTE; (*default message to be sent*) Message7F : ARRAY [0..6] OF BYTE:=254, 254, 127, 216, 97, 254, 13; (*variables supervising communication flow*) SendEnds : F_TRIG; SendStarts : R_TRIG; WaitForData : BOOL; StopWaiting : TON; FrameComplete : BOOL; LastRecord : INT; (*stores last reviewed byte*) SyncSignal : BOOL:=FALSE; EndSignal : BOOL:=FALSE; (*variables for RMessage analysis*) mStart, mEnd : INT; crcHigh, crcLow : BYTE; RMessageCRC : CRC_Calculate; Command : BYTE; (*variables for preparing message on demand*) FoundNewState : BOOL; MessageState : typRing_BUFFER; MessageStCrc : CRC_Calculate; CrcLenExtension : BYTE; (*variables storing data received from Integra*) Sensors : ARRAY[0..55] OF BOOL; NewStates : ARRAY[0..39] OF BOOL; i : BYTE; END_VAR
Program w trybie ciągłym wysyła do centrali komendę 0x7F tj. „wymień wszystkie nowe stany”. Odpowiedzi umieszczane są w tabeli NewStates. Na początku każdego cyklu tabela ta jest analizowana. Jeśli którykolwiek ze stanów = TRUE, kolejne wysyłane komendy, będą dotyczyły tych właśnie stanów. Po odczytaniu wszystkich nowych stanów, program powraca do wysyłania komendy 0x7F. Oto kod:
SendStarts(CLK:=Send); (*trigger sensing the begin of sending*) IF SendStarts.Q THEN (*if sending starts*) (*prepare the variables*) FoundNewState:=FALSE; i:=0; (*checking if there is an unread Integra64 state stored in NewStates ARRAY*) WHILE (i<40) AND (NOT FoundNewState) DO IF NewStates[i] THEN (*new event found*) FoundNewState:=TRUE; (*marker to finish searching*) NewStates[i]:=FALSE; (*reset the state in NewState ARRAY*) CrcLenExtension:=0; (*preparing new message to be sent*) MessageState.Data[0]:=254; MessageState.Data[1]:=254; MessageState.Data[2]:=i; (*command number=index in the ARRAY*) MessageStCrc(m:=ADR(MessageState), start:=2, end:=2); (*calculate CRC*) MessageState.Data[3]:=WORD_TO_BYTE(MessageStCrc.hi); IF MessageStCrc.hi=254 THEN (*if CRC.hi=254 add 240 after it*) MessageState.Data[4]:=240; CrcLenExtension:=1; END_IF; MessageState.Data[4+CrcLenExtension]:=WORD_TO_BYTE(MessageStCrc.lo); IF MessageStCrc.lo=254 THEN (*if CRC.lo=254 add 240 after it*) MessageState.Data[5+CrcLenExtension]:=240; CrcLenExtension:=CrcLenExtension+1; END_IF; MessageState.Data[5+CrcLenExtension]:=254; MessageState.Data[6+CrcLenExtension]:=13; MessageState.Index:=7+CrcLenExtension; END_IF; (*End of IF new event found*) i:=i+1; END_WHILE; (*end of loop for checking NewState ARRAY*) (*the clue of this IF statement – decide what message to send*) IF FoundNewState THEN (*if there is an new state to be red*) Message:=ADR(MessageState.Data); MessageLen:=INT_TO_BYTE(MessageState.Index); ELSE (*otherwise send the standard 7F message*) Message:=ADR(Message7F); MessageLEN:=7; END_IF; END_IF; (* End of IF Send Start detected*) (*definition of the Communication module*) CommContext( xOPEN_COM_PORT:=TRUE, bCOM_PORT_NR:=2, cbBAUDRATE:=1920, cpPARITY:=0, csSTOPBITS:=1, cbsBYTESIZE:=8, cfFLOW_CONTROL:= 0, iBYTES_TO_SEND:=MessageLEN, ptSEND_BUFFER:=Message, (*here is where the message pointer goes*) xSTART_SEND:=Send, utRECEIVE_BUFFER:=RMessage, xINIT:= Init, bERROR=>Error, xCOM_PORT_IS_OPEN=> IsOpen); SendEnds(CLK:= Send); (*trigger sensing the end of sending*) IF SendEnds.Q THEN (*if sending ends…*) WaitForData:=TRUE; END_IF; (* Impulse used to start all over if correct answer is not coming*) StopWaiting(IN:= WaitForData, PT:=t#1s); IF WaitForData THEN (*looking for the bytes 0xFE 0xFE = frame starts repeated in next cycles despite finding one 0xFE 0xFE in case new sync frame signal comes analysis starts from where it ended in the previous cycle*) WHILE (LastRecord<RMessage.Index-1) DO IF RMessage.Data[LastRecord]=254 AND RMessage.Data[LastRecord+1]=254 THEN SyncSignal:=TRUE; mStart:=LastRecord+2; (*message start marker*) LastRecord:=LastRecord+1; END_IF; LastRecord:=LastRecord+1; END_WHILE; (*reset LastRecord to where the 0xFE 0xFE ended*) IF SyncSignal THEN LastRecord:=mStart; END_IF; (*looking for bytes 0xFE 0x0D = frame ends*) WHILE (LastRecord<RMessage.Index-1) AND SyncSignal DO IF RMessage.Data[LastRecord]=254 AND RMessage.Data[LastRecord+1]=13 THEN EndSignal:=TRUE; mEnd:=LastRecord-1; (*message end marker*) END_IF; LastRecord:=LastRecord+1; END_WHILE; (*if a complete frame has been received*) IF SyncSignal AND EndSignal THEN (*check CRC, watch out for 240 bytes - they should be dropped*) IF RMessage.Data[mEnd]=240 THEN (*if one of the CRC numbers is 0xF0, drop it*) mEnd:=mEnd-1; END_IF; crcLow:=RMessage.Data[mEnd]; IF RMessage.Data[mEnd-1]=240 THEN (*if one of the CRC numbers is 0xF0, drop it*) mEnd:=mEnd-1; END_IF; crcHigh:=RMessage.Data[mEnd-1]; mEnd:=mEnd-2; (*function block for calculating CRC*) RMessageCRC(m:=ADR(RMessage), start:=mStart, end:=mEnd); (*check if CRC is okay*) IF RMessageCRC.hi=crcHigh AND RMessageCRC.lo=crcLow THEN (*analysis of the received command*) Command:=RMessage.Data[mStart]; CASE Command OF (*zones violation*) 0: IF (mEnd-mStart)>15 THEN (*check for required response length*) FOR i:=0 TO 6 DO Sensors[0+i*8]:=RMessage.Data[mStart+1+i].0; Sensors[1+i*8]:=RMessage.Data[mStart+1+i].1; Sensors[2+i*8]:=RMessage.Data[mStart+1+i].2; Sensors[3+i*8]:=RMessage.Data[mStart+1+i].3; Sensors[4+i*8]:=RMessage.Data[mStart+1+i].4; Sensors[5+i*8]:=RMessage.Data[mStart+1+i].5; Sensors[6+i*8]:=RMessage.Data[mStart+1+i].6; Sensors[7+i*8]:=RMessage.Data[mStart+1+i].7; END_FOR; END_IF; (*here other commands are to be filled*) (*list new states*) 127: IF (mEnd-mStart)>4 THEN (*check for required response length*) FOR i:=0 TO 4 DO NewStates[0+i*8]:=RMessage.Data[mStart+1+i].0; NewStates[1+i*8]:=RMessage.Data[mStart+1+i].1; NewStates[2+i*8]:=RMessage.Data[mStart+1+i].2; NewStates[3+i*8]:=RMessage.Data[mStart+1+i].3; NewStates[4+i*8]:=RMessage.Data[mStart+1+i].4; NewStates[5+i*8]:=RMessage.Data[mStart+1+i].5; NewStates[6+i*8]:=RMessage.Data[mStart+1+i].6; NewStates[7+i*8]:=RMessage.Data[mStart+1+i].7; END_FOR; END_IF; END_CASE; END_IF; (*End of IF CRC OK*) (*marker to finish waiting and send new message*) FrameComplete:=TRUE; END_IF; (*END of IF a frame has been received*) END_IF; (*END of IF WaitingForData*) (*if a complete frame has been received or waiting time has passed*) IF StopWaiting.Q OR FrameComplete THEN (*reset all the variables*) WaitForData:=FALSE; RMessage.Index:=0; SyncSignal:=FALSE; EndSignal:=FALSE; LastRecord:=0; mStart:=0; mEnd:=0; FrameComplete:=FALSE; Send:=TRUE; END_IF;
Program ten wykorzystuje blok funkcyjny CRC_Calculate, który wylicza CRC zgodnie z zasadami opisanymi w dokumentacji Satela INT-RS. Oto kod:
FUNCTION_BLOCK CRC_Calculate VAR_INPUT m : POINTER TO typRing_BUFFER; start : INT; end :INT; END_VAR VAR_OUTPUT lo : WORD; hi : WORD; crc : WORD; END_VAR VAR i : INT; d : WORD; tcrc : WORD; END_VAR * * * * * IF end=0 THEN end:=m^.Index; END_IF; tcrc:=16#147A; FOR i:=start TO end DO tcrc:=ROL(tcrc, 1); tcrc:=tcrc XOR 16#FFFF; tcrc:=tcrc+HEX_HI(in:=tcrc)+m^.Data[i]; END_FOR; crc:=tcrc; d:=tcrc / 4096; hi:=d*16; tcrc:=tcrc-d*4096; d:=tcrc/256; hi:=hi+d; tcrc:=tcrc-d*256; d:=tcrc/16; lo:=d*16; tcrc:=tcrc-d*16; lo:=lo+tcrc;
…powyższy blok wykorzystuje funkcję HEX_HI:
FUNCTION HEX_HI : WORD VAR_INPUT in : WORD; END_VAR VAR div4096 : WORD; END_VAR * * * * * div4096:=in/4096; HEX_HI:=div4096*16; in:=in-4096*div4096; HEX_HI:=HEX_HI+in/256;
Ostatecznie całość składa się z 3 elementów: programu RS232, bloku funkcyjnego CRC_Calculate i funkcji HEX_HI:
...i co dalej? Jeśli chodzi o stany czujek ruchu, które umieszczane są w tabeli Sensors, można je śledzić z programu głównego PLC_PRG:
VAR Move_Wejscie, Move_Hol, (...) Move_Kotl : R_TRIG; END_VAR{/codecitation} * * * * * Move_Wejscie(CLK:=RS232.Sensors[0]); Move_Hol(CLK:=RS232.Sensors[1]); (...) Move_Kotl(CLK:=RS232.Sensors[12]);
W chwili wykrycia ruchu przez którąkolwiek z czujek, Integra, odpowiadając na powtarzaną cały czas komendę 127, poinformuje o czekającym nowym stanie pod komendą 0 (NewStates[0]=TRUE), kolejną komendą wysłaną przez sterownik będzie 0, na którą INTEGRA odpowie, informując, która z czujek została naruszona (Sensors[x]=True), zdarzenie to zostanie odnotowane przez któryś z triggerów w PLC_PRG, który przez 1 cykl zwróci wartość Q=TRUE (Move_xxx.Q=TRUE). Fakt ten z kolei można wykorzystać np do włączenia światła na korytarzu (Light_XXX to blok Fb_LatchingRelay):
LIGHT_XXX(xSwitch:=IN_X, xCentON:=Move_xxx.Q);
Motywy porzucenia poprzedniego rozwiązania i zmierzenia się z komunikacją RS232 są bardzo niebiznesowe. Po pierwsze irytowało mnie klikanie przekaźników, które dało się słyszeć mimo zamkniętych drzwi obudowy alarmu i pomieszczenia, w którym się znajduje… Po drugie, chciałem zobaczyć, czy dam radę zrozumieć, o co w ogóle w tym wszystkim chodzi. Poszukiwania takie, jak zawsze, dostarczają wielu bezcennych momentów ‘wow’.