「メガプロセッサ」をプログラムします

夏のGeektimesには、 メガプロセッサに関する記事がありました。これは重量が半トンで、ケンブリッジ近くの通常のタウンハウスのリビングルーム全体を占める個別のトランジスタとLEDのプロセッサです。 私は、このメガプロジェクトに地理的に近いことを利用して、そのために提示可能なものをプログラムすることにしました。たとえば、 以前のメガプロセッサ用の「Digital Rain」ソフトウェアを使用することです。

メガプロセッサコマンドシステムについては、開発者のサイトで説明されています

ほとんどの命令は1バイトで構成され、その後に直接オペランド(1または2バイト)が続く場合があります。 汎用レジスタ(R0〜R3)は4つしかありませんが、等しくありません。たとえば、メモリアクセスコマンドの場合、アドレスはR2またはR3になければなりません。 また、オペランドは残りの2つのレジスタのいずれかにあります。

x86またはARM命令セットに慣れているプログラマーにとって、メガプロセッサ命令セットは非常に貧弱に見えます:算術命令の間接ベース+オフセットアドレスまたは直接オペランドはありません( addq ±1addq ±2を除く)。 しかし、いくつかの予期しない可能性があります:個別のsqrtコマンド、およびシフトコマンド用の.wtモード。これにより、結果が拡張ビットの合計に置き換えられます。 したがって、たとえば、1組のコマンドld.b r1, #15; lsr.wt r0, r1 ld.b r1, #15; lsr.wt r0, r1は、 r0の単位ビット数を計算しr0 (就職の面接官に愛されている質問です!)。 (x86またはARMでよく知られているmovニーモニックではなく)レジスタに直接値をロードするコマンドのldニーモニックは、それを実行する方法を示します。実際、プロセッサの観点からは、 ld.b r1, (pc++)が実行されます。

それでは始めましょう。

メガプロセッサのプログラムは、割り込みベクトルのテーブルで(アドレス0から)開始します。 4つのベクトルのそれぞれに4バイトが割り当てられます。 0x10から、実際のプログラムコードを見つけることができます。 64KBのアドレス空間から、前半全体(アドレス0x8000まで)をコードで使用できます。 アドレス0xA000-0xA0FFは「ディスプレイ」に対応します。これは、各ビットにLEDインジケータが装備された個別のメモリです。 「メモリ容量は256バイトです。」を書き込むときにマークが間違っていました。コードとデータのメインメモリではなく、「ビデオメモリ」の量です。

プログラムの4つの割り込みベクターのうち、 resetベクターのみが使用され、他のベクターには1つのreti命令からの「スタブ」があります。 (x86またはARMのプログラマーの場合、割り込みハンドラーからのreturnコマンドはiretニーモニックではおなじみです。)プログラムのこれらの割り込みはいずれも発生しないため、「スタブ」を配置することさえできません。

 reset: jmp start; nop; ext_int: reti; nop; nop; nop; div_zero: reti; nop; nop; nop; illegal: reti; nop; nop; nop; 

開始後の最初のことは、スタックと変数を初期化することです。 アドレス0x2000から始めて、スタックを小さくします。これで十分な余裕があります。 必要な変数は2つだけです。現在のRNG値のseedと、32個の値のposition配列です。この列の「ドロップ」がクリープする場所を監視する「表示列」ごとに1つです。 配列は32バイトのランダムバイトで初期化されます。 サブルーチン呼び出しであるjsrコマンドは、x86のcallまたはARMのblに対応しcall

 start: ld.w r0, #0x2000; move sp, r0; // set random positions ld.b r1, #32; init_loop: jsr rand; // returns random value in r0 ld.b r2, #position; add r2, r1; st.b (r2), r0; addq r1, #-1; bne init_loop; 

