Propeller日記 (2)
長嶋 洋一
Propeller日記(1)
2008年3月10日(月)
Propellerマニュアルのチュートリアルを終えたところで、次の練習として、 Parallax社のサイトにあったPDFから、まず これを、次に これをテキストとすることにした。 ただし、このテキスト(Labと呼ぶらしい)をそのままを追い掛けるのも面白くないので、自分なりの目標を設定した。まず第一に、既にPropellerマニュアルのチュートリアルでも複数のCogsを起動する方法まで試したが、 ここでは「どこまで単独Cogで性能が出るのか」も試してみる方針とした。 クロックはPropeller Demo Boardの5MHzクリスタルでも、16倍PLLモードによって最大性能の80MHzにできるので、 そこで単独Cogでどこまでメインメモリ(強制的に時分割されて待たされる)を使って仕事が出来るのか、を試してみたい。
そして第二に、せっかくビデオ出力のサンプルを試したので、このタスクだけは例外として並列処理させて、 内部情報はいちいちビデオ表示してみることとした。 おそらく、これはテキストからは逸脱した実験となるが、今後できれば裏モニタとして、 あるいはスタンドアロンのビジュアル・インスタレーションとしてビデオ出力を活用したいので、 なるべく慣れておくためである。
最初の「Fundamentals: Propeller I/O and Timing Basics」では、 大部分のCPUが行う「入力の読み込み」「判断」「出力の制御」を整理する。 入力と出力については、たいていの場合にはタイミングに敏感であり、「入力の監視」「出力の更新」という動作となる。 このFundamentalsでは、入力の監視のサンプルとして「プッシュボタン」回路と、 出力の更新のサンプルとして「LED点灯」回路とを、PropellerチップのI/Oピンに設定して使用する。 これは応用として他の回路になっても、Propellerのソフトウェアの動作原理としては、基本的に同等である。 このLabテキストをマスターすることで、以下のような応用が出来る、という。
- Turn an LED on - assigning I/O pin direction and output state
- Turn groups of LEDs on - group I/O assignments
- Signal a pushbutton state with an LED - monitoring an input, and setting an output accordingly
- Signal a group of pushbutton states with LEDs - parallel I/O, monitoring a group of inputs and writing to a group of outputs
- Synchronized LED on/off signals - event timing based on a register that counts clock ticks
- Configure the Propeller chip’s system clock - choosing a clock source and configuring the Propeller chip’s Phase-Locked Loop (PLL) frequency multiplier
- Display on/off patterns - Introduction to more Spin operators commonly used on I/O registers
- Display binary counts - introductions to several types of operators and conditional looping code block execution
- Shift a light display - conditional code block execution and shift operations
- Shift a light display with pushbutton-controlled refresh rate - global and local variables and more conditional code block execution
- Timekeeping application with binary LED display of seconds - Introduction to synchronized event timing that can function independently of other tasks in a given cog.
Labテキストでは、「Parts List and Schematic」として、以下のような回路図が示されている。
しかし、手元のPropeller Demo Boardでは、
となっている。そこで、PDFとは決別して、Propeller Demo Boardに対応させて、 以下のようにポートの割り当てを変更することにした。 LEDについては、抵抗を経由して、本来の点灯処理と異なる見え方がある可能性を留意すればよい。
- P0-P7がフリーで空いている→ユニバーサル基板に接続可
- P8-P11はマイク入力のA/Dとヘッドホン出力アンプ用出力
- P12-P15はRCAビデオ出力用のD/A
- P16-P23は、LEDが接続されるとともに、VGA出力のための抵抗が相互を連結
実際にPropeller Demo Board上に、3つのスイッチと抵抗を配線してみた。 これ以上の回路は、外にブレッドボードを延長しないと入りそうもない。(^_^;)
Labテキストでは、これに続いて「Propeller Nomenclature」(Propeller用語体系)として、 以下のように、Propellerマニュアルで学んできた用語を整理している。 Propellerのプログラミングにはspinとアセンブラの2つの言語があるが、 Labテキストではspinを扱う、としている。 また、このLabではTop Objectを単独で扱うサンプルのみ、としている。
- Cog - a processor inside the Propeller chip. The Propeller chip has eight cogs, making it possible to perform lots of tasks in parallel. The Propeller is like a super-microcontroller with eight high speed 32-bit microcontrollers inside. Each internal microcontroller (cog) has access to the Propeller chip’s I/O pins and 32 KB of global RAM. Each cog also has its own 2 KB of ram that can either run a Spin code interpreter or an assembly language program.
- Spin and assembly languages - The Spin language is the high-level programming language created by Parallax for the Propeller chip. Cogs executing Spin code do so by loading a Spin interpreter from the Propeller chip’s ROM. This interpreter fetches and executes Spin command codes that get stored in the Propeller chip’s Global Ram.
- Method - block of executable Spin commands that has a name, access rule, and can optionally receive and return parameter values and create local (temporary) variables.
- Global and local variables - Global variables are available to all the methods in a given object, and they reserve variable space as long as an application is running. Local variables are defined in a method, can only be used within that method, and only exist while that method executes commands. When it’s done, the memory these local variables used becomes available to for other methods and their local variables. Local and global variables are defined with different syntax.
- Object - an application building block comprised of all the code in a given .spin file. Some Propeller applications use just one object but most use several. Objects have a variety of uses, depending partially on how they are written and partially on how they get configured and used by other objects. Some objects serve as top level objects, which provide the starting point where the first command in a given application gets executed. Other objects are written to provide a library of useful methods for top level or other objects to use.
さて、ここからいよいよサンプルプログラムであるが、ポートの番号を変更していること、 ビデオ出力も同時に実験することから、PDFのサンプルとは違ったソースとなる。 例えば、最初に載っていたサンプルソースは以下である。
'' File: LedOnP4.spin PUB LedOn ' Method declaration dira[4] := 1 ' Set P4 to output outa[4] := 1 ' Set P4 high repeat ' Endless loop prevents program from endingしかしこれを、実際には以下のようなプログラムに変更してみた。
{{ exp001.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" PUB Main Monitor LedOn PUB Monitor | 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("Counting by fives:")) 'Now count by fives repeat Temp from 5 to 30 step 5 TV.Str(Num.ToStr(Temp, Num#DEC)) if Temp < 30 TV.Out(",") PUB LedOn dira[16] := 1 ' Set P4 to output outa[16] := 1 ' Set P4 high repeat ' Endless loop prevents program from endingこれをコンパイル、実行してみると、ビデオモニタに2行のメッセージが表示され、 さらにP16のLEDが点灯した。 ビデオ出力のMonitorメソッドは、内部的にCogに表示処理の無限ループを指定している筈だが、 呼び出し元のメソッドについては終了して戻ってくることが判った。 ここで、試しにMainの部分だけを変更して
PUB Main LedOn Monitorと反対にしてみたところ、P16のLEDは点灯したものの、ビデオには何も表示されなかった。 これは、Mainメソッドの1行目のLedOnが呼ばれて、その最後の無限REPEATから帰ってこないためだろう。
再びexp001.spinに戻して、今度はLedOnメソッドの最後のREPEATを消してみたところ、P16のLEDは点灯しなかった。 これは、メソッドが最後のステートメントまで実行されて「終了」となったため、 目に見えない一瞬のLED点灯の後に、Propellerがローパワーモードに移り、 I/Oピンが初期(待機)状態の「入力」に戻ったためである。 I/OポートにCPUが書き込むというよりは、Propellerでは、Cogは出力状態として出力レジスタを管理している。 Cogの制御を離れてもI/Oピンの状態がラッチされているのではない、というのは、 AKI-H8など他のCPUとは考え方が違うところなので、注意が必要のようだ。
Labテキストではこれに続いて、どのようにPropellerが動作しているか、 という解説が続いているが、これは既にPropellerマニュアルのチュートリアルで分かっているので、 軽くスルーした。 ただ、「複数のCogが同じ入出力ピンを使おうとした場合にはWired-ORとなる」という説明には、 ちょっと確認の実験をしたくなった。 そこで、以前に作っているOutput.spinを呼び出す、新しいexp002.spinを以下のように作ってみた。
{{ exp002.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 MAXLEDS = 6 'Number of LED objects to use OBJ Num : "Numbers" TV : "TV_Terminal" LED[6] : "Output" PUB Main Monitor dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 50, 0) LED[NextObject].Start(16, 1000, 0) PUB Monitor | 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("Counting by fives:")) 'Now count by fives repeat Temp from 5 to 30 step 5 TV.Str(Num.ToStr(Temp, Num#DEC)) if Temp < 30 TV.Out(",") PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSこの実験のポイントは、相次いで異なるCogでLED点灯処理を呼び出しているのに、 敢えてピン指定を「同じP16」としているところである。 コンパイルして実行してみると、P16のLEDが、
というのを交互に繰り返している。 これはまさに、P16への出力がWired-ORされている、という事である。 このexp002.spinの例では、Mainメソッドとしては、この4行のステートメントは終了している。 しかし、それぞれの中で呼び出されたCogは無限ループを走っているために、 ビデオモニタの文字もLEDの点滅動作も連続している。 プログラムの方としては、「現在いくつのCogが走っているのか」を気にしなくていい、 と言えばそうだが、ちょっと落ち着かない気分でもある。 これはまた追って、実験をしてみることにしよう。
- 1秒間、連続して点灯
- 1秒間、細かく点滅
Labテキストではこの後に、
dira[16] := 1 outa[16] := 1というステートメントを、
dira[16] := outa[16] := 1と書くこともできる・・・などと続けているが、これは混乱の元で好きではないので、パス。 これに続いて、I/Oピンの定義をまとめて出来る、というところでは、以下の例があった。
dira[16..21] := %111111 outa[16..21] := %101010この「%」は、2進バイナリでの記述方法として、多くの局面で活用するシンタックスである。これは収穫。 なお、
outa[16..21] := %101010と記述すれば、Pin16がON(1)でPin17がOFF(0)・・・となるが、これを逆にして
outa[21..16] := %101010と記述すれば、Pin21がON(1)でPin20がOFF(0)・・・となる。 つまり昇順にも降順にも対応していて、代入のビットマップは「登場した順」ということである。
Labテキストは、続いて「Reading an Input, Controlling an Output」となった。 いよいよ、初めてのポート入力である。 入力を示すのはinaレジスタであり、これはdiraでポートが出力に指定されている場合には、 そのoutaレジスタの値を返すという。 Propellerは3.3V電源で動作するので、入力ピンの電圧がスレショルド電圧の1.65Vを越えていればHigh(1)、 越えていなければLow(0)、となる。 inaレジスタの内容は、INAコマンドが実行された時に更新される。 そこで、以下のexp003.spinを作って実行してみると、 見事にPropeller Demo Boardの上のブレッドボードに置いたボタンスイッチに対応して、 Pin18、Pin19、Pin20のLEDがそれぞれ、独立にON/OFF点灯動作した。
{{ exp003.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 MAXLEDS = 6 'Number of LED objects to use OBJ Num : "Numbers" TV : "TV_Terminal" LED[6] : "Output" PUB Main Monitor dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 50, 0) LED[NextObject].Start(16, 1000, 0) dira[0..2] ~ ' Set to Input (redundant) repeat outa[18] := ina[0] outa[19] := ina[1] outa[20] := ina[2] PUB Monitor | 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("Counting by fives:")) 'Now count by fives repeat Temp from 5 to 30 step 5 TV.Str(Num.ToStr(Temp, Num#DEC)) if Temp < 30 TV.Out(",") PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSLabテキストによれば、このスイッチ入力とLED出力のところのステートメントは、 「outa[18..20] := ina[0..2]」というようにも記述できるという。 また、インデントが重要というのが、以下の図とともに示されていた。 これは既にPropellerマニュアルで確認済みではあるものの、今後とも注意が必要だ。
この後、システムクロックや水晶振動子の精度の話、PLLによるクロック逓倍などの記述があるが、これも既知なのでパス。 レジスタ演算については敢えて再掲しておけば、例えば
PUB BlinkLeds dira[16..23] := %111111 repeat outa[16..23] := %111111 waitcnt(clkfreq/2 + cnt) outa[16..23] := %000000 waitcnt(clkfreq/2 + cnt)というのは、ポストセット演算子「~~」とポストクリア演算子「~」を使えば
PUB BlinkLeds dira[16..23]~~ repeat outa[16..23]~~ waitcnt(clkfreq/2 + cnt) outa[16..23]~ waitcnt(clkfreq/2 + cnt)というのと同じで、これはさらにビット単位での反転演算子「!」を使えば
PUB BlinkLeds dira[16..23]~~ outa[16..23]~~ repeat !outa[16..23] waitcnt(clkfreq/2 + cnt)と同じになる、というようなこと、さらに2進数と10進数についての解説などが続いた(これもパス)。 この後には、LEDの並びを2進数バイナリと見立てて、 「outa[16..23]++」は「outa[16..23] := outa[16..23] + 1」と同じだ、 という変数インクリメントの例などが延々と続いているが、ちょっと寄り道をしてみることにした。 以下のプログラムexp004.spinでは、ビデオ出力のメソッドを再びMainとして、 ここで「スイッチを押す」or「スイッチを離す」というイベントが起きるたびに、 ローカル変数dummyをインクリメントして、その数値をビデオ出力する、というシステムにしてみた。
{{ exp004.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 MAXLEDS = 6 'Number of LED objects to use OBJ Num : "Numbers" TV : "TV_Terminal" LED[6] : "Output" PUB Main | Temp, dummy Num.Init 'Initialize Numbers TV.Start(12) 'Start TV Terminal Temp := 1000 TV.Str(string("start ")) TV.Str(Num.ToStr(Temp, Num#DEC)) 'its result in decimal TV.Out(",") dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 50, 0) LED[NextObject].Start(16, 1000, 0) dira[0..2]~ ' Set to Input (redundant) dummy := ina[0] + ina[1] + ina[2] repeat outa[18] := ina[0] outa[19] := ina[1] outa[20] := ina[2] if dummy <> ina[0] + ina[1] + ina[2] dummy := ina[0] + ina[1] + ina[2] Temp++ TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSビデオ出力と同時に、別タスクとしてP16のLED点滅出力を行い、 さらにスイッチ入力に対応したLED点灯も行っている。 AKI-H8でもなんでも、ここまでのマルチタスクシステムは、なかなか簡単には実現できない。 これが問題なく動いたことで、ようやく、Propellerが身近になってきた。
2008年3月11日(火)
Labテキストでは続いて、Conditional Repeat Commandsのトピックである。 Propellerのspin言語には、以下のように、ループ制御の関係の多くの予約語があるようである。Syntax options for repeat make it possible to specify the number of times a block of commands is repeated. They can also be repeated until or while one or more conditions exist, or even to sweep a variable value from a start value to a finish value with an optional step delta.これと、プリ演算子とポスト演算子との組み合わせで、「20までカウントするループ」を11種類も解説する、 とあったが、いちいち試すほどソソラレないのでパスすることにした。 さらにfromとtoを使って、「20までカウントするループ」を3種類、加えて解説する、というのもパスした。 こんなのは実際に動くソフトを開発する時にちょっと実験すれば判ることだろう。 Labテキストではさらに、
と続いたが、これもPropellerマニュアルのリファレンスにある事なのて、パスしておいた。 さらに続いて、
- Propellerの演算子のボキャブラリ
- シフト演算子
- ifとelseなど
- VARブロックの変数 : byte、word、long
- Limit Minimum "#>" Operator
- Limit Maximum "<#" Operator
- 比較演算子と条件
の説明があった。 これはたまたま、昨日の最後の実験プログラムで試して、複数のローカル変数はコンマで並べられる、 と発見していたので、これもパス。
- global変数(VARブロックで定義、全オブジェクトのメソッドで共有)
- ローカル変数(メソッド名にパイプ記号「|」で並べる)
最後のTimekeeping Applicationsというトピックだけは、ちょっと重要なので注目した。 クリスタル振動子によって、システムはクウォーツ精度の正確さを持つ、というのは簡単ではない、 という話題である。 ここでは以下の2つのサンプルプログラムが提示されている。 ポート4-9に、outa[9..4]によって、secondsという定数をバイナリ表示している、 いわば「1秒時計」である。まずは「悪い時間管理の例」。
CON _xinfreq = 5_000_000 _clkmode = xtal1 + pll1x VAR long seconds PUB BadTimeCount dira[9..4]~~ repeat waitcnt(clkfreq + cnt) seconds ++ outa[9..4] := secondsそして「良い時間管理の例」。
CON _xinfreq = 5_000_000 _clkmode = xtal1 + pll1x VAR long seconds, dT, T PUB GoodTimeCount dira[9..4]~~ dT := clkfreq T := cnt repeat T += dT waitcnt(T) seconds ++ outa[9..4] := seconds「悪い時間管理の例」では、REPEATループの中のwaitcnt命令中で加算処理を実行するため、 この演算中はcntがカウントされず、全体の時間計測に遅延が加算されていく。 これが積み重なった場合に、時計としては致命的な誤差を生じる。
「良い時間管理の例」の方では、変数として定義したdTとTとでwaitcntの処理時間を計算しているので、 誤差が生まれない。 これを使った「時計」プログラムの例は、以下である。 この考え方は、オーディオのサンプリング周期とか、シリアル通信のボーレートなど、 正確な時間間隔を必要とする場面での定番となるだろう。
CON _xinfreq = 5_000_000 _clkmode = xtal1 + pll1x VAR long seconds, minutes, hours, days, dT, T PUB GoodClock dira[9..4]~~ dT := clkfreq T := cnt repeat T += dT waitcnt(T) seconds++ if seconds // 60 == 0 minutes++ if minutes == 60 minutes := 0 if seconds // 3600 == 0 hours++ if hours == 24 hours := 0 if seconds // 86400 == 0 days++ outa[9..4] := secondsこれで、 これはオシマイである。 次に、 これをやってみることにした。 タイトルはずばりMethods and Cogs。 Propellerの根幹の部分とオブジェクト指向プログラミング、 という「肝」の部分であり、まだ現時点で完全には理解できていないかもしれない「壁」である。
Introductionでは、spinにおけるメソッドについて、まず以下のように整理している。
- オブジェクトはメソッドと呼ばれるブロックから構成されている
- spinではメソッド名はプログラム制御に使われ、メソッド同志でパラメータをやりとり出来る
- あるメソッドが他のメソッドを利用するのをmethod callと呼ぶ
- callされたメソッドは終了すると、リターン値とともに呼び出し元に戻る
- メソッドが呼び出し元からパラメータ(引き数)を受け取るように記述できる
- 引き数は初期設定、動作定義、演算値などの引渡しに使われることが多い
ここからはPropeller独自のこととなるが、メソッドはそれぞれ別個のCogを並列的に起動する。 spin言語は、メソッドをCogに起動する命令、Cogを特定する命令、Cogを停止する命令を持っている。 spinメソッドがCogに起動されるためには、global変数アレイが、 メソッドのリターンアドレスをメモリ中に割り当てるために定義されている必要がある。 このメモリをstack spaceと呼ぶ。
メソッドの呼び出しとリターン、引き数の引渡しについては、CやJavaと同様である。 例えば以下の例では、メソッドBlinkTestから呼び出されたメソッドBlinkへの引き数は、 全てローカル変数のパラメータリスト項目への代入となる。 このようにすることで、異なるパラメータを用いて、このメソッドを他にも再利用して呼び出すことができる。
PUB BlinkTest Blink(4, clkfreq/3, 9) PUB Blink(pin, rate, reps) dira[pin]~~ outa[pin]~ repeat reps * 2 waitcnt(rate/2 + cnt) !outa[pin]これに対して、以下のようにstackを確保(1メソッドあたりおよそ10Longs)すると、 それぞれのCOGNEWコマンドに対応したCogが起動して、並列処理が実現できる。 それぞれのCogsを起動するTopレベルのメソッドには自動的にCog(0)が使われるので、 起動される3つのCogsは、新たにCog(1)からCog(3)となる。 メソッドBlinkは共通に呼ばれるが、それぞれ引き数パラメータは異なり、 点滅の時間は異なった処理を実現できる。
この例では、Cog(0)は3つのステートメントで3つのCogsを起動すると、 もうステートメントが無いので、ここでシャットダウンする。 他の3つのCogsもそれぞれのREPEATループを終了すると、 リターンを監視するCogも無いのでシャットダウンする。 最後のCogの処理が終わると、PropellerはLow Power Modeになる。 ただし、組み込みシステムを作る場合には、この状態にはならないで、 無限ループが続く筈である。
Cog(0)が未使用のglobal RAMをアクセスして格納してくれるまでの間、spinメソッドを実行する他のCogsは、 自分のメソッドcallのリターンアドレス、ローカル変数、中間演算表現を持っていなければならない。 このためにglobal RAMに確保するテンポラリ記憶領域をstack spaceと呼び、stackとして定義する。 上の例ではlong stack[30]と記述することで、30要素のLong配列を確保した。 Cog(0)が自分自身のstackとして使用するのは、Propeller Toolのメモリ一覧でブルーの領域、 未使用RAMの先頭部分である。 stack領域が端数が飛び出している場合には、4Longs(16bytes)単位で次の先頭の部分である。
ここまでに登場していたCOGNEWコマンド(引き数はメソッドとstackの2つ)は、たとえば
cognew(Blink(4, clkfreq/3, 9), @stack[0]) という場合、Propeller自身が自動的に、 次に空いているCogを割り当てるので、実際にどのCogであるかは実行するまでわからなかった。 ところが、COGINITコマンド(引き数はCog指定とメソッドとstackの3つ)というのがあり、
coginit(6, Blink(4, clkfreq/3, 9), @stack[0]) とすると、これは明示的にCog(6)を実行させることができる。 プログラミングとしては、こちらの方がスッキリするようにも思う。 ただしリファレンスマニュアルによれば、COGINITは返り値が無いとのことなので、 実行した場合、既に何か走っていたプロセスは強制終了されるらしい。
CogをストップさせるのがCOGSTOPコマンド(引き数はCog指定の1つ)であり、 使い方としては
cogstop(Cog) のようになる。 これは明示的にCogの指定が必要なので、これまでに扱った例では、 COGNEWして「空いているCogのID」を取得すると、COGNEWの返り値であるCog IDをglobal変数に格納し、 それを使ってSTOPしていた。 しかし、全てのプロセスのCog IDを明示的に固定するというのも、それほど悪くないように思う。
テキストではこれに続いて、ここまで謎だったstackの詳細を解説している。 stackが呼ばれると、それぞれ以下の領域が必要となるという。 これまでどうも歯切れが悪かった理由は、固定長でなく可変長であるからのようだ。
- 2 - return address
- 1 - return result
- number of method parameters
- number of local variables
- workspace for intermediate expression calculations
一例として、「Blink(pin, rate, reps)」が呼ばれる場合のstackであれば、
- 2 - return address
- 1 - return parameter
- 3 - pin, freq, and reps parameters
- 1 - time local variable
- 3 - workspace for calculations.
となって、10Longsのstackが必要となる。 これは正確には、以下のルールによるのだという。 面倒なので、とりあえず10Longsとか12Longsでいいかなぁ。(^_^;)
STEP 1: As you develop your object, provide a large amount of stack space for any Spin code launched via COGINIT or COGNEW. Simple code may take around 8 longs, but more complex code may take hundreds of longs. Start with a large value,128 longs for example, and increase it as needed to ensure proper operation.STEP 2: When your object's development is complete, include this object ("Stack Length") within it and call Init before launching any Spin code. NOTE: For the Init's parameters, make sure to specify the proper address and length (in longs) of the stack space you actually reserved.
Example: VAR long Stack[128] OBJ Stk : "Stack Length" PUB Start Stk.Init(@Stack, 128) 'Initialize Stack for measuring later cognew(@MySpinCode, @Stack) 'Launch code that utilizes StackSTEP 3: Fully exercise your object, being sure to affect every feature that will cause the greatest nested method calls and most complex set of run-time expressions to be evaluated. This may have to be a combination of hard-coded tests and physical, external stimuli depending on the application.
STEP 4: Call GetLength to measure the stack space actually utilized. GetLength will return a pointer to a result string and will serially transmit the results on the TxPin at the BaudRate specified. Use 0 for BaudRate if no transmission is desired. The value returned in the string will be -1 if the test was inconclusive (try again, but with more stack space reserved), 0 if the stack was never used, or some other value indicating the maximum utilization (in longs) of your stack up to that moment in time.
Example: If the application uses an external 5 MHz resonator and its clock settings are as follows: CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 {{ Then the following line will transmit "Stack Usage: #" on I/O pin 30 (the Tx pin normally used for programming) at 19200 baud; where # is the utilization of your Stack. }} Stk.GetLength(30, 19200)STEP 5: Set your reserved Stack space to the measured size and remove this object, Stack Length, from your finished object.
なお、ここでのstackという名称は、予約語でもなんでもない。VARブロックに確保して、 「@」記号によってCOGNEWコマンドやCOGINITコマンドとともにパラメータとして与えれば、 どんなシンボルであってもstackとして使用される。 また、spin言語は内部的に、「_stack」というシンボルを予約している。 これはTopレベルのメソッドがCog(0)を呼び出すためのスタック用スペースである。 従って、このシンボルを勝手に使うことはできない。
次に、以下の図によって、Return Parameterの解説があった。 これまでになかった収穫として、メソッドの返り値と、ローカル変数とを、同時に記述する方法があった。
PUB ButtonTime(pin) : dt | t1, t2t のようにすればいいらしい。 返り値は1つなので、コロンの後にまずこれを書いて、さらにパイプの後に、コンマで切っていくつもlocal変数を定義する。
Propellerマニュアルのチュートリアルにも登場したが、spin言語には、 resultという変数の名前の予約語がある。 これは、
PUB ButtonTime(pin) | t1, t2 ' Optional return parameter local var name removed repeat until ina[pin] t1 := cnt repeat while ina[pin] t2 := cnt result := t2 - t1 ' Value stored by result is automatically returnedのように、明示的に返り値を定義していなくても、メソッドが終了すると、自動的にここにリターン値が入る。 別にresultを使ってもいいわけだが、明示的に返り値を定義することを推奨する、と書かれている。 続く最後のトピックはCog ID Indexingである。 COGNEWによって起動されたCog IDは、COGNEWのリターン値(result)として返ってくるので、 これをglobal変数として、後のCOGSTOPに利用したり、Cogの利用状況トラッキングに使用できる。
・・・さて、これでCogsがマスターできたのかどうか。 昨日の、「ビデオ表示出力、LEDポート出力、スイッチ入力」というタスクを実行できている、 exp004.spinを改造して、実験してみることにした。再掲すると以下である。
{{ exp004.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 MAXLEDS = 6 'Number of LED objects to use OBJ Num : "Numbers" TV : "TV_Terminal" LED[6] : "Output" PUB Main | Temp, dummy Num.Init 'Initialize Numbers TV.Start(12) 'Start TV Terminal Temp := 1000 TV.Str(string("start ")) TV.Str(Num.ToStr(Temp, Num#DEC)) 'its result in decimal TV.Out(",") dira[16..23]~~ 'Set pins to outputs LED[NextObject].Start(16, 50, 0) LED[NextObject].Start(16, 1000, 0) dira[0..2]~ ' Set to Input (redundant) dummy := ina[0] + ina[1] + ina[2] repeat outa[18] := ina[0] outa[19] := ina[1] outa[20] := ina[2] if dummy <> ina[0] + ina[1] + ina[2] dummy := ina[0] + ina[1] + ina[2] Temp++ TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSそしてこのexp004.spinは、外部オブジェクトとして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~よく見ると、とりあえず動いていたものの、ちょっとバグがある事に気付いた。 まず最初に、exp005.spinのMainにあった「dira[16..23]~~ 'Set pins to outputs」は不要なので消した。 ポートの出力dir設定はOutput.spinの中のToggleメソッドにあるからである。
その上で、以下のようにexp005.spinを修正した。 Cogの様子を調べるために、とりあえず、ビデオ出力を初期化したところで、 LED点滅処理のために2つのCogを呼び出して、そのリターン値を表示してみた。
{{ exp005.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 MAXLEDS = 6 'Number of LED objects to use OBJ Num : "Numbers" TV : "TV_Terminal" LED[6] : "Output" PUB Main | Temp, dummy Num.Init 'Initialize Numbers TV.Start(12) 'Start TV Terminal TV.Str(string("start ")) Temp := LED[NextObject].Start(16, 50, 0) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := LED[NextObject].Start(16, 1000, 0) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") dira[0..2]~ ' Set to Input (redundant) dummy := ina[0] + ina[1] + ina[2] repeat outa[18] := ina[0] outa[19] := ina[1] outa[20] := ina[2] if dummy <> ina[0] + ina[1] + ina[2] dummy := ina[0] + ina[1] + ina[2] Temp++ TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") PUB NextObject : Index repeat repeat Index from 0 to MAXLEDS-1 if not LED[Index].Active quit while Index == MAXLEDSすると結果は、1でも2でもなく、ビデオ表示は常に「4, 5, ・・・」となった。 リターン値は+1しているので、最初のStartメソッドの返り値が4ということは、Cog(3)ということになる。 つまり、Mainで走るのはCog(0)だとすると、ビデオ出力の「TV.Start(12)」を実行した段階で、 もうCog(1)からCog(2)まで、計3個のCogが起動されていることになる。 これではOutput.spinの中でのCogチェックがあまり意味を持たないので、 両者を合体して、以下のようなexp006.spinとしてみた。
{{ exp006.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR long Stack[80] OBJ Num : "Numbers" TV : "TV_Terminal" PUB Main | Temp, dummy Num.Init TV.Start(12) TV.Str(string("Propellar " , "start ")) Temp := cognew(Toggle(16, 50, 0), @Stack[0]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(16, 1000, 0), @Stack[10]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(17, 100, 0), @Stack[20]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(17, 1000, 0), @Stack[30]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(18, 200, 0), @Stack[40]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(18, 1000, 0), @Stack[50]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") dira[0..2]~ dummy := ina[0] + ina[1] + ina[2] repeat outa[18] := ina[0] outa[19] := ina[1] outa[20] := ina[2] if dummy <> ina[0] + ina[1] + ina[2] dummy := ina[0] + ina[1] + ina[2] Temp++ TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") PUB Toggle(Pin, DelayMS, Count) dira[Pin]~~ repeat !outa[Pin] waitcnt(clkfreq / 1000 * DelayMS + cnt) while Count := --Count #> -1すると結果のビデオ表示は「3. 4, 5, 6, 7, -1, ・・・」となった。 最後のCOGNEWで失敗して-1が返り、あとはスイッチ操作で「0, 1, 2, ・・・」となった。 やはり、MainでCog(0)、ビデオ出力のためにCog(1)とCog(2)が働いているようである。
2008年3月12日(水)
昨日の最後のexp006.spinの実験から、ちょっと思い付いたことがあったので、 以下のexp007.spinを作ってみた。{{ exp007.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 VAR long Stack[80] OBJ Num : "Numbers" TV : "TV_Terminal" PUB Main | Temp, dummy Num.Init TV.Start(12) TV.Str(string("Propellar " , "start ")) Temp := cognew(Toggle(16, 50, 0), @Stack[0]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := cognew(Toggle(16, 1000, 0), @Stack[10]) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := 5 dira[0..2]~ dira[18..20]~~ dummy := ina[0] + ina[1] + ina[2] repeat outa[18..20] := ina[0..2] if dummy <> ina[0] + ina[1] + ina[2] dummy := ina[0] + ina[1] + ina[2] cogstop(Temp) TV.Str(Num.ToStr(Temp, Num#DEC)) TV.Out(",") Temp := ++Temp & %00000111 PUB Toggle(Pin, DelayMS, Count) dira[Pin]~~ repeat !outa[Pin] waitcnt(clkfreq / 1000 * DelayMS + cnt) while Count := --Count #> -1このプログラムは、スイッチを押されると「cogstop(Temp)」と、Tempで指定したCogを停止させる。 Tempの値は「Temp := ++Temp & %00000111」によって、 初期値からインクリメントして、7の次にはゼロに戻る。 Cog(0)はメイン処理なので、いわば「自爆」プログラムである(^_^;)。 そして、ここでは「Temp := 5」と、Cog(5)からとなっているが、 Propeller Editorでこれを色々に変えて、F10で即コンパイル・実行して実験してみた。 以下がその結果である。大体のところは予想通りの結果となった。
- 初期値 = 5 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作(1秒点灯/1秒高速点滅、の繰り返し)している。 スイッチを押すと、「5, 」とビデオ表示されLEDが点灯、 スイッチを離すと、「6, 」とビデオ表示されLEDが消灯。 スイッチを押すと、「7, 」とビデオ表示されLEDが点灯、 スイッチを離すと★、LEDは消灯するがビデオ表示に変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 ★のところでCog(0)をストップしたので、Mainが終了してしまったためである。 Cog(3)とCog(4)は生きているので、P16のLEDの間歇点灯動作がずっと続く。- 初期値 = 6 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと、「6, 」とビデオ表示されLEDが点灯、 スイッチを離すと、「7, 」とビデオ表示されLEDが消灯。 スイッチを押すと★、LEDも点灯せず、ビデオ表示も何も変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 ★のところでCog(0)をストップしたので、Mainが終了してしまったためだが、 LEDが点灯しないのがちょっと謎。 Cog(3)とCog(4)は生きているので、P16のLEDの間歇点灯動作がずっと続く。- 初期値 = 7 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと、「7, 」とビデオ表示されLEDが点灯、 スイッチを離すと★、LEDは消灯するがビデオ表示に変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 ★のところでCog(0)をストップしたので、Mainが終了してしまったためである。 Cog(3)とCog(4)は生きているので、P16のLEDの間歇点灯動作がずっと続く。- 初期値 = 0 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと★、LEDも点灯せず、ビデオ表示も何も変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 ★でCog(0)をストップしたので、Mainが終了してしまったためだが、 LEDが点灯しないのがちょっと謎。 Cog(3)とCog(4)は生きているので、P16のLEDの間歇点灯動作がずっと続く。- 初期値 = 1 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと★、LEDが点灯し、ビデオ画面が消える。 スイッチを離すと、LEDが消灯し、これ以降、スイッチを操作しても何も起きなくなる。 ★のところでCog(1)をストップすると、これはビデオ表示に重要な動作をしているらしく、 ビデオが無くなる。 Cog(0)はここでは生きているので、次のスイッチ入力→LED消灯まで実行する。- 初期値 = 2 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと★、LEDが点灯し、ビデオ表示は何も変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 ★のところでCog(2)をストップすると、ビデオ表示は生きているものの、何もしなくなった。- 初期値 = 3 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと、「3, 」とビデオ表示されLEDが点灯、P16のLEDは1秒ごとの点滅になった。 スイッチを離すと、「4, 」とビデオ表示されLEDが消え、P16も消えた。 スイッチを押すと、「5, 」とビデオ表示されLEDが点灯。 スイッチを離すと、「6, 」とビデオ表示されLEDが消えた。 スイッチを押すと、「7, 」とビデオ表示されLEDが点灯、 スイッチを離すと、LEDは消灯するがビデオ表示に変化が無くなる。 これ以降、スイッチを操作しても何も起きなくなる。 最初のスイッチONでCog(3)がストップ、次にCog(4)がストップしたため。- 初期値 = 4 の場合
まず「3, 4, 」とビデオ表示され、P16のLEDが間歇点灯動作している。 スイッチを押すと、「4, 」とビデオ表示されLEDが点灯、P16のLEDは1高速の点滅になった。 P16はこれ以降はずっとこの状態。 スイッチを離すと、「5, 」とビデオ表示されLEDが消えた。 スイッチを押すと、「6, 」とビデオ表示されLEDが点灯 スイッチを離すと、「7, 」とビデオ表示されLEDが消えた。 これ以降、ビデオ表示に変化が無くなり、スイッチを操作しても何も起きなくなる。 最初のスイッチONでCog(4)がストップしたため。また、外部オブジェクトとして呼び出しているTV_Terminal.spinを調べて、 ビデオ表示のためのオプションを発掘した。 カーソル座標を与えることは出来ないが、ホームと改行とタブがあれば、 画面内の特定の位置に情報を表示することは容易である。 全て、「TV : "TV_Terminal"」と指定している下で、 「TV.Out(x) 」のステートメントのxの値として指定する。 16進で$20から$7Eまでは普通のキャラクタであるが、それ以外については、 実験してみたところ、以下のようである。
- TV.Out($00 ) - home
- TV.Out($01 ) - 文字色は白(default)
- TV.Out($02 ) - 背景色が黒ならば文字色は緑
- TV.Out($03 ) - 背景色が黒ならば文字色は赤
- TV.Out($04 ) - 画面の背景色は黒(default)
- TV.Out($05 ) - 画面の背景色は青、文字色は「白+背景色」
- TV.Out($06 ) - 画面の背景色は緑、文字色は「白+背景色」
- TV.Out($07 ) - 画面の背景色は赤、文字色は「白+背景色」
- TV.Out($09 ) - Tab
- TV.Out($0D ) - return
さて、Propellerの処理状態をモニタするビデオ出力という道具を得たところで、 いよいよハードとアセンブラの世界に進んでみることにした。 Propellerのサイトには、Parallax社のエンジニアが提供するだけでなく、 世界中の物好き(^_^;)がPropellerを試して作ったオブジェクトライブラリが多数、 置かれていた。 その中で気になったのが、なんといっても
MIDI in object for Propeller であった。 なんとPropellerで、既に、MIDI入力のライブラリを作った人がいたのである。 開発者のTom Dimock氏によれば、動機となったプロジェクトは
This object was created as part of my project to equip my Austin Pipe Organ to play from a MIDI stream. I acquired the organ about twenty years ago, when the church that owned it decided to replace it with an electronic organ after the pipe organ was damaged in a flood. I worked with a close friend to build a MIDI controller for it based on the 8051 micro-computer. I etched my own circuit boards and we actually got it all working - one of the first MIDI capable pipe organs in the world. But the system did not age well, and was difficult to impossible to modify. Several years ago the organ stopped playing. I had been looking at several possibilities for building a new controller - the Javelin chip from Parallax looked very promising (I was a Java programmer at work), until I found that it could not deal with the 31.25KB baud rate of MIDI. The SX processors could have done the job, and I was starting to look into them when the Propeller chip was announced. The organ now plays from my breadboard implementation, and I should have my ProtoBoard version up and running very soon.とのことである。 やはり、普通じゃなかった(^_^;)。 ドキュメント中にあった回路図は以下であるが、本人も 「I'm pretty sure that you could reduce the 5v connection to the opto-isolator to 3.3v and eliminate the 1K resistor, but I have not tried that. 」と書いているように、ちょっとこれはいただけない。 Propellerの入力ピンに3.3V以上の電圧がかかって、壊してしまう危険性がある。(^_^;)(エキサイト翻訳) この物は、MIDIの流れからプレーするために私のオースチンPipe Organを備えるために私のプロジェクトの一部として作成されました。 私はおよそ20年前に器官を取得しました。((その時、パイプオルガンが洪水で破損した後にそれを電子オルガンに取り替えると決めました)それを所有していた教会)。 私は、8051年のマイコンに基づいてそれのためにMIDIコントローラを造るために親友と共に働いていました。 私たちは実際にすべて働かせました--私は私自身のサーキット板をエッチングしました、そして、世界における最初のMIDIできるパイプオルガンの1つ。 しかし、システムは、よく老朽化しないで、変更する不可能に難しかったです。 数年前に、器官は、プレーするのを止めました。 私は、新しいコントローラを造るのをいくつかの可能性を見続けていました--ParallaxからのJavelinチップは非常に有望に見えました(私は仕事でJavaのプログラマでした)、私が、それがMIDIの31.25KBのボーレートに対処することができないのがわかるまで。 SXプロセッサは仕事したかもしれません、そして、Propellerチップが発表されたとき、私はそれらを調べ始めていました。 器官は現在私のパンこね台実現からプレーします、そして、私には、すぐ、ProtoBoardバージョンアップと走行があるべきです。
そこで、手元にあるいつものフォトカプラTLP552と、電圧レベルシフトのためにバイポーラトランジスタの2SC1815で2段(反転の反転)に受けて、 まずは以下のような回路図としてみた。 汎用でもっとも定番の2SC1815は、例えば RSコンポーネンツ で簡単にゲットできる。
そしてとりあえず、ここにあった MidiIn.spin にはまったく手を加えずに、ライブラリMidiIn.spinを呼び出す、以下のようなexp008.spinを作ってみた。 スイッチ入力とLED点滅の処理はもう動作確認済みなので、 MIDI入力があったら、そのデータをビデオ表示として刻々とモニタする、というつもりである。
{{ exp008.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" midiIn : "MidiIn" PUB Main | Temp, dummy Num.Init TV.Start(12) TV.Str(string("Propellar " , "start ")) midiIn.start(7, 3) repeat dummy := midiIn.evtCheck if dummy <> -1 Temp := midiIn.evt TV.Str(Num.ToStr(Temp, Num#HEX)) TV.Out(" ")すると、詳細は不明ながら、MIDIを送ってみたら、なにやら受信してビデオ表示できた。 以下がその模様の写真である。 これで、ビデオ信号とMIDI信号のハードと、Propellerのソフトウェアが結合したことになる。 呼び出している外部オブジェクトのMidiIn.spinは、アセンブラで書かれているので、 spin言語とPropellerアセンブラとの連携という初めてのサンプルとしても動いたことになった。
2008年3月13日(木)
とりあえず動いたものの、この MidiIn.spin についてはまったく分かっていないので、これを少しずつ変更して試しながら理解して、 自分なりのソフトウェア部品として改良していくことにした。 まずexp008.spinを複製してexp009.spinとリネーム、 MidiIn.spinを複製してMidiIn01.spinとリネームした。 呼ぶ方と呼ばれる方とで対応する必要があるからである。 そして、Propellerマニュアルを片手に、少しずつ少しずつMidiIn01.spinから不要の部分を削り、 同等の動作となることをいちいち確認しつつ、プログラムの核心に迫っていくことにした。 「初めてで慣れていないCPU」「他人のプログラムを解析する」という両方を満たすには、 とりあえずこれしかないだろう。まず最初に気付いたのは、exp008.spinで「MIDIを受けている」と思っていた動作が、 どうやらMIDIメッセージを2回に1回だけしか受け取っていないらしい、という事だった。 MIDI送り側のMax/MSPでいくつか試した結果である。 調べてみた結果、
dummy := midiIn.evtCheck if dummy <> -1 Temp := midiIn.evt TV.Str(Num.ToStr(Temp, Num#HEX))の部分にバグがあった。 midiIn.evtCheckメソッドを呼んで、この返り値が-1でなければデータがあるのでmidiIn.evtメソッドを呼ぶ、 としたつもりだったが、よくソースを読むと、 midiIn.evtCheckの返り値が-1でない場合には、その返り値はもうMIDI受信データであった。 逆にmidiIn.evtメソッドは、新しいMIDIデータが来るまで待たされるものだった。 そこで、以下のようにexp009.spinを変更することで、無事に全てのMIDIを受信してビデオ表示するようになった。
dummy := midiIn.evtCheck if dummy <> -1 TV.Str(Num.ToStr(dummy, Num#HEX))このように調べつつ、実験で確認しながら以下のような事項を整理していった。
- Propellerプロセッサ内のCogには、普通のCPUのレジスタに相当するものは無い。 Cog内のRAM領域全てがレジスタみたいなものである。 指定したアドレスのメモリがそのまま格納先オペランドになり、指定したアドレスをそのまま即値オペランドにできる。
- VARブロックに定義するglobal変数というのは、それぞれのobjectの中の各メソッドで共有する、 そのCog内におけるglobal、という意味である。 他のobjectのVARブロックで、たまたま同じシンボルのglobal変数が定義されていても(例えばcog)、無関係である。 他のobjectの内部の変数を取得したかったら、その値を返すメソッドを定義して呼び出す必要がある。
- 新しいオブジェクトに対して、初期化のstartメソッドを呼ぶと、たいてい「Cog ID + 1」が返ってくる。 たいていstartの中でstopを呼んでいるが、まぁこれは無くても良い。(^_^;)
- DATブロックには、データやPropellerアセンブラコードを記述する。 DATブロックの先頭にはORGディレクティブを置く。 このブロック内の先頭アドレスのラベルに「@」を付けて、NEWCOGで起動する。 DATブロックの最後にはFITディレクティブを置く。 これにより、Propeller内部RAMに入るかどうかをコンパイラが調べてくれる。
- 変数にlong命令で数値を格納するところの「0-0」という表記は、 単に全部ゼロで埋まっている、ということなので、「0」と同じ。 「RES」というのは、long変数の領域確保(reserve)だけなので、数値は初期化されていない(不定)。
- バイナリ表現は「%01100011」など。 16進表現は「$013F007F」など。
- 「test eventEnable,doNoteOn wz」と「if_z mov ignoreNoteOn,#0」のような2行ペアは、 「変数eventEnableの、doNoteOnビットについてビットテストして、結果をゼロフラグに格納」して、 「ゼロフラグが立っていれば値#0を変数ignoreNoteOnに格納」ということ。
- ラベルは予約語以外で先頭が「_」あるいは文字で始まるものとして、globalに自由に定義できる。 ローカルなラベルにしたい時には先頭に「:」を付ける。 ラベルはJMPやCALLやCOGINITで使われる。
- 「#」は、その値をそのまま指定するという意味。 「jmp some_labal」だと「some_labalとして定義されたglobal変数の中身のアドレスへ飛べ」となり、 「jmp #some_labal」と「some_labalとラベル定義された場所へ飛べ」となる。
- Propellerでは、基本的にはサブルーチンコールCALLでなく、ジャンプテーブルでの分岐ジャンプJMPを使う。 どうしてもCALLでサブルーチンコールする場合には、おまじないみたいだが、 リターン「 RET」の場所には「サブルーチンのアドレス_ret」というラベルが必要であるという。 ここは重要なので、そのうちいずれ、きちんと調べてみよう。
いろいろ試した結果、以下のexp009.spinとMidiIn01.spinの組み合わせまでシンプルにできた。 これで、通常のMIDIイベントを受けて、きちんと16進表示でビデオ表示できている。 まずはexp009.spinである。
{{ exp009.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" midiIn : "MidiIn01" PUB Main | dummy Num.Init TV.Start(12) TV.Str(string("Propellar " , "Start! ")) TV.Str(string("MidiIn Cog =")) dummy := midiIn.start(7) TV.Str(Num.ToStr(dummy, Num#DEC)) repeat dummy := midiIn.eve if dummy <> -1 TV.Str(Num.ToStr(dummy, Num#HEX9))起動すると「MidiIn Cog = 4」と表示される。 MainにCog(0)、ビデオ出力のためにCog(1)とCog(2)が使用され、 MIDI入力の初期化でCog(3)が使われて、+1された値4が返ってきているためである。 そして、だいぶスッキリしたMidiIn01.spinである。
VAR long eventHead , eventTail long eventBuffer[32] PUB start(_midiPin) : okay midiPin := _midiPin event_head := @eventHead event_tail := @eventTail event_buffer := @eventBuffer bitticks := clkfreq / 31_250 halfticks := bitticks / 2 longfill(@eventHead,34,0) okay := cognew(@entry, 0) + 1 PUB eve : event event := -1 if eventTail <> eventHead event := eventBuffer[eventTail] eventTail := (eventTail + 1) & $0F DAT org entry mov midiMask,#1 shl midiMask,midiPin mov nextDataHandler,#getMidiByte getMidiByte waitpne midiMask,midiMask mov midiBits,#9 mov bitClk,cnt add bitClk,halfticks add bitClk,bitticks :midiBit waitcnt bitClk,bitticks test midiMask,ina wc rcr midiByte,#1 djnz midiBits,#:midiBit shr midiByte,#32-9 and midiByte,#$FF test midiByte,#$80 wz if_z jmp nextDataHandler mov t1,midiByte and t1,#$F8 cmp t1,#$F8 wz if_z jmp #getMidiByte mov eventHandler,#getMidiByte mov command,midiByte shr command,#4 mov channel,midiByte and channel,#$0F shl channel,#16 mov t1,command and t1,#$07 add t1,#:cmdTable jmp t1 :cmdTable jmp #noteOffCmd jmp #noteOnCmd jmp #aftertouchCmd jmp #controllerCmd jmp #programChangeCmd jmp #channelPressureCmd jmp #pitchWheelCmd writeEvent rdlong t1,event_head mov event_offset,t1 shl event_offset,#2 add event_offset,event_buffer wrlong event_data,event_offset add t1,#1 and t1,#$0F wrlong t1,event_head writeEvent_ret ret noteOnCmd mov nextDataHandler,#note mov eventHandler,#noteOn jmp #getMidiByte noteOffCmd mov nextDataHandler,#note mov eventHandler,#noteOff jmp #getMidiByte aftertouchCmd mov nextDataHandler,#note mov eventHandler,#aftertouch jmp #getMidiByte controllerCmd mov nextDataHandler,#cont_num mov eventHandler,#controller jmp #getMidiByte programChangeCmd mov nextDataHandler,#program_num mov eventHandler,#programChange jmp #getMidiByte channelPressureCmd mov nextDataHandler,#channel_pressure mov eventHandler,#channelPressure jmp #getMidiByte pitchWheelCmd mov nextDataHandler,#pitch_wheel_lo mov eventHandler,#pitchWheel jmp #getMidiByte note mov noteValue,midiByte shl noteValue,#8 mov nextDataHandler,#velocity jmp #getMidiByte velocity mov velocityValue,midiByte mov nextDataHandler,#note jmp eventHandler cont_num mov controllerNumber,midiByte shl noteValue,#8 mov nextDataHandler,#cont_val jmp #getMidiByte cont_val mov controllerValue,midiByte mov nextDataHandler,#cont_num jmp eventHandler program_num mov programValue,midiByte jmp eventHandler channel_pressure mov channelPressureValue,midiByte jmp eventHandler pitch_wheel_lo mov pitchWheelLoValue,midiByte mov nextDataHandler,#pitch_wheel_hi jmp #getMidiByte pitch_wheel_hi mov pitchWheelHiValue,midiByte shl noteValue,#7 mov nextDataHandler,#pitch_wheel_lo jmp eventHandler noteOn mov event_data,noteOnEvt or event_data,channel or event_data,noteValue or event_data,velocityValue call #writeEvent jmp #getMidiByte noteOff mov event_data,noteOffEvt or event_data,channel or event_data,noteValue or event_data,velocityValue call #writeEvent jmp #getMidiByte aftertouch mov event_data,aftertouchEvt or event_data,channel or event_data,noteValue or event_data,velocityValue call #writeEvent jmp #getMidiByte controller mov event_data,controllerEvt or event_data,channel or event_data,controllerNumber or event_data,controllerValue call #writeEvent jmp #getMidiByte programChange mov event_data,programChangeEvt or event_data,channel or event_data,programValue call #writeEvent jmp #getMidiByte channelPressure mov event_data,channelPressureEvt or event_data,channel or event_data,channelPressureValue call #writeEvent jmp #getMidiByte pitchWheel mov event_data,pitchWheelEvt or event_data,channel or event_data,pitchWheelHiValue or event_data,pitchWheelLoValue call #writeEvent jmp #getMidiByte noteOnEvt long $00000000 noteOffEvt long $01000000 aftertouchEvt long $02000000 controllerEvt long $03000000 programChangeEvt long $04000000 channelPressureEvt long $05000000 pitchWheelEvt long $06000000 bitticks long 0 halfticks long 0 midiPin long 0 event_head long 0 event_tail long 0 event_buffer long 0 t1 res 1 event_offset res 1 event_data res 1 midiMask res 1 midiBits res 1 bitClk res 1 midiByte res 1 nextDataHandler res 1 eventHandler res 1 rtEventHandler res 1 command res 1 channel res 1 noteValue res 1 velocityValue res 1 controllerNumber res 1 controllerValue res 1 programValue res 1 channelPressureValue res 1 pitchWheelLoValue res 1 pitchWheelHiValue res 1 fit2008年3月14日(金)
昨日、とりあえず動いたサンプルプログラムは、メイン側のexp009.spinはオリジナルだが、 PropellerアセンブラのMidiIn01.spinについては、実際の動作について理解しないまま、 「とりあえず動いている(使えている)」だけのものである。 そこで、Propellerアセンブラと、Cogの動作についてさらに理解するために、 この内部をさらに詳細に追いかけてみることにした。 まずexp009.spinを複製してexp010.spinとリネーム、 MidiIn01.spinを複製してMidiIn02.spinとリネームした。 そして、さっそくMidiIn02.spinでバグを発見した。 HTMLマニュアルには、 「I only have a 16 slot event buffer, with no provision for handling buffer overflow.」と書かれていたが、VAR long eventHead , eventTail long eventBuffer[32] PUB start(_midiPin) : okay midiPin := _midiPin event_head := @eventHead event_tail := @eventTail event_buffer := @eventBuffer bitticks := clkfreq / 31_250 halfticks := bitticks / 2 longfill(@eventHead,34,0) okay := cognew(@entry, 0) + 1 PUB eve : event event := -1 if eventTail <> eventHead event := eventBuffer[eventTail] eventTail := (eventTail + 1) & $0F ・・・ writeEvent rdlong t1,event_head mov event_offset,t1 shl event_offset,#2 add event_offset,event_buffer wrlong event_data,event_offset add t1,#1 and t1,#$0F wrlong t1,event_head writeEvent_ret retというのはちょっと矛盾している。 もし、MIDIイベントキューバッファが16スロットであるなら、
VAR long eventHead , eventTail long eventBuffer[16] PUB start(_midiPin) : okay midiPin := _midiPin event_head := @eventHead event_tail := @eventTail event_buffer := @eventBuffer bitticks := clkfreq / 31_250 halfticks := bitticks / 2 longfill(@eventHead,18,0) okay := cognew(@entry, 0) + 1 PUB eve : event event := -1 if eventTail <> eventHead event := eventBuffer[eventTail] eventTail := (eventTail + 1) & $0F ・・・ writeEvent rdlong t1,event_head mov event_offset,t1 shl event_offset,#2 add event_offset,event_buffer wrlong event_data,event_offset add t1,#1 and t1,#$0F wrlong t1,event_head writeEvent_ret retとなる筈である。このように変更してみたら、ちゃんと問題なく動いた。 そして、もしMIDIイベントキューバッファが32スロットであるなら、
VAR long eventHead , eventTail long eventBuffer[32] PUB start(_midiPin) : okay midiPin := _midiPin event_head := @eventHead event_tail := @eventTail event_buffer := @eventBuffer bitticks := clkfreq / 31_250 halfticks := bitticks / 2 longfill(@eventHead,34,0) okay := cognew(@entry, 0) + 1 PUB eve : event event := -1 if eventTail <> eventHead event := eventBuffer[eventTail] eventTail := (eventTail + 1) & $1F ・・・ writeEvent rdlong t1,event_head mov event_offset,t1 shl event_offset,#2 add event_offset,event_buffer wrlong event_data,event_offset add t1,#1 and t1,#$1F wrlong t1,event_head writeEvent_ret retとなる筈である。このように変更してみても、ちゃんと問題なく動いた。 ということは、本人も書いているように(^_^;)、 公開されているMidiIn.spinにもバグが残っている、という事である。 ちょっと安心した。
さて、いよいよ核心の一つに挑戦するところに来た。MIDI情報のハンドリングそのものである。 Propellerには、シリアル通信のためのUART(専用チップ)などは無い。 Cogのソフトウェアだけで、シリアルでもビデオでも出来るぞ、という事である。 MIDI信号のフォーマット にまで立ち返って、スタートビットとかストップビットまで考える、 というのは久しぶりである。
プログラム内で唯一、いかにもMIDIだ、という数値の定義は、
bitticks := clkfreq / 31_250 halfticks := bitticks / 2である。 シリアル信号の状態を、より高速のソフトウェアでサンプリングして値を調べるので、 変化点のエッジを起点として、「ビット幅の半分」の中間点の値をそのビットの値とするわけだ。 アセンブラルーチンでとりあえずそれらしいのは、
entry mov midiMask,#1 shl midiMask,midiPin mov nextDataHandler,#getMidiByte getMidiByte waitpne midiMask,midiMask mov midiBits,#9 mov bitClk,cnt add bitClk,halfticks add bitClk,bitticks :midiBit waitcnt bitClk,bitticks test midiMask,ina wc rcr midiByte,#1 djnz midiBits,#:midiBit shr midiByte,#32-9 and midiByte,#$FF test midiByte,#$80 wz if_z jmp nextDataHandlerのあたりだろう。 要するに、初期化の後に、Cog(3)の全ての無限ループはgetMidiByteのところにジャンプしてくる。 entry直後の
mov midiMask,#1でまず、midiMaskに1を代入、
shl midiMask,midiPinにより、入力ピンの数値だけこれを左シフトする。 つまり、「入力ピンのビット位置にだけ1が立った値」がmidiMaskである。 そして、
mov nextDataHandler,#getMidiByteによって、nextDataHandlerの中身も、とりあえずは#getMidiByteとなる。 このnextDataHandlerの中身は、MIDIの3バイトメッセージを区別して受ける時に変更される。 さて、肝心のgetMidiByteでの処理は、
waitpne midiMask,midiMaskである。 waitpneの書式は「waitpne State, <#> Mask」とある。 ここで調べて判ったのだが、spinのINAコマンドというのは、 入力の32ビットを全て一気に取り込めるらしい。
Temp := INA[7]とすれば、ポート7の入力が1か0で得られるが(このポートが出力に設定されていればその出力レジスタの設定値)、
Temp := INAとすると、なんとTempには、32ビットの入力が全て、ビット配置で得られるという。 ちなみにINAというのは、将来的にあと32ビット追加して、bit32-63をINBとするらしい。 そこでこの
waitpne midiMask,midiMaskの場合には、入力ピンの位置にだけビットマスクを立ててINAとANDを取って、 つまり指定された入力ピンの状態だけに注目して、 その結果を再びmidiMaskに代入している。 MIDIラインは通常はHighなので、この結果はずっと1であり、 Not Equalなので、何かがMIDIラインに起きるまでは、ここでずっと待ち続けていることになる。 これに続く
mov midiBits,#9 mov bitClk,cnt add bitClk,halfticks add bitClk,bitticksに進むのは、MIDIスタートビットが来てからである。 とりあえずmidiBitsに9を代入、そしてbitClkに cnt + halfticks + bitticks をセットする。 まず現在地のcntをセットしてからhalfticksとbitticksを加算するので、 この演算時間の遅延は影響しない。 ただしちょっと気になったのは、これよりも
mov bitClk,cnt add bitClk,halfticks add bitClk,bitticks mov midiBits,#9の方が、直前のループを抜けた(変化を検出した)直後のcntを得られるので、より正確ではないか、という事である。 スタートビットのエッジが受信動作の起点であるからである。 変更しても動作は同じだったが、こちらに改良した。
さて、スタートビットの開始エッジに続いて調べたいのは、続く先頭ビットの中央の瞬間である。 そのため現在値cntに次のビットまでのビット幅bitticksと、さらにビット幅の半分halfticksを加えたわけである。 続く以下のループが、シリアル受信の根幹である。
:midiBit waitcnt bitClk,bitticks test midiMask,ina wc rcr midiByte,#1 djnz midiBits,#:midiBitローカルアドレスなので、ラベルの先頭はコロンで:midiBitである。 waitcntの書式は「waitcnt Target, <#> Delta」とある。 CogはクロックcntがTargetに一致するまでwaitする。 つまり最初にこのwaitを抜けるのは、スタートビットの先頭エッジから、 MIDI規約で1ビット半の場所、データの先頭ビットの中間点である。 そして、TargetであるbitClkの値にDelta(bitticks)が加えられる。 つまり、次にこのループに戻ってくると、そこでヒットするのは次のビットの中間点、 データの2ビット目の中間点である。 そして実際のデータ取り込みは、
test midiMask,ina wcで行われる。TESTは続くオペランド同士のANDを取った結果をフラグに返すが、 値をロードしない。ANDであればAND演算の結果が第一オペランドにロードされる。 ここではmidiMaskとINAのANDを取り、つまりINAの該当ビットの状態がキャリーフラグ(C)にセットされる。
rcr midiByte,#1は「1ビットだけ、midiByteを右にビットシフトする。MSBにC(キャリー)を入れる」という処理である。 midiByteは初期化していなかったが、毎回更新されるので不要である。 MIDIの先頭ビットはデータのLSBであった。
djnz midiBits,#:midiBitつづくdjnzの書式は「djnz Value, <#> Address」とある。 ループ変数のmidiBitsがデクリメントされ、non zeroであれば指定先にジャンプする。 これが#のある即値アドレス:midiBitである。 ループカウンタのmidiBitsの初期値は9だったので、 このループはストップビットまで9ビットのデータを取得してから次に進む。
shr midiByte,#32-9 and midiByte,#$FFこのSHRコマンドにより、次々にデータ(各ビット幅の中間点)の状態がキャリーに入り、 それがmidiByteに上から格納されてきたのを、さらに「32-9」ビットだけ右シフトしたことで、 midiByteの下8ビットにデータが移動する。 これに$FFでANDをとるとストップビットと上位ビットは消えて、 midiByteには「受信されたナマのMIDI情報」が格納されたことになる。
ここまでの理解が正しいことを確認するために、 外付け回路としてフォトカプラを受けるトランジスタのバッファが2段だったのを、 シンプルに1段にしてみることにした。 シリアル信号としては反転することになるが、インバータ回路を加えることなく、 Propellerアセンブラの変更(反転)で同じ動作になるかどうか、という確認でもある。 まずは回路図を以下に変更した。これをPropeller用MIDI受信のオリジナル標準回路にしよう。
そしてプログラムは、以下のように2箇所の変更で、あっさりと動いてくれた。 waitpne→waitpeqと判定条件を反転させて、 スタートビットの監視をローレベルからの立ち上がりとすること、 $FFとxorする行の追加によって、得られたデータを全ビットまとめて反転すること、の2つである。
getMidiByte waitpeq midiMask,midiMask mov bitClk,cnt add bitClk,halfticks add bitClk,bitticks mov midiBits,#9 :midiBit waitcnt bitClk,bitticks test midiMask,ina wc rcr midiByte,#1 djnz midiBits,#:midiBit shr midiByte,#32-9 xor midiByte,#$FF and midiByte,#$FFPropeller Demo Board上のブレッドボード回路も、トランジスタが1つ減って、スッキリした。
こうなると、もっとMIDI入力メソッドをオリジナルにしたくなる。 Max/MSPと組み合わせて使う場合には、MIDIステータスごとに区別する必要もなく、 要するにMIDIメッセージとして2バイトタイプか3バイトタイプか、であればよい。 呼び出し側でMIDIステータスを見て判定するからである。 そこで、exp010.spinを複製してexp011.spinとリネーム、 MidiIn02.spinを複製してMidiIn03.spinとリネームして、 さらに「使える」ライブラリ化を目指すことにした。 東京卒展・ 卒業式 などしばらく日が開くが、それもプログラミングにはプラスとなるかもしれない。
2008年3月19日(水)
前日3/18の午後から再開して、コピペのミスに起因する単純なバグに悩みつつも、 MIDI受信処理のオリジナル化は無事に完了した。 前のバージョンではMIDIハンドリングが別人のもので分かりにくかったが、 こちらは完全にアセンブラレベルで自前なので、迷うところが無くなった。 この差はとても大きい。Propellerアセンブラにもだいぶ慣れた。 以下が、メインの呼び出し側のexp011.spinである。 MIDIメッセージについてはリアルタイム関係とエクスクルーシブは完全に無視して、 longの4バイトのうち下3バイトを表示する。 2バイトメッセージ(プログラムチェンジ、チャンネルプレッシャー)では、 形式的には3バイト表示で、2バイト目にダミーの0を入れて、実際のバリューは3バイト目である。{{ exp011.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" midiIn : "MidiIn03" PUB Main | dummy Num.Init TV.Start(12) TV.Str(string("MIDI_input_Cog =")) dummy := midiIn.start(7) TV.Str(Num.ToStr(dummy, Num#DEC)) repeat dummy := midiIn.event if dummy <> -1 TV.Str(Num.ToStr(dummy, Num#HEX7))そして以下が、全面改訂したMidiIn03.spinである。 イベントハンドラとジャンプテーブルとサブルーチンコールを無くして、 AKI-H8で慣れ親しんだMIDI受信アルゴリズムに近付けてみた。 ただしAKI-H8の内部UARTのハードと違って、Cogのソフトウェアでシリアル受信している。 当面はこれをオリジナルのライブラリとして使うつもりである。 MIDI受信のFIFOは、メッセージとして3バイトのパックとなっているが、 とりあえず64longsの深さとしてみた。
VAR long rx_Head, rx_Tail, rx_Buff[64] PUB start(_midiPin) : status midiPin := _midiPin rx_top := @rx_Head rx_end := @rx_Tail rx_fifo := @rx_Buff bitticks := clkfreq / 31_250 halfticks := bitticks / 2 longfill(@rx_Head,66,0) status := cognew(@asm_entry, 0) PUB event : status status := -1 if rx_Tail <> rx_Head status := rx_Buff[rx_Tail] rx_Tail := (rx_Tail + 1) & $3F DAT org asm_entry mov midiMask,#1 shl midiMask,midiPin getMidiByte waitpeq midiMask,midiMask mov bitClk,cnt add bitClk,halfticks add bitClk,bitticks mov testBits,#9 :check_loop waitcnt bitClk,bitticks test midiMask,ina wc rcr rx_data,#1 djnz testBits,#:check_loop shr rx_data,#32-9 xor rx_data,#$FF and rx_data,#$FF test rx_data,#%10000000 wz if_z jmp #:running mov t1,rx_data and t1,#%11110000 cmp t1,#%11110000 wz if_z jmp #getMidiByte mov rsb,rx_data mov dcb,#0 jmp #getMidiByte :running mov t1,rsb and t1,#%11100000 cmp t1,#%11000000 wz if_z jmp #:byte_2 tjnz dcb,#:byte_3 add dcb,#1 mov keyno,rx_data jmp #getMidiByte :byte_2 mov event_data,rsb shl event_data,#16 or event_data,rx_data jmp #:write_event :byte_3 mov dcb,#0 mov event_data,rsb shl event_data,#16 mov t1,keyno shl t1,#8 or event_data,t1 or event_data,rx_data :write_event rdlong t1,rx_top mov rx_pointer,t1 shl rx_pointer,#2 add rx_pointer,rx_fifo wrlong event_data,rx_pointer add t1,#1 and t1,#$3F wrlong t1,rx_top jmp #getMidiByte t1 long 0 midiMask long 0 testBits long 0 bitClk long 0 bitticks long 0 halfticks long 0 midiPin long 0 rx_top long 0 rx_end long 0 rx_fifo long 0 rx_data long 0 rx_pointer long 0 event_data long 0 rsb long 0 dcb long 0 keyno long 0 fitMIDI受信が出来て、Max/MSP/jitterとの親和性が出て来たところで、 次にやってみるのは、当然、MIDI送信ということになる。 PropellerのサイトにはMIDI送信のライブラリは何もなかったが、 細部まで理解できたMidiIn03.spinの完成によって、 MIDI送信については参考例がなくてもなんとか出来る気がした。 MIDIでは、受信は非同期シリアルなので、常にデータラインを監視して、 スタートビットのエッジからの受信処理は、裏であまり判定処理などが重いと、 次のデータを読み損ねる危険性があった。 しかし送信については、ゆっくり判定処理して、あとは各ビットを送信するループでは、 ただひたすら、規定のビット幅だけwaitすればいいので、原理的には格段に容易である。
開発においては、定番である「MIDIスルーボックス」、 すなわち「MIDI FIFO経由で受けた情報をMIDI送信FIFOに積んでMIDI送信」となる。 これまでの、ビデオモニタとMIDI受信のCogsと並列動作させることも当然、盛り込んでいく。
まず、外付けのMIDI送信回路としては、Propellerのポート出力が3.3Vレベルなので、 74HC05のスレショルド2.5Vより十分高いと判断して、単純に以下の回路とした。 定番のMIDI送信回路からすると、こちらもデータが反転していることになるが、 受信が反転しているので、混乱のないように、送信もセットで反転、とした。 これをPropeller用MIDI送信のオリジナル標準回路にしよう。
Propellerアセンブラでは、MIDI受信ではビット幅の中間点という時間が必要になったが、 MIDI送信では、スタートビットから後は、全て規定ビット幅ごとにシリアルデータを出せばいいので、 こちらもだいぶシンプルになった。 奮闘半日、手探りでビットを立てたり落としたり実験しながら、 なるべくMIDI受信のアセンブラルーチンと対応できるように注意して、 実際に動いてみたのが、以下のプログラムである。
まず、メインの呼び出し側のexp012.spinである。 ビデオ出力画面には、まずMIDI受信の初期化をしてそのCog IDを表示し、 次にMIDI送信の初期化をしてそのCog IDを表示する。 あとは、リアルタイムやエクスクルーシブを除く、通常のMIDIメッセージを受信して、 先頭1バイトの0を加えたlongの受信FIFOバッファに積み、下3バイトを16進表示でビデオ出力する。 そして、このデータを再びlongの送信FIFOバッファに積んでおくと、 ここから並列処理でMIDIの3バイト/2バイトメッセージが送信される。
{{ exp012.spin }} CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 OBJ Num : "Numbers" TV : "TV_Terminal" midiIn : "MidiIn03" midiOut : "MidiOut01" PUB Main | dummy Num.Init TV.Start(12) TV.Str(string("MIDI_input_Cog =")) dummy := midiIn.start(7) TV.Str(Num.ToStr(dummy, Num#DEC)) TV.Str(string(", MIDI_output_Cog =")) dummy := midiOut.start(6) TV.Str(Num.ToStr(dummy, Num#DEC)) repeat dummy := midiIn.event if dummy <> -1 midiOut.fifoset(dummy) TV.Str(Num.ToStr(dummy, Num#HEX7))続いて以下が、 完全オリジナルのMIDI送信ライブラリの第一号、MidiOut01.spinである。 こちらではサブルーチンコールも、ちゃんと「お約束」を守って使っている。 MIDI送信のFIFOは、メッセージとして3バイトのパックとなっているが、 こちらも64longsの深さとしてみた。
VAR long tx_Head, tx_Tail, tx_Buff[64] PUB start(_midiPin) : status midiPin := _midiPin tx_top := @tx_Head tx_end := @tx_Tail tx_fifo := @tx_Buff bitticks := clkfreq / 31_250 longfill(@tx_Head,66,0) status := cognew(@asm_entry, 0) PUB fifoset(_tx_data) tx_Buff[tx_Head] := _tx_data tx_Head := (tx_Head + 1) & $3F DAT org asm_entry mov midiMask,#1 shl midiMask,midiPin or dira,midiMask :fifo_check rdlong t1,tx_end rdlong t2,tx_top cmp t1,t2 wz if_z jmp #:fifo_check mov t2,t1 shl t1,#2 add t1,tx_fifo rdlong event_data,t1 mov t1,t2 add t1,#1 and t1,#$3F wrlong t1,tx_end mov tx_data,event_data shr tx_data,#16 call #send_event and tx_data,#%11100000 cmp tx_data,#%11000000 wz if_z jmp #:byte_2 mov tx_data,event_data shr tx_data,#8 call #send_event :byte_2 mov tx_data,event_data call #send_event jmp #:fifo_check send_event xor tx_data,#$FF and tx_data,#$FF shl tx_data,#1 or tx_data,#1 mov testBits,#10 mov bitClk,cnt add bitClk,bitticks :bit_send shr tx_data,#1 wc muxc outa,midiMask waitcnt bitClk,bitticks djnz testBits,#:bit_send send_event_ret ret t1 long 0 t2 long 0 midiMask long 0 testBits long 0 bitClk long 0 bitticks long 0 midiPin long 0 tx_top long 0 tx_end long 0 tx_fifo long 0 tx_data long 0 event_data long 0 fitPropeller Demo Boardのブレッドボード上にMIDI受信とMIDI送信のハードまで載って、 以下がこの「高価なMIDIスルーボックス」の写真である。 ビデオ信号での16進数表示まで、Propellerの別のCogで行っていて、 まだ3個のCogは何もしていない。 色々と盛り込んだら、なかなか楽しいチップのようだ。 次にはやはり、A/DとD/A、センサI/F、オーディオI/Oなどだろうか。
あらためて整理・確認すると、このMIDI送信アセンブラルーチンの「肝」の部分は、以下である。 送信したい1バイトデータをtx_dataに入れて、以下のルーチンをCALLしている。
send_event xor tx_data,#$FF and tx_data,#$FF shl tx_data,#1 or tx_data,#1 mov testBits,#10 mov bitClk,cnt add bitClk,bitticks :bit_send shr tx_data,#1 wc muxc outa,midiMask waitcnt bitClk,bitticks djnz testBits,#:bit_send send_event_ret retハードウェアで反転するのでデータをまず反転し、マスクとANDして「ストップビット」を確保する。 次いで1ビット左シフトしてから「スタートビット」を入れる。 ここでMIDI規約のビット幅をタイマとしてセットする。 「shr tx_data,#1 wc」によってこのデータを1ビット右シフトして、 ハミ出たLSBをキャリーフラグに格納し、 「muxc outa,midiMask」によって、このキャリーフラグに対応して、 マスク(MIDI送信用の入出力ピン)したポートのビットのみを出力する。 このループの繰り返しでそれぞれのシリアルのビットを設定し、タイマで待ち合わせる、という事である。
これまで多数のMIDI処理マイコンシステムを製作してきたが、シリアル通信については、 UART(専用ハードウェア)を必ず使ってきた。 マイコンと知り合って25年以上になるが、ソフトウェアでシリアルの1ビットごとを読み込んだり、 上げ下げしてMIDI通信を実現したのは、なんとこれが初めてである。 MITなど世界のIT教育に重点を置いたParallax社であるが、 Propellerというのは、まさに温故知新プロセッサでもある、と痛感した。
Propeller日記(3) へ
「日記」シリーズ の記録