Jak neimplementovat modbus: průvodce krok za krokem

Modbus je protokol používaný v průmyslových zařízeních určený pro výměnu dat (například: máme klimatizační jednotku, která nám tímto protokolem prozradí teplotu a spoustu dalších informací). Protokol je to poměrně hodně jednoduchý a přímočarý. Navzdory tomu ale historie ukazuje, že v něm existuje velká spousta možností, jak jej zmrvit tak, aby člověk, který se s daným zařízením snaží komunikovat, na tom strávil co nejvíce času a mozkové kapacity, než ze zařízení začne lézt něco, co začne vypadat opravdově. Jelikož je hrozná nuda, když to funguje na poprvé dle dokumentace a správně a občas (ale opravdu jen občas) se někteří výrobci značně přiblíží implementaci dle specifikace a zdravého rozumu, ukážeme si léty osvědčené postupy, jak to implementátorovi co nejvíce zkomplikovat, aby si užil co nejvíce srandy.

Stručně o modbusu

Než se pustíme do samotného návodu, jak co nejvíce zkomplikovat implementaci modbusu tomu, kdo to bude dělat, tak velice rychle trochu teorie. Modbus protokol existuje v několika verzích a běhá po vícero různých přenosových vrstvách. Budeme se zabírat těmi nejpoužívanějšími, tedy modbus RTU přes sériovou sběrnici RS485 a modbus TCP na ethernetu. Oboje funguje prakticky, liší se v malých detailech. Jedná se o master / slave protokol; průmyslové zařízení je slave, my co se s tím snažíme komunikovat, máme mastera. Master pošle dotaz, slave na něj reaguje a odpovídá. U modbusu je v dotazu stěžejní adresa zařízení (zejména na RS485 lze teoreticky mít více zařízení na jedné fyzické sběrnici), funkce a nějaké parametry funkce. Mezi nejpoužívanější funkce, o kterých si budeme povídat, patří read coils (01), read discrete inputs (02), read holding registers (03) a read input registers (04). Číslo v závorce udává číslo funkce, tak jak je definována ve specifikaci protokolu a tak jak je potom uvedena i v datovém paketu požadavku. Napsal jsem je schválně, protože na povídání o nich ještě za chvíli přijde řada :-) Funkce se dají rozdělit na dva typy - buď čtou bitové hodnoty (read coils a read discrete inputs), tedy hodnoty, která nabývá jen dvou stavů. Nebo čtou registry (zbylé dvě funkce) což je v terminologii modbusu 16 bitová hodnota (tedy dostanete 16 bitů hodnot, povětšinou se to pak interpretuje jako číslo :-) ). U každé této funkce se pak následně ještě ptáme na adresu, kterou čteme a počet, kolik toho chceme přečíst. Příklad: řekneme, že čteme funkcí 04 na adrese 100 a chceme 4 registry. Měly by se nám vrátit tedy input registry 100, 101, 102 a 103. A takhle stejně to funguje u všech funkcí. Funkce 01 a 02, respektive 03 a 04 jsou jinak prakticky totožné a fungují úplně stejně. Jediný rozdíl je, že specifikaci říká, že discrete input a input register jsou hodnoty pouze ke čtení, kdežto coils a holding register jsou i pro zápis (ano, modbusem se dají data nejen číst, ale i zapisovat). Pokud si vzpomeneme na náš příklad s klimatizační jednotkou, pak teplota dodávaného vzduchu z teplotního čidla by měla být v input registrech - je to měření z čidla a nic s tím nezmůžeme. Naproti tomu třeba setpoint teploty jednotky - tedy na kolik má chladit, by byl v holding registru, protože jej můžeme i zapsat a tedy jej změnit. Že to vypadá jednoduše a nedá se to zkomplikovat? Ale kdeže :-) Jdeme na to.

Než vůbec dojde na modbus...

