2011年05月09日

TTL ALU 74281

74 シリーズの中には、Arithmetic Logic Unit (ALU) がいくつかある。

  • TTLのみでコンピュータを自作する(第7話)

    ここのサイトに ALU 一覧がある。74181 は、そのなかでも有名で、現在でもなんとか入手できる。

    気になるのは、78281 。アキュームレータ付きで、シフトもできる。TTL で CPU を作るなら便利そうなのだが、こいつは、ググってもほとんど情報がない。

78281 互換の module を作り、それを使ったなにか簡単な CPU を作ってみたい。

    AVR互換コアは作ったが、ALUありきの設計にはしなかった。独立したモジュールさえ作らなかったのだ。

    それで、ALU には少々負い目を感じている。簡単なもので良いから、ALU というものを作っておきたい。

さて、74281 とは、実際どういうものだろう?

74281 加減算、AND/OR/EXOR、その他計15種 4bit ALU出力にシフトレジスタ付

説明はこうなっている。

データシート は見つかった。ただ、紙媒体をスキャンしたもので読みにくい。

74281 の仕様



    ピン配置

    74281
    A1 1 24 VCC
    A2 2 23 A0
    RS1 3 22 CP
    RS0 4 21 SIO0
    RC 5 20 AS0
    SIO3 6 19 AS1
    A3 7 18 AS2
    Cn 8 17 M
    ~G 9 16 F0
    Cn+4 10 15 F1
    ~P 11 14 F2
    GND 12 13 F3

    入出力

    input Cn // Carry Input
    input [3:0] A // DATA IN

    output [3:0] F // DATA OUT
    output Cn+4 // carry-look-ahead
    output ~P // carry-propagate
    output ~G // carry-generate

    input M // Mode Control
    input [2:0] AS // Function Select
    input [1:0] RS // Reg. Select
    input RC // Reg. Control
    input CP // Clock
    inout SIO0 // RI/LO
    inout SIO3 // LI/RO

    ( reg [3:0] B ) // internal register

    TABLE1 ARITHMETIC FUNCTIONS (M == 0)

    AS[2:0]
    0 0xe + Cn
    1 B - A - ~Cn
    2 A - B - ~Cn
    3 A + B + Cn
    4 B + Cn
    5 ~B + Cn
    6 A + Cn
    7 ~A + Cn

    TABLE2 LOGIC FUNCTIONS (M == 1)

    AS[2:0]
    0 0
    1 A ^ B
    2 ~( A ^ B )
    3 A ^ B
    4 A & B
    5 ~( A | B )
    6 ~( A & B )
    7 A | B

    TABLE3 SHIFT MODE FUNCTIONS (posedge CP)

    RS[1:0] , RC Breg SIO0 SIO3
    0/1 Load B Reg ( F->B ) F0 F1 F2 F3 Z Z
    2 Shift UP SIO0 F0 F1 F2 Z F3
    3 Arith Shift UP SIO0 F0 F1 B3 Z F2
    4 Shift Down F1 F2 F3 SIO3 F0 Z
    5 Arith Shift Down F1 F2 SIO3 B3 F0 Z
    6/7 Hold B0 B1 B2 B3 Z Z

    仕様を書き写すとこういうものであるらしい。

    入力は、4bit の A で、出力は F 。B は内部のレジスタ -- アキュームレータ。

    ALU は 算術演算と論理演算の 2 つのモードがある。

    シフトも 算術シフトができる。

    機能としては、十分で、アキュームレータ 1 つしかない CPU を作るなら、配線が少なくて済む。

    これだけの情報で大体は分かるが ~P, ~G の説明はない。

    このあたりをみれば、分かる。設計するための情報は揃った。

    追記:データシート AM25LS281XC に P/G の論理式が載っていた。使い方の例は、 Am2902 との接続方法のみ。一方 データシート DM74S281N には、182 との接続方法のみ(詳しく)載っている。

    P,G は、182 (や Am2902) を使って高速化する場合のみ必要で、2 個を使った 8bit 版程度では不要。16 bit 版でも カスケード接続はできる。また、P,G を生成するより 最初から 16bit 版(など)を作った方が記述が楽。

