Найти тему

QSerialPort и правильная работа с портами или как связать внешнее устройство с Qt приложением по UART

Всем добрый день. В это статье мы изучим класс QSerialPort в Qt и подробно разберём сигнально-слотовые соединения. Итак, начнём!

Первым этапом необходимо запустить Qt Creator и создать тестовое приложение Qt Widgets (рис.1).

Рисунок 1 -  создание приложения
Рисунок 1 - создание приложения

В качестве основного класса выбираем QMainWindow (рис. 2), но это в данном случае особой роли не играет.

Рисунок 2 - выбор главного класса приложения
Рисунок 2 - выбор главного класса приложения

Далее выбираем любимый компилятор и создаём проект. После чего раскрываем дерево проекта (рис. 3).

Рисунок 3 - дерево проекта
Рисунок 3 - дерево проекта

Сразу же создадим класс для работы с UART. Но перед этим в проект необходимо подключить модуль serialport, для этого в .pro файле необходимо прописать следующую строку: "QT+= serialport" (рис. 4).

Рисунок 4 - подключение модуля serialport
Рисунок 4 - подключение модуля serialport

Теперь необходимо создать класс C++. Для этого щёлкаем правой кнопкой мышки по проекту и выбираем пункт: "Добавить новый...". В этом меню выбираем класс C++ (рис. 5).

Рисунок 5 - создание класса C++
Рисунок 5 - создание класса C++

В качестве базового класса выбираем QObject (рис.6).

Рисунок 6 - настройка класса C++
Рисунок 6 - настройка класса C++

Первым делом в заголовочном файле необходимо подключить класс QSerialPort:

#ifndef UART_H
#define UART_H
#include <QObject>
#include <QSerialPort>
class UART : public QObject
{
Q_OBJECT
public:
explicit UART(QObject *parent = nullptr);
signals:
public slots:
};
#endif // UART_H

Теперь можно приступить к созданию методов и слотов:

#ifndef UART_H
#define UART_H
#include <QObject>
#include <QSerialPort>
class UART : public QObject
{
Q_OBJECT
private:
QSerialPort* Port; //Указатель на объект QSerialPort
bool PortInit(QString name);//Инициализация порта
void sendData(QByteArray Data,int length); //Метод для отправки данных в порт
public:
explicit UART(QObject *parent = nullptr);
signals:
public slots:
void slotRead(); //Чтение данных из порта
};
#endif // UART_H

Перейдём в .cpp файл и реализуем методы и слоты. В инициализации порта необходимо указать скорость, стоповые биты, имя и тд. Листинг метода привожу ниже: (WARNING! Прежде чем открыть порт, его необходимо настроить!)

bool UART::PortInit(QString name)
{
Port->setParity(QSerialPort::NoParity);
Port->setStopBits(QSerialPort::OneStop);
Port->setDataBits(QSerialPort::Data8);
Port->setFlowControl(QSerialPort::NoFlowControl);
Port->setBaudRate(QSerialPort::Baud9600);
Port->setPortName(name);
Port->close();//Закрываем порт от греха по дальше =)
if(Port->open(QSerialPort::ReadWrite))
{
return true;
}
else
return false;
}

Кстати. В конструкторе класса необходимо создать объект QSerialPort:

UART::UART(QObject *parent) : QObject(parent)
{
Port = new QSerialPort(this);
}

Теперь можно приступать к методу отправки данных. Здесь нет ничего сложного, но его реализация сильно зависит от протокола (Датчик отпечатков пальцев, с которым я недавно работал, требовал по байтовой отправки данных, а другое устройство может требовать, например, строку целиком). Для примера сделаем по байтовый протокол:

void UART::sendData(QByteArray Data, int length)
{
if(Data.length() == length)
{
for(int i = 0;i<length;i++)
{
QByteArray temp;
temp.resize(1);
temp.append(Data[i]);
Port->write(temp);
Port->waitForBytesWritten();
temp.clear();
}
Data.clear();
}
}

В этом методе мы делаем проверку длины массива и если, она совпадает, то выполняем побайтовое отправление данных, так как метод Port->write() принимает только QByteArray, то приходится создавать буферный массив из одного элемента.

Теперь необходимо данные принять. Для этой цели нужно воспользоваться сигнально-слотовыми соединениями Qt: создать слот и связать его с нужным сигналом. В данном случае сигнал - QSerialPort::readyRead(), а слот - UART::slotRead();. Листинг:

UART::UART(QObject *parent) : QObject(parent)
{
Port = new QSerialPort();
connect(Port,&QSerialPort::readyRead,this,&UART::slotRead);//Соединяем сигнал со слотом
}

Перейдём к реализации слота UART::slotRead, так как ПК гораздо медленнее микроконтроллеров, то необходимо создать буфер для приёма данных, который в свою очередь будет проверять длину сообщения:

