2023-12-17(Sun)追記: 「送受信データが変換される」を追加しました。またLinux manページのリンク先をDebianのそれに変更しました(OSDNが安定しないため)。
USB-シリアル変換ケーブルで接続するデバイスからデータを受信するLinuxアプリを業務で作成することになりました。Webエンジニアなのに。そもそもシリアル通信もLinuxアプリもやったことのない人間にそんなタスクをアサインするのはどうかと思うのですが、まあそこはそれ、いろいろ調べてなんとかかたちにするところまで持っていきました。その過程で気づいた、Webにある情報では気づきにくかった、あるいは見当たらなかった情報をいくつかメモとして残しておきます。ちょっとどこまで一般化できるかわからないのですが……
受信状態にならない
実際のデバイスとつないでまず直面したのが受信状態にならないという障害。待ち受け主体なのでepoll(7)で待機する構造にしたのですが、epoll_wait(7) から制御が戻ってきても読み取り可状態になりません(struct epoll_event 構造体の eventsデータメンバに EPOLLIN EPOLLPRI 両フラグが設定されることがない)。他のデバイスをつなぐとうまく行ったり、逆に実際のデバイスとGNU Screenでアクセスするとうまく行ったりで、肝心の組みあわせだけ駄目な状態……
皆目見当がつかなかったのですが、必死になってWebを検索したところシリアル通信のパラメータを設定するtcsetattr(2)呼び出しの際の初期化で自分がやっているのとは異なる処理を見つけました。具体的には次。
- 既存設定の原則維持のために tcgetattr(3) で取得した、型が
strict termios構造体の変数のデータメンバのうち、c_lflagの値からカノニカルモード指定を意味するICANONフラグを明示的にクリア(c_lflag &= ~ICANON)
試してみたところ無事読み取り可状態になるようになりました。非カノニカルモードって明示すべきなんですねえ。なるほど。まあ慣れている人には書くまでもないあたりまえのことなんでしょうが……
先頭数バイトが欠ける
一難去ってまた一難、次に見舞われたのは受信データの先頭数バイトが欠けるという現象。read(2)で読み込むとなぜか読み込めないバイトが出てきます。失われる長さは常に一定。
これはWeb上には関係すると思われる情報がまったく見当たらず心底途方にくれました。しかたがないので通信設定をしらみつぶしに確認していこうとstrict termios構造体 c_ccデータメンバを全クリアしてみたところ思いがけず成功。原因特定を進めたところ、これも同構造体 c_lflagデータメンバに設定する ISIG (シグナル発生)フラグを明示的にクリアすることで解決しました。
詳細はLinux側の挙動になるので明確には確認できていないのですが、epoll_wait(7) で応答が返ってくる前に割り込み処理が呼び出されてその処理で読み取りデータが消費されると考えると挙動のつじつまが合います。シリアル通信データを epoll(7) / poll(2)で待ち受けるときはシグナルは無効にするのがよさそうです。
ただし今回のケースでは割り込み回避のために SIGINT シグナル処理用の独立したスレッドを用意(sigwait(3)で待機)、処理しないシグナルは sigprocmask(2) でマスク(ブロック)しているので、その影響もあるかもしれません。
送受信データが変換される
「受信状態にならない」のカノニカルモードの話とつながるのですが、Linuxのシリアル通信APIは一部制御文字の自動変換機能を備えており、バイナリデータとして正しく送受信するには次のフラグも明示的にクリアする必要があります。
c_iflagISTRIPINLCRIGNCRICRNLIUCLC
c_oflagOLCUCONLCROCRNLONOCRONLRETOFILL
というわけでLinuxでシリアル通信を行うときは通信設定が重要という話でした。参考になりましたら幸いです。