mcommit's message

ソフトウェア開発の仕事をしているsimotinといいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

Linuxでシリアル通信のプログラム(C言語)を書く

Linux上でC言語でシリアル通信をするプログラムを書く際に、いろいろ調べたので書いておきたいと思います。

シリアル通信プログラムの流れ

Linuxでシリアル通信プログラムを書く際の大まかな流れですが、

  1. シリアルポートに対応するデバイスファイルをオープンする
  2. termios構造体を使って通信設定する
  3. termios構造体の設定値をポートに反映させる
  4. read/write関数を使って通信する

というような流れになります。

シリアルポートのデバイスファイル

一般的にUnix/Linux系OSではシリアルポートは/dev/ttyS* にマッピングされています。tty はテレタイプ(teletype)を意味するそうです。

termios構造体

termios構造体を使ってシリアル通信のための設定をします。

■参考にするmanページ

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html

設定処理は結構面倒臭い感じになりますが、おおむねビット毎にパリティやらデータビットやらの役割が決められているような感じですね。

制御コードに気をつけろ!

シリアル通信のプログラミングで気を付けないといけないことがいくつかあります。
termios構造体は名前の通り端末との通信を扱うことを意識した構造体です。

そもそもUNIXは現代のパソコンとは使い勝手の異なるシステムでした。

Unixシステムには、端末と呼ばれるキャラクタ画面とキーボードがついた装置が複数つながれていて、何か計算をしたい人たちはこの「端末」からログインし、コンピュータを使っていました。

そして、termios構造体はこの「端末」との入出力を制御するために使われていたのです。

今となっては懐かしい時代ですね・・・産まれてなかったけど。

前置きが長くなりましたが、センサや各種デバイスと通信する際は生データ(8bit)で通信することが多いと思いますが、
termios構造体を使う場合そのままでは生データとして受信できません。上位ビットがマスクされてしまうようです。

生データを扱う場合は、cfmakeraw関数により設定値を生データの送受信用の値にしてあげる必要があるそうです。

ちなみに、cfmakerawの中の人は、

 termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    termios_p->c_oflag &= ~OPOST;
    termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    termios_p->c_cflag &= ~(CSIZE | PARENB);
    termios_p->c_cflag |= CS8;

をしてくれているそうです。

raspberry piで動かしてみる。

というわけでtermios構造体を使うC言語のコードを書いてみました。
以前Raspberrypiで、Rubyを使ってシリアル通信をする記事を書きましたが、

mcommit.hatenadiary.com


今回はtermios構造体を使うC言語のコードを書いて、Raspberry piで動かしてみました。
ソースコードはこんな感じです。

Raspberry pi で動かしてみたコード(C言語)

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>

#define SERIAL_PORT "/dev/serial0"

int main(int argc, char *argv[])
{
    unsigned char msg[] = "serial port open...\n";
    unsigned char buf[255];             // バッファ
    int fd;                             // ファイルディスクリプタ
    struct termios tio;                 // シリアル通信設定
    int baudRate = B9600;
    int i;
    int len;
    int ret;
    int size;

    fd = open(SERIAL_PORT, O_RDWR);     // デバイスをオープンする
    if (fd < 0) {
        printf("open error\n");
        return -1;
    }

    tio.c_cflag += CREAD;               // 受信有効
    tio.c_cflag += CLOCAL;              // ローカルライン(モデム制御なし)
    tio.c_cflag += CS8;                 // データビット:8bit
    tio.c_cflag += 0;                   // ストップビット:1bit
    tio.c_cflag += 0;                   // パリティ:None

    cfsetispeed( &tio, baudRate );
    cfsetospeed( &tio, baudRate );

    cfmakeraw(&tio);                    // RAWモード

    tcsetattr( fd, TCSANOW, &tio );     // デバイスに設定を行う

    ioctl(fd, TCSETS, &tio);            // ポートの設定を有効にする

    // 送受信処理ループ
    while(1) {
        len = read(fd, buf, sizeof(buf));
        if (0 < len) {
            for(i = 0; i < len; i++) {
                printf("%02X", buf[i]);
            }
            printf("\n");
        }

        // エコーバック
        write(fd, buf, len);
    }

    close(fd);                              // デバイスのクローズ
    return 0;
}

Raspberrypi のデバイスファイルについて

前回の記事でも書きましたが、Raspberrypiのシリアルポートのデバイスファイルは "/dev/serial0"としてマッピングされています。
手元のRaspberrypiだと/dev/ttyS0だとopenは成功しましたが、うまく通信できませんでした。ご注意ください。

結線は以前の記事でも書きましたが、
6 :GND
8 :TXD
10:RXD
になります。

感想

組み込みソフトのプログラミングだとシリアル通信のプログラミングは比較的簡単な部類に入ります。
UARTのデバイスドライバは割り込みハンドラを入れてもC言語で200~300行程度にしかなりません。
UARTは各種デバイスやセンサなどとのインターフェースとして使われますが、「端末」との通信のように制御文字処理する必要はありません。
今回termiousを使ってみてその辺の作法がよくわからず少し苦労しました。

現代ではサーバへのログインアクセスはもっぱらSSHになりました。
RS-232Cを使ったシリアルコンソールログインなどは組み込みLinuxを使う時くらいしかありません。
(SSHと違ってシリアルコンソールだとviがうまく動かなかったりするんですよね...)

そのためか「termios/端末プログラミング」について触れられている書籍はあまりありません。

いろいろと本棚の書籍やKindleをあさっていると、何と詳解Unixプログラミングには「端末入出力」の章があるではありませんか!サンクス、Mr.スティーブンス.

詳解UNIXプログラミング 第3版

詳解UNIXプログラミング 第3版

詳解Unixプログラムは分厚くて机に置いておくと邪魔になるので普段めったに読まない本ですが、こういうときに役に立つんですね。Unix/Linux系のコードを書くときはやはりありがたい一冊です。

ちなみに、Linuxプログラミングインターフェースという本でも端末プログラミングについて解説されている章があるようです。こちらの本は読んでみたいのですが、ちょっと高いな・・・

Linuxプログラミングインタフェース

Linuxプログラミングインタフェース

termiousを使うプログラムはUnixとしての「端末プログラミング」の作法を押さえておく必要がありそうです。

あと、よく考えてみればRubyのserialportがRaspberry pi上で動いているので、素直に拡張ライブラリのコードを読んでみるというのも1つの手段だと思いました。時間があるとき読んでみたいと思います。