1つのコマンド(#position + r1) 1バイトをアドレス(#position + r1)に書き込むことはできないため、最初に別の追加コマンドでアドレスを計算する必要があります。

プログラムの主要部分は、「表示列」ごとに右から左に移動し、その中の「ドロップ」を1つ下にシフトする無限のサイクルです。 「ドロップ」の下位2ビットはその色(3-「オン」、0、1または2-「オフ」)、残りの6ビット-座標(0..63)を示すため、「下方シフト」は4の追加を意味します。 「ドロップ」のみが「ディスプレイ」の下部にクロールされ(値は255を超えました)、新しいランダムバイトに置き換えられます。

 busy_loop: ld.b r1, #32; next_col: ld.b r2, #position; add r2, r1; ld.b r0, (r2); addq r0, #2; addq r0, #2; btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; 

1つのコマンドで4をaddq r0, #2ことは不可能なので、 addq r0, #2回繰り返し、結果の8ビット目をチェックして255を超えているかどうかを判断します。超えた場合は、新しいランダム値をposition配列に保存します。 そうでない場合は、古いものを4増やします。条件付きジャンプコマンドbmiは、最後のアクションの結果が負の場合、つまりbusy_loopサイクルの先頭にbusy_loopます。 NULL列を処理した後。

乱数はどのように生成しますか? 32ビットのデジタルレインで使用したRANDUはもはや適切ではありません。メガプロセッサは16ビットの数値しか乗算できません。 したがって、 単純なRNGリストから、乗数が16ビットであるものを取得します。 「ターボパスカル」と呼ばれるRNGが気に入りました。

 rand: ld.w r0, seed; ld.w r1, #33797; mulu; addq r2, #1; st.w seed, r2; move r0, r2; ret; 

このシンプルできれいなRNGは、生成された値をr0に返しますが、残念ながら、他のすべてのレジスタの値を台無しにします。 どちらの場合でも、 randを呼び出すと、 r1 「表示列」のインデックスがあり、保存および復元する必要があることに注意してください。 そして、 r2はオフセット(#position + r1) r2必要です。 したがって、このオフセットの計算をrand内に配置できます。

 rand: push r1; // ! ld.w r0, seed; ld.w r1, #33797; mulu; addq r2, #1; st.w seed, r2; pop r1; // ! move r0, r2; ld.b r2, #position; // ! add r2, r1; // ! ret; start: ld.w r0, #0x2000; move sp, r0; // set random positions ld.b r1, #32; init_loop: jsr rand; st.b (r2), r0; addq r1, #-1; bne init_loop; busy_loop: ld.b r1, #32; next_col: ld.b r2, #position; add r2, r1; ld.b r0, (r2); addq r0, #2; addq r0, #2; btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; 

ここでの最後のトリックは、 ld.b r2, #position; add r2, r1;を計算することld.b r2, #position; add r2, r1; ld.b r2, #position; add r2, r1; next_colループの開始時に、 randルーチン内のジャンプに置き換えることnext_colできnext_col

 rand: push r1; ld.w r0, seed; ld.w r1, #33797; mulu; addq r2, #1; st.w seed, r2; pop r1; move r0, r2; add_position: ld.b r2, #position; add r2, r1; ret; start: <...> busy_loop: ld.b r1, #32; next_col: jsr add_position; // ! ld.b r0, (r2); addq r0, #2; addq r0, #2; btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; 

ここで最も興味深いのはnext_colサイクルの後半で、ディスプレイに「ドロップ」を描画します。

  move r3, r1; // x (0..1f) lsr r3, #3; // byte addr in row (0..3) ld.b r2, #0xfc; // y mask and r2, r0; // y * 4 (0..fc) add r3, r2; // byte addr in screen ld.w r2, #0xa000; add r3, r2; // byte addr in memory ld.b r2, #2; lsr.wt r0, r2; ld.b r2, #7; and r2, r1; // bit index in byte (0..7) lsl r2, #1; lsr r0, #2; roxr r2, #1; ld.b r0, (r3); // and now apply test r2; bpl blank; bset r0, r2; jmp apply; blank: bclr r0, r2; apply: st.b (r3), r0; jmp next_col; 

目的のビットを「点灯」または「消去」するには、まず「ビデオメモリ」の対応するバイトのアドレスを計算する必要があります。 「列」の番号はr1に格納されており、ドロップの位置と「色」はr0に格納されているため、バイトアドレスは(r1 >> 3) + (r0 & 0xfc) + 0xa000として計算されます。 その後、コマンドld.b r2, #2; lsr.wt r0, r2; ld.b r2, #2; lsr.wt r0, r2; ドロップの色を決定しますr0最下位ビットの両方が設定されている場合、これらのコマンドの結果として、値2はr0ます。 それ以外の場合、値は0または1です。最後に、 r2下位3ビットで目的の「ビデオメモリ」ビットの番号を記憶し、シーケンスlsl r2, #1; lsr r0, #2; roxr r2, #1;てドロップカラーを高ビットr2 「プッシュ」しlsl r2, #1; lsr r0, #2; roxr r2, #1; lsl r2, #1; lsr r0, #2; roxr r2, #1; -2番目のコマンドは、カラービットをr0からCFフラグにプッシュし、最後(CFの参加による右への循環シフト)はこのビットをr2プッシュします。 必要なすべての値に十分なレジスタがない場合は、工夫する必要があります! 最後に、バイトが目的のアドレスの「ビデオメモリ」から取得され、カラービットに応じて、このバイトが設定されるか、目的のビットがリセットされます。 bsetおよびbclrは、第2オペランドの下位ビットのみを使用するため、上位ビットr2のカラービットはそれらをr2しません。 シーケンスtest r2; bpl blank;この高位ビットをチェックしtest r2; bpl blank; test r2; bpl blank; -条件分岐コマンドbplは、最後のアクションの結果が正の場合、つまり、 少し色が落ちます。

結果は次のとおりです。



コード全体
 reset: jmp start; nop; ext_int: reti; nop; nop; nop; div_zero: reti; nop; nop; nop; illegal: reti; nop; nop; nop; rand: push r1; ld.w r0, seed; ld.w r1, #33797; mulu; addq r2, #1; st.w seed, r2; pop r1; move r0, r2; add_position: ld.b r2, #position; add r2, r1; ret; start: ld.w r0, #0x2000; move sp, r0; // set random positions ld.b r1, #32; init_loop: jsr rand; st.b (r2), r0; addq r1, #-1; bne init_loop; busy_loop: ld.b r1, #32; next_col: jsr add_position; ld.b r0, (r2); addq r0, #2; addq r0, #2; btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; move r3, r1; // x (0..1f) lsr r3, #3; // byte addr in row (0..3) ld.b r2, #0xfc; // y mask and r2, r0; // y * 4 (0..fc) add r3, r2; // byte addr in screen ld.w r2, #0xa000; add r3, r2; // byte addr in memory ld.b r2, #2; lsr.wt r0, r2; ld.b r2, #7; and r2, r1; // bit index in byte (0..7) lsl r2, #1; lsr r0, #2; roxr r2, #1; ld.b r0, (r3); // and now apply test r2; bpl blank; bset r0, r2; jmp apply; blank: bclr r0, r2; apply: st.b (r3), r0; jmp next_col; seed: dw 1; position:; 

最後に、GIF-KDPVのように「ドロップ」を点滅させます。 実際、これは、プログラムの実行速度が2倍遅くなることを意味します。サイクルの各繰り返しでbusy_loopが最初に点灯し、次にすべての「ドロップ」を消します。 点火ハーフイテレーションでは、ビデオメモリの2ビットを設定する必要があります。「ドロップ」の現在の位置と前の(最後のハーフイテレーションによって消滅する)の位置です。

そのため、次の場合は「ドロップ」を点灯する必要があります。a)値の下位2ビットが両方とも設定されている場合。 b)半反復に点火します。 -そして、他のすべての場合には消火します。 これをすべて実装する最も簡単な方法は、ドロップの色を定義する一連のコマンド( ld.b r2, #2; lsr.wt r0, r2; )で、固定値#2を点火ハーフイテレーションで値2を持つflag変数に置き換えることld.b r2, #2; lsr.wt r0, r2; 1消火時:

 busy_loop: ld.b r1, #3; // ! ld.b r2, flag; // ! sub r1, r2; // ! st.b flag, r1; // ! ld.b r1, #32; next_col: jsr add_position; ld.b r0, (r2); ld.b r3, flag; // ! lsr r3, #1; // ! lsl r3, #2; // ! add r0, r3; // ! btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; move r3, r1; // x (0..1f) lsr r3, #3; // byte addr in row (0..3) ld.b r2, #0xfc; // y mask and r2, r0; // y * 4 (0..fc) add r3, r2; // byte addr in screen ld.w r2, #0xa000; add r3, r2; // byte addr in memory ld.b r2, flag; // ! lsr.wt r0, r2; 

busy_loopサイクルの開始時に、3から現在のflag値を減算します。 2を1に、1を2に変更します。次に、各反復で「ドロップ」を下に移動する代わりに( addq r0, #2; addq r0, #2; )値(flag >> 1) << 2 、t to r0を追加します.e。 点火半反復で4、消光で0。

最後に追加するのは、「ドロップ」自体から-4のオフセットでバイト単位で、点火ハーフイテレーションにもう1ビットを設定することです。

  // and now apply test r2; bpl blank; bset r0, r2; st.b (r3), r0; // ! addq r3, #-2; // ! addq r3, #-2; // ! btst r3, #8; // ! bne next_col; // ! ld.b r0, (r3); // ! bset r0, r2; // ! jmp apply; blank: bclr r0, r2; apply: st.b (r3), r0; jmp next_col; 

btst r3, #8; bne next_col;確認してくださいbtst r3, #8; bne next_col; btst r3, #8; bne next_col; 「ディスプレイ」の上端を超えないようにし、アドレス0x9FFxに何かを書き込もうとしないようにします。

意図したとおりにドロップが点滅します



コード全体
 reset: jmp start; nop; ext_int: reti; nop; nop; nop; div_zero: reti; nop; nop; nop; illegal: reti; nop; nop; nop; rand: push r1; ld.w r0, seed; ld.w r1, #33797; mulu; addq r2, #1; st.w seed, r2; pop r1; move r0, r2; add_position: ld.b r2, #position; add r2, r1; ret; start: ld.w r0, #0x2000; move sp, r0; // set random positions ld.b r1, #32; init_loop: jsr rand; st.b (r2), r0; addq r1, #-1; bne init_loop; busy_loop: ld.b r1, #3; ld.b r2, flag; sub r1, r2; st.b flag, r1; ld.b r1, #32; next_col: jsr add_position; ld.b r0, (r2); ld.b r3, flag; lsr r3, #1; lsl r3, #2; add r0, r3; btst r0, #8; beq save; jsr rand; save: st.b (r2), r0; addq r1, #-1; bmi busy_loop; move r3, r1; // x (0..1f) lsr r3, #3; // byte addr in row (0..3) ld.b r2, #0xfc; // y mask and r2, r0; // y * 4 (0..fc) add r3, r2; // byte addr in screen ld.w r2, #0xa000; add r3, r2; // byte addr in memory ld.b r2, flag; lsr.wt r0, r2; ld.b r2, #7; and r2, r1; // bit index in byte (0..7) lsl r2, #1; lsr r0, #2; roxr r2, #1; ld.b r0, (r3); // and now apply test r2; bpl blank; bset r0, r2; st.b (r3), r0; addq r3, #-2; addq r3, #-2; btst r3, #8; bne next_col; ld.b r0, (r3); bset r0, r2; jmp apply; blank: bclr r0, r2; apply: st.b (r3), r0; jmp next_col; seed: dw 1; flag: db 2; position:; 

ここで、Megaprocessorで独自のプログラムを実行するには、作成者が自宅を訪れたことに同意する必要があります。 しかし、1か月後には、メガプロセッサーはケンブリッジコンピューター歴史センターに移動し、週5日一般に公開される予定であると述べました。

メガプログラミングに成功!

Source: https://habr.com/ru/post/J309654/


All Articles