Raspberry PI 4 で RS485 通信 (双方向)

まとめ

Raspberry Pi でサポートしている UART は RTSトグルをサポートしていないので RTS は自力でトグルする必要がある

使用した機器

今回は Raspberry Pi 4 と TTL-RS485 コンバーターを使用しました。

使用したTTL-RS485 コンバーター www.amazon.co.jp

UART2とUART4を有効にする。

www.ingenious.jp

こちらを参考に UART2 と UART4 を有効にする。

Linuxシリアルコンソールを無効化してから /root/config.txt の末尾に以下を追加した

dtoverlay=disable-bt
dtoverlay=uart2,ctsrts
dtoverlay=uart4,ctsrts

結線

Raspberry Pi 4 と TTL-RS485 コンバーター を以下のように結線します。

※ ICと記載されている部分がコンバーターになります

f:id:bamch0h:20211214011221p:plain

TTL-RS485 コンバーター

RS485は差動信号通信なので、2線ありますが、送信と受信が混線できません。なので送信モードと受信モードを切り替えるためには DEとREに切り替え信号を送る必要があります。

・送信イネーブル (DE) Hにすると送信回路がオンになります。

・受信イネーブル (RE) Lにすると受信回路がオンになります。

なので、送信するときは DE / RE の両方を HIGH にして、受信するときは DE / RE 両方を LOW にします。

PL011 は RTS トグルをサポートしていない

f:id:bamch0h:20211214004426p:plain

引用ドキュメント:https://developer.arm.com/documentation/ddi0183/f/

上記の通り、Raspberry Pi の UART チップの PL011の RTS ハードウェアフローコントロールでは受信バッファが規定値に達しないと RTS が動作してくれないので、送信する直前に RTS を LOWにして、送信し終わったら HIGH にするような仕組みはハードウェアレベルでは存在していない。なので自分自身で信号を制御する必要があります。

TIOCMGET / TIOCMSET

参考文献:Manpage of TTY_IOCTL

TIOCMGET / TIOCMSET を使うとシリアルの通信ラインを制御できます。RTS以外にもいろいろな情報が int でとれるので、TIOCM_RTS でビットマスクをして RTSのライン状態を取得、設定します。

RTS を OFF に設定する場合は以下のようにします。制御信号はOFFの時にHIGH、ONの時に電圧がLOWに設定されます。なので、RTSをOFFすると、TTL-RS485コンバーターのDE/REにはHIGHになり、送信モードになります。

int RTS_flag = 0;
ioctl(fd, TIOCMGET, &RTS_flag); // 一度取得
RTS_flag &= ~TIOCM_RTS; // 
ioctl(fd, TIOCMSET, &RTS_flag);

送信後待ち処理

RTS を OFF した後、write() した後 RTS を ON することでデータの送信処理は完了しますが、write() は FIFO にデータを積んだ後すぐ戻ってくるため、送信 FIFO が空になるまで待つ必要があります。待ち時間は以下の計算式で計算できます。

(データ長 + Stop ビット + パリティビット + Start ビット) * 送信データバイト数 / ボーレート = 待ち時間 (sec)

データ長 8 bit, Stop ビット 1 bit, 偶数パリティあり, 2400 bps で送信データが8バイトの場合の計算は以下の通りです。

(8 + 1 + 1 + 1) * 8 / 2400 = 11 * 8 / 2400 = 0.3666... (sec) = 366 (ms) = 366666 (μs)

これを usleep等の引数に渡して待ち処理を入れます。

送信側プログラム例

上記を踏まえて、送信プログラムを組むと以下のようになります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <linux/serial.h>
#include <sys/ioctl.h>

void my_write(int fd) {
    unsigned char buf[8] = {
        0x01,
        0x03,
        0x00,
        0x00,
        0x00,
        0x02,
        0xC4,
        0x0B
    };

    unsigned int size = sizeof(buf);

    int RTS_flag = 0;
    ioctl(fd, TIOCMGET, &RTS_flag);

    RTS_flag &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);

    write(fd, buf, size);

    int duration = size * 11 * 1000000 / 2400;
    usleep(duration);

    RTS_flag |= TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);
}

