Qt Serial Port (QSerialPort)

QSerialPort and QSerialPortInfo — opening, configuring, reading, writing serial ports, and async communication for embedded devices.

6 min read
55203 chars

What Is QSerialPort?

QSerialPort is Qt’s cross-platform serial port API. It supports RS-232, RS-485, UART on Linux (/dev/ttyUSB0), Windows (COM3), macOS (/dev/tty.usbserial), and embedded Linux.

┌─────────────────────────────────────────────────────┐
│                  Qt Application                     │
│                                                     │
│   QSerialPort                                       │
│   ┌──────────────────────────────────────┐         │
│   │  open() → configure → read/write     │         │
│   └──────────────────┬───────────────────┘         │
│                       │                             │
└───────────────────────┼─────────────────────────────┘
                        │
              ┌─────────▼──────────┐
              │  /dev/ttyUSB0      │  (Linux)
              │  COM3              │  (Windows)
              └────────────────────┘
                        │
              ┌─────────▼──────────┐
              │  Microcontroller   │
              │  Sensor / Modem    │
              └────────────────────┘

CMakeLists.txt Setup

find_package(Qt6 REQUIRED COMPONENTS SerialPort)
target_link_libraries(MyApp PRIVATE Qt6::SerialPort)

List Available Ports

#include <QSerialPortInfo>

const auto ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : ports) {
    qDebug() << "Port:"        << info.portName();
    qDebug() << "Description:" << info.description();
    qDebug() << "Manufacturer:"<< info.manufacturer();
    qDebug() << "VID:PID:"     << info.vendorIdentifier()
                                << info.productIdentifier();
    qDebug() << "---";
}

Opening & Configuring a Port

#include <QSerialPort>

QSerialPort serial;
serial.setPortName("/dev/ttyUSB0");   // Linux; "COM3" on Windows
serial.setBaudRate(QSerialPort::Baud115200);
serial.setDataBits(QSerialPort::Data8);
serial.setParity(QSerialPort::NoParity);
serial.setStopBits(QSerialPort::OneStop);
serial.setFlowControl(QSerialPort::NoFlowControl);

if (!serial.open(QIODevice::ReadWrite)) {
    qWarning() << "Cannot open:" << serial.errorString();
    return;
}
qDebug() << "Port opened successfully";

Baud Rate Quick Reference:

ConstantBPS
Baud96009600
Baud1920019200
Baud3840038400
Baud115200115200
Baud10000001 Mbps

Async Reading (Event-Driven)

// In a QObject-based class
connect(&serial, &QSerialPort::readyRead,
        this, &MyClass::onDataReceived);

void MyClass::onDataReceived() {
    QByteArray data = serial.readAll();
    qDebug() << "Received:" << data.toHex(' ');
    // OR for text:
    qDebug() << "Text:" << QString::fromUtf8(data);
}

Writing Data

// Write raw bytes
QByteArray command = QByteArray::fromHex("020103FF03");
serial.write(command);
serial.waitForBytesWritten(1000);  // wait up to 1s

// Write text
serial.write("AT+VERSION\r\n");

// Flush
serial.flush();

Synchronous Read (Blocking, for simple cases)

serial.write("STATUS?\r\n");

if (serial.waitForReadyRead(2000)) {  // wait up to 2s
    QByteArray response = serial.readAll();
    // Wait for more data
    while (serial.waitForReadyRead(100))
        response += serial.readAll();
    qDebug() << "Response:" << response;
} else {
    qWarning() << "Timeout waiting for response";
}

Framing — Read Until Newline

// In-class buffer for partial reads
QByteArray m_rxBuffer;

void onDataReceived() {
    m_rxBuffer += serial.readAll();

    while (true) {
        int idx = m_rxBuffer.indexOf('\n');
        if (idx < 0) break;  // no complete line yet

        QByteArray line = m_rxBuffer.left(idx).trimmed();
        m_rxBuffer.remove(0, idx + 1);

        qDebug() << "Line:" << line;
        parseFrame(line);
    }
}

Fixed-Length Frame Parser

const int FRAME_SIZE = 8;  // [STX][LEN][CMD][D0][D1][D2][CRC][ETX]

void onDataReceived() {
    m_rxBuffer += serial.readAll();

    while (m_rxBuffer.size() >= FRAME_SIZE) {
        // Find STX
        int stx = m_rxBuffer.indexOf('\x02');
        if (stx < 0) { m_rxBuffer.clear(); break; }
        if (stx > 0)  m_rxBuffer.remove(0, stx);  // discard garbage

        if (m_rxBuffer.size() < FRAME_SIZE) break;

        QByteArray frame = m_rxBuffer.left(FRAME_SIZE);
        if (frame.last() == '\x03') {
            processFrame(frame);
        }
        m_rxBuffer.remove(0, FRAME_SIZE);
    }
}

Error Handling