Nejlepším způsobem, jak z modbusu nedostat žádná data, je zmrvit už přenosovou vrstvu tak, aby se zařízení nikdo na nic ptát nechtěl. Skutečně výborné možnosti nabízí varianta na sběrnici RS485. Ale i u modbus TCP varianty na ethernetu se dají komplikace vymyslet. Předně v IP světě jsme taknějak zvyklí, že zařízení po síti reagují odkudkoliv. Zařízení má IP adresu, poslouchá na síti, tak to bychom si mohli ty data číst z více systémů zároveň, že? Specifikace protokolu modbus TCP s tím dokonce počítala; respektive ona počítala i s tím, že v rámci jednoho spojení se můžeme asynchronně vícekrát ptát. Tak to ale hned zarazíme. Má zařízení už jedno aktivní připojení po modbusu? Tak další samozřejmě odmítne a nepřijme. Jelikož stejně prakticky nikdo nevyvíjí průmyslové zařízení, které by fakt komunikovalo po ethernetu a tedy modbus TCP ve finále dělá jenom nějaký převodník na nějakou obskurní sériovou sběrnici, tak když mu pošlete více dotazů naráz, tak nevrátí nic. Na sériové sběrnici by vám to přece taky neprošlo. A taky můžeme trochu prudit se securitou, to je teď v módě. Takže povinně nařídíme access listy, které nejdou vypnout a zařízení neuvedené v access listu prostě komunikovat nebude. Dá se to vylepšit ještě o to, že do access listu se musí uvést i zdrojový TCP port (ten prakticky žádný software, natož jiné průmyslové zařízení, nastavit napevno nedovolí, takže good luck!) a že master zařízení musí být ve stejné L2 síti, jako slave. Takže můžeme řikat, jak je to hrozně bezpečný a ty modbus pakety už nám nikdo ani nebude posílat, protože o to nikdo nebude stát :-) Skutečnou srandu si ale můžeme užít u RS485. RS485 nemá žádné předepsané konektory. Ke svému životu nutně potřebuje dva vodiče. Jelikož se jedná o sběrnici, je možné mít na ni připojeno vícero zařízení naráz. Defakto tedy "jedním kabelem," kdy každý vodič do jednoho zařízení vstoupí a hned z něj pokračuje zase do dalšího. Správný a nudný přístup je tedy zařízení na připojení těchto dvou vodičů vybavit třeba svorkovnicí. Do svorkovnice se může pak připojit vodič, případně rovnou dva vodiče, kde druhý pokračuje k dalšímu zařízení a ono to bude připojené správně a fungovat. Což ale nechceme :-) Takže když máme zařízení na sériové sběrnici a chceme pokud možno znemožnit to, aby na tu sběrnici někdo připojil další zařízení, tak uděláme co? Vezmeme nějaký idiotský konektor, do kterého víc jak ty dva potřebné vodiče zapojit nepůjdou! Takže například vezmeme RJ-12 a hned máme krásnou sériovou sběrnici pro jedno zařízení :-) Pak je samozřejmě nejlepší používat co nejobskurnější konektory, aby to pokud možno zapojit nešlo. Lepší než RJ-12 je tedy třeba sluchátkový RJ-11, který lze dnes sehnat pomalu už jen v muzeu. Já osobně bych se nebál jít ještě dál, RJ-50 je možná ještě lepší volba a nebál bych se to ani zapojovat na nějaký konektor pro koaxiální kabely :-) A pokud doopravdy chceme znemožnit připojení více zařízení na jednu sběrnici (zejména prodáváme-li zařízení, kterých si zákazník nejspíše koupí více a mohl by to použít), existuje ještě jeden sofistikovanější způsob - zařízení bude mít natvrdo nastavenou modbus adresu, která nepůjde změnit. U RS485 pak lze provést ještě jeden trik pro dokonalé zmatení nepřítele, tedy zákazníka :-) Ten je zejména v kombinaci s následujícími triky také velice účinný. Jak zde padlo, RS485 pro komunikaci využívá dva vodiče. Samotná komunikace je realizována měřením / nastavením napětí mezi těmito vodiči. Tedy není jedno, který je který a který se kam zapojí. Nudný způsob, jak vodiče označit, je + a -, respektive varianty jako RxTx+ a RxTx-. Znamínka se označují podle toho, jestli je zde při logické 1 kladné nebo záporné napětí. Což je moc přímočaré a moc jasné a leckdo by to pak na první pokus zapojil správně. Samotná specifikace RS485 vodiče označuje jako A a B. To už je daleko lepší. Specifikace také říká, že vodič B by měl odpovídat RxTx+. To nám ale není překážkou, kdo by dodržoval specifikace... takže podle nálady to přehodíme (anebo ne) a v dokumentaci to nijak neupřesníme :-) Osobně i zde bych šel ještě dál a udělal další vylepšení - vodiče bych označoval nějakým hieroglyfem nebo jinými znaky nějaké dávno vyhynulé obrázkové abecedy. A nijak neupřesnil :-)

Registr sem, registr tam...

