Arduinoとの通信1

電子回路(Arduino)で構成された実験装置を自分で作成して、Matlab(PTB)と通信する方法をご説明します。

Arduinoについてはここでは詳しく説明しませんが、市販の本もたくさんあり、それほど専門的な知識を必要としません。例えばArduinoを利用すると、圧力や温度センサーからの情報を簡単にMatlabに読み込むことができます。

ここでは2つの圧力センサーを用いて、それぞれの出力をMatlab側で受け取る方法を例にあげてご説明します。

まず圧力センサーですが、インターリンク社のFSR 406 を使用しています。赤と黒の導線をはんだづけします。これを2組使用します。

次にArduino側です。(画像をクリックすると拡大表示されます)

今回は、5V、GND、A0、A1しか使用しません。

ArduinoとパソコンをUSBケーブルで接続します。

電源はUSBケーブルから取っています。

ブレッドボード側です。(画像をクリックすると拡大表示されます)

回路の配線としては以上になります。

プログラムについて

ArduinoとMatlab(PTB)との通信には、シリアル通信を利用します。シリアル通信についてはこちらもご参照ください。(実際にはRS232cではなく、USBケーブルを使用します)

シリアルポートの名前は、Macであれば、ArduinoをUSBケーブルで接続した状態でターミナルを立ち上げて、

ls /dev/tty.usb*

と入力すると確認できるはずです。

Windows ではCOM1などの表記になります。デバイスマネージャーから確認できます。

Arduinoのスケッチは次の通りです。

  1. setupの中で、文字AをMatlabに送信。(Arduino側の準備ができた合図)
  2. Matlabから文字A(ASCIIコード 65)を受け取ったらセンサーから読み込んだ値を送信。
  3. Matlabから文字A「以外」を受け取ったらデータの送信を終了。
// 2つのセンサーをA0とA1に接続
const int SENSOR1 = 0; // アナログ入力 A0
const int SENSOR2 = 1; // アナログ入力 A1
int sen1 = 0; // A0から読み込んだデータ
int sen2 = 0; // A1から読み込んだデータ
int inByte = 0; // バッファーから読み込んだ1バイト分のデータ
int SA = 0; // Serial.available();
int flag = 0; // flagが1のときはパソコン(Matlab)にデータを送信
void setup() {
  Serial.begin(38400); // 通信速度(bits/sec)はお使いの環境に合わせてください。
  Serial.print("A\n"); // Arduinoの準備が整ったことをパソコンに通知
}
void loop() {
    
    SA = Serial.available();
    if (SA > 0) { // バッファーにデータがたまっていたら
        inByte = Serial.read(); // バッファーのデータを1バイト読み込む(バッファーは空になる)
        
        if (inByte == 65) {// Matlabから'A'が送られていたら
          flag = 1;
        } else {
          flag = 0;
        }
    }
    
    if (flag == 1){
        // アナログデータの読み取り
        sen1 = analogRead(SENSOR1);
        sen2 = analogRead(SENSOR2);
      
        // パソコンへのデータの送信
        Serial.print(SA, DEC);
        Serial.print(",");
        Serial.print(inByte, DEC);
        Serial.print(",");
        Serial.print(sen1, DEC);
        Serial.print(",");
        Serial.print(sen2, DEC);
        
        Serial.print("\n"); //終端子
        
        delay(10); // サンプリング間隔(ms)。チャタリング防止にもなっている。
    }
}

Matlabのソースコードは次の通りです。

  1. Arduinoから文字Aを受け取ったら、データの読み込み(fscanf)を開始。
  2. 読み込んだデータはその都度ファイルに出力。(ファイルへの書き込みは最後に一括して一度だけ行うのがよいのかとも思いましたが、私の環境では特に違いはありませんでした)
  3. Arduinoとの通信は2ブロック行っています。(for文の繰り返し回数)

特に注意が必要なのは、常にArduinoからのデータを受け付けていると、入力バッファーにデータが蓄積されるという点です。BytesAvailableの値が大きいということは、それだけデータが蓄積されているということであり、実際の時間よりも遅れたデータを読み込んでいることになります。

function serialsample1
% 実験参加者名の入力
SubName = input('Name? ', 's'); % 名前をたずねる
if isempty(SubName) % 名前の入力がなかったらプログラムを終了
    return;
end;
% 出力ファイルの上書き確認を行う
SaveFileName=[SubName '.csv']; % 出力ファイル名
if exist(SaveFileName, 'file') % すでに同じ名前のファイルが存在していないかの確認
    resp=input([SaveFileName 'はすでに存在します。上書きをしてよい場合は y を入力してエンターキーを押してください。'], 's');
    if ~strcmp(resp,'y') 
        disp('プログラムを強制終了しました。')
        return
    end
