●Arduinoでシリアル通信

ArduinoにはRS232cシリアルポートが見当たりませんが、基板上にUSBシリアル変換チップが組み込まれていて、
それによりパソコンとのシリアル通信を行い、作成したプログラムなどを書き込んでいます。
安価な互換Arduinoを購入すると、USBシリアル変換チップがマイナーでドライバが標準でインストールされて無いため
チップの製造元から探してインストールする羽目になるのはそのためです。

シリアル通信を利用することによりArduinoとパソコンでデータの送受信をしたり、Arduino同士での通信などができます。


■開発環境からの送受信

Arduino IDEのツール→シリアルモニタ
シリアルモニタを開くと接続されているArduinoとのシリアル通信の確認ができます。


▼ポートの準備

最初の一度だけSerialライブラリを初期化します。
1番目の引数に転送速度を入れます。
デフォルトは,8ビット,パリティなし,1ストップビットです。
基本的にこれだけで用は足りますが、もしパリティとストップビットの設定をしたい場合には2番目の引数で設定でできます。
Serial.begin(9600);

パリティの設定方法
2番目の引数に、ビット数・パリティ・ストップビット を表す定数を設定します。
SERIAL_8E1 偶数パリティ(even)
SERIAL_8O1 奇数パリティ(odd)

Serial.begin(9600,SERIAL_8E1);
この場合、転送速度9600、8ビット、偶数パリティ、ストップビット1、になります。


▼文字の送信

C言語の考え方のprintlnは改行付き出力、printは改行なし出力の基本的な操作は同じです。
print構文は数字を出力しても、アスキーコードに変換され文字として出力します。

Serial.println(10,DEC);
これは、数字10を10進数(DEC)で文字として表示、後ろに改行を付けるを表します。

第二引数には表示する進数を入れます。
通常は、BIN OCT DEC HEXとマクロで指定されている定数を入れますが、
聞いたことのない17進数で101を表示したいとなったら、

Serial.println(101,17);
とすると、5G というこれまた見たことのない17進数の結果が表示されます。

文字列を出力するには、文字列を""で囲んで渡します。

Serial.print("Hello\n");
これは文字列Hello(改行)を出力します。

次の文はprintに渡す変数の型が違いますが、型が違うと、その型に合わせて表示されます。
char型なら一文字として表示されまが、int型だと値として表示されます。

int i = 'a';
Serial.println(i);
char c = 'a';
Serial.println(c);
数字の97とaが出力されます。

数字を出力するにはwriteを使います。

Serial.write(0x41);
0x41はアスキーコードの'A'であり、Aが表示されます。
しかし、0x00の値はwriteで送信できません

Serial.write(0);
Serial.write(0x00);

この場合、何も送信されません。
そこで、0が含まれる場合には値を文字に変換して送信する必要があります。

char buf[3];
byte i;
sprintf(buf, "%02x", i++);
Serial.println(buf);

このようにすると値が二つの文字に変換され送信できるようになります。
0→00、1→01
ただ、データ量が倍になるため通信量が多い場合には他の変換方法がおすすめです。


▼文字を出力するサンプルプログラム。
void setup () {
  Serial.begin(9600);
}

void loop () {
  Serial.println(10,DEC);
  Serial.print("Hello\n");

  int i = 'a';
  Serial.println(i);

  char c = 'a';
  Serial.println(c);

  Serial.write(0x41);
  while(1);
}

出力結果

10
Hello
97
a
A

シリアルモニタを開くとこのように結果が表示されます。



▼受信バッファに到着している値の数

受信バッファに到着している文字数を知りたいときには、次のようにします。
void loop() {
  int i = Serial.available();
  Serial.println(i);
  delay(500);
}


▼文字の受信

char c = Serial.read();
バッファに受信データが無い場合には -1 を返しますが、バイナリデータだと-1を含むので注意が必要です。
Serial.read()はノンブロッキングですので到着している文字数を取得して受信する必要があると思います。
シリアルモニタで文字を送信するとArduinoが受信しますが改行文字LF(0x0A)が文字の最後に追加されて送られてきます。

