Soros port kezelése Linux alól
avagy hogyan fűzzünk RS-232/485-ös buszt a kedvenc operációs rendszerünket futtató számítógépre?
Előbb-utóbb legtöbbünk találkozik azzal a problémával már az egyetem alatt is, hogy a PC soros portján kell kommunikálni valamilyen eszközzel, legyen az valami régebbi device, mikrokontroller vagy egy "terepi" buszrendszer. Ezeknél az alkalmazásoknál általában magára hagyott rendszerekről van szó, ahol nincs állandó emberi felügyelet a számítógépünk mellett, esetleg a PC mintegy "beágyazott" számítógépként működik valami nagyobb eszközben. Nagyon jól jön az ilyen problémák megoldásánál a Linux közmondásos megbízhatósága. Pár tízezer forintból összerakhatunk egy olyan PC-t, amiben egyetlen mozgó alkatrész sincsen (például ha flash-memóriát használunk merevlemez helyett), és az így összerakott rendszert nyugodtabb szívvel hagyhatjuk magára.
A soros port programozása nem valami nehéz dolog Linux alól, de mégis sokat lehet vele dolgozni, mire működőképes rendszert alakíthatunk ki, mivel nem valami jól dokumentált a probléma. Elsődleges forrásunk legyen a Serial HowTo, és olvassuk el a (kissé hiányos) Serial Programming HowTo-t is, aminek ez a rövid cikk egyfajta kiegészítése lenne.
A soros port nem valami nagy teljesítményű, modern interface, azonban a mai napig elég elterjedt. (Egyik tanárunk szavaival élve: az iparban "a soros port körül forog a világ".) Ha a világ nem is forog talán körülötte, néha azért célravezető lehet a használata; például ha több mikrokontrollert szeretnénk mondjuk egy épületen belüli hálózatba kötni, akkor megfontolandó, hogy esetleg RS485-ös buszrendszert használjunk. És a PC soros portja egy – a sarki boltban megvásárolható – interface-szel RS485-re kapcsolható.
Az (EIA) RS485 aszinkron busz half-duplex változata egy csavart érpáron "fut". Az érpáron a feszültség szimmetrikus, ezáltal kevésbé érzékeny a zavarjelekre. Az átviteli sebesség a kábelhossz függvénye, de maximum 10Mbit/sec a szabvány szerint (bár megfelelő chipsettel elérhető akár 25 Mbit/sec is). Egy (maximálisan 1 km hosszú) kábelre legfeljebb 32 eszköz fűzhető fel. Ahhoz, hogy PC-ről erre a buszra csatlakozzunk, rögtön adódik, hogy a soros porton keresztül tegyük ezt.
Fontos megjegyezni, hogy az RS485 nem tartalmaz semmi előírást sem a protokollra. Tehát elvileg akármilyen szabványt alkalmazhatnánk az adatok forgalmazására.
Az RS-232 (szabványos nevén EIA-RS-232) nagyon ősi protokoll. Két végpont összekötésére használhatjuk alacsony sebességgel, rövid kábellel. Bár full-duplex kapcsolatot is lehetővé lesz, mi most a half duplex kapcsolatot fogjuk használni, azaz egy időben csak egy irányban kommunikálhatunk. Minden jelet külön vezeték továbbít, és az adatot kódoló feszültséget a földhöz viszonyítjuk. Aszinkron buszról van szó itt is, tehát nincs órajel.
A főbb továbbított jelek a következők:
GND A föld.
TXD Transmitted Data, vagyis a leadott adat, feszültségtől függően 0 vagy 1 az értéke.
RXD Received Data, vagyis a vett adat.
DCD A Data Carrier Detect azt jelöli, hogy össze van-e kötve a két végpont. Nem mindig használják.
DTR A Data Terminal Ready értéke 1, ha a számítógép kész a kommunikációra, tehát általában a soros port megnyitásától igaz.
CTS Clear To Send, a kábel másik oldalán lévő számítógép ezzel jelzi, hogy készen áll arra, hogy mi elkezdjünk adni.
RTS Request To Send, ezzel jelzi a számítógépünk, hogy készen állunk az adásra.
Fontos megjegyezni, hogy az RTS és a CTS , az un. "flow control" jelzőbitek a legtöbb esetben mindig nullára vannak állítva. Azonban ha RS-485-höz akarunk hozzáférni, akkor muszáj használnunk ezeket a jeleket is, mert több RS232/485 konverter is a CTS/RTS változását figyelve működik.
Nézzük meg, hogy milyen módon megy át egy byte a soros porton!
| Start Bit | Adatbitek | Paritásbit | Stopbit |
A startbit feladata az aszinkron kommunikáció időzítése. Értéke mindig 1, a stopbit pedig 0. Paritásbit helyett használhatunk két stopbitet is. Az adat lehet 7 vagy 8 bites. Mivel ez egy nagyon alacsonyszintű protokoll, mind a fogadó, mind a küldő félnek tudnia kell, hogy hány adatbitet küld, páros vagy páratlan paritást használ-e, esetleg két stopbitet küld.
Szerencsére ezeket a feladatokat elvégzi helyettünk a számítógépünk UART chipje, és a kommunikáció alatt nekünk csak az adatbyte-ok küldésével és fogadásával kell foglalkoznunk. Az UART felprogramozása nem valami nehéz feladat egy egységsugarú mikrokontroller esetében, azonban Linux alatt az egész meglehetősen el van bonyolítva. Azért nehezítették meg ennyire a dolgunkat, mert a POSIX-os srácok alapvetően arra készülhettek, hogy mi a soros porton egy terminált akarunk majd a számítógéphez kapcsolni, és nem pedig, teszem azt, mikrokontrollereket.
Szóval soros kommunikáció Linux alatt. Ugye UNIX alatt majdnem minden file. Linux esetében a soros port a /dev/ttySx néven alatt írható vagy olvasható, ahol x egy tetszőleges természetes szám a COM portjaink számától függően. Ahhoz, hogy látszódjon a ttySx a /dev/ alatt, be kell hogy töltsük a serial.o kernel modult például a modprobe serial paranccsal. A /dev/ttySx az első írás vagy olvasás alkalmával jön létre. Alapbeállításban csak a root férhet hozzá, tehát gondoskodnunk kell róla, hogy minden rendszerboot után megfelelő jogosultságokat kapjon, vagy pedig rendszergazda jogokkal kell futtatnunk a soros-portot használó programunkat.
Vágjunk a dolgok közepébe, és nyissuk meg a soros portot egy egyszerű C program segítségével!
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
[ ... ]
int serialFd;
struct flock serialFlock;
if((serialFd = open("/dev/ttyS0", O_RDWR | O_NOCTTY)) < 0)
return ERROR;
serialFlock.l_type = F_WRLCK;
serialFlock.l_whence = SEEK_SET;
serialFlock.l_start = 0;
serialFlock.l_len = 0;
if (fcntl(serialFd, F_SETLK, &serialFlock) < 0)
return ERROR;
Az open()-nel megnyitjuk a portot, read-write módban. "Fel kell készülni mindenre" alapon az O_NOCTTY flag biztosítja, hogy ezen a device-on nem futhat a processzt kontrolláló terminál.
Az fcntl() függvénnyel megadunk néhány attribútumot, pontosabban a processzünk egy write lock-ot rak a device-ra. (Akit bővebben érdekel a lockolás kérdése, az tanulmányozza át a man fcntl oldalt.) Innentől a soros portot a serialFd nevű file-leiírón keresztül érhetjük el programunkban.
termios t;
int serialStatus;
if (tcgetattr(serialFd, &t))
return ERROR;
ioctl(serialFd, TIOCMGET, &serialStatus);
A tcgetattr() függvénnyel lekérdezzük az eredeti állapotát a soros port tulajdonságait meghatározó rekordnak, hogy csak a fontos részeket módosítsuk a továbbiakban.
t.c_iflag &= ~(ISTRIP | IXON | IXOFF | INLCR | ICRNL);
t.c_iflag |= IGNBRK | CRTSCTS ;
t.c_oflag &= ~OPOST;
t.c_cflag &= ~(CSTOPB | CSIZE | PARODD); //EVEN paritas
t.c_cflag |= (CLOCAL | CREAD | CS8 | PARENB);
t.c_lflag &= ~(ICANON | ECHO | ISIG);
t.c_cc[VMIN] = 0 ;
t.c_cc[VTIME] = 1 ;
cfsetispeed(&t, B57600);
tcsetattr(serialFd, TCSAFLUSH, &newt);
A termios flagek beállításánál lehet nagyon elszállni a programozásban. A termios rekord egy kifejezetten soros port tulajdonságainak felprogramozására létrehozott adattípus. Ajánlott a man cfsetispeed beható tanulmányozása. Azonban ez egy igen unalmas manpage; így lusták kedvéért a következő két táblázatban itt le van írva, hogy miket érdemes feltétlenül beállítani. (Ez a flagbeállítás eddig minden PC-n működött, amivel próbáltam.)
Érdekes dolog a beállítandó dolgok közül a termios::c_cc tömbje. Itt két fontos dolgot lehet elrontani: a VMIN és a VTIME értékét. A VMIN meghatározza, hogy minimum hány karaktert olvassunk be. Ha 0-ra rakjuk, akkor a VTIME egy timeout-ot állít be – állítólag – tizedmásodpercben. Ezzel a két értékkel ízlés szerint nagyon sokat lehet trükközni, de ebben a cikkben ez sajnos nincs leírva. A Serial Programming HowTo és a megfelelő man oldalak részletesen leírják a működésüket, azonban nekem ez a beállítás volt a legpraktikusabb.
A cfsetispeed() beállítja a sebességet, itt 5.76Kbaudra, a tcsetattr() pedig alkalmazza a beállításainkat.
A kikapcsolt flagek jelentése a következő:
ISTRIP 8. bit figyelmen kívül hagyása.
IXON,IXOFF XON/XOFF típusú flow control engedélyezése a kimeneten/bemeneten.
INCLR Bemenetnél NL karakter helyett küldjünk CR-t.
ICRNL Bemenetnél a CR karaktert kicseréli NL-re.
OPOST Implementációfüggő kimenet-feldolgozás engedélyezése*.
CSTOPB Két stopbit használata.
CSIZE Karakterméret maszkot fogunk használni.
PARODD Páratlan paritás vizsgálata.
ICANON Kanonikus mód engedélyezése. Ez akkor hasznos, ha terminalt valósítunk meg a soros porton.
ECHO Echozza a bejövő karaktereket.
ISIG Bizonyos karakterek érkezésekor bizonyos szignált generál.
A bekapcsolt flagek jelentése:
IGNBRK Break feltétel mellőzése a bemeneten.
CRTSCTS Engedélyezzük a CTS/RTS (flow control) vezetéket. A legtöbb RS485 konverter használata esetén kötelező.
CLOCAL Mellőzzük a modemvezérlő sorokat.
CREAD Engedélyezzük a vételt.
CS8 8 bites adatbyte-okat használunk.
PARENB Engedélyezzük a paritás generálást/ellenőrzést
Most, hogy kész az inicializálás, túl is vagyunk a nehezén, kezdhetünk végre programozni.
serialFile = fdopen(serial_fd,"rw");
if (serialFile==NULL) {
printf("Nem tudom megnyitni a serial portot. n");
return ERROR;
}
int serialStatusRead, serialStatusWrite;
serialStatusRead = serialStatusWrite = serialStatus;
serialStatusWrite &= ~TIOCM_DTR;
serialStatusWrite |= TIOCM_RTS;
serialStatusRead &= ~TIOCM_RTS;
serialStatusRead |= TIOCM_DTR;
unsigned char buf[8];
unsigned char be[8];
//
// Erteket adunk buf[8]-nak.
// [ .. ]
//
// Iras
ioctl(serialFd, TIOCMSET, &serialStatusWrite); //FONTOS
tcflush(serialFd, TCOFLUSH);
write(serialFd,buf,8);
usleep(3); //FONTOS
// Olvasas
ioctl(serialFd, TIOCMSET, &serialStatusRead); //FONTOS
tcflush(serialFd, TCIFLUSH);
res = fread(&be,1,8,serialFile);
if (res>0) {
//Kiertekeljuk a beolvasott byte-okat...
}
A vicc kedvéért először nyissuk meg a portot file-ként is. Utána definiálunk pár státuszváltozót, majd kiírjuk a buf[] nyolc tetszőleges byte-ját a soros portra. Utána pedig beolvassuk, hogy mit írt valaki válaszul, bárki is legyen a vonal végén.
Nézzük gyorsan végig, hogy mi az írás folyamata! Az ioctl() segítségével felkészítjük az eszközt az írásra, a megfelelő flag beállításával. A tcflush() parancs "flussol", vagyis kiírja/beolvassa a bufferben várakozó, ki nem írt/be nem olvasott adatokat. A write parancs működéséhez nem lehet túl sokat hozzáfűzni.
Az olvasás nagyon hasonlóan történik. A példánkban maximum 8 byte-ot olvasunk be. A fread a beolvasott byte-ok számával tér vissza, tehát a timeout lejárása után mindenképpen visszakapjuk – az esetleges sikertelen olvasás esetén változatlan tartalmú – be[] paramétert.
Érzékeny jószág a soros port, ezért nagyon fontos, hogy hagyjunk időt az írásra a device-t kezelő chipnek. Ha hamarabb kezdünk olvasni, minthogy az eszköz befejezte volna az írást, akkor megzavarhatjuk portunk nőiesen érzékeny lelkivilágát, és kommunikációs kísérletünk eredménytelenül zárul. Erre való az usleep() függvényhívás a kódban. Természetesen változó hosszúságú bufferek írásakor a várakozás időtartamára vigyázni kell. Ez a várakozás egyébként annyira lényeges, hogy nélküle a kis programrészlet egyáltalán nem működik!
Remélem, hogy ez a rövid cikk segítségére lesz azoknak, akik esetleg abba a problémába ütköznek, hogy Linux alatt kell sorosan kommunikálniuk, és az itt közölt kis forráskód sok bosszúságtól kíméli majd meg őket.
U.i.: Köszönet a linux++@mlf.linux.org levelezőlista tagjainak, akik segítettek a bajban, amikor a flagek igencsak összezavarodtak.