前記事はグダグダになってしまったので、仕切りなおし。
経緯 :
もともと ALU の勉強として、TTL の ALU 78281 を作り、それを元にしてなにか作ろうともくろんでいたのだが、どんどん興味が変わっていって、結局 自作コアの rtavr の ALU になった。
その後、rtavr の ALU は、機能拡張をして、機能的に過不足のないものが出来た。
rtavr には関係ないのだが、可変ビット長への対応もいれている。
で、この ALU に アキュームレータを付けたものを作ってみたら、CoolRunner II CPLD (XC2C64A-VQ44) にうまく収まった。これをベースになにか作るのも面白いかも知れないので紹介。
インターフェイスと機能:
module rtavr_alu_reg # (
parameter ALU_WIDTH = 8
) (
// ALU
input C_IN // Carry Input
, input Z_IN // zero Input
, input [ALU_WIDTH-1:0] B // DATA IN
, output [ALU_WIDTH-1:0] F // DATA OUT
, output C_OUT // carry-look-ahead
, output H_OUT // half carry-look-ahead
, output Z_OUT // zero
, output OVR // overflow
, input [2:0] S // Function Select
, input [1:0] ES // Function Select (Extended )
// アキュームレータ
, input [1:0] RS // Reg. Select
, input CP // Clock
, inout SIO0 // RI/LO
, inout SIO3 // LI/RO
ALU の A 入力は、アキュームレータの出力に接続されている。基本的には、
F = A 演算 B;
A <= F;
という使い方をする。フラグは、次の 4 種類で、AVR で必要なもの。
C : キャリーフラグ
H : ハーフキャリー
Z : ゼロ
OVR : オーバフロー
S は演算の指定。ES は、入力に定数を使うオプションで、INC/DEC や 2 の補数に変換する NEG などに使う。
// S == 0 : ADD (wo carry)
// S == 1 : ADC (with carry)
// S == 2 : SUB (wo carry)
// S == 3 : SBC (with carry)
// S == 4 : AND
// S == 5 : XOR
// S == 6 : OR
// S == 7 : MOV
//
// S = 0 1 2 3 4 5 6 7
// C_out : Carry[8] o o o o x x x x
// H_out : Carry[4] o o o o x x x x
// V_out : Over Flow o o o o 0 0 0 x
// Z_out : Zero o o o (o) - - - x
// (o) if (Z_in == 1)
// - no change
// x don't care
// EXTENDED Ops
// ES[0] : -1 を B の代わりに使う
// ES[1] : 0 を A の代わりに使う
// (※ A を B の代わりに使う)
// NEG : 0 - B (A == B) : S = 2, ES[1] = 1
// COM : A ^ -1 : S = 5, ES[0] = 1
// INC : A - -1 : S = 2, ES[0] = 1
// DEC : A + -1 : S = 0, ES[0] = 1
※ AVR では、 ES[1] は、NEG を実装するときにしか使わない。ALU単独で使っているから B に A と同じ値を 入力している。アキュームレータ付きでは、 ES[1]のとき、 A を B の代わりに使うように仕様を変更した方が良い。ただし、XC2C64A に入らない。
Implement の結果を書いておく。(変更後 のものは、XC2C128 で算出)
変更前 変更後 トータル
Macrocells Used 45 49 64
Pterms Used 204 249 224
Registers Used 8 8 64
Pins Used 32 32 33
Function Block Inputs Used 117 134 160
アキュームレータは操作は RS を使い posedge CP で駆動する。
RS == 0 : F の値を A にロード
RS == 1 : F を 左シフト LSB は、SIO0 から入力して、MSB を SIO3 に出力
RS == 2 : F を 右シフト LSB は、SIO0 に出力して、MSB を SIO3 から入力
RS == 3 : HOLD
(※)シフトは、F をシフトしたものを A にロードする。
処理コード :
wire WC = (S[0] & C_in);
wire [ALU_WIDTH-1:0] AX = (ES[1]) ? 0 : A;
wire [ALU_WIDTH-1:0] BX = (ES[0]) ? -1 : B;
( ※ wire [ALU_WIDTH-1:0] BX = (ES[1]) ? A :(ES[0]) ? -1 : B; )
wire [ALU_WIDTH:0] WCX = WC; // change bit width
wire [ALU_WIDTH:0] ALU_OUT = ((S == 0)|(S == 1))
? ({1'b0, AX} + ({1'b0, BX } + WCX))
: ((S == 2)|(S == 3))
? ({1'b0, AX} - ({1'b0, BX } + WCX))
: (S == 4) ? {1'b0, (AX & BX)}
: (S == 5) ? {1'b0, (AX ^ BX)}
: (S == 6) ? {1'b0, (AX | BX)}
: {1'b0, BX } ;
assign Z_out = (S == 3) ? (Z_in & (ALU_OUT[ALU_WIDTH-1:0] == 0))
: (ALU_OUT[ALU_WIDTH-1:0] == 0);
assign C_out = ALU_OUT[ALU_WIDTH];
assign F = ALU_OUT[ALU_WIDTH-1:0];
// ((S == 4)|(S == 5)|(S == 6)) ? 0 // (S ==7) Not care
assign V_out = (S[2]) ? 0:
( ~(S[1]) & (F[ALU_WIDTH-1] & ~AX[ALU_WIDTH-1] & ~BX[ALU_WIDTH-1]
| ~F[ALU_WIDTH-1] & AX[ALU_WIDTH-1] & BX[ALU_WIDTH-1])
| (S[1]) & (F[ALU_WIDTH-1] & ~AX[ALU_WIDTH-1] & BX[ALU_WIDTH-1]
| ~F[ALU_WIDTH-1] & AX[ALU_WIDTH-1] & ~BX[ALU_WIDTH-1]) )
;
assign H_out =
( ~(S[1]) & ( ~F[(ALU_WIDTH-1>>1)] & A[(ALU_WIDTH-1>>1)]
| ~F[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)]
| AX[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)] )
| (S[1]) & ( F[(ALU_WIDTH-1>>1)] & ~AX[(ALU_WIDTH-1>>1)]
| F[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)]
| ~AX[(ALU_WIDTH-1>>1)] & BX[(ALU_WIDTH-1>>1)] ) )
;
中身は意外と短い。ALU を全部載せてもこれだけ。
wire SO0;
wire SO3;
wire SI0;
wire SI3;
assign SO0 = F[0];
assign SO3 = F[ALU_WIDTH-1];
always @(posedge CP)
begin
acc <= (RS == 0) ? F
: (RS == 1) ? { F[ALU_WIDTH-2:0] , SI0 }
: (RS == 2) ? { SI3 , F[ALU_WIDTH-2:0] }
: acc;
end
assign SIO0 = (RS == 2) ? SO0 : 1b'Z ;
assign SIO3 = (RS == 1) ? SO3 : 1b'Z ;
assign SI0 = SIO0;
assign SI3 = SIO3;
こっちはアキュームレータ。ALU は確認済みだが、こちらは書いただけ。
これを使って、なにか簡単なプロセッサを設計してみようと思う。
- マイクロプログラミング方式で、8 bit の INST を INDEX にして マイクロプログラム用ROM(以下 μROM) に接続。
- μROMの出力 は、各 bit が ES,S, RS や その他の制御線に接続されている。(全部で 24bit 以内)。
- マイクロプログラム用の PC(以下 μPC) は、次の INST を読み込むまでインクリメント。この制御のための制御線も μROM の出力に接続されている。
- 16bit の インデックスレジスタ、PC が 1 つづつ。それらの制御線も μROMの出力が接続されている。
- B 入力は、バスのラッチで、制御線は同様になっている。
- 総命令数は、16 前後で、マイクロプログラムは、64 前後だった。
実をいうと、大昔に設計したものを再現しようと思っている。同じものにはなるはずもないが、形にして残しておきたい。
詳細は思い出せないのだが、こんな感じだった。
アドレス 6bit x 24bit 幅なら 分散RAM で 96 LUT 。ブロック RAM に割り付ければコストは無視できる。レジスタ用の FF は、PC 16 , INDEX 16, LATCH 8, と μPC 8 (6 ?) で、40 。
FPGA なら、それなりにコンパクトなものになるようだ。
設計してみよう。
wire [7:0] load_data;
reg [5:0] UPC ;
reg [23:0] UROM [0:63];
wire [23:0] urom_out = UROM[UPC];
というのをまず考えてみる。
wire u_loadpc = urom_out[7];
always @(negedge CLK)
begin
if (RESET)
begin
UPC <= 0;
end
else
begin
if (u_loadpc) UPC <= load_data[5:0];
else UPC <= UPC + 1;
end
end
マイクロプログラムの制御はこれだけ。
メモリから LATCH に INST を ロードするには、
wire c_bus_rd = urom_out[8];
wire c_bus_wr = urom_out[9];
wire c_addr_sel = urom_out[10]; // 0: PC , 1: INDEX
wire c_load_latch = urom_out[11];
wire c_load_index_l = urom_out[12];
wire c_load_index_h = urom_out[13];
wire c_load_pc_l = urom_out[14];
wire c_load_pc_h = urom_out[15];
wire c_inc_pc = urom_out[16];
reg [7:0] LATCH ;
reg [15:0] PC ;
reg [15:0] INDEX ;
wire [15:0] BUS_ADDR = (c_addr_sel) ? INDEX: PC;
always @(negedge CLK)
begin
if (RESET)
begin
LATCH <= 0;
PC <= 0;
INDEX <= 0;
end
else
begin
if (c_load_latch) LATCH[7:0] <= BUS_DATA;
if (c_load_index_l) INDEX[7:0] <= BUS_DATA;
if (c_load_index_h) INDEX[15:8] <= BUS_DATA;
if (c_load_pc_l) PC[7:0] <= BUS_DATA;
else if (c_load_pc_h) PC[15:8] <= BUS_DATA;
else if (c_inc_pc) PC <= PC + 1;
end
end
// +pPiIlswr
UROM[n+0] <= 24'bxxxxxxx1000010010xxxxxxx;
UROM[n+1] <= 24'bxxxxxxx0000000001xxxxxxx;
こんな風に組み立てていく。
... 思い出した。INST の読み込みは、各命令の実行後必要だし、μプログラムにも JUMP ぐらいは作ったのだった。
wire u_loadpc = urom_out[7];
wire u_jump = urom_out[6];
always @(negedge CLK)
begin
if (RESET)
begin
UPC <= 0;
end
else
begin
if (u_loadpc) UPC <= load_data[5:0];
else if (u_jump) UPC <= urom_out[5:0];
else UPC <= UPC + 1;
end
end
こうだったような。ちなみに、urom_out[5:0] は、u_jump == 1 のときだけ使っているから、それ以外のときは、別の目的に使える。何に使っていたか思い出せないが 分岐命令とかの 制御だったような...
条件分岐命令をインプリメントするときに、μ命令の方も 条件による分岐が必要になる。が、u_jump の条件をいじれば良い。... これは 後で考えよう。
次は、ALU を組み込む。
wire [7:0] alu_b = LATCH;
wire [7:0] alu_f;
assign store_data = alu_f[7:0];
wire [1:0] alu_rs = urom_out[23:22];
wire [1:0] alu_es = urom_out[21:20];
wire [2:0] alu_s = urom_out[19:17];
reg C;
reg Z;
reg V;
reg H;
rtavr_alu ALU (
.B(alu_b), .F(alu_f)
, .S(alu_s) , .ES(alu_es)
, .CP(CLK) , .RS(alu_rs)
, .C_in(C) , .Z_in(Z)
, .C_out(C_OUT) , .H_out(H_OUT)
, .V_out(OVR) , .Z_out(Z_OUT)
);
always @(negedge CLK)
begin
if (RESET)
begin
C <= 0;
Z <= 0;
end
else
begin
C <= C_OUT;
Z <= Z_OUT;
end
end
全部割り当てたら 24 bit 使いきってしまった。条件分岐のために、あと 1 bit 欲しい。24bit にこだわる必要はないのだが ... 整理して浮かそう。
... 思い出した。ALU の S (演算選択) を 8 bit ある命令空間の下位 3 bit にしたのだった。(以下 INST_ATTR )
で、UPC を 上位にもっていく。UPC[0] は、重なるから 常に 0 をロード。-- 要するに 奇数番地から始まる命令は作れない。
このINST_ATTR は他の目的にも使える。ひとつが、分岐命令の条件選択。最大 8 通り作れるわけだ。フラグは 4 つあるから、0 のとき 分岐 と 1 のとき分岐の組み合わせで 合計 8 通り。
これで、3 bit 減って 1 bit の追加で、合計 22 bit 。
もうひとつは、シフトのときの動作の指定。シフト動作は、RS として UROM に割り当てているが、C になにをロードするかの指定と、SHIFT_IN するデータを何にするかの組み合わせがある。INST_ATTR から 3 bit 割り当てて、足りなければ UROM を割り当てることにする。
zsp8-0.1
- zsp8-0.1.tar.gz
ここまでの成果を zsp8-0.1 として置いておく。μプログラムが出来ていないが Implement だけはやってみた。
Macrocells Used Pterms Used Registers Used Pins Used Function
Block Inputs Used
ベース 135/256 565/896 60/256 36/80 322/640
拡張版 140/256 586/896 60/256 36/80 361/640
これは、CPLD XC2C256-VQ100 への Implement 結果。拡張版は、ES[1] == 1 のとき A 入力を B に持っていく拡張。
ひょっとしたら XC2C128 に入るかもと思ったがちょっと無理のようだ。
しばらく、この記事はお休み。気が向いたら続きを書くかも。