■Arduinoでパソコンからシリアル通信でリレーモジュールを操作するプログラム
1 デジタルピン10本のON/OFFが出来ること
2 現在のON/OFF状態を取得できること
3 アナログピン6本の電圧が計れること
4 パソコンから通信を確認できること
この4項目の事柄が出来ればだいぶ色々な制御に使えそうです。
他に必要だと思える機能として、
1 パソコンとの通信のエラー保護(シリアル通信の偶数パリティと命令を2重で送る事でエラーの可能性を非常に下げる事が出来そうです)
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'
&& (buff[1] == '0' || buff[1] == '1')
&& (buff[2] == '0' || buff[2] == '1')
&& (buff[3] == '0' || buff[3] == '1')
&& (buff[4] == '0' || buff[4] == '1')
&& (buff[5] == '0' || buff[5] == '1')
&& (buff[6] == '0' || buff[6] == '1')
&& (buff[7] == '0' || buff[7] == '1')
&& (buff[8] == '0' || buff[8] == '1')
&& (buff[9] == '0' || buff[9] == '1')
&& (buff[10] == '0' || buff[10] == '1')
&& buff[11] == '\r' && buff[12] == '\n' && buff[13] == '\0' && str_pt == 13
) {
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 - 1)) {
str_pt = 0;
buff_old[0] = '\0'; //エラーのため旧命令は破棄
}
} else {
//データを受信せずに500ms経過した場合にはタイムアウトで初期化
if (str_pt) {
if ((millis() - timeout_timer) > 500) {
str_pt = 0;
buff_old[0] = '\0'; //エラーのため旧命令は破棄
}
}
}
trans_pin_status();
}
2024/08/18
赤文字部分を修正しました、一つはバッファの境界を超えていたので修正。
もう一つは周期的なノイズが入ると命令形式が成立してしまう事があるためエラー時の旧命令の削除です。
(周期的なノイズはパソコンの特定の機種で電源ON時やハブの接続順により発生しました)
信頼性を上げる為に、w命令での命令形式の確認を追加しました。
上記プログラムをArduinoに書き込んでD2からD11番のデジタルピンにLEDを取り付けてデバックしている所です。
アクティブローのリレーモジュールに接続して動作を確認している所です。
■ 2重通信から3重通信へ
Arduinoとパソコンのシリアルケーブルを光ファイバーに変換して通信するようにしてみました。
そうすると、1週間に一度ほどこのようなエラーが発生するようになりました。
文字化けした同一のデータを2回受信した事によりプログラムが落ちています。
つまり、送受信のデータが頻繁に文字化けしていて偶然にも2回連続で同一個所の値が変化してプログラムが落ちたと想像できます。
そこで、安易な解決方法として、2重通信から3重通信に変更してみます。
#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 buff_old2[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) || strcmp(buff_old2, buff)) {
//比較文字列と違うため保存
strcpy(buff_old2, buff_old);
strcpy(buff_old, buff);
Serial.println("NG");
} else {
//比較文字列と同じため命令を実行
if (buff[0] == 'w'
&& (buff[1] == '0' || buff[1] == '1')
&& (buff[2] == '0' || buff[2] == '1')
&& (buff[3] == '0' || buff[3] == '1')
&& (buff[4] == '0' || buff[4] == '1')
&& (buff[5] == '0' || buff[5] == '1')
&& (buff[6] == '0' || buff[6] == '1')
&& (buff[7] == '0' || buff[7] == '1')
&& (buff[8] == '0' || buff[8] == '1')
&& (buff[9] == '0' || buff[9] == '1')
&& (buff[10] == '0' || buff[10] == '1')
&& buff[11] == '\r' && buff[12] == '\n' && buff[13] == '\0' && str_pt == 13
) {
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') {
//3回出力
Serial.print("r,");
Serial.println(pin_status);
Serial.print("r,");
Serial.println(pin_status);
//delay(50);
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);
//delay(50);
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'; //命令が終了したため旧命令は破棄
buff_old2[0] = '\0';
}
str_pt = 0;
}
//バッファサイズを超えないようにする
if (str_pt >= (BUF_SIZE - 1)) {
str_pt = 0;
buff_old[0] = '\0'; //エラーのため旧命令は破棄
buff_old2[0] = '\0';
}
} else {
//データを受信せずに500ms経過した場合にはタイムアウトで初期化
if (str_pt) {
if ((millis() - timeout_timer) > 500) {
str_pt = 0;
buff_old[0] = '\0'; //エラーのため旧命令は破棄
buff_old2[0] = '\0';
}
}
}
trans_pin_status();
}
赤文字部分が3重通信の為に修正した個所です。
これによりどれぐらいのエラーが抑え込めるのか楽しみです。
2024年10月から検証しています。