C++ でのシリアル ポート接続

Muhammad Husnain 2023年10月12日
C++ でのシリアル ポート接続

この記事では、Windows API を使用してシリアル ポート接続を開き、読み取り、書き込み、および管理するための基本について説明します。

このチュートリアルの主な目的は、プログラミングにおけるシリアル通信がどのように機能するかについての基本的な理解を提供し、正しい方向に向けて始めることです。

この記事では、C/C++ の基本的な知識があり、プログラムをコンパイルして実行できること、開発環境が Windows API 呼び出しを使用するように設定されていることを前提としています。

C++ でのシリアル ポート接続

C++ でデータの読み取りまたはシリアル ポートへの書き込みを行うには、次の 6つの手順があります。

  1. シリアルポートを開く
  2. いくつかの基本的なプロパティを設定します
  3. タイムアウトを設定する
  4. データの読み取りまたは書き込み
  5. ポートをクリーンアップする
  6. いくつかの高度な機能

シリアルポートを開く

最初のステップはポートの開放です。 これは、特に Windows ファイル I/O を既に知っている場合は、最も簡単で簡単な手順の 1つです。

まず、必要なヘッダー ファイル、つまり windows.h がファイルに含まれていることを確認する必要があります。 次に、次のコードを使用します。

HANDLE h_Serial;
h_Serial = CreateFile("COM1", GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING,
                      FILE_ATTRIBUTE_NORMAL, 0);
if (h_Serial == INVALID_HANDLE_VALUE) {
  if (GetLastError() == ERROR_FILE_NOT_FOUND) {
    // serial port not found. Handle error here.
  }
  // any other error. Handle error here.
}

最初の行で HANDLE 型変数を作成し、関数 CreateFile を呼び出してそれを初期化しました。 この関数の最初の引数は、開く必要があるファイルの名前です。 この場合、シリアル ポートを開きたいので、そのポートの名前、つまり COM1 を使用しました。

2 番目の引数は、データの読み取りまたは書き込みが必要かどうかを指定することです。 実行するタスクがない場合は、この引数をそのままにしておくことができます。

次の 2つの引数は常に 0 のままです。 次の引数は、既存のファイルを開く必要があるか、新しいファイルを作成する必要があるかを指定することです。

この場合はポートなので、OPEN_EXISTING を使用しました。 次の引数は、通常の読み取りまたは書き込み以外に凝ったものは必要ないことを Windows に指定することです。

最後の引数も常にゼロに保たれます。

基本プロパティの設定

ポートの HANDLE を取得した後、バンド レート、バイト サイズ、ストップ ビットなどのいくつかの基本的なプロパティを設定する必要があります。これは、構造体 DCB を使用して行われます。

DCB dcbSerialParam = {0};
dcbSerial.DCBlength = sizeof(dcbSerialParam);

if (!GetCommState(h_Serial, &dcbSerialParam)) {
  // handle error here
}

dcbSerialParam.BaudRate = CBR_19200;
dcbSerialParam.ByteSize = 8;
dcbSerialParam.StopBits = ONESTOPBIT;
dcbSerialParam.Parity = NOPARITY;

if (!SetCommState(h_Serial, &dcbSerialParam)) {
  // handle error here
}

上記のコード スニペットでは、コードの最初の行で DCB のオブジェクトを作成し、それをゼロで初期化して値をクリアしました。 次の行では、Windows の必須ステップであるこの構造体の長さを設定しています。

その後、関数 GetCommState を呼び出し、2つのパラメーター、つまり、ポート HANDLEDCB オブジェクトを渡して、現在使用中のパラメーターを埋めました。

これを取得したら、重要なパラメーター、つまり BaudRateByteSizeStopBits、および Parity を設定する必要があります。

Windows では、特別な定数を使用して BaudRate を提供する必要があります。 たとえば、CBR 19200 は 19200 ボーを表し、CBR 9600 は 9600 ボーを表し、CBR 57600 は 57600 ボーを表します。

ByteSize を直接定義できますが、StopBitsParity には追加の変数が必要です。 ONESTOPBITONE5STOPBITS、および TWOSTOPBITSStopBits の可能性です。

EVENPARITYNOPARITY、およびODDPARITYは、Parityの最も広く使用されている代替手段です。 他にも存在しますが、あまり知られていません。

詳細については、MSDN ライブラリ アイテム (DCB を検索) を参照してください。

DCB 構造体を優先順位に設定した後、これらの設定をシリアル ポートに適用する必要があります。 SetCommState 関数は、これを達成するために使用されます。