Pokud implementátor navzdory veškeré snaze rozchodil komunikaci po fyzické vrstvě, začneme mu konečně házet klacky pod nohy v samotném modbus protokolu :-) Prvním základním krokem je ho zmást přímo v dokumentaci, aby vůbec nevěděl, na co se ptá. V datovém paketu modbusu jsou registry číslovány od nuly. Tedy registr s číslem nula je validní, existující registr. To je ale hrozně nepřirozené, takže proč to nečíslovat od jedničky? Tedy do dokumentace zapíšeme, že na registru 1 je něco a myslí se tím registr 0. Číslování od 1 je nicméně dneska hodně běžné, hromada softwaru ho má už v sobě a leckdo tenhle podlý trik už čeká. Ale v kombinaci s ostatním, je to stále účinné, zvláště dodržíme-li nějaké další zásady. Například tedy zásadně do dokumentace neuvedeme, které číslování jsme zvolili. A i kdyby jsme zvolili číslování od nuly, tak nultý registr vynecháme a začneme registrem číslo 1, aby to tak nevypadalo. Dále můžeme do číslování registrů vnést něco, co se čísla registru vůbec netýká, například nějaký prefix. Můžeme si teda říct, že pro naše úžasné zařízení máme prefix 35 a na 3 místa pak budeme číslovat registry. Do dokumentace tedy budeme psát čisla registrů 35001, 35002, 35003, ... a budeme tím samozřejmě myslet registry 1, 2, 3, ... Ale rozhodně to neupřesníme, ať si na to přijdou sami. Stejný princip se ještě často používá spolu s matením na použitou funkci. Tedy budeme mít třeba prefix 3 a řekneme, že ten prefix specifikuje vyčítací funkci. Když tedy v dokumentaci budeme mít registry 3001, 3002, 3003, ... leckdo by to mohl pochopit tak, že máme registry 1, 2, 3, ... a vyčítáme je funkcí read holding registers. Ale my to samozřejmě naimplementujeme tak, že registry budou fungovat pouze s funkcí read input register. Přece tam nebudeme ten prefix psát, aby bylo jasné, co tím myslíme :-) Ohledně číslování registrů je potřeba si dát také pozor ještě na jednu věc. Jak padlo na začátku, v dotazu říkáme počáteční registr, který chceme a kolik jich chceme přečíst. Teoreticky lze tedy přečíst hodně registrů zaráz a v nejhorším případě by tedy někdo mohl přečíst všechny údaje, které ho zajímají, jenom jedním dotazem. To samozřejmě nechceme, protože by to měl moc jednoduché. Takže mezi adresy registrů úmyslně naházíme registry tak, aby tam byly díry a nešlo se na stav zařízení doptat jedním dotazem. Čim víc děr, tím lépe. Pokud nepotřebujeme předávat velké množství registrů, tak je nejlepší udělat díru za každým. A s tímhle se pojí ještě jedna věc. Celkem legitimní je, když se master zeptá ne registr, který neevidujeme, mu odpovědět chybou (k tomu se také ještě dostaneme :-) ) / neodpovědět. Protože tam žádná data nejsou. My ale pro jistotu nebudeme odpovídat i ve chvíli, kdy máme pocit, že se druhá strana nezeptala na to, co chtěla. Typickým příkladem jsou více registerové hodnoty. Registr má 16 bitů, což není tak mnoho. Pokud potřebujeme uložit nějaké dlouhé nebo desetinné číslo, nebude nám to stačit. Řeší se to potom tak, že se hodnota uloží přes více (obvykle stačí 2) registry, ze kterých si to pak druhá strana složí. Takže - máme třeba registry 10 a 11, které ukrývají jednu takhle rozloženou hodnotu. Takže pochopitelně nebudeme reagovat na dotazy, které se ptají jen na registr 10 nebo jen na registr 11. My v nich sice hodnoty máme, ale jelikož přece víme, že jsou k ničemu, tak nevrátíme vůbec nic, dokud se protistrana nezeptá na oba registry naráz. Je to celkem účinná metoda, když v kombinaci s ním zmateme nepřítele, teda zákazníka, už na začátku tím, že neví, jestli počítáme od 1 nebo od 0 a může se celou dobu ptát o registr vedle. Stejným způsobem se dá ale komplikovat život ještě větším nesmyslem: rozdělíme si registry za sebe dle nějakých pro nás logických celků. A prohlásíme, že master bude číst celek zaráz, jinak nedostane nic. Můžeme mít teda registry 10 - 20 a 40 - 55, vymyslíme jim nějaká skupinová jména. A modbus bude reagovat pouze na čtení 10 registrů od 10 a 15 registrů od 40. Zeptá se někdo jenom na jeden registr s adresou 12? Nevrátíme mu nic. Že někoho zajímá jenom obsah registru 54? Smůla, přečte si všech 15. A vůbec přitom nemusí vadit, že v registrech jsou uložená nijak nesouvisející data.

Výjimky, diagnostika, ...

Modbus protokol má ve specifikaci i exceptions. Tedy když zařízení pošleme dotaz, který je špatný - ať už úplně nebo se ptáme na registr, na který nemáme nebo cokoliv, zařízení může odpovědět výjimkou. Výjimek je několik. Vhodně vrácená výjimka by tedy implementárovi měla napovědět, co udělal špatně. Takže když už se obtěžujeme vracet nějakou výjimku, tak zásadně vracíme nějakou úplně jinou, která se daného problému netýká. Pokud tedy přišel dotaz na špatný registr, nevrátíme illegal data address, ale lépe třeba slave device failure. Ovšem ve všech případech je vracení výjimek změkčilost. Nejlepší je nevracet vůbec nic. Když vrátíte výjimku, nepřítel se dozví, že ty dráty na 485ce zapojil správně.

