Qtの基礎 - シリアル通信

提供: MochiuWiki : SUSE, EC, PCB

概要

QtSerialPortライブラリは、Qtライブラリのアドオンモジュールであり、ハードウェアシリアルポートとバーチャルシリアルポートの両方に単一のインターフェースを提供する。

シリアルインターフェースは、そのシンプルさと信頼性から、組み込みシステムやロボット開発等の業界ではよく使用されている。

QtSerialPortライブラリを使用することにより、開発者はシリアルインターフェイスへのアクセスが必要なQtアプリケーションの実装に必要な時間を大幅に短縮することができる。

シリアル通信には、主にRS-232CとRS-485という2つの規格がある。
RS-232Cは、ポイント・ツー・ポイント通信に適した1対1の通信方式であり、通信距離は最大約15メートル程度である。
PC周辺機器やモデム等との接続に広く使用されている。

一方、RS-485は、マルチポイント通信が可能な産業用シリアル通信規格である。
最大約1200メートルの長距離通信に対応しており、差動信号方式を採用することでノイズに強い特性を持つ。
1つのバスに複数のデバイス(最大32台、リピータを使用すればそれ以上)を接続できるため、産業用制御システムやビルオートメーション等で広く使用されている。


RS-485とRS-232Cの選択指針

通信システムを設計する場合、RS-485とRS-232Cのどちらを選択するかは、以下に示す要因を考慮する。

  • 通信距離
    15メートル以下の短距離通信であればRS-232C
    15メートル以上の長距離通信が必要な場合はRS-485
    特に工場やビル内での長距離配線が必要な場合は、RS-485の優位性が顕著である。


  • 接続デバイス数
    1対1の通信であればRS-232Cが適している。
    複数のデバイスを接続する必要がある場合はRS-485
    センサネットワークや分散制御システム等では、RS-485のマルチポイント接続が必須となる。


  • ノイズ環境
    ノイズの少ないオフィス環境であればRS-232Cで問題ない。
    産業環境等のノイズが多い環境ではRS-485の差動信号方式が有利である。
    モータやインバータ等のノイズ源が近くにある場合は、RS-485が推奨される。


  • コストと実装の複雑さ
    RS-232Cは実装が単純で、追加のハードウェアが不要である。
    RS-485は終端抵抗やRTS制御等の追加実装が必要だが、長距離やマルチポイント通信のメリットがある。
    プロジェクトの要件と予算を考慮して、適切な規格を選択する必要がある。


  • 通信速度
    RS-232Cは最大115.2[kbps]程度が実用的である。
    RS-485は最大10[Mbps]まで対応可能だが、距離とのトレードオフがある。
    高速通信が必要な場合でも、通信距離を考慮して適切なボーレートを選択する必要がある。



RS-232C通信

RS-232Cは、最も基本的なシリアル通信規格であり、1対1のポイント・ツー・ポイント通信に使用される。
QtSerialPortクラスは、RS-232C通信を簡単に実装するための機能を提供している。

プロジェクトへの追加

Qtのシリアルポートモジュールをプロジェクトファイルに追加する。

 # QMake
 
 QT += core serialport


 # CMake
 
 find_package(Qt6 REQUIRED COMPONENTS SerialPort)
 target_link_libraries(mytarget PRIVATE Qt6::SerialPort)


データの送信

以下の例では、ボーレートは9600[bps]、データビットは8[ビット]、パリティなし、ストップビットは1[ビット]の8N1で、シリアル通信を行っている。

 #include <QCoreApplication>
 #include <QSerialPort>
 #include <QSerialPortInfo>
 #include <QDebug>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QSerialPort serial;
    serial.setPortName("/dev/ttyUSB0");                // シリアルポート名 (環境に合わせて変更すること)
    serial.setBaudRate(QSerialPort::Baud9600);         // ボーレート 9600[bps]
    serial.setDataBits(QSerialPort::Data8);            // データビット 8[ビット]
    serial.setParity(QSerialPort::NoParity);           // パリティなし
    serial.setStopBits(QSerialPort::OneStop);          // ストップビット 1[ビット]
    serial.setFlowControl(QSerialPort::NoFlowControl); // フロー制御なし
 
    if (!serial.open(QIODevice::ReadWrite)) {
       qDebug() << "Could not open serial port : " << serial.errorString();
       return -1;
    }
 
    // データの送信
    QByteArray dataToSend = "Hello, Serial Port!";
    serial.write(dataToSend);
 
    serial.close();
  
    return a.exec();
 }


データの受信

同期式

以下の例では、同期的にデータを受信している。
実用的な設計を行う場合は、信頼性と応答性を高めるために、シグナルおよびスロットを使用した非同期でデータを受信する方法が推奨される。

 #include <QCoreApplication>
 #include <QSerialPort>
 #include <QSerialPortInfo>
 #include <QDebug>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    QSerialPort serial;
    serial.setPortName("/dev/ttyUSB0");                // シリアルポート名 (環境に合わせて変更すること)
    serial.setBaudRate(QSerialPort::Baud9600);         // ボーレート 9600[bps]
    serial.setDataBits(QSerialPort::Data8);            // データビット 8[ビット]
    serial.setParity(QSerialPort::NoParity);           // パリティなし
    serial.setStopBits(QSerialPort::OneStop);          // ストップビット 1[ビット]
    serial.setFlowControl(QSerialPort::NoFlowControl); // フロー制御なし
 
    if (!serial.open(QIODevice::ReadWrite)) {
       qDebug() << "Could not open serial port : " << serial.errorString();
       return -1;
    }
 
    // データの受信 (同期式)
    // 非同期でデータを受信する方法を推奨する
    // 1[Sec]待機
    if (serial.waitForReadyRead(1000)) {
       QByteArray responseData = serial.readAll();
       while (serial.waitForReadyRead(10))
          responseData += serial.readAll();
 
       qDebug() << "Recieved data : " << responseData;
    }
    else {
       qDebug() << "Failed to recieve data";
    }
 
    serial.close();
  
    return a.exec();
 }