74281 の設計



    完全に同じものを作ることは、できるだろう。だが、TOP モジュールにするとすれば、CPLD に入れるにしても 規模が小さすぎる。せめて x2 の 8bit 版にしたい。

    TOP モジュールにしないとすれば、シフトレジスタ部の SIO0/SIO3 が 双方向で使いにくい。また、ALU 部と シフトレジスタ部は分離したいところ。

    というわけで、ALU 部 と シフトレジスタ部 を別々に作り、TOP モジュールで組み合わせることにする。また、シフトレジスタ部の SIO0/SIO3 は、IN/OUT 別々にする。

    モジュールの定義は、

    alu_74281(ALU)
    input C_IN // Carry Input
    input [3:0] A // DATA IN
    input [3:0] B // DATA IN
    output [3:0] F // DATA OUT
    output C_OUT // carry-look-ahead
    output P // carry-propagate
    output G // carry-generate

    input M // Mode Control
    input [2:0] AS // Function Select

    acc_74281(shift reg.)
    output [3:0] B // DATA OUT
    input [3:0] F // DATA IN

    input [1:0] RS // Reg. Select
    input RC // Reg. Control
    input CP // Clock
    input SI0 // RI/LO
    output SO0 // RI/LO
    input SI3 // LI/RO
    output SO3 // LI/RO


    こんな感じか。

ALU を 74382 に変更

    74281 の ALU を作ってみたのだが、選択が 4 bit もあり、結構スライスを使う。382 は、3bit で、必要十分のように思える。
    データシートを見ると 真理値表もある。

    • 74F381 ( P , G の真理値表 )
    • 74F382 ( OVR , Cn4 の真理値表 )

    真理値表をみながら考えたのだが、FPGA はもともと LUT で論理を合成する。すなおに LUT を使うと ...

    • 真理値表に書いてある入力は、S 3bit + Cn + A + B で 6bit 。
    • 出力は、F 4bit + OVF + Cn+1 で 6bit と内部 Cn 生成に 3bit 。
    • ( 6bit 入力 = 4 LUT ) x 9 = 36 LUT (18 slices)
    • 8bit 化なら ( 6bit 入力 = 4 LUT ) x 17 = 68 LUT (34 slices)
    • Spartan-6 ならもっと効率が良く 1/4 で済む。
    • LUT_4 しかない FPGA だと、後半の OVR , Cn は LUT を使わずに 0 固定にすれば ちょっと規模を減らせる。( 互換ではなくなるけど .. 使うとは思えない )

    これだけで済むとは思えないが、結構コンパクトになりそうだ。うまく行くなら AVR互換コア(rtavr) にフィードバックして、コアを小さくできるかも知れない。

      rtavr の ALU は、8bit で 出力は OV と Cn[4] , Cn[8] が必要。機能的にも 8bit 化 382 で十分。Cn[4] は、内部用として生成しているから、規模は同じと見積もれる。

    で、やってみた。最初分散メモリを使おうとしたのだが、うまくいかないので、強引に if 文 (正確には ? 演算子のネスト) で 書いてみた。

    by LOGIC by CASE(full) by CASE(shrinked)
    Number of Slice Flip Flops 8 8 8
    Number of 4 input LUTs 73 82 77
    Number of occupied Slices 37 45 39
    Total Number of 4 input LUTs 74 82 77
    Number used as a route-thru 1 0 0
    Number of bonded IOBs 30 30 30
    Number of BUFGMUXs 1 1 1

    Maximum frequency(MHz): 126.646 70.967 115.221

    結果はこれ。74281 のアキュームレータを組み合わせている。(そうしないと周波数が出ないので) 。
    ロジックで書いたのは、論理演算の OVR , C_OUT を計算していないから、shrinked version に相当する。残念ながら ちょっと負けている。ロジックの方はバグっているかも知れないので、最終的な結果ではないものの、残念。

    だが、良く良く考えて見れば、rtavr の ALU部は、H フラグの生成で無駄がある。8bit の演算をしていて、Cn[4] を作れば良いだけなのに、計算とは別に H フラグを生成しているのだ。

    ロジックで組むのと遜色ないのであれば、この無駄が省けるかも知れない。... というわけで、何度も方針変更になってしまうが、AVR の ALU をまじめに作る方針に変更。同じように、74281 のアキュームレータと組み合わせて比較してみよう。