int main(int argc, char *argv[])
{
    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open error\n");
        return -1;
    }

    struct termios tio = {0};
    tio.c_cflag |= CREAD;
    tio.c_cflag |= CLOCAL;
    tio.c_cflag &= ~CRTSCTS;
    tio.c_cflag |= B2400;
    tio.c_cflag |= PARENB;
    tio.c_cflag |= 0;
    tio.c_cflag |= CS8;

    // non canonical mode setting
    tio.c_lflag = 0;
    tio.c_cc[VTIME] = 0;
    tio.c_cc[VMIN] = 1;

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &tio);

    my_write(fd);

    unsigned char buf[1];
    unsigned char rxbuf[4086] = {0};
    int len = 0;
    int total = 0;
    while(1) {
        len = read(fd, buf, 1);
        if (len <= 0) {
            fprintf(stderr, "read error!!\n");
            break;
        }

        rxbuf[total] = buf[0];
        total++;

        if(total == 9) {
            for(int i = 0; i < total; i++) {
                printf("%02X:", rxbuf[i]);
            }
            printf("\n");

            memset(rxbuf, 0, total);
            total = 0;

            my_write(fd);
        }
    }

    close(fd);
    return 0;
}

受信側プログラム例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <linux/serial.h>
#include <sys/ioctl.h>

void my_write(int fd) {
    unsigned char buf[9] = {
        0x01,
        0x03,
        0x04,
        0x00,
        0xE2,
        0x01,
        0xF2,
        0xDA,
        0x10
    };

    unsigned int size = sizeof(buf);

    int RTS_flag = 0;
    ioctl(fd, TIOCMGET, &RTS_flag);

    RTS_flag &= ~TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);

    write(fd, buf, size);

    int duration = size * 11 * 1000000 / 2400;
    usleep(duration);

    RTS_flag |= TIOCM_RTS;
    ioctl(fd, TIOCMSET, &RTS_flag);
}

int main(int argc, char *argv[])
{
    int fd;
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        fprintf(stderr, "open error\n");
        return -1;
    }

    struct termios tio = {0};
    tio.c_cflag |= CREAD;
    tio.c_cflag |= CLOCAL;
    tio.c_cflag &= ~CRTSCTS;
    tio.c_cflag |= B2400;
    tio.c_cflag |= PARENB;
    tio.c_cflag |= 0;
    tio.c_cflag |= CS8;

    // non canonical mode setting
    tio.c_lflag = 0;
    tio.c_cc[VTIME] = 0;
    tio.c_cc[VMIN] = 1;

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &tio);

    char buf[255] = {0};
    unsigned char rxbuf[4086] = {0};
    int total = 0;
    int len;
    printf("recv...\n");
    while(1) {
        len = read(fd, buf, 1);
        if (len <= 0) {
            fprintf(stderr, "read error!!\n");
            break;
        }

        rxbuf[total] = buf[0];
        total++;

        if(total == 8) {
            for(int i = 0; i < total; i++) {
                printf("%02X:", rxbuf[i]);
            }
            printf("\n");

            memset(rxbuf, 0, total);
            total = 0;

            my_write(fd);
        }
    }

    close(fd);
    return 0;
}

実行例

受信側を起動してから別ターミナルで送信側を実行します。引数にはシリアルデバイスを指定します。

$ ./recv /dev/ttyAMA2
$ ./sender /dev/ttyAMA1

まとめ

Raspberry pi 4 と TTL-RS485 コンバーターを使用して RS485 通信 (双方向)をやってみました。Raspberry pi 4 では RTSトグル処理をユーザーレベルで制御しなければならないため使い勝手があまりよくないと感じました。RS485はマイコン等を使用したほうがRaspberry pi 4 のコードはシンプルになるかもしれませんね。