先日公開した、Chromebook・Windows・Mac・各種スマホで使えるwebUSBの技術を用いたプログラミング教材について、技術的な解説・カスタマイズ紹介の第4回目です。今回は上級者向けの内容をお伝えします。
教材自体の作り方・導入方法はこちら↓
改良編第1回はこちら↓。本編では省略されたアナログ入力や気圧センサの使い方、Chromebookでの開発方法などを紹介しています。
改良編第2回はこちら↓。モータの追加、プログラミングアプリ側の改造方法などを紹介しています。
改良編第3回はこちら↓。教材本体との通信書式やMIDIデバイス化してiOSとUSB通信する方法などを解説しています。
第4回目では、プログラムの実行コードに関する説明をしていきます。
作成したプログラムが実行される流れ
まずは、プログラミングアプリ上で作成したブロック形式のプログラムが、どのような過程を経てロボットで実行されるのかを説明していきます。
作成したプログラムは、まずプログラミングアプリ内部でバイナリ形式の実行コードにコンパイルされます。続いて通信メッセージによって、教材本体のEEPROMに転送(書き込み)されます。プログラムの転送が終わったら、教材本体のメモリマップを操作して、プログラム実行開始を支持することで、教材が本体に書き込まれたプログラムを開始します。教材が現在プログラムを実行中か、またどの処理を実行しているかという情報は常にメモリマップ上で更新されるので、プログラミングアプリ側は読み取ったメモリマップ情報を元に、現在実行されている命令ブロックを表示しています。
メモリマップのアドレス0x02(EXEC_MODE)が、プログラムの実行状態を表します。Arduinoのソースでは下記の通りに定義されており、このアドレスに1(EXEC_START)を書き込むと、ファームウェアがそれを検知して2(EXEC_INEXEC)に移行します。後はプログラムの終了が出るまで2と3(EXEC_INWAIT)を繰り返します。
//実行コードの状況定義 #define EXEC_NONE 0 // 停止 #define EXEC_START 1 // 開始要求 #define EXEC_INEXEC 2 // 実行中 #define EXEC_INWAIT 3 // wait中 #define EXEC_STOP 4 // 停止要求
メモリマップのアドレス0x03(SEQ_COUNT_MSB)、0x04(SEQ_COUNT_LSB)が、現在プログラムを実行している箇所(実行アドレス)を表します。これらは2byteの数値が上二桁・下二桁で2項目に分かれています。
プログラムの実行を始める前に、この実行アドレスを、EEPROM上のプログラムの開始場所に合わせておく必要があります。前回少し触れましたが、EEPROMは冒頭にメモリマップの初期値が記録されており、その後にプログラムの実行コードが記録されているので、実行コードはEEPROMのアドレス0x0022から始まります。
実行コードの形式
バイナリ化された実行コードは、「LEDを点ける」等のような専用命令を持たず、通信メッセージと同様に、メモリマップの操作・数値演算・実行場所の移動(ジャンプ)といった原始的な命令を中心に構成されています。具体的には、コマンドは下記の種類に分かれます。
- メモリマップの読み書き
- 数値演算
- 実行場所のジャンプ(条件ジャンプ含む)
- 待機
- プログラムの終了
数値演算は、いわゆる逆ポーランド記法に近い実装になっており、スタックに二つの数値を積んでその演算方法を指定して答えを求めます。スタックに積める数値は定数(0~255)かメモリマップの指定アドレスの現在値を指定できます。演算方法は四則演算・剰余・ビット演算が可能です。演算結果は新たにスタック積まれます。スタックの値をメモリマップの指定アドレスに書き出したり、スタックの値に対して0か否かによってジャンプさせることができます。
ジャンプは上記の条件によるもの以外に、必ず行われるジャンプも存在します。ジャンプ先の指定は絶対アドレスで行われます。
待機は指定の時間(ミリ秒単位)プログラムの実行を待ちます。待機中はプログラムの実行状態がEXEC_INWAITになります。指定できる時間は定数だけで、変数の指定はできません。
終了は、実行されるとその時点でプログラムを終了します。メモリマップの内容は初期化されないので、LEDやブザーなどは、その時点の挙動が継続します。
これらの実行コードは、Arduinoのソースでは、列挙子で下記のように定義されてます。実行コードを生成するjavascript側にも、js/robot.jsにほぼ同じ定義が記述されています。
//プログラムの実行コードコマンド定義 typedef enum { RC_STOP =0x80, // 停止 RC_JUMP =0x81, // ジャンプ RC_CALL =0x82, // コール RC_RET =0x83, // リターン RC_WAIT_W =0x89, // 固定時間待ち RC_MEMW_B =0x90, // メモリ書き込み RC_C_INIT =0xc0, // 計算 スタック初期化 RC_C_DUP =0xc1, // 計算 値の複製 RC_C_C_L =0xc4, // 計算 定数 RC_C_MR_B =0xc8, // 計算 メモリ読み出し RC_C_MR_W =0xc9, // 計算 メモリ読み出し RC_C_MR_L =0xca, // 計算 メモリ読み出し RC_C_MW_B =0xcc, // 計算 メモリ書き込み RC_C_MW_W =0xcd, // 計算 メモリ書き込み RC_C_MW_L =0xce, // 計算 メモリ書き込み RC_C_ADD =0xd0, // 計算 加算 RC_C_SUB =0xd1, // 計算 減算 RC_C_MUL =0xd2, // 計算 乗算 RC_C_DIV =0xd3, // 計算 除算 RC_C_MOD =0xd4, // 計算 余り RC_C_AND =0xd5, // 計算 ビットAND RC_C_OR =0xd6, // 計算 ビットOR RC_C_XOR =0xd7, // 計算 ビットXOR RC_C_NOT =0xd8, // 計算 ビットNOT RC_C_EQ =0xd9, // 計算 = RC_C_NE =0xda, // 計算 != RC_C_GT =0xdb, // 計算 > RC_C_GE =0xdc, // 計算 >= RC_C_LT =0xdd, // 計算 < RC_C_LE =0xde, // 計算 <= RC_C_JUMP =0xe0, // 計算 真ならジャンプ RC_C_CALL =0xe1, // 計算 真ならコール } rc_codetype;
実行コード例
下記のプログラムを実行コードに変換した例を説明します。
これを実行コードに変換すると下記になります。一見ただの16進数の数値の羅列ですが、細かく読み解くと、前述の実行コード、及びそれに紐づく数値に分かれて解読することができます。
900f01ff8901f4900f010080
冒頭の4byteは、1ブロック目のライトをつける命令に相当します。
90 メモリ書き込み 0f LED1のアドレス 01 1byte ff 255を書き込む
次の3byteは、0.5秒間待つブロックに相当します。ここでの待ち時間は0.5秒=500ミリ秒と255を超えるため、2byteで待ち時間を表現しています。
89 待機 01f4 500ミリ秒待つ(0x1f4は10進数で500)
次の4byteは、0.5秒後にLEDを消す処理に相当します。
90 メモリ書き込み 0f LED1のアドレス 01 1byte 00 255を書き込む
最後の1byteはプログラムの終了です。
80 プログラムの終了
もう一つ、少し複雑なプログラムにおける実行コード例を見てみましょう。
こちらの実行コードは下記になります。
c0c807c41cdbe0002e8100369009020d3089030081002280
実行コードを細かく解説すると下記の通りです。各行の先頭にある4桁の16進数は、そのコードが始まるEEPROM上のアドレスです。今度はジャンプ命令が登場するので、その行き先がわかりやすいように記載しています。
0022:c0 スタックの初期化 0023:c8 07 条件分岐ブロックの開始。メモリマップアドレス0x07(今の気温)を読み出してスタックに積む 0025:c4 1c 定数0x1c(10進数で28)をスタックに積む 0027:db スタックした数値同士を比較(A>B) 0028:e0 002e 比較条件が成立したら、0x002eにジャンプする→ブザーブロックの処理へ 002b:81 0036 0x0036にジャンプ。この場合上記の条件が不成立ならジャンプする、という挙動になる 002e:90 09 02 0d30 ブザーブロックに相当。音程と音長をメモリマップに書き込む。 0033:89 0300 ブザー音が鳴り終わるまで待つ(0x300=768ミリ秒) 0036:81 0022 0x0022にジャンプ。「ずっと繰り返す」の終端→プログラム冒頭へ 0039:80 プログラム終了
これらの実行コードの変換、いわゆるコンパイルは、javascript側のjs/blockly_blocks.js内に実装されている各ブロックの処理にて行われています。そこも含めてブロックの個別の実装についての解説を次章で行います。
ブロックの新規作成
新しく独自のブロックを作成する方法について説明します。ブロックの追加は、改良編2にて、コメントアウトされたモータブロックを使えるようにする方法で簡単に説明していますが、もう少し踏み込んで説明します。
ブロック個別の処理は、Blockly.Blocks[(ブロック名)]で始まるコードで定義されており、各ブロックで共通の設定項目等が存在します。例えば「待つ」のブロックのコードは下記の内容です。
Blockly.Blocks['wait_base'] = { init: function() { this.jsonInit({ "message0": 'つづける %1', "previousStatement": null, "nextStatement": null, "tooltip": "今のじょうたいを、その時間だけつづけてから、次のブロックへすすみます。", "args0": [ { "type": "input_value", "name": "VALUE", "check": "Number" } ], "colour": 290, }); }, topID:-1, bottomID:-1, makecode:function(){ Blockly.makecode_commonfunc(this); var inputBlk = this.getInputTargetBlock('VALUE'); var waittime=inputBlk.getFieldValue('FIELDNAME') || 0; waittime*=1000; BLOCKAREA.code += `${itohex(CODE_MAP.RC_WAIT_W)}`; BLOCKAREA.code += `${itohex(waittime/256)}${itohex(waittime%256)}`; BLOCKAREA.address += 3; this.bottomID = BLOCKAREA.address; Blockly.makecode_bottom(this); } };
init:function()はブロックの初期化に関するメソッドです。主にブロックの色や入力欄等について設定します。この設定に関する仕様は本教材独自の物ではなく、google blocklyのものになります。下記web記事などが参考になります。
https://qiita.com/abarth500/items/d6ebaf33878bf748c51e
makecode:function()は、ブロックの内容を実行コードにコンパイルするメソッドです。自身のブロックの設定値などを取得して、ブロックの機能に応じた実行コードを生成して、実行コードに追加していきます。
topID,bottomIDは、実行コード作成時に、自身のブロックに関する処理が開始・終了するアドレスを記録します。これらは、プログラムのコンパイル時に算出します。
その他、「待つ」ブロックには含まれませんが、繰り返しや条件分岐等、別のブロックを囲む形状のブロックには、終端側の実行コードを生成するmakecode_bottom:function()のメソッドも存在します。
プログラムのコンパイル
プログラムをコンパイルして実行コードに変換する処理は、「はじめ」ブロックよりブロックの接続順にmakecode:function()メソッドが実行され、グローバルな変数BLOCKAREA.codeに追記されていきます。また、プログラムの全体サイズ及び各ブロックの開始・終了アドレスを算出するため、2回コンパイル処理が行われます。現在の実行コードのアドレス値はグローバル変数BLOCKAREA.addressに記録され、また配列BLOCKAREA.addressmapにブロックごとの開始・終了アドレスが記録されます。この配列は実行コード中のジャンプ命令でジャンプ先アドレスとして参照され、コンパイル1回目で全ブロック分のアドレスリストを完成させ、コンパイル2回目で、記録されたアドレスを参照します。
なお、各ブロックのmakecode:function()メソッド以外に、多くのブロックで共通する処理などについて、下記のようなヘルパー関数が存在します。
Blockly.makecode_commonfunc = function(block){ if(!(block.id in BLOCKAREA.addressmap)){ block.topID = BLOCKAREA.address; BLOCKAREA.addressmap[block.id]={topID:block.topID}; } } Blockly.makecode_bottom = function(block){ if((block.id in BLOCKAREA.addressmap)){ BLOCKAREA.addressmap[block.id].bottomID=block.bottomID; } }
これらは、前述したブロック毎の開始・終了アドレスの記録に関するメソッドです。多くのブロックではmakecode:function()メソッドの開始・終了がそのまま開始・終了アドレスに相当するため、そこでこれらのメソッドが呼び出されています。一部のブロックではこれらを使わず、類似する処理を独自に実装しているものもあります。
また、実行コードは16進数の文字列で作成していきますが、一つの数値を正しく2桁の16進数に変換するためのメソッドとしてfunction itohex(num)が定義されています。何らかの変数を実行コードに組み込むときはこれを必ず使います。
function itohex(num){ if(isFinite(num)){ if(num<0){ var d = Math.floor(num) >>> 0; // 負数を2の補数で表現 return `0${d.toString(16)}`.slice( -2 ); // "ffffffd3" } else return `0${Math.floor(num).toString(16)}`.slice( -2 ); } return "00"; }
ちなみに、現在の実装では、「はじめ」ブロックから切り離されたブロックはコンパイルが行われません。
コンパイルが完了し実行コードが完成したら、通信処理で教材本体にプログラムが転送されます。もし「実行」ボタンをクリックされていた場合は、本記事の冒頭で説明したように、メモリマップのアドレス0x02(EXEC_MODE)~0x04(SEQ_COUNT_LSB)を書き換えて、プログラムを開始します。
ツールボックスへの追加
ブロックをプログラムで使用する際はツールボックスへの登録が必要です。ツールボックスの設定はjs/toolbox.xmlで行われます。このファイルは拡張子が示す通りXML形式のファイルで、<toolbox>タグで囲まれた内容が一つのツールボックスを表します。こちらも本教材独自ではなく、google blocklyのライブラリを利用したものなので、そちらの解説を参照していただくと深く理解できます。
https://developers.google.com/blockly/guides/configure/web/toolbox
js/toolbox.xmlでは、初心者向け・上級者向け・プログラム実行中の三つのツールボックスが登録されています。一つのツールボックスには、categoryタグでブロックのカテゴリを設定し、その中にblockタグでブロックを設定します。
新たにカテゴリを作成してそこにブロックを追加する場合、一番簡単な書式は下記のようになります。ここでは「おわり」ブロックを使った例ですが、blockタグのtypeに、blockly_blocks.jsのBlockly.Blocks[(ブロック名)]で定義したブロック名を入れると、そのブロックをツールボックスに設定することができます。
<category name="おわり" colour="20"> <block type="end"></block> </category>
その他、細かい設定を行うと、ブロック内の設定項目の初期値や、複数のブロックを接続した状態でツールボックスに表示する、と言ったことも可能です。下記はLEDブロックの設定で、fieldタグで、操作対象のLEDを指定したり、valueタグからblockタグで「?秒」のブロックを追加して、時間指定タイプのLEDブロックを作ったりしています。
<block type="led_base"> <field name="LEDCOLOR">#00ff00</field> <value name="VALUE"> <block type="do_sec"> <field name="FIELDNAME">0.5</field> </block> </value> </block>
次回予告
今のところ他に何か解説するネタがありませんが、もし今後何らかの解説リクエストが発生したら、続きを書くかもしれません。