torutkのブログ

ソフトウェア・エンジニアのブログ

シリアル通信処理をJavaで

組み込み系ソフトウェア開発では、機器間の通信にシリアル通信(RS-232C)を使用することがよくあります。そのため、試験ツールとしてRS-232Cを使うプログラムを作りたいことがあります。
シリアル通信がテキスト(ASCII文字)であれば、各種ターミナルソフト(例:TeraTerm)を使えばプログラムを作成しなくても送受信する試験ができます。しかし、組み込み系では通信データにテキストではなくバイナリを使用することが多く、標準的なターミナルソフトでは対応できません(一部バイナリデータを扱うターミナルソフトもあるようです)。

試験ツールをC言語で書くと、GUIを使うのが大変で、また文字列処理も不得手なので、Javaを使って作りたいなと思うことがあります。

Javaからシリアルポート(俗にRS-232C)とパラレルポートを使用するためのオプションAPIとして、Java Communication API仕様が制定されています。現時点でAPIのバージョンは3.0です。このJava Communication API仕様(Ver.3.0)の実装がSunから無償で公開されています。現時点ではSPARC Solaris/x86 Solaris/x86 Linux版のみとなっています。

Windows版はAPI仕様Ver.2.0の頃の実装が昔提供されていたものの最近は提供されていません。昔のバージョンはメンテナンス期間が過ぎておりバグ修正もされていません。したがって、Sunの昔のWindows版実装を入手して使用するよりも、Java Communication API仕様に準拠したオープンソースのRXTXを使用した方がよいです。

このRXTXは、パッケージ名がjavax.commではなくgnu.ioとなっていますが、Java Communication APIで制定されたクラスが実装されており、import文以外は同じようにプログラミングできます。

