さて、今度はシリアルポートとの通信テストプログラムを作ってみよう。前回同様、SerialTestといった名前で、Win32コンソールのプロジェクトを生成して、List 1の内容をコピー、ビルドする。前回と異なり、標準的なライブラリのみで完結するので、プロパティの変更は必要ないから、そのままビルドすれば良い。

まずプログラムを説明する。やることは単純である。図1に処理のフローチャートをまとめたが、もう少し細かく紹介しよう。

まず#includeだが、ここで<windows.h>をincludeしているのは、それが一番楽だからである。例えばSetCommState()はwinbase.hを、WideCharToMultiByte()はwinnls.hをそれぞれincludeする必要があるが、windows.hをincludeしておけば、内部でこれらをまとめてincludeしてくれるのであっさりと済む。BUFSIZEはまぁ「適当なサイズのバッファ」で、今回は別にサンプルなのでとりあえず80文字分も取っておけばいいだろうという話だ(Buffer underranの話は後述)。続くCOMPORTとBAUDRATEは文字通りArduinoが繋がっているCOMポートのデバイス名と通信ボーレートである。この辺り、最終的なプログラムは外部から変更できるようにしないと不便だが、とりあえずテストなのでDEFINEで済ませている。

_tmain()であるが、バッファの話は後述するとして、通信ポートを扱う場合にはHANDLE形の変数が必要になる。これをhCOMとしてここで定義している。

さて、まずCOMポートのOpenだ。WindowsというかMS-DOSの時代から、通信ポートもCOM:というデバイス名(というかドライブ名)として扱えるようになっているから、通信ポートもCreateFile()で扱う。CreateFile()の引数はこちらこちらで確認してもらう(日本語版の結果が相変わらず変なので、英語版で確認することを強く推奨)として、別にそれほど珍しいことをしているわけではない。ここでエラー(COMポートの指定が間違っているとか)ならばまぁ撥ねるとして、続いてCOMポートの設定である。ここではDCB(Device Control Block)を使っての設定であるが、これもそれほど難しい訳ではない。まずCreateFile()で取得したhCOMを指定してGetCommState()を呼び出し、現在のCOMポートの設定をdcbBufに取り込む。ついで、変更したい箇所だけを変更後に、SetCommState()で書き戻すという具合だ。設定項目は「// パラメータ設定」で示した通信速度、bit長、Parity/Stop bit程度でいいだろう。Flow Controlとかは省いたが、相手がArduinoの場合にFlow Controlが必要になるケースはちょっと考えにくいので、今回は省いた。

以上で初期設定は終り、いよいよフローチャートでの繰り返し部分に入るわけだが、その直前に、

    // 2秒ほど待機
    Sleep(2000);

というコードがある筈だ。実は、これが割とArduinoと通信する場合の肝の部分だったりする。Arduinoと通信する場合、COMポートをCreateFile()でオープンしてから数秒間(概ね1~2秒)はデータを送ってもArduinoが受け取れない(途中で消えてしまう)という現象がある。これは他にも、例えば通信をWrite(PC→Arduino)からRead(Arduino→PC)に切り替える、あるいは逆にReadからWriteに切り替える場合も同じで、どちらも数秒間無通信状態が続く。まだ筆者は他のプラットフォーム(MacやLinuxなど)では試していないので、これがArduinoに乗っているUARTチップの問題なのか、ドライバの問題なのかは判断できないのだが、いずれにしても頻繁に通信を切り替えるようなアプリケーションはArduinoには不適当で、多分受信専用と送信専用の2つのArduinoを使うほうがうまくいく。

先ほどの話にちょっと絡むが、MS-DOSではCOMポートがデバイスというかファイル扱いされるから、例えば、

echo 1234z > COM6:

とかをやれば通信できそうなものなのに、実際はこれだとうまく通信できない。ところが、

copy con COM6:
(ここで数秒待つ)
1234z
(Ctrl+z)←Ctrl+Zを入力

とやると、今度はArduinoが1234zという文字を受け取れて、これにあわせて表示が更新される。面白いのは、この2秒待つのは最初だけで、一度通信できるようになれば、後はフルに通信が可能となっている。なので、

copy con COM6:
(ここで数秒待つ)
1234z
2345z
3456z
  :
  :

という調子に入力すれば、そのまますぐ反映される筈だ。最初、この癖を知らずに筆者は随分苦闘することになってしまったが、そんなわけでキー入力を受け取って処理する前に数秒間の待ちを入れるのが案外に重要である。

(続く)

List 1:

// SerialTest.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//

#include "stdafx.h"
#include <string.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define BUFSIZE 80
#define COMPORT _TEXT("COM6")
#define BAUDRATE 9600

int _tmain(int argc, _TCHAR* argv[])
{
    HANDLE      hCOM;       // COMポートアクセス用Handle
    wchar_t     unicodeBuf[BUFSIZE];// キーボード入力用バッファ(unicode)
    char        ansiBuf[BUFSIZE];   // 転送用バッファ(ansi)
    int         charInLength;   // 入力文字長
    unsigned long   charOutLength;  // 出力文字長

    // RS232CポートへのHandle作成
    hCOM = CreateFile(COMPORT,
              GENERIC_READ | GENERIC_WRITE,
              FILE_SHARE_READ | FILE_SHARE_WRITE,
              NULL,
              OPEN_EXISTING,
              0,
              NULL);
    if (hCOM == INVALID_HANDLE_VALUE)
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port open error\n"));
    _exit(0);
    }

    // 現在のRS232Cポートの通信パラメータ取得
    DCB    dcbBuf;
    dcbBuf.DCBlength = sizeof(DCB);

    if (!GetCommState(hCOM, &dcbBuf))
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port access error\n"));
    _exit(0);
    }

    // パラメータ設定
    dcbBuf.BaudRate = BAUDRATE; // 通信速度
    dcbBuf.ByteSize = 8;    // データ長
    dcbBuf.Parity = NOPARITY;   // パリティビット
    dcbBuf.StopBits = ONESTOPBIT;// ストップビット

    // RS232Cポートにパラメータ設定
    if (!SetCommState(hCOM, &dcbBuf))
    {
    CloseHandle(hCOM);
    _tprintf(_TEXT("COM port access error\n"));
    _exit(0);
    }

    // 2秒ほど待機
    Sleep(2000);

    while(1)
    {
    // stdinから1行読み込む
    if (!_getws_s(unicodeBuf, BUFSIZE))
    {
        // キーボード入力に失敗
        _tprintf(_TEXT("Key input error\n"));
        break;
    }
    charInLength = (int)_tcslen(unicodeBuf);

    // UnicodeをANSIに変換
    if (!WideCharToMultiByte(CP_ACP,
                 WC_NO_BEST_FIT_CHARS,
                 unicodeBuf,
                 charInLength,
                 ansiBuf,
                 sizeof(ansiBuf),
                 NULL,
                 NULL))
    {
        // 入力文字の変換に失敗
        _tprintf(_TEXT("Data convert error\n"));
        break;
    }

    // 入力された文字列をRS232Cポートに送る
    if( !WriteFile(hCOM, ansiBuf, charInLength, &charOutLength, NULL))
    {
        _tprintf(_TEXT("Data send error\n"));
        break;
    }
    }

    //後処理
    CloseHandle(hCOM);

    return 0;
}