''Spin Baby Spin'
''原題:'Spin 愛しのSpin'
状態:この文書の翻訳作業は完了しています
原文:NV134.pdf
翻訳者:caskaz
対象読者:Spin言語についての初歩知識
今月はPropellerマルチコントローラの紹介をしよう。
高水準Spinプログラム言語でプログラムして同時にマルチプロセッサを走らせる能力を持つチップの特徴を話そう。
先月述べたように実際僕はアセンブラ言語に熟練するような仕事をしていないし
いくつかのプロジェクトでお手上げになってしまった事もない。
Propellerを使えば僕でも(君にも)アセンブラを習得せずに複雑なマルチプロセッサのプロジェクトを作れるようになる。なぜそれをしたいのか?
センサをモニタし処理をするデバイスがあったとしよう。
リアルタイムでデータ表示処理を行うプログラムが表側で動作し、Cogの一つがその裏側でセンサのモニタを行うようにPropellerが働いている、なんてクールだろう。
これが今月やろうとしていることなんだ。
簡単なプログラムでポテンショメータの値を読み、その値をシリアルLCDに表示するのが目標だ。
ポテンショメータの値を読む為にBASIC StampのRCTIME文を真似たrctimeと言う名のオブジェクトを作ろう。簡単だろう?
そしてコードを書いて表側のプログラムとは完全に独立して動作し、表側では表示だけで済むように出力値をスケーリングする役割のようないくつか便利な機能も盛り込もう。
雑談はもう十分だ、始めよう。アーカイブを解凍してrctime.spinファイルをオープンしよう。これのオリジナルはBeau Schwabeが作ったプログラムだ。
このオブジェクトの肝はrctimeメソッドだ。
PUB rctime(pin, state, zofs, div, rcAddr) | rc_temp if lookdown(pin : 0..27) state := 0 #> state <#1 zofs #>= 0 div #>= 1 repeat rc_temp~ outa[pin] := state dira[pin]~ waitcnt(clkfreq / 2000 + cnt) dira[pin]~ rc_temp := cnt waitpne(state << pin, |< pin, 0) rc_temp := || (cnt – rc_temp) rc_temp := (rc_temp – zofs) #> 0 rc_temp /= div long[rcAddr] := rc_temp if mode == 0 quit
rctimeメソッドをpublicにしている。こうするとテスト時などにコールしたり、スタートメソッドを使ってCogで起動することもできる。
このメソッドにはいくつか引数がある。pin(使用するI/Oピン)、state(RC回路のコンデンサ初期状態0又は1)、zofs(直列接続した保護抵抗の影響を補償するゼロオフセット)、div(出力値をスケーリングする除数)そして最後はrcAddr(メソッドがアップデートする変数のアドレス)
ローカル変数rc_tempもある、これはターゲット変数にコピーされる前のRC時定数値の保存スペースとして使われている。
Basic Stampのようにテストでコールしてコードをみてみよう。Figure134.1のような標準回路でゼロオフセット、スケーリング除数を使わずにポテンショを読んでみる。
rctimeをコールするには下記のようにする:
pot.rctime(0, 1, 0, 1, @potVal)
rctimeメソッドのリターン値をコピーする代わりにターゲット変数のアドレス(オペレータ@を使用)を引数にしている。これには理由がある。(後述)
rctimeメソッドのコードを見てみよう。ピン入力を制限してスタートしている。RC回路をEEPROM I2CラインやPropellerpプログラミングピンに接続するのはお薦めできない。
それを防ぐためにlookdownで0..27を使っている。
PBASICのlookdownは1からN,lookdownn(Propellerではlookdownz)は0からN-1をリターンすることを思いだしておくれ。Spinでも同じだ。
lookdownを使うことで非ゼロ値はピンが範囲内にあることを意味している。これはSpinで僕が好きな技の一つだ。それは下記よりエレガントだと思う。
If (pin => 0) and (pin =< 27)
特にlookdown(lookdownz)はリストに区切り文字のコンマを使える。
pin を評価したら次のステップは stateとzero offsetとdivisorが有効な値であることを確認する。
これはlimit演算子で簡単に処理できる。
ここでRC測定の仕組を説明しよう。
まず、変数rc_tempをクリアしピンを出力にセットして希望の状態にする。RC回路の充電(1)又は放電(0)することで状態設定する。
220オームの抵抗と0.1uFのコンデンサを使うと充放電は110us必要だ。もっと大きな抵抗にしたり部品を替える事で500usにすることもできる。
コンデンサを充電してからピンを入力にセットしてシステムカウンタ(cnt)の値をrc_tempにコピーしてピンの状態が変化するまで待つ。(大体ピン入力がVdd/2のところで変化する)
状態の変化をモニタするにはwaitpneを使う。これは1つ以上の選択したピンがターゲット値と合致しなくなるまでプログラムを休止させるコマンドだ。
waitpne(target, mask, 0)
targetはI/Oピンの目的の状態、maskはPropellerの入力とANDする値、そして”0”はポートAである(これは将来I/Oが64bitになったときの為のリザーブである)
PropellerのピンをmaskとANDしてtargetと一致するとメソッドは次へと進む。
このプログラムでは1本のピンだけだが必要なら32本までのピンをモニタできる。
例えば4ビットのバイナリスイッチがA3..A0に接続されていた場合スイッチが%1010にセットされたらプログラムを休止させたい、waitpneは下記のようにする
waitpne(%1010, %1111, 0)
4本全部A3..A0が比較に使われるので、%1111をmaskとして使う。
rc_timeメソッドでは1本のピンのみを監視するので、コンデンサの状態をピン番号でシフトすることでtargetをセットしている。pin maskはデコード演算子(|<)で作っている。これは指定したビット数だけ左シフトして働く。
もし、rc_timeがI/OピンA3を使っていたら、maskは%1000となる。
オーケー、ピンの状態が変化したら、充放電サイクルの期間に進んだカウント数を得る為にシステムカウンタからrc_timeの値を減算する。
絶対値(||)はリターン値を正にする。(bit31がセットされるとSpin数値演算処理はその値を負とみなす)zero offsetを減算し、divで測定値を除算する。
結果はターゲットアドレスにLongで保存される。
これはBASICのPOKEをつかってるような感じだ。
ターゲットアドレスはメインRAMであることに注意してくれ。
このプログラムはrctimeメソッドを別のCogに実行させているからだ。全てのCogはメインRAMにアクセスできる。
けれどもその前に 全てのコード全体がrepeat loopで囲まれているのでrctimeをどうやって終了させるのか不思議にかんじているかもしれない。
rctimeがコールされる時、グローバル変数modeがゼロにされている。
rctimeメソッドの最後でその変数はチェックされ、もしゼロならrepeat loopはquitで終了する。
裏側でメソッドを走らせてターゲット変数を定期的にアップデートする為にどうやって別のCogにrctimeメソッドを走らせるのか、楽しそうだろ?
rctimeオブジェクトのスタートメソッドの詳細
PUB start(pin, state, zofs, div, rcAddr) : okay stop mode := 0 okay := cogon := (cog := cognew(rctime(pin, state, zofs, div, rcAddr) , @stack)) > 0 if okay mode := 1
startメソッドが最初にstopメソッドをコールしているのは奇妙だと思うだろう。
キチンとしなければならない事はオブジェクト(この場合はpot)を割り当てる事だ。
もし、リスタートするならそのオブジェクトはまず止めなければならない。そして、同時にメモリに同じオブジェクトを複数持つ事ができる。
もし、いくつかのオブジェクトを裏側で使うなら対応するいくつかのCogが必要だ。
そして、他に影響をあたえずに止めたりリスタートできる。
次のステップはmode変数をクリアすることだ。実際には動作していないのを除いてrctimeが別のCogで動作している事をトップオブジェクトで知るようなコードにしたくないので、それをcognewで行わせる
未使用のCogがあればcognewでrctimeをそのCogで起動できる。その場合、cog,cogon,okayはrctimeで使われるcogナンバーだ。
一旦rctimeがアップし起動したらメソッドが稼働中を保持する為にmodeを1にセットしてターゲット変数を自動的にアップデートする。これはがターゲット変数のアドレスを渡す理由だ。表側のプログラムで何かの相互作用がなければ変数をアップデートする。これはとってもクールだ。
裏側のプロセスを動作させるのにアセンブラを習得する必要はない。それってクールだろ?
自由に使える8個のCogで想像できるどんな事も多くの事が新しくなり広々となる。
僕はSpinに蹴飛ばされながら、ある種の感情をもって睡眠も惜しんでプログラミングした事を率直に言おう。
その感情というのは僕が12年間BASIC StampのプログラミングをしてきたのはPropellerを評価する準備期間だったことだ。
オーケー、rctimeの利点を得る表側ののプログラム(トップオブジェクト)を見てみよう。
CON _clkmode = xtal1 + pll16x xinfreq = 5000_000 LCD_PIN = 0 LCD_BAUD = 19_200 LCD_LINES = 2 POT_PIN = 1 VAR long potVal OBJ lcd : “debug_lcd” pot : “rctime” PUB main if lcd.start( LCD_PIN, LCD_BAUD, LCD_LINES) lcd.cursor(0) lcd.cls lcd.backlight(true) if pot.start(POT_PIN, 1, 1520, 642, @potVal) repeat lcd.home lcd.def(potVal, 5) waitcnt(clkfreq / 5 + cnt)
上記は全く簡単なコードだ。表示デバイスにParallaxシリアルLCDを使ってる。
SEETON BPI-216表示デバイスを使っても問題はない。
debug_lcdと同じメソッドがあるオブジェクトdebug_BPI-216を作ればいいだけだ。
変更点はボーレートが9600とlcdのオブジェクトファイルだ。他は内部で処理される。
pot.startメソッドの後にあるrepeatループをチェックしてみよう。
pot.rctimeメソッドを使ってないことに気が付いたと思う。rctimeメソッドは別のCogで動作して自動的に更新しているからだ。
Rctimeが更新している間にメインプログラムからpotValを読んでしまう心配はない。
Propellerは一度に1個のCogだけがメインシステムRAMにアクセスできる、だから衝突は起きない。
このセクションを要約する前にzofs(1520)とdiv(642)の引数について不思議に思っているかもしれない。
この値は経験的に3つのステップに由来する
一番目のステップは”0”(zofs)と”1”(div)でpot.startを実行することだ。
ポットをゼロポジションに回した時pot.startは1520をリターンする。
これはどこからきたのか?小さな放電遅延時間はは220オームの抵抗値のせいだ。
二番めはzofsに1520を代入してpot.startをもう一度実行する
ポットの位置は先程と同じはずなので今度はポットを最大まで回して出力をみる
僕の場合、その値は大体64125だった(ブレッドボードのノイズ等の影響で変わる)
ポットを1〜99で読みたいのでdivisorに642を使った
3番目はこれをチェックすることだ。ビンゴ!ポットの読み取り値は0〜99だ
Stacking It All Up
オーケー、今月はまとめる前に話さなければならない技術細部がある
SpinプログラムがCogで動作している時それは用意されたstackと呼ばれるRAMの領域を必要とする。 それは処理中の値の変化を保持している(つまり、演算実行中に使われている)
かつてマルチスレッドのデバイスの仕事をしていた時、新しいスレッドの為のスタック設定の指導は大きめに取っておいて、それからスレッドがクラッシュするまでスタックを減らすというものだった。僕は全く正しい方法ではないと思っていたよ。
なぜスタックを大きくしないのかそしてそなぜそれを怠るのか?
それはコードスペースの浪費だからだ。メモリは賢く使うべきだ
良いニュースがある:PropellerのCogの一つを使って別のオブジェクトの使用スタックを決める事ができる。僕にこれを作れるぐらいのスキル持ってたら良かったんだけどね。
でもこれを利用することはできる。
これを使うとmulti-cogプロジェクトの開発を始めるのが楽になる。
ファイルに含まれているのはstack_monitor.spinというオブジェクトだ。
これはPhil Pilgrimが作った。長い間Parallaxの友人であるPhilは大変面白い製品を作った。
stack_monitorは1個のCogで動作するオブジェクトだ。
これは別のオブジェクトがスタックとして使う一群のメモリを監視する。
それはスタックを使う、オブジェクトのstartメソッドで最初に別のCogに実行させなければならない(それ自身のstartメソッドで)
そしてLEDにつないでいるMSB〜LSBのピンにスタック使用出力をバイナリの形式で出力する
rctimeオブジェクトのstartメソッドの最初の行に配置
stackmon.start(@stack, 32, 23, 16)
これは32の初期サイズを持つスタックをコールした配列を指定している。
僕はPropellerDemoBoard?を使っているのでLEDsはピンA23..A16を出力として使っている
さて、トップオブジェクトへ戻っても実行しよう
僕の場合、LEDsはピンA19..A16が点灯している、だからスタック使用数は15longsだ
オーケー、動作したかい?stack_monitor.startメソッドはターゲットのスタック領域全部にあるパターン(FILLERという名の定数)を書き込んでおく。
PRI monitor(addr, size, ledMsb, ledLsb) | idx, used outa[ledMsb..ledLsb]~ dira[ledMsb..ledLsb]~~ repeat used := 0 repeat idx from addr to addr + size * 4 step 4 used -= long[idx] <> FILLER outa[ledMsb..ledLsb] := used
メソッドは実際全く小さい。
これはLEDをクリアしてスタートする、そして無限ループに入る前にそれを出力にする
ループ内で使われる変数はクリアされ、それからチェックするスタックの要素がFILLERパターンにマッチするか走査していく。このコードは本当に利口だ。Usedを修正する行を見てみよう。
減算してるだろう?実際オブジェクトのスタックで使うlongsの数を加算したいのにこれっておかしいよね。
なぜこれが動作するのか:式の右側(-=の後ろ)はtrueかfalseとして評価される。
大事な事はSpinではtrueは-1と定義されていることだ。だから-1の引き算は+1の加算と同じ意味になる。ゼロ(false)の引き算はなにも変化しない。これってクールだろう?
オーケー、これで充分だと思う。
Propellerと3ヶ月の勉強で充分にわかったと思う。自分自身が勤勉だと覚えておいてほしい。
これはPBASICから出発する大きなチャンスだ、そのパワー全部が君の自由になる。
当然その使い方を学ばなければならない。
もし自分では実現出来ない特殊なアイデアがあれば僕にそれを送ってくれ。
そこから別のものを作るかもしれない。
次回まで楽しくぶん回そう!