void UART::slotRead()
{
QByteArray arr;
arr.append(Port->readAll());
if(arr.length()>=Rxlength)
{
RxArr.clear();
RxArr = arr;
arr.clear();
}
}

Длину принимаемого сообщения мы задаём методом UART::setRxLength(int length), так как слот - это метод, который вызывается сигналом и на вход принимает данные указанные в сигнале (Либо ничего не принимает). Листинг:

void UART::setRxLength(int length)
{
Rxlength = length;
}

Суть сигнально-слотового соединения в том, что слот будет вызываться каждый раз, когда генерируется сигнал, а в данном случае, когда на порт пришли какие-нибудь данные.

Теперь можно перейти к созданию приложения с использованием нашего класса. А для этого создадим простенький интерфейс (рис. 7).

Рисунок 7 - интерфейс приложения
Рисунок 7 - интерфейс приложения

Но реализовывать кнопочки интерфейса пока ещё рано. Для начала нужно создать ещё пару слотов в классе UART и создать QByteArray для отправки (Зачем это нужно объясню позже):

void UART::slotEnableLed()
{
TxArr.clear();
TxArr.resize(1);
TxArr.append(static_cast<char>(1));
sendData(TxArr,1);
}
void UART::slotDisableLed()
{
TxArr.clear();
TxArr.resize(1);
TxArr.append(static_cast<char>(0));
sendData(TxArr,1);
}

Ну и их описание в заголовочном файле:

public slots:
void slotRead(); //Чтение данных из порта
void slotEnableLed();//Включение светодиода на Ардуинке
void slotDisableLed();//Выключение светодиода на Ардуинке
}

Теперь можно наконец-то перейти в класс интерфейса, создать объекты класса UART и QThread (QThread необходимо подключить):

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
com = new UART();
thr = new QThread();
}

Теперь надо переместить наш класс и объект порта в поток, для этого указатель на объект порта переносим в раздел public, а дальше переносим объекты командой QThread::moveToThread():

com->moveToThread(thr);
com->Port->moveToThread(thr);

Теперь осталось запустить поток, но тут есть один момент (о котором и я сам забыл) у объекта потока есть сигнал QThread::started(), который генерируется тогда, когда запускается поток, поэтому от кнопки connect и поля ввода можно избавиться (рис.8).

Рисунок 8 - изменённый интерфейс
Рисунок 8 - изменённый интерфейс

А в классе UART необходимо создать слот инициализации порта, в котором мы будем вызывать метод инициализации:

void UART::slotInit()
{
PortInit("COM3");
}

Ардуинка у всегда висит на одном порте (у меня COM3), поэтому железно прописываем название порта (WARNING! На Linux системах порты называются по другому!).

Когда слот инициализации создан можно перейти к соединению. сигналов и слотов :

connect(thr,&QThread::started,com,&UART::slotInit);//Открываем порт
connect(ui->pushButton_2,&QPushButton::clicked,com,&UART::slotEnableLed);//Соединяем кнопку ВКЛ
connect(ui->pushButton_3,&QPushButton::clicked,com,&UART::slotDisableLed);//Соединяем кнопку ВЫКЛ
com->moveToThread(thr);
com->Port->moveToThread(thr);

Также пропишем пару qDebug() для отслеживания работы программы. Кстати во время первичного тестирования всплыла пара багов, но это мой косяк, поэтому выкладываю исправленный листинг файла uart.cpp (Здесь только исправленные функции):

void UART::sendData(QByteArray Data, int length)
{
qDebug()<<Data;
if(Data.length() == length)
{
for(int i = 0;i<length;i++)
{
QByteArray temp;
temp.resize(0);
temp.append(Data[i]);
// qDebug()<<temp;
Port->write(temp);
Port->waitForBytesWritten();
temp.clear();
}
Data.clear();
}
}
void UART::slotRead()
{
QByteArray arr;
arr.append(Port->readAll());
if(arr.length()>=Rxlength)
{
RxArr.clear();
RxArr = arr;
arr.clear();
}
}
void UART::setRxLength(int length)
{
Rxlength = length;
}
void UART::slotEnableLed()
{
TxArr.clear();
TxArr.resize(0);
TxArr.append(static_cast<char>(1));
sendData(TxArr,1);
}
void UART::slotDisableLed()
{
TxArr.clear();
TxArr.resize(0);
TxArr.append(static_cast<char>(2));
sendData(TxArr,1);
}
void UART::slotInit()
{
if(PortInit("COM3"))
{
qDebug()<<"Port Opened!";
}
else
{
qDebug()<<"Port Failure!";
}
}

Моя ошибка заключалась в том, что метод append добавляет ещё один элемент в массив, из-за этого массив становился длиной в 2 байта.

Теперь можно подключить саму плату Ардуино и убедиться, что приложение подключится к порту (рис.9).