非同期

非同期でデータを受信する場合、QSerialPortクラスのシグナルとスロットのメカニズムを使用する。
これにより、データが受信された時に自動的に通知されて、アプリケーションのメインループをブロックすることなくデータを受信することができる。

まず、QObjectクラスを継承したクラスを定義する。
シリアルポートからのデータの受信を管理するためのスロットを用意する。
以下の例では、シリアルポートがデータを受信したことを検出するために、readyReadシグナルを使用している。

 // SerialPortReader.hファイル
 
 #include <QCoreApplication>
 #include <QSerialPort>
 #include <QDebug>
 
 class SerialPortReader : public QObject
 {
    Q_OBJECT
 public:
    SerialPortReader(QSerialPort *serialPort, QObject *parent = nullptr) : QObject(parent), m_serialPort(serialPort)
    {
       connect(m_serialPort, &QSerialPort::readyRead, this, &SerialPortReader::handleReadyRead);
    }
 
 private slots:
    void handleReadyRead()
    {
       const QByteArray data = m_serialPort->readAll();
       qDebug() << "Recieved data : " << data;
    }
 
 private:
    QSerialPort *m_serialPort;
 };


 // main.cppファイル
 
 #include "SerialPortReader.h"
 #include <QCoreApplication>
 #include <QSerialPort>
 #include <QDebug>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication app(argc, argv);
 
    QSerialPort serial;
    serial.setPortName("/dev/ttyUSB0");                // シリアルポート名 (環境に合わせて変更すること)
    serial.setBaudRate(QSerialPort::Baud9600);         // ボーレート 9600[bps]
    serial.setDataBits(QSerialPort::Data8);            // データビット 8[ビット]
    serial.setParity(QSerialPort::NoParity);           // パリティなし
    serial.setStopBits(QSerialPort::OneStop);          // ストップビット 1[ビット]
    serial.setFlowControl(QSerialPort::NoFlowControl); // フロー制御なし
 
    if (!serial.open(QIODevice::ReadWrite)) {
       qDebug() << "Could not open serial port : " << serial.errorString();
       return -1;
    }
 
    SerialPortReader reader(&serial);
 
    return app.exec();
 }


データの送受信

非同期

非同期でデータを受信する場合、QSerialPortクラスのシグナルとスロットのメカニズムを使用する。
これにより、データが受信された時に自動的に通知されて、アプリケーションのメインループをブロックすることなくデータを受信することができる。

まず、QObjectクラスを継承したクラスを定義する。
シリアルポートからのデータの送受信を管理するためのスロットを用意する。

シリアルポートがデータを受信したことを検出するために、readyReadシグナルを使用する。

シリアルポートがデータを送信完了したことを検出するために、QIODevice::bytesWrittenシグナルを使用する。
QIODevice::bytesWrittenシグナルは、データがデバイスのバッファに書き込まれた時に発生する。
これは必ずしも物理的な送信完了を意味するわけではない。
大量のデータを送信する場合、QIODevice::bytesWrittenシグナルは複数回発生する可能性がある。

そのため、送信データのサイズを記録して、QIODevice::bytesWrittenシグナルで受信したバイト数の合計がそのサイズに達した時に送信完了とみなす。

