ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

Z80作成日記をみて思ったことをつらつらと

 DE0で8bit CPUのデコーダを動かす1を見てなんとなく思ったことを書いてみます。
単に僕だったらこう書くなぁというだけで、こうした方が良いという話では無いです。

デコーダから信号を切りだそう。

 ぱっとみて思ったのがalways @(code) begin〜endまでが長くて、最初は良いんだけど、途中で命令入れ替えたり、追加したりしていったら、僕なら間違えるだろうなと感じました。

 まずは、独立してそうな信号op_dwに注目し切り出してみました。Verilogのソースを追いながら、命令とop_dwの関係をExcelにまとめるとこんな感じに。


 オレンジが1、青が0です。色が不健康に見えるのは僕が今時のExcelの使い方をよく分かって無いからです。
この表を見ながらop_dwの部分だけmoduleにしました。

module decoder_dw(
	input [7:0] code,
	output reg op_dw
	);	
	wire[1:0] sect = code[7:6];
	wire[3:0] subop = code[3:0];
	wire[1:0] reg16 = code[5:4];

 	always @(code) begin
		case(sect) //上位2bit
		2'b00: begin
		//HALT, JR NZ,e, JR NC,e, JR e, JR Z,e, JR C,n'n のみdw
		   op_dw <= (code == 16'h10 ||  code == 16'h20  ||  code == 16'h18  ||  code == 16'h30  ||  code == 16'h28  ||  code == 16'h30 || code == 16'h38) ;
		end
		2'b01: begin
		//40h〜7Fhまでの LD 命令は全てdw
			op_dw <= 1;
		end
		2'b10: begin
		//算術命令は全てnot dw
			op_dw <= 0;
		end
		2'b11: begin
			case(subop)
			4'h0: op_dw <= 1; // LD (HL),BC LD (HL),DE LD BC,(HL) LD DE,(HL)
			4'h1: op_dw <= 1; // LD命令
			4'h2: op_dw <= 1; // JP NZ,n'n,JP NC,n'n,INCW HL,INCW SP
			4'h3: op_dw <= (code == 4'hC3);  //1:JP n'n 0:OUT (n),A OUT(BC),A DI
			4'h4: op_dw <= 1; // LD VW,(SP) LD (SP),VW DECW HL DECW SP
			4'h5: op_dw <= 1; // LD命令
			4'h6: op_dw <= 0; // ADD A,n SUB n AND n OR n
			4'h7: op_dw <= 1; // MUL A,B MUL A,C MUL A,D MUL A,E
			4'h8: op_dw <= 1; // JP Z,(VW) JP C,(VW) SUB HL,BC SUB HL,DE
			4'h9: op_dw <= 1; // JP(VW) MUL BC,DE JP (HL) LD SP,HL
			4'hA: op_dw <= 1; // JP Z,n'n JP C,n'n OUT (BC),HL IN HL,(BC)
			4'hB: op_dw <= 0; // LD V,(HL) IN A,(n) IN A,(BC) EI
			4'hC: op_dw <= 0; // LD BC,VW LD DE,VW LD HL,VW DIV BC,DE
			4'hD: op_dw <= 1; // LD VW,BC LD VW,DE LD VW,HL MOD BC,DE
			4'hE: op_dw <= 0; // ADC A,n SBC A,n XOR n CP n
			4'hF: op_dw <= 0; // DIV A,B DIV A,C DIV A,D DIV A,E
			endcase
		end
		endcase
	end
endmodule	

あわせてテストベンチも

module decoder_dw_tb();
	reg [7:0] code;
	wire op_dw;
	integer i;

	decoder_dw u0(.code(code), .op_dw(op_dw));

	initial begin
		code <= 8'h00;
		for (i = 0; i <= 255 ;  i = i + 1) begin
			#10 code <= i;
		end

		#10 code <= 0;
	end
endmodule

メリット

 メリットはこんな感じです。

  • op_dwの出力に関係する信号が一目で分かる。
  • 単体テストしやすい
  • 高速化したいときに便利
  • 自動生成への第一歩

 一つ目以外はたいしたメリットじゃないです。FPGAで使うVerilogでは、細かくalwaysを分ける人もいれば、大きなalwaysの中に全部入れる人もいます。僕の観測範囲だと、古い人は比較的細かく分ける傾向があります。

 単体テストしやすいのは一見メリットに見えますが、ここだけ単体で試験してもあんまり意味が無いし、あまり細かい単位でmoduleに分けていくとシミュレーションで目的の信号に行くのが面倒になりますね。

 高速化っていうのは、クロックを150MHzから200MHzに持って行きたいとかそういうレベルで効いてきます。最大動作クロック数は、結局の所1clkで何段のLUTを引くかに帰着することが多いので、入力信号が明確になっていると最適化しやすいです。

 自動生成の第一歩ってのは、CPU全部を自動生成するのは流石に難易度が高いので、一部だけでも自動生成出来るところはやってしまいましょうと考えました。今回は色分けが綺麗に行っていたので手で書きましたが、もっとランダムに1と0が並んでいたらミスが入ってしまいます。実務だったら、書き捨てPythonを書き、Excelからセルの色を抜き出し、こんな感じで256行並べると思います。

always @(code) begin
    case(code) //上位2bit
    8'h00: op_dw <= 1;
    8'h01: op_dw <= 1;
    8'h02: op_dw <= 0;
    8'h03: op_dw <= 1;
    // ずらずら
    8'hFF: op_dw <= 0;
    endcase
end

 組み合わせ回路の最適化はCADが得意としているので、任せておけば大丈夫です。命令が変わったら、Excelを変更し、decoder_dwのモジュールだけ作り直せば、op_dwに関しては何も気にしなくてすみますね。

 Javaでシミューレータが動いているのなら、シミュレータは当然正解のop_dwが分かっているはずのなので、そこから生成する方が良いです。同じように、op_param1等も、それだけをデコードするモジュールを作るようにしていけば、デコーダの大半を自動生成できると思います。