Рисунок 9 - подключение платы Ардуино к приложение по UART
Рисунок 9 - подключение платы Ардуино к приложение по UART

Теперь необходимо запустить Arduino IDE или другую IDE, которая поддерживает работу с Arduino (Я лично пользуюсь Visual Studio + VMicro) и создать новый скетч (рис.10).

Рисунок 10 - Arduino IDE
Рисунок 10 - Arduino IDE

Кстати, порт необходимо закрыть по завершению работы приложения, поэтому необходимо создать слот, который закроет порт и соединить его с сигналом QThread::finished, а далее в деструкторе класса интерфейса остановить поток и удалить все объекты:

MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
com = new UART();
thr = new QThread();
connect(thr,&QThread::started,com,&UART::slotInit);//Открываем порт
connect(ui->pushButton_2,&QPushButton::clicked,com,&UART::slotEnableLed);//Соединяем кнопку ВКЛ
connect(ui->pushButton_3,&QPushButton::clicked,com,&UART::slotDisableLed);//Соединяем кнопку ВЫКЛ
connect(thr,&QThread::finished,com,&UART::slotClosePort);//Закрываем порт
com->moveToThread(thr);
com->Port->moveToThread(thr);
thr->start();
}
MainWindow::~MainWindow()
{
thr->quit(); //Завершаем поток
delete com;//Удаляем объект класса UART
if(thr->isFinished())//Ждём завершения потока
{
delete thr;//Удаляем поток
delete ui;//Удаляем интерфейс
}
}

Теперь перейдём в редактор кода Ардуино и реализуем простое мигнае светодиода по UART:

void setup() {
// put your setup code here, to run once:
pinMode(13,OUTPUT);
Serial.begin(9600);//Открываем порт
}
void loop() {
// put your main code here, to run repeatedly:
if(Serial.available()>0)//Ждём что-нибудь с порта
{
byte b =Serial.read();//Считываем байт
if(b == 0x01)
{
digitalWrite(13,HIGH);
}
else if(b == 0x02)
{
digitalWrite(13,LOW);
}
}
}

0x01 и 0x02 - HEX представление чисел 1 и 2 соответственно, которые мы указали раньше. Если код написан правильно и прошивка в Ардуино залита, то мы получим переключение светодиода из Qt приложения:

Демонстрация работы передачи данных по UART
Демонстрация работы передачи данных по UART

Осталось дело за малым - получить данные от платы Ардуино. Для этого допишем Serial.write() в скетч:

void setup() {
// put your setup code here, to run once:
pinMode(13,OUTPUT);
Serial.begin(9600);//Открываем порт
}
void loop() {
// put your main code here, to run repeatedly:
if(Serial.available()>0)//Жём что-нибудь с порта
{
byte b =Serial.read();//Считываем байт
if(b == 0x01)
{
digitalWrite(13,HIGH);
Serial.write(0x01);
}
else if(b == 0x02)
{
digitalWrite(13,LOW);
Serial.write(0x02);
}
}
}

Теперь Ардуино будет отправлять в ответ один байт данных, который мы будем отлавливать в слоте UART::slotRead() (Не забываем указать длину ожидаемого сообщения =) ):

void UART::slotRead()
{
QByteArray arr;
arr.append(Port->readAll());
if(arr.length()>=Rxlength)
{
//Обработка данных:
qDebug()<< "Data IN: "<< arr;
arr.clear();
}
}

Кстати, если вы ещё раньше заметили не нужный QByteArray RxArr, то не волнуйтесь, я его убрал =). К сожалению, полноценно протестировать приём данных у меня не получилось (Драйвер на моём ПК как-то криво работает с Qt из-за чего сигнал readyRead() отрабатывает не корректно), поэтому ограничусь просто выводом данных в консоль (рис.11).

Рисунок 11 - получение данных из порта
Рисунок 11 - получение данных из порта

Демонстрация работы:

Приём данных от платы Ардуино
Приём данных от платы Ардуино

Как видно - данные приходят с задержкой, вообщем это и есть то самое не корректное поведение сигнала readyRead().

На этом этапе статья подходит к концу. По итогу мы научились отправлять данные в COM порт и получать их от туда. Дальнейшая реализация приёма/передачи данных по UART сильно зависит от используемого протокола.

Выводы:

  1. QSerialPort - это удобный класс для работы с COM портом, с помощью которого можно связывать устройства на микроконтроллерах и ПК.
  2. Для правильной реализации работы с COM портом необходимо придерживаться сигнально-слотовых соединений Qt, а не использовать циклы и тп "костыли".
  3. Для того чтобы не блокировать основной поток необходимо всю работу с COM портом вынести в отдельный поток, используя класс QThread.
  4. Также возможны проблемы с сигналом readyRead(), но шанс её появления достаточно мал (Но вот меня он не обошёл =( ).