A számítógépek esetén alapvetően kétféle adatátviteli mód létezik: soros és párhuzamos. Párhuzamos kommunikáció esetén 8 bit átviteléhez 8db vezeték kell, amelyek egyszerre váltanak és így reprezentálnak egy bájt adatot.
A másik megoldás a soros adatátvitel, amihez elég két vezeték is. Itt a legegyszerűbb esetben egy adat és egy órajel vezetékünk van. Az órajel váltakozása jelzi a fogadónak, hogy a következő bit ki lett küldve és az adatvezetékre pedig a küldendő bájt bitjeit tesszük egyesével.
Mindkét módszernek megvan a maga előnye és hátránya. A párhuzamos kommunikáció gyorsabb tud lenni, ha órajel korlátunk van, mivel a 8 bites példánál maradva ugyanakkora órajel mellett párhuzamosan 8x annyi adatot tudunk kiküldeni, mint soros módon. Azonban ez a módszer nem skálázható és alkalmazható egy bizonyos sebesség felett, mert a vezetékek közötti áthallás komoly probléma kezd lenni. Éppen ezért szinte az összes modern kommunikációs busz (USB, PCI-E, HDMI, Display Port, SATA) soros átvitelt alkalmaz, mert az órajel manapság már nem akkora nagy probléma.
A fenti soros buszrendszerek jellemzője, hogy szabványos a protokolljuk azért, hogy a rendszerek könnyen tudjanak egymással kommunikálni. A könyv ezen szekciójában azonban nem ezekről lesz szó, hanem a PC eredeti soros portjáról. A PC a kezdetektől rendelkezett egy soros porttal, ami manapság már sok számítógépen fizikailag nem is található meg, de ez nem jelenti azt, hogy halott! Rengeteg eszköz USB felett még a mai napig is az eredeti soros port protokollján beszélget a számítógéppel.
Ennek az oka az, hogy ezen soros kommunikáció esetén nem sok minden van a szabványba rögzítve, így igencsak univerzális, testreszabható kommunikációs formát kapunk.
A Serial kommunikáció sebességét baudban szokták megadni. A baud az egy másodperc alatt átvihető bitek számát adja meg. Ebből lehet bit/s, vagy byte/s sebességet számolni, azonban a szabvány elég megengedő, így a byte fogalma Serial kommunikáció esetén lehet 7, 8 vagy akár 9 byte is, illetve minden egyes byte egy hosszabb start bit-tel és egy időben hosszabb stop bit-tel végződik. Vagyis végeredményben a sebesség nagymértékben függ a kommunikációs beállításoktól.
A baud ráták a kommunikáció esetén szabványosítva vannak. A sebesség meghatározása a küldő és fogadó fél között nem automatikus, manuálisan kell konfigurálni. Ha a küldő és fogadó fél nem azonos sebességen működik, akkor a kommunikáció félremegy és szétcsúszik. Ennek detektálására és javítására nem tartalmaz beépített megoldást a protokoll.
A szabványos baud ráták, amiken eszközök tudnak egymással kommunikálni: 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 31250, 38400, 57600, vagy 115200 baud.
Programozás
.NET esetén a soros port kommunikációt a SerialPort osztály valósítja meg, ami a System.IO.Ports névtérben található. Az alábbi kódrészlet egy C# konzolos program és egy Arduino sketch közötti kommunikációt mutat be:
Arduino kód:
//fogadó buffer és méretének definiálása
#define BUFFER_SIZE 16
char serialBuffer[BUFFER_SIZE];
void setup() {
// serial sebesség konfigurálása
Serial.begin(115200);
while (!Serial) {
; // natív usb portos vezérlők miatt várakozás
}
}
// Bufferba olvasunk maximum 16 byte-ot
// Ha több adat jön, akkor kiolvassuk a hardver bufferből
// de eldobjuk.
void ReadToBuffer(uint8_t size) {
uint8_t index = 0;
bool ended = false;
while (Serial.available() > 0) {
if (index < BUFFER_SIZE && !ended) {
uint8_t got = Serial.read();
serialBuffer[index] = got;
ended = got == '\0';
index++;
delay(1); //várakozás, hogy fel tudja dolgozni az adatot
} else {
Serial.flush();
}
}
}
void loop() {
if (Serial.available() > 0) {
ReadToBuffer(BUFFER_SIZE);
if (strncmp(serialBuffer, "hello", BUFFER_SIZE) == 0) {
Serial.println("Hello from Arduino");
}
else
{
Serial.print("Unknown command: ");
Serial.println(serialBuffer);
}
}
}
Az Arduino kód egy 16 karakter méretű bufferbe fogad karaktereket a PC-től. Amennyiben a fogadott karakterek megegyeznek a "hello" szöveggel, akkor a lap visszaválaszol egy "Hello from Arduino" szöveget. Ezzel fogom szemlélteni, hogy hogyan kell küldeni és fogadni adatot a C# oldalon:
using System;
using System.IO.Ports;
using System.Text;
using System.Threading.Tasks;
namespace SerialProgram
{
internal sealed class SerialWaiter : IDisposable
{
private readonly StringBuilder _buffer;
private readonly SerialPort _port;
public SerialWaiter(SerialPort port)
{
_buffer = new StringBuilder();
_port = port;
_port.DataReceived += PortDataRecieved;
}
private void PortDataRecieved(object sender, SerialDataReceivedEventArgs e)
{
_buffer.Append(_port.ReadExisting());
}
public async Task<string> ReadLineAsync()
{
while (waited < _port.ReadTimeout)
{
if (_buffer.Length > 0
&& _buffer[_buffer.Length - 1] == '\n')
{
break;
}
await Task.Delay(1);
}
return _buffer.ToString();
}
public void Dispose()
{
_port.DataReceived -= PortDataRecieved;
}
}
internal static class Program
{
private static async Task Main(string[] args)
{
Console.WriteLine("Elérhető portok:");
foreach (var port in SerialPort.GetPortNames())
{
Console.WriteLine(port);
}
Console.Write("Használni kívánt port neve: ");
string? selectedPortName = Console.ReadLine();
using (SerialPort serialPort = CreateSerialForAduino(selectedPortName, 115200))
{
serialPort.Open();
using (var waiter = new SerialWaiter(serialPort))
{
serialPort.Write("hello");
var response = await waiter.ReadLineAsync();
Console.WriteLine(response);
}
}
}
private static SerialPort CreateSerialForAduino(string? portName, int baudRate)
{
if (string.IsNullOrEmpty(portName))
throw new ArgumentException("Nem megfelelő soros port");
return new SerialPort
{
PortName = portName,
BaudRate = baudRate,
WriteTimeout = 1000,
ReadTimeout = 1000,
DataBits = 8,
StopBits = StopBits.One,
Parity = Parity.None,
RtsEnable = false,
DtrEnable = false,
};
}
}
}
A C# kód működéséhez először szükségünk lesz aSystem.IO.Ports névtérre. Ez .NET Framework alkalmazásoknál része a keretrendszernek, míg .NET alkalmazások esetén külön telepítenünk kell a System.IO.Ports NuGet csomagot, ami a megfelelő operációs rendszer absztrakciókat hozza magával.
A csomag fő osztálya a SerialPort. Ez írja le a soros portot. Az osztály GetPortNames() statikus metódusával lekérdezhetjük a rendszeren elérhető soros portok neveit. Általában a COM1 és a COM21 a PC-k esetén a fizikai DB-9-es csatlakozós soros port számára fenntartott nevek. Ebből adódóan az USB feletti soros portok nevei minimum COM3-tól indulnak.
Az osztály példányosításával konfiguráljuk a kommunikációt. Ezt a kódban a CreateSerialForAduino() metódus végzi, hogy elkülönüljön ez a logika a kód többi részétől.
A konfiguráció során olyan tulajdonságokat kell beállítanunk, mint a Baud Ráta (BaudRate), a port neve (PortName), az adatbitek és a stop bitek száma (DataBits és StopBits), paritás2 (Parity), illetve hogy aszinkron vagy szinkron módon kommunikálunk.
Az aszinkronitást az RtsEnable befolyásolja. Ez a kódban false-ra van állítva, mivel az Arduino és a legtöbb hardver az aszinkron módot alkalmazza. Ennek oka, hogy szinkron módban további két vezeték volt szükséges és ebből adódóan csak speciális esetekben használták. A DtrEnable tulajdonság a Data Terminal Ready vezérlőjel küldését befolyásolja. Ezt leginkább anno modemek használták és azt jelzi a fogadónak, hogy a hardver készen áll a kommunikációra.
Írni a StreamWriter esetén bemutatott Write metódussal tudunk. Sajnos Task alapú aszinkron módra nincs lehetőségünk, mivel ez az API a .NET 1.0-ban mutatkozott be. Adat érkezéskor a DataReceived esemény lövődik el. A kódban a SerialWaiter osztály ezt csomagolja be olyan módon, hogy aszinkron módon várakozni tudjunk egy Task segítségével.
-
A legtöbb PC alaplapon a mai napig megtalálható a soros port (COM1) fizikailag egy belső csatlakozósoron, ami kivezethető egy csatlakozóval. Ennek a kivezetéséhez szükséges kábel az otthoni felhasználásra készülő alaplapoknak nem része, de külön megvásárolható. Ennek oka az, hogy otthoni felhasználóknak nem igen van szüksége erre már legalább a kétezres évek második felétől.↩
-
A paritásbit az informatikában egy n bitből álló adatfolyam (n+1)-edik ellenőrző, hibajelző bitje, amivel egy bájt átvitele során egy bithiba detektálható. Az átvitt byte bitjeiben megszámoljuk az egyesek számát és ezek párossága alapján 0 (
Odd), 1 (Even) vagy fixen 0 (Space) vagy 1 (Mark) értéket küldünk konfigurációtól függően.↩