Używamy plików cookies (tzw. ciasteczka) by spersonalizować treść i ogłoszenia oraz by analizować ruch na stronie.  W sposób automatyczny dzielimy się informacjami o Twoim użyciu tego portalu z dostawcami ogłoszeń, którzy mogą połączyć te informacje z informacjami, które im udzieliłaś/łeś lub, które sami zebrali. Korzystanie z witryny bez zmiany ustawień dotyczących cookies oznacza, że będą one zamieszczane w Państwa urządzeniu końcowym.  Możecie Państwo dokonać w każdym czasie zmiany ustawień dotyczących cookies zmieniając opcje przeglądarki.

Komunikacja z Satel Integra 64 po RS232

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:

RS232

...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’.