背景
PCで自動制御や各種の測定を自動化する際、周辺装置に対してGPIOでオンオフ制御したり、I2Cで通信したり、アナログ値を読んだり、アナログ値を出力したりすることが頻繁に必要になるが、そのような目的に対応する拡張IOボードや拡張IOボックスが、色々なメーカーから発売されている。そのような製品は、絶縁性を備えていたり、ライブラリが充実していたり、いたれりつくせりではあるが、そこまでの性能や機能を必要としない場合は、Arduinoのような簡単なマイコンでその代わりは十分に果たせる。
今回は、Arduino Nanoを用いて、PCからUSB経由のシリアル接続(COMポート)からコマンドを送ることにより、Arduinoの指定したピンからH/Lのデジタル信号を出力したり、I2Cデバイスとの読み書きを行ったり、アナログ値の入出力、IRの出力等を行えるようにした拡張IOユニットを作ってみたので、備忘録として記録しておく。
ハードウエア
Arduino Nanoのモジュールを、ネジ端子のついた変換基板に載せただけ。(下の写真は、温度・湿度・気圧計測のBMP280モジュールがI2Cで接続されている状態)
PCとのインターフェースはUSBを使う。
仕様
端子仕様
Arduino Nanoに装備されているGPIOは基本的に全て使用可能。各ピンの電気的仕様は、Arduino Nanoの仕様を参照すること。
- D2: Digital In/Out
- D3: Digital In/Out、ただしIR出力をする時はこのピンから出力される。
- D4: Digital In/Out
- D5: Digital In/Out
- D6: Digital In/Out
- D7: Digital In/Out
- D8: Digital In/Out
- D9: Digital In/Out
- D10: Digital In/Out
- D11: Digital In/Out
- D12: Digital In/Out
- D13: Digital In/Out
- A0: Analog In/Digital In/Out
- A1: Analog In/Digital In/Out
- A2: Analog In/Digital In/Out
- A3: Analog In/Digital In/Out
- A4: I2CのSDA専用 (プルアップあり)
- A5: I2CのSCL専用 (プルアップあり)
- A6: Analog In
- A7: Analog In
コマンド
シリアル通信で送るコマンド表は下記。シリアル通信は、9600baudに固定されている。コマンド表の書式に従った文字列を送り、最後に改行(CRLF)を送った時点でコマンドが実行される。読み取り系のコマンドの場合は、その直後、シリアルポートを読めば指定したコマンドに相当する応答を読み取ることが出来る。
各パラメータを、コロン(:)で区切り指定する。コマンドの文字は全て大文字。空白やタブ等は入れない。HEX値で指定する所は、小文字のHEX値でも大丈夫。ピン番号や不当なパラメターに対するチェックはほとんど入れてないので、広く活用するためには、エラーチェック等を強化する必要あり。
VISAコマンドの書式を少し真似て、コマンドの先頭にもコロンを付けることにしているが、無くても動作する(先頭のコロンは無視するようにしているため)
コマンド | パラメター、例 |
PMOD |
:PMOD:ピン番号:入力あるいは出力指定 例):PMOD:2:OUTPUT 例):PMOD:3:INPUT 例):PMOD:4:PULLUP |
DIGW |
:DIGW:ピン番号:LOWあるいはHIGH指定 例):DIGW:2:LOW 例):DIGW:2:HIGH |
DIGR |
:DIGR:ピン番号 例):DIGR:3 ピンを読んだ結果は、HIGHレベルの時、”1”が、LOWレベルの時 “0”がそれぞれ返ってくる。 |
ANAW |
:ANAW:ピン番号:PWM出力値(HEX値で0からFFの範囲) ピン番号で指定できるのは、3, 5, 6, 9, 11のどれか。 例):ANAW:5:80 この例では、5番ピンに 80h (50%)デューティー比のPWM信号を出力する。指定出来るPWM値は0から FF。必ずHEX値で指定すること。10hex以下の場合、先頭のゼロは付けない。例 ANAW:5:e (この例では、5番ピンに eすなわち14のPWMを出力する) PWMのデューティー比の計算は、例えば x値の場合は、x/256になる(はず) |
ANAR |
注意:未実装 :ANAR:ピン番号 指定したピンのアナログ電圧を数値(0~255)で読む。 |
I2CW |
指定したスレーブアドレスのデバイスの、指定したサブアドレスに、指定したデータを書き込む。書き込むバイト数指定は無く、コロンで接続したデータ列を全て書き込む。最大同時書き込みバイト数は10バイト(要チェック) :I2CW:スレーブアドレス:サブアドレス:データ0:データ1:… 全てのアドレスやデータはHEX値で指定すること(大文字、小文字可)。エラーチェックは入っていないので注意。0xとかhexとかの接頭語、接尾後は付けてはいけない。 例):I2CW:76:E0:B6 例):I2CW:76:F4:27:00 |
I2CR |
指定したスレーブアドレスのデバイスの、指定したサブアドレスから、指定したバイト数のデータを読み込む。読み出しバイト数を指定する必要がある。全ての数字はHEX値で指定すること(大文字、小文字可)。読み出しバイト数もHEX値として解釈される。最大読み出しバイト数は理論上 255バイトだが、確認していない。32バイトまでは実績あり。 :I2CR:スレーブアドレス:サブアドレス:バイト数 例):I2CR:76:88:18 (この例では、76hのデバイスの 88h番地から 18h(24バイト)のデータを読み込む事になる。 結果は、コロンでバイト毎に区切られたHEX値の文字列で返ってくる。 (c7:6e:ae:66:32:00:60:91:10:d6:d0:0b:6e:22:4b:ff:f9:ff:0c:30:20:d1:88:13) |
IRWR |
注意:実装テスト中に付き、正しくは動作しない 指定したメーカーコードの、指定したコードに相当するパルス信号をD3ピンから出力する。 例) :IRWR:SONY:A91:12 この例で、ソニーのTVカテゴリー、電源コマンド(12ビット)を出力する。 |
Arduinoのコード
IRリモート用の IRremote ライブラリと、I2C用のWire.hを使っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
// // #include <IRremote.h> #define SIZE_OF_ARRAY(ary) (sizeof(ary)/sizeof((ary)[0])) #include <Wire.h> IRsend irsend; enum cmdList {PMOD, DIGW, DIGR, ANAW, ANAR, I2CW, I2CR, IRWR}; String inputString = ""; //文字列を指定したデリミターで分割する関数 int split(String *result, size_t resultsize, String src, char delimiter) { int index = 0; for (int i = 0; i < src.length(); i++) { char tmp = src.charAt(i); if ( tmp == delimiter ) { index++; if ( index > (resultsize - 1)) return -1; } else result[index] += tmp; } return (index + 1); //個数を返す } bool equal(String s0, String s1) { if (s0.compareTo(s1) == 0) return true; return false; } byte hex2byte(String src) { char buf[32]; src.toCharArray(buf, src.length() + 1); //include \0 at the end. return strtol(buf, NULL, 16); //source: HEX --> return byte } String parse_cmd(String cmd) { int index; //分割後の要素数 String ss[10] = {"\0"}; //分割された文字列を格納する配列 size_t arraysize = SIZE_OF_ARRAY(ss); //配列の要素数 char delimiter = ':'; //区切り文字 if (cmd.charAt(0) == ':') { cmd.remove(0, 1); //先頭の:は無視 } index = split(ss, arraysize, cmd, delimiter); byte cmd0; byte i2c_adr, i2c_sub_adr, i2c_n_bytes; byte pin, pin_value, pwm; char buf[32]; String i2c_buf = ""; String ret = ""; if (equal(ss[0], "PMOD")) cmd0 = PMOD; else if (equal(ss[0], "DIGW")) cmd0 = DIGW; else if (equal(ss[0], "DIGR")) cmd0 = DIGR; else if (equal(ss[0], "ANAW")) cmd0 = ANAW; else if (equal(ss[0], "ANAR")) cmd0 = ANAR; else if (equal(ss[0], "I2CW")) cmd0 = I2CW; else if (equal(ss[0], "I2CR")) cmd0 = I2CR; else if (equal(ss[0], "IRWR")) cmd0 = IRWR; switch (cmd0) { case PMOD: pin = ss[1].toInt(); if (equal(ss[2], "INPUT")) pinMode(pin, INPUT); else if (equal(ss[2], "OUTPUT")) pinMode(pin, OUTPUT); else if (equal(ss[2], "PULLUP")) pinMode(pin, INPUT_PULLUP); break; case DIGW: pin = ss[1].toInt(); if (equal(ss[2], "HIGH")) digitalWrite(pin, HIGH); else if (equal(ss[2], "LOW")) digitalWrite(pin, LOW); break; case DIGR: pin = ss[1].toInt(); ret = String(digitalRead(pin)); break; case ANAW: pin = ss[1].toInt(); pwm = hex2byte(ss[2]); analogWrite(pin, pwm); break; case I2CW: i2c_adr = hex2byte(ss[1]); i2c_sub_adr = hex2byte(ss[2]); Wire.beginTransmission(i2c_adr); Wire.write(i2c_sub_adr); for (int i = 3; i < index; i++) { //ignore: command, slave_address, sub_address Wire.write(hex2byte(ss[i])); } Wire.endTransmission(); break; case I2CR://read時、読むサブアドレスの指定が必要 i2c_adr = hex2byte(ss[1]); i2c_sub_adr = hex2byte(ss[2]); i2c_n_bytes = hex2byte(ss[3]); Wire.beginTransmission(i2c_adr); Wire.write(i2c_sub_adr); Wire.endTransmission(); Wire.requestFrom(i2c_adr, i2c_n_bytes); for (int i = 0; i < i2c_n_bytes; i++) { byte b = Wire.read(); if (b < 16) i2c_buf.concat("0"); i2c_buf.concat(String(b, HEX)); if (i != (i2c_n_bytes - 1)) i2c_buf.concat(":"); } ret = i2c_buf; break; case IRWR: for (int i=0; i < 3; i++) { irsend.sendSony(0xa90, 12); delay(25); } break; default: ret = "invalid command"; } return ret; } void setup() { Serial.begin(9600); Wire.begin(); } void loop() { if (Serial.available()) { char inChar = (char)Serial.read(); if (inChar == '\n') { String ret = parse_cmd(inputString); char buf[32]; ret.toCharArray(buf, ret.length() + 1); //include \0 at the end. if (ret.length() != 0) Serial.println(buf); inputString = ""; } else { inputString += inChar; } } } |
このArduino-IO拡張ツールとPC上のPythonプログラムを使って、Excel上に表現したシーケンス一覧からコマンドを読み取り、実行した結果をExcelに書き込むという自動化ツールを作成したので、後日、別の記事として掲載する予定。