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_iflag
ISTRIP
INLCR
IGNCR
ICRNL
IUCLC
c_oflag
OLCUC
ONLCR
OCRNL
ONOCR
ONLRET
OFILL
というわけでLinuxでシリアル通信を行うときは通信設定が重要という話でした。参考になりましたら幸いです。