connect(&serial, &QSerialPort::errorOccurred,
    [](QSerialPort::SerialPortError error) {
        if (error == QSerialPort::ResourceError) {
            qWarning() << "Device disconnected!";
            // Attempt reconnect
        } else if (error != QSerialPort::NoError) {
            qWarning() << "Serial error:" << error;
        }
    });

Visual: RS-232 Frame

START     DATA BITS (8)           PARITY  STOP
  │    ┌──┬──┬──┬──┬──┬──┬──┬──┐   │      │
  └────┤b0│b1│b2│b3│b4│b5│b6│b7├───┴──────┘
       └──┴──┴──┴──┴──┴──┴──┴──┘
  0       LSB              MSB       P     1

Protocol frame on top of UART:
[STX 0x02][LEN][CMD][PAYLOAD...][CRC][ETX 0x03]

Lab 1 — UART Terminal

class UartTerminal : public QWidget {
    Q_OBJECT
    QSerialPort  *m_serial;
    QTextEdit    *m_log;
    QLineEdit    *m_input;
    QByteArray    m_rxBuf;

public:
    UartTerminal(QWidget *p = nullptr) : QWidget(p) {
        auto *lay = new QVBoxLayout(this);

        // Port selector
        auto *hbox = new QHBoxLayout;
        auto *portBox = new QComboBox;
        for (auto &pi : QSerialPortInfo::availablePorts())
            portBox->addItem(pi.portName());
        auto *connectBtn = new QPushButton("Connect");
        hbox->addWidget(new QLabel("Port:"));
        hbox->addWidget(portBox);
        hbox->addWidget(connectBtn);
        lay->addLayout(hbox);

        // Log
        m_log = new QTextEdit;
        m_log->setReadOnly(true);
        m_log->setFont(QFont("Courier", 10));
        lay->addWidget(m_log);

        // Input
        auto *inputRow = new QHBoxLayout;
        m_input       = new QLineEdit;
        auto *sendBtn = new QPushButton("Send");
        inputRow->addWidget(m_input);
        inputRow->addWidget(sendBtn);
        lay->addLayout(inputRow);

        m_serial = new QSerialPort(this);
        m_serial->setBaudRate(QSerialPort::Baud115200);

        connect(connectBtn, &QPushButton::clicked, [=]() {
            m_serial->setPortName(portBox->currentText());
            if (m_serial->open(QIODevice::ReadWrite))
                m_log->append("<b>Connected to " + portBox->currentText() + "</b>");
            else
                m_log->append("<font color='red'>Error: " + m_serial->errorString() + "</font>");
        });

        connect(sendBtn, &QPushButton::clicked, this, &UartTerminal::sendData);
        connect(m_input, &QLineEdit::returnPressed, this, &UartTerminal::sendData);
        connect(m_serial, &QSerialPort::readyRead, this, &UartTerminal::onRead);
    }

private slots:
    void sendData() {
        if (!m_serial->isOpen()) return;
        QString cmd = m_input->text() + "\r\n";
        m_serial->write(cmd.toUtf8());
        m_log->append("<font color='#89b4fa'>TX: " + m_input->text() + "</font>");
        m_input->clear();
    }

    void onRead() {
        m_rxBuf += m_serial->readAll();
        while (true) {
            int nl = m_rxBuf.indexOf('\n');
            if (nl < 0) break;
            QString line = QString::fromUtf8(m_rxBuf.left(nl)).trimmed();
            m_rxBuf.remove(0, nl + 1);
            m_log->append("<font color='#a6e3a1'>RX: " + line + "</font>");
        }
    }
};

Lab 2 — Modbus RTU Frame Builder

// Simple Modbus RTU helper
class ModbusRtu {
public:
    // Build Read Holding Registers request (FC=0x03)
    static QByteArray readHoldingRegs(quint8 addr, quint16 startReg, quint16 count) {
        QByteArray pdu;
        pdu.append(static_cast<char>(addr));
        pdu.append(0x03);                            // FC
        pdu.append(static_cast<char>(startReg >> 8));
        pdu.append(static_cast<char>(startReg & 0xFF));
        pdu.append(static_cast<char>(count >> 8));
        pdu.append(static_cast<char>(count & 0xFF));
        quint16 crc = crc16(pdu);
        pdu.append(static_cast<char>(crc & 0xFF));
        pdu.append(static_cast<char>(crc >> 8));
        return pdu;
    }

    static QList<quint16> parseResponse(const QByteArray &resp) {
        QList<quint16> regs;
        if (resp.size() < 5) return regs;
        int byteCount = static_cast<quint8>(resp[2]);
        for (int i = 0; i < byteCount; i += 2) {
            quint16 val = (static_cast<quint8>(resp[3+i]) << 8)
                         | static_cast<quint8>(resp[4+i]);
            regs.append(val);
        }
        return regs;
    }

private:
    static quint16 crc16(const QByteArray &data) {
        quint16 crc = 0xFFFF;
        for (quint8 b : data) {
            crc ^= b;
            for (int i = 0; i < 8; ++i)
                crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
        }
        return crc;
    }
};

