BASIC Propeller Programming
''原題:'Propeller Basic プログラミング'
状態:この文書の翻訳作業は完了しています
原文:nvp5.pdf
翻訳者:caskaz
対象読者:PropBASICの初歩知識(この文書は初期[2010年 3月]のPropBASICを対象にしているので現在のバージョンとは違う記述があるかもしれません))
ParallaxとBASICはそれ自体素晴らしいけどそれがマカロニとチーズみたいに一緒になった。これを使うとまるで魔法使いみたいな気分になれるよ。
PropellerではBASICを使えないので失望している人が少なからずいた。
私のようにずっとBASICを使ってきた人達や組込みプロジェクトに多くの時間を割いてきたグループはPBASICに感謝している。
今やSpinは私にとってオーケーであり、全く心地よいものとなった。
30年ほどBASICや他の言語でプログラミングしてきたがやはりBASICは挫折する人が少ないといえる。
さて、よいおしらせだ。”Bean”の名で知られるTerry Hittが率いたSX/B開発チームがPropeller専用のBASICコンパイラ(PropBASIC)を作った。
もう「BASICが使えないからPropellerは使わないよ」という言い訳はできないよ。
はっきりしている事はPropBASICはアセンブラコードに変換する1パスコンパイラなのでBASICよりも速いということだ。
そのコードは一つ以上のSPINファイルだ。Version1でメインプログラムコードはひとつのCogに限定されているがこれで困ることはない。
Propellerアセンブラは強力なので他のCPUで数行のアセンブラコードを必要とするプロセスはPropellerでは一つのアセンブラコードですむからだ。
そのうえ、まだ7つのCogが残っていて、PropBASICはそれぞれにタスクを割り振る事ができる。そのうちにPropBASICはHub RAMのメモリを使って1個のCogメモリ2Kバイトの壁を越えるようコンパイルできるBill HenningのLMM(large memory model)アーキテクチャも実装するかもしれない。
SX/Bを使ったことのある人にとってはPropBASICはとてもよくわかり、少しの変更で今あるプロジェクトを移植することができるし、もしBASIC Stampを使ったことがあればPBASICに似ている印象を持つと思うよ。
私を含めて多くの人にとってBASIC StampからSX/Bへ乗り換えるのは大変なことなんだ。
でもBASIC StampからPropBASICに乗り換えるのはとっても簡単で楽しいことなんだ。
少し新しい事を学習しなくてはいけないけれど、すぐにマルチコアプロセッサを使えるようになるよ。
どの学習書でも必ず”Hello World”で始まるのがお決まりになってるけど、大きなプログラミングと格闘する前に基礎を覚えるのは大事だ。
マイクロコントローラの分野でそれに相当するのはLEDの点滅だろ?
LEDの点滅さえもできないのに、どうやってGPSを使った多軸ロボット制御なんてできるんだい?
ちらかって汚いプログラムリストは大嫌いだ。最後まできちんとしたプログラムをかく簡単な方法はテンプレートを使うことだ。沢山のプロジェクトを始めるのに使ったtemplate.pbasをダウンロードできる。
template.pbasを使ってPropBASICプログラムの内容を紹介するhello.pbasを書いてみたが、それはあとで披露しよう。
あえて言おう。
SX/BやBASIC Stampを使った事のある人ならPropBASICに乗り換えるのはとっても簡単だ。
DEVICE P8X32A, XTAL1, PLL16X XIN 5_000_000
上記はPropBASICでは定番だ。
これは16倍PLLを使った外部クロックを入力するプロセッサをP8X32Aを宣言している。
外部クロック周波数は5MHzでシステムクロックは80MHzだ(5 x 16)
もちろん定番以外もある。時間にシビアでない用途ならRCモードを選べる.こいつは低パワーというおまけもついてる。
RCモードのシステムクロックはRCFASTで大体12MHz、RCSLOWで大体20kHzだ。
RCFASTの設定は
DEVICE P8X32A
RCFASTの設定の場合はなにも指定しないけれどRCSLOWの場合はRCSLOWを指定しなければならない。
RCモードでは必ずPLLは1倍でXINは無視される。
繰り返すが、RC発信周波数はチップ単位で異なるのでRCモードは時間にシビアでない用途で使うべきだ。例えばシリアル通信が必要なプログラムではRCモードを使わないのが正しい。
スポックが今の文章を読んだら眉毛を少しつり上げるかもしれない。(カーク船長やドクター・マッコイに対してするように)
RCモードがシリアル通信に不向きなら、RCモードで動作しているPropellerチップはどうやってIDE(Propeller Tool)と通信しているのか?と。
よい質問だ。
実際IDEはダウンロードプロトコルの役割としてPropellerと拍子を合わせて、個々のチップに適応するためにIDEからボーレートを調整している。
だから、消費電流を減らすために低クロックスピードにしてシリアル通信を使いたい場合は下記のステップが必要になる。
・RCモードのクロックスピードを測定(FREQディレクティブはなし)
オシロを使って出力パルスの巾を測定して計算
・XINの替わりにFREQディレクティブでRC周波数を修正
10msecのパルスを出力するプログラムを作って(speed_test.pbas)オシロでパルスを計ってみた。−やってみるとパルス巾は9.06msecだった。
これはプロセッサは予想より少し速いクロックで動作しているということだ。
以下のように測定したパルス巾を予想していた10msecで割った値をscale factorとして計算しよう。
Scale = 10.0/measured
私の場合、scale factorは1.1038だった。クロック周波数20kHzのRCSLOWを選択しているのでプログラムの先頭に下記の調整値を付け加える
DEVICE P8X32A, RCSLOW FREQ 22_075
FREQ設定でコンパイルすることにより、PAUSEやSEROUTでも正しい時間管理がされる。
テンプレートの次の項目は定数定義だ。数字とキャラクタ文字は同じように扱える。
OnTime CON 250 OffTime CON 750 IsOn CON 1 IsOff CON 0 Asterisk CON “*”
プログラムでSERIN,SEROUTを使う場合はボーモード定数の設定もここで扱う。
Baud CON “T9600”
上記ではSERIN,SEROUTをTrueモードで使う場合の定義を示している。
PropBASICでは定数はグローバルとして扱われ、どのCogでも使用できる。
LED PIN 16 LOW
PropBASICではI/O ピンの宣言はPBASIC2.5やリセット後にピン状態を定義しておくSX/Bとよく似ている。ピンのオプションはINPUT(default),OUTPUT,LOW,Highなどである。
通信プログラムでこのピン定義を使用している。
RX PIN 31 INPUT TX PIN 30 HIGH
HIGHオプションの使用に注意:シリアル通信のTXピンをアイドル状態(output,high)にしておくようにスタートアップコードにおいている。
PropBASICはI/Oピンのグループを定義することもできる。例えばPropellerDemoBoard?のLEDはP16からP23に接続されている。これらのLEDをグループとして定義している。
LEDs PIN 23..16 LOW
ピングループ定義で最初の数字がグループの最上位ピン(MSB)、2番目の数字がグループの最下位ピン(LSB)となっている。数字の順番は重要でグループが入力としてセットされている場合は読み出し、出力でセットされている場合は書き込みのビットが影響をうける。
PropBASIC1.0では出力ピングループにバイナリを割り当てることもできる。
LEDs = %100000
上記はOK、しかし
LEDs = 1 << position
はNG。これは将来実装されるかもしれない。現在のところは下記のようにする。
__temp = 1 << position LEDs = __temp
PropBASICでは変数及び配列をHubに保存しており、それらはどのCogからもアクセスできる。
Hubの変数はbytes,words,longs,全部のタイプのarraysで定義される。
rxHead HUB Byte = 0 rxTail HUB Byte = 0 rxBuffer HUB Byte(64) = 0
PropBASICではHub変数の初期値を設定できるが、初期値設定されない場合はゼロにセットされる。(配列の場合は全部の要素)
Hub変数は通常の変数のようには扱えず、それらのread/writeには必ずRDxxxx/WRxxxxを使用する。(xxxxは変数のサイズ)
変数と同じようにHubにあるデータも共有できる、そして32KバイトのRAMには大きなテーブルデータも作れてどのCogからもアクセスできる。
Hubのデータはbytes,words,longsとして、それぞれDATA,WORD,LDATAディレクティブで宣言する。
Zip4 DATA %0001 DATA %0010 DATA %0100 DATA %1000
Hub変数のようにRDxxxx/WRxxxxを使ってHubのデータにアクセスする。
大きなテーブルデータの場合はFILEディレクティブを使って外部データを取り入れる。
SFX1 FILE “BABY.WAV” DATA 0
PropBASICで2個以上のCogを使用して「裏プロセス」の処理を実行するにはTASK定義が必要です。BASIC Stampを使った経験があったらシリアルデータの入力データのwait処理で悩んだことがあると思う。SX/Bではシリアル入力データ処理の為に割り込みを使っていました。しかし、もう気にすることはない。
PropellerではHub変数を使ってシリアル入力処理を別のプロセッサに行わせます。TASK定義は単純だがタスクが起動する前に設定する必要がある。
SERIAL_RX TASK
プログラムやタスクで使う変数はCogにとってローカルでLongとしてのみ定義できます。
Hub変数として配列宣言や初期値設定ができる。
Idx VAR Long
PropBASICはコンパイラによって生成されるアセンブラコードで使用されるいくつかのローカル変数(サブルーチンや関数内部で使うパラメータのような)を生成する。
コンパイル出力を見ると5個の変数__temp1〜__temp5があります。これらはPropBASICキーワード用に生成されたコードで使われます。又作成するPropBASICプログラムでこれらの変数を使用できますがその為には注意が必要だ。これらの変数はキーワードが使われたら変化してしまうから。
変数__param1〜__param4ぐらいは利用してもいいでしょう。これらはパラメータ渡しの時だけ使い、PropBASICキーワード用に生成されたコードで修正されたものを使ってはいけない。
始めに述べたようにPropBASICは1パスコンパイラでアセンブラコードを最適化しません。
良いニュースは出力コードを検査することでアセンブラを習得でき、コードスペースを最適化できることだ。
SX/Bの初期には意味がないと思われるコードを削除していたものだ。(PropBASICでもきっと同じ事が行われていると思う)
例えばPAUSEはよく使われる、このPAUSE 100のPASMが下記だ
mov __temp1, cnt adds __temp1, _1msec mov __temp2, #100 __L001 waitcnt __temp1, _1msec djnz __temp2, #__L001
オーケー、もしPAUSEが出現する度にアセンブラコードを生成したら酷いコードになるだろう。これはサブルーチンにPAUSEをカプセル化したら解決できる。
SX/BのようにDELAY_MSという名前のシェルは下記
DELAY_MS SUB 1
DELAY_MSは1個の引数が必要だ。Propeller変数は32bitなのでそれはとても長い遅延時間でもOKだ。
SX/Bを知ってる人にはPropBASICのFUNC宣言は少し違うと思うだろう。
1月にPropeller ForumメンバーがSIRCSのPropBASICへの移植について質問していた。
SIRCSオブジェクトはIRコードビットをビットカウントのようにリターンするものだ。
PropBASICは関数が返すパラメータ数を気にしない。(通常は1個)
それ故、定義は下記
SIRCS_RX FUNC 0
LED blinkerプログラム本体は大体下記のようになる
PROGRAM Start Start: FOR idx = 1 TO 3 LED = IsOn DELAY_MS OnTime LED = IsOff DELAY_MS OffTime NEXT DELAY_MS 1_000 GOTO Start
PROGRAMディレクティブはユーザが作ったプログラムの始点と、さらに時度生成start-upコードの位置も示している。
Propellerではstart-upコードはディレクティブの指すラベルへジャンプする前にI/Oピンの入力・出力と状態の設定も簡単にやってのけている。
見てのとうり、コードはまったくのBASICだ。PBASICやSX/B、他の言語を扱った人達にとっても簡単だろうと思う。
サブルーチンと関数のコードはリストの最後に書くことになっているのでDELAY_MSという名前のサブルーチンをリストの前部で定義してコードを書いてみた。
SUB DELAY_MS PAUSE __param1 ENDSUB
他の言語同様にサブルーチンは別のサブルーチンをcallすることもできる。ターミナルへのデータ送信のプログラムで2つのサブルーチンを書いた。
SUB TX_STR strAddr VAR __param2 strChar VAR __param3 strAddr = __param1 DO RDBYTE strAddr, strChar IF strChar = 0 THEN EXIT TX_BYTE strChar INC strAddr LOOP ENDSUB SUB TX_BYTE SEROUT TX, Baud, __param1 ENDSUB
2番目のサブルーチンはSEROUTのシェルで、最初のサブルーチンはシリアルポートに文字を送信するのに使う。
文字のアドレスを引数にしてTX_STRをcallするとその引数は__param1に渡される。
TX_BYTEに文字を渡すのに__param1が必要なのでサブルーチン内部で__param2,__param3を使っている。
新しい変数を宣言しない理由はPropellerでは全部がRAMにあるからだ。
その為、できるだけ__paramx変数を使い変数宣言を少なくしてアセンブラコードスペースを節約するのはよい習慣だ。
多分キャラクタコードを得るのにRDBYTEを使ってるコードに気付いてると思うがPropBASICでは全ての文字がHub RAMにある。もちろん、DATAディレクティブで文字列の定義もできる。
Banner DATA “PropBASIC”, 0
関数のコードはサブルーチンコードと比較するとFUNC..ENDFUNCブロックと関数の呼出し側に1つ以上の戻り値(ENDFUNCの直前にRETURNをおく)があることを除けばよく似ている。下記はPropBASICで書いたSIRCS受信関数だ
FUNC GET_SIRCS irCode VAR __param1 irBits VAR __param2 COUNTERA NEG_DETECT, IR, 0, 1 COUNTERB FREE_RUN, 0, 0, 1 Wait_Start: WAITPEQ IR, IR PHSA = 0 WAITPNE IR, IR PHSB = 0 WAITPEQ IR, IR IF PHSA < BIT_S THEN Wait_Start irCode = 0 irBits = 0 Check_Frame IF PHSB > MS_044 THEN IR_Done Wait_Bit: IF IR = 1 THEN Check_Frame PHSA = 0 WAITPEQ IR, IR irCode = irCode >> 1 Measure_Bit: IF PHSA > BIT_1 THEN irCode = irCode | $8000_0000 ENDIF INC irBits IF irBits = 20 THEN IR_Done GOTO Check_Frame IR_Done __temp1 = 32 – irBits irCode = irCode >> __temp1 RETURN irCode, irBits ENDFUNC
まったくそのまんまのコードだと思う。これはアセンブリコードに変換されるので大変速い。だからRC-5のような別のIRプロトコルで実験したい場合はhigh-level-code(sircs_rx.pbas参照)を使った方がいいよ。
PropBASICはSIRCSの例のようにPASMにあるWAITPEQ、WAITPNEなどのSpinやPASMのキーワードもいくつか持っていることを指摘しておくよ。
タスクは別のCogで動いてるのでその構成はサブルーチンや関数よりも少し込み入ってる。
実際、タスクはそれ自身が1つのプログラムなのでその中でのみ定義されるサブルーチンや関数があったりもする。
これらの宣言やコードの全部がTASK..ENDTASKブロック内にある。
TASK SERIAL_RX rxb VAR __param1 hPntr VAR __param2 DO SERIN RX, Baud, rxb RDBYTE rxHead, hPntr WRBYTE rxBuffer(hPntr), rxb INC hPntr hPntr = hPntr & $3F WRBYTE rxHead, hPntr LOOP ENDTASK
そのとうり、これが全部だ。タスクはCON,PIN,HUB宣言もできるしこれら全部の要素を含んでいる。
DO..LOOPの最初でSERINを使ってバイトデータを読んでいる。- PBASICやSX/Bでやってるように。
違いはこのタスクはそれ自身のCogで起動されシリアルデータを待っていてもメインプログラムの邪魔をしないことだ。
現在のヘッドポインタのデータをRDBYTEで読み出しrxBufferにそのオフセットを加えてWRBYTEで新しいシリアルデータを保存できる。
ヘッドポインタを+1してバッファ内に収まるように$3FとANDして、呼出し側が使っているHubメモリに書き戻している。
タスクをスタートするには2種類あるがたいていCOGSTARTを使う。
COGSTART SERIAL_RX
別のCogが立ち上がり準備が整うまで十分な時間は必要なので、タスクで供給したデータにアクセスしようとする前に数ミリ秒待たせたいこともあるかもしれない。
PropBASICコンパイラはタスクの名前でSpinファイルも作れるので、同一フォルダにあるかもしれないSpinプログラムを上書きしないようにタスクの名前に注意する必要がある。
オーケー。シリアルデータを受信しバッファリングする方法はわかった。ではメインプログラムでそのデータを使うにはどうしたらいいのだろう?
バッファからデータを取り出す関数を書いたよ。
FUNC RX_BYTE rxh VAR __param1 rxt VAR __param2 rxchar VAR __param3 DO RDBYTE rxHead, rxh RDBYTE rxTail, rxt LOOP UNTIL rxh <> rxt RDBYTE rxBuffer(rxt), rxchar INC rxt rxt = rxt & $3F WRBYTE rxTail, rxt RETURN rxchar ENDFUNC
最初のループでヘッドとテイルポインタの値を比較してデータを得ている。
バッファが空だとこの値は等しく、この値が違うとバッファインデックスとしてテイルポインタを使って受信データを得る。そしてヘッドポインタで行ったようにテイルポインタを+1してHubメモリに書き戻している。
別のCogがシリアルバッファにデータを保存するやっかい事をやってくれているのでバッファが空でもメインプログラムの邪魔をする事はない。
シリアルバッファで待つバイト数を戻す関数があるよ。
もしゼロならバッファが空なので受信データをただ待つだけのRX_BYTEをスキップする。
FUNC RX_CHECK head VAR __param1 tail VAR __param2 bufcnt VAR __param3 RDBYTE rxHead, head RDBYTE rxTail, tail IF head >= tail THEN bufcnt = head – tail ELSE bufcnt = tail – head ENDIF RETURN bufcnt ENDFUNC
これはバッファの先頭と最後尾間の差をリターンする。バッファはリング形式でtailはheadに追随し、headポインタがtailポインタより小さくなって負数にならないようにIF..THENを使っている。
難解なアセンブラ言語から解放してくれて速いプログラムをコーディングできるので沢山の人がこのコンパイラを楽しむと思う。
でもそれでもアセンブラうを使うのはやっぱり便利だ。
PropBASICでは複数行ならASM..ENDASMブロック、1行なら”\”を使ってコーディングできる。PropBASICはとってもナイスなコードを作ってくれるのであまりインラインアセンブラを使う必要はないけれど知っておいて損はないと思うよ。
ここだけで多様な機能をもつPropBASICを述べる事はできない。でもPropBASICとは何かとか初め方はわかってもらえたと思う。SX/B同様、条件付きコンパイルディレクティブもあるしPropBASICファイルに外部のアセンブラコードを読み込ませることもできる。
アプリケーション初期バージョンとしては本当によくできている。しかもプライスレスだ。
一言でいえばワクワクしているんだ。
SX/Bを使ってた人達はもちろん、初心者にとってもこんなにナイスなツールでプログラミングを始められることを伝えたい。
PropBASIC1.0の機能はSX/Bのよりも少し進んでいる。
アセンブラ言語の速度で大きなプログラムを作れるLMMへの移行などBeanに色んな提案をして言語を拡張するには時間がかかる。