以下の例では、8N1設定の非同期シリアル通信を行っている。

 // SerialCommunication.hファイル
 
 #include <QCoreApplication>
 #include <QSerialPort>
 #include <QDebug>
 
 class SerialCommunication : public QObject
 {
    Q_OBJECT
 
 private:
    QSerialPort *m_serialPort;
    qint64      m_bytesWritten;
    qint64      m_totalBytesToWrite;
 
 public:
    explicit SerialCommunication(QObject *parent = nullptr) : QObject(parent), m_bytesWritten(0), m_totalBytesToWrite(0)
    {
        m_serialPort = new QSerialPort(this);
        connect(m_serialPort, &QSerialPort::readyRead, this, &SerialCommunication::handleReadyRead);
        connect(m_serialPort, &QSerialPort::errorOccurred, this, &SerialCommunication::handleError);
        connect(m_serialPort, &QSerialPort::bytesWritten, this, &SerialCommunication::handleBytesWritten);
    }
 
    bool openPort(const QString &portName)
    {
       m_serialPort->setPortName(portName);
       m_serialPort->setBaudRate(QSerialPort::Baud9600);
       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)) {
          qDebug() << "シリアルポートのオープンに成功";
          return true;
       }
       else {
          qDebug() << "シリアルポートのオープンに失敗: " << m_serialPort->errorString();
          return false;
       }
    }
 
    void sendData(const QByteArray &data)
    {
       if (m_serialPort->isOpen()) {
          m_bytesWritten = 0;
          m_totalBytesToWrite = data.size();
          qint64 bytesWritten = m_serialPort->write(data);
 
          if (bytesWritten == -1) {
             qDebug() << "ポートへのデータの書き込みに失敗: " << m_serialPort->errorString();
          }
          else if (bytesWritten != data.size()) {
             qDebug() << "ポートへの全データの書き込みに失敗 - 書き込まれたバイト数: " << bytesWritten;
          }
          m_serialPort->flush();
       }
       else {
          qDebug() << "データの送信に失敗 : シリアルポートがオープンされていない";
       }
    }
 
 signals:
    void transmissionComplete();
 
 private slots:
    void handleReadyRead()
    {
       QByteArray data = m_serialPort->readAll();
       qDebug() << "受信データ: " << data;
 
       // ここで受信したデータを処理する
       // ...略
    }
 
    void handleBytesWritten(qint64 bytes)
    {
       m_bytesWritten += bytes;
       qDebug() << "Bytes written:" << bytes << "Total:" << m_bytesWritten << "of" << m_totalBytesToWrite;
 
       if (m_bytesWritten >= m_totalBytesToWrite) {
          qDebug() << "Transmission complete!";
          emit transmissionComplete();
 
          // ここで送信完了後の処理を行う
          // ...略
       }
    }
 
    void handleError(QSerialPort::SerialPortError error)
    {
       if (error == QSerialPort::NoError) {
          return;
       }
 
       qDebug() << "予期せぬエラー: " << m_serialPort->errorString();
 
       switch (error) {
          case QSerialPort::DeviceNotFoundError:
             qDebug() << "デバイスが見つからない : 接続を確認すること";
             break;
          case QSerialPort::PermissionError:
             qDebug() << "パーミッションエラー : 他のアプリケーションがそのポートを使用していないか確認すること";
             break;
          case QSerialPort::OpenError:
             qDebug() << "ポートのオープンに失敗 : 既に使用されていないか確認すること";
             break;
          case QSerialPort::WriteError:
             qDebug() << "送信エラー : 接続を確認して、再度送信できるかどうかを試すこと";
             break;
          case QSerialPort::ReadError:
             qDebug() << "受信エラー : 接続を確認して、再度受信できるかどうかを試すこと";
             break;
          case QSerialPort::ResourceError:
             qDebug() << "The port has been unexpectedly removed. Check the connection.";
             m_serialPort->close();
             break;
          default:
             qDebug() << "不明なエラー : 設定を確認すること";
       }
    }
 };


 // main.cppファイル
 
 #include "SerialCommunication.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    SerialCommunication serialComm;
 
    QObject::connect(&serialComm, &SerialCommunication::transmissionComplete, []() {
       qDebug() << "送信完了のシグナルを受信した時";
       // ここで送信完了後の処理を行う
       // ...略
    });
 
    // 任意のポート名を指定すること
    if (serialComm.openPort("COM1")) { 
       serialComm.sendData("Hello, Serial Port!");
    }
 
    return a.exec();
 }



RS-485通信

RS-485は、RS-232Cとは異なり、マルチポイント通信が可能な産業用シリアル通信規格である。
長距離通信とノイズ耐性に優れており、産業用制御システムで広く採用されている。

QtSerialPortクラスは、RS-485通信の実装に必要な基本的な機能を提供しているが、半二重通信の制御は開発者が実装する必要がある。

RS-485の特徴と実装時の注意点

RS-485通信を実装する際には、以下の特徴と注意点を理解する必要がある。

  • 差動信号方式について
    RS-485は、2本の信号線であるA線とB線を使用して、電位差でデータを伝送する。
    この方式により、ノイズの影響を受けにくく、長距離通信に適している。
    ノイズは両方の線に均等に影響するため、電位差を見ることでノイズの影響を打ち消すことができる。


  • 半二重通信
    一般的な2線式RS-485では、送信と受信を同時に行えない半二重通信となる。
    送信時には受信を停止し、受信時には送信を停止する必要がある。
    RTS信号を使用して、送受信の切り替えを制御する。
    この制御を適切に行わないと、データの衝突や損失が発生する可能性がある。


  • マルチポイント接続
    1つのバスに複数のデバイスを接続できるという特徴がある。
    各デバイスには一意のアドレスを割り当てる必要があり、通常は1から247までの値を使用する。
    マスター・スレーブ方式またはマルチマスター方式で通信を行うが、産業用途ではマスター・スレーブ方式が一般的である。


  • バス終端抵抗
    バスの両端に120[Ω]の終端抵抗を接続する必要がある。
    これは信号の反射を防ぎ、通信の信頼性を向上させるための重要な要素である。
    終端抵抗はハードウェア側の設定であり、ソフトウェアでは制御しない。


  • 通信距離と速度
    RS-485は最大約1200メートルの通信が可能である。
    ただし、ボーレートは通信距離に応じて調整する必要がある。
    高速通信では距離が短くなり、長距離では低速になる傾向がある。
    例えば、9600[bps]では約1200メートル、115200[bps]では約100メートル程度が目安となる。


RTS制御による半二重通信

RS-485では、RTS信号を使用して送信と受信を切り替える。
QSerialPortクラスでは、setRequestToSendメソッドを使用してRTS信号を制御できる。

以下の例では、RTS制御を実装した半二重通信を示している。

RTS制御の基本について説明する。
送信前にRTSをアサートして送信モードに切り替える。
データ送信後、一定時間待機してからRTSをデアサートして受信モードに戻す。
この待機時間は、データが完全に送信されるまでの時間を考慮して設定する。

