Site : tvaira.free.fr
Qt est une bibliothèque logicielle orientée objet développée en C++ par Qt Development Frameworks, filiale de Digia.
Voir la présentation de Qt.
RS-232 est une norme standardisant une voie de communication de type série sur trois fils minimum. Disponible sur presque tous les PC depuis 1981 jusqu’au milieu des années 2000, il est communément appelé le « port série ».
Sur les systèmes d’exploitation MS-DOS et Windows, les ports RS-232 sont désignés par les noms COM1, COM2, etc. Cela leur a valu le surnom de « ports COM », encore utilisé de nos jours.
On utilise maintenant des adaptateurs USB/RS-232 car les PC ne disposent plus d’interfaces physiques RS-232. Cela revient à exploiter logiciellement un port série virtuel. Ces périphériques utilisent en réalité une transmission série avec un convertisseur USB <–> RS-232 (les circuits les plus répandus sont pl2303, FTDI FT232, …). Certains adaptateurs ajoutent un circuit MAX232 pour mettre en forme des signaux conformes au standard RS-232. La prise en charge du périphérique est assurée par le système d’exploitation via un pilote de périphérique (driver).
Lire le cours sur la transmission série.
Un port série virtuel est une solution logicielle qui émule un port série standard.
Cela permet généralement :
De nombreux périphériques USB sont “vus” comme des ports séries virtuels par le système (XBee, Bluetooh, GPS, …).
Pour établir une communication effective via un port série physique (RS-232) ou virtuel, il est nécessaire de définir le protocole utilisé : notamment, le débit de la transmission (en bits/s), le codage utilisé, le découpage en trame, etc. La norme RS-232 (couche Physique) laisse ces points libres, mais en pratique on utilise souvent des trames d’un caractère ainsi constituées :
Remarque : De nombreux périphériques séries expriment encore leur vistesse de transmission en bauds. Le baud est l’unité de mesure du nombre de symboles transmissibles par seconde. Dans le cas d’un signal modulé utilisé dans le domaine des télécommunications, le baud est l’unité de mesure de la rapidité de modulation. Le terme “baud” provient du patronyme d’Émile Baudot, l’inventeur du code Baudot utilisé en télégraphie. Il ne faut donc pas confondre le baud avec le bit par seconde (bit/s) car il est souvent possible de transmettre plusieurs bits par symbole. Si on transmet un bit par symbole alors le baud et le bit par seconde (bit/s) sont équivalents.
Exemple : calcul du temps de transmission d’une trame de requête de 8 octets en considérant les paramètres suivants de la liaison série :
Taille de la trame (en bits) pour transmettre un octet : 1 (START) + 8 (DATA) + 1 (PARITÉ) + 1 (STOP) = 11 bits
Taille de la trame (en bits) pour transmettre une requête : 8 octets x 11 bits = 88 bits
Temps de transmission d’un octet : (1 x 11) / 9600 = 0,00114583 s = 1,14583 ms
Temps de transmission d’une requête : (8 x 11) / 9600 = 0,00917 s = 9,167 ms
Suivant votre version de Qt :
Qt 4 : Malheureusement, la version 4 de Qt ne fournit pas de classes pour gérer un port série nativement. On va donc devoir utiliser une bibliothèque logicielle externe à Qt : la classe QextSerialPort
disponible ici.
Qt 5 : le problème n’existe plus en Qt5 car on dispose des classes QSerialPort
et QSerialPortInfo
avec le module serialport
.
$ sudo apt-get install libqt5serialport5 libqt5serialport5-dev
Il faudra ensuite ajouter le module serialport
au fichier de projet .pro
:
QT += serialport
On crée un nouveau répertoire et on se déplace à l’intérieur :
$ mkdir mo-qextserialport-1
$ cd mo-qextserialport-1/
On télécharge l’archive :
$ wget https://github.com/qextserialport/qextserialport/archive/master.zip
Remarque : vous pouvez utiliser aussi cette archive qextserialport-1.2rc.zip.
On décompresse l’archive téléchargée :
$ unzip master.zip
$ rm -f master.zip
$ mv qextserialport-master qextserialport
On crée un nouveau fichier de projet :
$ vim mo-qextserialport-1.pro
include(qextserialport/src/qextserialport.pri)
TEMPLATE = app
TARGET =
DEPENDPATH += .
INCLUDEPATH += .
OBJECTS_DIR = ./tmp
MOC_DIR = ./tmp
DESTDIR = ./bin
# Input
SOURCES += main.cpp
On crée maintenant un programme de test du port série :
$ vim main.cpp
#include "qextserialport.h"
#include <QDebug>
#define PORT "/dev/ttyUSB0"
int main()
{
QextSerialPort *port;
// instanciation du port en mode asynchrone -> QextSerialPort::Polling
port = new QextSerialPort(QLatin1String(PORT), QextSerialPort::Polling);
// TODO : paramètrer le port (débit, ...)
// ouverture du port en lecture/écriture
port->open(QIODevice::ReadWrite);
qDebug("<debug> etat ouverture port : %d", port->isOpen());
// TODO : réceptionner et/ou envoyer des données
// fermeture du port
port->close();
qDebug("<debug> etat ouverture port : %d", port->isOpen());
delete port;
return 0;
}
On fabrique et on exécute le programme de test :
$ qmake
$ make
$ ./bin/mo-qextserialport-1
<debug> etat ouverture port : 1
<debug> etat ouverture port : 0
Le paramétrage du port série se fait avec les méthodes suivantes :
setBaudRate()
setParity()
setDataBits()
setStopBits()
Et pour les opération de lecture et d’écriture, on utilisera par exemple les méthodes read()
et write()
.
Documentation : QextSerialPort.
On crée un nouveau répertoire et on se déplace à l’intérieur :
$ mkdir mo-qserialport-1
$ cd mo-qserialport-1/
On crée un nouveau fichier de projet :
$ vim mo-qserialport-1.pro
QT += serialport
TEMPLATE = app
TARGET =
# Input
SOURCES += main.cpp
On crée maintenant un programme de test du port série :
$ vim main.cpp
#include <QSerialPort>
#include <QDebug>
#define PORT "/dev/ttyUSB0"
int main()
{
QSerialPort *port;
// instanciation du port
port = new QSerialPort(QLatin1String(PORT));
// TODO : paramètrer le port (débit, ...)
// ouverture du port
port->open(QIODevice::ReadWrite);
qDebug("<debug> etat ouverture port : %d", port->isOpen());
// TODO : réceptionner et/ou envoyer des données
// fermeture du port
port->close();
qDebug("<debug> etat ouverture port : %d", port->isOpen());
delete port;
return 0;
}
On fabrique et on exécute le programme de test du port série :
$ qmake
$ make
$ ./bin/mo-qserialport-1
<debug> etat ouverture port : 1
<debug> etat ouverture port : 0
Le paramétrage du port série se fait avec les méthodes suivantes :
setBaudRate()
setParity()
setDataBits()
setStopBits()
Et pour les opération de lecture et d’écriture, on utilisera par exemple les méthodes read()
et write()
.
Documentation : QSerialPort.
De nombreux périphériques utilisent un protocole de communication de type ASCII pour échanger des trames. Il existe des protocoles propriétaires et/ou standardisés. Certains sont très répandus : NMEA 0183 (GPS, station météo, …), commandes AT (modem, XBee , Bluetooth, …).
Cela revient :
QextSerialPort
(Qt4) ou QSerialPort
(Qt5)QString
de Qt.Remarque : Transmettre des données en ASCII permet de s’affanchir du problème de la réprensation des données en mémoire (voir little endian et big endian sur wikipedia).
Dans une communication, on distinguera trois types de trame :
La gestion de la communication va ensuite dépendre de l’architecture de votre application. Si votre application dispose d’une IHM, vous ne pourrez pas effectuer d’appels bloquants à partir du thread GUI (Graphical User Interface), qui est le fil d’exécution principal, sinon l’application risquerait de se “figer”. Le thread GUI est responsable de l’affichage et des interactions avec l’utilisateur et surtout c’est le seul thread qui doit modifier l’affichage.
En résumé, cela revient :
readyRead()
pour la réception et bytesWritten()
pour l’émission// instanciation du port en mode synchrone -> QextSerialPort::EventDriven
QextSerialPort *port = new QextSerialPort(portName, QextSerialPort::EventDriven);
// configuration
port->setBaudRate(BAUD9600);
port->setDataBits(DATA_8);
port->setParity(PAR_NONE);
port->setStopBits(STOP_1);
port->setFlowControl(FLOW_OFF);
//port->setTimeout(500);
// ouverture
port->open(QIODevice::ReadWrite);
// ...
// fermeture
port->close();
// instanciation du port
QSerialPort *port = new QSerialPort(portName);
// configuration
port->setBaudRate(QSerialPort::Baud9600);
port->setDataBits(QSerialPort::Data8);
port->setParity(QSerialPort::NoParity);
port->setStopBits(QSerialPort::OneStop);
port->setFlowControl(QSerialPort::NoFlowControl);
// ouverture
port->open(QIODevice::ReadWrite);
// ...
// fermeture
port->close();
Remarque : L’émission est généralement plus simple à mettre en oeuvre que la réception.
L’émission de trames périodiques peut se faire sous Qt avec un QTimer
(i.e. un minuteur ou temporisateur). Le principe est le suivant :
// instancie un objet QTimer
QTimer *timer = new QTimer(this);
// connecte le signal timeout() au slot envoyer()
connect(timer, SIGNAL(timeout()), this, SLOT(envoyer()));
// démarre le timer (la période s'expime en millisecondes)
timer->start(1000); // toutes les secondes
// ...
// arrête le timer
if(timer->isActive())
{
timer->stop();
}
À l’expiration du timer le signal timeout()
sera émis et le slot envoyer()
sera exécuté. Celui-ci devra fabriquer une trame et l’émettre sur le port.
Les fonctions d’écriture sont les suivantes :
qint64 write(const char * data, qint64 maxSize)
qint64 write(const char * data)
qint64 write(const QByteArray & byteArray)
Pour les trames apériodiques ou de requête, il suffit d’émettre la trame en utilisant un objet port
de type QextSerialPort
(Qt4) ou QSerialPort
(Qt5) :
int MaClasse::emettre(const QString &trame)
{
int nombresOctets = -1;
if (port == NULL || !port->isOpen())
{
return -1;
}
nombresOctets = port->write(trame.toLatin1());
return nombresOctets;
}
Remarque : Il faudra faire attention à la gestion des tampons (buffers) internes. On rappelle que le traitement fait par un ordinateur est beaucoup plus rapide que l’émission d’une trame (nanosecondes vs millisecondes !). Pour de grosses quantités de données, il faudra se synchroniser avec l’émission sur le périphérique pour éviter la saturation des tampons (buffers) internes.
Pour aller plus loin, on dispose :
bytesWritten(qint64 bytes)
: ce signal est émis chaque fois bytes octets de données ont été écrits sur le périphérique.waitForBytesWritten(int msecs)
: elle attend jusqu’à ce que le signal bytesWritten()
ait été émis ou que msecs millisecondes soient passées. Il est déconseillé de l’utiliser à partir du thread GUI.setTimeout(long millisec)
: elle définit les délais d’attente d’écriture (et aussi de lecture) pour le port en millisec millisecondes. C’est un délai d’expiration par caractère individuel et non pour l’opération entière.Évidemment, il faudra aussi s’intéresser au contrôle de flux. Le contrôle de flux, dans un réseau informatique, représente un asservissement du débit binaire des données transmises de l’émetteur vers le récepteur. Le stop and wait est la forme la plus simple de contrôle de flux. En communication série asynchrone RS232, deux modes de contrôle de flux sont proposés : en hardware via les lignes RTS/CTS (Ready To Send/Clear To Send) ou sous contrôle logiciel via les caractères ASCII XON/XOFF. Le contrôle hardware en RS232 nécessite 5 fils (Rx, Tx, Gnd, RTS, CTS) et le contrôle logiciel n’en nécessite que 3 (Rx, Tx, Gnd).
Sans l’utilisation d’une boucle d’évènements, il est possible de gérer le port série en mode bloquant (cf. thread) en utilisant la méthode :
waitForBytesWritten()
bloque les appels jusqu’à ce que les données ait été écrites sur le port série.Remarque : Cette méthode expire après ms millisecondes passé en argument (timeout). Le délai d’attente par défaut est 30000 millisecondes. Si msecs est égal à -1, la méthode n’aura pas de timeout.
Les fonctions de lecture sont très nombreuses :
qint64 read(char * data, qint64 maxSize)
QByteArray read(qint64 maxSize)
QByteArray readAll()
qint64 readLine(char * data, qint64 maxSize)
QByteArray readLine(qint64 maxSize = 0)
La classe QextSerialPort
(Qt4) ou QSerialPort
(Qt5) fournit le signal readyRead()
. Ce signal est émis une fois que de nouvelles données sont disponibles pour la lecture à partir du périphérique.
Il faut donc tout d’abord le connecter à un slot de réception :
void MaClasse::ouvrirPort(const QString &portName)
{
port = new QextSerialPort(portName, QextSerialPort::EventDriven); // Qt 4
// configuration du port ...
if (port->open(QIODevice::ReadWrite) == true)
{
// on connecte le signal readyRead() au slot recevoir()
connect(port, SIGNAL(readyRead()), this, SLOT(recevoir()));
}
}
Puis, le principe de réception de trame à partir d’un thread GUI sera le suivant :
void MaClasse::recevoir()
{
QByteArray donnees;
while (port->bytesAvailable())
{
donnees += port->readAll();
usleep(100000); // cf. timeout
}
QString trameRecue = QString(donnees.data());
// ....
}
Sans l’utilisation d’une boucle d’évènements, il est possible de gérer le port série en mode bloquant (cf. thread) en utilisant la méthode :
waitForReadyRead()
bloque les appels jusqu’à ce que de nouvelles données soient disponibles pour la lecture.Remarque : Cette méthode expire après ms millisecondes passé en argument (timeout). Le délai d’attente par défaut est 30000 millisecondes. Si msecs est égal à -1, la méthode n’aura pas de timeout.
En Qt5, vous disposez de la classe QSerialPortInfo pour faire cela. La classe QSerialPortInfo
fournit des informations sur les ports série existants sur votre machine.
On utilise la méthode statique QSerialPortInfo::availablePorts()
pour générer une liste d’objets QSerialPortInfo
. Chaque objet QSerialPortInfo
de cette liste représente un port série unique et on peut récupérer le nom du port (portName()
), l’emplacement du système (systemLocation()
), la description (description()
) et le fabricant (manufacturer()
). Cet objet peut également être utilisé comme paramètre d’entrée pour le constructeur ou la méthode setPort()
de la classe QSerialPort
.
Exemple qui génère une QStringList
des ports disponibles pouvant être utilisée dans un QComboBox :
QList<QSerialPortInfo> listePorts;
QStringList listePortsDisponibles;
listePorts = QSerialPortInfo::availablePorts();
for (int i=0; i < listePorts.size();i++)
{
QSerialPortInfo info = listePorts.at(i);
//if(info.portName().contains("ACM") || info.portName().contains("USB"))
{
if(!info.manufacturer().isEmpty())
listePortsDisponibles << info.manufacturer() + " (" + info.portName() + ")";
else
listePortsDisponibles << info.portName();
}
}
qDebug() << Q_FUNC_INFO << "listePortsDisponibles" << listePortsDisponibles;
Lien : Enumerator Example
La classe QString
contient de nombreuses méthodes pour manipuler des chaînes de caractère. Il vous faudra consulter très souvent sa documentation : doc.qt.io/qt-4.8/qstring.html.
Remarque : la documentation de Qt est très riche et très bien faite !
Pour le traitement d’une trame, les méthodes les plus utilisées sont :
startsWith()
et endsWith()
pour vérifier les délimiteurs de début de trame (par exemple le dollar $
) et de fin de trame (par exemple la séquence “\r\n
”)contains()
ou count()
pour détecter (ou compter) la présence de certains caractères (le plus souvent des délimiteurs comme la virgule ,
, le point-virgule ;
ou l’étoile *
)section()
ou split()
pour le découpage à partir de délimiteurs (comme la virgule ,
, le point-virgule ;
…) ou mid()
pour une extraction sans délimiteurreplace()
pour le remplacement et remove()
pour la suppression de caractèresindexOf()
pour obtenir la position d’un caractèreVoici quelques exemples d’utilisation de la classe QString
dans le cadre d’un traitement d’une trame NMEA 0183 :
#include <QDebug>
#include <QString>
int main(int argc, char *argv[])
{
QString phrase = "$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000*0E";
// Faire des essais :
//QString phrase = "";
//QString phrase = "GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000*0E";
//QString phrase = "$GPAAM,A,A,0.10,N,WPTNME*32";
//QString phrase = "$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000";
QString checksum;
const QString debutTrame = "$";
const QString typeTrame = "GPGGA";
const QString debutChecksum = "*";
// phrase vide ?
if(phrase.length() != 0)
{
// est-ce une phrase NMEA 0183 ?
if(phrase.startsWith(debutTrame))
{
// est-ce la bonne phrase ?
if(phrase.startsWith(debutTrame + typeTrame))
{
// y-a-t-il un checksum ?
if(phrase.contains(debutChecksum))
{
checksum = phrase.section(debutChecksum, 1, 1);
qDebug() << "checksum : 0x" << checksum;
}
else
qDebug() << "Attention : il n'y a pas de checksum dans cette phrase !";
}
else
qDebug() << "Erreur : ce n'est pas une trame GGA !";
}
else
qDebug() << "Erreur : ce n'est pas une trame NMEA 0183 !";
}
else
qDebug() << "Erreur : phrase vide !";
return 0;
}
Généralement, les protocoles intègrent dans la trame un processus de détection d’erreurs de transmission appelée somme de contrôle (checksum), cf. ci-dessous.
Une fois la trame vérifiée et découpée, il faudra sans doute assurer la conversion vers des données numériques (int
, double
, …) :
#include <QDebug>
#include <QString>
int main(int argc, char *argv[])
{
/* Exemple de base */
int i = 2;
double d = 3.14;
// Du numérique -> chaîne de caractères
QString entier = QString::number(i); // int -> QString
QString reel = QString::number(d); // double -> QString
//QString reel = QString::number(d, 'f', 1); // double -> QString (avec un chiffre après la virgule)
qDebug() << "L'entier i : " << entier;
qDebug() << "Le réel d : " << reel;
// De chaîne de caractères -> numérique
entier = "100";
reel = "2.71828";
i = entier.toInt(); // QString -> int
d = reel.toDouble(); // QString -> double
qDebug() << "L'entier i : " << i;
qDebug() << "Le réel d : " << d;
/* Exemple appliqué */
QString phrase = "$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000*0E";
QString horodatage;
unsigned int heure, minute;
double seconde;
// découpe la trame avec le délimiteur ',' et récupère le deuxième champ
horodatage = phrase.section(',', 1, 1);
// découpe une chaine à partir d'une position et un nombre de caractères
heure = horodatage.mid(0, 2).toInt();
minute = horodatage.mid(2, 2).toInt();
seconde = horodatage.mid(4, 2).toDouble();
qDebug() << "Horodatage : " << horodatage;
horodatage = QString::number(heure) + " h " + QString::number(minute)
+ " " + QString::number(seconde) + " s";
qDebug() << "Horodatage : " << horodatage;
return 0;
}
Remarque : on peut aussi utiliser la méthode arg()
(voir ci-dessous).
Un dernier exemple qui montre le calcul du checksum pour une trame NMEA 0183 :
#include <QDebug>
#include <QString>
unsigned char Simulation::calculerChecksum(QString data)
{
unsigned char checksum = 0;
// remarque : le dollar n'entre pas dans le calcul du checksum
for(int i=1;i<data.length() && data.at(i).toAscii() != '*';i++)
{
checksum ^= data.at(i).toAscii(); // XOR
}
//qDebug("(checksum) 0x%02X\n", checksum);
return checksum;
}
int main(int argc, char *argv[])
{
QString phrase = "$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000";
checksum = calculerChecksum(phrase);
// formate le checksum en ASCII sous la forme hexadécimale
strChecksum = QString("%1").arg(checksum, 2, 16, QLatin1Char('0'));
phrase += QString("*"); // ajoute le délimiteur de checksum
phrase += strChecksum.toUpper(); // ajoute le checksum converti en majuscule
phrase += QString("\r\n"); // ajoute le délimiteur de fin
// ...
return 0;
}
Il est possible de confier la gestion du port à un thread d’arrière-plan.
QSerialPort
fournit les méthodes suivantes pour gérer le port série en mode bloquant :
waitForReadyRead()
bloque les appels jusqu’à ce que de nouvelles données soient disponibles pour la lecture.waitForBytesWritten()
bloque les appels jusqu’à ce que les données ait été écrites sur le port série.Remarque : Ces deux méthodes expirent après ms millisecondes passé en argument (timeout). Le délai d’attente par défaut est 30000 millisecondes. Si msecs est égal à -1, les méthodes n’auront pas de timeout.
Qt fournit la classe QThread
pour la création et manipulation d’un thread. Cette classe possède une méthode run()
qui contiendra le code exécuté par le thread. Cette classe émet le signal started()
lorsqu’un thread est lancé et finished()
lorsque le thread est terminé. Il y a plusieurs approches possibles dans l’utilisation de QThread
, en voici deux :
QThread
et on implémente la fonction run()
qui contiendra le code du thread.#include <QThread>
class Communication : public QThread
{
Q_OBJECT
public:
Communication(QObject * parent = 0);
void run(); // le code du thread
private:
signals:
public slots:
};
// le code du thread
void Communication::run()
{
QextSerialPort *port = new QextSerialPort(portName, QextSerialPort::Polling);
while(isRunning())
{
// ...
}
}
Communication *communication = new Communication;
// on démarre le thread
communication->start(); // cela exécutera dans un thread la méthode run() de la classe Communication
QThread
et on assigne les objets héritant de QObject à ce thread en utilisant la fonction moveToThread()
. On connecte le started()
à un slot de cet objet pour exécuter le code du thread.Acquisition *acquisition = new Acquisition;
QThread *threadAcquisition = new QThread;
acquisition->moveToThread(threadAcquisition);
QObject::connect(threadAcquisition, SIGNAL(started()), acquisition, SLOT(main()));
QObject::connect(threadAcquisition, SIGNAL(finished()), acquisition, SLOT(terminer()));
// on démarre le thread
threadAcquisition->start(); // cela exécutera dans un thread la méthode main() de la classe Acquisition
// ...
Lire les Threads sous Qt.