このプログラムでは受信した文字をそのまま送信します。
void setup () {
  Serial.begin(9600);
}

void loop () {
 char c;
 c = Serial.read();
 if(-1==c) return ;
 Serial.print(c);
}

このプログラムでは a が送られてくると基板上のLEDが点灯し、 b が送られてくると消灯します。
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  int c;
  c = Serial.read();
  if (-1 == c ) return;
  if (c == 'a') {
    digitalWrite(LED_BUILTIN, HIGH);
  }
  if (c == 'b') {
    digitalWrite(LED_BUILTIN, LOW);
  }
}


▼バッファのフラッシュ

Serial.flush();

シリアルデータの送信が完了するまで待ちます。
パソコンなどのようにデータを送信完了するのではなく、送信完了までプログラムが止まる仕組みです。



●Arduino搭載のシリアル変換チップを使わずにRS232c通信を行う

ArduinoのUSBにはシリアル変換チップが入っており、パソコンとシリアル通信を行っています。
それを使ってパソコンで自作アプリを作成してもシリアル通信ができます。
しかし、それでは面白くないのでArduinoにRS232c端子を付けてみたいと思います。

まずは、Arduinoの基板上にRX TXの文字がありましたので、やってはいけないのですが直接RS232cに接続してみました。
何らかの文字は送受信できるのですが、文字コードが変わっておかしい状態でした。
そもそも、RS232c側の電圧が高すぎて、Arduinoが壊れるそうです。
何かがおかしいと思い調べてみると、

Arduino側のRX TXはUARTと言うらしく、0Vと5Vでビットを表現しており、0Vが0、5Vが1だそうです。
RS232cでは、±15Vの電圧がかかり、-3V以下が1、+3V以上が0を表すそうです。

そのため、直接接続する事は出来ないため、変換する部品が必要になってきます。
RS232c TTL変換コンバーターなどがあればそれを接続すると通信が出来ますが、そんなものは持ってないため自作します。

手元にRS232cとマイコンを接続するために使うMAX3232があったため、それで作成します。
まず、データシートを検索して使い方を調べます。


データシートから見た回路図を実際の回路図に近い形で書き直しました。
使わないTTL側の入力は、正しいかどうか不明ですがフリーは良くないですのでGNDに接続しました。



RS232c端子のピンの位置、2番がRX、3番がTX、4番がDTR、5番がGNDになります。
DTRはスケッチの書き込み用に使いますので、まだ使いません。
もう少し詳しい説明はこちら、
RS232c

手元にあった適当な部品を集めて作成します。
セラミックコンデンサはすべて0.1μFです。


実際に作成した所


RX TXは受信と送信のため、受信と送信、送信と受信のペアで接続しないとパソコンとArduinoは通信できません。
そのため、作成した基盤のRXはArduinoのTXに接続し、TXはArduinoのRXに接続する必要があります。

Arduinoを作成した基盤に接続してパソコンとのRS232c通信ができました。
これで、Arduino単体でRS232c通信ができるようになりました。


■作成した基盤でArduino Proへのスケッチの書き込み

USBからプログラムの書き込みができるボードでは必要ありませんが、(そもそも、書き込み専用のシリアル端子が無いArduinoでは出来ません)
Arduino Proは最小限の部品しかない為、スケッチの書き込み端子を利用してプログラムを書き込む必要があります。
arduino pro mini atmega328

ボード:Arduino Pro or Pro Mini
プロセッサ:ATmega328P (5V,16MHz)

このような互換Arduino Proボードを使用しました。
IDEからツールにて使用するボードとクロックを設定して、下の写真の矢印の部分の書き込み端子に作成した基盤の、TXをRXDに、RXをTXDに(送信と受信を接続する)、Vcc、GND、DTRをそのまま接続することにより書き込み出来ます。
あと、基本的な事ですが、Vcc、GNDに5Vの電源を忘れると書き込めません。
DTRはArduinoのリセット動作の為にあるので、もし、接続しなくても書き込み時にタイミングよくリセットを押すとうまく行きます。



