Propeller日記 (1)
長嶋 洋一
Propeller日記(2)
2008年2月9日(土)
自宅でただ1誌だけ定期購読している「トランジスタ技術」の3月号発売日。 いつものようにサッと読み流して、研究室に持って行って1ヶ月後には広告を取り外して本棚へ・・・という筈が、 1つの記事に釘付けに。ワンチップ・マイコン探訪・・・なんじゃこれは(^_^;)。 とりあえず、翌2/10、2/11は一般入試で忙しいので、週明けに読んでみる事にする。
8個の32ビットCPUが順番に処理をするマルチ・プロセッサ・マイコンPropeller P8X32 :桑野 雅彦2008年2月11日(月)
祝日で何も邪魔が入らないので、この記事をじっくり読んでみる。 これは凄い、というか、変態的・芸術的ですらある。 マルチタスクのために内部にCPUコアを8個並べて、強制的にタイムスライスするとは、 これは参った。さすがアメリカだぁ。2008年2月12日(火)
トラ技の記事にあった日本代理店のサイトから、 Propellerを開発したParallax社は、 Basic Stampを開発したところと判明。 なるほどなぁ。2008年2月14日(木)
翌2/15、2/16と出張の予定なので、 トラ技の記事にあったPropellerのページから、 とりあえず これと これとを印刷して、 新幹線の中で熟読することにした。2008年2月18日(月)
トラ技の紹介記事は、Propellerのサイトにある情報を翻訳した程度であり、回路図も参考例も、 全てサイトにあるサンプルである事が判った(^_^;)。 ・・・しかしPropeller CPUは、気に入った。これは天晴れである。 さっそく、Propellerのページから、 スターターキット、 Propeller CPUを10個、 USB書き込みクリップ、 試作用ボード、を注文した。 通常便(1週間程度)での発送としたので、22日-25日の沖縄旅行から帰ってきた頃には到着の予定である。2008年2月19日(火)
ブツを発注してしまったので、Windowsパソコンに開発ツールをとりあえずダウンロードしてインストール、 あとはPropellerのページから、 とりあえず これと これと これと これと これと これと これと これと これとを印刷してみた。 実際に読むのは、ハードが来てからということで。 さらに、主だったサンプルプログラム集も集めてみた。 登場してまだ2年ほどのPropellerは、ダウンロード数から、どうも世界中でコアなユーザは1500人ほどの模様。(^_^;)2008年2月26日(火)
沖縄旅行 から帰った翌日の晩に、遂にブツが到着した。インターネットで何でも買える時代、代理店も不要だなぁ。 アメリカからの送料はアメリカ域内だけのようで、日本の郵便会社に200円、さらに関税で800円を取られた。2008年2月27日(水)
アメリカから届いたものを並べてみた。さすがネットショッピング先進国、しっかりしている。付録のCDROMをHDDにコピーしようとしたら、何故か認識できない。Macでも駄目。 もう1台のWindowsノートでは認識したので、それでコピーCD-Rを作成して、読み込んだ。 でも中身はBasicStampなど、この会社の全製品のカタログとドキュメント類、さらにWinZIPからAcrobatリーダーまで 入っているものの、Propellerツールのバージョンはネットの最新よりも古く、とりたてて目新しいものは何も無かった。 もうCDROMの同梱すら不要な時代なのか。
2008年2月28日(木)
デモキットを開けてみた。パソコンとはたぶんUSBで繋ぐのだろうと繋いで、とりあえずツールを立ち上げ、 LEDを点滅させる、というサンプルプログラムのポートをボード上のLEDに変更して、「Run」でRAMに転送、 というのをしてみたら、イキナリ、動いた。(^_^;)転送先をEPROMにしてロードして、転送後にUSBを外してリセット起動したら、ちゃんとそっちのプログラムで動く。 これはAKI-H8よりも、ずっと簡単だぁ。(^_^)
2008年2月29日(金)
Propellerの開発ツールで気になっていたのは、フォントが重なったり切れたりすること。 テキストのサイズを変更するメニューはあるものの、フォントを切り替えるメニューは無い。いちいち秀丸エディタで別に開くのもナンだしなぁ。 ・・・ということで、せっかくなのでPropellerのメーカ、Parallax社に質問のメイルを出してみた。
* Operating system Microsoft Windows XP Home Edition Version 2002 Service Pack 2 * PC type (desktop or laptop) Desktop Acer Incorporated AMD Athlon 64 Processor 3500+ 2.21GHz、1.94GB RAM * Software running Propeller Tool * Version number Virsion 1.06 P8X (c)2004-2008 I have just bought your Propeller chips, Starter Kit, etc. And I have just started learning. PC-Demo board connection is good, demo program is well downloaded, and test program works well ! Hardware and interface are very smart !! TROUBLE and QUESTION Please watch the attached JPEG - screenshot of my PC. I cannot read by the font of the source code coming in succession in ugliness. I discovered the function to change the text size of the area of the source code in the menu of this tool. However, I was not able to discover the function to change the font type of the text. My OS is Japanese Windows XP environment. I think that this is one of the cause of troubles. Please teach the method of changing the font of the text of the source code in this tool.・・・すると、タイミングが良かったのか、出して数分でリプライのメイルが来た。(^_^;)
Yoichi Nagashima, That particular font is special for the Propeller tool; which is a fixed pitch font. This isn't something that can be changed because of the format of the Propeller Tool; it's something that is being spoken about in the forums, but there isn't a fix for it yet but you can monitor for the latest update from our engineers. Parallax Forums [ Propeller ]: http://forums.parallax.com/forums/default.aspx?f=25 Supporting Staff, Joshua Donelson [ Technical Support ]なかなか素早く、キチンとした対応に感心した。 固定フォントをアプリケーションが指定しているので変更できない・・・うーーーむ。 これはやっぱり、別ウインドウに秀丸エディタを開いて、コピペの往復ということかぁ。 そして、このフォーラムを見てみると、過去に5000本ほどの記事があった。 こりゃ大変だぁ。(^_^;)
・・・と捜すこと数分。 フォーラムの中にこんな情報を発見。
さっそくやってみた。コントロールパネルの「地域と言語」を、日本語から英語に変更。見た目はまったく変化ナシ。 ただし、開発環境を起動してみたところ、見事に綺麗になって、問題解決。困ったらFAQ、さすがだ。(^_^)
これで環境の障害は無くなった。あとはいろいろ、やってみるだけ。 とりあえず、 Propeller Manual を順にナナメ読みしてみることにした。 Chapter 1は、既にプリントアウトで読んでいたIntroductionだったので、パス。
Chapter 2は、Propeller Toolsの解説なので、ちょっときちんと読んでみた。 まず最初に、Parallax社のエンジニアはこれまで20年以上、色々なCPU開発環境を使ってきたので、 その経験と不満からこのツールを作った、と自画自賛していた(^_^;)。 その結論として、CPUのソースコードはもちろんplain textだが、さらにコードの注釈だけでなく、 そのプログラムの解説のDocumentもソース中に記述するようになっていた。
面白いのは、Propellerの内部マスクROMに格納されているフォントテーブルだ。 これは並べ方により以下の3種類のように表示されるが、回路図を構成できるパーツが装備されている。
そして、この特製のフォントセットは、Propeller Toolsをインストールして最初に起動した時に、 コンピュータのTrue Typeフォントとして、普通のテキストエディタでも利用できるようになる。 つまり、ソースコードに記述できるドキュメントとして、文字情報だけでなく、 Propellerや周辺回路の回路図まで記述できてしまうのだった。 これは素晴らしい。
Propeller Toolsは、ソフトウェア開発用のIDEとしては、まぁこんなものだろうという立派なもの。 説明がなくても大体のところは予想通りだが、よく出来ている。 内部のEditorサブウインドウ(Editor Paneと呼ぶらしい)において、 複数のソースをいくつも開いて比べたりコピペするための機能としては、
などで、色々にサポートしてくれる。これはソフトウェア開発をした事のある者には判る、嬉しい機能だ。
- どんどん開けばとりあえずタブウインドウとしてタブが並ぶ
- タブをPain内にドラッグするとそタブのウインドウが2画面目として上下に分割
- タブをデスクトップにドラッグするとそのタブの新しいウインドウが開く
エディタの機能としては、これまでにあったものの「良いトコ取り」になっていて、 たいていのことが出来る。ブロック指定も簡単、ショートカットも豊富。 慣れれば慣れるほどに効率が上がりそうなエディタ、というところだろう。
2008年3月3日(月)
いよいよChapter 3は、Propeller Programming Tutorialということで、ここが本番である。 後のChapter 4はSpin言語の、Chapter 5はアセンブリ言語のReferenceなので、 分厚いマニュアルのこの60ページほどがPropellerの「肝」である。新しいCPU/システムを普及させたいメーカとしては、このスターターキットとマニュアルにより、 ユーザの心をグッと掴むことが大切だ。 きちんと理解して使うためには当然のことだが、Chapter 3の冒頭では、 ある程度のプログラミングのスキル、そしてChapter 1とChapter 2をちゃんと読んでいる事、 という条件を付けている。 その一方で、これまでの古い概念にとらわれず、Propellerの新しい考え方をマスターして欲しい、 とも要請している。 まったく新しいコンセプトのPropellerならでは、の自負と気合いも感じられた。
Propellerの開発言語には、Spinというオブジェクト指向の高級言語と、ハードに即したPropeller Assembly言語、 との2つがあり、混在してプログラムを記述することが出来る。 過去のCとアセンブラの混在の場合、Cコンパイラはアセンブラソースを出力して両者が融合したが、 Spin言語がツールでコンパイルされた結果のトークン(バイナリ中間言語)は、 実行時にPropellerの内部のインタプリタで解釈されて実行される。 また、Propeller Assembly言語では、多くのSpin言語と同等の動作を記述できる。 従来の高級言語ではちょっと無理であるが、Spin言語では、アセンブラと同様に、 19200bpsのシリアルデータを直接ハンドリングするぐらいの処理は出来るのだそうだ。 Objectsと呼ばれるPropellerソースコード(***.spin)は、Spin言語のみで、 あるいはSpin言語とPropeller Assembly言語を混在して記述できる。 全ての処理をPropeller Assembly言語で記述することも可能だが、 最終的にプログラムを実行させるために、最低限、たった2行ではあるがSpinコードが必要である。
Propellerはオブジェクト指向の開発環境であるために、多くの完成されたオブジェクトを「ソフトウェア部品」として再利用できる。 多くの複雑な処理の融合体であるプログラムの場合には、マルチタスキングの部分に開発の重点が置かれるが、 Propellerの場合には処理をそれぞれ内部の8個のCPUに本質的に振り分けるので、 うまく理解できれば、かなり効率の良い、そして信頼性の高いソフトウェア開発ができると思われる。 各部のセンサとアクチュエータに分割されたジョブの集合体であるロボットなどには向いているだろう。 あるObjectファイルは他のObjectファイルをインクルードできるので、ソフトウェア部品のObjectについては、 具体的に内部は何も知らなくてもよさそうである。 最上位(プロジェクトそのものを表わす)のObjectをTop Object Fileと呼ぶ。
マニュアルの図で説明すると、 Propellerのプログラムは、Propellerチップ内の共有RAM領域にダウンロードされることで実行される。
あるいは、Propeller Toolで「EEPROMにダウンロード」と指示すれば、このチップ内の共有RAM領域のプログラムは、 さらにPropellerチップに外付けされた32KBのEEPROMにプログラムされる。 この状態でホストとの接続を切れば、リセット時にPropellerはEEPORMを捜して、 そのプログラムで動作を開始する。
ホストPCとPropellerとの接続に最低限必要なのは、Propeller PLUGという小型のUSB接続アダプタを使えば、 以下のような構成で住むことになる。 これは秋月電子のAKI-H8よりもさらに洗練されていて、手軽な開発環境へのこだわりを感じた。
Propeller PLUGの回路は以下のようなものである。Propeller Demo Boardにはもちろん、 この回路も載っている。 Propeller Demo Boardの全回路図は これである。
チュートリアルではここから、以下のような外付け回路を要請しているが、 実はこれは、スターターキットに入っているPropeller Demo Boardには実装されている。 つまり、スターターキットでは、ハードに何もしないでチュートリアルに入れるのである。 新しいCPUに接する時には、ハードとソフトの両方といきなり取り組むことになり、 秋月電子でもここが一つの壁だったのだが、さすが、というところだ。
さて、Exercise 1のページとなった。 何も言わず、黙ってPropeller Toolに以下を打ち込め、とのことである(^_^;)。 ただし、「PUBの行は左端から空けずに書け」「インデントは重要なので注意せよ」とのことである。 CやJavaやHTMLでは、改行もインデントもスペースも任意だったので、ちょっと戸惑う。 何がしかプログラミングをしてきた者であれば、このプログラムの動作はほぼ予想できるだろう。 PUBと全て大文字にしたのは、マニュアルのチュートリアルで理解のためであり、 大文字小文字については区別しないのだという。これにはちょっと安心した。
ここでF10、あるいはメニューでRunからCompile Current、さらにLoad RAMとすると、ほんの一瞬、 以下のようなダイアログが現れて、Propeller Demo Board上の赤と青のLEDが点滅して、 そしてP16のLEDが見事に点滅するようになった。
この一瞬の間にPropeller Toolが行った処理は以下である。
- ソースコードをコンパイルしてPropellerアプリケーションとした
- USBを経由してPropellerチップを捜した
- Propellerチップの内部RAMにこのプログラムを転送した
- Propellerチップにリセットをかけてこのプログラムを実行させた
- プログラムによって、P16の出力が点滅を繰り返した
ここで、Propeller Demo Board上にあるリセットボタンを押すと、もうこのプログラムは走らない。 何故なら、リセットによってRAMの中身は消えてしまうからである。 ところが、F10でなくF11を押すと、今度はダイアログがもっと長く表示され、最終的には同じ動作となった。 そしてPropeller Demo Board上にあるリセットボタンを押すと、再度、これが走った。 この違いは、F11はメニューでRunからCompile Current、さらにLoad EPROM、という処理だからである。 RAMに転送後、さらにこのプログラムは外付けEEPROMに書き込まれたので、リセット後、 ホストPCとはもはや通信していないので、次にEEPROMをサーチして、このプログラムを実行したのである。 AKI-H8などは、デバッグで何百回何千回とEEPROMに上書きするとやや心配があるが、 開発時には内部RAMに何度でも書き込みして、バグが取れたらEEPROMに、というのは嬉しい機能である。
実際に動いたところで、チュートリアルでは、ようやく以下のプログラムの詳しい解説が続いている。
PUB Toggle dira[16]~~ repeat !outa[16] waitcnt(3_000_000 + cnt)1行目の「PUB Toggle」は、Toggleという名前のpublicなメソッドの定義、ということである。 Toggleというのは任意の名称であるが、publicのPUBというタームは、全てのObjectsに必須のものである。
2行目の「dira[16]~~」によって、ポート16のdirection(方向)を、出力ポートに設定している。 DIRAというシンボルは、P0からP31までの32個のポートを入力にするか出力にするか、のdirectionレジスタを設定する。 「~~」という演算子は、Post-Set演算なので、「dira[16] := 1」と等価である。
3行目の「repeat」によって、それ以降のブロックを無限ループさせる。 このREPEATも予約シンボルである。 Cとかの感覚では、ループの最後を指定しないのがなんとも不安定であるが、 これがオブジェクト指向なんだろうか。(^_^;)
4行目の「!outa[16]」によって、ポート16の出力状態を反転する、 というトグル動作を実現する。ビット反転の「!」は、他の言語にもよくある演算子である。 OUTAというシンボルは、P0からP31までの32個のポートに対して、出力に設定されたポートの出力レジスタの値を設定する。
最後の行の「waitcnt(3_000_000 + cnt)」は、300万クロックサイクルのwaitである。 WAITCNTシンボルは「システムカウンタのwait」であり、CNTシンボルは、(現在の)システムカウンタレジスタの値である。 そこでこの命令は、「現在のシステムカウンタの値に300万カウント加算した値になるまでwait」という事になる。 defaultのPropellerチップのシステムクロックは12MHzなので、これを300万発カウントすると、およそ1/4秒となる。
最後にまとめとして「Propeller Toolではインデントをきちんと認識するので注意」と念押ししている。 IFとかCASEとかREPEATについては、インデントによって認識する、というところに留意が必要である。 スペースでもタブでも、最小では「スペース1個以上」がインデントの基準であるらしい。 混乱を避けるためには、インデントにスペースとタブを混在させない方がよさそうだ。
2008年3月4日(火)
Ex.1のまとめ(Quick Review)に続いて、次のトピックは、Cogs (Processors)である。 Propellerには8個の独立したCog(歯車)があり、これが最大の特長である。 それぞれのCogは違いに独立した動作も、あるいは協調動作も可能であり、実行中にこのモードを切り替えることもできる。 ところで、上のExercise 1でのサンプルプログラムでは、8個のうちのどのCogであるか、 はまったく意識していなかった。実は、8個のうちのCog(0)が、defaultのBoot-Upで使用されるプロセッサである。 以下のように、Propellerに電源が入ると、PCとの通信が検出されなかった場合、 内部のブートローダは、Propeller内蔵ROMからCog(0)の内部RAMに転送される。 そしてCog(0)で実行されるブートローダによって、外部EEPROMのプログラムをPropeller内部のメイン(共通)RAMに転送する。 その後、ブートローダはCog(0)の内部RAMに、Propeller内蔵ROMからSpinインタプリタを上書きロードする。 そして、Cog(0)のSpinインタプリタは、Propeller内部のメイン(共通)RAMにロードされたプログラムを実行していく。 ここまでの間、他の7つのCogは何もせず待機状態にある。
さて、続いてExercise 2のページとなった。 テーマはConstantsである。プログラムは以下のようになった。
CON Pin = 16 Delay = 3_000_000 PUB Toggle dira[Pin]~~ repeat !outa[Pin] waitcnt(Delay + cnt)動作はEx.1のものと同じだが、定数を使用することで、複数の場所に登場するI/Oポート番号とかディレイの時間を、 定数として定義することにより、効率的に指定できることになる。 複数のCogsを使い分けるためにもこれは必須となるだろう。
ここで登場したCONブロックは、global定数のための予約語である。 シンボルとして登場したPinとかDelayについては、予約語と重ならなければ、 なんでも定義できるようである。 このプログラムにより、他の場所でPinとかDelayというシンボルが使われれば、 その値は書き換えられない限り、16とか3,000,000となる。 なお文法上、大きなケタの数字を表わすために人間が日常的に使う「カンマ」は使えないので、 3ケタずつ区切るために「_」が使用できる。これは数字としては無視される。
ここまでに登場したブロック指定の予約語は、PUBとCONであるが、 他にも以下のようなものがある。 VARはglobalな変数、OBJはオブジェクトの参照、PRIはプライベートなメソッド、DATはデータブロックである。
CON Global Constant Block. Defines symbolic constants that can be used anywhere within the object (and in some cases outside the object) wherever numeric values are allowed. VAR Global Variable Block. Defines symbolic variables that can be used anywhere within the object wherever variables are allowed. OBJ Object Reference Block. Defines symbolic references to other existing objects. These are used to access those objects and the methods and constants within them. PUB Public Method Block. Public methods are code routines that are accessible both inside and outside the object. Public routines provide the interface to the object; the way methods outside of the object interact with the object. There must be at least one PUB declaration in every object. PRI Private Method Block. Private methods are code routines that are accessible only inside the object. Since they are not visible from the outside, they provide a level of encapsulation to protect critical elements within the object and help maintain the integrity of the object’s purpose. DAT Data Block. Defines data tables, memory buffers, and Propeller Assembly code. The data block’s data can be assigned symbolic names and can be accessed via Spin code and assembly code.Propellerオブジェクト(プログラム)としては、最低限、1つ以上のPUBブロックを持つ必要がある。 全てのブロックはどんな順序で置かれてもよいが、CONとVARとOBJとDATは1回だけ置くことができ、 PUBとPRIブロックはいくつでも置くことができる。 また、複数のPUBブロックが置かれた場合、一番最初にあるPUBには特別な意味があり、 Propellerプログラムのスタートポイントとなる。C言語のmain()みたいなものであろう。 デフォルトのPropeller Toolの編集画面では、これらのブロックを背景色の違いで区別できる。
次はExercise 3のページである。 テーマはコメント(注釈)である。プログラムは以下であり、中身はまったく同じである。
{{Output.spin Toggles Pin with Delay clock cycles of high/low time.}} CON Pin = 16 { I/O pin to toggle on/off } Delay = 3_000_000 { On/Off Delay, in clock cycles} PUB Toggle ''Toggle Pin forever {Toggles I/O pin given by Pin and waits Delay system clock cycles in between each toggle.} dira[Pin]~~ 'Set I/O pin to output direction repeat 'Repeat following endlessly !outa[Pin] ' Toggle I/O Pin waitcnt(Delay + cnt) ' Wait for Delay cycles要するに、シングルラインとマルチライン、コード部分とドキュメント部分、 という2*2=4種類ごとに、以下のようにコメント記号が用意されている。
- アポストロフィが1個ならシングルラインのコード部分の注釈
- アポストロフィが2個ならシングルラインのドキュメント部分の注釈
- 中カッコが1個のペアならマルチラインのコード部分の注釈
- 中カッコが2個のペアならマルチラインのドキュメント部分の注釈
引き続き、Exercise 4に進む。 ようやくここで、メソッドに引き数を渡したり、 有限回のループが登場して、プログラムらしくなってきた。
PUB Main Toggle(16, 3_000_000, 10) Toggle(17, 2_000_000, 20) PUB Toggle(Pin, Delay, Count) dira[Pin]~~ repeat Count !outa[Pin] waitcnt(Delay + cnt)この例では、最初に登場するPUBであるMainが全体のメインプログラムとなる。 そして、Toggleというメソッドでは、PinとDelayとCountというシンボルを引き数として、 それぞれPinでポートを指定して、Delayの間隔で点滅し、Countの回数で繰り返しを終了する。 同じたった1つのメソッドを、呼び出し側からパラメータを変えて指定できるというわけである。 Propellerの内部の8個のCPUは32ビットCPUなので、それぞれのシンボルで与えた定数は、 ワードでなくロング(32ビット幅、4バイト幅)となる。 REPEAT命令が繰り返し回数というパラメータを持つのは、 いちいちループカウンタの変数をチェックしなくて済むので、ちょっと嬉しい機能である。
2008年3月5日(水)
いよいよExercise 5、トピックはPropellerならではのParallel Processingである。 Exercise 4のプログラムでは、まず「Toggle(16, 3_000_000, 10)」が実行されてポート16のLEDが5回点滅し、 続いて「Toggle(17, 2_000_000, 20)」が実行されてポート17のLEDが10回点滅した。 これは逐次処理、すなわちserial processingである。普通のCPUで2個のLEDをそれぞれ別々のタイミングで点滅させるには、割り込み、 あるいはソフトウェア的にマルチタスクとして、時分割並列処理を実現する必要がある。 しかしPropellerでは、入出力ポートはそれぞれのCogに排他的に接続できるので、 以下のような簡単なプログラムで、それぞれのポートのLEDを点滅させるマルチタスクが簡単に実現できる。
{{Output.spin Toggle two pins, one after another simultaneously.}} VAR long Stack[9] 'Stack space for new cog PUB Main cognew(Toggle(16, 3_000_000, 10), @Stack) 'Toggle P16 ten… Toggle(17, 2_000_000, 20) 'Toggle P17 twenty… PUB Toggle(Pin, Delay, Count) {{Toggle Pin, Count times with Delay clock cycles in between.}} dira[Pin]~~ 'Set I/O pin to output direction repeat Count 'Repeat for Count iterations !outa[Pin] 'Toggle I/O Pin waitcnt(Delay + cnt) 'Wait for Delay cyclesこのプログラムでは、それぞれのLEDの点滅処理のために、2つのCogを起動している。 新しく登場したのはVARブロックであり、とりあえず9Longs(ByteでもWordでもなく32ビット幅なのでLongという単位)の、 Stackというシンボルの変数を確保している。これがMainメソッドで使用される。
Mainという名の一番最初に置かれたPUBブロックでは、「COGNEW」という新しいコマンドが登場した。 COGNEWが第1パラメータとして指定するのは、SpinまたはPropellerアセンブラのコードであり、 ここではToggleというメソッドである。 COGNEWが第2パラメータとして指定している「@Stack」は、 新しく起動されるCogがrunning stack spaceとして使うRAMのアドレスを示している。 「@」は、それに続くシンボルのスタック実アドレスを返す。 何故、ここで9Longs(36バイト)のスタックを確保したのか、については後で紹介するとされているが、 確かにPropeller ToolのObject Info画面では、9Longsが確保されているのが判る。 スタックには、テンポラリ情報としてリターンアドレス、リターン値、中間データなどが入るため、 実行されるSpinコードにより変わってくるらしい。
このプログラムを実行させると、まずCog(0)は「cognew(Toggle(16, 3_000_000, 10), @Stack)」を実行し、 これは次のCog(1)に「Toggle(16, 3_000_000, 10)」を実行させることで、P16のLEDの点滅が開始する。 Cog(0)は引き続き、「Toggle(17, 2_000_000, 20)」を自分のプログラムとして実行するので、 P17のLEDの点滅が開始する。これらの動作は同時に並列処理として進行する。 そしてCog(1)は、REPEAT回数によってToggleの処理が終了すると、その次にもうコードが記述されていないので、 動作を終了し停止する。 Cog(0)は、REPEAT回数によってToggleの処理が終了するとMainに戻るが、もう次のコードが無いので停止する。 どちらが先に終わるか、というのは関係ない。 このメカニズムは以下の図のように解説されている。
Propellerはハードウェア的なマルチタスクCPUであるが、開発環境のSpinはオブジェクト指向の概念を持つ。 Exercise 6では、いよいよオブジェクト指向のプログラミングが主役となってくる。 次のサンプル「Output.spin」は、シンボルをMainからStartと変更した、これ自体では何も動作しないオブジェクトである。
{{ Output.spin }} VAR long Stack[9] 'Stack space for new cog PUB Start(Pin, Delay, Count) {{Start new toggling process in a new cog.}} cognew(Toggle(Pin, Delay, Count), @Stack) PUB Toggle(Pin, Delay, Count) {{Toggle Pin, Count times with Delay clock cycles in between.}} dira[Pin]~~ 'Set I/O pin to output direction repeat Count 'Repeat for Count iterations !outa[Pin] 'Toggle I/O Pin waitcnt(Delay + cnt) 'Wait for Delay cyclesこれ自体は、Startというメソッドが呼ばれると、新しいCogのプログラムを起動する、 というオブジェクトであるが、これ自体では引き数が渡されないので、何も起きない。 まずはこのOutput.spinを置いておいて、実際のプログラムについては、 以下のようにMainからStartメソッドとToggleメソッドを呼ぶBlinker1.spinを作る。 複数のプログラムの連携(参照関係)となるので、ここで定義された「Output.spin」という名前は重要となる。
{{ Blinker1.spin }} OBJ LED : "Output" PUB Main LED.Start(16, 3_000_000, 10) LED.Toggle(17, 2_000_000, 20)このBlinker1.spinをコンパイルしてRAMにロードすると、Exercise 5とまったく同じ動作が実現できる。 新しく登場したOBJブロックの「LED : "Output"」は、外部のソースプログラムOutput.spinの中のメソッドを、 Mainから「LED」というシンボルで参照することを意味する。 このような継承関係はJavaやActionScriptと同様にドットで表現するので、 「LED.Start(16, 3_000_000, 10)」によって、Output.spinの中のStartメソッドが、 「LED.Toggle(17, 2_000_000, 20)」によって、Output.spinの中のToggleメソッドが呼ばれる。 この「object.method」という呼び出し方は、まさにオブジェクト指向のスタイルである。
このようにオブジェクトを参照した構造のプログラムをコンパイルすると、 Propeller Toolの左上のウインドウに以下のようにその関係が表示される。 これはプログラムのコンパイルが成功した時にのみ更新されるので、 開発中は常にここをチェックしていくことが大切である。
複数のオブジェクトを同時に開いて開発していく上で、Top Objectがどれであるか、という区別は重要となる。 この例では、エディタのタブがBlinker1.spinでなくOutput.spinにある時に、 F10で「このCurrentソースをコンパイル」と指定すると、Output.spinはTopではないので、 結果として何も実行されない。 このような混乱を避けるために、TopにあるObjectを明示的にメニューの「Top Object File」で指定して、 F11の「Topからコンパイル」を選ぶ、という作法を身に付けることも有益である。 また、下の階層や横並びのオブジェクト群は、Propeller ToolでOpenする必要はなく、 Topからコンパイルする時に、参照されていれば自動的にコンパイルされて、 全体として一つのプログラムを構成してくれる。
2008年3月6日(木)
Exercise 6のCogsの並列処理とオブジェクト指向の関係は、確かにマルチタスクではあるものの、 完全な「並列」処理ではなかった。 というのも、Cog(0)に優位性があり、あくまでCog(0)のプログラムが、 たまたまCOGNEWコマンドによって、もう1つのCog(1)のプログラムを呼び出した、 という関係だからである。 Exercise 7では、より一般的に、複数のオブジェクトを複数のCogsと結び付けるように展開する。まず最初に、以下のようにOutput.spinを拡張する。 別プロセスとしてToggleを開始するStartメソッドに対応して、 Activeなプロセスを停止させるStopメソッドを設ける。 また、Toggleメソッドのパラメータとして無限ループも実現できるようにする。 プロセスをStopするには、そのプロセスが現在走っているかどうかのチェックが必要なので、 このためのActiveメソッドも新設する。
{{ Output.spin }} VAR long Stack[9] 'Stack space for new cog byte Cog 'Hold ID of cog in use, if any PUB Start(Pin, Delay, Count): Success {{Start new blinking process in new cog; return TRUE if successful}} Stop Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) PUB Stop {{Stop toggling process, if any.}} if Cog cogstop(Cog~ - 1) PUB Active: YesNo {{Return TRUE if process is active, FALSE otherwise.}} YesNo := Cog > 0 PUB Toggle(Pin, Delay, Count) {{Toggle Pin, Count times with Delay clock cycles in between. If Count = 0, toggle Pin forever.}} dira[Pin]~~ 'Set I/O pin to output… repeat 'Repeat the following !outa[Pin] 'Toggle I/O Pin waitcnt(Delay + cnt) 'Wait for Delay cycles while Count := --Count #> -1 'While not 0 (make min -1) Cog~ 'Clear Cog ID variableまずVARブロックには、バイトサイズの「Cog」という変数を新たに定義した。 これはStartメソッドで起動されるCogのIDを格納・追跡するためのものである。 VARブロックで定義されたStackとCogは、いずれもOutputオブジェクトの中のPUBおよびPRIブロックの中で、 global変数として使われる。 つまり、あるメソッドによってこのglobal変数が更新された場合、他のメソッドはそれぞれ参照した時にその新しい値を得る。
Startメソッドでは、(Pin, Delay, Count)と引き数を定義したメソッドのシンボル定義部分にコロンでSuccessというシンボルを続けて、 「PUB Start(Pin, Delay, Count): Success」と記述した。 これは、起動処理が成功したかどうか、という結果を返り値Successとして定義する、という意味である。 Propellerには8個という有限のCPU数の制限があるので、Startメソッドは呼ばれたからといって必ず成功するとは限らない。 そこで返り値Successには、定番のTRUEないしFALSEというブーリアン値が入ることになる。 ちなみにPUBおよびPRIメソッドは、ここでのように定義されていてもいなくても、それぞれ常にロング値(4バイト)を返す。 つまりここで定義したSuccessという値は、 defaultでメソッドに定義されているRESULTという返り値の変数のエイリアスということになる。
Startメソッドには2行のコードが並んでいる。 まず最初にStopメソッドが呼ばれて、もし存在して実行されているプロセスがあればそれを停止させる。 これは、外部のオブジェクトから別途いちいちStopさせずに、このStartメソッドを呼ぶだけで、 もし事前に存在していたプロセスがあればこれを必ず停止させた上で、新しいプロセスを起動するためのものである。 事前にプロセスが無かった場合には、新しいCogが起動され、 過去に他のCogの起動のためにStackとして定義されたワークスペースの変数を、この起動のために上書きする。
最初が肝心なので、ここでこのStartメソッドの2行目、
Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) という複雑な行について、Propeller Toolの理解のために、しっかり見ていくことにする。 カッコの対応関係は、内側からペアを確認すればわかりやすい。 3つのパラメータ : Pin、Delay、Countを指定するメソッドToggleがまず、一番内側にある。
Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) このToggleを、スタックエリア「@Stack」の指定とともに、新しいCogに起動するのが、COGNEWコマンドである。
Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) このCOGNEWコマンドは、新しいCogの起動に成功すると、内部的に定義された0から7までのCog IDを返し、 失敗した場合には-1の値を返す。 他の言語では「=」であることが多いが、Propellerで定義されている代入演算子は「:=」である。 これにより、返り値に+1して、VARブロックで定義したCog ID変数Cogに代入するというのが、
Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) であり、この値は
となる。必要があれば、この値からCogを指定してストップすることができる変数である。 この値は最終的に、メソッドの返り値Successに代入される。
- 成功した場合には1から8までの値
- 失敗した場合にはゼロ
Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) Startの返り値SuccessはBooleanだ、としたが、正確にはSpin言語ではブーリアンの値は、FALSEがゼロで、TRUEが-1である。 しかし、実際に結果のブーリアンを比較演算する場合、コンペア演算はゼロをFALSE、non-zeroをTRUEと判定するので、 ここで+1したことで、成功すればゼロ以外(1-8)、失敗すればゼロ、という結果と矛盾しないのである。
なお、このように複雑なステートメントでは、Propeller Toolのエディタ画面では、 カーソルが置かれた位置(★)に対応して、その階層が含まれたカッコが強調表示されるようになっている。
Startメソッドに続いて、Stopメソッドもたった2行のステートメントである。 「if Cog」という行の意味は、もし変数CogがTRUEだったら(non-zeroだったら)、次の行を実行せよ、である。 変数Cogの値は、プロセスの起動に成功すれば(内部的Cog IDに+1された)1から8のCog番号、失敗したらゼロだったので、 このようなブーリアン判定と矛盾しない。
続くCOGSTOPコマンドはCOGNEWコマンドに対応するもので、 IFの条件判定に合致した時だけ実行される「cogstop(Cog~ - 1)」の意味は、 「cogstop(Cog-1)」の後に「Cog~」という事である。Cogの番号は、内部的なIDの0-7に対して+1しているので、 ここで-1して元に戻してCOGSTOPコマンドを実行する。 演算子「~」(ポストクリア)は、既に出て来た演算子「~~」(ポストセット)とペアになるもので、 実行後にその変数をゼロクリアする。 これにより、指定されたIDのCogが停止され、その変数Cogもゼロとなる。
続くActiveメソッドでは、注釈が全てを説明している。 「YesNo := Cog > 0」の意味は、「Return TRUE if process is active, FALSE otherwise」である。 シンボル「>」は、比較演算の結果を返す演算子である。 このブーリアン結果が、返り値として定義された変数YesNoに代入される。
最後のToggleメソッドでは、以前のExerciseに比べて、わずかに、しかし重要な変更が加わった。 いちばん最後の「Cog~」という行が最重要である。 これまでのToggleメソッドでは、Cogの処理が終了すると、そこでCogは停止するが、 これでは起動されたCogに割り当てられたIDはそのままで、StartとかActiveメソッドが再利用できるかどうか、 判断できない。 そこでToggleメソッドの最後、点滅処理を終了したところで、自分からCog変数をクリアして終了することで、 また新しくCogの割り当てが可能となる。
Toggleメソッドのもう一つの重要な改良は、「無限ループ」の実現である。 ここでは、最後のトリッキーなステートメント「while Count := --Count #> -1」が重要である。 これは単独ではなくて、「repeat..while」という構文の一部である。 過去の例では「repeat Count」として次に処理を記述して、Countの数だけ処理を繰り返す、 というものだった。 それをここではrepeatだけにして、処理のステートメントを囲み、最後にwhileで受ける、という構造である。 whileに続くブーリアン条件がTRUEであれば、このループが繰り返される。 無限にループするためには、「呼ばれるCountの値がゼロ」という条件を設定した。
ここでの焦点は、repeat..whileの条件となった「Count := --Count #> -1」である。 新しい演算子として「--」は、Javaなどと同じく、Pre-Decrement演算子である。 つまり、まず変数を-1してから条件判定に入る。 そして見なれない「#>」は、Limit Minimumという演算子である。 これは、「右側よりも左側が大きければ左側を返す」「そうでなければ右側を返す」というものである。 この結果が、代入演算子「:=」によってCountに入り、そのブーリアン判定結果がwhileの条件となる
例えば、Count=2として呼ばれたとすると、最初のrepeatでは、「--Count」によって値は1となり、 これは「#> -1」の条件によってその値が返されて、これがCountに代入される。 つまりCountの値はデクリメントされて、これはnon-zeroなのでTRUEとなり、さらにrepeatが続く。 Count=1となって呼ばれた2回目には、「--Count」によって値は0となり、 これは「#> -1」の条件によってその値が返されて、Countにゼロが代入される。 するとブーリアン判定はzeroなのでFALSEとなり、このrepeatループから抜け出して、 Cogを解放する、という終了処理に行く。 Countの値が1以上であれば全て同様なので、ループは「Countの値だけ繰り返す」ということになる。
そして、このメソッドが最初からCount=0として呼ばれたとすると、最初のrepeatでは、「--Count」によって値は-1となり、 これは「#> -1」の条件によって-1という値が返されて、これがCountに代入される。 するとCountの値はnon-zeroなのでTRUEとなり、さらにrepeatが続く。 2回目以降には、「--Count」によって値は-2となり、「#> -1」の条件によって-1という値が返されて、これがCountに代入される。 するとCountの値はnon-zeroなのでTRUEとなり、さらにrepeatが続く。 つまり、この場合Countにはずっと-1が入ったまま、無限ループとなる。
なかなかにトリッキーな、しかしそのお陰で非常にシンプルなコードとなったOutput.spinが用意できたところで、 Exercise 8では、これを呼び出す新しいコードBlinker2.spinを定義する。
{{ Blinker2.spin }} CON MAXLEDS = 6 'Number of LED objects to use OBJ LED[6] : "Output" PUB Main {Toggle pins at different rates, simultaneously} dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 3_000_000, 0) 'Blink LEDs LED[NextObject].Start(17, 2_000_000, 0) LED[NextObject].Start(18, 600_000, 300) LED[NextObject].Start(19, 6_000_000, 40) LED[NextObject].Start(20, 350_000, 300) LED[NextObject].Start(21, 1_250_000, 250) LED[NextObject].Start(22, 750_000, 200) '<-Postponed LED[NextObject].Start(23, 400_000, 160) '<-Postponed LED[0].Start(20, 12_000_000, 0) 'Restart object 0 repeat 'Loop endlessly PUB NextObject : Index {Scan LED objects and return index of next available LED object. Scanning continues until one is available.} repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSこれをコンパイルしてボードに転送・実行すると、以下のような複雑な振る舞いとなった。これを解読・理解するのが、 このExercise 8の目標となる。
- 6個のLEDはそれぞれ別個のタイミングで複雑に点滅した
- 約8秒後に、P20は点滅をやめ、P22が点滅を開始した
- さらに数秒後、P18は点滅をやめ、P23が点滅を開始した
- その後、P16は点滅をやめ、P20が点滅を開始したが、点滅の間隔は変化した
- 最終的には、P17とP20だけがずっと点滅を続けた
まずCONブロックでは、「MAXLEDS = 6」と、とりあえず定数を定義して、 続くOBJブロックでは、「LED[6] : "Output"」と、LEDという名前の(外部の)Outputオブジェクト配列を定義した。 これは、6つの独立した並列プロセスを実行させるためである。
Mainメソッドの冒頭では、「dira[16..23]~~」と、16ピンから23ピンまでのI/Oピンを、 まとめて出力ピンに定義している。 I/Oレジスタに対して、このように複数のピンをまとめて定義できるコマンドとして、 DIRA、OUTA、INAでも使うことができる。 なお、Propellerデモボードでは、ビデオ出力のD/A変換のための抵抗が付いているために、 18ピンから23ピンまでについては、別個に点滅させても相互に他を点灯させるなど混乱があるので、 了解しておく必要がある。
次の行から並んでいる「LED[NextObject].Start(16, 3_000_000, 0)」などの8行が、 このサンプルの主要なプログラム部分である。 NextObjectメソッドについては後述するが、これは次に起動できるCogを捜して返すものである。 ここでは使用するLEDを6個と限定しているので、最初の6行はすぐに実行されて、 それぞれのポートから6個のCogがLEDが点滅動作に入る。 最初の2つは、Countの値がゼロなので、無限ループで動作する。 残りの4つは、それぞれCountの指定だけループして終了する。 7行目のステートメントは、プロセスが終了して空いているCogがない期間はNextObjectメソッドから返ってこないので、 そこで足踏みして待たされる。 Delayの値とCountの値の乗算結果ほどの時間が経過すると、そのプロセスが終了して、 空いたCog番号とともに戻って来るので、ここでLED.Start処理が実行され、 7行目、8行目と実行される。 最後の9行目は、NextObjectでなく直接、「index 0」を指定しているので、 最初に起動されて無限ループだったP16のプロセスを終了させて、その代わりにこちらが起動される。 最後に「repeat」というステートメントがあるので、ここまで到達した状態で動作は無限ループ(待機)となる。 プロセスが終了したCogのI/Oピンは初期状態の入力ピンとなり、 LED出力の無限ループのピンとCogはその状態を続ける。
「NextObject」メソッドは、Outputオブジェクト中のActiveメソッドを用いて、 「次に使えるLEDオブジェクトの番号」が見つかるまで捜して返ってくる、という処理を行う。 ネストされたrepeatのうち、まず、外側の
については、返り値である変数Indexが、ここでは内側の実行後に6であるMAXLEDSと比較され、 等しい限りrepeatを繰り返し、異なると終了する(Indexを返り値として呼び出し元に戻る)。 そして内側のループでは、repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSという別のrepeatが行われる。 これは変数Indexが0からインクリメントされ、その後に内側を実行し、 Indexが「MAXLEDS-1」すなわちここでは5までを繰り返すループである。 ここで呼ばれた「not LED[Index].Active」は、Indexで指定されたLED[Index].Activeの結果を受けて、 「Return TRUE if process is active, FALSE otherwise」から、 ActiveでないLEDオブジェクトがあれば、IFの中身であるQUITコマンドが実行される。 QUITはrepeatループを1階層だけ脱出するものなので、内側のrepeatループから処理を終了して抜け出す。repeatrepeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDS該当するActiveでないLEDオブジェクトが存在しなければ、Indexはさらにインクリメントされた6となり、 内側repeatループの条件を外れて、外側のrepeatに戻る。 すると外側のrepeatのwhile条件と一致するので、このrepeatループの中身は再び最初から繰り返される。 つまり、空いたCogが見つからない間は、このメソッドからは呼び出し元に戻ってこない。
順に探す中で、ActiveでないLEDオブジェクトにヒットすると、 内側のrepeatループからQUITで出て来たIndexの値はMEXLEDSより小さいので、 外側のループが終了して、呼び出し元のメソッドに、このIndexを返り値として戻る。
ここで、Propeller ToolのObject InfoでRAM領域の使用状況を見てみると、以下のように、 Program領域は73Longsで、Variable領域はこれまでの10Longsでなく、60Longsが使われている。
そこで、ソースの「LED[6] : "Output"」を「LED[1] : "Output"」と変更して、 同様にコンパイル・ロードしてObject InfoでRAM領域の使用状況を見てみると、以下のように、 Program領域は68Longsで、Variable領域は10Longs(Stackの9LongsとCogの1byte)となった。 つまり、オブジェクト指向のPropellerでは、コードの部分は共通にメモリに1つだけ持ち、 増加したわずか5Longsは追加された5つのオブジェクトのオーバーヘッド用に使われた。 これに対して、並列処理するための個々のプロセスのワーク領域であるスタック等だけが、 最大起動プロセス数に対応して、ここでは6倍、確保されたことが判る。
マニュアルではここで、Object Lifetimeについて述べている。 Propellerのプログラムはコンパイルされ、Propeller内部のRAMにバイナリイメージとしてダウンロードされる。 それぞれのオブジェクトは、このコンパイルされたバイナリイメージと、 それぞれのインスタンス(具体的に呼ばれて実行される化身)ごとの変数領域を持っている。 CPU実行時間において、組み込みシステムであるPropellerのオブジェクトのLifeTimeはスタティックである。 つまりPCのようにプロセスを起動したり消滅させるのでなく、コンパイルした時点で、 そのオブジェクトはメモリに展開され、消されることはない。 オブジェクトをOBJブロックで定義するのは、このためにも重要なこととなる。
2008年3月7日(金)
PropellerマニュアルのExercise 9では、Propellerチップのクロックについて整理している。 Propellerが持っている内部RCオシレータのクロックは、の2種類である。 ここまでのExerciseの例では、何も明示的にクロックについて記述しなかったので、 defaultでfast modeの12MHzが使用されてきた。 ただしRCオシレータなので、「12MHzといっても8MHzから20MHzのどこか」という程度の誤差がある。
- slow mode (約20KHz)
- fast mode (約12MHz)
クロックの設定のためには、topオブジェクトファイルのCONブロック内に、 _CLKMODE、_CLKFREQ、_XINFREQ、の定数の中の1つ以上の値を定義する必要がある。
まず最初に、_CLKMODEについて検討する。 Propellerリファレンスマニュアルの180ページのTable 4-3には、以下のようにClock Modeセッティング定数が定義されている。
例えば、Exercise 7-8で使ったBlinker2.spinにたった1行、CONブロックに_CLKMODEの設定を加えた、 以下のBlinker3.spinを実行すると、数百分の1ほどに遅くなったPropellerの動作を確認できた。
{{ Blinker3.spin }} CON _CLKMODE = RCSLOW 'Set to internal slow clock MAXLEDS = 6 'Number of LED objects to use OBJ LED[6] : "Output" PUB Main {Toggle pins at different rates, simultaneously} dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 3_000_000, 0) 'Blink LEDs LED[NextObject].Start(17, 2_000_000, 0) LED[NextObject].Start(18, 600_000, 300) LED[NextObject].Start(19, 6_000_000, 40) LED[NextObject].Start(20, 350_000, 300) LED[NextObject].Start(21, 1_250_000, 250) LED[NextObject].Start(22, 750_000, 200) '<-Postponed LED[NextObject].Start(23, 400_000, 160) '<-Postponed LED[0].Start(20, 12_000_000, 0) 'Restart object 0 repeat 'Loop endlessly PUB NextObject : Index {Scan LED objects and return index of next available LED object. Scanning continues until one is available.} repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSこのプログラムのCONブロックの「_CLKMODE = RCSLOW」というステートメントを、 「_CLKMODE = RCFAST」に置き換えると、Blinker2.spinと同様の、defaultのスピードとなる。
Propellerの外部に、周波数精度の高い外部クロック素子(水晶振動子/セラミック振動子)を接続した場合には、 さらに_CLKMODEの色々なオプションが使用できる。 ここではPropeller Demo Boardに搭載されている、5MHzの水晶を利用する。 Blinker3.spinのCONブロックの記述を以下のように変更したBlinker4.spinとしてみると、 defaultの場合の動作の約半分(12MHz→5MHz)のスピードで実行された。
{{ Blinker4.spin }} CON _CLKMODE = XTAL1 'Set to ext. low-speed crystal _XINFREQ = 5_000_000 ' Frequency on XIN pin is 5 MHz MAXLEDS = 6 'Number of LED objects to use OBJ LED[6] : "Output" PUB Main {Toggle pins at different rates, simultaneously} dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 3_000_000, 0) 'Blink LEDs LED[NextObject].Start(17, 2_000_000, 0) LED[NextObject].Start(18, 600_000, 300) LED[NextObject].Start(19, 6_000_000, 40) LED[NextObject].Start(20, 350_000, 300) LED[NextObject].Start(21, 1_250_000, 250) LED[NextObject].Start(22, 750_000, 200) '<-Postponed LED[NextObject].Start(23, 400_000, 160) '<-Postponed LED[0].Start(20, 12_000_000, 0) 'Restart object 0 repeat 'Loop endlessly PUB NextObject : Index {Scan LED objects and return index of next available LED object. Scanning continues until one is available.} repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSこの「_CLKMODE = XTAL1」によって、Propellerのクロックを外部の低速クリスタルに指定し、 Propeller内部のオシレータが4MHzから16MHzの外部クリスタルをドライブするモードとなる。 1チップCPUでは外部クリスタルに、さらに10pF程度のセラミックコンデンサを必要とするものもあるが、 PropellerはクリスタルをPropellerのXIとXOの端子の間に接続するだけでよい。
外部の振動子あるいはクロック信号入力を使用する場合には、_CLKMODEに加えて、 _XINFREQまたは_CLKFREQの記述が必要である。 _XINFREQは外付けのクリスタルからXIピンに入って来る周波数、 _CLKFREQは供給されるシステムクロック周波数、を記述する。 これら2つは、後述するPLLセッティングとも関係する。
Blinker4.spinの例では、「_XINFREQ = 5_000_000」によって、 XIピンの入力が5MHzであると記述されているので、PropellerのXIピン・XOピンの間に5MHzのクリスタルを接続する。 これにより、Propeller Toolによって_CLKFREQの値は自動的に設定される。
逆に_CLKFREQを5MHzと指定すると、Propeller Toolは_XINFREQの値を設定してくれる。 しかし、後述するPLLでのクロック設定があるので、ここでの指定は_XINFREQの方が好ましい。 外付けのクリスタルの5MHzという周波数は、一般の組み込みCPUと比べて、 えらく遅いという印象があるが、それは間違いである。 以下の、ほんの一部だけ違ったBlinker5.spinをコンパイルして実行すれば判る。
{{ Blinker5.spin }} CON _CLKMODE = XTAL1 + PLL4X 'Set to ext. low-speed crystal, 4x PLL _XINFREQ = 5_000_000 ' Frequency on XIN pin is 5 MHz MAXLEDS = 6 'Number of LED objects to use OBJ LED[6] : "Output" PUB Main {Toggle pins at different rates, simultaneously} dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 3_000_000, 0) 'Blink LEDs LED[NextObject].Start(17, 2_000_000, 0) LED[NextObject].Start(18, 600_000, 300) LED[NextObject].Start(19, 6_000_000, 40) LED[NextObject].Start(20, 350_000, 300) LED[NextObject].Start(21, 1_250_000, 250) LED[NextObject].Start(22, 750_000, 200) '<-Postponed LED[NextObject].Start(23, 400_000, 160) '<-Postponed LED[0].Start(20, 12_000_000, 0) 'Restart object 0 repeat 'Loop endlessly PUB NextObject : Index {Scan LED objects and return index of next available LED object. Scanning continues until one is available.} repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSここでの違いは、「_CLKMODE = XTAL1」が「_CLKMODE = XTAL1 + PLL4X」になっただけである。 これは、Propeller内部のPLL(phase-locked loop)回路によってXINの周波数が4倍される周波数逓倍が行われて、 システムクロックが 5MHz * 4 = 20MHz になったことを意味する。 実際に、動作はdefaultよりも倍近く(12MHz < 20MHz)早くなった。
この例では、_XINFREQを5MHzとして、さらにPLL4Xを指定することで、 Propeller Toolは自動で_CLKFREQの値を20MHzと計算する。 ここに敢えて「_CLKFREQは5MHz」とさらに記述しても無視される。 従って、外部クリスタルを使用する場合には、PLLによるクロック逓倍のある場合も含めて、 CONブロックでのクロック周波数指定は_XINFREQを使った方がいいらしい。
Propellerのクロック生成PLLの設定オプションには、外部クリスタルを5MHzとすると、
の5種類がある。 もし、ここで「_CLKMODE = XTAL1 + PLL4X」を「_CLKMODE = XTAL1 + PLL16X」とすると、 システムクロックは 5MHz * 16 = 80MHz となる。 外部クリスタルは最大で60MHzまで使えるようだが、この場合にはPLL16Xどころか、PLL2Xも駄目である。 システムクロックの上限は80MHzとあるためだが、5MHzの安価なクリスタルで80MHzまで実現できるのは嬉しい。 クロック80MHzは、組み込みマイコンとしてはかなり高速な能力である。Propeller恐るべし。
- PLL1X - 5MHz
- PLL2X - 10MHz
- PLL4X - 20MHz
- PLL8X - 40MHz
- PLL16X - 80MHz
さて、クロック関係の定義が出たところで、Exercise 10では、 ここまで使ってきたサンプルプログラムの一部を修正した例となっている。 Propellerチップは、実行中でもクロックを変更できる。 そこで、遅延をクロック数でなく、実際の時間として記述する手法へと改良している。 重複する注釈を省略して、改良されたOutput.spinは以下のようになる。
{{ Output.spin }} VAR long Stack[9] byte Cog PUB Start(Pin, DelayMS, Count): Success Stop Success := (Cog := cognew(Toggle(Pin, Delay, Count), @Stack) + 1) PUB Stop if Cog cogstop(Cog~ - 1) PUB Active: YesNo YesNo := Cog > 0 PUB Toggle(Pin, DelayMS, Count) dira[Pin]~~ repeat !outa[Pin] waitcnt(clkfreq / 1000 * DelayMS + cnt) 'Wait for DelayMS while Count := --Count #> -1 Cog~ここでは、クロック数として数えていたDelayという遅延パラメータを、ミリ秒単位のDelayMSに変更した。 シンボルCLKFREQは、PLLまで含めた実際のシステムクロックをHz(Cycle per second)単位で返すコマンドである。 これを1000で割ったことでミリ秒単位となり、DelayMSを乗算しているので、 最終的な遅延は「DelayMS ミリ秒の遅延」となる。
Propellerの実行時にシステムクロックを変更する場合には、CLKSETコマンドを使用する。 この場合、何度システムクロックを変更しても、ループのたびにこのOutputオブジェクトは、 遅延を再演算して対応する。 このOutput.spinに対応して、Top側を変更してみた一例のBlinker6.spinのプログラムは以下となる。
{{ Blinker6.spin }} CON _CLKMODE = XTAL1 + PLL4X 'Set to ext. low-speed crystal, 4x PLL _XINFREQ = 5_000_000 ' Frequency on XIN pin is 5 MHz MAXLEDS = 6 'Number of LED objects to use OBJ LED[6] : "Output" PUB Main dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 250, 0) 'Blink LEDs LED[NextObject].Start(17, 500, 0) LED[NextObject].Start(18, 50, 300) LED[NextObject].Start(19, 500, 40) LED[NextObject].Start(20, 29, 300) LED[NextObject].Start(21, 104, 250) LED[NextObject].Start(22, 63, 200) '<-Postponed LED[NextObject].Start(23, 33, 160) '<-Postponed LED[0].Start(20, 1000, 0) 'Restart object 0 repeat 'Loop endlessly PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSこの例では、WAITCNTコマンドを簡易的に使うソフトウェアディレイとなっているので、 実際の動作では、Propellerのインストラクションサイクル分だけ、遅延の誤差が累積する。 これを避けて正確なタイマーとする方法については、Propellerマニュアルの322ページにある、 WAITCNTの解説を参照する必要があるという。 そこを見ると、Fixed DelaysとSynchronized Delaysという2つの項目で、 けっこうな分量の解説があった。 これは、いずれ正確なタイマを実現する時に、再度、確認することにした。
PropellerマニュアルのExercise 11では、一転してLibrary Objectsについて述べている。 Propeller Toolには、Parallax社のエンジニアが開発した多数のオブジェクトがあり、 これをソフトウェア部品として活用できる。 これらライブラリの例として、
が挙げられている。 トラ技の記事にもあったが、8個のCogsがそれぞれ、ビデオジェネレータを持っている、 というのは、何か使えるのでは・・・と期待させる。
- シリアル通信
- 浮動小数点数値演算
- 数値と文字列との相互変換
- TVディスプレイ信号生成
- PC標準のキーボード
- PC標準のマウス
- PC標準のRGBディスプレイ
Propeller ToolのObjectライブラリをブラウズするだけで簡単に呼び出せる、というので、 とりあえず以下のDisplay.spinを呼び出してみた。
{{ Display.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" PUB Main | Temp Num.Init 'Initialize Numbers TV.Start(12) 'Start TV Terminal Temp := 900 * 45 + 401 'Evaluate expression TV.Str(string("900 * 45 + 401 = ")) 'then display it and TV.Str(Num.ToStr(Temp, Num#DDEC)) 'its result in decimal TV.Out(13) TV.Str(string("In hexadecimal it's = ")) 'and in hexadecimal TV.Str(Num.ToStr(Temp, Num#IHEX)) TV.Out(13) TV.Out(13) TV.Str(string("Counting by fives:")) 'Now count by fives TV.Out(13) repeat Temp from 5 to 30 step 5 TV.Str(Num.ToStr(Temp, Num#DEC)) if Temp < 30 TV.Out(",")すると、Propeller Demo BoardからRCAケーブルで繋いだビデオモニタ(テレビ)の画面に、 ちゃんと情報が表示された。これは簡単だぁ。(^_^)
このプログラムの中では、OBJブロックにある、 NumbersとTV_Terminalとの2つのライブラリが使われている。 Numbersは数値データを文字列に変換するライブラリ、 TV_Terminalはそれをテレビ(ビデオモニタ)に表示するライブラリである。 モニタに表示されたのは、以下のような情報である。
900 * 45 + 401 = 40,901 In hexadecimal it's = $9FC5 Counting by fives: 5, 10, 15, 20, 25, 30Propeller Demo BoardのRCAビデオコネクタから出ているのはアナログ信号であり、 テレビに普通にこれが表示される、というのは、なかなか大変なことである。 表示がこのまま止まっている、という事は、これを担当したCogは、 毎秒60フレーム分の情報を、刻々と生成しているのだ。 PropellerのCogのうちの1つにこのビデオ表示タスクを走らせることで、 必要な動作を他のCogで実現するシステムのデバッグにも有効だろう。
2008年3月9日(日)
Display.spinのコードの中で目新しいものとして、Mainの定義のラインに登場した「PUB Main | Temp」が重要である。 返り値を示す「:」とはまったく異なる「|」というシンボルを使って、 「| Temp」と宣言したことで、ロング長を持つTempという変数が、ローカル変数として定義される。Display.spinのOBJブロックで「Num : "Numbers"」と「TV : "TV_Terminal"」と定義されていることで、 ライブラリのNumbers.spinとTV_Terminal.spinという2つのプログラムも自動で呼び出されている。 PUB Mainブロックの1行目「Num.Init」は、NumというのはNumbers.spinなので、 この中のInitメソッドが呼び出される。 Propeller ToolでNumbers.spinの中の該当部分(init)を見ると、以下のようになっている。 内部的なレジスタ設定のためのdefaultシンボルの定義なので、Numbers.spinの全てのObjectが呼ばれるより前に、 最初にこれが呼ばれないといけない。
Display.spinのPUB Mainブロックの2行目「TV.Start(12)」は、TVというのはTV_Terminal.spinなので、 この中のStartメソッドを、引き数12で呼び出している。 TV_Terminal.spinの中の該当部分(start)を見ると、以下のようになっている。 この引き数(内部的にはbasepinというシンボル)は、PropellerチップのどのI/Oピンをベースにするか、 という指定(4ピン単位のバウンダリ)で、そのピンからの3本のI/Oピン(ここではP12、P13、P14)によって、 外付けの1.1KΩ、560Ω、270Ωの抵抗によって、1V・75Ωのベースバンド・ビデオ信号を生成するD/Aコンバータを構成する。 あわせて、ビデオ信号を生成するために2つあるいはそれ以上のCogsを起動する。 オブジェクトのソースには、これらの例のように使用方法などのドキュメントも含まれているが、 一般的に、InitとかStartというメソッドは、そのオブジェクトを使用するいちばん最初に呼び出すことになる。
PUB start(basepin) '' Start terminal '' basepin = first of three pins on a 4-pin boundary (0, 4, 8...) to have '' 1.1k, 560, and 270 ohm resistors connected and summed to form the 1V, '' 75 ohm DAC for baseband video 'init bitmap and tile screen bitmap_base := (@bitmap + $3F) & $7FC0 repeat x from 0 to x_tiles - 1 repeat y from 0 to y_tiles - 1 screen[y * x_tiles + x] := bitmap_base >> 6 + y + x * y_tiles 'start tv tvparams_pins := (basepin & $38) << 1 | (basepin & 4 == 4) & %0101 longmove(@tv_status, @tvparams, paramcount) tv_screen := @screen tv_colors := @color_schemes tv.start(@tv_status) 'start graphics gr.start gr.setup(x_tiles, y_tiles, 0, y_screen, bitmap_base) gr.textmode(x_scale, y_scale, x_spacing, 0) gr.width(width) out(0)Display.spinのMainの、これに続く3行目から以下の4行が、ビデオ出力の1行目に 「900 * 45 + 401 = 40,901」と表示した部分に相当する。 最初の「Temp := 900 * 45 + 401」というステートメントは、 ローカル変数Tempに900 * 45 + 401を代入した、ということだろう。
Temp := 900 * 45 + 401 'Evaluate expression TV.Str(string("900 * 45 + 401 = ")) 'then display it and TV.Str(Num.ToStr(Temp, Num#DDEC)) 'its result in decimal TV.Out(13)その次の「TV.Str(string("900 * 45 + 401 = "))」を調べるために、 TV_Terminal.spinの中の該当部分(str)を見ると、以下のようになっている。 ここで登場したSTRINGというディレクティブ(命令)により、文字列を定義する。 Propellerでは、CやJavaと同様に、ストリングはゼロ(NULL)でターミネイトする。 これをz-stringとも呼ぶ。 Propellerでは、大部分の文字列操作において、ストリングの先頭アドレスと、 ゼロ値を持つバイトをそのストリングの末尾とすることになる。 このTV.Strでも、REPEAT文は末尾のゼロによって処理を終了している。
PUB str(string_ptr) '' Print a zero-terminated string repeat strsize(string_ptr) out(byte[string_ptr++])この次の、「TV.Str(Num.ToStr(Temp, Num#DDEC))」は、ローカル変数Tempの値を、 delimited decimal formatでストリングに変換する。 新しく登場した「Num#DDEC」の「#」シンボルは「オブジェクト参照定数」の意味である。 ここでは、Numberオブジェクトの中で、以下のようにformat定数「DDEC」として定義されているシンタックスを使う、 という意味になる。 これは、数値「40901」を、Thousandデリミタ(コンマを3ケタごとに挟む)形式の「40,901」に変換してから、 NULL末尾の文字列に変換する。 このTV.Strの返り値は、ストリングの先頭アドレスである。
この一連の表示プログラムの最後の「TV.Out(13)」は、ディスプレイに「13」という1バイトを出力する。 ASCIIコードの13は「キャリッジリターン(CR)」であり、表示上は見えないが、 TV_Terminalに対して、次のテキスト表示位置を「改行した次の行の先頭」に移動させる。 これ以降の表示については、以下同文、もう明らかである。
TV_Terminalオブジェクトは、さらにその中で、以下のようにtv.spinとgraphics.spinのオブジェクトを参照している。 いずれも長大なプログラムであり、さらにspin言語だけでなくPropellerアセンブラの塊である。 そのうちこの細部についてもカスタマイズ改造していくとして、 とりあえずは、これはブラックボックスのソフトウェア部品としてしか使えそうにない。
Propeller Toolは、これらの関連オブジェクトを検索して、まとめて表示してくれる。 プログラムには、自分がオリジナル制作するWorkフォルダと、システムが提供するLinraryフォルダがある、 と理解しておくことが重要だろう。 これで、Exercise 11もオシマイである。OBJ tv : "tv" gr : "graphics"さて、いよいよExercise 12、これはPropellerチュートリアルの最後のExerciseであり、 トピックはPropellerの色々な数値表現の扱いについてである。 Propellerは32ビットCPUなので、基本は符号付き整数として、 -2,147,483,648 から 2,147,483,647 の数値を定数として扱うとともに、 ランタイム数値表現として利用できる。 さらにPropellerコンパイラは、整数および固定小数点表現のリアル数値だけでなく、 IEEE-754互換のsingle-precision形式(符号ビット+8ビット指数[power]+23ビットの仮数[mantissa、 下図)の浮動小数点にも対応している。 Propellerのライブラリ群も全て、この数値表現をサポートしている。
まずは、Pseudo-Real Numbersすなわち整数と固定小数点の数値表現についてである。 例えば、「A = B * C / D」という数値演算に対して、「小数点以下2桁」と指定すると、 「A = 7.6 * 38.75 / 12.5」の結果として23.56という値を得る。 これはランタイムにおいては、全ての数値を100倍した整数として、以下のように扱っていることになる。
A = (B* 100) * (C * 100) / (D * 100) A = (7.6 * 100) * (38.75 * 100) / (12.5 * 100) A = 760 * 3875 / 1250 A = 2356このようにそれぞれ100倍した数値で演算して、最後に2356 / 100 = 23.56として結果を得る。 注意すべきなのは、このように演算する場合、途中経過のどの段階でも、 -2,147,483,648 から 2,147,483,647 という範囲を越えてはいけない、という制限がある事である。
次に、Floating-Point Numbersすなわち浮動小数点についてである。 Propellerマニュアルによれば、前述のPseudo-Real Numbersで済む処理については、 なるべくそちらを使うべし、と書かれている。 浮動小数点ライブラリとかは、表現の自由度があるものの、実行時にはかなり時間がかかるので、 実行スピードに余裕があればどうぞ使って・・・という論調のようだ。 spinインタプリタが実行時に直接に扱うのは整数だけだ、ということなので、 たぶん使うことはなさそうだ。 この後に、Chapter 3最後のRealNumbers.spinというサンプルがあったが、 どうも使わないもののようなので、パスした。 その中で参照されているFloatMath.spin(以下のリスト)を見てみたら、 これまで組み込みCPUでいちいち書いてきたような、整数の乗算と除算の組み合わせによる、 よくある数値演算ライブラリだったからである。 優れたPropellerチップとはいえ、最後はやっぱり、こうなるのだった。
PUB FFloat(integer) : single | s, x, m if m := ||integer 'absolutize mantissa, if 0, result 0 s := integer >> 31 'get sign x := >|m - 1 'get exponent m <<= 31 - x 'msb-justify mantissa m >>= 2 'bit29-justify mantissa return Pack(@s) 'pack result PUB FRound(single) : integer return FInteger(single, 1) 'use 1/2 to round PUB FTrunc(single) : integer return FInteger(single, 0) 'use 0 to round PUB FNeg(singleA) : single return singleA ^ $8000_0000 'toggle sign bit PUB FAbs(singleA) : single return singleA & $7FFF_FFFF 'clear sign bit PUB FSqr(singleA) : single | s, x, m, root if singleA > 0 'if a =< 0, result 0 Unpack(@s, singleA) 'unpack input m >>= !x & 1 'if exponent even, shift mantissa down x ~>= 1 'get root exponent root := $4000_0000 'compute square root of mantissa repeat 31 result |= root if result ** result > m result ^= root root >>= 1 m := result >> 1 return Pack(@s) 'pack result PUB FAdd(singleA, singleB) : single | sa, xa, ma, sb, xb, mb Unpack(@sa, singleA) 'unpack inputs Unpack(@sb, singleB) if sa 'handle mantissa negation -ma if sb -mb result := ||(xa - xb) <# 31 'get exponent difference if xa > xb 'shift lower-exponent mantissa down mb ~>= result else ma ~>= result xa := xb ma += mb 'add mantissas sa := ma < 0 'get sign ||ma 'absolutize result return Pack(@sa) 'pack result PUB FSub(singleA, singleB) : single return FAdd(singleA, FNeg(singleB)) PUB FMul(singleA, singleB) : single | sa, xa, ma, sb, xb, mb Unpack(@sa, singleA) 'unpack inputs Unpack(@sb, singleB) sa ^= sb 'xor signs xa += xb 'add exponents ma := (ma ** mb) << 3 'multiply mantissas and justify return Pack(@sa) 'pack result PUB FDiv(singleA, singleB) : single | sa, xa, ma, sb, xb, mb Unpack(@sa, singleA) 'unpack inputs Unpack(@sb, singleB) sa ^= sb 'xor signs xa -= xb 'subtract exponents repeat 30 'divide mantissas result <<= 1 if ma => mb ma -= mb result++ ma <<= 1 ma := result return Pack(@sa) 'pack result PRI FInteger(a, r) : integer | s, x, m Unpack(@s, a) 'unpack input if x => -1 and x =< 30 'if exponent not -1..30, result 0 m <<= 2 'msb-justify mantissa m >>= 30 - x 'shift down to 1/2-lsb m += r 'round (1) or truncate (0) m >>= 1 'shift down to lsb if s 'handle negation -m return m 'return integer PRI Unpack(pointer, single) | s, x, m s := single >> 31 'unpack sign x := single << 1 >> 24 'unpack exponent m := single & $007F_FFFF 'unpack mantissa if x 'if exponent > 0, m := m << 6 | $2000_0000 '..bit29-justify mantissa with leading 1 else result := >|m - 23 'else, determine first 1 in mantissa x := result '..adjust exponent m <<= 7 - result '..bit29-justify mantissa x -= 127 'unbias exponent longmove(pointer, @s, 3) 'write (s,x,m) structure from locals PRI Pack(pointer) : single | s, x, m longmove(@s, pointer, 3) 'get (s,x,m) structure into locals if m 'if mantissa 0, result 0 result := 33 - >|m 'determine magnitude of mantissa m <<= result 'msb-justify mantissa without leading 1 x += 3 - result 'adjust exponent m += $00000100 'round up mantissa by 1/2 lsb if not m & $FFFFFF00 'if rounding overflow, x++ '..increment exponent x := x + 127 #> -23 <# 255 'bias and limit exponent if x < 1 'if exponent < 1, m := $8000_0000 + m >> 1 '..replace leading 1 m >>= -x '..shift mantissa down by exponent x~ '..exponent is now 0 return s << 31 | x << 23 | m >> 9 'pack resultこれで、Chapter 3も終わりである。 Propellerマニュアルの残りは、spin言語とPropellerアセンブラのリファレンスなので、 Propellerマニュアルの勉強もこれにて終了と言える。 ここからどうするかについては、以下のように書かれていた。 なるほど、一人立ちなのかぁ。(^_^;)
Where to go from here... You should now have the knowledge you need to explore the Propeller chip on your own and develop your first applications. Use the rest of this manual as a reference to the Spin and Propeller Assembly languages, explore every existing library object that interests you and join the Propeller Forum to keep learning and sharing with other active Propeller chip users.
Propeller日記(2) へ
「日記」シリーズ の記録