待機時間の計算方法について説明する。
待機時間は、送信バイト数とボーレートから計算できる。
計算式は、送信バイト数かける10ビットをボーレートで割った値にマージンを加えたものとなる。
1バイトは通常、スタートビット、データビット8つ、ストップビットで10ビットとなる。
マージンは、ハードウェアの遅延やタイミングのズレを考慮して追加する。

 // RS485HalfDuplex.hファイル
 
 #ifndef RS485HALFDUPLEX_H
 #define RS485HALFDUPLEX_H
 
 #include <QObject>
 #include <QSerialPort>
 #include <QTimer>
 #include <QDebug>
 
 /**
  * RS-485半二重通信を制御するクラス
  * RTS信号を使用して送受信の切り替えを行う
  */
 class RS485HalfDuplex : public QObject
 {
    Q_OBJECT
 
 private:
    QSerialPort *m_serialPort;   // シリアルポートオブジェクト
    int m_baudRate;               // ボーレート(送信完了待機時間の計算に使用)
    QTimer *m_transmitTimer;      // 送信完了を監視するタイマ
    qint64 m_bytesToTransmit;     // 送信予定のバイト数
 
 public:
    explicit RS485HalfDuplex(QObject *parent = nullptr)
        : QObject(parent), m_baudRate(9600)
    {
        m_serialPort = new QSerialPort(this);
        m_transmitTimer = new QTimer(this);
        m_transmitTimer->setSingleShot(true);
 
        // データ受信時のシグナル接続
        connect(m_serialPort, &QSerialPort::readyRead,
                this, &RS485HalfDuplex::handleReadyRead);
 
        // 送信完了タイマのシグナル接続
        connect(m_transmitTimer, &QTimer::timeout,
                this, &RS485HalfDuplex::handleTransmitComplete);
 
        // エラー処理のシグナル接続
        connect(m_serialPort, &QSerialPort::errorOccurred,
                this, &RS485HalfDuplex::handleError);
    }
 
    /**
     * RS-485ポートを開く
     * @param portName ポート名(例: "/dev/ttyUSB0" または "COM1")
     * @param baudRate ボーレート
     * @return 成功した場合true
     */
    bool openPort(const QString &portName, int baudRate = 9600)
    {
        m_baudRate = baudRate;
 
        m_serialPort->setPortName(portName);
        m_serialPort->setBaudRate(baudRate);
        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)) {
            qDebug() << "RS-485ポートのオープンに失敗:" << m_serialPort->errorString();
            return false;
        }
 
        // 初期状態は受信モード(RTS = false)
        m_serialPort->setRequestToSend(false);
        qDebug() << "RS-485ポートをオープン(受信モード)";
 
        return true;
    }
 
    /**
     * データを送信する
     * RTS制御により送信モードに切り替えてからデータを送信する
     * @param data 送信するデータ
     */
    void sendData(const QByteArray &data)
    {
        if (!m_serialPort->isOpen()) {
            qDebug() << "エラー: ポートがオープンされていない";
            return;
        }
 
        m_bytesToTransmit = data.size();
 
        // 送信モードに切り替え(RTS = true)
        m_serialPort->setRequestToSend(true);
 
        // RTSが有効になるまで少し待機(ハードウェアの応答時間を考慮)
        // 実際のハードウェアに応じて調整が必要な場合がある
        QThread::msleep(10);
 
        // データを送信
        qint64 bytesWritten = m_serialPort->write(data);
        m_serialPort->flush();
 
        if (bytesWritten == -1) {
            qDebug() << "送信エラー:" << m_serialPort->errorString();
            m_serialPort->setRequestToSend(false);
            return;
        }
 
        qDebug() << "データ送信:" << data << "(" << bytesWritten << "バイト)";
 
        // 送信完了待機時間を計算
        // 計算式: (送信バイト数 × 10ビット) / ボーレート × 1000[ミリ秒] + マージン
        int transmissionTime = static_cast<int>((m_bytesToTransmit * 10.0 / m_baudRate) * 1000) + 10;
 
        // タイマを開始して送信完了を待つ
        m_transmitTimer->start(transmissionTime);
    }
 
    /**
     * ポートを閉じる
     */
    void closePort()
    {
        if (m_serialPort->isOpen()) {
            m_serialPort->close();
            qDebug() << "RS-485ポートを閉じた";
        }
    }
 
 signals:
    void dataReceived(const QByteArray &data);   // データ受信時のシグナル
    void dataSent();                              // データ送信完了時のシグナル
 
 private slots:
    /**
     * データ受信時の処理
     * 受信モード(RTS = false)の時のみデータを読み取る
     */
    void handleReadyRead()
    {
        // 送信モードの時は受信データを無視する
        if (m_serialPort->requestToSend()) {
            return;
        }
 
        QByteArray data = m_serialPort->readAll();
        qDebug() << "データ受信:" << data;
        emit dataReceived(data);
    }
 
    /**
     * 送信完了時の処理
     * 受信モードに切り替える
     */
    void handleTransmitComplete()
    {
        // 受信モードに切り替え(RTS = false)
        m_serialPort->setRequestToSend(false);
        qDebug() << "送信完了(受信モードに切り替え)";
        emit dataSent();
    }
 
    /**
     * エラー処理
     */
    void handleError(QSerialPort::SerialPortError error)
    {
        if (error == QSerialPort::NoError) {
            return;
        }
 
        qDebug() << "RS-485エラー:" << m_serialPort->errorString();
    }
 };
 
 #endif // RS485HALFDUPLEX_H


 // main.cppファイル
 
 #include "RS485HalfDuplex.h"
 #include <QCoreApplication>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication app(argc, argv);
 
    RS485HalfDuplex rs485;
 
    // データ受信時の処理
    QObject::connect(&rs485, &RS485HalfDuplex::dataReceived, [](const QByteArray &data) {
        qDebug() << "アプリケーションで受信したデータ:" << data;
        // ここでデータ処理を行う
    });
 
    // データ送信完了時の処理
    QObject::connect(&rs485, &RS485HalfDuplex::dataSent, []() {
        qDebug() << "アプリケーション: 送信完了";
        // ここで次の処理を行う
    });
 
    // RS-485ポートを開く
    if (rs485.openPort("/dev/ttyUSB0", 9600)) {
        // データを送信
        rs485.sendData("Hello, RS-485!");
    }
 
    return app.exec();
 }