タイムアウトを設定する

シリアル ポートからデータを読み取ると、シリアル ポートにデータが入っていない場合 (たとえば、シリアル ポート デバイスのスイッチが切られているか、切断されている場合)、データが表示されるのを待っている間にアプリケーションが停止する可能性があります。 この状況に対処するには、2つの方法があります。

まず、アプリケーションでマルチスレッドを使用できます。1つのスレッドはシリアル ポートの問題を処理し、もう 1つのスレッドは実際の処理を処理します。 これは面倒で複雑になる可能性があり、必須ではありません。

別の方法はもっと簡単です。データが表示されるのを待つのをやめるよう Windows に指示します。 これは、次の方法で実現されます。

COMMTIMEOUTS timeout = {0};
timeout.ReadIntervalTimeout = 60;
timeout.ReadTotalTimeoutConstant = 60;
timeout.ReadTotalTimeoutMultiplier = 15;
timeout.WriteTotalTimeoutConstant = 60;
timeout.WriteTotalTimeoutMultiplier = 8;
if (!SetCommTimeouts(h_Serial, &timeout)) {
  // handle error here
}

COMMTIMEOUTS 構造は単純で、上に挙げたフィールドだけです。 簡単な要約:

  • ReadIntervalTimeout - タイムアウトする前に受信文字間で経過する必要がある時間を指定します (ミリ秒単位)。
  • ReadTotalTimeoutConstant - 戻るまでの待機時間を提供します (ミリ秒単位)。
  • ReadTotalTimeoutMultiplier - 読み取り操作で要求された各バイトに応答するまでの待機時間をミリ秒単位で指定します。
  • WriteTotalTimeoutConstantWriteTotalTimeoutMultiplier - どちらも ReadTotalTimeoutConstantWriteTotalTimeoutMultiplier と同じことを行いますが、読み取りではなく書き込みを行います。

ReadIntervalTimeoutMAXDWORD に設定し、ReadTotalTimeoutConstantReadTotalTimeoutMultiplier の両方を 0 に設定すると、読み取り操作は、たとえ何も存在しない場合でも、バッファー内の任意の文字 (つまり、既に受信されている) で即座に返されます。

COMMTIMEOUTS 構造を構成した後、SetCommTimeouts メソッドを使用して変更をシリアル ポートに適用する必要があります。

データの読み取り/書き込み

必要なパラメータとタイムアウトを使用して、シリアル ポートを開いた後にデータの読み取りを開始できます。 これらは理解しやすいです。

シリアル ポートから n バイトを読み取るシナリオを考えてみましょう。 次に、次の手順に従います。

char sBuff[n + 1] = {0};
DWORD dwRead = 0;
if (!ReadFile(h_Serial, sBuff, n, &dwRead, NULL)) {
  // handle error here
}

ReadFile には、ファイル (シリアル ポート) への HANDLE、データが格納されるバッファー、読み取るバイト数、読み取られるバイト数に設定される整数への参照、および NULL がすべて渡されます。 . ReadFile操作によって読み取られたバイト数は、dwReadに保存されます。

データの書き込みも同じ手順です。 唯一の違いは、WriteFile 関数がデータを書き込めることです。

ポートを閉じる

シリアル ポートの使用が終了したら、ハンドルを閉じます。 そうしないと、再起動するまで誰もシリアルポートを使用できなくなるなど、奇妙なことが起こる可能性があります。

いずれにせよ、それはかなり簡単に達成できるので、次の点に注意してください。

CloseHandle(h_Serial);

エラーを処理する

お気づきかもしれませんが、各システム コールの後に、エラーを処理する必要があることを示す注記を挿入しました。

これは常に優れたプログラミング手法ですが、より頻繁に失敗する I/O 関数では特に重要です。 次のすべての関数は、失敗すると 0 を返し、成功すると 0 以外を返します。

エラーコードを DWORD として返す GetLastError() 関数を使用して、問題の原因を突き止めます。 FormatMessage メソッドを使用して、これを意味のある文字列に変換できます。

char lastErr[1020];
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
              GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
              lastErr, 1020, NULL);
Muhammad Husnain avatar Muhammad Husnain avatar

Husnain is a professional Software Engineer and a researcher who loves to learn, build, write, and teach. Having worked various jobs in the IT industry, he especially enjoys finding ways to express complex ideas in simple ways through his content. In his free time, Husnain unwinds by thinking about tech fiction to solve problems around him.

LinkedIn