今回は、Arduinoを使って、ロジアナ(Logic Analyzer)もどきを作ってみようと思います。その前に、非同期シリアル通信機能に正式対応したOlive+をダウンロードして、インストールを済ませておいてくださいね。
このページでは、筆者の環境で自由に使用可能な、Arduino Microを使って作業を進めますが、UNOでも動作可能です(筆者のArduino UNOは、1時間毎に気温・湿度のロギング中のため、フリーなのはMicroのみ)。
まずは、目標仕様を決めましょう。
モニタできるのは、1ピン(1端子)のみとします。1ピンだけなので、どちらかと言うと、オシロスコープに近いイメージです。しかし、アナログ値が取り込みできるわけではなく、ロジック値(HIGHまたはLOW)だけなので、やはりそこはロジアナ……。と、言うことで、命名、「Logiscope」。
Olive+に拡張した非同期シリアル通信機能でArduino側のプログラムと通信して、測定した波形をOlive+で表示します。
Arduino側のプログラムで、指定した端子の最初の立ち上がりエッジ(rising edge)または、立ち下がりエッジ(falling edge)を捕まえて、そこからひたすら、digitalRead()で端子状態をモニタし続け、端子状態が変化(トグル)した時間をmicros()で取得して記録します。力技です。
機体性能任せのベストエフォートなので、性能を超えた短いパルスは取りこぼします。トグルした時間も、多少前後すると思われます。±数μsの誤差は覚悟の上で、ざっと、波形を確認することを目的とします。
他にも、ハードウェアのデバッグに使えそうないくつかの機能を実装しておきます。以下、Arduino側のプログラムに実装すべきコマンドの仕様です。
それでは、この目標仕様に向かって、Arduino側とWindows側のプログラム(スクリプト)を設計していきます。
目標仕様に記した、【who】とか【ana】とか【out】とか、決まった情報を返すだけだったり、analogWrite()みたいなAPIを叩いたら終わりなコマンドは、とっとと実装してしまいましょう。
一番の肝は【trg】コマンドの実装ですので、詳しく検討を進めます。
取り込みの方法は、目標仕様にも記した通り、力技です。
まず、【trg】コマンドで指定された、取り込み開始条件(トリガ)に従った信号変化を待ちます。"r"なら最初の立ち上がりエッジ(0→1の変化点)、"f"なら最初の立ち下がりエッジ(1→0の変化点)からスタートです。取り込みを開始した時間をmicros()で計測して、覚えておきます。
usec_start=micros ();
もし、変化しない信号を取り込もうとしてしまった場合、いつまで待ってもトリガが見つからない場合があり得ますので、一定時間(usec_tout)待ってもトリガが見つからなかった場合、エラーを返すようにしておきます。
トリガが見つかったら、後はひたすら、digitalRead()で端子状態を読み続け、端子の状態変化(0→1と1→0の両方)を検知したら、micros()で時間を取得して、記録します。
所詮は、デジタル値(0/1)なので、取り込みスタートしたときのレベル(0/1)と、トグルした時間だけがわかれば、後で波形を再現可能です。
トグル時間を記録せずに、ひたすらSerialで転送することができるでしょうか?仮に9,600baudの非同期シリアル通信で、データを垂れ流そうとした場合、1バイトのデータに対して、スタートビット(1ビット)+データ(8ビット)+パリティ(0ビット)+ストップビット(1ビット)=計10ビット送信する必要があります。計測した時間micros()はunsigned long(32ビット)ですので、1回当たり4バイト×10ビット=40ビットのフレームが非同期シリアル通信されることになります。1ビットの転送時間は1/9,600[s]≒104[μs]ですので、40ビット転送するのにかかる時間は約4.16[ms]となり、さすがに、リアルタイムで非同期シリアル通信に垂れ流すのは無理だとわかります。
計測したデータは、一旦、Arduino本体のメモリ(RAM)に記憶した上で、取り込み終了後に、ゆっくり非同期シリアル通信で転送するしかありません。
限られたメモリ(RAM)容量に、なるべくたくさんのデータを記憶したいので、記録すべき時間データを圧縮しておきます。
まず、ArduinoのHP ⇒ REFERENCEから、時間の計測に使用するmicros()のドキュメントをみると、「16MHzのArduino機体では、micros()の解像度は4μs」だと書いてあります。つまり、読み出した値は常に4の倍数なので、4で除しても何ら情報を失いません。これで、32ビットだった時間データが30ビットに圧縮できました。
さらに、32ビットもの時間が本当に必要でしょうか?例えば15ビットで表現可能な値は、0〜32,767ですので、×4倍してμsに換算すると、32,767×4=131,068μs≒131msです。どのような波形を観測するか?によりますが、100ms以上記録可能なのであれば、実用上十分かもしれません。
さらに、記録すべきなのは、後で波形を再現するための、「トグルした時間」です。「トグルした時間」さえわかれば、トリガからの絶対時間であろうが、直前のトグル時間からの相対時間であろうが、波形が再現できるなら、どちらでもOKです。
上図の様に、直前のトグル時間からの相対値を記録した方が、利点が大きそうです。
期待としては、ほとんどの記録時間は15ビット以下に収まるはずですので、符号化1ビット分も含めて、32ビットだったデータが一気に16ビットに圧縮できたことになります。
モデル的には、小さい数値の出現確率が非常に高いデータだと考えられるため、更にunsigned char(8ビット)で符号化した方が、圧縮効率は上がると思いますが、この辺で止めておきます。
今回使用するArduino Microには、ATmega32U4というマイコンが使われていて、RAMの容量は2.5Kバイト搭載しています。ただし、Arduino UNOでも同じプログラムを使用可能にしたいので、ATmega328と同じ2Kバイト以内に収める前提で考えます。
前節で検討したように、1回のトグルで記録すべき時間データは16ビット=2バイトですから、2KバイトのRAM全部を使うと、最大で1,024ポイントのトグル時間を記憶可能です。とは言っても、RAMはスタックやCプログラムの変数、等でも使用するため、2Kバイトすべてを時間の記録用に使えるわけではありません。
どのくらい使えるか?は、Arduino IDEでプログラムをコンパイルしてみるとわかります。
例)コンパイル結果表示
最大32256バイトのフラッシュメモリのうち、スケッチが7188バイト(22%)を使っています。
最大2048バイトのRAMのうち、グローバル変数が588バイト(28%)を使っていて、
ローカル変数で1460バイト使うことができます。
フラッシュメモリの方は、コードサイズなので、今回は関係ありません。RAMの内、「ローカル変数で1460バイト使うことができます」と言うところ、この場合、使用可能なメモリは、1,460バイト以下だとわかります。グローバル変数をたくさん使っているプログラムだと、さらにRAMの空き容量は少なく表示されるはずです。
実際には、この1,460バイトの中から、スタックやローカル変数の類などが使われるため、時間の記録領域に1,460バイト使い切ろうとすると、メモリが足りずにエラーとなります。まあ、1,460は目安ですね。
いずれにせよ、時間の記録領域は、動的に確保することにして、かつ、動的に確保する最大値を規定できるようにしておこうと思います。
typedef unsigned int uint;
const int wf_bufmx=16;
static uint wf_buf[wf_bufmx];
// 初期化
int idx;
for (idx=0; idx<wf_bufmx; idx++)
wf_buf[idx]=NULL;
wf_ptr0=0;
wf_ptr1=0;
// メモリの獲得
if (wf_buf[wf_ptr0]==NULL)
wf_buf[wf_ptr0]=new (uint [wf_bufmx]);
// ポインタの更新
if (wf_ptr1>=wf_bufmx)
{
wf_ptr1=0;
wf_ptr0++;
if (wf_ptr0>=wf_bufmx)
{
// ERROR:バッファオーバフロウ
}
}
// データの書き込み
wf_buf[wf_ptr0][wf_ptr1]=data;
wf_ptr1++;
記憶領域は、wf_bufmx×wf_bufmxケのマトリクス(二次元配列)になっていて、スタートは記憶領域を確保していません。必要になった時に、unsigned intをwf_bufmxケづつ確保して行き、最大でwf_bufmx×wf_bufmxケのunsigned intを確保しますので、この例だと、最大で512バイトの領域を使います。
wf_bufmx=16にしたのは、圧縮する前のunsigned longを記録しようとしていた時の名残ですので、RAMの残容量と相談の上、wf_bufmxの設定値を大きくすることができます。
Serialでデータを転送し終われば、記録したデータは不要ですので、解放します。
void delete_wfbuf (void)
{
// free memory for waveform buffer
int idx;
for (idx=0; idx<wf_bufmx; idx++)
if (wf_buf[idx]!=NULL)
{
delete [ ] wf_buf[idx];
wf_buf[idx]=NULL;
}
wf_ptr0=0;
wf_ptr1=0;
}
動的に、必要な時だけ、メモリを確保して、不要になったら解放します。これで、他の用途でRAMを使う時に、お互いを圧迫しなくて済みます。
完成したら、Arduino単体でテストしてみましょう。
Arduino側制御プログラム:logiscope.ino
シリアルモニタを開いて、コマンドを送信します。期待通りの動きと、応答が返ってくることを確かめます。
筆者は、デジタルピン#3にLEDを接続しておき、デジタルピン#2からもジャンパを飛ばして、デジタルピン#3に接続しておきました。
実行サンプル:【 】内はシリアルモニタからの送信データ
【who】
OK: logiscope ver.0.06
【out,3,1】
OK: digitalWrite(3, 1) 同時にLEDが明るく光ります。
【ana,3,64】
OK: analogWrite(3, 64) 同時にLEDが暗めに光ります。
【trg,2,r,5000】
1RE,5000
236
1004
1264
2020
2284
3048
3312
4084
4332
END
他のコマンドも一通り送信してみて、挙動と応答を確認したら、単体テスト終了です。
この時点ではまだ、Windows側のスクリプトには手も付けていませんでしたが、シリアルモニタを使って独立・先行してデバッグできるのは、大きな利点だと思います。
前節の最後に触れた通り、Windows側のプログラムが完成していなくても、シリアルモニタでデバッグできてしまうのは、大きな利点なのですが、その反面、Windows側のプログラム(スクリプト)は、Arduino側のプログラムが動き出さないと、デバッグできません。
なので、筆者の作業工程は、大抵、このページで記述した通りの順番で行っています。目標仕様を作り、Arduino側のプログラムを作成、単体でデバッグして、Windows側のプログラム(スクリプト)の作成に取り掛かる、という順番です。
ユーザインターフェイスとしては、Olive+のスクリプトでユーザからのコマンド入力を受け付けます。スクリプト側でコマンドを解析して、必要に応じ、Arduinoにコマンドを送信します。Arduinoからの応答を受け取って、レスポンス表示領域に表示します。
ユーザには、Arduinoに送信するコマンドをそのまま、打ち込んでもらうことにします。本当は、もう少し直観的に解り易い文法で入力してもらって、スクリプトで解析してArduino側のプログラムに合わせてコマンドを発行すれば良いのでしょうが、手を抜きました。
目標仕様では、Arduino側に実装すべきコマンドを定義しておきましたが、Windows側のU/Iにも、使い勝手を良くするためにいくつかのコマンドを定義することにします。これらのコマンドは、スクリプトが処理するので、Arduinoに送信することはありません。
【trg】コマンド実行時にArduinoから送信されてくる波形データを、ウインドウに描画する辺りがちょっと大変ですが、何度か取り込みながら表示を調整して完成します。
完成したWindows側のスクリプトと、改めてArduino側の制御プログラムをリンクしておきます。
Arduino側制御プログラム:logiscope.ino
Windows側U/Iスクリプト:logiscope.olv
Arduino IDEを使って、logiscope.inoをArduinoへコンパイル&書き込みしておきます。
Windowsでlogiscope.olvを実行すると、Olive+が起動して、Arduinoがつながっている通信ポートを問い合わせてきますので、メニューから選択してOKボタンを押します。
コマンド入力を求めるウインドウが開きますので、あとは、実装したコマンドを入力して送信すれば、応答があるはずです。
Arduino側プログラムの「単体テスト」で使ったのと同じ回路を構成しておきます。
実行サンプル:【 】内はLogiscopeからの送信データ
【ana,3,64】 …… デジタルピン#3にデューティ64のPWM波形を出力します。
【trg,2,r,5000】 …… デジタルピン#2をジャンパ線で#3に接続しておきます。
緑の縦線は、画面上部に記載した時間の区切り線です。中央〜下に青線で引かれた矩形が、取り込んだ波形です。赤い数字は、波形がトグルしたときの時間をμs単位で記入してあります。波形の上側にある赤数字は立ち上がり時間、下側にある赤数字は立ち下がり時間です。
このスケールだと、繰り返し波形4つ分しか見えていませんが、4つ分の繰り返しで4,084μsであることが読み取れます。従って、このPWM波形の周波数fは、f=1/(4,084/4)≒979Hzとなります。
もしかしたら、皆さんの所ではちょっと違った波形が見えているかも?です。Arduino HP ⇒ REFERENCE ⇒ analogWrite()のドキュメントによると、「analogWrite()が出力するPWMの周波数は、約490Hz」と、あります。例外があって、「UNOのデジタルピン#5と#6は、約980Hz」だと記載されています。
今使っているのはLeonardo(Micro)なので、#3が980Hzだったのかもしれませんね。観測する端子をデジタルピン#5に変えてみましょう。
実行サンプル:【 】内はLogiscopeからの送信データ
【ana,5,64】 …… デジタルピン#5にデューティ64のPWM波形を出力します。
【trg,2,r,5000】 …… デジタルピン#2をジャンパ線で#5に接続し直しておきます。
今度は、周波数f≒492Hzの波形が観測できました。やはり、UNOとMicroでは、端子によってPWMの周波数が異なっているようです。
PWMに立ち返ってみます。PWM(Pulse Width Modulation)は、HIGHパルスの幅を変更して、一定時間内の平均電圧を制御しようとする技術です。analogWrite()では、0〜255の数値を取ることが可能で、0だと、HIGHパルスが全く出力されず、0V一定の出力となります。逆に、255を指定すると、今度は波形のLOW部分が全く出力されず、5V一定の出力です。その間の数値では、リニアにHIGHパルスの幅が変異していきます。例えば、真ん中の128を指定すると、HIGH幅とLOW幅が等しい矩形波になります。
実行サンプルでは、64を指定しましたので、HIGH幅:LOW幅=64:(255−64)の比になっているはずです。上図最初の波形から数値を拾ってみると、500:(2,024−500)=64:195ということで、ほぼ、期待通りの比になっています。ちなみに、期待値は、64:191なので、ちょっと誤差があります。
16MHzのArduinoが、力業でサンプリングしている上に、神様ともいうべきmicros()の解像度が4μsなのですから、この程度の誤差は生じて当然です。温かい目で見守って(使って)くださいm(1__)m。
筆者の所には、Arduino UNOとMicroの2機が稼働しているので、Micro側のLogiscopeから、UNO側の非同期シリアル通信端子(デジタルピン#0、RX、受信側)を取り込んでみます。
準備:
MicroでLogiscopeを実行する
UNOとMicroをPCに接続
UNOとMicroのGND同志をジャンパ線で接続
Microのデジタルピン#2とUNOのデジタルピン#0(RX)をジャンパ線で接続
Arduino IDEを起動して、UNO用のコンフィギュレーションに設定
シリアルモニタを開く
実行サンプル:【 】内はLogiscopeからの送信データ
【trig,2,f,5000】 …… RXはアイドル時HIGHの信号なので、トリガはf (立ち下がりエッジ)
…… ここで、UNOのシリアルモニタから、文字列【who】を送信
上のトリガ待ち時間中に送信すること
今、トリガ待ち時間を5sに設定してあるため、【trg】コマンドを送信してから5s以内にUNOへデータを送信しないと、タイムアウトエラーになりますので、注意してください。デバッグ中はタイムアウトを短くしておかないと面倒なので、5sにしてありますが、落ち着いたら、もう少し長くした方が使いやすいと思います(1^^;)。
上の波形はLogiscopeで観測したそのままの波形、下には各フレーム毎に色分けした波形を示してあります。この例では、4フレームのデータを受け取っています。
各フレームは、一定時間TのLOWで始まり(スタートビット)、時間T毎にLOWまたはHIGHのデータが8ビット分続きます。最後に時間TのHIGH(ストップビット)を受け取って、1フレームの受信が終了します。この波形を測定した時の設定は、Serial.begin(9600);でしたので、T=1/9,600≒104μsです。
最初のフレームは、二進数で11101110を受信しています(スタート、ストップビットを除いた、8ビットデータ)。同様に、00010110、11110110、01010000と続いています。どうやら、データの送・受信順番は、LSBファースト(LSBを最初に送り、最後にMSBを送信する)の様です。
受信ビット列 | 受信したデータ | 意味 |
11101110 | 0x77 | 'w'のASCIIコード |
00010110 | 0x68 | 'h'のASCIIコード |
11110110 | 0x6f | 'o'のASCIIコード |
01010000 | 0x0a | LFコード |
シリアルモニタから送った文字列【who】+LFが、RXの通信波形で確認できました。
「第6回 気温と湿度の測定」で工作した、気温・湿度データの測定装置を使って、AM2302との通信波形を取り込んで、確かめてみます。
気温・湿度測定しているUNOのデジタルピン#2と、Microのデジタルピン#2をジャンパ線で接続します。いつものように、UNOとMicroのGNDも接続しておきます。
あとは、UNOで気温・湿度データを読み取るタイミングで、Logiscopeを使って波形を取り込みます。
実行サンプル:【 】内はLogiscopeからの送信データ
【trg,2,f,6000】
上の画像をクリックすると拡大表示されるので、見難い場合は拡大してご覧ください。
一番最初の長いLOW期間が、Arduino→AM2302へのスタートシグナルです。
その次の→LOW→HIGHがAM2302から返ってきたレスポンスシグナルです。
その後は、ひたすらデータが40ビット分転送されてきます。終了すると、プルアップでHIGHに戻って、通信は終了です。
データ部分は、LOW幅は一定で、HIGHの幅が狭いところが値0、広いところが値1を示しています。この取り込み波形でも、HIGH幅の広い/狭いは判断できますので、ビット値を読み取ってみます。AM2302のデータシートにあったように、ビットの送信順番はMSBファースト(MSBから先に送って、最後がLSB)です。先ほどの非同期シリアル通信RX波形とは逆順です。
湿度データ:0b0000_0001_1100_0100=0x01c4=452=45.2%RH
気温データ:0b0000_0001_0010_0101=0x0125=293=29.3℃
パリティ:0b1110_1011=0xeb (0x01+0xc4+0x01+0x25=0xebで、一致)
いくつかの端子波形を取り込んでみましたが、意外と使えるのではないでしょうか?デバッグ中、「こんな時にロジアナがあったらな〜」などと思ったとき、1ピンだけですが、取り込んで波形を確認することができますから。
後日、「#4-2 Logiscopeの拡張」で機能を強化しています。機能強化版の解説については、そちらをご覧ください。このページからダウンロード可能なプログラムも、機能強化版となっていますのでご注意ください。
その後さらに、デジタルオシロスコープの機能を追加しています。解説については「#4-4 デジタルオシロスコープ」をご覧ください。