Propeller日記 (2)

長嶋 洋一


Propeller日記(1)

Propeller日記(3)

Propeller日記(4)

Propeller日記(5)

続・Propeller日記(1)

続・Propeller日記(2)

続・Propeller日記(3)

続・Propeller日記(4)

続・Propeller日記(5)

続々・Propeller日記(1)

続々・Propeller日記(2)

続々・Propeller日記(3)

続々・Propeller日記(4)

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では、

  • P0-P7がフリーで空いている→ユニバーサル基板に接続可
  • P8-P11はマイク入力のA/Dとヘッドホン出力アンプ用出力
  • P12-P15はRCAビデオ出力用のD/A
  • P16-P23は、LEDが接続されるとともに、VGA出力のための抵抗が相互を連結
となっている。そこで、PDFとは決別して、Propeller Demo Boardに対応させて、 以下のようにポートの割り当てを変更することにした。 LEDについては、抵抗を経由して、本来の点灯処理と異なる見え方がある可能性を留意すればよい。

実際に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が、

  • 1秒間、連続して点灯
  • 1秒間、細かく点滅
というのを交互に繰り返している。 これはまさに、P16への出力がWired-ORされている、という事である。 このexp002.spinの例では、Mainメソッドとしては、この4行のステートメントは終了している。 しかし、それぞれの中で呼び出されたCogは無限ループを走っているために、 ビデオモニタの文字もLEDの点滅動作も連続している。 プログラムの方としては、「現在いくつのCogが走っているのか」を気にしなくていい、 と言えばそうだが、ちょっと落ち着かない気分でもある。 これはまた追って、実験をしてみることにしよう。

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 == MAXLEDS  

Labテキストによれば、このスイッチ入力と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種類も解説する、 とあったが、いちいち試すほどソソラレないのでパスすることにした。 さらにfromtoを使って、「20までカウントするループ」を3種類、加えて解説する、というのもパスした。 こんなのは実際に動くソフトを開発する時にちょっと実験すれば判ることだろう。 Labテキストではさらに、

  • Propellerの演算子のボキャブラリ
  • シフト演算子
  • ifelseなど
  • VARブロックの変数 : byte、word、long
  • Limit Minimum "#>" Operator
  • Limit Maximum "<#" Operator
  • 比較演算子と条件
と続いたが、これもPropellerマニュアルのリファレンスにある事なのて、パスしておいた。 さらに続いて、
  • 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 Stack

STEP 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.

(エキサイト翻訳) この物は、MIDIの流れからプレーするために私のオースチンPipe Organを備えるために私のプロジェクトの一部として作成されました。 私はおよそ20年前に器官を取得しました。((その時、パイプオルガンが洪水で破損した後にそれを電子オルガンに取り替えると決めました)それを所有していた教会)。 私は、8051年のマイコンに基づいてそれのためにMIDIコントローラを造るために親友と共に働いていました。 私たちは実際にすべて働かせました--私は私自身のサーキット板をエッチングしました、そして、世界における最初のMIDIできるパイプオルガンの1つ。 しかし、システムは、よく老朽化しないで、変更する不可能に難しかったです。 数年前に、器官は、プレーするのを止めました。 私は、新しいコントローラを造るのをいくつかの可能性を見続けていました--ParallaxからのJavelinチップは非常に有望に見えました(私は仕事でJavaのプログラマでした)、私が、それがMIDIの31.25KBのボーレートに対処することができないのがわかるまで。 SXプロセッサは仕事したかもしれません、そして、Propellerチップが発表されたとき、私はそれらを調べ始めていました。 器官は現在私のパンこね台実現からプレーします、そして、私には、すぐ、ProtoBoardバージョンアップと走行があるべきです。

とのことである。 やはり、普通じゃなかった(^_^;)。 ドキュメント中にあった回路図は以下であるが、本人も 「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以上の電圧がかかって、壊してしまう危険性がある。(^_^;)

そこで、手元にあるいつものフォトカプラ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に自由に定義できる。 ローカルなラベルにしたい時には先頭に「:」を付ける。 ラベルはJMPCALLCOGINITで使われる。

  • #」は、その値をそのまま指定するという意味。 「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

                        fit

2008年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箇所の変更で、あっさりと動いてくれた。 waitpnewaitpeqと判定条件を反転させて、 スタートビットの監視をローレベルからの立ち上がりとすること、 $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,#$FF

Propeller 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

                        fit

MIDI受信が出来て、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

                        fit

Propeller 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) へ

「日記」シリーズ の記録