「#4-4 デジタルオシロスコープ」では、AVR系のArduinoで使用可能なデジタルオシロスコープ機能をLogiscopeに追加しました。MCU毎に仕様の異なるI/Oユニット(AD変換器)を使って実現する機能のため、特定のMCU限定の機能となってしまいます。最近はSTM32系のArduinoをデバッグ用(Logiscope実行用)に使うことが多いので、STM32でもデジタルオシロスコープ機能を使えるようにしておきます。
※ただし、ATmega系Arduino(5V機)のデバッグに3.3Vアナログ信号(STM32)のデジタルオシロスコープを使う機会があるかどうか?きわめて疑問ではあります。
まず、両者の比較表をご覧に入れます。
ATmega328 | STM32F103 | |
解像度 | 10ビット(0〜1,023) | 12ビット(0〜4,095) |
フルスケール | 5V | 3.3V |
1LSBに対応する電圧 | 約4.88mV | 約0.81mV |
変換時間 |
13サイクル 13μs(fcpu@16MHz) |
14サイクル 1.17μs(fcpu@72MHz) |
最速変換時間は、ATmega328では16MHzで13サイクル、13μsでした。
STM32F103では、AD変換器クロックfadcで14サイクル(=1.5サンプル+12.5変換)です。fadcは、fcpu/2,/4,/6,/8の中から選択可能で、最大14MHzが仕様です。よって、fcpu=72MHzだと、fadc=fcpu/6=12MHzの場合で、変換時間1.17μsが最速です。
変換時間としては、STM32の方が1桁以上短い(速い)です。
STM32のコアライブラリでは、どういう訳か、サンプル時間に55.5サイクルを設定してあるので、12MHzクロックで68サイクル(=55.5+12.5、約5.7μs)となっている模様。
サンプル&ホールド回路内のコンデンサを、アナログ端子に接続した外部回路のインピーダンスで充電するような形になるので、確かに、サンプル時間を長くとった方がAD変換精度は向上するのですが……。変換時間(12.5サイクル)をはるかに超えるサンプル時間って、取りすきじゃない?
サンプル時間の設定については、後ほど別途検討することとします。
STM32のAD変換器は12ビットですので、変換データは12ビットの長さを持っています。しかし、ATmega328と同じバッファ(uint8配列)に取り込むので、変換結果の上位8ビットだけを使うことにします。
次にサンプリングレートですが、ATmega328では、AD変換器のプリスケーラを変更することで、レートを設定しました。しかし、STM32F103のプリスケーラは、fcpu/6,/8と、自由度が無い上に、ATmega328のレートと全く異なるため、Olive+スクリプトの方も変更する必要が出てきます。
データシートを読んでいると、タイマのイベントによって、定期的にAD変換をスタートする機能があることが分かりました。タイマを使って、所望の間隔でAD変換をスタートすることができれば、ATmega328のレートと合わせることもできるでしょう。
STM32F103には、7本のタイマユニット(TIM1,TIM2〜4,WDT×2,システムタイマ)が搭載されています。この内で、特定の用途に使われるWDTとシステムタイマは除外、1本だけ異なる仕様で、他のタイマより複雑な使い方が可能なTIM1も除外して、データシートでジェネラルタイマと呼ばれているTIM2〜4の内、TIM2を使うことにしました。
TIM2〜4は同じ仕様なので、空いているチャンネルを選んで、後で他のチャンネルに変更することも容易です。
今度はTIM2からイベントを飛ばして、AD変換器にトリガをかけるための設定を探ります。と、データシートを読み進めると、ものすごい量のレジスタ……。どんだけ複雑にしたいのやら……。「データシートを読むだけでウンザリだ」と思っていたところ、福音が。あるではないですか!STM32コアライブラリの中に、timer.hとtimer.cが。
開いてみると、ひと通りの関数がそろっているようです。なんと、割り込みですら、コアライブラリの関数を叩くだけで実現できてしまいました。
// timer2 setting
timer_pause (TIMER2);
timer_attach_interrupt (TIMER2, TIMER_CC1_INTERRUPT, isr_tim2_cc1);
timer_set_compare (TIMER2, TIMER_CH1, 0x0000);
timer_set_reload (TIMER2, 0x0e9f);
timer_resume (TIMER2);
素晴らしい。「ブラボウ!」と、叫んでいました(心の中で……)。
上のコードで、TIM2のコンペアマッチ割り込み(TIMER_CC1_INTERRUPT)に、ISR(isr_tim2_cc1()関数)を設定し、周期を0x0e9f(約104μs)に設定しています。カウンタが0x0000になると、コンペアマッチ割り込みが発生します。
タイマのモードは、コアライブラリによってPWM波形を出力可能なTIMER_PWMモードに設定されています。単にコンペアマッチ割り込みを発生するだけなら、このモードで構わないので、モードは変更しないことにしました。ただし、周期やコンペアマッチレジスタを変更しているので、恐らく、TIM2をベースにするPWM波形の周期やデューティなどが変わってしまうはずです(同時使用不可)。
今回はLogiscopeと言うことで、「PWM出力と同時使用不可」という仕様制限で問題ないはず。ただし、一応、変更したレジスタ値は、取り込み終了後に元に戻すようにしておきます。
しかし、簡単だったのもここまで。ここからAD変換器にトリガを掛けようとすると、さすがに直接レジスタを叩かないとダメみたいです……。結局データシートとにらめっこか?
いや、待って。タイマ割り込みがかけられたのなら、その割り込み処理ルーチン(ISR)内で、AD変換をソフトウェアスタートすれば良いだけなのでは?うん、そうしよう。タイマの割り込み間隔を調整するだけでAD変換のサンプリング時間も調整できるし、何より、山ほどあるレジスタの意味を理解する苦痛を考えたら、精神衛生上も好ましい(1^−^)d。
void isr_tim2_cc1 (void)
{
// isr on timer2 cc1 interrupt
if ((*(*ADC1).regs).SR&(ADC_SR_STRT | ADC_SR_EOC))
{
// ERROR: adc trigger interval too short
adc_err=true;
}
else
{
// start adc1
(*(*ADC1).regs).CR2 |= ADC_CR2_SWSTART;
}
}
AD変換器の方も、コアライブラリの中にadc_read()関数が用意されていて、チャンネル(端子)番号を指定するだけで、AD変換結果を返してくれます。AD変換器の初期化等、必要な設定は起動時に済ませてあるようなので、こちらも、ソフトウェアスタートビット(ADC_CR2_SWSTART)を設定するだけで済みました。
サンプリングレートが上がってくると、前のAD変換結果を参照する前に、タイマ割り込みがかかってしまうようになるため、フラグ(ADC_SR_STRTおよびADC_SR_EOC)をチェックして、エラーステータスを返すようにしてあります。
「#4-4 デジタルオシロスコープ」で示したATmega328のクロック設定とサンプリングレートの対応表をおさらいしておいてください。STM32F103でも、この表と同じ周期でAD変換を実施して、ATmega328で取り込んだのと同じ時間軸でデータを取り込むこととします。
switch (sclk)
{
case 0: // 1.625us
case 1: // 1.625us
timer_set_reload (TIMER2, 0x0039);
break;
case 2: // 3.25us
timer_set_reload (TIMER2, 0x0074);
break;
case 3: // 6.5us
timer_set_reload (TIMER2, 0x00e9);
break;
case 4: // 13us
timer_set_reload (TIMER2, 0x01d3);
break;
case 5: // 26us
timer_set_reload (TIMER2, 0x03a7);
break;
case 6: // 52us
timer_set_reload (TIMER2, 0x074f);
break;
case 7: // 104us
default:
timer_set_reload (TIMER2, 0x0e9f);
break;
case 8: // 208us
timer_set_reload (TIMER2, 0x1d3f);
break;
case 9: // 416us
timer_set_reload (TIMER2, 0x3a7f);
break;
}
タイマ周期の設定は、timer_set_reload()関数で可能なので、上コードの様に周期を設定することにしました。0〜7までは、ATmega328の設定値と互換にしてあります。設定値8と9を追加してあり、より長い周期でサンプリングできるように拡張しておきます。
ATmega328版の【osc】コマンドで指定するクロック設定値と同等のサンプリングレートになるようにエミュレートできたので、ATmega328版とSTM32F103版で同じインターフェイス(同じ使い勝手)を実現できて一石二鳥です。
さてここで、サンプル時間の議論に戻っておきます。STM32のコアライブラリでは、デフォルトでサンプル時間55.5サイクルが設定されていて、変換時間としては約5.7μsになるのでしたね?
上コードのコメントに記した割り込み周期と比べると、sclk≧4の設定では、変換時間5.7μsであっても、まだ余裕があるので、そのまま55.5サイクル設定のまま使用しても良さそうです。sclk=3でも、周期6.7μs>変換時間5.7μsですので、変更しなくても大丈夫かもしれませんが、さすがに余裕が無い(ギリギリな)ので、sclk≦3の場合には、サンプル時間の設定を、最短の1.5サイクルに変更することにします。
if (sclk<4)
adc_set_sample_rate (ADC1, ADC_SMPR_1_5);
:
:
: (取り込み終了後、元の設定値に戻す)
// recover adc1 setting
adc_set_sample_rate (ADC1, ADC_SMPR_55_5);
こちらも、終了後に元の設定値に戻しておきます。
ということで、STM32F103への対応は、Arduino側のプログラムで吸収可能で、Olive+のスクリプトの方は変更なしでそのまま使用可能、だったのですが、縦軸の目盛り(スケール)を表示するのに、Olive+のスクリプトにも手を加えてあります。完成版のプログラムをリンクしておきます。
完成版プログラム:logiscope.ino
Olive+スクリプト:logiscope.olv
例によって、オリジナルのデータを更新しておきましたので、どこからダウンロードしても、今回の最新版がダウンロードされます。
では、新しいプログラムをSTM32ボードに転送して、Logiscopeを起動します。まずは、PA0とPA6をジャンパ線で接続しておいてください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,6,64】 …… PA6にデューティ64/255のPWM波形を出力
OK: analogWrite(6, 64)
【osc,0,4,4,0】 …… PA6に接続した、PA0のアナログ波形をサンプリング
実験サンプル:【 】内はLogiscopeからの送信データ
【trg,0,r,26624】 …… PA6に接続した、PA0のデジタル波形を取り込み
普通のデジタルPWM波形をアナログ値とデジタル値でサンプリングして波形にしました。横軸が揃っていて、同じ波形に見えるので、サンプリング時間等の計算は正しいみたいですね。
アナログ波形も確認しておきましょう。「#4-4 デジタルオシロスコープ」で作ったCR回路をベースにして、PWM出力の波形を測定してみます。以下図の様な回路をブレッドボード上に構築してください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,6,64】 …… PA6にデューティ64/255のPWM波形を出力
OK: analogWrite(6, 64)
【osc,0,2,4,0】 …… PA0のアナログ波形をサンプリング
PWM波形は本来方形波ですが、CRの時定数によって波形が鈍り、上の様なアナログ波形が観測できます。64/255デューティのPWMでは、平均的な電圧値は0.85V近辺(目測)でしょうか?これが、PWMでLEDの明るさやモータの回転速度をアナログ的に調整できる原理になっているのですね。
PWM波形のデューティ値をいくつか変更してみて、波形の平均的な電圧値が上下する様子を観測してみてください。
※上図の測定結果を見る限り、普通に平均値Vcc×duty=3.3×64/255≒0.83Vが見えているみたいですね。
速い方のクロック設定(sclk<4)でも試してみたところ、クロック設定3でも取り込み可能でした。クロック設定2以下では、サンプリング周期が早すぎてエラーになり、取り込みできません。fcpu=72MHzのSTM32をもってしても、さすがに早すぎるみたいです。
まとめておくと、ATmega328版ではクロック設定4〜7、STM32F103版ではクロック設定3〜9が設定可能範囲となります。同じ数字を選択すると、フルスケール(3.3Vと5V)の違いを除き、ATmega328でもSTM32F103でも、横軸は同じデータが表示されます。
STM32F103には、AD変換器が2ユニット搭載されており、同時変換などの機能もあるみたいなので、うまく使うと、複数チャンネルのデジタルオシロスコープとかも作れそうです。興味のある方は挑戦してみてはいかが?