SerialPort i nasłuchiwanie w C#

W poprzedniej notce zaprezentowałem prosty emulator Modbus, który niedawno napisałem. W tej notce postaram się napisać w jaki sposób zaimplementowałem obsługę portu szeregowego COM oraz nasłuchiwanie nieblokujące aplikację.

Zaczynając od początku, SerialPort jest klasą z .NET Framework, która umożliwia obsługę portów szeregowych. Na początek przykład, w jaki sposób pobrać listę dostępnych w komputerze portów:

string[] ports = SerialPort.GetPortNames();

I tyle, tablica stringów zawiera nazwy dostępnych portów. Ważniejszym krokiem jest ustanowienie połączenia (tu przykładowe dane):

SerialPort sp = new SerialPort();
sp.ReadTimeout = 1000;
sp.WriteTimeout = 1000;
sp.PortName = "COM1"	// Jeden z wylistowanych portów
sp.BaudRate = 115200;
sp.DataBits = 8
sp.Parity = Parity.None;
sp.StopBits = StopBits.One
sp.Encoding = Encoding.BigEndianUnicode;
 
try
{
    sp.Open();
}
catch (Exception err)
{
    /* ... */
}

Skoro port został otwarty można już coś do niego wysłać:

byte[] data = /* jakieś dane */
sp.Write(data, 0, data.Length);

Odbieranie danych wygląda analogicznie:

byte[] data = null;
if (sp.BytesToRead > 0)
{
    data = new byte[sp.BytesToRead];
    sp.Read(data, 0, data.Length);
}

Troszkę większym problemem jest sprawienie, by aplikacja oczekiwała na nowe dane, a w razie czego je obsłużyła, nie powodując jednocześnie zastoju. Do tego celu należy użyć wątków, a dokładniej jednego, w którym należy wywołać taką funkcję:

private boolean bReceiving = false;
 
public void Receive()
{
    byte[] data;
    while (bReceiving)
    {
        if (sp.BytesToRead > 0)
        {
            data = new byte[sp.BytesToRead];
            sp.Read(data, 0, data.Length);
            receiveHandler.DataReceivedEvent(data);
        }
 
        Thread.Sleep(50); // Żeby nie zużywać niepotrzebnie zasobów
    }
}

Funkcja ta, jak widać zawiera pętle, która sprawdza czy istnieją dane do odczytu, jeśli tak to je odczytuje i przekazuje dalej do metody, która z nich skorzysta. Kod wywołania wątku oraz interfejs receiveHandler jest następujący:

interface IDataReceiveListener
{
    void DataReceivedEvent(byte[] data);
}
 
Thread transmissionThread = null;
 
void Run()
{
    bReceiving = true;
    transmissionThread = new Thread(() => { Receive(); });
    transmissionThread.Start();
}

Teraz najważniejsze, ponieważ korzystam w tym kodzie z dwóch wątków (głównego + dodatkowego), w tym miejscu należy zwrócić szczególną uwagę na dostęp do danych prze oba wątki. W mojej aplikacji założyłem, że drugi wątek obsługuje obiekt SerialPort (mimo że tworzę go w pierwszym wątku), zatem dostęp do tego obiektu z wątku głównego (tutaj w przypadku wysyłania) musi być opatrzony sekcją krytyczną:

Thread.BeginCriticalRegion();
 
sp.Write(data, 0, data.Length);
 
Thread.EndCriticalRegion();

Natomiast jest jeszcze przypadek gdy wątek drugi odbiera dane i odwołuje się do obiektów z wątku pierwszego. W tym przypadku należy skorzystać funkcji Invoke() klasy Form, która przyjmuje obiekt typu delegate, a jego zadaniem jest wywołanie odpowiedniej funkcji z wątku pierwszego. Kod który to robi jest następujący:

private delegate void ProcessRequestDelegate(byte[] data);
 
/* funkcja interfejsu IDataReceiveListener */
public void DataReceivedEvent(byte[] data)
{
    this.Invoke(new ProcessRequestDelegate(this.ProcessRequest), new object[] { data });
}
 
private void ProcessRequest(byte[] data)
{
    // some stuff
}

I to wszystko na ten temat. Jak widać pisanie aplikacji okienkowych w C# jest banalnie proste.

Modbus Emulator

modbusEmulatorJednym z ostatnich zadań na laboratorium Informatycznych Systemów Sterowania (i jednocześnie jedynym ciekawym) było zaimplementowanie protokołu Modbus w postaci aplikacji emulujących urządzenia master i slave, komunikujących się ze sobą bezpośrednio korzystając z portów szeregowych COM (RS-232). Zadanie to zrealizowałem minimalistycznie tworząc jedną aplikację, która emuluje oba urządzenia i implementuje 6 pierwszych publicznych funkcji wraz z podglądem rejestrów.

Aplikacja jest napisana w C# i korzysta z Windows Forms oraz klasy SerialPort (która załatwia całą komunikację po porcie COM). Całość implementacji bazuje na specyfikacji Modbusa znajdującej się tutaj. Oprócz tego podczas pisania wykorzystywałem również kilka aplikacji:

Dwie ostatnie aplikacje posłużyły mi jako referencja implementacji protokołu, ponieważ sama specyfikacja nie zawiera informacji o sposobie liczenia CRC i LRC, więc trzeba było do tego dojść metodą prób, błędów i Google.

Sam program obsługuje (jak już wcześniej wspomniałem) 6 pierwszych, publicznych funkcji protokołu Modbus dla obu urządzeń (Master i Slave) przesyłając dane w trybie ASCII lub RTU. Urządzenie Slave posiada podgląd rejestrów Coils, Discrete Inputs, Input Registers oraz Holding Registers, po 4096 sztuk każdy (wszystkie Read-Only, generowane na podstawie seed = 0). Aplikacja posiada również podgląd wysyłanych i otrzymywanych ramek w postaci logu transmisji.

Program wraz z kodem źródłowym znajdują się tutaj (wymaga .NET Framework 2.0) .