LabWindows/CVI におけるシリアル通信の安定化構成と実装例

1. イントロダクション:産業環境でのシリアル通信の役割

工業制御、計測器連携、および組込みシステムのデバッグプロセスにおいて、RS-232C や USB シリアル変換による直列通信は依然として重要な手段です。高速通信を求められる現代においても、そのシンプルさと頑健性は多くの老朽化したセンサーや PLC 機器で支持されています。LabWindows/CVI(以降 CVI)は、NI 社が提供する強力な計測・制御向け開発環境であり、ネイティブ C コードとの親和性が高いため、此类のプロトコル実装に頻繁に採用されます。

上位機ソフトウェアの開発においては、単に API を呼び出すだけでなく、パラメータ設定の妥当性、エラー伝播の処理、リソース管理といったシステムレベルの視点が求められます。本稿では、CVI 標準ライブラリの OpenComConfig を中心に、実際の現場で遭遇する課題に対応できる堅牢なシリアル通信モジュールの構築手法について解説します。

2. 初期化関数 OpenComConfig の詳細技術分析

シリアルポートへのアクセス開始は、通信スタックの根幹となる最初のステップです。CVI が提供する OpenComConfig 関数は、物理ポートの識別からデータ転送速度の設定までを一括で管理しますが、パラメータの誤解釈はデータ破損や接続拒否を招く主要原因となります。

int OpenComConfig(int portNumber, 
                  char deviceName[], 
                  long baudRate, 
                  int parity, 
                  int dataBits, 
                  int stopBits, 
                  int inputQueueSize, 
                  int outputQueueSize);

2.1 ポート識別子の特定方法

portNumber パラメータは整数型で、Windows 上の COM1〜COM10 などの論理番号と連動しています。ただし、OS レベルのデバイスマネージャー表示名称とは微妙に異なる場合があるため注意が必要です。
deviceName は通常空文字列 "" で問題ありません。CVI は内部マッピングによって自動的に適切なデバイスパス(例:\\.\COM10)を生成します。特定の仮想ドライバを使用している特殊な環境以外では、この引数を省略することが推奨されます。

2.2 プロトコルパラメータの整合性確認

物理層の設定値は、接続相手(スレーブ機器)の設定と完全に同一である必要があります。

  • BaudRate(通信速度): 環境ノイズの影響を考慮し、必要な帯域を満たす最速の値ではなく、安定性が保証された値(例:9600bps)を選択するのが一般的です。115200bps は短距離やノイズ少ない環境限定です。
  • DataBits(データ長): 8 ビットが業界標準です。一部のレガシー仕様を除き、この設定で統一してください。
  • StopBits(停止ビット): 1 ビットが標準的ですが、遅延補正のために 2 ビットを使用する特殊なケースも存在します。
  • Parity(パリティ検査): 誤り検出のための機能です。0=None(無)、1=Odd(奇数)、2=Even(偶数)。セキュリティ要件がない限り、オーバーヘッド削減のため None が採用されることが多いです。

2.3 バッファサイズの最適化

inputQueueSize および outputQueueSize は OS カーネル内のキュー容量を指します。データ送信の待ち時間が長い環境では入力バッファ不足により「Overflow」エラーが発生するリスクがあります。1024 バイトより十分な余裕を持たせた 4096 バイト程度を目安とし、メモリリソースに影響しない範囲で調整を行います。

3. 設計パターン:モジュール化とエラーハンドリング

毎回ハードコーディングされた設定を行うのではなく、構成情報を構造化し、エラー状況を抽象化して記述することで、保守性を向上させることができます。

#define MAX_SERIAL_BUFFER_SIZE  4096

typedef enum {
    SER_SUCCESS = 0,
    SER_ERR_PORT_NOT_FOUND = -6,
    SER_ERR_BUSY = -3,
    SER_ERR_IO = -4
} SerialStatusCode;

/**
 * @brief シリアルポートの初期化および設定
 */
SerialStatusCode InitSerialChannel(int portIndex, long speed, int pty) {
    // 予期せぬライブラリエラーの抑制
    DisableBreakOnLibraryErrors();

    int handleStatus = OpenComConfig(portIndex, "", speed, pty, 8, 1, 
                                     MAX_SERIAL_BUFFER_SIZE, 
                                     MAX_SERIAL_BUFFER_SIZE);

    EnableBreakOnLibraryErrors();

    if (handleStatus == 0) {
        // オープン成功時のクリーンアップ
        FlushInQ(portIndex);
        return SER_SUCCESS;
    } else {
        // エラーコードのマッピング
        switch (handleStatus) {
            case -6: LogMsg("Port does not exist."); break;
            case -3: LogMsg("Port occupied by another application."); break;
            default: LogMsg("Generic IO error occurred."); break;
        }
        return (SerialStatusCode)(-abs(handleStatus));
    }
}

