Optimisation de la réception de données série en temps réel avec C#

L'utilisation de la classe SerialPort en .NET pour la communication industrielle ou robotique exige une architecture robuste. La réception de flux de données en continu ne se résume pas à lire une chaîne de caractères ; elle nécessite une gestion fine de l'asynchronisme, de l'intégrité des trames et de la synchronisation des threads.

Architecture recommandée pour un gestionnaire de port série

Pour garantir une performance optimale, il est préférable de séparer la lecture brute de la logique métier. Voici une implémentation utilisant un tampon binaire pour éviter les problèmes d'encodage liés aux caractères spéciaux.

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;

public class UartDataController : IDisposable
{
    private SerialPort _comPort;
    private readonly List<byte> _inputBuffer = new List<byte>();
    private readonly object _syncLock = new object();

    public event Action<byte[]> PacketValidated;

    public void OpenConnection(string port, int speed = 115200)
    {
        _comPort = new SerialPort(port, speed)
        {
            Parity = Parity.None,
            DataBits = 8,
            StopBits = StopBits.One,
            ReadBufferSize = 8192,
            ReceivedBytesThreshold = 1
        };

        _comPort.DataReceived += HandleDataArrival;
        _comPort.Open();
    }

    private void HandleDataArrival(object sender, SerialDataReceivedEventArgs e)
    {
        if (e.EventType == SerialData.Eof) return;

        int bytesToRead = _comPort.BytesToRead;
        byte[] tempArray = new byte[bytesToRead];
        
        _comPort.Read(tempArray, 0, bytesToRead);

        lock (_syncLock)
        {
            _inputBuffer.AddRange(tempArray);
            AnalyzeBuffer();
        }
    }

    private void AnalyzeBuffer()
    {
        // Exemple : Recherche d'un paquet délimité par 0x02 (STX) et 0x03 (ETX)
        while (_inputBuffer.Contains(0x02) && _inputBuffer.Contains(0x03))
        {
            int startIdx = _inputBuffer.IndexOf(0x02);
            int endIdx = _inputBuffer.IndexOf(0x03);

            if (endIdx > startIdx)
            {
                int length = endIdx - startIdx + 1;
                byte[] packet = _inputBuffer.GetRange(startIdx, length).ToArray();
                _inputBuffer.RemoveRange(0, endIdx + 1);

                PacketValidated?.Invoke(packet);
            }
            else
            {
                // Nettoyage si le délimiteur de fin arrive avant le début
                _inputBuffer.RemoveRange(0, endIdx + 1);
            }
        }
    }

    public void Dispose()
    {
        if (_comPort != null)
        {
            _comPort.DataReceived -= HandleDataArrival;
            if (_comPort.IsOpen) _comPort.Close();
            _comPort.Dispose();
        }
    }
}

Considérations techniques essentielles

1. Fragmentation des données

L'événement DataReceived ne garantit jamais la réception d'un message complet. Le protocole TCP/IP ou série peut fragmenter une trame de 20 octets en deux événements de 10 octets. Votre code doit impérativement accumuler les octets dans un tampon (buffer) global jusqu'à ce qu'une condition de fin (délimiteur ou longueur fixe) soit remplie.

2. Concurrence et accès à l'interface utilisateur

L'événement de réception s'exécute sur un thread secondaire géré par le pool de threads du système. Toute tentative de modification directe d'un composant UI (comme une TextBox ou un DataGrid) provoquera une exception de type Cross-thread operation not valid.

// Solution pour WinForms
this.Invoke(new MethodInvoker(() => {
    statusLabel.Text = "Données reçues à " + DateTime.Now.ToLongTimeString();
}));

// Solution pour WPF
Application.Current.Dispatcher.Invoke(() => {
    logView.Items.Add("Nouveau paquet");
});

3. Gestion du débordement du tampon d'entrée

Si le matériel envoie des données à une fréquence très élevée (ex: capteur 1kHz) et que le traitement est lourd, le tampon interne du port série (4096 octets par défaut) peut saturer.

  • Augmentez ReadBufferSize si nécessaire.
  • Utilisez une BlockingCollection<byte[]> pour déléguer le traitement à un thread consommateur distinct, libérant ainsi immédiatement le thread de lecture série.

4. Fiabilité de la fermeture du port

La méthode Close() peut parfois bloquer si elle est appelée pendant qu'une lecture est en cours. Une pratique sûre consiste à se désabonner de l'événement avant la fermeture et à utiliser un bloc try-catch pour intercepter les déconnexions brutales (arrachement de câble USB).

try 
{
    if (_comPort.IsOpen) 
    {
        _comPort.DiscardInBuffer();
        _comPort.Close();
    }
}
catch (Exception ex) 
{
    // Loguer l'erreur de fermeture (ex: port déjà fermé par le système)
}

Optimisation des performances

Pour les appliactions à haute fréquence, évitez l'utilisation de ReadExisting() ou ReadLine() qui effectuent des conversions de chaînes de caractères coûteuses en ressources. Privilégiez la lecture de tableaux d'octets bruts (byte[]) et traitez les données avec des opérations binaires ou la classe Span<T> pour minimiser les allocations mémoire.

Étiquettes: C# .NET SerialPort RS232 Multi-threading

Publié le 12 juin à 17h34