Введение
QSerialPort — модуль Qt для работы с последовательными (COM/USB-UART) портами, через которые Arduino по умолчанию общается с компьютером. В статье разберём базовый процесс обнаружения подключённой платы, открытия соединения с правильными параметрами и простейший обмен текстовыми командами — основу для всего последующего цикла статей.
Концепция
Arduino подключается к компьютеру как виртуальный последовательный порт (на Windows — COMx, на Linux — обычно /dev/ttyUSB0 или /dev/ttyACM0, на macOS — /dev/cu.usbmodemXXXX) через USB-UART мост, встроенный в большинство плат Arduino. QSerialPortInfo::availablePorts() позволяет программно перечислить доступные порты и их метаданные (производитель, идентификатор устройства), что помогает автоматически найти именно Arduino среди других подключённых последовательных устройств. После открытия порта с правильными параметрами (скорость передачи данных, биты данных, чётность) обмен данными происходит через тот же QIODevice-интерфейс, что и для файлов и сетевых сокетов в основном цикле статей.
Пример кода
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QDebug>
QString findArduinoPort()
{
const auto ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &portInfo : ports) {
// Arduino Uno/Nano часто используют чипы FTDI или CH340 с известными VID/PID,
// что помогает отличить именно плату Arduino от других подключённых устройств
if (portInfo.description().contains("Arduino", Qt::CaseInsensitive) ||
portInfo.manufacturer().contains("Arduino", Qt::CaseInsensitive)) {
return portInfo.portName();
}
}
return QString(); // не найдено — пользователю нужно выбрать порт вручную
}
// arduinoconnection.h — базовый класс соединения с Arduino через QSerialPort
#pragma once
#include <QSerialPort>
#include <QObject>
#include <QDebug>
class ArduinoConnection : public QObject
{
Q_OBJECT
public:
explicit ArduinoConnection(QObject *parent = nullptr) : QObject(parent)
{
connect(&m_serialPort, &QSerialPort::readyRead, this, &ArduinoConnection::onReadyRead);
connect(&m_serialPort, &QSerialPort::errorOccurred, this, &ArduinoConnection::onErrorOccurred);
}
bool connectToPort(const QString &portName)
{
m_serialPort.setPortName(portName);
m_serialPort.setBaudRate(QSerialPort::Baud9600); // должна совпадать со значением в Serial.begin() в скетче
m_serialPort.setDataBits(QSerialPort::Data8);
m_serialPort.setParity(QSerialPort::NoParity);
m_serialPort.setStopBits(QSerialPort::OneStop);
m_serialPort.setFlowControl(QSerialPort::NoFlowControl);
if (!m_serialPort.open(QIODevice::ReadWrite)) {
qWarning() << "Не удалось открыть порт:" << m_serialPort.errorString();
return false;
}
return true;
}
void sendCommand(const QString &command)
{
// Символ конца строки важен — прошивка Arduino будет ждать его как маркер
// завершения команды (см. статью 282 про протокол на стороне Arduino)
const QByteArray data = (command + "n").toUtf8();
m_serialPort.write(data);
}
private slots:
void onReadyRead()
{
m_receiveBuffer.append(m_serialPort.readAll());
// Arduino обычно присылает данные построчно — разбираем буфер по символу новой строки
while (m_receiveBuffer.contains('n')) {
const int newlineIndex = m_receiveBuffer.indexOf('n');
const QByteArray line = m_receiveBuffer.left(newlineIndex).trimmed();
m_receiveBuffer.remove(0, newlineIndex + 1);
if (!line.isEmpty()) {
emit lineReceived(QString::fromUtf8(line));
}
}
}
void onErrorOccurred(QSerialPort::SerialPortError error)
{
if (error != QSerialPort::NoError) {
qWarning() << "Ошибка последовательного порта:" << m_serialPort.errorString();
}
}
signals:
void lineReceived(const QString &line);
private:
QSerialPort m_serialPort;
QByteArray m_receiveBuffer;
};
Пояснения к коду
findArduinoPort() использует метаданные, предоставляемые драйвером последовательного порта (description(), manufacturer()), для попытки автоматически опознать именно Arduino среди всех доступных последовательных портов — это не стопроцентно надёжный способ (зависит от того, что именно сообщает конкретный USB-UART чип конкретной платы), но удобен как первая попытка перед тем, как предложить пользователю выбрать порт из списка вручную.
ArduinoConnection::connectToPort() настраивает все необходимые параметры последовательного соединения — критично, что setBaudRate() должен точно совпадать со значением, указанным в Serial.begin() внутри самого скетча Arduino (статья 282); несовпадение скорости передачи данных — самая частая причина, когда соединение формально открывается успешно, но принимаемые данные оказываются полной «кашей» из нечитаемых символов. onReadyRead() показывает обработку буферизации построчных данных — поскольку readyRead() может сработать до того, как пришла полная строка (или с несколькими строками сразу), накопление в m_receiveBuffer и разбор по символу n через цикл while обязательны для корректного построчного протокола.
Подводные камни
- Несовпадение скорости передачи данных (
baudRate) между настройкамиQSerialPortв Qt-приложении иSerial.begin()в скетче Arduino — самая частая первая проблема при первом подключении, проявляющаяся как полностью нечитаемые, «мусорные» данные при приёме, а не как явная ошибка соединения, поскольку сам факт открытия порта на неправильной скорости обычно не вызывает ошибки. - Открытие порта сразу после подключения USB-кабеля без задержки, в момент, когда Arduino ещё выполняет автоматический сброс при установлении DTR-сигнала (что многие платы Arduino делают при открытии последовательного порта, перезагружая скетч) — первые несколько сотен миллисекунд после открытия порта плата может быть всё ещё перезагружающейся и не готовой к реальному обмену данными, и для надёжности стоит добавить небольшую паузу после открытия порта перед отправкой первой команды.
- Попытка открыть последовательный порт, уже занятый другой программой (например, всё ещё открытым монитором последовательного порта в Arduino IDE) — приводит к ошибке открытия порта, которая в сообщении об ошибке не всегда явно указывает именно на эту причину; частая практическая рекомендация для новичков — убедиться, что Arduino IDE Serial Monitor закрыт перед запуском Qt-приложения, использующего тот же порт.
- Отсутствие явной обработки ситуации отключения USB-кабеля во время работы приложения — без обработки
QSerialPort::errorOccurred(как показано в примере) внезапное физическое отключение устройства может оставить приложение в неопределённом состоянии, продолжающем считать соединение активным; для надёжного приложения нужна явная логика обнаружения отключения и попытки переподключения.