前回購入したArduino DUEでLogiscopeを動かしてみたいと思います。
Logiscopeは、以下の様な特徴を持つ電子工作のデバッグ用プログラムです。
基本的に、他の機体で作ったシステムのデバッグをするためのプログラムです。例えばUNOで作ったシステムをデバッグする時、筆者はSTM32+Logiscopeを使っています。STM32は3.3V定格のMCUでありながら、5VトレラントのデジタルI/Oポートを備えているため、5V定格であるUNOのデバッグにも使用可能ですし、なにより72MHzと高速なため、より解像度の高い波形取り込みが可能なためです(内蔵RAMも20Kバイトと大きい)。
DUEを入手した後も、STM32に勝るデバッグ用装置は無いと思います。……5Vトレラントが大きいし、そもそも、DUEはデバッグされる側(アプリケーション)に使うため。
しかし、Logiscopeにはもう一つの使い道があります。それは、I/Oポートやメモリ空間へアクセスが可能と言う、ある意味「モニタプログラム」的な使い方です。
例えば、DUEのタイマユニットを使おうとした時、タイマユニットのI/Oレジスタにはたくさんの設定ビットが有って、どのビットに何を設定して良いのか、データシートと睨めっこしても、良く解らないことが多々あります。そんな時、以下の様にして、コアライブラリが設定するタイマのレジスタ値を確認することが出来ます。
Logiscopeは、ほぼコアライブラリを使って実装されているので、新しいマイコンボード(Arduino)への移植には最適なモチーフです。それほど苦労することなく、移植が可能だという事です。
ただし、【osc】コマンドだけは違います。コアライブラリでは、analogRead()関数がAD変換終了するまでブロックされてしまい、高速にアナログ信号を取り込むことが出来ません。また、一定周期でアナログ信号を取り込むのにも、難が有ります。
このため、Logiscopeプログラム内で【osc】コマンドだけは各MCUのI/Oユニットを直接制御しており、DUE用にコードを追加する必要が生じます。
使用するI/Oユニットは、一定周期で割り込みをかけるためのタイマと、ノンブロッキングでAD変換値を取得するためのAD変換器です。各MCUで割り込みの扱いも異なるので、DUEでの割り込みサービスルーチンの定義方法や割り込みコントローラの設定方法についても学ぶ必要があります。
そのためにも、まずはLogiscopeのモニタプログラムとしての部分を先に確定して、使いたいと思います。手順は以下の通りです。
まず、プラットフォームテストにDUEを追加しました。
// platform test
#ifdef __AVR_ATmega328P__
#elif __AVR_ATmega32U4__
#elif __STM32F1__
#elif __SAM3X8E__ // ← これがDUE用に追加したチェック
#else // error
#error "This architecture is not supported."
#endif // platform test
次に、Logiscopeのプログラムで使っているデータ型をDUEに合わせて定義しました。
#ifdef __SAM3X8E__
typedef signed char int8;
typedef short int16;
typedef long int32;
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned long uint32;
#endif // __SAM3X8E__
更に、Logiscopeのプログラムで使用している定数を調べました。
// resolution of micros()
int idx;
uint32 usec;
uint32 usec0;
uint32 usec1;
uint32 usec2;
const int NUM=10000;
usec=micros ();
usec0=usec;
usec1=usec;
usec2=1000;
for (idx=0; idx<NUM; idx++)
{
while (usec==usec1)
usec=micros ();
usec2=(usec - usec1<usec2)? usec - usec1: usec2;
usec1=usec;
}
Serial.println (usec2);
Serial.println (usec - usec0);
上のコードは、micros()関数が返す値の解像度を調べるコードです。
実行結果:
1
15859
最小の解像度が1μsで、それをNUM=10,000回繰り返すのに要した時間が15,859μsなので、1回当たり1.59μsという計算です。この値、何回か実行すると値がばらつきました。直観的に、何か変です。
コアライブラリのソースを開いて、micros()関数の実装を確認してみました。
<User>\AppData\Local\Arduino15\packages\arduino\hardware\sam\1.6.12\
cores\arduino\wiring.c
※上ディレクトリの参照については、こちらもご参照ください。
コードの中身は引用しませんので、興味のある方は上のソースをご確認ください。
かなり複雑なことをしています。システムタイマのカウンタなど3つの値を複数回読み出して、直前の値と今回の値が等しくなるまで、繰り返しています。ようやく、前後の値が揃って安定すると、値を計算して返しているみたいです。
これは……?
今まで(UNO)のつもりで、うかつにmicros()を使うと思わぬタイムロスを喰らってパフォーマンスを落としてしまうかもしれません。厳密かつ、高速、高解像度のタイマカウンタが必要な場合には、自分でタイマユニットを叩いた方が良いかも?です。
複雑すぎて、意味不明で、こんなに難しい処理が必要な訳もわからなくて、チンプンカンプンです。が、ひとまず、見なかったことにして、micros()関数の解像度を決めて設定しました(↓)。
const uint32 usec_RESOL=1UL; // resolution of micros()
これで、コンパイルは通りました。勿論、【osc】コマンド部分は、コンパイルエラーが出ない程度に対処しておきました。
早速、LogiscopeをDUEに書き込んで、シリアルモニタからコマンドを送信してみます。
実験サンプル:【 】内はシリアルモニタからの送信データ
【rd4,40080004】
OK: read long(0x40080004)=0x00000000
【rd4,40080008】
OK: read long(0x40080008)=0x00000000
アドレス0x40080004などは、SAM3X8Eデータシートで調べたタイマ(TC0)のレジスタアドレスです。上サンプルの様にしてTC0の全レジスタ値を読み出しておきました。
【ana,2,96】
OK: analogWrite(2, 96)
今度はanalogWrite()関数を使ってデジタル端子D2にPWM(デューティ96/255)を出力しました。D2の端子にはタイマ(TC0)のチャンネル0出力TIOA0がアサインされています。
ここで、DUEのタイマについてざっとまとめておきます。
再びタイマのレジスタ値を読み出します。
【rd4,40080004】
OK: read long(0x40080004)=0x0a06c400
【rd4,40080008】
OK: read long(0x40080008)=0x00000000
最初に読み出した値と、今回読み出した値の差分が、【ana】コマンドで実行したanalogWrite()関数が設定を変更した値です。結構沢山のレジスタが有るのですが、ほとんどがリセット値0x00000000のままだったり、状態が反映されるステータスレジスタだったりで、注目すべきは以下の3つでした。
アドレス | レジスタ名 | 設定値 | 備考 | |
0x40080004 | Channel Mode Register | TC0_CMR0 | 0x0a06c400 |
waveformモード カウントアップ+リセットRC MCK/2 等 |
0x40080014 | Register A | TC0_RA0 | 0x00003dc3 | デューティ=96/255×RC0 |
0x4008001c | Register C | TC0_RC0 | 0x0000a410 | PWM周期=1ms |
Arduino HPからREFERENCEをたどってanalogWrite()関数を調べると、DUEのPWM周波数は1,000Hzだと記載されています。モードレジスタTC0_CMR0でカウントクロックをMCK/2に設定していて、MCK=84MHzクロックなので、PWM周期はTC0_RC0=1/(1,000Hz)×(MCK/2)=42,000=0x0000a410となります。
デューティは、96/255に設定しましたので、TC0_RA0=96/255×TC0_RC0≒15,811=0x00003dc3となり、実際に設定された値(上表)と一致します。
※AVR系と異なり、カウンタCVはレジスタRCに設定した値まで上がりません。0〜RC−1までカウントすると、0に戻ります。計算した周期(カウント数)をそのままRCに設定すれば良く、直観的に解り易いインターフェイスです。
意外と簡単で、使い易そうで良かったです。タイマは全ての制御の基本ですからね、今後頻繁に使うでしょうから。
Logiscopeでは、【osc】コマンドでアナログ信号を一定周期毎にサンプリングするため、タイマを使って割り込みをかけます。
PWM波形を出力するわけでは無いので、コアライブラリのanalogWrite()関数等の邪魔にならないタイマを使いたいと思います。前回作成した端子配置表にて、ピンソケットに引き出されているタイマ出力(TIOAxやTIOBx)を確認すると、チャンネルTCC0〜TCC2、TCC6〜TCC8の端子が見つかりますが、TCC3〜TCC5の出力端子が見当たりません。
TCC3〜TCC5の出力端子はどこへ行ったのか?と、DUEの回路図を追いかけたところ、ピンソケットへの引き出しが無くて浮いているようです。
つまり、一定周期で割り込みをかけるだけならば、TCC3〜TCC5のいずれかのチャンネルを使えば、analogWrite()関数によるPWM出力と干渉せずに使えるという事です。
別な角度から、かつ、確認の意味も込めて、コアライブラリのソースにもざっと目を通してみました。
コアライブラリのソースを眺めていて、もう一つ分かったのは、クロックの供給設定が必要なことです。PMC(Power Management Controller)のレジスタで、タイマへクロックを供給する設定をしておかないと、そもそも、タイマを使えません。
クロックの供給はチャンネル(TCC0〜TCC8)毎に設定が必要で、PMC内設定レジスタのビットに割り当てられています。このビット番号をペリフェラルIDと呼んでいて、例えばTCC3へクロックを供給するにはTCC3のペリフェラルIDが必要となります。
ペリフェラルIDは、SAM3X8Eデータシートの「9. Peripherals」に表があります。TCC3のペリフェラルIDは30でした。
以上より、Logiscopeで調べたPWM出力のレジスタ設定値を参考に、TCC3を以下の通り設定しました。
// TC1 setting
REG_PMC_PCER0=PMC_PCER0_PID30; // TCC3 peripheral id=30
REG_TC1_CCR0=TC_CCR_CLKDIS; // stop count
// waveform mode, count up + clear on RC, MCK/2
REG_TC1_CMR0=TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC |
TC_CMR_TCCLKS_TIMER_CLOCK1;
REG_TC1_RC0=4368; // 周期に合わせて設定する。
REG_TC1_CCR0=TC_CCR_SWTRG | TC_CCR_CLKEN; // start count
UNOと違って32ビットのタイマが9本もあって、使い出が有りそうです。
なお、SAM3X8E I/Oレジスタの識別子(REG_TC1_CCR0等)は、以下のファイルで宣言されています。
<User>\AppData\Local\Arduino15\packages\arduino\hardware\sam\1.6.12\
system\CMSIS\Device\ATMEL\sam3xa\include\
component\*.h
instance\*.h
ディレクトリが深い……。
同様にして、AD変換器の設定を調べました。
こちらは前節のタイマと異なり、Logiscopeが起動した時点で既にレジスタが設定されていました。恐らく、コアライブラリの初期化関数内で設定されたのだと思います。
※AD変換器にもPMCのクロック有効/無効ビットがあるのですが、コアライブラリが既にクロックを供給しているので、改めて設定する必要はありません。
起動後、既に設定されていたレジスタは以下の通りです。
アドレス | レジスタ名 | 設定値 | 備考 | |
0x400c0004 | Mode Register | ADC_MR | 0x10380200 | 各変換フェーズの時間設定など |
0x400c0094 | Analog Control Register | ADC_ACR | 0x00000101 |
IBCTL=1 最下位の"1"はデータシートに記載のないビット。何? |
デフォルトの設定値は解ったのですが、ADC_MRの各変換フェーズの時間設定が難問でした。データシートから読み取ってみたので、以下にまとめます。
ADC_MRレジスタには、tSTART、tTRACK、tSETL、tTRANSの各時間を設定するビットがあります。上表で確認した、コアライブラリの設定値を読み解くと、以下となります。
まず、設定としては上の様になっていました、と言う事。正確には実験して検証が必要ですが、ADC_MRを見る限り、AD変換器をスリープモードには設定していないので、tSTART=512サイクルは待たないはず、とか、実際の使い方に照らして、とこにどれだけの時間を待たせるか、設定を変更した方が良さそう。
仮に上の時間全て実行すると、1回のAD変換に要する時間は約40μsとなります。
今回は、Logiscopeのデジタルオシロスコープ機能(【osc】コマンド)を実装するためにAD変換器を使います。3.3Vフルスケールのアナログ信号をAD変換器でデジタル変換して、上位8ビットしか使用しません。取り込むアナログ端子は1本だけなので、アナログチャンネルの切り替えも起こりません。なるべく高速に取り込みたいので、変換速度を最優先にしたいところです。
この設定で、AD変換を実施しようと思います。【osc】コマンドの実行終了後、コアライブラリの設定値に戻すようにプログラムを組んでおきます(↓、adc_mr_bak)。
// adc setting
uint32 adc_mr_bak;
adc_mr_bak=REG_ADC_MR; // 後で元の設定に戻すため、現在値を退避。
// fadc=MCK/4=21MHz, 10 bit low resolution mode
REG_ADC_MR=ADC_MR_TRANSFER (1) | ADC_MR_TRACKTIM (0) |
ADC_MR_SETTLING_AST3 | ADC_MR_STARTUP_SUT0 |
ADC_MR_PRESCAL (1) |
ADC_MR_LOWRES_BITS_10;
// channel setting
nchn=ax2adch[nchn];
REG_ADC_CHDR=0xffff; // disable all channels
REG_ADC_CHER=(0x01<<nchn); // enable nchn
上コード中、チャンネル設定のところにSAM3X8EでのI/Oレジスタ設定に特徴的な処理が見えています。
なんだか不思議な作り。AD変換器だけではなくて、他のI/Oユニットにもこの形式が採用されています。
普通、この手のレジスタはリード/ライト可能にしてあって、特定のチャンネルを有効/無効にしたい場合、MCUでリード-モディファイ-ライトサイクルを発生させて、レジスタを書き込みます。この場合、リードとライトの間で割り込みが発生してしまうと、その割り込み処理中にレジスタ値が変更されても、その後のモディファイ-ライトで古い値が上書きされてしまいます。
これを防ぐために、このようなレジスタ構造が取られているのだと思います。
ただ、筆者には、まどろっこしくて使い難いです。
割り込みは、以下ファイルに宣言されている「void XXX_Handler(void)」関数を、自分のプログラム内で上書き宣言すれば良い様です。ここで、XXXはI/Oユニットの短縮名です。
<User>\AppData\Local\Arduino15\packages\arduino\hardware\sam\1.6.12\
system\CMSIS\Device\ATMEL\sam3xa\include\sam3x8e.h
void TC0_Handler (void);
void TC1_Handler (void);
void TC2_Handler (void);
void TC3_Handler (void);
void TC4_Handler (void);
void TC5_Handler (void);
void TC6_Handler (void);
void TC7_Handler (void);
void TC8_Handler (void);
上の例は、タイマ8チャンネル分のサンプルです。こんな感じで全ての割り込み可能なI/Oユニット分が宣言されています。
この後、割り込みコントローラ(NVIC;Nested Vectored Interrupt Controller)で割り込みを許可/禁止する時に使うIDも、このファイル(sam3x8e.h)で定義されていますので、ついでに確認しておいてください(↓)。
TC0_IRQn=27, /**< 27 SAM3X8E Timer Counter 0 (TC0) */
TC1_IRQn=28, /**< 28 SAM3X8E Timer Counter 1 (TC1) */
TC2_IRQn=29, /**< 29 SAM3X8E Timer Counter 2 (TC2) */
TC3_IRQn=30, /**< 30 SAM3X8E Timer Counter 3 (TC3) */
TC4_IRQn=31, /**< 31 SAM3X8E Timer Counter 4 (TC4) */
TC5_IRQn=32, /**< 32 SAM3X8E Timer Counter 5 (TC5) */
TC6_IRQn=33, /**< 33 SAM3X8E Timer Counter 6 (TC6) */
TC7_IRQn=34, /**< 34 SAM3X8E Timer Counter 7 (TC7) */
TC8_IRQn=35, /**< 35 SAM3X8E Timer Counter 8 (TC8) */
ざっと見たところ、上のIRQ番号と、データシート「9. Peripherals」に示されたペリフェラルIDは一致しているようです。
割り込みを有効にするためには、NVICと、各I/Oユニットの両方で割り込みを許可する設定が必要です。次の節で具体的な例を示します。
Logiscopeでは一定周期のサンプリング時間を知るためにタイマ割り込みを使います。タイマにはTCC3を使いますので、割り込みサービスルーチン(ISR、DUEの環境では「割り込みハンドラ」と言う言葉を使用しているみたいです)を定義します(↓)。
void TC3_Handler (void)
{
// TC1 channel#0 (TCC3) interrupt service routine
// status (flag clear)
REG_TC1_SR0;
// 割り込み処理内容を記述する。
// :
}
タイマの節でも指摘しましたが、ここでも、タイマのユニット番号TC0〜TC2とチャンネル番号TC0〜TC8に同じ「TC」を使っているので、ややこしいです。本ページではチャンネル番号をTCC0〜TCC8と記載することにしましたので、TCC3のハンドラはTC3_Handler()です。
AVRでは割り込みがかかると自動的に割り込みフラグがクリアされていましたが、SAM3X8Eでは自動クリアしてくれませんので、上例の様に明示的にフラグをクリアしておかないと、同じ割り込みがかかり続けてハングアップします。
※割り込みフラグのクリア方法はI/Oユニット毎に異なるかもしれないので、データシートで確認してください。タイマの場合、ステータスレジスタ(TCy_SRx)を読み出すとクリアされるので、上コードの様になります。
タイマTCC3の割り込みを有効に設定します(↓)。
REG_TC1_IER0=TC_IER_CPCS; // CPCS interrupt enable
TC_IER_CPCSは、レジスタRCとのコンペアマッチで割り込みをかける識別子です。
前節のAD変換器で述べたのと同じ様な、割り込みマスクレジスタ(TCy_IMRx)、割り込み許可レジスタ(TCy_IERx)、割り込み禁止レジスタ(TCy_IDRx)の、一式が有ります。リード-モディファイ-ライト問題の回避手法ですね。
最後に、NVICでもTCC3の割り込みを許可する必要があります(↓)。
// NVIC setting
NVIC_EnableIRQ (TC3_IRQn);
以上でようやく、TCC3のコンペアマッチ発生によってTC3_Handler()関数が実行されます。
タイマとAD変換器と割り込みの使い方が解って、【osc】コマンドを実装することが出来ました。以下にDUE対応版のLogiscopeプログラムをリンクしておきます。
DUE対応版プログラム:logiscope.ino
DUE対応版スクリプト:logiscope.olv
いつもの様に、オリジナルのファイルを差し替えたので、どこからダウンロードしても同じ最新バージョンです。Logiscopeの使い方については、以下の記事を参照してください。
logiscope.inoをコンパイルしてDUEに書き込んだら、Windowsでlogiscope.olvを起動してください。logiscope.olvの実行には拙作Olive+が必要ですので、予めダウンロードコーナなどからダウンロードしてインストールを済ませておいてください。
メモリアクセスコマンドを使って、I/Oレジスタの設定値を確認してみましょう。
実験サンプル:【 】内はLogiscopeからの送信データ
【rd4,400c0004】
OK: read long(0x400c0004)=0x10380200
0x400c0004は、ADC_MRレジスタのアドレスです。ADC_MRレジスタの設定値0x10380200が読み出せました。
DUEのD2端子とD3端子をジャンパ線で接続してから、以下を実行してください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,2,96】
OK: analogWrite(2, 96)
【trg,3,r,5000】
Arduinoのリファレンスマニュアルに記載された通り、周期約1,000μsのPWM波形を観測できました。デューティも、約375.8/1,000≒95.8/255と、指定値96/255にほぼ一致しています。
まず、以下図の様な回路をブレッドボードなどを使ってDUEの外部に構築してください。ジャンパ線で、DUEのD2およびA0端子と接続します。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,2,128】
OK: analogWrite(2, 128)
【osc,0,8,3,769】
良いみたいです。
周期1,000μs、デューティ50%のPWM波形を、時定数τ=CR=200μsで充放電させた波形です。
今回のアップデートから、時間軸方向の目盛り(μs/ドット)と、電圧軸方向の目盛り(mV/ドット)を表示するように改定しました。取り込んだ波形を表示した状態で、Olive+の簡易編集機能(「編集メニュー ⇒ 簡易編集」)を実行して縦・横方向のドット数を計測すると、表示された目盛りの係数を掛け算することで時間や電圧に換算することが出来ます(下図)。
上図例では、ボックスカーソルを周期と振幅に合わせたので、周期:横幅154ドット×6.508μs/ドット≒1,002μs、振幅:高さ83ドット×36.26mV/ドット≒3.01Vと読むことが出来ます。
上のサンプルではsck=3で取り込みました。これ以上サンプリングレートを上げる(sck=2および1)と、AD変換が間に合わなくてエラーとなります。STM32 @72MHzでもsck=3が限界だったので、同程度のパフォーマンスでした。
DUEには、DA変換器が2チャンネル搭載されているので、この出力を確認しておきます。
DA変換器も、analogWrite()関数で出力可能です。PWM出力端子を指定すると、指定したデューティのPWM波形(0/1のデジタル波形)が出力されますが、DA変換器端子を指定すると、指定したアナログ電圧が出力されます。
analogWrite()関数には、デジタル端子番号Dxを指定する必要があります。前回示したDUEのピン配置表で、DA変換器出力端子は以下の様に割り当たっています。
DUEのDAC0端子とA0端子をジャンパ線で接続してから、以下を実行してください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,66,128】
OK: analogWrite(66, 128)
値の範囲は0〜255ですので、128を指定すると、3.3V×128/256≒1.65Vが出力されます。その電圧をA0端子でAD変換して取り込んでみました(↓)。
【ina,0】
OK: analogRead(0)=512, 1.650V
ぴったり、期待値通りでした。
本当は、タイマやAD変換器等のレジスタ設定値解析時にあると便利だったのですが、一段落付いたところでメモリの連続領域をダンプするためのコマンドを追加しました。使用方法に関してはこちらをご覧ください。
実験サンプル:【 】内はLogiscopeからの送信データ
【dump4,40080004,7】
long[ 0x40080004 ]= 0x0a06c400
long[ 0x40080008 ]= 0x00000000
long[ 0x4008000c ]= 0x00000000
long[ 0x40080010 ]= 0x00000681
long[ 0x40080014 ]= 0x00003dc3
long[ 0x40080018 ]= 0x00000000
long[ 0x4008001c ]= 0x0000a410
処理はOlive+側スクリプト(logiscope.olv)で実行しており、Arduino側に【rd1】/【rd2】/【rd4】コマンドを必要回数投げて、その応答を表示しています。毎回コマンドをつらつらと打ち込むよりは、楽なはずです。
以上で、LogiscopeのDUEへの移植と、その動作テストは終了です。お疲れさまでした。
LogiscopeのDUEへの移植を通じて、一気に🚀、AD変換器、タイマ、割り込みの使い方を習得できてしまいました。まだ他にも、試しに使ってみないとならないI/Oユニットは沢山あるのですが、そちらは一旦お休みして、そろそろ、TFTシールドの方に取り掛かりたいと思います。