■CH340モジュールでスケッチを書き込む

作成した書き込み基盤でArduino Proに書き込んでいましたが、大きくて扱いづらいので、 もっとコンパクトなCH340モジュールというものを手に入れました、これを使って書き込みたいと思います。

KKHMF 3個 CH340モジュール USBターンTTLシリアル
このモジュールを使ってPro Miniにスケッチを書き込む場合には、FTDI コネクターに接続します。

FTDI コネクターはココになります

5V -- VCC
GND -- GND
TXD -- RXI
RXD -- TXO

の順に接続します。
その後、Arduinoのリセットボタンを押しながら、マイコンボードに書き込むボタンを押します。
画面下の表示が、次のように変わった瞬間にArduinoのリセットボタンを離すと、USB基盤の青いLEDがチカチカして書き込みができるはずです。


しかし、リセットボタンを毎回押すのは面倒なので、自動でリセットがかかるように改造します。
まずは、基盤の回路図から必要な配線を引き出します。

13番ピンにあるDTRを引き出します。

この引き出した線をArduinoのDTRと接続すると、リセットボタンを操作しなくても書き込めるようになります。



●自作パソコンソフトとArduinoを通信させる

▼自作パソコンソフトからポートを開くたびにArduinoがリセットされる場合

パソコンとArduinoがシリアルで通信するソフトを書いたりしているとポートを開くたびにリセットされる事があります。
その時には、DTRが有効になっていないでしょうか?
有効だとポートを開いた時にArduinoがリセットされます。

VB6だとDTREnableをFalseにするとリセットがかからなくなります。

このようにしておけばリセットがかからずに連続動作します。


■Arduinoでパソコンから文字列を受信するプログラム

受信した文字が[LF]であった場合には、そこを終端として処理を行います。
バッファサイズは255byte、バッファオーバーフロー対策もしてあります。
[LF]が来ない時の受信データの保持タイムアウトは500msで経過後はリセットされます。

***********[CR][LF]

という形式でパソコンなどからASCII文字データを送ると動作します。
次のプログラムでは test[CR][LF] を受信すると基板上のLEDが点灯するようになっています。
#define BUF_SIZE 255

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
}

char buff[BUF_SIZE];
unsigned int str_pt;
unsigned long int timeout_timer;

void loop() {
  if (Serial.available()) {
    char c = char(Serial.read());
    timeout_timer = millis();//msを取得
    buff[str_pt] = c;
    str_pt++;
    if (c == '\n') {//LFを終端文字とする
      buff[str_pt] = '\0';
      if (strcmp("test\r\n", buff)) {
        //比較文字列と違う
        digitalWrite(LED_BUILTIN, LOW);
        Serial.println("NG");
      } else {
        //比較文字列と同じ
        digitalWrite(LED_BUILTIN, HIGH);
        Serial.println("OK");
      }
      str_pt = 0;
    }
    //バッファサイズを超えなようにする
    if (str_pt >= BUF_SIZE) {
      str_pt = 0;
    }
  } else {
    //データを受信せずに500ms経過した場合にはタイムアウトで初期化
    if (str_pt) {
      if ((millis() - timeout_timer) > 500) {
        str_pt = 0;
      }
    }
  }
}


■Arduinoでパソコンからシリアル通信でリレーモジュールを操作するプログラム

1 デジタルピン10本のON/OFFが出来ること
2 現在のON/OFF状態を取得できること
3 アナログピン6本の電圧が計れること
4 パソコンから通信を確認できること

この4項目の事柄が出来ればだいぶ色々な制御に使えそうです。
他に必要だと思える機能として、

パソコンとの通信のエラー保護(シリアル通信の偶数パリティと命令を2重で送る事でエラーの可能性を非常に下げる事が出来そうです)
パソコンの故障などで通信が出来ない場合(通信途絶後60秒でON/OFFを初期状態に戻してOFF側に持ってゆく)

以上の機能が必要だと考えられます。


▼書き換えプロトコルを考える

