DTR と Arduino の関係

副題: C# (.Net) の SerialPort クラス経由で Arduino を操作する場合の tips

C#Arduino という時点で文献の数が極度に少ないので四苦八苦してたのですが、どうにか感触はつかめたっぽいのでメモ。

C# で書いた制御ソフトから Arduino をシリアル経由で触っている最中に、ソフト側から LED 点灯の指示を出しても Arduino 側では一瞬点灯後即消灯する、という不可解な現象に遭遇しました。再現性が必ずしもあるわけではなく、原因を探るのに難儀したのですが、どうやら犯人は

SerialPort.DtrEnable = true;

だったようです。

一部のサイト

SerialPort.DtrEnable = true;

が必須だと書いてあったのでそのまま鵜呑みにしていたのですが、実は DTR 信号線にはまた別の役割があります。 arduino.cc のリファレンスページから抜粋、和訳。

自動(ソフトウェア)リセットについて

スケッチをアップロードする前に物理的にリセットスイッチを押すのではなく*1Arduino Duemilanove は繋がったコンピューターからソフトウェア的にリセットがかけられるように設計されています。これは、ハードウェアによるフロー制御信号の一種である DTR 信号線を、 FT232RL から 100 nf コンデンサを挟んで ATmega168 もしくは ATmega328 のリセット回路へと接続することで実現されています。この信号線が有効な状態、すなわち LOW の状態になると、リセット回路の電圧がICをリセットするのに十分な時間、降下します*2。この特性を利用して、 Arduino IDE の Upload ボタンを押すだけでスケッチのアップロードを行えるのです。DTR 信号線の電圧降下のタイミングはスケッチのアップロード開始のタイミングと一致するよう調整されているので、ブートローダーはほとんどタイムアウト状態になることはありません。

このリセット特性は、ほかにも影響することがあります。 Duemilanove が Mac OS X もしくは Linux マシンに繋がっていた場合、ソフトウェアからUSBを経由した接続がある度にリセットがかかります。そのため、接続後 0.5 秒程度は Duemilanove 上ではスケッチではなくブートローダーが動作することになります。ブートローダー動作中は悪意のあるデータ、つまり新たなスケッチのアップロードに関するデータ以外は無視するように設計されているので、接続が開かれた直後に送信されたデータの最初の数バイトは無視されます。もし、 Arduino で動作するスケッチが起動時に初期設定等のデータを受け取る仕様になっているならば、送信側のソフトウェアではポートの開放後それらのデータ送信を行うまで1秒程度待つ必要があります。

Duemilanove の回路上にある "RISET-EN" とラベルされたランドを切断することで、この自動リセットを無効にすることができます。再度有効にするには、ランドどうしをはんだで接合してください。または、 5V ピンとリセットピンを 110 Ω(ohm) の抵抗で接続することでも自動リセットを無効にすることができます。詳しくはこちらのスレッドを見てください。

If a sketch running on the board receives one-time configuration or other data when it first starts, make sure that the software with which it communicates waits a second after opening the connection and before sending this data.

Arduino - ArduinoBoardDuemilanove

とあるように、 DTR 信号線はリセット回路に直結してるので、 DTR を Enable にするとその都度ソフトウェア・リセットがかかってしまいます。リセットを掛けるということはそれまで保持していた変数や各IOポートの状態なども全て初期化されてしまうということ*3ですので、上記解説ページにもあるとおりデータの送信タイミングに気を遣う必要があります。最初に書いた、私が遭遇したケースでは、おそらく、

信号受信 → リセット → LED 点灯 → 光った!!!
信号受信 → LED 点灯 → リセット → なんで光らないの???

というパターンだったのではないかと思います。

ちなみに、 Mac/Linux では接続時に勝手に DTR リセットがかかるようなので特段問題はない*4のですが、 Windows の場合、起動前から USB に刺しっぱなしとかで起動後にソフトウェア側からの呼びかけに応答しない、などという事態がしばしば発生します。この場合にはソフトウェア的にでも、本体のリセットボタンを押すというハードウェア的にでも、どちらでも構わないのですが、リセットを掛けてやると復活するということが多々あります。

そういう場合、 VS2008 で SerialPort クラスを使うと、たとえば以下のように書けます。

private string makeSerialConnection(string tx)
{
  string rx = null;

  try
  {
    using (System.IO.Ports.SerialPort sp = new System.IO.Ports.SerialPort("COM1", 9600, Parity.None, 8, StopBits.One))
    {
      sp.ReadTimeout = 5000;
      sp.DtrEnable = true;
      sp.Open(); 

      // Waiting for arduino to reset...
      System.Threading.Thread.Sleep(3000);

      sp.DtrEnable = false;
      sp.Write(tx);

      while (rx == null) { rx = sp.ReadLine().Trim('\r'); }
      sp.Close();
      sp.Dispose();

    }
  }
  
  catch (Exception e)
  {
    MessageBox.Show(e.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
    rx = null;
  }

  return rx;

}

ソース見てもらえばすぐ分かると思いますが、一度

SerialPort.DtrEnable = true;

としてポートを開き、

System.Threading.Thread.Sleep(3000);

で 3 秒スリープして Arduino がリセットするのを待ち、改めて

SerialPort.DtrEnable = false;

としてからメッセージを送信しています。個人的な感触では3秒も要らない気もしますが、念のため。

本来、 SerialPort クラスを使う場合は一般的に非同期的に通信する必要があります。今回、特に

System.Threading.Thread.Sleep(3000);

を使っていることもあり、応答性は非常に悪く、このままの形でこのコードを使うのはおすすめしません。

あまり .netarduino の関係を纏めたものが無く、 DTR について触れているものもなかったので書いてみました。ちなみに Processing ではどういう処理になってるのかは分からないです。

*1:一世代前の Arduino NG ではスケッチアップロード前の手動リセットが必須だった

*2:回路図(PDF注意)を見るとタクトスイッチと並列に設計されているのが分かる

*3:再度 setup() が実行されるということ

*4:わけではないけど…