まずは、「#2-3 ロジアナもどきを作る」から、Logiscopeのおさらいをお願いします。
1チャンネルですが、信号波形を取得できたり、I/Oポートの入出力、アナログ端子の読み出しなどが可能なデバッグ用ツールとして、重宝しています。
今回は、このLogiscopeを以下の点において拡張してみたいと思います。
「#4-1 中国から配送されるネットショッピング」で購入したSTM32ボード(STM32F103搭載)への対応も含めて拡張します。
時折、「タイマ(TC1)の設定はどうなっているのだろう?」などと思うことがありました。そのような時には、一々プログラムを組んで、目的のI/Oレジスタを読み出し、シリアルモニタで表示していました。それを何回か繰り返した後で、「I/Oレジスタの読み書きをできるプログラムを作っておいた方が後々楽だな」と、思い立ちました。元々、システムのデバッグ用に使っていたLogiscopeに組み込んでしまうのが、使い勝手的にも一番良いでしょう。
ATmega328、ATmega32U4に代表される8ビットRISCマイコンシリーズをAVRシリーズ(単にAVRまたはAVR8)と呼びます。AVRのアーキテクチャは、どうやら、メモリ種毎に別々な空間を持つみたいです。なので、元々は、タイマやAD変換器、UARTなどI/Oユニットのレジスタ(I/Oレジスタと呼びます)にアクセスするには、I/O空間をアクセスするための専用命令(in命令とout命令、「#3-1 LCD1602に表示する」で解説しました)を使う必要があります。
AVRのI/O空間は0x00〜0x3fまでの64バイトに限定されており、in/out命令のコード上、64バイトを超えてI/Oレジスタを持つことができません。ところが、ATmega328には、64バイトを超えたI/Oレジスタが存在しており、in/out命令ではアクセスできないI/Oレジスタも存在しています。
……(1^^)アーキテクチャに固執することは諦めたのか、ATmega328には、データ空間にI/Oレジスタが配置されていて、専用のin/out命令を使わなくても、普通にRAMにアクセスする命令でI/Oレジスタにアクセスできるようになっています。
このように、I/OレジスタがRAMなどと同じ扱いでデータ空間に配置されていることを「メモリマップドI/O」なんて呼びます。昔からモトローラ&BE(Big Endian)だった筆者にとっては、なじみの深いアーキテクチャです。ATmega328では、0x20〜0xffまでの224バイトがI/Oレジスタ空間として予約されています。
in/out命令でアクセスする時のI/O空間アドレス0x00〜0x3fは、オフセット0x20がついて、0x20〜0x5fの範囲でデータ空間からもアクセスが可能です(I/O空間でもデータ空間でも、どちらからもアクセス可能)。0x5fを超えるアドレスに配置されたI/Oレジスタは、in/out命令ではアクセスできず、普通のメモリアクセス命令を使ってアクセスします。
と言うことで、何も考えなければ、常に、データ空間にマッピングされたI/Oレジスタ(0x20〜0xff)にアクセスすればOKです。ただし、実行速度に関してはin/out命令を使った方が速いですし、コードサイズの節約にもなるので、本来は使い分けた方がお得です。
Logiscopeでは、速度面にもコードサイズ面にも特にこだわりませんし、一律同じ方法でアクセスできた方がスマートなので、データ空間のメモリマップドI/Oの方にアクセスすることにします。
AVRは8ビットRISC MCUですが、I/Oレジスタには16ビットタイマや10ビットAD変換器の様に、16ビットサイズのI/Oレジスタを持ったユニットが搭載されています。8ビットMCUからは8ビット毎にしかアクセスできないので、16ビットのI/Oレジスタは、2回に分けてアクセスしなければなりません。
問題になるのが、1回目のアクセスと2回目のアクセスの間で、I/Oレジスタ値がアップデートされてしまった場合です。例えば、16ビットタイマのカウンタレジスタが、0x01ffから0x0200へカウントアップされる瞬間を挟んで、レジスタを読み出す場合。
最初に上位データ0x01を読み ⇒ ここで0x0200へカウントアップされる ⇒ 続いて下位データを読んだらデータ0x00が読めたので、今のカウンタ値は0x0100、となってしまいます(明らかに間違い。正しくは0x01ffか、0x0200のどちらかが読み出されるべき)。
そのようなことが起きないように、ATmega328のデータシートには、「16ビットI/Oレジスタにアクセスするときの注意事項」が明記されています。
色を変えて強調しておきましたが、アクセス順番は、読み出しと書き込みでは逆順になります。この順番って、一般的な8ビットMCUの常識なのでしょうか??ちょっと変則的に感じました。「Cの実装はどうなっているの?」と思ってコンパイル後のアセンブラソースを眺めてみたところ、上の通りの実装になっていました。
volatile uint *const adr=0x84;
noInterrupts ();
uint dat=(*adr);
(*adr)=0x1234;
interrupts ();
↓ コンパイルされたアセンブラ
CLI
LDS R18,0x0084 ; 下位データ (読み出し)
LDS R19,0x0085 ; 上位データ
LDI R24,0x34
LDI R25,0x12
STS 0x0085,R25 ; 上位データ (書き込み)
STS 0x0084,R24 ; 下位データ
SEI
つまり、Cで普通に実装したら、ATmega328のI/Oレジスタアクセス規約を守ることが可能です。Cのコードに記載しておきましたが、アクセスの途中で割り込まれないように、noInterrupts() 〜 interrupts()で囲んでおく必要があります(アセンブラの場合はCLI〜SEI)。
※ちなみに、アドレス0x84,0x85は、ATmega328に搭載されている16ビットタイマ(TC1)のタイマカウンタレジスタ(TCNT1)です。
※16ビットI/Oレジスタへのアクセス規約(上述)は、in/out命令を使ったときにも同様に適用されます。ただし、見たところ、I/O空間の範囲内には、16ビットアクセスが必要なI/Oレジスタは配置されていない様子(ATmega328)。
前節のサンプルコードで、さりげなく、変数*adrをvolatile宣言しているのですが、必要です。これをつけないと、Cが勝手に最適化してしまう場合があります。
volatile をつけた場合 | volatile をつけない場合 | |
volatile uint16 *const adr=(uint16 *)0x84; while ((*adr)<0x1234) ; |
uint16 *const adr=(uint16 *)0x84; while ((*adr)<0x1234) ; |
|
↓ コンパイル後のアセンブラソース | ↓ コンパイル後のアセンブラソース | |
_WHL001 LDS R24,0x0084 LDS R25,0x0085 CPI R24,0x34 SBCI R25,0x12 BRCS _WHL001 |
LDS R24,0x0084 LDS R25,0x0085 _WHL001 CPI R24,0x34 LDI R18,0x12 CPC R25,R18 BRCS _WHL001 |
*adrにセットした0x84は、ATmega328では16ビットタイマカウンタ(TCNT1)のI/Oレジスタアドレスなので、「タイマのカウンタ値が0x1234より小さい間ウェイト」というコードなのですが、volatileをつけなかった右のコードは期待通りには動作しません。
whileループを形成する分岐(Branch)先のラベル(_WHL001)位置に注目です。Cからしてみたら、「(*adr)なんて、何回読み出しても同じ値でしょ?1回読み出したら十分」ってことです。volatileをつけなかった場合には、ループの外で1回だけ(*adr)を読み出し、ループ内では再度読み出すことの無いコードにコンパイルされています。
volatile (「揮発性の」)をつけることで、Cは考えを変えます。
volatileなんて、「組み込み型C言語」に触れるまで使ったことがありませんでしたが、重要なキーワードなんですね。I/Oレジスタへのアクセスだけでなく、マルチスレッドのプログラミングで、複数のスレッドから共有されるメモリ領域などにも、volatileが必要になるものと思われます。覚えておきましょう。
※Arduino HP ⇒ REFERENCEによると、割り込みルーチン内で値をセットする変数にもvolatileが必要なのだそうです。今までつけてなかったけど、あんまり気にならなかったなぁ……。次から気を付けよう。
今後のこともあるので、STM32F103のデータシートを検索しておきましょう。
STM32F103のデータシートを検索する(別ウインドウが開きます)
STM32F103(ARM Cortex-M3)の方は、まだ使い込んでいませんし、ATmega328に比べてI/Oユニットの数も多く、さすがに把握しきれていません。
しかし、ARMは32ビットMCUですので、AVRの様に8ビットアクセスに分割する必要もなく、1バスサイクルで32ビット「ドンっ」とアクセス可能(アトミック)です。よって、アクセスの順番を気にしたり、割り込みを禁止したりする必要はありません。
以上より、シリアルモニタで受け付ける、I/Oレジスタへのアクセスコマンドを以下の通りとします。
AVRに4バイトI/Oレジスタは存在しませんが、STM32F103で必要なので実装しておきます。
以下、16ビット読み出しコマンド部分の抜粋です。
else if (strncmp (lbuf,"rd2,",4)==0)
{
// rd2,<addr>
// word (16 bits) read from <addr>
volatile uint16 *adr;
uint16 dat;
adr=(uint16 *)xtoi (&lbuf[4]);
#ifdef __AVR__
noInterrupts ();
#endif // __AVR__
dat=(*adr);
#ifdef __AVR__
interrupts ();
#endif // __AVR__
// response
sprintf (lbuf,"OK: read word(%p)=0x%04x",adr,dat);
Serial.println (lbuf);
}
やむを得ない事由が無い限り、割り込みを禁止するべきではないので、STM32F103の場合には実行しないように#ifdefで場合分けしてあります。
全体のコードは、完成版プログラム(logiscope.ino)でご確認ください。
※STM32F103で未定義のアドレスを読み出しに行くと、制御が戻ってこなくなることがありました。例外(バスエラーか何か?)が発生してしまったのか、何なのか、把握しきれていませんが、ご注意ください。万一、そのような状態になってしまった場合には、ボード上のリセットスイッチで回復できます(シリアル接続は切れてしまうので、再接続が必要)。
レジスタ値をリードできるようになったことで、「このタイマは、今どういう設定で動いているのだろう?」などと思った場合に、すぐに読み出して確かめることができますし、ちょっとした設定変更であればI/Oレジスタに書き込んで変化を確認することもできます。昔の「モニタプログラム」みたいな感覚です。ご活用ください。
そもそも、今、何でSTM32F103を搭載したマイコンボードを買ったの?という理由をひも解くと、「Logiscopeの波形取り込み機能を高速化したかった」ことに辿り着きます。
まず、Arduino UNOを使って実験してみましょう。UNOのD9とD2をジャンパ線でつないで、Logiscopeを起動してください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,9,64】 …… D9のPWM波形でデューティを64/255に設定する。
OK: analogWrite(9, 64)
【trg,2,r,20000】 …… D2に接続した、D9のPWM波形を取り込む。
セッティングが正しければ、以下の様な波形が表示されたはずです。
実験を続けます。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,9,1】 …… D9のPWM波形でデューティを1/255に設定する。
OK: analogWrite(9, 1)
【trg,2,r,20000】 …… D2に接続した、D9のPWM波形を取り込む。
今度はどうでしょうか?手元の環境では、以下の様な波形となりました。
analogWrite()が出力する約490HzのPWMで、デューティ 1/255だと、HIGH幅(≒8μs)が短すぎて、うまく取り込みできないのです。たまたま、HIGHをサンプリングできた時だけ、HIGHパルスが表示されるので、歯抜けになっています。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,9,2】 …… D9のPWM波形でデューティを2/255に設定する。
OK: analogWrite(9, 2)
【trg,2,r,20000】 …… D2に接続した、D9のPWM波形を取り込む。
デューティ 2/255(HIGH幅≒16μs)では、歯抜けはありませんでした。つまり、16μs程度の幅があれば、何とか取り込み可能なようです。測定したHIGH幅は、16μsまたは20μsの幅で観測されました。時間の測定に使用しているmicros()の解像度が4μsですので、妥当な値です。
取り込む部分のコードを精査してみましたが、端子のサンプリングだけならまだしも、データの符号化とバッファへの書き込みで一杯一杯となっており、これ以上の高速化は筆者の腕では無理でした(1^^;)。
STM32F103は32ビットのMCUで、かつ72MHzという高速動作をしているARM Cortex-M3なので、同じプログラムでも、自動的にサンプリングレートは向上する、という訳です。自分の腕の無さを棚に上げて、結局ハードウェアに頼るのか!ってことですよね……m(1^^;)m。
今回、STM32F103用にコンパイルする過程で、修正が必要だった個所は以下の通りです。
Serial.begin (9600);
while (!Serial)
;
delay (10);
なぜか、delay(10)が無いと、最初のシリアル送信データがPCで受け取れませんでした。理由は不明。少し時間が経過した後に送信したデータは受け取れていたので、単に、「10ms待つ」という行為自体が必要だったように見えます。
個人的に使っていた型宣言が、STM32用ライブラリとコンフリクトしていたので、修正しました。
#ifdef __AVR__
typedef unsigned char uint8;
typedef unsigned int uint16;
typedef unsigned long uint32;
#endif // __AVR__
デバッグ中、どうも波形表示が合わなくて気が付いたのですが、STM32のmicros()は、解像度が4μsより小さい様です。下一桁の数字として、0〜9まですべての数値が出てくるので、1μs程度の解像度があると考えられます。以下の様に、micros()の解像度を定数にすることで、AVR系と場合分けしました。
#ifdef __STM32F1__
const uint32 usec_RESOL=1UL; // resolution of micros()
#else // __STM32F1__
const uint32 usec_RESOL=4UL; // resolution of micros()
#endif // __STM32F1__
AVR→STM32のために修正したのは、以上三点のみです。
STM32F103には、20KバイトのRAMが搭載されています(ATmega328の10倍!)ので、波形取り込み用のバッファサイズを少し拡張しておきましょう。
// matrix (2D) buffer
#ifdef __STM32F1__
const int wf_bufmx=32; // matrix size of waveform buffer
#else // __STM32F1__
const int wf_bufmx=16; // matrix size of waveform buffer
#endif // __STM32F1__
もっと大きくしても平気ですが、あまり大きくしても波形表示の方がついてこないので、こんな所で十分でしょう。必要なRAM容量は、以下の式で計算可能です。
最大バッファサイズ=wf_bufmx×wf_bufmx×2バイト
場合によっては、取り込みバッファサイズをさらに大きくしたいケースもあり得ると思いますので、使用可能なRAM容量と相談の上、決めてください。
バッファサイズを拡張すると、そのデータを受け取るPC側のスクリプト(logiscope.olv)にも変更が必要です。接続先がUNOかSTM32かを意識することなく使用できるように、【trg】コマンドで波形を取り込んだ時のレスポンスに、バッファサイズ(wf_bufmx)を返すようにしておきます。
【trg,2,r,1000】
1RE,1000,<wf_bufmx値>
:
<波形データ>
:
END
PC上のスクリプト(logiscope.olv)で、【trg】コマンドのレスポンスからwf_bufmx値を取り込み、自動的に必要なバッファサイズを確保するようにしておきました。
今度は、STM32F103を使ってLogiscopeを実行してみましょう。STM32F103ボードのPA2とPA6をジャンパ線でつないでおいてください。
実験サンプル:【 】内はLogiscopeからの送信データ
【ana,6,1】 …… PA6のPWM波形でデューティを1/255に設定する。
OK: analogWrite(6, 1)
【trg,2,r,20000】 …… PA2に接続した、PA6のPWM波形を取り込む。
刃こぼれすることなく、取り込みできているようです。
UNOと異なり、STM32F103のPA6で出力されるPWMは、周波数が550Hzくらいでした(上の取り込み波形より)。従って、デューティ 1/255のHIGH幅≒7μsとなります。上の波形で、立ち上がりエッジの時間と立ち下がりエッジの時間を引き算するとHIGH幅が求まるのですが、6〜8[μs]となっています。期待値7μsに対して6〜8[μs]でばらつくのは、原理上妥当と考えられます。
プログラム上は、同じmicros()の時間内に複数回の信号変化を取り込むと、幅0μsのグリッチが取り込まれるはずなのですが、ちょっと限界を試してみます。
analogWrite()のPWMでは、これ以上短いパルスを生成できないので、STM32ボードと同時に、UNOをPCに接続して以下の様なプログラムを実行します。
void setup (void)
{
pinMode (9, OUTPUT);
}
void loop (void)
{
digitalWrite (9, HIGH);
digitalWrite (9, LOW)
delay (1);
}
これでUNOのD9端子に約1ms周期で、数μs程度のHIGHパルスが出力されます。
STM32とUNOのGNDを接続し、STM32のPA8とUNOのD9をジャンパ線で接続してください。UNOの出力は5V信号ですので、STM32の5Vトレラント端子PA8を使います。STM32でLogiscopeを実行して以下のコマンドを送信します。
実験サンプル:【 】内はLogiscopeからの送信データ
【trg,8,r,20000】 …… PA8に接続した、UNO D9端子の波形を取り込む。
計測できたHIGH幅は、3〜5[μs]の範囲で、平均は約4.2μsでした。
UNOのプログラムで、digitalWrite()を使わずに、インラインアセンブラ(「#3-1 LCD1602に表示する」参照)でポートを叩くと、もっとHIGH幅の短いパルスを生成可能です。取り込み可能なHIGH幅の限界を調べておきましょう。
void setup (void)
{
pinMode (9, OUTPUT);
}
void loop (void)
{
// D9=PB1
const unsigned char reg_portb=0x05;
unsigned char dat;
dat=0x02;
__asm__ __volatile__ (
"out %1,%0"::
"r"(dat),
"I"(reg_portb)
);
delayMicroseconds (3);
dat=0x00;
__asm__ __volatile__ (
"out %1,%0"::
"r"(dat),
"I"(reg_portb)
);
delay (1);
}
UNOに上のプログラムを書き込み、delayMicroseconds()の引数を変えて試してみました。HIGH幅2μsだと歯抜けが発生、3μsでは歯抜けせずにサンプリングできることが分かりました。STM32F103(@72MHz)+Logiscopeでの最小サンプリング幅は3μsだと考えて良いでしょう。
0μsのグリッチを観測できるほどには、サンプリングレートは上がっていませんでしたが、このスピード(〜3μs)の波形が歯抜けすることなくサンプリングできれば、満足ですo(1^−^)o。
改定版のプログラムをリンクしておきます。
改定版プログラム:logiscope.ino
Olive+スクリプト:logiscope.olv
オリジナルの「#2-3 ロジアナもどきを作る」でリンクしたプログラムをアップデートしておいたので、どちらからアクセスしても、今回の改定版がダウンロード可能です。
これで、Arduinoのデバッグに使える範囲が「ぐっ」と広がったはず。
ただし、5V系のUNO、Micro、Pro Mini等のデバッグをする時に、STM32F103のLogiscopeを使う場合は要注意です。5V系の信号をSTM32でモニタする時には、「5Vトレラント」ポートに接続しなければなりません。また、STM32のAD変換器は、MAX 3.3Vまでしか接続できないので、5V系の信号を繋いでアナログ値を読み込むことはできません(下表の通り、AD変換器の入力に割り当たっている端子は、非5Vトレラント)。
どの端子が5Vトレラントか?あるいは、Logiscopeで指定すべきArduinoのピン番号が、どのように割り当たっているのか?を、以下表にまとめておきます。
STM32F103C (QFP48) Pin Assignment | |||
QFP48# | NAME | 5V | PIN#(Arduino) |
44 | BOOT0 | - | |
7 | NRST | - | |
5 | OSC_IN | - | |
6 | OSC_OUT | - | |
10 | PA0/WKUP/USART2_CTS/ADC12_IN0/TIM2_CH1_ETR | - | 0 |
11 | PA1/USART2_RTS/ADC12_IN1/TIM2_CH2 | - | 1 |
12 | PA2/USART2_TX/ADC12_IN2/TIM2_CH3 | - | 2 |
13 | PA3/USART2_RX/ADC12_IN3/TIM2_CH4 | - | 3 |
14 | PA4/SPI1_NSS/USART2_CK/ADC12_IN4 | - | 4 |
15 | PA5/SPI1_SCK/ADC12_IN5 | - | 5 |
16 | PA6/SPI1_MISO/ADC12_IN6/TIM3_CH1 | - | 6 |
17 | PA7/SPI1_MOSI/ADC12_IN7/TIM3_CH2 | - | 7 |
29 | PA8/USART1_CK/TIM1_CH1/MCO | FT | 8 |
30 | PA9/USART1_TX/TIM1_CH2 | FT | 9 |
31 | PA10/USART1_RX/TIM1_CH3 | FT | 10 |
32 | PA11/USART1_CTS/CANRX/USBDM/TIM1_CH4 | FT | 11 |
33 | PA12/USART1_RTS/CANTX/USBDP/TIM1_ETR | FT | 12 |
34 | PA13/JTMS/SWDIO | FT | 13 |
37 | PA14/JTCK/SWCLK | FT | 14 |
38 | PA15/JTDI | FT | 15 |
18 | PB0/ADC12_IN8/TIM3_CH3 | - | 16 |
19 | PB1/ADC12_IN9/TIM3_CH4 | - | 17 |
20 | PB2/BOOT1 | - | 18 |
39 | PB3/JTDO | FT | 19 |
40 | PB4/JNTRST | FT | 20 |
41 | PB5/I2C1_SMBAl | - | 21 |
42 | PB6/I2C1_SCL/TIM4_CH1 | FT | 22 |
43 | PB7/I2C1_SDA/TIM4_CH2 | FT | 23 |
45 | PB8/TIM4_CH3 | FT | 24 |
46 | PB9/TIM4_CH4 | FT | 25 |
21 | PB10/I2C2_SCL/USART3_TX | FT | 26 |
22 | PB11/I2C2_SDA/USART3_RX | FT | 27 |
25 | PB12/SPI2_NSS/I2C2_SMBAl/USART3_CK/TIM1_BKIN | FT | 28 |
26 | PB13/SPI2_SCK/USART3_CTS/TIM1_CH1N | FT | 29 |
27 | PB14/SPI2_MISO/USART3_RTSTIM1_CH2N | FT | 30 |
28 | PB15/SPI2_MOSI/TIM1_CH3N | FT | 31 |
2 | PC13/TAMPER-RTC | - | 32 |
3 | PC14/OSC32_IN | - | 33 |
4 | PC15/OSC32_OUT | - | 34 |
1 | VBAT | - | |
24 | VDD1 | - | |
36 | VDD2 | - | |
48 | VDD3 | - | |
9 | VDDA | - | |
23 | VSS1 | - | |
35 | VSS2 | - | |
47 | VSS3 | - | |
8 | VSSA | - |
AD変換器入力端子 |
5Vトレラント端子 |
「5V」の欄に「FT」と書かれた端子が5Vトレラント端子です。Port-A(PAx)の前半やPort-B(PBx)の一部に、変則的な非5Vトレラント端子があるので、そこに5V信号を繋いでしまわないように注意しましょう。
※蛇足ですが、PA11,PA12は、USBの差動信号入力に割り当たっていますので、USBを使うときにはI/Oポートとしては使用できません。普通にピンヘッダに引き出されているので注意が必要です。
個人的にARMは好きです、ネイティブARMは……!でも、Cortex-M3はThumbですからね……、あまりアセンブラには触れたくないですね。今一、萌えない(どちらかと言うと嫌い、美しくない)。
幸い72MHzと高速なので、アセンブラには手を出さずにCでプログラムしても十分なパフォーマンスが得られるはずです。アセンブラに触れない様にしたら、MCUなんて何でも同じですから(モトローラ勢の憂い(1;ω;))。