これはCPU実験 Advent Calendar 2015の17日目の記事です。

CPU 実験の 1st アーキテクチャとして実装したインオーダパイプラインコアについて

を中心にまとめました。

完全なソースコードは https://github.com/arcturu/CarteletV1 にあります。

twoprocとは

twoproc とは VHDL のデザイン手法のひとつで、適当に書くとしっちゃかめっちゃかになりがちな VHDL コードの構造化と、バグの予防に非常に役立ちます。

詳細については twoprocの書き方 と、リンク先ブログ内にあるスライドが非常にわかりやすいのでそちらを参照して下さい。

僕が感じた twoproc を採用する直接的なメリットとしては

などがあります。

パイプライン構成

パタヘネ

を参考に以下の四段構成にしました。

パタヘネでは五段目としてメモリアクセスフェーズがありますが、Cartelet v1 (1st アーキテクチャ) ではメモリリード命令も実行フェーズ内で完結させるようにしました。このようにした理由は、キャッシュメモリをつけるとヒット時とミス時でアクセスに必要なクロック数が変わり、それを違うパイプライン段で受けるのがしんどかったためです。

Cartelet v1 ではリードに 0 クロックしかかからないフリップフロップのキャッシュを 16 個つけていますが、実行フェーズでの挙動は

となります。

パイプライン化の方法

パイプラインという概念は本や講義では当たり前のように出てきて、「それ名前つけるほどのものでもなくね?」という感じがしつつも、実際に VHDL でどう書くのかと言われると少し詰まってしまうのではないでしょうか。僕はそうでした。

pipeline

こんな感じのパイプラインの概念図はよく目にしますが、以下の Cartelet v1 の cpu.vhd の抜粋と見比べるとほぼそのまま対応する部分があることがわかると思います。

https://github.com/arcturu/CarteletV1/blob/master/cpu.vhd

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.types.all;

entity CPU is
    port (
        clk : in std_logic;
        rst : in std_logic;
        cpu_in : in cpu_in_type;
        cpu_out : out cpu_out_type);
end entity;

architecture struct of CPU is
    type fetch_readreg_reg_type is record
        pc : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        fetched : std_logic;
        data : std_logic_vector (31 downto 0);
    end record;
    type readreg_ex_reg_type is record
        pc : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        data : std_logic_vector (31 downto 0);
        ex_op : std_logic_vector (5 downto 0);
        dest_num : std_logic_vector (4 downto 0);
        dest_value : std_logic_vector (31 downto 0);
        lhs_num : std_logic_vector (4 downto 0);
        lhs_value : std_logic_vector (31 downto 0);
        rhs_num : std_logic_vector (4 downto 0);
        rhs_value : std_logic_vector (31 downto 0);
        imm : std_logic_vector (15 downto 0);
    end record;
    type ex_wb_reg_type is record
        pc : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        dest_num : std_logic_vector (4 downto 0);
        data_source : data_source_type;
        sram_state : sram_state_type;
        sram_addr : std_logic_vector (19 downto 0);
        sram_data_to_be_written : std_logic_vector (31 downto 0);
        write : std_logic;
        data : std_logic_vector (31 downto 0);
        next_pc : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
    end record;
    type wb_mem_reg_type is record
        sram_state : sram_state_type;
        sram_data_to_be_written : std_logic_vector (31 downto 0);
        dest_num : std_logic_vector (4 downto 0);
    end record;
    -- (cache などの type 宣言)
    type reg_type is record
        sram_cache : sram_cache_type;
        sram_in_buf : std_logic_vector (31 downto 0);
        pc : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        bubble_counter : std_logic_vector (3 downto 0);
        repeat : std_logic;
        load_size : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        load_counter : std_logic_vector ((PMEM_ADDR_WIDTH - 1) downto 0);
        forwarder_file : forwarder_file_type;
        fetch_counter : std_logic_vector (3 downto 0);
        fetch_readreg_reg2 : fetch_readreg_reg_type;
        readreg_ex_reg : readreg_ex_reg_type;
        ex_wb_reg : ex_wb_reg_type;
        wb_mem_reg : wb_mem_reg_type;
        regs : registers_type;
        fregs : registers_type;
        fpcond : std_logic;
        alu_in : alu_in_type;
        fpu_in : fpu_in_type;
        cpu_state : cpu_state_type;
    end record;
    constant reg_init : reg_type := (
        sram_cache => (others => (
            data => (others => '0'),
            valid => '0',
            tag => (others => '0'))),
        -- (略)
        cpu_state => ready);