1 デジタルピン10本のON/OFFが出来ること
これを実現するに当たって、次のようなコマンドを考えました

w0000000000[CR][LF]

先頭にwを付ける事により書き込みコマンドを表します。
その後ろにはデジタルピン2番から12番までの10ビットをASCII文字の0/1で表現し最後にCRLFを取り付けます。
この命令を2回送信することによりデジタルピンの出力が書き換えられて、書き換え完了後にはOK、書き換え失敗時にはNGの文字を返します。

w1000000000[CR][LF]
この文字列を送るとデジタルピン2番がONになり、他のピンはOFFになります。

w0000000001[CR][LF]
この文字列では11番がONになり、他のピンはOFFになります。

w1111111111[CR][LF]
2番から11番まですべてONになります


▼読み出しプロトコルを考える

2 現在のON/OFF状態を取得できること
実現するに当たって、次のコマンドを考えました。

r[CR][LF]

この命令を2回送信することにより、現在のON/OFF状態が返ってきます。
通信エラーを回避するために2回、同じ文字列が返ってきます。

3 アナログピン6本の電圧が計れること
実現するに当たって、次のコマンドを考えました。

a[CR][LF]

この命令を2回送信することにより、現在のアナログピンA0 A1 A2 A3 A4 A5 の電圧をカンマ区切り + この命令を実行した回数 が返ってきます。
通信エラーを回避するために2回、同じ文字列が返ってきます。

4 パソコンから通信を確認できること
パソコン側ソフトを書いていて必要だと感じたのが、テストコマンド、実現するに当たって、次のコマンドを考えました。

t[CR][LF]

コマンドを送ると常に NG[cr][lf] が返ってくるコマンドを作成しました。


以上を踏まえて、実際に使用するために必要となる機能を追加して作成してみました。
パソコンからは、9600bps、偶数パリティ、ストップビット1で接続する必要があります。
#include <avr/wdt.h>

#define SYSTEM_TIMEOUT 60  //通信途絶後60秒で初期化
#define BUF_SIZE 255

void setup() {
  wdt_enable(WDTO_4S);  //WDTを4秒で設定し有効化
  //デジタルピン2番から11番まで出力に設定
  for (int i = 2; i < 12; i++) {
    pinMode(i, OUTPUT);
  }
  Serial.begin(9600, SERIAL_8E1);  //偶数パリティ
  serial_wdt_reset();
}

char buff[BUF_SIZE];
char buff_old[BUF_SIZE];

char pin_status[11] = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '\0' };  //デジタルピン2番から11番の状態を保持し一度に転送
unsigned int str_pt;
unsigned long int timeout_timer;

//ピンの状態をまとめて転送
void trans_pin_status() {
  int j = 0;
  for (int i = 2; i < 12; i++) {
    if (pin_status[j] == '1') {
      digitalWrite(i, LOW);  //出力先がアクティブローのため反転
    } else {
      digitalWrite(i, HIGH);
    }
    j++;
  }
}

//ピンに転送前の元データを初期化する
void reset_pin_status() {
  for (int i = 0; i < 10; i++) {
    pin_status[i] = '0';
  }
  pin_status[10] = '\0';
}

unsigned long serial_wdt_counter;
//シリアル通信がSYSTEM_TIMEOUT秒停止した場合に出力をリセットする
void serial_wdt() {
  unsigned long pos = ((unsigned long)millis()) - serial_wdt_counter;
  if (pos > ((unsigned long)((unsigned long)SYSTEM_TIMEOUT * 1000))) {
    reset_pin_status();  //タイムアウトしているため初期化
  }
}

//リセットタイマーをリセットする
void serial_wdt_reset() {
  serial_wdt_counter = millis();
}

