Programowanie sterowników PLC już dawno nie ogranicza się do realizowania logiki przekaźników programowalnych za pomocą języka drabinkowego. Wraz z kolejnymi edycjami standardu do świata automatyki przemysłowej wchodziły kolejne elementy znane bardziej z języków programowania C, C++ czy JAVA. Szczęśliwie, nie oznaczało to rezygnacji z prostoty. Nadal proste zadania można realizować po staremu. W większych projektach dostajemy jednak cały arsenał narzędzi, dzięki którym praca może być łatwiejsza.


Jednym z takich narzędzi są wskaźniki. Aby zrozumieć ich znaczenie i siłę, konieczne jest spojrzenie na budowę pamięci w sterowniku PLC (i w sumie w każdym innym urządzeniu elektronicznym).

Budowa pamięci

Pamięć składa się z komórek. W zależności od architektury jedna komórka przechowuje 8,16, 32 lub 64 bity danych. W przypadku sterowników PLC jest to 8 bitów. Każda komórka ma unikalny adres.

W momencie kiedy urządzenie chce zapisać jakąś wartość w pamięci, najpierw rezerwuje sobie odpowiedni obszar. W przypadku większości zmiennych konieczne jest zajęcie większej liczby komórek. Przykładem jest zmienna typu całkowitego (INT) zajmująca w pamięci 2 kolejne komórki. Na zarezerwowanym obszarze zakodowywane są dane. W przypadku zmiennych typu BYTE, WORD czy INT jest to po prostu binarny zapis liczb całkowitych. STRING jest natomiast liczbowym zapisem kolejnych liter tekstu zgodnie ze standardem ASCII. Warto jednak pamiętać, że dla pamięci nie ma znaczenia, co jest w niej zapisane. Zawsze będzie to ciąg zer i jedynek i to od nas zależy, jak zostanie zinterpretowany.

Dlatego oprócz zarezerwowania obszaru pamięci i zapisania danych, sterownik określa też, jaki typ danych jest przechowywany pod danym adresem. Tylko wtedy takie dane są dla programu PLC użyteczne. Dobrym przykładem może być zadeklarowanie dwóch zmiennych: jednej typu BYTE, drugiej typu STRING(1). Obydwie mają ten sam rozmiar, ale są różnych typów. Jeśli przypiszemy im następujące wartości:

bZmienna1:BYTE:=65;
sZmienna2:STRING(1):=”A”;

to w pamięci sterownika będą one wyglądać dokładnie tak samo:

Adres pamięci Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
16#543AB514 0 1 0 0 0 0 0 1

W trakcie kompilacji tworzona jest tabela zawierająca wszystkie zmienne i struktury wraz z ich adresami. W przypadku zmiennych zajmujących więcej niż jedną komórkę za adres uznaje się pierwszą zajętą komórkę.

Nazwa typ rozmiar adres
bZmienna1 BYTE 1 16#1A65B28T
rZmienna2 REAL 4 16#87B797A6
aZmienna3 ARRAY[0..9]OF REAL 40 16#7643738A
wZmienna4 WORD 2 16#87C24304

Normalnie do zmiennych odwołujemy się po nazwie. Jest to wygodne, ponieważ dzięki temu kod jest czytelny, a kompilator może pilnować, aby podczas przepisywania wartości z jednej zmiennej do drugiej były one zawsze tego samego typu. Takie rozwiązanie ma jednak swoje wady i ograniczenia. Jeśli do bloku funkcyjnego przekazujemy zmienną za pomocą zmiennej zadeklarowanej jako VAR_INPUT, oznacza to, że wewnątrz bloku funkcyjnego znajduje się wewnętrzna kopia zmiennej przypisanej na zewnątrz bloku. W takim przypadku podczas każdego cyklu sterownika wartość zmiennej jest przepisywana z miejsca na miejsce. Operacja taka zawsze zabiera pewną ilość zasobów obliczeniowych sterownika. W przypadku pojedynczych zmiennych nie jest to problem, ale gorzej jeśli mamy do czynienia z kilkusetelementowymi tablicami struktur. Kolejnym aspektem jest pamięć sterownika. Korzystając ze zmiennych typu VAR_INPUT lub VAR_OUTPUT wymuszamy tworzenie wielu kopii tej samej zmiennej.