begin
--    pmem : dmem_sim port map (
    pmem : mem port map (
        clka => clk,
        wea => pmem_we,
        addra => pmem_addr,
        dina => pmem_din,
        douta => pmem_dout);

    alu_0 : alu port map (
        clk => clk,
        rst => '0',
        alu_in => alu_in,
        alu_out => alu_out);

    fpu_0 : fpu port map (
        clk => clk,
        rst => '0',
        fpu_in => fpu_in,
        fpu_out => fpu_out);


    comb : process (cpu_in, r, pmem_dout, alu_out, fpu_out)
        variable v : reg_type;
        -- (略: reg_type に入れる程でもない(次のクロックまで記憶する必要が無い) variables)
    begin
        v := r;
        cpu_ex_rst8 := '0';
        cpu_ex_go := '0';
        -- (略: variable の初期化)
        inst := (others => '0');
        case r.cpu_state is
-- この辺はプログラムローダ ----------------------------------------------------------------------
            when ready =>
                -- (略: プログラムローダと実行モードの切り替えをしている)
            when ploading =>
                -- (略: プログラムメモリにプログラムを書き込む)
            when dloading =>
                -- (略: データメモリ(SRAM) に .data 領域のデータを書き込む)
            when running =>
                pmem_we <= (others => '0');

                v_forwarder := r.forwarder_file(0);
-- write back -----------------------------------------------------------------------------
                if r.repeat = '0' then
                    v.forwarder_file(1) := r.forwarder_file(0);
                    if r.ex_wb_reg.write = '1' then
                        case r.ex_wb_reg.data_source is
                            when src_alu =>
                                tmp_data := alu_out.data;
                                v.regs (to_integer(unsigned(r.ex_wb_reg.dest_num))) := tmp_data;
                                v.forwarder_file(0).floating := '0';
                                v_forwarder.floating := '0';
                           -- (略: alu 以外の演算器からの出力データを処理
                        end case;
                        v.forwarder_file(0).reg_num := r.ex_wb_reg.dest_num;
                        v_forwarder.reg_num := r.ex_wb_reg.dest_num;
                        v.forwarder_file(0).valid := '1';
                        v_forwarder.valid := '1';
                        v.forwarder_file(0).value := tmp_data;
                        v_forwarder.value := tmp_data;
                    elsif v.repeat = '0' then
                        v.forwarder_file(0).valid := '0';
                        v_forwarder.valid := '0';
                    end if;
                end if;

-- execute -------------------------------------------------------------------------------------
                --      forwarding
                ex_lhs_value := r.readreg_ex_reg.lhs_value;
                ex_rhs_value := r.readreg_ex_reg.rhs_value;
                ex_dest_value := r.readreg_ex_reg.dest_value;

                -- (略: フォワーディングされてきたデータを ex_lhs_value などに入れる)

                flush_read := '0';
                if r.bubble_counter = x"0" then
                    v.ex_wb_reg.pc := r.readreg_ex_reg.pc;
                    v.ex_wb_reg.write := '0';
                    v.ex_wb_reg.sram_state := idle;
                end if;
                case r.readreg_ex_reg.ex_op is
                    when OP_ADD =>
                        v.ex_wb_reg.dest_num := r.readreg_ex_reg.dest_num;
                        v.ex_wb_reg.write := '1';
                        v.ex_wb_reg.data_source := src_alu;
                        v.alu_in.command := ALU_ADD;
                        v.alu_in.lhs := ex_lhs_value;
                        v.alu_in.rhs := ex_rhs_value;
                    when OP_ADDI =>
                    -- (略: 個々の命令の処理)
                    when others =>
                end case;
                if v.bubble_counter = x"0" and flush_read = '0' and v.repeat = '0' and no_fetch = '0' then
                    v.pc := std_logic_vector(unsigned(v.pc) + 1); -- pc すすめる
                end if;

-- read and decode -----------------------------------------------------------------------------
                -- (略: デコードして v.readreg_ex_reg を更新)

-- fetch ---------------------------------------------------------------------------------------
                v_pmem_addr := v.pc;
                if v.bubble_counter = x"0" and flush_read = '0' and v.repeat = '0' and no_fetch = '0' then -- v なのは意図的です
--                    v.fetch_readreg_reg2.pc := r.fetch_readreg_reg.pc;
                    v.fetch_readreg_reg2.pc := v.pc;
                    v.fetch_readreg_reg2.fetched := '0';
                end if;

                v.sram_in_buf := cpu_in.sram_din;
            when others =>
                pmem_we <= (others => '0');
        end case;

-- 計算結果を出力 -------------------------------------------------------------------------------
        cpu_out.ex_pop8 <= cpu_ex_pop8;
        cpu_out.sram_we <= cpu_ex_sram_we;
        cpu_out.sram_addr <= cpu_ex_sram_addr;
        cpu_out.sram_dout <= cpu_ex_sram_dout;
        cpu_out.ex_rst8 <= cpu_ex_rst8;
        cpu_out.state <= r.cpu_state;
        cpu_out.ex_go <= cpu_ex_go;
        cpu_out.ex_data <= cpu_ex_data;
        pmem_addr <= v_pmem_addr;
        pmem_din <= v_pmem_din;
        fpu_in <= v.fpu_in;
        alu_in <= v.alu_in;
        v.regs (0) := (others => '0'); -- must be zero
        rin <= v;
    end process;
    reg : process (clk)
    begin
        if rising_edge(clk) then
            r <= rin;
        end if;
    end process;
end struct;

ここで、データが流れる順序としては IF, ID, EX, WB なのにコード中では WB, EX, ID, IF の順に書いてあるのはこうすることでフォワーディングが自然に書ける(WB に来た計算済みデータを variable として EX に流し、EX のオペランドの値としてつかえるようにすればフォワーディングできる)ためです。ただあんまりステージ間を跨いだ variable をつかいまくるとそれがクリティカルパスになり、何のためにパイプライン化しているのかわからなくなってくるので、ステージを跨いだ variable は可能な限り減らさないと周波数が上がりません。Cartelet v1 ではフォワーディング周りがクリティカルパスになっています。

また、CPU モジュールから ALU, FPU などは切り出していますが、ここへの入力には v.alu_in や v.fpu_in という variable をつないでいます。v.alu_in などは EX フェーズで変更され、そのクロックのうちに ALU に入れたいからです。これを v.alu_in でなく r.alu_in とすると、EX フェーズで v.alu_in を変更した次のクロックで ALU にデータが入る事になり、1クロック遅れます。そもそも reg_type の中に alu_in などを入れずに、ALU の入力に直に EX フェーズで入れてしまえばいいような気がしますが、それをすると ALU を使わない命令 (fadd など) 時にも ALU の入力をいれる事を忘れがちです。これを忘れると ALU の入力にはラッチが生える(クロックの立ち上がり時にデータを拾い、次のクロックの立ち上がり時まで記憶するフリップフロップ動作ではなく、クロックの立ち上がり後も、Hi の間はデータの変化を拾い続けるラッチ動作になる)ことで xst (合成ツール) のタイミング制約判定が甘くなり?本来あるべきクリティカルパスが見えなくなる事があります(このあたりの原理はよく理解していませんが問題が起きることは確かです)。ALU の場合はとくに問題になりませんが、パスが伸びがちな FPU との接続部分にラッチが生えるとクリティカルなバグになることがあります。僕は FPU をつなぐときにラッチができていたのが原因で、マンデルブロ集合を描くサンプルプログラムを実行した時に、実行するたびに結果が変わった(しかも破壊されている)ことがあります。

〜破壊された mandelbrot の例〜

broken mandelbrot

ラッチへの対処法としては、signal への代入が全場合分けで尽くされているかどうかをチェックすることと、ラッチができると xst が

“WARNING:Xst:737 – Found n-bit latch for signal .”

のような warning を出すのでこれを見落とさずにしっかり直すことに尽きます。コードが長くなってくると数百個の warning が出ることがザラにあるので warning への感度がさがりますが、ラッチだけは絶対に消すように心がけています。

まとめ

Cartelet v1 のスペックは 39億命令 88MHz 102.7s (IPC 0.43) です。

今は OoO superscalar な 2nd アーキテクチャ Cartelet 2 を書いています。メモリ周りと分岐を上手いことしたいと思います。

おまけ

(左)フォワーディングのバグによって壊れたレイトレ画像

(右)正しい画像

min-rt6 min-rt

一見まともそうですが、ゴミがついていたりグラデーションが変だったりします。

原因を調べた所、出力ユニットのキューがあふれた場合に実行がストールするとフォワーディングが壊れて RAW ハザードが起きていたようでした。

comments powered by Disqus