今回は、ATmega328に搭載されたAD変換器を使って、デジタルオシロスコープを作ってみたいと思います。
Arduinoのライブラリで、analogRead()を実行すると、ATmega328のAD変換器を使って変換したデジタル値が取得できます。
uint16 anlg;
uint32 usec;
anlg=analogRead (0); // 1回目:初期化処理の分、変換時間が伸びる
usec=micros ();
anlg=analogRead (0); // 2回目以降の実力値を計測
usec=micros () - usec;
Serial.println (usec);
上の様なプログラムを実行すると、usec=112μs辺りの数値が返ってきます。ATmega328のAD変換器を起動すると、AD変換器の動作クロックで約13サイクル後に結果が返ってくるので、一番遅いクロック(clk/128=16MHz/128=125kHz)で動かした場合、1/125kHz×13サイクル=104μs後に結果を取得できます。analogRead()にかかった時間は、104μs+オーバヘッド処理時間と考えて矛盾しません。
※ATmegaのAD変換器は、最初の変換時に回路の初期化処理が入って、初めの一回だけ時間がかかるので、2回目のanalogRead()で時間を計測しています。
AD変換器の変換速度だけで考えると、1/104μs≒9.6kHzのサンプリングレートでAD変換を実行することが可能です。ただし、analogRead()を使ってしまうと、ソフトウェア処理となり、割り込みなどによって処理時間がばらつくので、決まった一定時間毎にサンプリングし続けることはできません。また、取り込んだデータをバッファ等に記憶するための処理なども必要になるので、更に、サンプリングレートが低下するはずです。
AD変換が終了するまでの104μsに、他の処理を実行できず、処理がブロックされてしまうのも、マイナス評価です。
デジタルオシロスコープを作ろうとする場合、サンプリングレートが一定にできないと、測定した波形が歪んでしまうため、ソフトウェア処理でサンプリングするのには無理があります。
Arduinoのライブラリに頼るのは、この辺が限界ですね。今回は、ATmega328のAD変換器を直接制御して、ハードウェアで定期的にAD変換を実施し、次々とアナログ電圧(のデジタル変換値)を拾ってみます。
ATmega328のデータシートを読んでみると、搭載されているAD変換器には、フリーランニングモードという動作モードがあることがわかります。このモードでAD変換器を起動すると、AD変換を開始 ⇒ 終了 ⇒ データと終了フラグセット ⇒ 次のAD変換を開始 ⇒ 終了 ⇒ データと終了フラグセット ⇒ 次のAD変換を開始 ⇒ ……と、自動的にAD変換を実行し続けてくれます。
このモードでは、AD変換器の動作クロックで13サイクル毎に、新しい変換値と変換終了を示すフラグがセットされることになります。ソフトウェアで制御しなくても、決まった時間毎に変換値が更新されるので、今回の用途にはピッタリです。
変換が終了したことは、終了フラグ(ADIFビット)を読み出すとわかります。1つのデータを処理した後で、ADIFをクリアしておき、ADIFをポーリングすると、次のデータが更新されるタイミングがわかります。ADIFが立つまでの間は、他の処理を続行できるので、バッファへの書き込みなど、最低限必要な処理をAD変換処理と並行して実行することができ、効率的です。
ATmega328のAD変換器をフリーランニングモードに設定するためには、以下の様に設定します。
レジスタ名 | アドレス | 設定値 | 説明 |
ADCSRA | 0x007a | 0xf7 |
AD変換器有効、変換開始、自動トリガ有効、フラグクリア クロック選択:clk/128 |
ADCSRB | 0x007b | 0x00 | フリーランニングモード |
ADMUX | 0x007c | 0x60 |
リファレンス電圧:AVcc (=5V) データレジスタ:左詰め 入力端子:ADC0 |
赤文字にしたところは、サンプリングレートや変換する端子に従って変更します。
AD変換したデータは10ビットですので、16ビットサイズのデータレジスタADCH,ADCLからデータを読み出す必要があります。以下図のように、10ビットのデータを右詰め/左詰めから選択することが可能となっています。
ADCHレジスタ (アドレス=0x0079) | ADCLレジスタ (アドレス=0x0078) | |||||||||||||||
右詰め | - | - | - | - | - | - |
AD C9 |
AD C8 |
AD C7 |
AD C6 |
AD C5 |
AD C4 |
AD C3 |
AD C2 |
AD C1 |
AD C0 |
左詰め |
AD C9 |
AD C8 |
AD C7 |
AD C6 |
AD C5 |
AD C4 |
AD C3 |
AD C2 |
AD C1 |
AD C0 |
- | - | - | - | - | - |
このレジスタは、16ビットサイズですので、「#4-2 Logiscopeの拡張」で解説した「アクセス規約」に従っておく必要があります。
ただし、今回は、10ビットデータの内、上位8ビットしか使いません。よって、「左詰めモード」を選択してあります。この場合、ADCHだけを読み出せば良いので、普通に8ビットアクセスすればOKです(ADCLレジスタは読まない)。8ビットしか使用しない理由は、データを取り込むためのRAM容量を節約するためと、高速クロック(>200kHz)で変換した場合には絶対精度が±4.5LSBに低下する(下位ビットは意味をなさない)ためです。
変換クロックの設定は、以下の値を設定可能です。
設定値 ADCSRAの下位3ビット |
クロック | 周波数 |
変換時間 (13サイクル) |
サンプリング レート |
備考 |
0 | clk/2 | 8MHz | 1.625μs | 615kHz |
16MHzのUNOでは、 仕様範囲外 (∵Up to 76.9k[sps]; Samples Per Second) |
1 | clk/2 | 8MHz | 1.625μs | 615kHz | |
2 | clk/4 | 4MHz | 3.25μs | 308kHz | |
3 | clk/8 | 2MHz | 6.5μs | 154kHz | |
4 | clk/16 | 1MHz | 13μs | 76.9kHz | |
5 | clk/32 | 500kHz | 26μs | 38.5kHz | |
6 | clk/64 | 250kHz | 52μs | 19.2kHz | |
7 | clk/128 | 125kHz | 104μs | 9.6kHz | analogRead()の設定値 |
※「周波数」から右の欄は、MCU(ATmega328)の動作クロック(clk)が16MHzの場合です。
データシートによると、clk/16を使った場合が最速設定みたいです。グレイに塗った設定値は、16MHzのArduino UNO等では、仕様範囲外となります。Pro Miniなどで、3.3V@8MHz機がありますので、そのような場合に使用可能な設定値だと思われます。
また、変換クロックが200kHz以下であれば絶対精度±2LSBなのですが、200kHzを超えた周波数で使用する場合には±4.5LSBに低下します。精度や分解能を犠牲にしても、サンプリングレートを上げたい(高速化したい)ニーズはあるので、「200kHzを超える周波数では使えません」みたいな仕様になっていないのは好感が持てますね。
では、実際にAD変換したデータを取り込むためのプログラムをコーディングして行きます。取り込み手順は以下の様になります。
全体のコードは、完成版プログラム(logiscope.ino)でご確認ください。
以下のコマンドを追加してあります。
実行すると、測定したアナログ値(8ビット、10進数)をシリアルモニタに送信してくるので、Olive+のスクリプトで受け取って波形を表示すれば完成です。
ここで、完成版のプログラムをリンクしておきます。「#2-3 ロジアナもどきを作る」で作成したオリジナルのプログラムを置き換えているので、どちらからアクセスしても、最新版がダウンロードできます。
完成版プログラム:logiscope.ino
完成版スクリプト:logiscope.olv
早速使ってみたいのですが……、何か良い例題があるかと言うと……、特にないですね……(1^^;)。あんまりアナログ信号は取り扱わないから……。
まずはベタに、CR回路の充放電電圧波形でも測定してみますか。
ブレッドボードに以下の様な回路を構築します。あまり、手持ちコンデンサの種類が無いので、スタータキットに入っていた、10μFの電解コンデンサを使いました(他の種類でも可)。
ご存知の通り、時定数C×Rで充放電電圧波形の立ち上がり(下がり)時間が決まってきます。時定数CR[s]で、電圧波形が最終到達値のおよそ63.2%の位置まで到達します。サンプリング可能な時間とのバランスを見て、抵抗R=1kΩとしました。この場合の時定数CR=10μF×1kΩ=10msですので、充電させる時には約10msでVccの63.2%位置まで上昇するはずです。
上に示した回路図で、SW1を押すと充電、SW2を押すと放電となりますので、Logiscopeを起動して以下の様に操作します。
実験サンプル:【 】内はLogiscopeからの送信データ
…… SW2を1s程度押し続けて、十分放電しておく。SW2を離す。
【osc,0,4.9,6,0】
…… ここで、SW1を1s程度押す。
赤い四角形は、時定数10msと、Vcc×63.2%位置を示しています。期待通りの測定波形でした。
Arduino UNOを使い、取り込みバッファサイズwf_bufmx=16とした場合、取り込み可能な時間は、<変換時間>×(wf_bufmx×wf_bufmx×2)となります。<変換時間>は、変換クロックの設定表で確認してください。<clk>=6の場合で、取り込み時間≒26.6msとなることがわかります。時定数10msに対して、ちょうど良さそうなので、<clk>=6で取り込みました。
続いて、放電波形です。電圧の変化方向が逆になるため、トリガを負の値にしておかないと取り込みできません。その点に注意して実験します。
実験サンプル:【 】内はLogiscopeからの送信データ
…… SW1を1s程度押し続けて、十分充電しておく。SW1を離す。
【osc,0,-4.9,6,0】
…… ここで、SW2を1s程度押す。
今度の赤い四角形も、時定数10msと、Vcc×(100−63.2)%位置を示しています。こちらも期待通り。
「他に何かないかなぁ〜」と、考えていたところ、「#3-7 ブザーとサウンドセンサ」で使ってみたサウンドセンサですが、アナログ出力を、まだ使ってみていなかったことを思い出しました。ちょうど良いので、サウンドセンサのアナログ出力をモニタしてみます。
Arduinoとサウンドセンサを以下の様に接続し、Logiscopeを起動します。
Arduino | 接続 | サウンドセンサ |
5V | → | + |
GND | ⇔ | G |
× | DO | |
A0 | ← | AO |
まずは、無音状態のアナログ出力をモニタしましょう。
おそらく、音を拾ったマイクの出力なので、人間の可聴域20〜20k[Hz]の信号波形が出力されるのでしょう。標本化定理に従って、最高周波数(20kHz)の2倍、40kHz程度でサンプリングします。AD変換クロックの設定についてまとめた表から、ちょっと足りませんが、<clk>=5 (38.5kHz)で良いでしょう。
無音状態なので、ほとんど信号変化は無いはずです。よって、トリガはかけずに<trig>=0を指定します。
取り込むサンプル数ですが、logiscope.inoの中でwf_bufmx=16に設定してあるので、最大のサンプリングデータ数はwf_bufmx×wf_bufmx×2=512サンプルです(RAMが許す限りバッファサイズwf_bufmxを大きくすることが可能ですので、必要に応じて変更して使ってください)。最大まで取り込んだとしても、512/38.5kHz=13.5msという、非常に短い時間の波形しか取り込みできませんので、<smpl>=0を指定して、最大の512サンプルまで取り込みます。
実験サンプル:【 】内はLogiscopeからの送信データ
【osc,0,0,5,0】 …… 無音(静かな)状態で実行してください。
「#3-7 ブザーとサウンドセンサ」で、音が鳴った時にパルスを出力するデジタル出力(DO)の波形を観測した時、音に対する感度を調整したことを思い出してください。ちょうど、LEDが点灯/消灯の境界辺りにマイクロポテンシオメータを合わせておくと、上図の様に、無音状態のアナログ出力がVcc/2=2.5V付近に観測できることが分かりました。
次は、トリガをかけて、拍手の波形を取得してみます。
トリガの設定方法は、少し難しく書いてありますが、基本的には、1〜127の整数値を渡せばOKなはずです。無音状態の波形では、ほとんど2.5Vフラットな波形に見えていましたので、閾値は、4とか8くらいの数値で十分だと思われます(マイクの感度や出力レベル、測定環境の雑音レベルにもよりますが……)。<trig>=8を指定して取り込んでみます。
実験サンプル:【 】内はLogiscopeからの送信データ
【osc,0,8,5,0】 …… 実行後、トリガ待ちになるので、マイクのそばで拍手を叩く。
もし、拍手の波形がトリガで指定した閾値を超えないようだと、うまく取り込みできないかもしれないので、トリガの値や拍手の音の大きさ(強く叩くとか)などで調整してみてください。逆に、周りの雑音がうるさい環境だと、「トリガ待ち」状態にならず、すぐに取り込みを実行してしまう場合があるかもしれませんので、その場合は<trig>=16〜32など、少し大きな値を設定してみてください。
おまけで、【osc】コマンドで測定したデータをWAVファイルに変換するスクリプトを作ってみました。Olive+のスクリプトでWAVファイルを生成する方法については、Olive+のHPからサンプルスクリプトの解説をご覧ください。
WAVファイル生成スクリプト:osc2wav.olv
前節の実験と同じ回路を保持したまま、ArduinoでLogiscopeも起動したままにして、上のスクリプトを実行してください。上のスクリプトがArduinoのLogiscopeと通信して、サウンドセンサのアナログ出力(AO)を取り込んだデータからWAVファイルを生成します。
いくら、一瞬の拍手とは言え、さすがに13.5msでは拍手に聞こえないかもしれないので、以下のように工夫してみました。
合わせて、188ms程度の録音ができるはず。それでも、たったの0.188s……(短っ!)。
生成したWAVファイル#1:ar44_clap.wav
一瞬なので、聞き逃さないようにしてくださいね。マイクの性能なのか、時間が短すぎてエネルギが足りないのか、すごく小さい音でしか聞こえません。
録音できるのはかなり短い時間なので、ナニですが、拍手の代わりに「わんっ!」など、短い音声を吹き込むと、それはそれで「らしく」聞こえました。
生成したWAVファイル#2:ar44_wan.wav
ちょっと、音量が小さいので、Olive+のスクリプト(osc2wav.olv)で読み込んだ後で、自動的に音量を最大化する様に設定してあります(auto_max=true)。そのまま録音したい場合には、falseに書き換えてお使いください。
最後はただのお遊びです(1^o^;)。Logiscopeに追加した、デジタルオシロスコープの機能に関しては、使い道あるでしょうか?筆者は、ここまでArduinoの工作をしてきて、あんまりアナログ信号に出会ったことが無いので、使い道が思いつきませんが……。
PWM波形をLPF(CRフィルタ等)で鈍らせてsin波を作り出すとか、何らかのアナログ出力を持ったセンサのデータ解析とかに使えるかもしれません。「いつか」役に立つ時も来るでしょう。
後日、デバッグ用(Logiscope実行用)に使うことが多くなったSTM32でも、デジタルオシロスコープを使用できる様、バージョンアップしました。詳しくは、「#5-4 デジタルオシロスコープ(STM32対応)」をご覧ください。
プログラムとスクリプトは、オリジナルを差し替えてあるので、どこからダウンロードしても最新バージョンがダウンロードできます。