RXTXライブラリの設定(Windows

Windows版での話しを以下に記述します。まず、RXTXのサイトからダウンロードします。

  • rxtx-2.1-7-bins-r2.zip

これを適切なディレクトリに展開します。(ここでは、C:\java以下に展開します)
コンパイル時は、クラスパスにC:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jarを追加します。実行時は、コンパイル時のクラスパスに加え、システム・プロパティjava.library.pathにC:\java\rxtx-2.1-7-bins-r2\Windows\i386-mingw32を記述します。(環境変数PATHに追加してもよい)

インストール文書には、RXTXcomm.jarをJDKのextdirにコピーし、rxtxSerial.dllとrxtxParallel.dllをJDKのbinにコピーするようあります。確かにここに置けば設定不要となりますが、JDKのバージョンを変えるごとに入れ直しになるので、ここでは他のプログラム開発と同様コマンドラインで指定することにします。

C:\work> javac -cp C:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jar;. MyApp.java
  :
C:\work> java -cp C:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jar;. -Djava.library.path=C:\java\rxtx-2.1.7-bins-r2\Windows\i386-mingw32 MyApp
  :

RXTXライブラリを使用したシリアル通信(Windows

シリアルポートの取得とオープン(COM1)
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;

class SomeClass {
    boolean openSerialPort() {
        try {
            portId = CommPortIdentifier.getPortIdentifier("COM1");
            port = (SerialPort)portId.open("SomeClass", 2000);
        } catch (NoSuchPortException e) {
            e.printStackTrace(e);
            return false;
        } catch (PortInUseException e) {
            e.printStackTrace(e);
            return false;
        }
        return true;
    }
    CommPortIdentifier portId;
    SerialPort port;
}

CommPortIdentifierのstaticメソッドgetPortIdentifierで、指定した名前のポート識別子を取得します。指定した名前に対応する識別子がないときはNoSuchPortExceptionがスローされます。

CommPortIdentifierクラスにはこのメソッドの他に、getPortIdentifiersメソッドですべてのポート識別子のEnumerationを取得するメソッドがあります。

ポート識別子が取得できたら、次はポートのopenを行います。openメソッドの戻り値はCommPort型ですが、シリアルポートの場合はCommPortの派生クラスSerialPort型のインスタンスが返却されます。ここではCOM1を指定しているので、instanceofでの検査を省略してSerialPort型にダウンキャストしています。

シリアルポートの設定
    :
import gnu.io.UnsupportedCommOperationException;

class SomeClass {
        :
    boolean setSerialPort() {
        try {
            port.setSerialPortParams(
                19200,                   // 通信速度[bps]
                SerialPort.DATABITS_8,   // データビット数
                SerialPort.STOPBITS_1,   // ストップビット
                SerialPort.PARITY_NONE   // パリティ
            );
            port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
        } catch (UnsupportedCommOperationException e) {
            e.printStackTrace();
            return false;
        }
        port.setDTR(true);
        port.setRTS(false);
        return true;
    }
        :
}

通信速度、データビット数、ストップビット数、パリティの設定は、SerialPortクラスのsetSerialPortParamsメソッドで行います。通信速度はint型の値を直接指定しますが、残りはSerialPortクラスの定数を使用します。

制御信号のうち出力となるのがDTRとRTSです。Java Communications APIでは、SerialPortクラスのsetDTRメソッド、setRTSメソッドで信号をアクティブ・インアクティブに切り替えることが出来ます。初期化の手続きとしてはまずDTRをアクティブにするので、setDTR(true)を呼び出しています。

データの受信(ポーリング)
class SomeClass {
        :
    public void read() {
        InputStream in = port.getInputStream();
        byte[] buffer = new byte[1024];
        while (true) {
            int numRead = in.read(buffer);
            if (numRead == -1) {
                break;
            }
            // bufferから読み出し処理
        }
    }
}

SerialPortインスタンスからgetInputStreamでストリームを取得できます。あとはそこから読み出すだけです。

ここで、readメソッドは受信データがなければブロックすると思っていましたが、RXTXでは戻り値0で即リターンしています。

SunのJava Communications API実装(Windows版 2.0)で試してみると、readメソッドがブロックしています。実装系によって振る舞いに多少の違いがあるようです。

データの受信(イベント)

先の入力ストリームのreadの場合、スレッドを1つ専用に割り付けることになります。もう1つのデータ受信方法として、イベントによる受信があります。SerialPortEventListenerを定義してSerialPortに登録しておくと、データ受信が発生したときにSerialPortEventListenerのserialEventメソッドが呼ばれます。データ受信以外の制御信号の変化やパリティ等のエラーもこのイベントで受信することができます。

import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import java.util.TooManyListenersException;
    :
class SomeClass {
        :
    class SerialPortListener implements SerialPortEventListener {
        public void serialEvent(SerialPortEvent event) {
            if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
                read();
            }
        }
    }

    void enalbeListener() {
        try {
            port.addEventListener(new SerialPortListener());
            port.notifyOnDataAvailable(true);
        } catch (TooManyListenersException e) {
            // エラー処理
        }
    }
        :
}

SerialPortEventListenerインタフェースの唯一のメソッドserialEventを実装します。引数SerialPortEventインスタンスのgetEventTypeメソッドでイベント種類を判別します。データ受信の場合は、DATA_AVAILABLE定数で識別します。このイベント発生後にシリアルポートの入力ストリームを読み出せば、受信バッファにデータが入っているので入力ストリームの読み出しがブロックされたり0バイトでリターンすることはありません(タイムアウトを設定する場合を除く)。

イベントの登録はSerialPortインスタンスにaddEventListenerメソッドで行います。このメソッドは検査例外TooManyListenersExceptionを投げるので、try-catchで囲んでいます。SerialPortクラスの実装は、イベントリスナーを1つだけしか保持しないとAPIドキュメントに記載されています。
addEventListenerメソッドの登録以外に、SerialPortインスタンスのnotifyOnDataAvailable(true)を呼んでおく必要があります。忘れるとデータ受信イベントがリスナーに渡されません。

データ受信イベントは数バイト〜十数バイト着信した時点で発生するようです。そのため、これを超えるサイズのデータを送信している場合、一回の受信イベントでは一部分しか受信することができません。数回の受信イベントをまとめる必要があります。

SerialPortのスーパークラスであるCommPortのenableReceiveThresholdメソッドを使うと、受信イベントの発生を引数で指定したサイズのバイト数を受け取るまで遅延させることができます。例えば64を指定した場合、データが64バイト受信するまでは受信イベントが発生しません。これは逆に64バイト未満のデータを受信しても、受信イベントが発生しないということにもなります。

また、CommPortクラスのenableReceiveTimeoutメソッドを使うと、受信イベント待ちを指定した時間(ミリ秒)ごとにタイムアウトさせて受信イベントを発生させることができます。タイムアウトによる受信イベントのタイミングで入力データを読み出した際は、サイズ0でリターンします。

データの受信、データブロックの切り出し

シリアル通信で、データブロックを周期的に送信している場合、受信側がそのデータブロックの先頭・終了を認識して切り分けるには工夫が必要となります。SerialPortのinputStreamからreadするデータサイズはタイミングによって変化します。データブロックの途中までであったり、あるいはデータブロックの途中から読み出すこともあります。

シリアル通信の場合は、データブロックの先頭を切り出す特殊な方法を通信フォーマットとして埋め込んでおくのがよさそうです。(データ中には表われないコードをデータブロックの先頭を示すなど)

それがない場合は、データ非送信期間が一定時間経過することで切り分けるのが一案です。(例:enableReceiveTimeoutで1000ミリ秒をセットしておき、データ受信イベントで読み出しサイズ0であったとき=1000ミリ秒データ受信がないとき、受信バッファをクリアして次の読み出しをデータ先頭とする)

DTR, RTSの制御について

PC/AT互換機RS-232Cは、デフォルトでDTR、RTS信号がアクティブのようです。上述サンプルのようにsetDTR/setRTSメソッドでfalseにするとDTR、RTS信号をインアクティブにできますが、プログラム終了後はDTR、RTS信号がまたアクティブになります。

一般的な(?)端末側制御手順

端末側(DTE)とモデム側(DCE)との一般的な制御信号の制御手順を調べてみます。

  1. 端末側はDTR(データ端末レディ)信号をアクティブにします。
  2. モデム側はDSR(データセットレディ)信号をアクティブにします。
  3. モデム側はDCD(キャリア検出)信号をアクティブにします。
  4. 端末側はRTS(送信要求)信号をアクティブにします。
  5. モデム側はCTS(送信許可)信号をアクティブにします。
PC同士をクロスケーブルで接続する場合

クロスケーブルの結線にもいろいろ流儀があるようです。制御信号の結線が異なるので、使用するケーブルによってプログラムの記述が変わる可能性があります。

DCD 1        1 DCD
RXD 2 ------ 3 TXD
TXD 3 ------ 2 RXD
DTR 4 ------ 6 DSR
SG  5 ------ 5 SG
DSR 6 ------ 4 DTR
RTS 7 ------ 8 CTS
CTS 8 ------ 7 RTS
DCD 1 ----+- 7 RTS
          +- 8 CTS
RXD 2 ------ 3 TXD
TXD 3 ------ 2 RXD
DTR 4 ------ 6 DSR
SG  5 ------ 5 SG
DSR 6 ------ 4 DTR
RTS 7 -+---- 1 DCD
CTS 8 -+


(以下記述追加予定)