Wskaźniki – wprowadzenie

Z pomocą przychodzą tu wskaźniki. Jest to specjalny rodzaj zmiennej, umożliwiający łączenie adresu fizycznego pamięci z typem przechowania danych. Wskaźniki robią w zasadzie to, co kompilator podczas tworzenia przedstawionej wcześniej tabeli. Jednak w przeciwieństwie do kompilatora, to programista może dowolnie manipulować wszystkimi parametrami.

Wskaźnik deklarujemy jak zwykłą zmienną. Najlepiej zrobić to ręcznie. Deklaracja wskaźnika powinna wyglądać tak:

pWskaznik1 : POINTER TO WORD;

Oczywiście typ danych może być też tablicą, strukturą, blokiem funkcyjnym, tablicą struktur itp.

Wskaźnik jako taki jest zmienną typu DWORD i przechowuje adres zmiennej. Aby go zdobyć, należy skorzystać z funkcji ADR():

pWskaznik1 :=ADR(wZmienna1);

W przeciwieństwie do zwykłych zmiennych kompilator nie sprawdza zgodności typów danych. Oznacza to, że do powyższego wskaźnika można swobodnie przypisać adres zmiennej innej niż WORD.

pWskaznik1 :=ADR(iZmienna2);

Należy robić to jednak w pełni świadomie, ponieważ błąd na tym etapie może w najlepszym razie dostarczyć programowi bezużyteczne dane, a w najgorszym zatrzymać cały sterownik.

Aby odczytać wartość zmiennej przekazanej za pomocą wskaźnika, należy skorzystać z następującej składni:

wWartoscZdekodowana:=pWskaznik1^;

Konwersja typów danych za pomocą wskaźników

Opisane wcześniej właściwości wskaźników sprawiają, że są one idealnym narzędziem do wszelkiego rodzaju konwersji typów danych. Najprostszym przykładem jest odczyt wartości zmiennoprzecinkowych za pomocą protokołu Modbus. Protokół ten z założenia operuje na rejestrach typu WORD, podczas gdy zmienna typu REAL zajmuje 2 WORDy. Zadanie konwersji jest o tyle prostsze, że odczytane po Modbusie wartości najczęściej przechowywane są w tabeli, np.

aTabelaModbusowa:ARRAY[0..100]OF WORD;

Wiemy, że interesująca nas zmienna Modbus znajduje się na adresie 10 powyższej tabeli. W rzeczywistości oznacza to, że zajmuje komórki 10 i 11.
W celu konwersji na REAL wystarczy zadeklarować następujący wskaźnik:

pWskaznikREAL:POINTER TO REAL;

W kodzie programu konieczne jest wywołanie polecenia:

pWskaznikREAL:=ADR(aTabelaModbusowa[10]);

Dzięki temu wskaźnik będzie obserwował odpowiedni obszar pamięci i interpretował go jako liczbę zmiennoprzecinkową. Aby uzyskać dostęp do tak otrzymanej wartości, należy skorzystać z operatora ^.

pWskaznikREAL^;

Tablice o dowolnej wielkości
Dość dokuczliwym ograniczeniem klasycznego podejścia do tablic jest konieczność sztywnego deklarowania ich wielkości. Jeśli tworząc blok funkcyjny na jego wejściu chcemy przyjmować tablicę, to zawsze będziemy skazani na podłączanie do niego tablicy o dokładnie takiej samej wielkości.