Coily, input, holding... není to jedno?

Jak jsem na začátku uvedl, nejpoužívanější funkce jsou read coils, read discrete inputs, read holding registers a read input register. Přičemž první dvě operují s 1 bitovými hodnotami, druhé dvě s 16 bitovými (registry). Nicméně je dosti zbytečné používat funkce, které protokol umožňuje, když to můžeme nabastlit i tou druhou funkcí. Nejtypičtějším příkladem je ignorace coilů a discrete inputů a naproti tomu rvaní bitových proměnných do registrů. Takže - máme zařízení, které vrací třeba 30 nějakých stavových (1 bit) hodnot - jako jestli je v alarmu a podobně. Dalo by se na to tedy použít 30 discrete inputů, které jsou na to určeny. Ale registr má také 16 bitů, že? Takže to místo toho nacpeme do dvou registrů. Tohle už dneska rozhazuje málokoho - dělá se to tak často, že už většina softwaru si to umí rozkládat a skládat sama. Ale pořád i dnes se objeví nějaký vtipný bug - například software ten jeden registr vyčte 16x místo jednou (protože z něj bere 16 hodnot). A ikdyž to umí, tak často je potřeba ty hodnoty pořád rozkládat nějak ručně. Zajímavou alternativou, která kupodivu ještě nikoho nenapadla, ačkoliv je dle mě geniální, je to otočit a naopak do discrete inputů ukládat hodnoty registrů. Jen ať si to jedno číslo skládají ze 16 výstupů :-) Šetřit počet registrů se ale dá i jiným dalším zábavným způsobem. Máme nějaký číselný status od 1 do osmi a pak teplotu, která nabývá v nízkých hodnot? Tak hurá, frcneme to do jednoho registru - první čtyři bity je status, zbývajících 12 teplota. Možná ještě lepší by to bylo udělat na přeskáčku - dát status na první dva a poslední dva bity a teplotu doprostřed :-) Další poměrně častá je záměna coilů vs discrete inputů a input vs holding registrů. Specifikace říká, že coils a holding registry by měly být ty, které lze i zapisovat a měnit, kdežto discrete input a input registry jsou pouze ke čtení. Vzhledem k tomu, že stejně půlka zařízení vrací hodnoty typu teplota v holding registrech, tak je to už dnes jedno, stejně si nikdo nepamatuje který typ měl být k čemu...

Když 16 bitů nestačí...

16 bitů není mnoho a mnohdy skutečně nestačí na data, která bychom chtěli vracet. Typicky čísla v nějakém větším rozsahu nebo desetinná čísla. Běžnou praxí teda je tuhle jednu hodnotu rozložit do více registrů - obvykle stačí 2, čímž získáme 32 bitů. Zde ale nastává spousta prostoru, jak to zkomplikovat. Předně pokud nepřítel neví, jestli nečte registry o 1 vedle, tak to ani nemusí zjistit, pokud jsou registry dostatečně na sobě nahuštěny. Dále nastává zábava, jak vůbec hodnoty ze dvou registrů složit za sebe. Modbus dle specifikace pro datové pakety používá big endian. Ale to se týká té hlavičkové části. Není tedy žádný důvod, proč by naše data ve dvou a více registrech neměla být uspořádána dle little endian. A pochopitelně v dokumentaci se o tom vůbec nezmiňujeme. Dále pokud obsahem registrů je něco složitějšího, jako třeba desetinné číslo, lze jej uložit taky vícero způsoby. Ať už vyberem jakýkoliv, rozhodně o tom do dokumentace nic nepíšeme. Ono totiž dostat ze dvou registrů hodnotu desetinného čísla takovou, jaká by měla být, je ve finálně úplně stejná zábava, jako zapojit správně neoznačené vodiče na RS485, uhodnout rychlost sériové linky a vymlátit z toho nějaké data, když to nevrací jedinou výjimku! :-)

Na závěr

Zní tento blog jako nějaký sarkasmus, či vtip? No, možná zní... a možná by to i vtipné bylo, kdyby každá z popisovaných vlastností neexistovala v nějakém zařízení, které jsem měl v ruce. Vlastně bych asi musel dlouze přemýšlet, jestli jsem někdy viděl takové zařízení, které by nemělo alespoň jednu ze zmíněných dobrých vlastností. A vždycky, když si myslím, že už mě nic překvapit nemůže, tak se objeví nějaká další perla. Takže možná ještě jednou bude i pokračování :-)