Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

こんばんは。
普段は組み込み系(主に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()併用すればできそう? 

tomoya0x00
元は車載関連の組み込み屋。現在はAndroidアプリ作ってる。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away