Arduino の回路シュミレーション


ドキュメント履歴: 2019-02-16 初回アップ

内容




  1. Arduinoでデバッグしたい!
  2. Arduinoと言う RISCコアの高性能マイコンチップを使用したユニットが中国のネット通販でワンコイン程度で安く買えることを知って、「70の手習い」よろしく Arduino IDEとか VisualStudio Codeなどという統合開発環境下 C言語でコーディングを始めた。
    今までマイコンチップと言えば MicroChip社の PICと呼ばれるワンチップマイコンのプログラムをアセンブラ言語でシコシコ書くだけだったが、PICと比べると on boardでプログラムが書き込みできる(=別途書き込み器を使わないで済む)気軽さや、PICの変則的なレジスタ命令などに比べて素直な C言語のコーディングに ついついハマってしまった。
    しかし 標準の Arduino(UNO/Nanoなど)には 書き込み用に USB変換チップは搭載されているもののデバック用チップが非搭載なので、タイマー割込を使った途端バグってしまいデバッグ機能の必要性を思い知らされた。しかし「デバッグ」と言っても所詮アマチュアで大きなシステムではなく実行途中の変数の値をチェックする程度の簡単なプログラムのシュミレーションとかトレースが出来ればいい、と言うレベルの話。
    そこで Webで 「Arduino シュミレーション」などと検索したら Webアプリの電気回路シュミレータ(アナログシュミレータではない:ロジックシュミレータ)の中に Arduinoの動作モデルが準備されているものがあることが分かり、どうやらこの回路シュミレータの中でブレークポイントを設定して変数をチェックできるらしいと分かった。
    無料だしブラウザ版なので試すのにプログラムをインストールする必要もないので早速 IDを登録して使ってみたところ、(少なくとも私には)驚くほど高機能で、しかも非常に使い勝手が良いUIだった。回路シュミレータと聞くと初期設定やら何やら物凄く面倒臭そうな気がしていたが、ほとんど論理シュミレーションだけ(素子の遅延などは無視している模様)でプログラムの途中経過や結果が確認できる。面白くてつい本旨のデバッグ完了後もあれこれ使い回してしまった。
    実は現在の Thinkercadは、123Dなどで有名な Autodesk社が買収して新しく提供しているらしく、同一プラットフォームで三次元モデリング用CADも使える(と言うかこっちの方がメイン?)。
    どうしてこんな高機能なアプリが「タダ」なのかと訝ってしまうが、アプリもそうだが部品を全く買わずに「完全無料」でプログラムの確認・練習ができると言うのはありがたい。(但し現状では使える部品種類はけして多いとは言えない。シュミレーション出来るマイコンは Arduino UNO系(Atmel ATmega328P) だけ)


  3. 回路シュミレータ:Thinkercad Thinkercad
  4. 上に述べたように、この回路シュミレータは Webアプリ(Chrome/FireFoxなどのブラウザで Thinkercadのページにアクセスして、登録した IDでログインすれば即使える)なのでインターネット環境は必須になる。まず Thinkercadのページにアクセスして初回は ID登録(居住国、メアド、生年月日、パスワードだけを登録)すれば、即利用が可能になる。
    上のログイン後の初期画面でシュミレータを使うには「Circuits」ボタンをクリックして「新しい回路を作成」をクリックする。(2回目からは前回の作業中の画面が呼び出される。ブラウザを閉じても再度アクセスしてログインすれば前回の状態が復元される。「保存」と言う概念がなく、回路やコードを変更するとほぼリアルタイムで保存されるので、間違って部品やコーディングを大幅に削除すると「戻る」ボタンでしか救済されない。)
    Webアプリと言うと、クライアント(手元のPC)で入力したデータがサーバーに送られて処理され、その結果が返送されてきて PCに表示される・・・・と言うようなマダルッコシイ動作になるのでは?と言う気がするが、実際に操作してみると通常のアプリとほとんど変わりないレスポンスだ。おそらく画面表示などへのフィードバックはブラウザの Javaなどが処理しているのだろう。


  5. 回路図作成方法
  6. 上の図の「Circuits」ボタンをクリックすると右図のようなウインドウになる。標準では右側に部品リストが、左側に回路図作成エリアが表示される。正直な感想としては、利用できる部品種類はそんなに多くない。
    まずすべての作業の前に作成する回路の名前をつける。初期状態では ⑪の部分にはアトランダムに適当な名前が入っているのでここをクリックしてアクティブにしてから、これから作成する回路に判別しやすい名前をつける。
    基本的な作図の手順は右側の部品エリアから作図エリアに部品をマウスでつまんで Dragするだけ。実際のはんだ付けでキバンを組んだことがあれば、部品の相互位置関係も想像できると思われるが、あとからいくらでも変更、追加、削除などの編集が可能なので、初心者でもとりあえず「エイヤッ」と配置すればいい。
    実際のmilピッチの穴あきユニバーサルボード使用のハンダ付けでは後からの修正は結構面倒で、とりわけ足の多い大物部品はハンダを全部吸い取らないと移動できないので位置を間違えると周辺部品への配線が混乱してしまうことになり結構神経を使うが、この辺りは CADならではの利点だ。
    部品の角度の変更は⑥のボタンを押すと 30°ずつ回転する。⑦のボタンは操作のアンドゥ、リドゥ、⑧は回路図から部品リストへの切り替え、③の「エクスポート」ボタンは回路基板データをダウンロードする(ブラウザで設定したダウンロードフォルダに「XXXXXXX(1).brd」などの名前のファイルがダウンロードされる。XXXXXXX の部分は⑪のタイトル名)
    ただし、2019/02月時点で 「Import」機能はサポートされていない模様でエクスポートしたファイルの用途は??

    幾つかの部品を Dragしてからその部品の端子部分にマウスカーソルを合わせると端子部分が赤くその周りが黒い端子マークが表示され、端子名がある場合はそれが下に吹き出しで表示されるのでそこをクリックするとワイヤーの始点/終点となる。ワイヤーは部品同士を空中配線しても、部品リストにある「ブレッドボード」上に配置して中継してもどちらでも構わない。ブレッドボードでは全ての穴が始点/終点の対象となり、一つの穴に複数のワイヤーの始点/終点を指定することも出来る。
    LEDの Anode/Casodeなどの極性、トランジスタの Emitter/Base/Correctorなどの端子の区別はこの端子名を頼りに結線する。途中でワイヤーを曲げたいときは端子以外の場所でクリックすれば変曲点になる。結線する相手の端子の上で同じように端子マークになったところでクリックすればワイヤーの終点になる。部品端子以外のところではワイヤーは自由な角度で引けるが、垂直/水平になったときは薄青色のカーソルが表示されるのでこれを頼りにクリックしていくと見た目キレイな配線が描ける。
    一旦始点/終点を入力して配置を確定したワイヤーはもう一度クリックして選択すれば変曲点が●マークで表示されるので、そこを Dragすれば別の端子へ変更したり曲げ位置の修正ができる。
    またワイヤーや LEDの色、抵抗値などの属性はその部品をクリックすると部品名などがポップアップして表示されるので選択、変更が出来る。(抵抗の場合、抵抗値を変えるとカラーコードまで変化するのは面白い)
    2019/2月 時点では ⑬の「Annotation」アイコンで付箋のような注釈が配置できるが、テキストを入力しても表示されない模様。文字数を増減すると枠が変化しているから文字色が透明なんだろうか?
    また、③ の「export」ボタンで設計データをローカルにダウンロードすることは出来るが、逆方向の「import」手段はなさそうだ。


  7. Arduino のプログラム
  8. さて、当初目的の Arduinoのプログラムのトレースをするためにコードを記述してみる。
    ① の「コード」ボタンを押すと部品エリアの前にコードウインドウがせり出してくる。(回路に Arduino を配置してないとコーディングの対象がないという警告が表示される)
    コーディングは初期状態では「ブロック」編集モードなので⑨の「ブロック」の横の▼ボタンを押して「テキスト」を選択することで c言語用のエディター画面になる。初期状態ですでに簡単な Arduinoの「Lチカ」プログラムが記述されているので、必要に応じてコーディングを追加・編集していく。
    コーディングが終わったら、③の「シュミレーションを開始」ボタンを押せば Arduino上のあるいは Arduinoに結線された LEDなどが点滅する。


  9. デバッグ
  10. さて、懸案のブレークポイントで停止させるには「虫」のようなアイコン()を押す。それをクリックするとコードの右側に簡単な説明が表示される。
    ブレークポイントはコードの行数字部分()をクリックすれば地色が変わって設定出来る。
    ブレークポイントを設定したら「シュミレーションを開始」ボタン()をクリックすればコンパイル・実行した後プログラムがそこで一旦停止する。
    その状態で知りたい変数の上にマウスオーバーさせるとその変数の値が表示され()処理の途中経過が確認できる。
    この状態からプログラムの再開ボタン()をクリックすると次のブレークポイントまで実行される。
    のボタンは「次の関数をステップオーバー」だが、停止している行が関数でなければ1ステップ実行される。当然関数(サブルーチン)なら中の処理は飛ばすことが出来る。
    ブレークポイントは幾つでも設定できるのでこれだけでかなり助かる。あとはブレークの条件設定や指定変数のトレースが出来れば言うことなしなんだが。
    これが無料で使えるとは! こんなに簡単に使えるとは! と言う驚き。現在は LEDや LCD、ステッピングモータ、サーボモーターなどが画像上で表示されるだけだが、おそらく今後ギヤやレバーのような 3Dメカパーツをモーターで駆動して、スイッチなどで位置検出して制御する、その結果ロボットや車などが 3D画面上で障害物を避けて動き回る・・・・などと言うメカ・電気にまたがったクロスオーバーなシュミレーションが出来るようになるんだろう。楽しみだ。


  11. ライブラリーの利用
  12. コードエリアに初期に表示される「Lチカ」プログラムを見ると、Atduino IDEなどで必須の 「#include "arduino.h"」 などのプリプロセッサーがない。どうやら このシュミレータのコンパイラはかなり簡易版らしく基本のライブラリが含まれていて、それ以外のライブラリの追加は「#include」文ではなく図の「Libraries」ボタンを押して表示される一覧の中から左側の「含める」ボタンを押すと例えば 「#include <LiquidCrystal.h>」のようなインクルード文がコードに挿入される。
    現時点でここにリストされているのは EEPROM/IRremote/LiquidCrystal/Keypad/NeoPixel/Servo/SoftwareSerial/Wire/SD/SPI/Stepper の 11個だけ。結構使うであろうタイマーライブラリなどは見当たらない。
    試しに、"#include <MsTimer2.h>" とやってみたら当然の如くファイルが見つからないと言うエラーが表示されてしまった。
    さて、タイマー割り込みを使いたいのにライブラリーリストにもなく 「#include <MsTimer2.h>」の記述も出来ないのではシュミレーターの意味がない。と思って Web検索すると英文ページに 「ヘッダーファイルとそのソースファイルの中身をコピーしろ」というような記述が見つかった。確かに、ライブラリは構造化、再利用化などのために生じた仕組みだから、メインのコードにそれらを全部含めてしまえば済むというのはうなずける。
    そこで Arduino IDEのライブラリフォルダの中の 「MsTimer2.h」「MsTimer2.cpp」という2つのファイルの中身を一部のコンパイル制御文を除いてコピーしてみた。
    幸いにして「MsTimer2.h」の中には更に別のファイルを多重でインクルードするようなインクルード文はないのでコピペも簡単だ。
    あまりセンスがいいプログラムとは言えないが、以下のようなコードで一応 LEDがタイマー割り込みでボワーッと点滅しつつ、常時スイッチを監視して押されたらモーターを回転する動作が画面シュミレーションで確認できた。

    const int LED_L = 13;		// on board LED
    const int LED_g = 6;		// green LED 出力
    const int Sw_1 = 17;
    const int Mot_1 = 4;
    int inc0 = 1;
    int ii;
    const int inc1 = 256;
    volatile unsigned long tma;	// interupt counter
    volatile boolean tgl;
    
    // ****** from here MsTimer2.h ******
    namespace MsTimer2 {
    	extern unsigned long msecs;
    	extern void (*func)();
    	extern volatile unsigned long count;
    	extern volatile char overflowing;
    	extern volatile unsigned int tcnt2;
    	
    	void set(unsigned long ms, void (*f)());
    	void start();
    	void stop();
    	void _overflow();
    }
    
    // ****** from here MsTimer2.cpp ******
    unsigned long MsTimer2::msecs;
    void (*MsTimer2::func)();
    volatile unsigned long MsTimer2::count;
    volatile char MsTimer2::overflowing;
    volatile unsigned int MsTimer2::tcnt2;
    #if defined(__arm__) && defined(TEENSYDUINO)
    static IntervalTimer itimer;
    #endif
    
    void MsTimer2::set(unsigned long ms, void (*f)()) {
    	float prescaler = 0.0;
    	
    	if (ms == 0)
    		msecs = 1;
    	else
    		msecs = ms;
    		
    	func = f;
    
    #if defined (__AVR_ATmega168__) || defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) || defined (__AVR_ATmega328P__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB1286__)
    	TIMSK2 &= ~(1<<TOIE2);
    	TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
    	TCCR2B &= ~(1<<WGM22);
    	ASSR &= ~(1<<AS2);
    	TIMSK2 &= ~(1<<OCIE2A);
    	
    	if ((F_CPU >= 1000000UL) && (F_CPU <= 16000000UL)) {	// prescaler set to 64
    		TCCR2B |= (1<<CS22);
    		TCCR2B &= ~((1<<CS21) | (1<<CS20));
    		prescaler = 64.0;
    	} else if (F_CPU < 1000000UL) {	// prescaler set to 8
    		TCCR2B |= (1<<CS21);
    		TCCR2B &= ~((1<<CS22) | (1<<CS20));
    		prescaler = 8.0;
    	} else { // F_CPU > 16Mhz, prescaler set to 128
    		TCCR2B |= ((1<<CS22) | (1<<CS20));
    		TCCR2B &= ~(1<<CS21);
    		prescaler = 128.0;
    	}
    #elif defined (__AVR_ATmega8__)
    	TIMSK &= ~(1<<TOIE2);
    	TCCR2 &= ~((1<<WGM21) | (1<<WGM20));
    	TIMSK &= ~(1<<OCIE2);
    	ASSR &= ~(1<<AS2);
    	
    	if ((F_CPU >= 1000000UL) && (F_CPU <= 16000000UL)) {	// prescaler set to 64
    		TCCR2 |= (1<<CS22);
    		TCCR2 &= ~((1<<CS21) | (1<<CS20));
    		prescaler = 64.0;
    	} else if (F_CPU < 1000000UL) {	// prescaler set to 8
    		TCCR2 |= (1<<CS21);
    		TCCR2 &= ~((1<<CS22) | (1<<CS20));
    		prescaler = 8.0;
    	} else { // F_CPU > 16Mhz, prescaler set to 128
    		TCCR2 |= ((1<<CS22) && (1<<CS20));
    		TCCR2 &= ~(1<<CS21);
    		prescaler = 128.0;
    	}
    #elif defined (__AVR_ATmega128__)
    	TIMSK &= ~(1<<TOIE2);
    	TCCR2 &= ~((1<<WGM21) | (1<<WGM20));
    	TIMSK &= ~(1<<OCIE2);
    	
    	if ((F_CPU >= 1000000UL) && (F_CPU <= 16000000UL)) {	// prescaler set to 64
    		TCCR2 |= ((1<<CS21) | (1<<CS20));
    		TCCR2 &= ~(1<<CS22);
    		prescaler = 64.0;
    	} else if (F_CPU < 1000000UL) {	// prescaler set to 8
    		TCCR2 |= (1<<CS21);
    		TCCR2 &= ~((1<<CS22) | (1<<CS20));
    		prescaler = 8.0;
    	} else { // F_CPU > 16Mhz, prescaler set to 256
    		TCCR2 |= (1<<CS22);
    		TCCR2 &= ~((1<<CS21) | (1<<CS20));
    		prescaler = 256.0;
    	}
    #elif defined (__AVR_ATmega32U4__)
    	TCCR4B = 0;
    	TCCR4A = 0;
    	TCCR4C = 0;
    	TCCR4D = 0;
    	TCCR4E = 0;
    	if (F_CPU >= 16000000L) {
    		TCCR4B = (1<<CS43) | (1<<PSR4);
    		prescaler = 128.0;
    	} else if (F_CPU >= 8000000L) {
    		TCCR4B = (1<<CS42) | (1<<CS41) | (1<<CS40) | (1<<PSR4);
    		prescaler = 64.0;
    	} else if (F_CPU >= 4000000L) {
    		TCCR4B = (1<<CS42) | (1<<CS41) | (1<<PSR4);
    		prescaler = 32.0;
    	} else if (F_CPU >= 2000000L) {
    		TCCR4B = (1<<CS42) | (1<<CS40) | (1<<PSR4);
    		prescaler = 16.0;
    	} else if (F_CPU >= 1000000L) {
    		TCCR4B = (1<<CS42) | (1<<PSR4);
    		prescaler = 8.0;
    	} else if (F_CPU >= 500000L) {
    		TCCR4B = (1<<CS41) | (1<<CS40) | (1<<PSR4);
    		prescaler = 4.0;
    	} else {
    		TCCR4B = (1<<CS41) | (1<<PSR4);
    		prescaler = 2.0;
    	}
    	tcnt2 = (int)((float)F_CPU * 0.001 / prescaler) - 1;
    	OCR4C = tcnt2;
    	return;
    #elif defined(__arm__) && defined(TEENSYDUINO)
    	// nothing needed here
    #else
    #error Unsupported CPU type
    #endif
    
    	tcnt2 = 256 - (int)((float)F_CPU * 0.001 / prescaler);
    }
    
    void MsTimer2::start() {
    	count = 0;
    	overflowing = 0;
    #if defined (__AVR_ATmega168__) || defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) || defined (__AVR_ATmega328P__) || defined (__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB1286__)
    	TCNT2 = tcnt2;
    	TIMSK2 |= (1<<TOIE2);
    #elif defined (__AVR_ATmega128__)
    	TCNT2 = tcnt2;
    	TIMSK |= (1<<TOIE2);
    #elif defined (__AVR_ATmega8__)
    	TCNT2 = tcnt2;
    	TIMSK |= (1<<TOIE2);
    #elif defined (__AVR_ATmega32U4__)
    	TIFR4 = (1<<TOV4);
    	TCNT4 = 0;
    	TIMSK4 = (1<<TOIE4);
    #elif defined(__arm__) && defined(TEENSYDUINO)
    	itimer.begin(MsTimer2::_overflow, 1000);
    #endif
    }
    
    void MsTimer2::stop() {
    #if defined (__AVR_ATmega168__) || defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) || defined (__AVR_ATmega328P__) || defined (__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB1286__)
    	TIMSK2 &= ~(1<<TOIE2);
    #elif defined (__AVR_ATmega128__)
    	TIMSK &= ~(1<<TOIE2);
    #elif defined (__AVR_ATmega8__)
    	TIMSK &= ~(1<<TOIE2);
    #elif defined (__AVR_ATmega32U4__)
    	TIMSK4 = 0;
    #elif defined(__arm__) && defined(TEENSYDUINO)
    	itimer.end();
    #endif
    }
    
    void MsTimer2::_overflow() {
    	count += 1;
    	
    	if (count >= msecs && !overflowing) {
    		overflowing = 1;
    		count = count - msecs; // subtract ms to catch missed overflows
    					// set to 0 if you don't want this.
    		(*func)();
    		overflowing = 0;
    	}
    }
    
    #if defined (__AVR__)
    #if defined (__AVR_ATmega32U4__)
    ISR(TIMER4_OVF_vect) {
    #else
    ISR(TIMER2_OVF_vect) {
    #endif
    #if defined (__AVR_ATmega168__) || defined (__AVR_ATmega48__) || defined (__AVR_ATmega88__) || defined (__AVR_ATmega328P__) || defined (__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB646__) || defined(__AVR_AT90USB1286__)
    	TCNT2 = MsTimer2::tcnt2;
    #elif defined (__AVR_ATmega128__)
    	TCNT2 = MsTimer2::tcnt2;
    #elif defined (__AVR_ATmega8__)
    	TCNT2 = MsTimer2::tcnt2;
    #elif defined (__AVR_ATmega32U4__)
    	// not necessary on 32u4's high speed timer4
    #endif
    	MsTimer2::_overflow();
    }
    #endif // AVR
    // ****** end of MsTimer2.cpp ******
    
    void timer20ms() {		// timer interupt 20ms period
      tma++;
      if(tma < 50){			// 1Sec だけスイッチ OFFでも LED細かく点滅 > tmaがカウントアップしている確認
        digitalWrite(LED_L, tgl);
        tgl = !tgl;
      } else if(tma == 50){
        digitalWrite(LED_L, LOW);
        tgl = true;
      }
    }
    
    void setup() {
    
      pinMode(LED_g, OUTPUT);		//timer fire every 20ms
      pinMode(A5, INPUT_PULLUP);
      pinMode(Sw_1, INPUT_PULLUP);
      pinMode(Mot_1, OUTPUT);
      MsTimer2::set(20, timer20ms);
      MsTimer2::start();
    }
    
    void loop() {
        int PWM_v;
        unsigned long tmb;
    
        while (tma <= 50) {
    		// wait 1sec from start
        }
      
        tmb = tma;
    /*  1 を 1bitずつ 左右シフトして -1 することで
        0/1/3/7/15/31/63/127/255 を順次得て PWM駆動する
    */
      if (tgl) {
        PWM_v = inc0 - 1;
        analogWrite(LED_g, PWM_v);
        inc0 = inc0 << 1;
        if (inc0 >= 256) {
          tgl = !tgl;
        }
      } else {
        PWM_v = inc0 -1;
        analogWrite(LED_g, PWM_v);
        inc0 = inc0 >> 1;
        if (inc0 <= 1) {
          tgl = !tgl;
        }
      }
        while (tma <= tmb) {
          // 20ms wait
          if (digitalRead(Sw_1) == LOW){
            digitalWrite(Mot_1, HIGH);
          }else{
            digitalWrite(Mot_1, LOW);
          }
        }
        delay(50);
    }
    
    なお、シュミレーションとは関係ないが割り込みルーチンの中で操作するグローバル変数には 「volatile」修飾子を忘れないこと。


  13. 回路
  14. 回路はこんな感じ。

  15. プロジェクトの整理・複製
  16. 何回か繰り返してから再度ログインしてみたら色々試した途中経過などのデータがたくさん溜まってしまったことに気づいた。これを整理するには右上の⑰「Select」にチェックを入れてから削除対象の回路にチェック⑱を付けて、⑲の「Delete」ボタンを押すと削除できる。
    更に、設計したプロジェクトデータ(回路図、コード)はクラウド上に保存されるので同じ IDでログインすれば、どこからでも編集可能になる。会社で作業して、そのまま自宅に戻って自宅の PCで続きを作業することも可能だ。
    もう一つ、動作確認できたプロジェクトをベースに新しいプロジェクトを編集する場合、そのままだと元のプロジェクトデータは即時上書きされて残らない。あるいは、間違って大幅なコードの削除・修正などしてしまうといわゆる「unDo」機能がないので簡単にはもとに戻せない。定期的なバックアップをしたいと思ってもプロジェクト全体を外部にExportする機能もない。
    そこで活用したいのが「複製」機能。右図のようにLoginページで Circuitsを選ぶと保存されているプロジェクトが一覧出来る。ここで該当のプロジェクトの上にマウスカーソルを合わせると右上に表示される歯車マーク「Options」ボタンをクリックして表示されるプルダウンメニューから「複製」を選んでこのプロジェクトの編集を選びテーマ名を変更することにより複製される。(テーマ名の変更方法はここの⑪をクリックして修正する)
    現状は単に複製しただけだと空のプロジェクトしか保存されないが、一旦開いて何某か変更を加えると新しい名前でプロジェクトが複製される。大きなデータのプロジェクトを編集する場合は定期的にこの操作を行う習慣をつけたほうが良さそうだ。


オススメのシェーバー  by Amazon



熟睡したいなら  by 楽 天

Access Counter:  総アクセス数