rtavr_alu

    まだロジックでの演算のみだが、rtavr の ALU をモジュールとして独立させたのが出来た。評価環境があるから、正しく出来たかどうかの検証が楽。3 スライスだが、規模が小さくなって嬉しかったりするが、それはサテオキこんな仕様。

    module rtavr_alu (
    input [7:0] A
    , input [7:0] B
    , output [7:0] F

    , input C_in
    , input H_in
    , input V_in
    , input Z_in
    , output C_out
    , output H_out
    , output V_out
    , output Z_out

    , input [2:0] S
    );
    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SBC (with carry)
    // S == 3 : SUB (wo 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 - - - -
    // H_out : Carry[4] o o o o - - - -
    // V_out : Over Flow o o o o - - - -
    // Z_out : Zero o o o (o) - - - -
    // (o) if (Z_in == 1)

    S は、こんな割り当て。余ったので、加減算に carry ありなしを割り当て 計 8種類。フラグは変更しない OP があるので 全部 IN/OUT がある。C/V は 382 と同じようなものだが、Half carry と zero フラグがある。

    zero フラグは、演算結果の後に付くから、いままで説明してきたのとは扱いが違う。Half carry は内部状態を外に出すかんじ。

    実は、zero フラグを モジュールに持ってきたため、階層が増えて遅くなっている。だが、Z_out を入力から直接生成するような 専用コードを書くことで逆に速くできるかも知れない。

    ゴードは小さいので全部書くと

    wire [8:0] ALU_OUT = ((S == 0) | (S == 1)) ? ({1'b0, A} + {1'b0, B }
    + {8'b0, (S[0])? C_in : 1'b0 })
    : ((S == 2) | (S == 3)) ? ({1'b0,A} - {1'b0, B }
    - {8'b0, (S[0]) ? 1'b0: C_in })
    : (S == 4) ? {C_in, (A & B) }
    : (S == 5) ? {C_in, (A ^ B) }
    : (S == 6) ? {C_in, (A | B) }
    : {C_in, B } ;

    assign F = ALU_OUT[7:0];

    assign C_out = ALU_OUT[8];

    assign Z_out = (S == 7) & Z_in
    | ( (S == 2) & Z_in | ~((S == 2)|(S == 7)) )
    & (ALU_OUT[7:0] == 0);

    assign V_out = ((S == 0)|(S == 1)) & (F[7] & ~A[7] & ~B[7]
    | ~F[7] & A[7] & B[7] )
    | ((S == 2)|(S == 3)) & ( F[7] & ~A[7] & B[7]
    | ~F[7] & A[7] & ~B[7] )
    | (S == 7) & V_in ;
    // ((S == 4)|(S == 5)|(S == 6)) ? 0

    assign H_out = ((S == 0) | (S == 1)) & (
    ~F[3] & A[3]
    | ~F[3] & B[3]
    | A[3] & B[3] )
    | ((S == 2) | (S == 3)) & (
    F[3] & ~A[3]
    | F[3] & B[3]
    | ~A[3] & B[3] )
    | (S[2]) & H_in;

    こう。これを仕様として、LUT 版を作ってみよう。

    LUT を引くのは、前半の算術演算のみ。LUT も 382 で作っているので同じように作る。サイズは 32 エントリ。
    問題は、Z フラグ。論理演算は、キャリーを使わないから、ボトルネックにならない。
    算術演算の Z フラグの計算で、上位 1bit か 2bit を直接演算すれば良いのではないか?

    ただ上位 1bit でも (Z_in) が加わるので 64 エントリになる。2bit なら 256 エントリ。

    とりあえずテーブルの元。

    // input: { S , C_in , A[0], B[0] }
    // ADD ADC SBC SUB
    // F 01100110 10101001 01101001 01100110
    // C 00010001 00010111 01001101 01000100
    // OV 00010001 00011000 01000010 01000100
    // (not used) (not used)
    // input: { Z_in, S[1:0] , C_in , A[0], B[0] }
    // Z 10011001 10010110 00000000 10011001 10011001 10010110 10010110 10011001
    // input: { Z_in, S[1:0] , C_in , A[1:0], B[1:0] }
    // Z 1010000010100000101000001010000010100000101000000000000000000000
    // 0000000000000000000000000000000010100101101001011010010110100101
    // 1010000010100000101000001010000010100000101000000000000000000000
    // 1010010110100101000010100000101010100101101001011010010110100101

    これ動かすことだけはできたが、全然ダメだ。遅くなって規模も増えた。

    うまく合成してくれないのも理由のひとつではあるが、設計上の失敗がいくつかある。

    ひとつの理由は、キャリーあり/なし を INDEX に組み込んでしまったこと。最下位ビットしか関係なく、テーブルが無駄になる。

    もうひとつの理由は、1桁づつ計算すること。普通に加減算のコードを書くと、ISE は、2桁づつ計算するように合成するようだ。ISE にまかせた方がよさそうな気がする。

    モジュール化自体は、規模が減ったし良かった。他にもやりたいことがあるので、今回は、これぐらいにしておこう。

