24
24

More than 5 years have passed since last update.

Goでのシリアル通信でハマった事

Last updated at Posted at 2015-12-12

こんばんは。
普段は組み込み系(主にWindowsCEやOSレス)のC, C++やってます。
Goはほぼ趣味でたまに触っている程度なので、お手柔らかに…。

対象の方

  • Goでシリアル通信してみたい方
  • Goで組み込みっぽい事をやってみたい方

はじめに

Goでお手軽にサブGHz帯通信をしたかったので、go-im920 をつくりました。
インタープラン株式会社様の920MHz無線モジュール、IM920用の制御ライブラリです。

シリアル通信のライブラリは tarm/serial を使わせて貰ったのですが、データ受信でハマったのでご紹介します。
開発はWindows上でおこなっています。

最初はリードタイムアウト

main.go
sc := &serial.Config{Name: "COM4", Baud: 19200, ReadTimeout: 1 * time.Second}
buf := make([]byte, 128)
n, _ = sc.Read(buf)

上記のコードだと受信データサイズが128byteとなるかReadTimeoutするまで、Read()から復帰しません。
IM920は送信したコマンドに対し毎回応答を返してくるのですが、その度にReadTimeoutを待つのは時間がかかりすぎです。
かといって、ReadTimeoutを短くし過ぎると応答を受信できない可能性があります。
いくつかのコマンドを除き応答は固定長(4byte)だったので、とりあえず buf := make([]byte, 4) で逃げました。

次は応答受信しきる前に復帰

tarm/serial の このコミット から、受信データがあれば即座に復帰するように挙動が変わりました。

IM920はほとんどのコマンドに対して "OK\r\n" か "NG\r\n" を返してきますが、この変更で応答を1byte受信しただけ(例:"O")でRead()が復帰するようになってしまいました。困った。

受信インターバルタイマを有効にしようと思ったけど方針転換

Windowsの場合、例えば「受信処理トータルでのタイムアウト」と「データ受信間隔に対してのタイムアウト」が 設定 できます。

Linuxの場合、TERMIOS でタイムアウト周りの設定が出来るようですが、Windowsのように「受信処理トータルでのタイムアウト」と「データ受信間隔に対してのタイムアウト」の両立はこれ単体ではできないようです。1

当初はtarm/serialを変更して受信インターバルタイマを有効にしようと考えてましたが、Linuxで同等の挙動を実現するのは難しそう&tarm/serialのREADME.mdに

Please note that this is the total timeout the read operation will wait and not the interval timeout between two bytes.

と記載があったので、受信インターバルタイマ無しがtarmさんの期待値だと判断し、go-im920 内部でどうにかすることにしました。

1byteずつRead()することでtarm/serialのReadTimeoutは受信インターバルタイマとしてのみ使用し、time.NewTimer()で受信処理トータルでのタイムアウトを検知します。

go-im920.go
func (im *IM920) receive(p []byte) (readed int, err error) {
    timer := time.NewTimer(im.readTimeout)
    defer timer.Stop()

    readedInitialbyte := false

    for {
        select {
        case <-timer.C:
            if readed == 0 {
                err = fmt.Errorf("error: Read failed: no data")
            }
            return
        default:
            n, rerr := im.s.Read(p[readed : readed+1])
            if rerr != nil {
                err = fmt.Errorf("error: Read failed: %s", rerr)
                return
            }
            if n > 0 && !readedInitialbyte {
                readedInitialbyte = true
            }
            if n == 0 && readedInitialbyte {
                return
            }

            readed += n
            if readed >= len(p) {
                return
            }
        }
    }
}

もっとうまいやり方があるのかもしれませんが、正常に動作しているようなので良しとします。

最後に

このネタ全然Goっぽくない…。


  1. 実はselect()併用すればできそう? 

24
24
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
24