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

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



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



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



x86またはARM命令セットに慣れているプログラマーにとって、メガプロセッサ命令セットは非常に貧弱に見えます:算術命令の間接ベース+オフセットアドレスまたは直接オペランドはありません( addq ±1



addq ±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日一般に公開される予定であると述べました。



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



All Articles