4. 非同期読み取りとタイムアウト制御の実装

シリアル受信処理における最大の課題は、データの長さの不確定性と、読み取り待ちによるプロセッサハングです。単純な ComRd 呼び出しではなく、タイムアウト機構を組み合わせたルーチンを構築します。

/**
 * @brief 指定したタイムアウト内で終端文字を取得する読み取り関数
 * @param pIndex 対象ポート番号
 * @param outBuf 格納先バッファ
 * @param maxSize バッファ最大サイズ
 * @param timeoutSec 総待機時間
 */
int ReadDataWithTerminator(int pIndex, char *outBuf, int maxSize, double timeoutSec) {
    int collectedBytes = 0;
    int byteCount;
    double startTime = TickCount() / 1000.0;
    
    // 個々のコンポーネントは短時間のタイムアウトで設定
    SetComTime(pIndex, 0.1); 

    while (collectedBytes < maxSize - 1) {
        byteCount = ComRd(pIndex, &outBuf[collectedBytes], 1);

        if (byteCount > 0) {
            collectedBytes++;
            startTime = TickCount() / 1000.0; // データ到着時リセット
            
            // 例:改行コードで終了判定
            if (outBuf[collectedBytes - 1] == '\n') {
                outBuf[collectedBytes] = '\0';
                return collectedBytes;
            }
        } else {
            // 現在の時間差で全体タイムアウト判定
            if ((TickCount() / 1000.0 - startTime) >= timeoutSec) {
                break;
            }
        }
    }
    
    outBuf[collectedBytes] = '\0';
    return (collectedBytes > 0) ? collectedBytes : -1;
}

5. 接続状態監視と回復ロジック

長期稼働時には、物理的なコネクク抜去や電源トラブルによる切断を検知する必要があります。GetComConnectionState を活用し、異常を検出した場合に自動再試行または再起動処理を実装します。

void ShutdownChannel(int pIndex) {
    if (GetComConnectionState(pIndex) == 1) {
        CloseCom(pIndex);
        printf("Channel closed successfully.\n");
    }
}

int MainLoop(void) {
    int comId = 3;
    int retryPolicy = 0;
    
    // 初期化
    if (InitSerialChannel(comId, 9600, 0) != SER_SUCCESS) {
        return ERROR_INIT_FAILURE;
    }

    // メイン処理フロー
    while (IsApplicationRunning()) {
        // 送信処理
        unsigned char txCmd[] = {0xAA, 0x01, 0xFF};
        ComWrt(comId, txCmd, sizeof(txCmd));
        
        // 受信処理
        char rxBuffer[256];
        int recvLen = ReadDataWithTerminator(comId, rxBuffer, sizeof(rxBuffer), 2.0);
        
        if (recvLen > 0) {
            ProcessPacket(rxBuffer, recvLen);
            retryPolicy = 0; // リトライリセット
        } else if (recvLen == -1) {
            retryPolicy++;
            if (retryPolicy > 3) {
                // 接続確認とリカバリ
                if (GetComConnectionState(comId) != 1) {
                    ShutdownChannel(comId);
                    Delay(1000);
                    InitSerialChannel(comId, 9600, 0);
                    retryPolicy = 0;
                } else {
                    LogWarning("Device Unresponsive");
                }
            }
        }
        
        Delay(500);
    }
    
    ShutdownChannel(comId);
    return SUCCESS;
}

6. トラブルシューティング要点

以下の項目をチェックリストとして運用すると、デバッグ効率が向上します。

接続不可の場合

  • デバイスマネージャー上で COM ポート名が正しく認識されているか確認する。
  • 別の PC から同じケーブルを接続し、動作確認を行う(物理媒体の検証)。
  • 既存プロセスがポートをロックしていないかチェックする。

乱码またはデータ欠落の場合

  • ボーレートとパリティ設定がターゲット機器と一致するか再確認する。
  • 入出力のワイヤー端子(TXD/RXD)が逆配線されていないか確認する。
  • 入力キューサイズを増やし、データ蓄積によるオーバフローを防ぐ。

通信断絶後の復旧

  • GetComConnectionState のステータスが変更されているか定期的に監視する。
  • 一定回数失敗した場合、一度 CloseCom を実行してから再度 OpenComConfig を呼び出して状態をリセットする。

タグ: LabWindows/CVI SerialCommunication RS232 c-language EmbeddedSoftware

10月8日 22:03 投稿