Modbusプロトコル

RS-485通信では、Modbusプロトコルが最も広く使用されている。
Modbusは、産業用機器間の通信に特化したシンプルで信頼性の高いプロトコルである。

  • Modbus RTUの基本構造
    メッセージは、デバイスアドレス、ファンクションコード、データ、CRCの順に構成される。
    • デバイスアドレス
      1バイトで、通信相手のデバイスを識別する。
    • ファンクションコード
      1バイトで、実行する操作を指定する。
      データは可変長で、読み書きするデータやアドレスを含む。
    • CRC
      2バイトで、エラー検出用のチェックサムとして使用される。


  • 主要なファンクションコードについて
    これらのファンクションコードを使用することで、ほとんどの産業用制御が可能になる。
    • 0x03
      ホールディングレジスタの読み取りに使用される。
    • 0x06
      単一レジスタへの書き込みに使用される。
    • 0x10
      複数レジスタへの書き込みに使用される。


CRC計算において、Modbus RTUでは、CRC-16を使用してデータの整合性を確認する。
CRCは、メッセージ全体であるアドレスからデータまでに対して計算される。
受信側では、受信したCRCと計算したCRCを比較して、データの正当性を検証する。

  • CRCの初期化:
    (初期値は全ビット1)
  • 各バイト に対して:
    (排他的論理和)
  • 8回繰り返し (各ビットに対して) :

    反復計算の詳細:

    各ビット に対して:
  • 最終結果:
    (リトルエンディアン形式)


 // ModbusRTU.hファイル
 
 #ifndef MODBUSRTU_H
 #define MODBUSRTU_H
 
 #include <QObject>
 #include <QSerialPort>
 #include <QTimer>
 #include <QDebug>
 #include <QVector>
 
 /**
  * Modbus RTU通信を実装するクラス
  * RS-485上でModbusプロトコルを使用してデバイスと通信する
  */
 class ModbusRTU : public QObject
 {
    Q_OBJECT
 
 private:
    QSerialPort *m_serialPort;
    int m_baudRate;
    QTimer *m_transmitTimer;
    QTimer *m_responseTimer;
    QByteArray m_receiveBuffer;
    int m_expectedResponseLength;
 
 public:
    explicit ModbusRTU(QObject *parent = nullptr) : QObject(parent), m_baudRate(9600), m_expectedResponseLength(0)
    {
       m_serialPort = new QSerialPort(this);
       m_transmitTimer = new QTimer(this);
       m_transmitTimer->setSingleShot(true);
 
       m_responseTimer = new QTimer(this);
       m_responseTimer->setSingleShot(true);
 
       connect(m_serialPort, &QSerialPort::readyRead, this, &ModbusRTU::handleReadyRead);
       connect(m_transmitTimer, &QTimer::timeout, this, &ModbusRTU::handleTransmitComplete);
       connect(m_responseTimer, &QTimer::timeout, this, &ModbusRTU::handleResponseTimeout);
    }
 
    /**
     * Modbusポートを開く
     */
    bool openPort(const QString &portName, int baudRate = 9600)
    {
       m_baudRate = baudRate;
 
       m_serialPort->setPortName(portName);
       m_serialPort->setBaudRate(baudRate);
       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)) {
          qDebug() << "Modbusポートのオープンに失敗:" << m_serialPort->errorString();
          return false;
       }
 
       m_serialPort->setRequestToSend(false);
       qDebug() << "Modbusポートをオープン";
 
       return true;
    }
 
    /**
     * ホールディングレジスタを読み取る(ファンクションコード 0x03)
     * @param slaveAddress スレーブアドレス(1-247)
     * @param startAddress 開始アドレス
     * @param numberOfRegisters レジスタ数
     */
    void readHoldingRegisters(quint8 slaveAddress, quint16 startAddress, quint16 numberOfRegisters)
    {
       // リクエストメッセージの構築
       QByteArray request;
       request.append(slaveAddress);                           // スレーブアドレス
       request.append(0x03);                                   // ファンクションコード(Read Holding Registers)
       request.append(static_cast<char>(startAddress >> 8));   // 開始アドレス上位バイト
       request.append(static_cast<char>(startAddress & 0xFF)); // 開始アドレス下位バイト
       request.append(static_cast<char>(numberOfRegisters >> 8));   // レジスタ数上位バイト
       request.append(static_cast<char>(numberOfRegisters & 0xFF)); // レジスタ数下位バイト
 
       // CRCの計算と追加
       quint16 crc = calculateCRC(request);
       request.append(static_cast<char>(crc & 0xFF));     // CRC下位バイト
       request.append(static_cast<char>(crc >> 8));       // CRC上位バイト
 
       // 期待される応答サイズを計算
       // 応答サイズ = スレーブアドレス(1) + ファンクションコード(1) + バイト数(1) + データ(N) + CRC(2)
       m_expectedResponseLength = 5 + (numberOfRegisters * 2);
 
       qDebug() << "Read Holding Registers リクエスト送信:"
                << "Slave=" << slaveAddress
                << "Start=" << startAddress
                << "Count=" << numberOfRegisters;
 
       sendModbusRequest(request);
    }
 
    /**
     * 単一レジスタに書き込む(ファンクションコード 0x06)
     * @param slaveAddress スレーブアドレス
     * @param registerAddress レジスタアドレス
     * @param value 書き込む値
     */
    void writeSingleRegister(quint8 slaveAddress, quint16 registerAddress, quint16 value)
    {
       // リクエストメッセージの構築
       QByteArray request;
       request.append(slaveAddress);                              // スレーブアドレス
       request.append(0x06);                                      // ファンクションコード(Write Single Register)
       request.append(static_cast<char>(registerAddress >> 8));   // レジスタアドレス上位バイト
       request.append(static_cast<char>(registerAddress & 0xFF)); // レジスタアドレス下位バイト
       request.append(static_cast<char>(value >> 8));             // 値上位バイト
       request.append(static_cast<char>(value & 0xFF));           // 値下位バイト
 
       // CRCの計算と追加
       quint16 crc = calculateCRC(request);
       request.append(static_cast<char>(crc & 0xFF));
       request.append(static_cast<char>(crc >> 8));
 
       // 応答はリクエストのエコーバック(同じ長さ)
       m_expectedResponseLength = 8;
 
       qDebug() << "Write Single Register リクエスト送信:"
                << "Slave=" << slaveAddress
                << "Address=" << registerAddress
                << "Value=" << value;
 
       sendModbusRequest(request);
    }
 
 signals:
    void holdingRegistersReceived(quint8 slaveAddress, QVector<quint16> values);
    void writeCompleted(quint8 slaveAddress, bool success);
    void modbusError(const QString &errorMessage);
 
 private:
    /**
     * Modbusリクエストを送信する
     */
    void sendModbusRequest(const QByteArray &request)
    {
       m_receiveBuffer.clear();
 
       // 送信モードに切り替え
       m_serialPort->setRequestToSend(true);
       QThread::msleep(10);
 
       // データを送信
       m_serialPort->write(request);
       m_serialPort->flush();
 
       // 送信完了待機時間を計算
       int transmissionTime = static_cast<int>((request.size() * 10.0 / m_baudRate) * 1000) + 10;
       m_transmitTimer->start(transmissionTime);
    }
 
    /**
     * 送信完了時の処理
     */
    void handleTransmitComplete()
    {
       // 受信モードに切り替え
       m_serialPort->setRequestToSend(false);
 
       // 応答タイムアウトを設定(2秒)
       m_responseTimer->start(2000);
    }
 
    /**
     * データ受信時の処理
     */
    void handleReadyRead()
    {
       if (m_serialPort->requestToSend()) {
          return;
       }
 
       m_receiveBuffer.append(m_serialPort->readAll());
 
       // 期待される長さのデータが受信できたら処理する
       if (m_receiveBuffer.size() >= m_expectedResponseLength) {
          m_responseTimer->stop();
          processModbusResponse(m_receiveBuffer);
          m_receiveBuffer.clear();
       }
    }
 
    /**
     * 応答タイムアウト時の処理
     */
    void handleResponseTimeout()
    {
       qDebug() << "Modbus応答タイムアウト";
       emit modbusError("応答タイムアウト");
       m_receiveBuffer.clear();
    }
 
    /**
     * Modbus応答を処理する
     */
    void processModbusResponse(const QByteArray &response)
    {
       if (response.size() < 5) {
          emit modbusError("応答が短すぎる");
          return;
       }
 
       // CRCの検証
       quint16 receivedCRC = static_cast<quint16>((static_cast<quint8>(response[response.size() - 1]) << 8) | static_cast<quint8>(response[response.size() - 2]));
       quint16 calculatedCRC = calculateCRC(response.left(response.size() - 2));
 
       if (receivedCRC != calculatedCRC) {
          qDebug() << "CRCエラー: 受信=" << QString::number(receivedCRC, 16)
                   << "計算=" << QString::number(calculatedCRC, 16);
          emit modbusError("CRCエラー");
          return;
       }
 
       quint8 slaveAddress = static_cast<quint8>(response[0]);
       quint8 functionCode = static_cast<quint8>(response[1]);
 
       qDebug() << "Modbus応答受信: Slave=" << slaveAddress << "Function=" << functionCode;
 
       // ファンクションコードに応じた処理
       if (functionCode == 0x03) {
          // Read Holding Registersの応答
          int byteCount = static_cast<quint8>(response[2]);
          QVector<quint16> values;
 
          for (int i = 0; i < byteCount; i += 2) {
             quint16 value = static_cast<quint16>((static_cast<quint8>(response[3 + i]) << 8) | static_cast<quint8>(response[4 + i]));
             values.append(value);
          }
 
          qDebug() << "読み取った値:" << values;
          emit holdingRegistersReceived(slaveAddress, values);
       }
       else if (functionCode == 0x06) {
          // Write Single Registerの応答(エコーバック)
          qDebug() << "書き込み成功";
          emit writeCompleted(slaveAddress, true);
       }
       else if (functionCode & 0x80) {
          // エラー応答(ファンクションコードの最上位ビットが1)
          quint8 exceptionCode = static_cast<quint8>(response[2]);
          qDebug() << "Modbusエラー: Exception Code=" << exceptionCode;
          emit modbusError(QString("Exception Code: %1").arg(exceptionCode));
       }
    }
 
    /**
     * Modbus CRC-16を計算する
     */
    quint16 calculateCRC(const QByteArray &data)
    {
       quint16 crc = 0xFFFF;
 
       for (int i = 0; i < data.size(); ++i) {
          crc ^= static_cast<quint8>(data[i]);
 
          for (int j = 0; j < 8; ++j) {
             if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
             }
             else {
                crc >>= 1;
             }
          }
       }
 
       return crc;
    }
 };
 
 #endif // MODBUSRTU_H


 // main.cppファイル
 
 #include "ModbusRTU.h"
 #include <QCoreApplication>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication app(argc, argv);
 
    ModbusRTU modbus;
 
    // レジスタ読み取り結果の処理
    QObject::connect(&modbus, &ModbusRTU::holdingRegistersReceived, [](quint8 slaveAddress, QVector<quint16> values) {
       qDebug() << "スレーブ" << slaveAddress << "からレジスタ読み取り完了:";
       for (int i = 0; i < values.size(); ++i) {
          qDebug() << "  レジスタ" << i << "=" << values[i];
       }
    });
 
    // 書き込み完了の処理
    QObject::connect(&modbus, &ModbusRTU::writeCompleted, [](quint8 slaveAddress, bool success) {
       qDebug() << "スレーブ" << slaveAddress << "への書き込み" << (success ? "成功" : "失敗");
    });
 
    // エラー処理
    QObject::connect(&modbus, &ModbusRTU::modbusError, [](const QString &errorMessage) {
       qDebug() << "Modbusエラー:" << errorMessage;
    });
 
    // Modbusポートを開く
    if (modbus.openPort("/dev/ttyUSB0", 9600)) {
       // スレーブアドレス1のレジスタ0番地から2ワード読み取り
       modbus.readHoldingRegisters(1, 0, 2);
 
       // スレーブアドレス1のレジスタ0番地に値1234を書き込み
       // modbus.writeSingleRegister(1, 0, 1234);
    }
 
    return app.exec();
 }