Problem można dość łatwo ominąć korzystając ze wskaźników. Załóżmy, że tworzymy bloczek, który ma za zadanie zsumować liczby z tablicy zawierającej liczby typu INT. Naturalne jest, że chcielibyśmy mieć możliwość podłączenia pod taki bloczek tablicy o dowolnym rozmiarze. Poniżej kod przykładowego bloku:

FUNCTION FunSumArray :DINTVAR_INPUT
pTablicaWejsciowa:POINTER TO INT;
uiRozmiarTablicy:DINT;
END_VAR
VAR
i:INT;
diSuma:DINT;
END_VAR(*kod funkcji*)

FOR i:=0 TO uiRozmiarTablicy DO
diSuma:=diSuma+ pTablicaWejsciowa^;
pTablicaWejsciowa:= pTablicaWejsciowa+2;
END_FOR

FunSumArray:=diSuma;

Z funkcji korzystamy w następujący sposób:

PROGRAM PLC_PRG
VAR
aTablica:ARRAY[0..99]OF INT;
iWynik:INT;
END_VAR(*kod programu*)

iWynik:= FunSumArray(ADR(aTablica),100);

Przedstawiony przykład działa w taki sposób, że funkcja za pomocą wskaźnika dostaje adres początku tablicy INTów. Dzięki temu, że przekazywana jest też informacja o rozmiarze tablicy, możliwe jest „przejście” przez całą tablicę z wykorzystaniem pętli FOR.

W każdej iteracji wskaźnik jest przesuwany o 2 pozycje, ponieważ zmienna typu INT zajmuje zawsze 2 komórki pamięci.

Takie rozwiązania często można spotkać w bibliotekach do komunikacji.

Zmienna działająca jak VAR_IN_OUT w funkcjach

Jednym z ograniczeń funkcji jest brak możliwości deklarowania zmiennych jako VAR_IN_OUT. Tego typu zmienne są bardzo użyteczne, ponieważ mogą jednocześnie przekazywać do bloku funkcyjnego parametry oraz być zmieniane przez kod wewnątrz bloku.
To ograniczenie można jednak ominąć stosując wskaźniki

FUNCTION FunMnozenie2 :BOOLVAR_INPUT
pZmiennaWejsciowa:POINTER TO INT;
END_VAR
VAR
iTemp:INT;
END_VAR(*kod funkcji*)

iTemp:=pZmiennaWejsciowa^;
pZmiennaWejsciowa^:=iTemp*2;
FunMnozenie2:=true;

 

PROGRAM PLC_PRG
VAR
iZmienna:INT:=1;
END_VAR(*kod programu*)

FunMnozenie2 (ADR(iZmienna));

Powyższy przykład będzie działać w taki sposób, że w każdym cyklu sterownika zmienna przekazana do funkcji jest mnożona przez 2. Widać, że zmienna wejściowa funkcji działa tak jak zadeklarowana jako VAR_IN_OUT w bloku funkcyjnym.

Błędy, jakich należy unikać

Ponieważ wskaźniki są wykorzystywane w wielu popularnych bibliotekach, każdy programista PLC powinien nimi w miarę swobodnie operować.

Większe możliwości tego narzędzia wiążą się też z większą odpowiedzialnością, której nie może zdjąć z barków programisty nawet najlepszy kompilator.

Błąd, którego należy unikać za wszelką cenę to odwoływanie się do obszarów pamięci, na których nic nie zostało zadeklarowane. Może się tak zdarzyć, jeśli w przykładzie z tablicą podamy większy niż w rzeczywistości rozmiar tablicy.

Jeśli tak się stanie, program PLC zostanie zatrzymany, a w CODESYS lub e!COCKPIT pojawi się komunikat Access violation.

W kolejnym artykule opiszę zastosowanie referencji, które pod wieloma względami przypominają wskaźniki.

Krzysztof Nosal, WAGO.PL

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Zobacz również

Bliski kuzyn wskaźnika

W poprzednim artykule (Wskaźniki – najbardziej uniwersalne narzędzie