// Usage
QByteArray req = ModbusRtu::readHoldingRegs(0x01, 0x0000, 10);
serial.write(req);
if (serial.waitForReadyRead(500)) {
    QByteArray resp = serial.readAll();
    auto regs = ModbusRtu::parseResponse(resp);
    for (int i = 0; i < regs.size(); ++i)
        qDebug() << QString("Reg[%1] = %2").arg(i).arg(regs[i]);
}

Interview Questions

Q1: What is the difference between readAll() and read(n) on QSerialPort?

readAll() returns all currently available bytes. read(n) returns up to n bytes. For framed protocols, use a buffer and check for complete frames — don’t rely on one readAll() per frame since TCP/serial data often arrives in fragments.

Q2: Why should you use async (signal-based) reading instead of blocking waitForReadyRead()?

Blocking calls freeze the event loop — UI stops updating, timers stop firing. Async reading via readyRead keeps the application responsive. Only use blocking reads in worker threads or simple command-line tools.

Q3: How do you reconnect automatically when a USB serial device is unplugged?

Connect to QSerialPort::errorOccurred and watch for ResourceError. Use a QTimer to poll QSerialPortInfo::availablePorts() and re-open when the port reappears.

Q4: What baud rate mismatch causes?

Garbled data (framing errors, parity errors). The receiver interprets bit timings incorrectly. Always ensure both sides use the same baud, data bits, parity, and stop bits.

Q5: How do you handle half-duplex RS-485 with QSerialPort?

RS-485 requires toggling a direction pin (DE/RE) before sending and after receiving. On Linux, use setRequestToSend(true/false) or a custom GPIO. Some USB-RS485 adapters handle this automatically.


Application: Sensor Gateway

class SensorGateway : public QObject {
    Q_OBJECT
    QSerialPort  *m_port;
    QByteArray    m_rxBuf;
    QTimer       *m_pollTimer;
    quint8        m_pollAddr = 1;  // Modbus device address

public:
    SensorGateway(const QString &portName, QObject *p = nullptr)
        : QObject(p)
    {
        m_port = new QSerialPort(this);
        m_port->setPortName(portName);
        m_port->setBaudRate(QSerialPort::Baud9600);
        m_port->setDataBits(QSerialPort::Data8);
        m_port->setParity(QSerialPort::NoParity);
        m_port->setStopBits(QSerialPort::OneStop);

        connect(m_port, &QSerialPort::readyRead, this, &SensorGateway::onRead);
        connect(m_port, &QSerialPort::errorOccurred, this, &SensorGateway::onError);

        m_pollTimer = new QTimer(this);
        m_pollTimer->setInterval(1000);
        connect(m_pollTimer, &QTimer::timeout, this, &SensorGateway::poll);
    }

    bool start() {
        if (!m_port->open(QIODevice::ReadWrite)) {
            qWarning() << "Cannot open port:" << m_port->errorString();
            return false;
        }
        m_pollTimer->start();
        qDebug() << "Gateway started on" << m_port->portName();
        return true;
    }

signals:
    void sensorData(quint8 addr, quint16 reg0, quint16 reg1);

private slots:
    void poll() {
        QByteArray req = ModbusRtu::readHoldingRegs(m_pollAddr, 0, 2);
        m_port->write(req);
    }

    void onRead() {
        m_rxBuf += m_port->readAll();
        if (m_rxBuf.size() >= 9) {  // Modbus response: addr+fc+count+2regs+crc
            auto regs = ModbusRtu::parseResponse(m_rxBuf);
            if (regs.size() >= 2)
                emit sensorData(m_pollAddr, regs[0], regs[1]);
            m_rxBuf.clear();
        }
    }

    void onError(QSerialPort::SerialPortError err) {
        if (err == QSerialPort::ResourceError) {
            qWarning() << "Device disconnected";
            m_pollTimer->stop();
        }
    }
};

// main.cpp
SensorGateway gw("/dev/ttyUSB0");
QObject::connect(&gw, &SensorGateway::sensorData,
    [](quint8 addr, quint16 r0, quint16 r1) {
        float temp = r0 / 10.0f;  // e.g., 365 → 36.5°C
        float volt = r1 / 100.0f; // e.g., 330 → 3.30V
        qDebug() << QString("Addr:%1 T=%2°C V=%3V")
            .arg(addr).arg(temp).arg(volt);
    });
gw.start();

References

ResourceLink
Qt Docs: QSerialPorthttps://doc.qt.io/qt-6/qserialport.html
Qt Docs: QSerialPortInfohttps://doc.qt.io/qt-6/qserialportinfo.html
Qt Docs: Qt Serial Port Modulehttps://doc.qt.io/qt-6/qtserialport-index.html

Next tutorial → Qt Database (SQLite)