もう少し検討してみる。

    もう、テーブル引きによる演算はあきらめた。そんなことより、zero の検出の高速化の方が重要。

    説明しておくと、rtavr での ボトルネックは、次のパスに決まっている。

      S1 でブランチ命令をデコードしていて、1つ前の命令が 加減算中で Z Flag の確定を待って 次の PC(プログラムカウンタ)を確定させる。

      加減算が一番遅く、現在は、その結果を元に Z Flag の計算をしているのだ。

    Z Flag が早く確定すれば、ボトルネックが解消するのだ。

    さて、加算で、結果が 0 になるケースを考えてみよう。

    キャリー C_in が 1 のとき、(A ^ B) == 8'b11111111 ときだけ結果が 0 。
    キャリー C_in が 0 なら (A ^ (B-1)) が 8'b11111111 のとき。

    では、減算はどうだろう。

    減算は、B の 2 の補数 を加算する。2 の補数は、(~B + 1) だから

    C_in が 1 のとき、(A ^ (~B + 1)) == 8'b11111111 ときだけ結果が 0 。
    C_in が 0 なら (A ^ (~B)) が 8'b11111111 のとき。これは、(A ^ B) == 0 に置き換えられる。

    これをインプリメントしてみよう。... しかし 、ここまでするのなら演算自体もまとめられそうな ...

    その前に仕様変更。

    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SBC (with carry)
    // S == 3 : SUB (wo carry)

    2 と 3 を入れ替えたほうが良さそうだ。

    // S == 0 : ADD (wo carry)
    // S == 1 : ADC (with carry)
    // S == 2 : SUB (wo carry)
    // S == 3 : SBC (with carry)

    こうすることで、キャリー付きが S[0] & C_in で表現できる。

ALU_Z_TUNE_1


    wire WC = (S[0] & C_in);
    wire [8:0] F_a =
    (~S[1]) ? ({1'b0, A} + {1'b0, B } + {8'b0, WC })
    : ({1'b0, A} - {1'b0, B } - {8'b0, WC });

    wire [7:0] R0 = (A ^ B);
    wire [7:0] R1 = (A ^ (B-1));
    wire [7:0] R2 = (A ^ (~B+1));

    wire F_a_z = ~S[1] & WC & (R0 == 8'hff)
    | ~S[1] &v ~WC & (R1 == 8'hff)
    | S[1] & WC & (R2 == 8'hff)
    | S[1] & ~WC & (R0 == 8'h00) ;

    wire [7:0] F_l = (S[1:0] == 0) ? (A & B)
    : (S[1:0] == 1) ? (A ^ B)
    : (S[1:0] == 2) ? (A | B)
    : B;

    assign F = (~S[2]) ? F_a[7:0] : F_l[7:0];

    assign C_out = (~S[2]) ? F_a[8] : C_in;

    assign Z_out = (~S[2]) ? (Z_in | (S[1:0] != 3)) & F_a_z
    : (S[1:0] == 3) & Z_in | (S[1:0] != 3) & (F_l == 0);

    (V_out/H_out は変更なし)

    これで +42 スライスも増えてしまった。そして肝心の性能は落ちたり... 上がったケースもあったのだが、どの変更が関係しているか分からなく...

    さて、2 の補数を作っているところがある。もったいなく感じて、演算もまとめってみた。


    wire [8:0] B2 = S[1] ? ~{1'b0, B} : {1'b0, B};
    wire [8:0] B2I = B2 + 1;
    wire [8:0] F_a = {1'b0, A} + (( S[1] ^ WC ) ? B2I : B2);

    wire [7:0] R0 = (A ^ B);
    wire [7:0] R1 = (A ^ (B-1));
    wire [7:0] R2 = (A ^ B2I);

    (あとは同じ)

    この変更で、わずかに規模が増えて、さらに遅く.... なかなか難しいものだ。

    wire F_a_z = (~S[1] & WC) ? (R0 == 8'hff)
    : (~S[1] & ~WC) ? (R1 == 8'hff)
    : ( S[1] & WC) ? (R2 == 8'hff)
    : (R0 == 8'h00) ;

    これに変えると、少し速くなった。だが、ノーマルより少し速いだけ。

(続く)
posted by すz at 02:37| Comment(1) | TrackBack(0) | CPLD
この記事へのコメント
Posted by 自作中毒ヂヂィ at 2011年05月28日 05:25
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/44839789
※ブログオーナーが承認したトラックバックのみ表示されます。

この記事へのトラックバック