Arduino

Основы QSerialPort: обнаружение портов, открытие соединения, протокол обмена с Arduino

Введение

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 (как показано в примере) внезапное физическое отключение устройства может оставить приложение в неопределённом состоянии, продолжающем считать соединение активным; для надёжного приложения нужна явная логика обнаружения отключения и попытки переподключения.