ARM系列のCPU、特に、ARM7DTMIをコアとしたものについて見聞きすることが多いのですが、PICやAVRのように、手軽に秋葉原で入手できるという状況ではないようです。
とりあえず、容易に入手できるものを挙げておきます。いずれも基板です。
雑誌DesignWaveに付属していたもの。現在でも入手可能なようです。
CPUはANALOG DEVICESのADUC7026。RSインタフェースとJTAGインタフェースが載っています。ソフトの書き込み等は、RS経由でも可。
電源周りの回路について、色々と、話題を提供してくれています。手持ちのボード2枚は、現在のところ回路変更なしでも元気に動いています。
写真は、自作のパラレルポートJTAGインタフェースを付けた状態です。この回路のJTAGドライバーは公開されていて、GDB(GNU DEBUGGER)との組合せでデバッグできます。
秋葉原では、ITプラザで購入できます。
CPUはPHILIPSのLPC2138。とても小さな基板です。JTAGインタフェースが出ています。標準的には、同じOptimize社のEZ-USB(USBインタフェース付きCPU)を使用するとJTAGでデバッグ可。
基本的には、キット商品で、チップ部品の半田付けが少々面倒。完成品もあり。
写真は、MINI EZ-ARM7 + EZ-USB。
こちらも、ITプラザで購入できます。CPUがPHILIPSのLPC2214です。こちらは、バスが外に出ているチップなので、大き目のシステムを組む場合には便利でしょう。EZ-USBとの組合せ推奨。未入手です。
CPUはATMELのAT91R40807。内部ROMがなく、プログラムは、外付けのFLASHに書き込む形。開発セットが用意されています。未入手。
写真は最近使用しているIAR社のJTAGアダプタ「J-LINK」。USB接続で手軽に使えます。開発ツールは非常に高速でストレス無く使えます。
ARM7についてブログの方でゴタゴタと書いていた内容が、割合にアクセスが多いのに気がつきました。日付も古くなって下のほうへ移動してしまったので、こちらにもまとめておきます。
ここのところ、Arm7(Am7 ACEGではない)のアセンブラでプログラムを書いているのですが、キャリーフラグで嵌ってしまいました。
要するにキャリーがセットした状態では、キャリーフラグが0。リセットした状態ではキャリーフラグが1。直接、キャリーフラグを見る命令(というか、表現)、ADDCSなどを使わなければ、問題が無かったのですが。
BCS (キャリーフラグが1ならば分岐)は、キャリーが発生していなければ分岐というややこしいことになっています。
条件コードを見ると、CS(キャリーフラグが1)は、HS(符号無しで大きいか等しい)と同じもの。確かに、この条件はキャリーが発生していないことと等価ですね。
pc(r15、プログラムカウンタ)の扱いも特殊で、こちらはマニュアルを読んでいるときに気がついたのですが、mov r0,pc と書くと、r0には、現在のアドレス+8が入ります。この命令を実行後、pcは+4されるので、過去に読んだr0の方が大きいアドレスを示すという珍妙さです。パイプライン処理の都合なんでしょうか。
思い込みがいけないのですが、常識的に動けよと思ったりもします。何にせよ、思わぬところで新しい発見があります。
ARM7に限りませんが、RISC といっているCPUは、マシン語レベルではメモリーへのアクセスが不自由です。これは、1命令1ワードの形式を取っているゆえの宿命ともいえます。
メモリーのアドレス幅が32ビット。1ワードも32ビット。これでは、アドレスを命令語の中に埋め込むことができません。メモリのXXXX番地から1ワード読み込む。たったこれだけのことをマシン語では実現できないことになっています。
実際、ARM7のメモリアクセスは,LDR/STRという命令で行いますが、この命令にはレジスタ相対形式しかありません(アドレッシング・モード自体は凝ったものがあるのですが)。
任意のアドレスから読み込むためには、そのアドレスをレジスタに格納する必要があります。このためのアドレスを直値として入れることもできません。つまり、レジスタに固定の値を読み込む場合にも、命令語1ワードの中に1ワードを埋め込むことができないという制限がかかってしまうわけです。
この使い難さを、やや、姑息とも思える方法で回避することになります。その手段として擬似命令がサポートされています。
ADRの内容をレジスタR0に読み込む。これは、実際は、PC相対アドレスにアセンブラが置き換えています。プログラムの近所のメモリーならばこれでアクセスできます。
ADRをR0にセットします。実際は、任意の直値をレジスタにセットする擬似命令です。これは、アセンブラがちょっと巧妙に処理しています。吐き出す命令は、LDR R0,[PC+#XXX]というもの。つまりPC相対であるアドレスから読み込み、その「あるアドレス」は適当なタイミングでアセンブラが領域を確保し、「ADR」が格納されています。
この擬似命令を使った場合は、アドレスの到達範囲の制限はありません。
ADRをR0にセットします。LDR R0,=ADRに似ていますが、ADD R0,PC,#XXXのようにPCに対する直値演算を利用します。それゆえ、アドレスの到達範囲は狭いです(ワードアラインでない場合-247〜263、ワードアラインで-1012〜1028)。
ADRをR0にセットします。ADR R0,ADRを拡張してアドレスの到達範囲を拡げたもの(ワードアラインでない場合64KB以内、ワードアラインで256KB以内)。 演算命令を二つ使用しています。ADD R0,PC,#XXXX / ADD R0,R0,#YYYYなど。
まぁ、使用するメモリも少ないし、そんなに気にしなくてもPC相対で十分じゃん。って思いがちなのですが、大抵のCPUでは32ビットの空間にROM/RAM/IOなどが伸び伸びと配置されているので、広い範囲でのアクセスが必要になるものです。
直値をセットする場合でも、同様で、結局、汎用はLDR R0,=ADR。アセンブラがこっそりと、近所に値を埋め込んでそれを読み込むという方法です。
分岐命令の場合、ARM7ではおよそ32MBの相対アドレスが使えるので、普通のプログラミングの場合は十分ですが、ROMが分散している場合(CPU内蔵と外部ROM)などではとても届きません。ジャンプの場合は、LDR PC,=ADRのお世話になります。BL(ブランチ&リンク)つまり、サブルーチンコールの場合には、手でLR(戻りアドレスレジスタ)に値をセットしてからジャンプするか、LDR PC,=ADRを書いたルーチンをBLで呼び出します。
結局、1ワードの命令語の中に1ワードの値を格納できないために、なんとも、歯がゆい方法でプログラムを書くことになってしまうのでした。
ARM7には、[ハードウェア]スタックがありません。通常使うレジスタは、汎用レジスタとしてR0-R15があります。この中で、R15はPC(プログラムカウンタ)。R14はLR(リンクレジスタ)として働きますが、その他はまったく対等に扱えます。演算命令も、R14は全く同様に、R15はちょっと制限つきで使えます。
サブルーチン・コールで困るじゃないか。ということなのですが、通常の分岐に用意されているのはB(ブランチ)命令。サブルーチンコールはBL(ブランチ&リンク)命令。
BL命令は、指定の番地へ分岐する前に戻りアドレスをR14=LRに保存します。戻るときは、LRの内容をPCに書き込めば(MOV PC,LR)結果的に元に戻るというワイルドな仕様。多重呼び出しではプログラマが自己責任でLRを保存するという考え方です。
とは言え、やはり、スタックは便利。そこで、ソフトウェアでお約束としてスタックを実現することになります。このとき、スタックポインタとして使用するのはR13。本来はどのレジスタを使用してもいいはずですが、実は、R13-R15は、割り込みなどモードによって自動的にバンク切り替えがされるレジスタで、結局、SPには、R13を使えということです。アセンブラでも「SP」と書けばR13をあらわすことになっています。
スタックの実現方法も多様に出来るようになっていて、アドレスの小さいほうから伸びるとか、アドレスの大きいほうから伸びるとか、SPが示すのはスタックトップとか、スタックの次の位置とか・・・いろいろマニュアルにも書いてありますが、「アドレスの大きいほうから小さいほうへと伸び、SPはスタックの先頭を示す」というのが、お薦めです。
すると、サブルーチンの書き方は、その中で他のルーチンを呼ばない(リーフ)場合と、呼ぶ場合で多少変えて書きます。
(1)他のルーチンを呼ばないとき
SUB00: ( 処理 ) MOV PC,LR
(2)他のルーチンを呼ぶとき
SUB00: STMFD SP!,{R1-R8,LR} (処理) LDMFD SP!,{R1-R8,PC}
(1)の最後の、MOV PC,LRが要するにRETURN。ルーチン内ではLRは壊してはいけません。
(2)では、STMFD SP!,{R1-R8,LR}がプロローグコードで、ここでは、R1-R8,LRという九つのレジスタをスタックに待避。最後のLDMFD SP!,{R1-R8,PC}がエピローグコードで、待避したR-R8を戻し、さらにLR(戻り先アドレス)をPCに入れて結果的にRETURN。R1-R8は例としてあげたもので、何でもかまいません。ルーチン内でLRを使用できます。
何のことは無いといってもスタック操作に気を使いながらコーディングをしなければなりません。
直接アドレスでメモリーを読むのが苦手なので、ルーチン内の作業領域はスタックフレームに作るのが吉。例えば4ワードの作業領域を作るならば…
SUB00: STMFD SP!,{R1-R8,LR} SUB SP,SP,#4*4 ....... LDR R0,[SP,#4] ; 2番目のワードを参照 ......... ADD SP,SP,#4*4 LDMFD SP!,{R1-R8,PC}
の様に意識してスタックフレームを作成します。「4*4」とバイト数を直接指定するのが渋いですね。もし、スタックフレームに初期値を入れるならば、SUB SP,SP,#4*4の代わりに、STMFD SP!,{R0-R3}などと書けば、R0-R3の内容がスタックフレームに積まれます。
当然ながら、PUSH/POPも無いので、複数のレジスタをまとめてPUSH/POPならば,PUSH R0,R1,R2/POP R2,R1,R0の代わりに STMFD SP!,{R0-R2} / LDMFD SP!,{R0-R2}と書きます(ただしレジスタの順序はいつも同じ)。
単純なPUSH R0/POP R0などは、もっとスタックを意識して、STR R0,[SP,#-4]!/LDR R0,[SP],#4 というわざとらしい書き方が要求されます。STR R0,[SP,#-4]!は、SPを-4してアドレスとし(プリ修飾)、そこへ、R0を書き込み、アドレスをSPに戻す(ライトバック…最後の「!」がこれを指示)。LDR R0,[SP],#4は、SPのアドレスからR0に読み込み、SPに+4(ポスト修飾、常にライトバック)。
アセンブラの醍醐味というか、スタック操作を楽しめるのですが、一種の様式美の世界。ルネサスのSH3/4なども同じようにハードウェアスタックがありませんね。割り込みの応答が多少速くなる(はず)といったメリットもあるのでしょうが、面倒なだけのような気もします。流行なのでしょうね。きっと。
キャリーフラグについて、もう少し、調べてみました。結局のところ、「ARM アーキテクチャリファレンスマニュアル」をきちんと読まないといけないようですね。
大まかに言えば、加算系列の命令( ADD,ADC,CMN )では、キャリーが発生すると、Cフラグは1。
減算系列の命令( SUB,SBC,RSB,RSC,CMP )では、ボローが発生すると、Cフラグは0。
要するに加算系の命令と、減算系の命令では、キャリーフラグの取り扱いが違うということです。
キャリーフラグが操作される別のケースもあります。
ARM7では、演算命令の多くは、OP DEST,SRC1,SRC2の形式で書きますが、この中のSRC2をシフト修飾(LSL,LSR)してから演算を適用できます。
この演算命令の「シフタ操作」でキャリーが発生する場合です。シフタを使わない場合はキャリーフラグは変わらないようです。
今、キャリーフラグが1であるとして、
MOV R0,#0 ; C=1 ORRS R0,R0,R0 ; C=1 ORRS R0,R0,R0,LSR #1 ; C=0
となりました。2行目ではシフタ指定が無いので、キャリーフラグは変わらず。3行目はR0を右に1ビットシフトした結果、押し出された0がキャリーフラグに入ります。
次のような例は、興味深い例です。
MOV R0,#0 MOV R1,#1 ADDS R0,R0,R0 ; C=0 ADC R0,R0,R1,LSR #1 ; R0= ????
演算内容は、R0=R0+(R1>>1)+CARRY。
シフタからのキャリーは、1。となるとR0は1になるか?
ですが、R0は0になりました。
この場合、シフタのキャリーは無視されて、期待通り、ADC命令実行直前のキャリーフラグが使われるようです。
ARM7をアセンブラでコーディングしていて面白いのは条件実行という機能です。
これは、大抵の命令が現在の条件(キャリーとかゼロとかのフラグ)を指定して実行するか否かを指定できるようになっている機能です。
例えば、ADD(加算命令)は、ADD R0,R1,R2のように書きますが、これに条件(HI)を加えて、ADDHI R0,R1,R2のように書けば,条件(HI)が成立するときだけ加算を実行します。成立しないときは、この命令は実行されませんが、CPUタイムは同じだけ消費します。条件実行を使用して書くとすっきりとプログラムが書けます。
条件実行を使わないと、
CMP R0,#0 BNE SKIP ADD R0,R0,#1 SKIP:
となるものが、条件実行を使うと、
CMP R0,#0 ADDEQ R0,R0,#1
無駄なラベルを書かなくても良いので見た目もすっきりします。また、分岐を行わないのでパイプラインが乱れず、処理速度も速くなります。といっても、実行しなくても処理時間は変わらないので、あまり長い条件節を書くと、さすがに、分岐で飛ばしたほうが有利なのですが、ARM7のパイプラインが3段なので、2−3命令ぐらいならば、この書き方が有利だそうです。別の例も見てみます。
条件実行を使わないと、
CMP R0,#1 BEQ SAMEVAL CMP R0,#7 BEQ SAMEVAL CMP R0,#9 BEQ SAMEVAL B OTHERVAL SAMEVAL:
といった処理が、
CMP R0,#1 CMPNE R0,#7 CMPNE R0,#9 BNE OTHERVAL (SAMEVAL:)
といった具合。CMP命令自体に条件実行機能を使うことで、複合条件をコンパクトに書けます。
もう一つ面白いのは、演算結果のフラグへの反映についてで、命令の後に、Sをつけないと一部を除き(CMP、TSTなど)、フラグへ反映しません。
CMP R0,#1 ADD R0,R0,#4 SUB R1,R0,#7 BEQ WHERE
のようにずらずらと書いても、最後のBEQで最初のCMP R0,#1のフラグを調べることができます。これも、プログラムをすっきりと書くのに役立ちます。他方、
MOV R0,#10 LOOP: ..... SUB R0,R0,#1 BHI LOOP
は、単純な繰り返しのサンプルですが、下から2行目のSUBはフラグをセットしないため、正しくループしません、SUBはSUBSと書く必要があります。慣れるまで何度かこれで失敗しました。
お気づきでしょうが、条件分岐命令も、用意されているわけではなく、分岐命令(B)に条件実行機能が付加されて、結果的に条件分岐命令となります(BEQなど)。ブランチ&リンク命令(BL)にも条件実行機能が働くので、昔懐かしい条件付サブルーチンコールが実現できます(BLEQなど)。ついでに、条件付リターンは典型的には、MOVEQ PC,LRと書けばよいわけです。
何故か、ちょっと、ワクワクして楽しそうでしょ?ARM7のアセンブラ。Cなどで書いていると、なかなか、こういう楽しみは味わえません。やっぱ、アセンブラでしょ。
JTAGを小ざかしく使った開発手法です。
CPUにFLASH−ROMが内蔵されており、最終的にはそこにプログラムを書き込みます。このFLASHが書き込み回数制限があり、比較的少ない。使用経験の少ないCPUを使うときにはマニュアルではっきりしないことを実験するなど試行錯誤が多いのでこの回数制限は非常に困ったことになります。
通常ならば、必要最小限の小さなブートローダーをROMにえいっとばかり書いて、以降はRAM上でシステム周りをデバッグ、ある程度まとまってからFLASHに書き込むという手順を踏むのですが、小ざかしくJTAGの機能を使うことでもう少し楽になります。
ARM7の場合、REMAPという機能が搭載されていることが多いのです。これは、各種ベクターが0番地付近から始まるため、ベクター書き換えの必要があるダイナミックな実行に備え、0番地付近をFLASHやRAMのいずれに割り当てるかをソフト的に決める機能です。
デバッグするプログラムをFLASHの代わりにRAMにロードしてデバッグしたいのですが、このためにはロードする前にREMAPする必要があります。もちろん、ルーチンをリロケータブルに書いておくとか、デバッグ用にベクターだけサンクするなどもできるのですが、ストレートにROMイメージと同等にデバッグできるわけではありません。
このとき、使用しているJTAGのデバッガでは、プログラムロード前に、JTAGを通してポート操作をする機能があり、それを使用します。つまり、ロード前に、CPUのREMAP機能をJTAGで切り替え、0番地近辺をRAMにしてからロードするように指示する訳です。
こうして、システムモニターを作成し、出来上がったら内蔵FLASHに焼きこみ。次に、アプリケーション作成の段取り。
これも、ある程度までのデバッグはRAM上で行いたい。内蔵RAMでは容量が足りないので外部RAM上で動作させることにします。ところが、障害になるのが、外部メモリの制御機能。CPUの外部制御機能を操作してからでないと外部RAMにアクセスできません。つまり、単純にはデバッガでロードできません。
ここでも、やはり、JTAGのポート操作機能を用い、外部メモリの設定(バス幅、アクセスタイムなど)を行ってから、外部RAMにロードするように指示しできます。この状態でロードしておき、FLASHにあらかじめ焼きこんであるシステムを0番地から実行してアプリケーションを呼び出させればアプリケーションをRAM上でデバッグが出来ることになります。
この辺の開発手順は、環境によって臨機応変に対応するのですが、JTAGの機能(とARM7の「D」機能)を小ざかしく利用することで、ほとんどの工程をRAM上で行うことができ、非常に効率の良い開発ができます。
まさに、恐るべしJTAGという話でした。
ARM7の単一メモリアクセスは、LDR/STRを使用します。この二つの命令は、実はマシン語上はわずかに1ビット違うだけ、対称的に設計されています。このへん、RISCを標榜するだけあって体系的な構成。したがって、LDRを覚えれば、STRはほぼ理解できます。
LDRはワード(4バイト)、バイトのメモリをアクセスする命令。バリエーションでハーフワード(2バイト)アクセスや、符号付のLDRも同じニモニックに割り当てられていますが、実は、これは別のマシン語命令でアドレスモードなども異なっています。
LDRのアドレスモードは一見複雑なようですが、実は単純。ベースレジスタ相対+オフセットしかありません(メモリのダイレクトアドレスは不可!!)。ベースレジスタはR0-R15を使用。これにバリエーションが付きます。
組合せ4通りできそうですが、ポスト修飾してから、それをベースレジスタに戻さない処理は意味がありません。そこでアセンブラでの書き方は、ちょっと汚いですが、3通り。
プリ修飾、ライトバック無し。R1+offsetのアドレスの内容をR0へ読むだけ。
プリ修飾、ライトバック有。最後に「!」を書きます。R1+offsetでアドレスを計算。そこからR0へ読み込み。R1へR1+offsetを代入(ライトバック)。
ポスト修飾。ライトバック有。R1で示されるアドレスの内容をR0へ読み込む。その後、R1へR1+offsetを代入。
オフセットは±自由に指定できます。上の「2」は他のCPUではプリインクリメント、プリデクリメントに相当。「3」はポストインクリメント、ポストデクリメントに相当します。
オフセットの種類は大まかに3種類。
LDR R0,[R1,#0x20]のように書きます。±12ビット(2KB)の範囲で書けます。
LDR R0,[R1,R2]のように書きます。R1+R2がアドレス。
LDR R0,[R1,R2,LSL #2]のように書きます。
これが最も曲者です。この例ならば、R2を左に2ビットシフト(4倍)してR1に加えたものがアドレスとなります。スケールつきのアクセスということですね。通常の使用法ならば、シフタはLSLだけですが、ついでにということでしょうか、使用できるシフタは、LSL(論理左シフト)/LSR(論理右シフト)/ASR(算術右シフト)/ROR(右回転)/RRX(キャリー付き右回転)。RRXだけは特殊でシフト回数が1回に固定。従って、LDR R0,[R1,R2,RRX]とシフト回数を省略して書きます。
それぞれの例をポスト修飾で書いてみると次のようになります。
ちょっと書き方が美しくありませんが、機能としてはすっきりと整理されていることがわかります。
ちなみに、ポスト修飾のときに必ずライトバックするので、マシン語上でのライトバックビットが不要。そこで、ポスト修飾のときにこのビットがセットされると、LDRTあるいはLDRBTという特権命令になり、特権モードから(見えないはずの)ユーザーモードのレジスタをアクセスする命令になります。
また、LDRH、LDRSH、LDRSB、STRHは、最初のほうで触れたように本来のLDRとは別の命令で、アドレスモードが限定されており、オフセットの直値は8ビット、シフタは使えません。美しくないですね。
ARM7の演算命令は、RISCを標榜する他のプロセッサと同様、レジスタ間でしか働きませんが、結構、大げさな書き方をします。
DEST←SRC1 op SRC2 のように格納先、演算元をすべて指定できます。ADD DEST,SRC1,SRC2の形式で書きます。DEST、SRC1はレジスタを指定しますが、SRC2はもう少し柔軟に書けます。
演算の種類は、ADD(加算)、ADC(キャリー加算)、SUB(減算)、SBC(キャリー減算)、AND(論理積)、ORR(論理和)、EOR(排他的論理和)といった定番のほかにBIC(ビットクリア)、RSB(SRC2-SRC1の減算)、RSC(SRC2-SRC1のキャリー減算)があります。
DEST←SRCの転送先、転送元を書きます。演算の種類は、MOV(移動)、MVN(ビット反転して移動)の二種類。MOV DEST,SRCの形式で書きます。SRCは「柔軟な書き方」ができます。
見慣れないMVNは、後述のように直値の範囲が狭いのでそれを補うためでしょう。
SRC1 op SRC2のように演算元を指定します。演算結果を反映しないので格納先はありません。CMP SRC1,SRC2の形式で書きます。SRC2は「柔軟な書き方」ができます。
演算の種類は、TST(ANDの結果)、TEQ(EORの結果)、CMP(SUBの結果)、CMN(ADDの結果)。CMNはやはり直値の範囲が狭いのを補うためでしょう。TEQはよく使い方が判りません。比較の一致が取れますが、それならば、CMPで良いはず。マニュアルではVフラグが変わらないので便利と書いてあったのですが…
ここで大きな特徴は、「柔軟な書き方」です。LDR命令と同様にシフタを絡めることができます。大きく三つの機能があります。
直値は8ビットとと狭いのですが、2ビット単位でローテートした値を取れます。0xFFは当然可。0xFF0000も可。0x3FCも可。ちょっと面倒ですね。
よく使う-1(0xFFFFFFFF)は表せません。このため、MVN(ビット反転して移動)とか、CMN(加算比較)などのちょっと見慣れない命令が必要になったわけです。
ADD R0,R1,#1
と書けば、R0:=R1+1を表します。
これは、単純に任意のレジスタが取れます。
ADD R0,R1,R2
は、R0:=R1+R2
レジスタにシフタを作用させてかけます。まず、第一の書き方は、
ADD R0,R1,R2,LSL #2
の形式。この例ならば、R0:=R1+(R2<<2)となります。
第二の書き方は、
ADD R0,R1,R2,LSL R3
の形式。この例は、R0:=R1+(R2<<R3)を表します。要するにシフトカウントをレジスタで指定できるものです。
これらの二つの書き方では、シフタとして使えるのは、LSL(論理左シフト)、LSR(論理右シフト)、ASR(算術右シフト)、ROR(右回転)。
さらに、特殊なシフタとして、RRX(キャリー付き右回転)があります。これは、1ビットのシフトしかできないので、次のように書きます。
ADD R0,R1,R2,RRX
R0:=R1+(R2を1ビットキャリー付き右回転)となります。
演算をざっと眺めると、他のプロセッサでよく見かけるような命令が見つかりません。
まず、大きいのは、シフト関係の命令が無いこと。これは、シフタがあるので、MOV命令で書けます。LSLを単独で使うならば、MOV R0,R1,LSL #2などとすればよいわけです。キャリーに反映させる場合は、MOVS R0,R1,LSL #1。
他に用意されている言わばショートカット形の命令は、もともとの形式で書いても動作速度は変わらないので、不要です。
ありそうで、実装されていないのは、
などです。
RISCチップは往々にして乗除算などが実装されていません。ARMも例に漏れず、実装されていなかったのですが、ARM7TDMIでは乗算がCPU命令として実装されています。32ビット乗算、64ビット符号無し乗算、64ビット符号付乗算が、それぞれ、乗算と、積和の形で実装されています。マシンコードの隙間を使った汚い実装です。
チップによっては、外部の乗算器を使用するものもありますから、まぁ、それに比べればプログラムが書きやすいかもしれません。
除算は、実装されていません。おかげで、いまどき?除算処理を書く楽しみがあります。
ちょっと込み入った処理を書くと不便なのは、ロングシフト(例えば64ビット)がないこと。ちょっと込み入った書き方が必要になってしまいます。これもまた、プログラミングの楽しみではあるのですが…
SWIは、ARM7では、とっても汚い命令です。
SWI--ソフトウェアインタラプト--。主に、ユーザープログラムから、OSやモニターの機能を呼び出すために使います。ARM7では、SWI nの形で書き、任意の番号を付加して使用できます。
SWIを実行すると、例外が発生し、モードをスーパーバイザー・モードに切り替え、所定のベクターへ処理が移行します。モード切替のときに、簡単なコンテクストスイッチを行います。これは、レジスタセットの一部の切り替えと、ステータスレジスタの保存がその内容です。ここまでは、まぁ、よろしいのですが…
SWIの番号を得る方法が汚い。この番号は、マシン語の中に埋め込まれているのですが、これは、ベクターに分岐した時点で単純に得ることができません。とどのつまり、「呼び出したSWIのマシン語の中にある番号を読み取る」という原始的な方法をとります。LRレジスタに戻り先があるわけですから、その前の命令のアドレスを得て、マシン語から直接、番号を取る。
これだけでも十分汚いのですが、実は、ARM7にはTHUMBという16ビットのマシン語命令セットがあります。このセットからSWIを使うと、通常の32ビットの例外処理が呼ばれますが、当然、呼び出し側のコードは32ビットコードとは異なります。
従って、例外処理では、どちらのコードから呼び出されたかを、保存されたステータスレジスタを調べて判別し、32ビットコードなら戻りアドレスの4バイト前、16ビットコードならば、戻りアドレスの2バイト前からマシン語を読み、それぞれのコードに応じて番号を取り出すことになります。
定型的な処理なので、プログラムは簡単なのですが、やはり、美しくないと思いませんか?
いわゆるコールは、BLを使います。これは、LRに戻りアドレスをセットして、ブランチするというものです。ブランチする際には、B命令を使うのが普通ですが、レジスタ間接その他ちょっと手の込んだ分岐の場合には、LDR命令などで直接PCに書き込みます。
しからば、コールの際にも、この手を使うには、LRに戻りアドレスをセットしたから、PCに書けばよいということになります。
MOV LR,PC LDR PC,=JUMPLABEL
あるいは、
MOV LR,PC MOV PC,R0
などと書きます。
PCはARM7の特殊事情で、読み取ると実行中の位置+8を示します。1命令は4バイトですから、この時点で、次の次の命令の位置を指すわけです。最初の例ならば、MOV LR,PCでは、LRには、LDR PC,=JUMPLABELの次の命令の位置が入ることになり、結果的に適正な戻り番地がセットされます。
実は、これ、マニュアルには、(アーキテクチャに依存するので)PCの示す値を利用するな。と注意書きがあったので、避けていたのですが、別の箇所ではBLの代用として書いてありました。考えてみれば、LDR XX,=YYYYなどではPC+8をアセンブラが使用しているのですから、結局、使っているんですよね。
ということで、晴れて、使用できるということにしました。
ARM7は、組み込み用のCPUなのに、大げさな動作モードがあります。ちょっとした小さなハードに組み込むには煩雑すぎる気もするのですが、これを意識しないとプログラミングできません。そこで、これを小さなシステムで使用する観点から見てみます。モードの大半は例外(割り込みの類)に結びついていて、それ以外は二つだけ(SYSTEM/USER)です。
通常のプログラムを実行するモード。特権はない。
FIQ例外(FIQ割り込み)を実行する。
IRQ例外(IRQ割り込み)を実行する。
SWI例外(SWI命令)を実行する。起動時(リセット例外)はこのモード。
プリフェッチアボート例外/データアボート例外を実行する。小規模のシステムではこれが起きる場合はプログラムの誤りぐらい。通常は扱わなくても良い。
未定義命令例外を実行する。命令を拡張する場合などに使用するが、通常は使用する必要が無い。
通常のプログラムを実行する。特権を持つ。
ユーザーモード以外は特権を持ち、すべての命令の実行、メモリのアクセスなどができます。メモリアクセスはCPUによっては、特権で保護されるようです。ユーザーモードでは割り込み禁止ができません。これが、一番大きな制限。
整理して考えると、
割り込み部分以外のプログラム実行ですが、小さなシステムでは、アプリケーションコードがそのまま制御コードのようなもの、そこで割り込み禁止のできないユーザーモードは使いにくいです。すると、スーパーバイザー・モードか、システムモード。
リセットするとスーパーバイザーモードになるので、そのまま、プログラムを走らせるのも一つの方法。SWIを使わないならば、これでも十分です。
すっきりさせるならば、システムモードに切り替えてプログラムを実行。SWI(ソフトウェア割り込み)例外時にスーパーバイザー・モードとする。
面倒なのは、例外が起きると、コンテキスト切り替えが自動で行われ、スタックが、それぞれのスタックに切り替えられることです。つまり、モード毎に独自のスタックが必要となります(ユーザーモードとシステムモードは同じでよい)。このため、リセット時にプログラム中で使用する各モードを巡回してスタックの設定をする必要があります。
また、例外が起きるとステータスレジスタが、対応するモードのバックアップ用のレジスタに退避されます。例外の復帰にはこれを戻す必要があります。他のプロセッサならば、IRETなど専用命令が用意されているのですが、ARM7ではこれが無く、その代わりに、一般の命令が特殊な動作をするようになっています。
一例を挙げると、MOVS PC,R0。PCに代入する演算命令にS(フラグ反映)を指定するとステータスレジスタを復帰します。ADDやSUBなどでも同様ですし、LDMFD(LDMxx)などでもレジスタリストにPCがあると指定によってステータスレジスタの復帰がおこなれます。これは、とても汚いです。本当に、ARMのダークサイド。
例外で、もう一つ面倒なのは、ステータスレジスタの保存がスタックでなく、レジスタに行われること。このため、多重割り込みなどの際には、保存されたステータスレジスタをさらにスタックなどに保存する必要があります。
いずれにしても、小規模で使うには、ちょっと、大げさな仕組みでは無いでしょうか。最小限のお付き合いで済ませたいものです。
ARM7のマシン語コードは通常の32ビットコードの他に、命令長が16ビットのTHUMBコードがあります。これは一種の縮退コードで、CPUはこのコードをフェッチ後、内部で32ビットの通常のARMコードに展開してから実行するそうです。
このTHUMBは、命令長が短いのでコードサイズが小さくなる(はず)というメリットが一つ。さらに、バス幅が16ビットのROMを実装した場合などには、速度が速くなるメリットが一つ。16ビットバスでは、32ビットコードだと命令の取り出しに2回の読み出しが必要ですが、THHMBでは1回で済みます。とりわけ、外部FLASHなどは遅い上に16ビット幅で接続されることが多いので、この速度上のメリットはかなりなものです。
全部THUMBで組めれば、それなりに、よろしいのですが、割り込みや起動時などの例外処理は32ビットコードとなります。結局、システム関係は32ビットコードで書く必要があるので、THUMBと32ビットコードと、都合2種類の両方を扱う必要があります。
また、THUMBはコードサイズを詰めたためか、本来のARMのコードの面白い特徴がなくなっています。条件実行とか、シフタとかですね。使えるレジスタの数も減るし…プログラムを楽しもうという立場からは、魅力半減といったところです。コンパイラから使うならば、上のような特徴を踏まえて出力コードの選択をするだけなのですが…
親指サムが中に入っているARM7。一粒で二度おいしいというか、アセンブラの楽しみが二倍ある。と信じたい。
ARM7のハードウェア割り込みは、基本的には、FIQ(高速割り込み)とIRQの二つのみ。割り込み要因を解析して分岐する方法です。
IRQ割り込みがかかると、プロセッサモードを切り替えて、ステータスレジスタをセーブレジスタにコピー。さらに、レジスタ(LR,SP)を該当モードに切り替え、LRには戻り番地が設定されます。
FIQ割り込みの場合は、さらに多くのレジスタが切り替わるので、作業用のレジスタの保存も不要。
ここまで一気にしてくれるので、プログラムは楽なはず。
ですが、多重割り込みをする際には、これがアダになります。割り込みの退避情報がスタックではなく、レジスタにされるため、IRQ中にIRQがかかると、退避情報の上書きが行われてしまうため、これを割り込みの入り口で退避する必要があります。
よほど天邪鬼で無い限り、LRはプッシュされているので、よしとして、ステータスのセーブレジスタもプッシュする必要があります。もちろん、戻る前にセーブレジスタは回復しなければなりません。
割り込み処理でもっと面倒なのは、複数の割り込み要因の解析。このために、割り込みコントローラが搭載されることが多いのですが、ARM7では、これはコアの外の部分。つまり、使用するCPUによって大きく異なります。割り込み要因をどこまで保持するか、多重割り込みの際には、割り込みのマスクをいつ解除するか。結局、CPU毎に異なるので一般的な説明ができません。
このあたりが、ARM7の面倒なところです。同じARM7でもCPUが異なるとこの辺の手法が変わってしまうので、結局、目を皿のようにしてマニュアルを読み、(大抵はわかりにくい記述なので)実験をする必要があります。IO関係も同様。結局のところ、過去の経験がそのままでは使えません。同一シリーズのものを使えばいいんでしょうが…
PICとか、AVRは、この辺り、当然ながら統一されているので、チップが変わってもそれほどの変更は無く、せいぜい初期化が多少変わるぐらい。場合によるとソース・プログラムのほとんどが流用できたりします。この点もPICが愛される理由かもしれません。
アマチュアの工作では、なかなか、ARM7は使いにくいです。入手も容易でないし、カニカニの表面実装だから扱いにくい。基板つきのものは、それなりによいお値段。
実際、プログラムを書いていると、部品のように使うならともかく、ちょっと、まとまったものでは、PICやAVRで役に足りないのを無理矢理使ったりします。ニーズはあるんですよね。ある程度の汎用CPU。
FLASH_ROM+RAMで、外部バス無し。IO関係がちょっとあれば、ほとんど用が足りる。組み込みでARM7を使った経験でも、ピンが余ってます。未使用ピンの対策ばかりしていたりして。
クロックもそれほど柔軟な必要は無い。IOは数よりもドライブ能力が高い方がいい。PICやAVRのように少ピン多品種で揃えて、そこからチョイスするほうが無駄が無い。コア以外の部分はシンプルにして共通化する。
こうすると、多くても40ピン。18ピンでも余裕でしょう。8ピンでもI2Cなどのシリアルラインがあれば実用的に使える。DIPで充分じゃん!
DIPのARMがあればいい。