void loop() {
  wdt_reset();  //WDTのリセット
  serial_wdt();
  if (Serial.available()) {
    char c = char(Serial.read());
    timeout_timer = millis();  //msを取得
    buff[str_pt] = c;
    str_pt++;
    if (c == '\n') {  //LFを終端文字とする
      buff[str_pt] = '\0';
      if (strcmp(buff_old, buff)) {
        //比較文字列と違うため保存
        strcpy(buff_old, buff);
        Serial.println("NG");
      } else {
        //比較文字列と同じため命令を実行
        if (buff[0] == 'w') {
          int j = 0;
          for (int i = 1; i <= 10; i++) {
            if (buff[i] == '1') {
              pin_status[j] = '1';
            } else {
              pin_status[j] = '0';
            }
            j++;
          }
          Serial.println("OK");
          serial_wdt_reset();
        } else if (buff[0] == 'r') {
          //2回出力
          Serial.print("r,");
          Serial.println(pin_status);
          Serial.print("r,");
          Serial.println(pin_status);
          serial_wdt_reset();
        } else if (buff[0] == 'a') {
          static unsigned long c;
          int _ar[6];
          _ar[0] = analogRead(A0);
          _ar[1] = analogRead(A1);
          _ar[2] = analogRead(A2);
          _ar[3] = analogRead(A3);
          _ar[4] = analogRead(A4);
          _ar[5] = analogRead(A5);

          Serial.print("a,");
          for (int i = 0; i < 6; i++) {
            Serial.print(_ar[i]);
            Serial.print(',');
          }
          Serial.println(c);
          Serial.print("a,");
          for (int i = 0; i < 6; i++) {
            Serial.print(_ar[i]);
            Serial.print(',');
          }
          Serial.println(c++);
          serial_wdt_reset();
        } else if (buff[0] == 't') {
          Serial.println("NG");
        }
        buff_old[0] = '\0';  //命令が終了したため旧命令は破棄
      }
      str_pt = 0;
    }
    //バッファサイズを超えないようにする
    if (str_pt >= BUF_SIZE) {
      str_pt = 0;
    }
  } else {
    //データを受信せずに500ms経過した場合にはタイムアウトで初期化
    if (str_pt) {
      if ((millis() - timeout_timer) > 500) {
        str_pt = 0;
      }
    }
  }
  trans_pin_status();
}

上記プログラムをArduinoに書き込んでD2からD11番のデジタルピンにLEDを取り付けてデバックしている所です。


アクティブローのリレーモジュールに接続して動作を確認している所です。


■シリアル通信でリレーモジュールを操作するパソコンソフト

VisualBasic 6.0で適当に作りました。
プロジェクトを開いて実行すると、次のような画面が開きます。

Aの部分がデジタルピンのON/OFFに対応し色が変わり、ボタンを押すとON/OFFが切り替わります。
Bの部分が自動制御された場合のON/OFF状態を色で表します。
Cの部分がアナログピン6本のデータが表示されます。
Dの自動制御チェックボックスがありますが、ここをONにするとフォームにある自動制御表示のB部分が実際のポートA部分に転送されて制御が開始されます。
制御関数のPrivate Sub auto_control_stub() が2秒毎に実行されるようになっていますので制御したい機器に合わせて書き換えてください。
Eの部分はA命令を使用したカウンタの値を表示しています。


Form1の下の方に auto_control_stub() 部分がありますのでココを書き換えると自動制御ができるようになります。

VisualBasic6.0で作成したプログラムのダウンロードはこちら
arduino_serial1.zip
このプログラムではauto_control_stub()に5分ごとにON/OFFが繰り返されるように書いてあり、自動制御をONにすると5分ごとにON/OFFします。

このプログラムとArduinoを実際の現場で使用してみようと思い、念のためリレーの接点が破損しないよう270Vサージ吸収バリスタと125V CRサージアブソーバーを取り付けました。


Arduinoとリレーモジュールを板に取り付けました、さて実際に使用したらどうなることやら楽しみです。



▼パソコンソフトの改良

デジタルピンをまとめて自動制御の設定をしていましたが、個別に自動制御を設定できないと都合が悪そうなので改造しました。

ボタンの横のチェックボックスにチェックを入れた個所のみに自動制御がかかります。
今回改造したプログラムのダウンロードはこちら arduino_serial2.zip


▲トップページ > マイコンなど