end
% 呼び出しておいたほうがよい関数たち。
AssertOpenGL; 
KbName('UnifyKeyNames'); %OSで共通のキー配置にする
ListenChar(2); % Matlabに対するキー入力を無効
GetSecs;
WaitSecs(0.1);
%HideCursor;
openFlag = 0;
try
   
    % シリアルポートオブジェクト
    myserial = serial('/dev/tty.usbmodemfa1311', 'BaudRate', 38400) % お使いの環境に合わせてシリアルポートの名前を書き換えてください。
    % windowsでは serial('COM1'・・・などの表記になります。
    fopen(myserial);
    myserial.Status;
    get(myserial)
    
    % 出力ファイルを開く
    Fid = fopen(SaveFileName, 'wt');
    openFlag = 1;
    
    fprintf(Fid, '%s\n', SubName);
    fprintf(Fid, '%s\n', datestr(now, 'yy-mmdd-HH:MM'));
    fprintf(Fid, '%s\n', '');
    
    % 出力ファイルの見出し
    fprintf(Fid, '%s\n', '通し番号,oldBA,BA,データ数,バイト数,SA,inByte,Sen1,Sen2');
        
    % Arduinoとの通信を確立するため、データ'A'を受け取るのを待つ。
    while myserial.BytesAvailable == 0 
    end;    
    startChar = fscanf(myserial); % 入力バッファーからデータを読み込み
    %whos('startChar')
    %double(startChar)
    if startChar ~= 'A'
        error('通信を確立できませんでした。')
    end;
    
    for i = 1:2
        while myserial.BytesAvailable
            fread(myserial, myserial.BytesAvailable); % バッファーを空に。
            WaitSecs(0.5);
        end;
        
        fprintf('スペースキーを押すとシリアル通信を開始します。\n');
        while KbCheck; end; % いずれのキーも押していないことを確認
        while 1 % while 文の中をぐるぐる回ります。
            [ keyIsDown, keyTime, keyCode ] = KbCheck; % キーが押されたか、そのときの時間、どのキーか、の情報を取得する
            if keyIsDown
                if keyCode(KbName('SPACE')) % 
                    fprintf(myserial, '%c', 'A'); % Arduinoに送信許可を送る。(終端子を含まない形で、1バイト送る)
                    break; % while文を抜ける。
                end;
                while KbCheck; end;                    
            end;
        end;
        %myserial.BytesAvailable
        fprintf('計測中・・・\n');
        cnt = 1;
        baseTime = GetSecs;
        while GetSecs - baseTime < 5
            oldBA = myserial.BytesAvailable;
            if oldBA > 0 % データの一部のみが入力バッファーにある可能性があることに注意。
                %--------------------------------
                % カンマ区切りのデータなどの扱い
                rawdata = fscanf(myserial); % 終端子が読み込まれるまで待つ。
                tmpCell = textscan(rawdata, '%d%d%d%d', 'delimiter', ','); % Arduinoから送られてくるデータは4種類(%dの数に注意)
                [SA, inByte, sen1, sen2] = deal(tmpCell{:});
                
                % BA = BytesAvailable。これが大きな値だと読み込んだデータに時間の遅れがあるということ。
                % inByteが65のとき、これは「A」のASCIIコードを表している。
                % inByteが10のとき、これは終端子「LF」のASCIIコードを表している。
                info = whos('rawdata');
                fprintf(Fid, '%d,%d,%d,%d,%d,%d,%d,%d,%d\n', cnt, oldBA, myserial.BytesAvailable, info.size(2), info.bytes, SA, inByte, sen1, sen2);
                cnt = cnt + 1;
            end;
        end;
        fprintf(myserial, '%c', 'B'); % Arduinoにデータの送信を停止させる。('A'ではなく'B'を送っていることに注意)
        fprintf(Fid, '\n');
        cnt % 読み込んだデータ(行)の数
        fprintf('計測を終了しました。\n');
        WaitSecs(2); % 少し待ち時間をはさまないと次のブロックにデータが残ることがある。
    end;
    %終了処理
    fclose(Fid); % ファイルを閉じる。
    fclose(myserial);
    delete(myserial);
    clear myserial;
    
    ShowCursor;
    ListenChar(0);
    
catch % 以下はプログラムを中断したときのみ実行される。
    %if exist('Fid', 'var') % ファイルを開いていたら閉じる。
    if openFlag
        fclose(Fid);
        disp('fclose');
    end;
    
    fclose(myserial);
    delete(myserial);
    clear myserial;
    
    ShowCursor;
    ListenChar(0);
    psychrethrow(psychlasterror);
end

Matlabのプログラムと同じフォルダのなかにCSVファイルが作成されます。

センサーの情報が正しく書き込まれていることを確認してください。また入力バッファーのデータ(BA)が大きな値になっていないことを確認してください。

通信速度(baudrate; ビット/秒)について

Arduinodでは、300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200 ビット/秒に、Matlabでは110、300、600、1200、2400、4800、9600、14400、19200、38400、57600、115200、128000、256000 ビット/秒に対応しているようです。Matlabの規定値は9600です。

上記のサンプルでは、1回あたりに送信されるデータ(終端子までのデータ)は10バイト前後になりますので次のように考えます。

10バイト=80ビット(1バイトは8ビット)

サンプリング間隔が10msの場合、1秒間には、

80ビット×(1000 ms / 10 ms) = 8000 ビット

この場合は9600ビット/秒以上の通信速度があれば十分なようです。

データの個数について

今回のサンプルでは、サンプリング間隔が10msで、5秒間データを計測しているので理論的には500個のデータが記録されるはずです。しかし実際に測定(cntの値)してみると、私の環境では460個でした。

ということは実際のサンプリング間隔は、5000 ms / 460 = 約10.87 ms になります。

おそらく1msほどが、データの書き込みなどに費やされているものと思われます。

今回のサンプルではPTBの機能はキー入力や時間取得にわずかに使用した程度です。自作の装置から受け取ったデータを、リアルタイムで刺激の描画に利用する方法については、Arduinoとの通信2をご覧ください。

参考資料