マルチドロップ通信

RS-485では、複数のデバイスを1つのバスに接続するマルチドロップ構成が可能である。
以下の例では、複数のスレーブデバイスと通信するマスターデバイスの実装を示している。

  • アドレス指定について
    各スレーブデバイスには一意のアドレスを割り当てる必要がある。
    アドレスは通常1から247までの値を使用し、0はブロードキャストアドレスとして予約されている。
    マスターは、通信したいスレーブのアドレスを指定してメッセージを送信する。


  • ポーリング方式について
    マスターが順番に各スレーブにリクエストを送信する方式である。
    各スレーブは自分のアドレス宛のメッセージにのみ応答する。
    この方式により、バス上でのデータ衝突を防ぐことができる。


  • バスの競合回避
    同時に複数のデバイスが送信しないよう制御する必要がある。
    マスター・スレーブ方式では、マスターのみが通信を開始できるため、競合が発生しない。
    スレーブは、マスターからの問い合わせに対してのみ応答する。


 // RS485MultiDropMaster.hファイル
 
 #ifndef RS485MULTIDROPMASTER_H
 #define RS485MULTIDROPMASTER_H
 
 #include <QObject>
 #include <QSerialPort>
 #include <QTimer>
 #include <QVector>
 #include <QDebug>
 
 /**
  * RS-485マルチドロップ通信のマスターデバイスクラス
  * 複数のスレーブデバイスとポーリング方式で通信する
  */
 class RS485MultiDropMaster : public QObject
 {
    Q_OBJECT
 
 private:
    QSerialPort *m_serialPort;
    int m_baudRate;
    QVector<quint8> m_slaveAddresses;    // スレーブアドレスのリスト
    int m_currentSlaveIndex;             // 現在ポーリング中のスレーブインデックス
    QTimer *m_transmitTimer;             // 送信完了監視タイマ
    QTimer *m_responseTimer;             // 応答タイムアウトタイマ
    QTimer *m_pollingTimer;              // ポーリング間隔タイマ
    QByteArray m_receiveBuffer;          // 受信バッファ
    int m_pollingInterval;               // ポーリング間隔(ミリ秒)
 
 public:
    explicit RS485MultiDropMaster(QObject *parent = nullptr) : QObject(parent), m_baudRate(9600), m_currentSlaveIndex(0), m_pollingInterval(1000)
    {
       m_serialPort = new QSerialPort(this);
       m_transmitTimer = new QTimer(this);
       m_transmitTimer->setSingleShot(true);
 
       m_responseTimer = new QTimer(this);
       m_responseTimer->setSingleShot(true);
 
       m_pollingTimer = new QTimer(this);
 
       connect(m_serialPort, &QSerialPort::readyRead, this, &RS485MultiDropMaster::handleReadyRead);
       connect(m_transmitTimer, &QTimer::timeout, this, &RS485MultiDropMaster::handleTransmitComplete);
       connect(m_responseTimer, &QTimer::timeout, this, &RS485MultiDropMaster::handleResponseTimeout);
       connect(m_pollingTimer, &QTimer::timeout, this, &RS485MultiDropMaster::pollNextSlave);
    }
 
    /**
     * マスターを初期化する
     * @param portName ポート名
     * @param slaveAddresses スレーブアドレスのリスト
     * @param baudRate ボーレート
     * @param pollingInterval ポーリング間隔(ミリ秒)
     */
    bool initialize(const QString &portName, const QVector<quint8> &slaveAddresses, int baudRate = 9600, int pollingInterval = 1000)
    {
       m_baudRate = baudRate;
       m_slaveAddresses = slaveAddresses;
       m_pollingInterval = pollingInterval;
 
       m_serialPort->setPortName(portName);
       m_serialPort->setBaudRate(baudRate);
       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)) {
          qDebug() << "RS-485マルチドロップマスターのオープンに失敗:"
                   << m_serialPort->errorString();
          return false;
       }
 
       m_serialPort->setRequestToSend(false);
 
       qDebug() << "RS-485マルチドロップマスターを初期化";
       qDebug() << "スレーブ数:" << m_slaveAddresses.size();
       qDebug() << "ポーリング間隔:" << m_pollingInterval << "ミリ秒";
 
       return true;
    }
 
    /**
     * ポーリングを開始する
     */
    void startPolling()
    {
       if (m_slaveAddresses.isEmpty()) {
          qDebug() << "エラー: スレーブが登録されていない";
          return;
       }
 
       qDebug() << "ポーリング開始";
       m_currentSlaveIndex = 0;
       pollNextSlave();
    }
 
    /**
     * ポーリングを停止する
     */
    void stopPolling()
    {
       m_pollingTimer->stop();
       m_transmitTimer->stop();
       m_responseTimer->stop();
       qDebug() << "ポーリング停止";
    }
 
 signals:
    void slaveDataReceived(quint8 slaveAddress, const QByteArray &data);
    void slaveNoResponse(quint8 slaveAddress);
 
 private slots:
    /**
     * 次のスレーブをポーリングする
     */
    void pollNextSlave()
    {
       if (m_slaveAddresses.isEmpty()) {
          return;
       }
 
       quint8 slaveAddress = m_slaveAddresses[m_currentSlaveIndex];
       qDebug() << "スレーブ" << slaveAddress << "をポーリング"
                << "(" << (m_currentSlaveIndex + 1) << "/" << m_slaveAddresses.size() << ")";
 
       // 簡単なデータ要求メッセージを構築
       // 実際のプロトコルに応じて適切なメッセージを構築する必要がある
       QByteArray request;
       request.append(slaveAddress);
       request.append("DATA_REQUEST");
       sendRequest(request);
 
       // 次のスレーブインデックスを更新
       m_currentSlaveIndex = (m_currentSlaveIndex + 1) % m_slaveAddresses.size();
    }
 
    /**
     * リクエストを送信する
     */
    void sendRequest(const QByteArray &request)
    {
       m_receiveBuffer.clear();
 
       // 送信モードに切り替え
       m_serialPort->setRequestToSend(true);
       QThread::msleep(10);
 
       // データを送信
       m_serialPort->write(request);
       m_serialPort->flush();
 
       // 送信完了待機時間を計算
       int transmissionTime = static_cast<int>((request.size() * 10.0 / m_baudRate) * 1000) + 10;
       m_transmitTimer->start(transmissionTime);
    }
 
    /**
     * 送信完了時の処理
     */
    void handleTransmitComplete()
    {
       // 受信モードに切り替え
       m_serialPort->setRequestToSend(false);
 
       // 応答タイムアウトを設定(500ミリ秒)
       m_responseTimer->start(500);
    }
 
    /**
     * データ受信時の処理
     */
    void handleReadyRead()
    {
       if (m_serialPort->requestToSend()) {
          return;
       }
 
       m_receiveBuffer.append(m_serialPort->readAll());
 
       // 改行文字を検出したら応答完了とみなす
       // 実際のプロトコルに応じて適切な終了条件を設定する必要がある
       if (m_receiveBuffer.contains('\n') || m_receiveBuffer.contains('\r')) {
          m_responseTimer->stop();
 
          quint8 slaveAddress = static_cast<quint8>(m_receiveBuffer[0]);
          QByteArray data = m_receiveBuffer.mid(1);
 
          qDebug() << "スレーブ" << slaveAddress << "から応答:" << data.trimmed();
          emit slaveDataReceived(slaveAddress, data);
 
          m_receiveBuffer.clear();
 
          // 次のポーリングをスケジュール
          m_pollingTimer->start(m_pollingInterval);
       }
    }
 
    /**
     * 応答タイムアウト時の処理
     */
    void handleResponseTimeout()
    {
       // ポーリング中のスレーブのアドレスを取得
       int prevIndex = (m_currentSlaveIndex - 1 + m_slaveAddresses.size()) % m_slaveAddresses.size();
       quint8 slaveAddress = m_slaveAddresses[prevIndex];
 
       qDebug() << "スレーブ" << slaveAddress << "からの応答なし";
       emit slaveNoResponse(slaveAddress);
 
       m_receiveBuffer.clear();
 
       // 次のポーリングをスケジュール
       m_pollingTimer->start(m_pollingInterval);
    }
 };
 
 #endif // RS485MULTIDROPMASTER_H


 // main.cppファイル
 
 #include "RS485MultiDropMaster.h"
 #include <QCoreApplication>
 
 int main(int argc, char *argv[])
 {
    QCoreApplication app(argc, argv);
 
    RS485MultiDropMaster master;
 
    // スレーブからのデータ受信時の処理
    QObject::connect(&master, &RS485MultiDropMaster::slaveDataReceived, [](quint8 slaveAddress, const QByteArray &data) {
       qDebug() << "アプリケーション: スレーブ" << slaveAddress << "からデータ受信:" << data.trimmed();
       // ここでデータ処理を行う
       // ...略
    });
 
    // スレーブ無応答時の処理
    QObject::connect(&master, &RS485MultiDropMaster::slaveNoResponse, [](quint8 slaveAddress) {
       qDebug() << "アプリケーション: スレーブ" << slaveAddress << "が応答なし - 接続を確認すること";
    });
 
    // マスターを初期化
    QVector<quint8> slaveAddresses = {1, 2, 3};  // スレーブアドレス1, 2, 3を登録
    if (master.initialize("/dev/ttyUSB0", slaveAddresses, 9600, 1000)) {
       // ポーリングを開始
       master.startPolling();
    }
 
    return app.exec();
 }