(DE0-CV) FPGAマガジンのRISC-VをDE0-CVに実装する

FPGAマガジン No.18でRISC-Vを実装していたので自分もやってみる。

FPGAマガジンだとVerilogで記述しているけど、自分はVHDLで記述する。
なおかつ実装するのはDE0-CV。

ソースコードは紙面に載っているだけでサポートとかでの提供はないっぽい?
紙面を読みながらまずはfmrv32im_decode.vを実装してみる。

まずはentity

entity fmrv32im_decode is
    port (
        RST_N           : in    std_logic;
        CLK             : in    std_logic;

        -- インストラクション・コード
        INST_CODE       : in    std_logic_vector(31 downto 0);

        -- レジスタ番号
        RD_NUM          : out   std_logic_vector( 4 downto 0);
        RS1_NUM         : out   std_logic_vector( 4 downto 0);
        RS2_NUM         : out   std_logic_vector( 4 downto 0);

        -- イミディエイト
        IMM             : out   std_logic_vector(31 downto 0);

        -- 命令
        INST            : in    t_INST
    );
end entity;

INSTはズラッと並べるのもしんどいのでpackageファイルでrecodeタイプ宣言している。 こんな感じ。

    type t_INST is record
        INST_LUI            : std_logic;
        INST_AUIPC          : std_logic;
        INST_JAL            : std_logic;
        INST_JALR           : std_logic;
--------------------------------------------
--      略
--------------------------------------------
        INST_DIV            : std_logic;
        INST_DIVU           : std_logic;
        INST_REM            : std_logic;
        INST_REMU           : std_logic;
    end record;

次は命令タイプの判別とイミディエイトの生成部分

    -- イミディエイト判別
    sIMM.i_type <= JUDGE_I(INST_CODE);
    sIMM.r_type <= JUDGE_R(INST_CODE);
    sIMM.s_type <= JUDGE_S(INST_CODE);
    sIMM.b_type <= JUDGE_B(INST_CODE);
    sIMM.u_type <= JUDGE_U(INST_CODE);
    sIMM.j_type <= JUDGE_J(INST_CODE);
    c0_type     <= '0';
    
    -- イミディエイト生成
    process (CLK, RST_N) is
    begin
        if RST_N = '0' then
            IMM <= (others => '0');
        elsif rising_edge(CLK) then
            -- 各イミディエイトとtype判定結果をandすることで、
            -- 判定したtypeのイミディエイトのみ残して他をマスクしている。
            -- 多bit and 1bit というのはVHDL-2008の構文
            -- (多bitすべてに1bitをandする回路になる)
            IMM <= ( (IMM_I(INST_CODE) and sIMM.i_type)
                  or (IMM_S(INST_CODE) and sIMM.s_type)
                  or (IMM_B(INST_CODE) and sIMM.b_type)
                  or (IMM_U(INST_CODE) and sIMM.u_type)
                  or (IMM_J(INST_CODE) and sIMM.j_type) );
        end if;
    end process;

sIMMもpackageで定義したrecordタイプの信号。

    type t_IMM is record
        r_type : std_logic;
        i_type : std_logic;
        s_type : std_logic;
        b_type : std_logic;
        u_type : std_logic;
        j_type : std_logic;
    end record;

JUDGE_*()はfunction。 FPGAマガジンの実装と違い、こんな感じでそのタイプに属するOPCODEを判定してORしている。
OPCODEもpackageで定義したrecordタイプ。

    function JUDGE_R(
        INST_CODE : std_logic_vector(31 downto 0)
    ) return STD_LOGIC is
        variable O : std_logic_vector(4 downto 0);
    begin
        O := INST_CODE(6 downto 2);  -- OPCODE
        return ( O ?= OPCODE.OP_ADD or O ?= OPCODE.OP_SUB or O ?= OPCODE.OP_SLT or O ?= OPCODE.OP_SLTU
              or O ?= OPCODE.OP_AND or O ?= OPCODE.OP_OR  or O ?= OPCODE.OP_XOR
              or O ?= OPCODE.OP_SLL or O ?= OPCODE.OP_SRL or O ?= OPCODE.OP_SRA
             );
    end function;

ちなみにR-TYPEだとOPCODEぜんぶ同じ。論理合成で消える。
命令ぜんぶ書いたほうがわかりやすいかな。。と思ってこんな実装にしたけど、記述ミスでバグが入りそう……。
あとで作り直すかも。

?=ってのはVHDL-2008の構文。

イミディエイト生成箇所は、IMM_*()というfunctionで各イミディエイトを抽出して、イミディエイト判定結果で
andしてマスクするという作りにした。

イミディエイト抽出しているfunctionはこんな感じ。
RISC-V仕様書のChapter2, 2.2 Base Instruction Formatに書いてあるとおりに作った。

    -- Create Immediate for I_TYPE
    --  31          11 10        5 4         1        0
    --  -- inst[31] -- inst[30:25] inst[24:21] inst[20]
    function IMM_I(
        INST_CODE : std_logic_vector(31 downto 0)
    ) return std_logic_vector(31 downto 0) is
        variable tmp : std_logic_vector(31 downto 0);
    begin
        for i in 31 downto 11 loop
            tmp(i) := INST_CODE(31);
        end loop;
        tmp(10 downto  5) := INST_CODE(30 downto 25);
        tmp( 4 downto  1) := INST_CODE(24 downto 21);
        tmp(           0) := INST_CODE(          20);
        return tmp;
    end function;

レジスタ番号生成部はFPGAマガジンそのまま。
上の処理と整合性を取るならこれもfunction化すべきかも。

    -- レジスタ番号生成
    RD_NUM  <= INST_CODE(11 downto  7) when (sIMM.r_type or sIMM.i_type or sIMM.u_type or sIMM.j_type or c0_type)
        else (others => '0');
    RS1_NUM <= INST_CODE(19 downto 15) when (sIMM.r_type or sIMM.i_type or sIMM.s_type or sIMM.b_type)
        else (others => '0');
    RS2_NUM <= INST_CODE(24 downto 20) when (sIMM.r_type or sIMM.s_type or sIMM.b_type)
        else (others => '0');

あとは各種ファンクション生成部を作るだけ。
ただ、これが面倒そう。ゴリゴリ書くのは嫌なのでどうにか工夫したい。

FPGAで、VHDLで書いたinteger信号の範囲は守られるのだろうか

例えばVHDLで下記のようなintegerの信号を定義したとする。

signal sCountVal : integer range 0 to 9;

この信号は0~9の値を取る、と宣言している。
これはFPGAでどのように論理合成されるのだろうか。

FPGA上では信号線で値を表現するので、例えば信号線3本、3bitの信号なら0~7の値を取り、
信号線4本、4bitの信号なら0~15の値を取る。
値の取りうる範囲は2のべき乗にしかなり得ない。

0~9を表現できるのは4bitだが、では10~15はどうなるのだろうか。
10~15の値にならないように論理合成されるのだろうか。
DE0-CVでちょっと試してみよう。

DE0-CVの4つのプッシュスイッチのうち2つ(KEY0とKEY1)と7セグメントLEDを使って動作を確認する。
KEY0を押したらintegerのカウンターをカウントアップ
KEY1を押したらintegerのカウンターをカウントダウン
カウンターの値を7セグメントLEDに出力
という回路を作った。

コンパイルはQuartus Prime 17.0で行った。

    --------------------
    -- 略
    --------------------

    -- DE0-CVのPushスイッチ4つの立ち上がりエッジ信号
    signal sKEY_hiedge          : std_logic_vector( 3 downto 0);

    -- integerのカウンター
    signal sCountVal            : integer range 0 to 9;

begin
    --------------------
    -- チャタリング除去とか立ち上がりエッジ検出とかの記述
    -- 略
    --------------------

    -- up/down count
    process (CLOCK_50, RESET_N) is
    begin
        if (RESET_N = '0') then
            sCountVal <= 0;
        elsif rising_edge(CLOCK_50) then
            if (sKEY_hiedge(0) = '1') then
                sCountVal <= sCountVal + 1;  -- KEY0を押したらカウントアップ
            elsif (sKEY_hiedge(1) = '1') then
                sCountVal <= sCountVal - 1;  -- KEY1を押したらカウントダウン
            end if;
        end if;
    end process;

    --------------------
    -- 値を7セグメントLEDに出力
    -- 略
    --------------------

動かしてみた。

FPGAにsofファイルを書き込むと7セグメントLEDに0が表示される。
KEY0を押すとカウントアップする。
9までカウントアップした。
f:id:m_keishi2006:20170729230721j:plain

もしVHDLの記述通り、範囲が0-9であるのであれば次のカウントアップでオーバーフローして値が0になるはずである。
KEY0を押してみた。
f:id:m_keishi2006:20170729230729j:plain

7セグメントの値がA(16進表記)に変化した。
VHDLで記述したrangeの範囲を超えてしまった。
そのままカウントアップすると、F(16進表記)まで変化して、
f:id:m_keishi2006:20170729230735j:plain

もう一度押すとオーバーフローして0に戻った。
f:id:m_keishi2006:20170729230743j:plain

ちなみに0からカウントダウンするとFになった。

つまり、FPGAの実装としては4bit信号でカウンターが構成されているということ。
integerのrangeはあくまで何bitの信号とするのか決めるだけであり、値の範囲の制御はちゃんと記述する必要がある。
これを認識しないと思い違いによるバグを生みかねないので注意しよう。

ちなみにSTARCではintegerのrangeの範囲は2のべき乗にしなければならないというルールがあったはず。
(うろ覚えだけど)

追記:
ModelSimでカウントアップしてみた。
integerの範囲を超えるたときにFatal ErrorになってSimが止まった。
SimのFatal Errorが誤動作の原因になるような記述を見つける指標になると思う。

VisualStudioでVHDL編集する (2)


VisualStudioでVHDL編集する (1)

V3SのTrialライセンスは1ヶ月間のみ有効 1ヶ月後は使えないので、それ以上使いたいならライセンスを買う必要がある。
ここではPrivateライセンスの購入方法を解説する。

Privateライセンスを買う

V3Sのライセンス体系はこのようになっている。
f:id:m_keishi2006:20170717121726p:plain

Privateライセンスは$42で、だいたい\5,000以下で買うことができる。
有効期限は1年間。
これを高いと思うか安いと思うかは人それぞれ・・・。
お金を出すか出さないか判断するためにも、Trialライセンスで十分に試そう。

Privateライセンスのところの「Purchase」をクリックすると下記の画面になる。
f:id:m_keishi2006:20170717164756p:plain

名前とメールアドレス、そして使用するPCのMACアドレスを入力する。 ライセンスはMACアドレスに紐付いているので、ここで登録したMACアドレスのPC以外からの利用はできない。

必要情報を入力して次に進むとこの画面になる。
f:id:m_keishi2006:20170717165106p:plain

入力は日本語で大丈夫だった。
クレジット、PayPalAmazonのいずれかで支払うことができる。
自分はPayPalで支払った。

お金を支払うと、ViDE-Softwareから「v3s-private-license.lic」というファイルがメールで送られてくる。
これをPCのどこかに格納する。

ライセンスをV3Sで読み込む

VisualStudioを起動し、V3S - Preference - Licensing を開く。
Path to license file をチェックして、「v3s-private-license.lic」の場所を読み込ませる。
License Summary がチェックマークになったら読み込み成功。
これで1年間、V3Sを利用できるようになった。
f:id:m_keishi2006:20170717165937p:plain
※画像は色々と情報を消してます。

MACアドレスを変更する。

MACアドレス変更にはSerial Numberが必要です。

MACアドレスを変更にはライセンスのシリアルナンバーが必要である。
シリアルナンバーは Preference - Licensing の Serial Number から確認可能。
v3s-private-license.licをテキストエディタで開いてもSerial Numberが書いてある。

MACアドレスを変更するということはPCを変更するということ。
gmail等でライセンスファイルを受け取った人は良いが、PCにしか保存していない人は、
シリアルナンバーがわからずにMACアドレス変更に苦労するかもしれない。
ライセンスファイルが送られてきたメールはできればクラウド上に保管しよう。

MACアドレス変更はとってもかんたん
ここのMACアドレス変更URLから変更しよう。

注意!
かんたんにMACアドレスを変更できるのは1回のみ。 2回目からはViDE-Softwareに連絡しなきゃならない。

VisualStudioでVHDL編集する (1)

V3SというVisualStudioのアドインを使う。

開発元はViDE-Software

ライセンス形態はこんな感じ。
自分は1ヶ月のTrialライセンスを使ってみて、良さそうだと感じたのでPrivateライセンスを購入した。
f:id:m_keishi2006:20170717121726p:plain

Trialライセンスを使う

  1. VisualStudioをインストールする

V3SはVisualStudioのアドインなので、当然ながらVisualStudioをインストールしないと使えない。
VisualStudioをインストールしよう。
V3SはVisualStudioのバージョンごとに提供されており、2017-07-17では

VS2010, VS2012, VS2013, VS2015, VS2017

に対応したバージョンが提供されている。

なお、アドインはVisualStudio Expressでは利用できないので注意。
Community版を使用しよう。(Pro版が使える環境ならそちらで)

※4月にインストールしたときはVS2017だとプロジェクトを開いたときにVisualStudioが落っこちて使えなかった。
 Updateされたのかわからないが、ひとまずVS2015バージョンで説明する。

VisualStudioのインストールは解説記事がたくさんあるので説明するつもりはない。
つもりだったが、VisualStudio2017がリリースされてから以前のバージョンをインストールするのが面倒になっていたので
(自分の覚え書きも兼ねて)簡単に説明する。

VisualStudioのダウンロードページを開く
ダウンロード | IDE、Code、Team Foundation Server | Visual Studio

最新版のVisualStudioであれば VisualStudio Communityの「無償ダウンロード」をクリックすればインストーラが手に入るので
そのままインストールすればOK。

それより以前のバージョンをお探しの場合はここでは見つからないので、下の方にグリグリスクロールして「以前のバージョン」をクリックする。
f:id:m_keishi2006:20170717123504p:plain

表示されたページの「Visual Studio (MSDN) サブスクリプションにサインインする」をクリックする。
f:id:m_keishi2006:20170717124033p:plain

Microsoftアカウントでサインインするように促されるのでサインインする。
もしMicrosoftアカウントを持っていない人は作成する必要がある。

・・・すごく厭らしい。まぁ、VisualStudio利用者はMicrosoftアカウントを持っている人が多いと思うが。。

ちなみに、「MSDN サブスクリプションをお持ちでない方へ」というところの「今すぐ無料で参加する」ではインストーラは手にはいらないので注意。
そもそもそれもMicrosoftアカウントを要求してくる。

サインインしたら、検索窓で「Community 2015」とでも検索すればCommunity 2015のインストーラが手に入る。
インストールするのは Visual Studio Community 2015 with Update 3 とかで良いんじゃないかな。
その他のバージョンが欲しい場合は2015のところを必要なバージョンに変更すれば良い。

  1. V3Sをインストールする。

V3SはVisualStudioのMarketplaceからダウンロードできる。 下記からインストールしているVSのバージョンを選んでダウンロードし、インストールする。

marketplace.visualstudio.com

インストールしているVSバージョンと違うものはインストールできないようになっている。

インストール後、VisualStudioを起動してメニューバーに「V3S」が見えたらインストールできている。
f:id:m_keishi2006:20170717125434p:plain

  1. V3Sを使ってみる。

「新しいプロジェクト」のウィザードに、「V3S-Extension」が追加されているので選ぶ。
適当にプロジェクトを作成する場所を選んでOKを押す。 f:id:m_keishi2006:20170717125844p:plain

こんなウィザードが出てくる。
f:id:m_keishi2006:20170717130212p:plain

AlteraのProjectファイルやXilinxのProjectファイルから、登録されているソースを自動で読み込んでくれる機能を持っている。 ただ、この機能を利用するとVisualStudioプロジェクトを作成する場所を間違うとVisualStudioが落っこちるので注意。 (そのうち解説できればいいな~)

ここではSample Projectを選ぶ。
SampleProjectが作成されると、ソリューションエクスプローラーにソースファイルが表示される。
f:id:m_keishi2006:20170717130721p:plain

試しにspi.vhdを開いてみるとこんな感じで表示される。
f:id:m_keishi2006:20170717130931p:plain

シンタックスハイライトだけでなく、マウスオーバーすると定義が表示される。
これで「あれ、この信号は何bitだったっけ?」とか「このrecordタイプの信号って何があったっけ?」とか悩まずに済む。
f:id:m_keishi2006:20170717131220p:plain
f:id:m_keishi2006:20170717131543p:plain

その他にもVisualStudioのショートカット機能やスニペット機能を使える。

設定を変えたい場合はメニューバーで V3S - Preference から。 Code Coloring で色を変更できる。

なお、Verilogシンタックスハイライトしか対応していない。
VHDL相当の機能は開発中らしい。

とりあえずTrial版の説明はここまで。
次回はPrivate版の購入方法を解説する。

VisualStudioでVHDL編集する (2)

PC購入

使用していたPC(HP Z400)がOSの修復中から起動しなくなったので新しいPCを購入。
(セーフモードですら起動しなくなった)

CADとかするだろうということでWorkstationなんぞを使っていたけれど、ぶっちゃけCADなんかやらなかった。
今回は、CPUはCore i7とし、GPUモロモロを旧PCから流用するつもりでドスパラで組んだ。
流用するのはGPU(GTX970とQuadro2000)、HDD(1TBx2でRAID構成だった。今回はRAIDにせず計2TBで使用)
メモリは残念ながら再利用不可だった。再利用しようと思って刺せなくて気が付いた。
旧PCはPC3-10600で、新PCはPC4-19200。会社のPCがPC3-10600だったと思うので会社に持っていこう。

覚書として新PCのスペックを記載。

項目 内容
OS Windows10 Pro 64bit
メモリ 16GB DDR4 SDRAM(PC4-19200/8GBx2/デュアルチャネル)
電源 玄人志向 700W 静音電源 (80PLUS TITANIUM / KRPW-TI700W/94+)
CPU インテル Core i7-7700K (クアッドコア/HT対応/定格4.20GHz/TB時最大4.50GHz/L3キャッシュ8MB)
SSD 500GB SSD
HDD1 2TB HDD
HDD2(*) 1TB HDD
HDD3(*) 1TB HDD
光学ドライブ ブルーレイドライブ (読み書き対応)
GPU1(*) GeForce GTX970
GPU2(*) Quadro 2000
Officeソフト Microsoft® Office Home and Business 2016 (Word/Excel/PowerPoint/Outlook/OneNote)

約21万円だった。
※HDD2,HDD3,GPU1,GPU2は旧PCから流用

HDD2とHDD3が認識しない……。
ディスクの管理でも出てこないので、たぶん電源をつながなきゃならないのだと思う。
SATAケーブルしかつないでない。旧PCがそうだったから……)

Quadro2000からちゃんと映像出力されない。マウスカーソルしか見えない。
なんか直近のWindows10のアップデートからこんな感じ。 旧PCで何とかしようと電源のON/OFFくりかえしてたらOSがぶっ壊れた。。のでもう諦めることにする。

せっかくCore i7-7700Kを積んでいるので、GTX1080Tiとか載せてみたい。
そんなお金ないけど。

7セグメント・デコーダ

7セグメントLEDのデコーダをモジュール化した。
iValue(4bit)に応じた値 (0-F) を出力する。

デコード値はconstantで設定している。 kDigitがデコード値を格納した2次元配列になっていて、
例えばkDigit(3)は7セグメントLEDで3を表示する値となる。

iValueの値をconv_integerでintegerに変換するだけ。

library IEEE;
    use IEEE.std_logic_1164.all;
    use IEEE.std_logic_misc.all;
    use IEEE.std_logic_arith.all;
    use IEEE.std_logic_unsigned.all;

entity Decoder7seg is
    port (
        CLOCK_50          : in    std_logic;
        RESET_N           : in    std_logic;     
        
        iEnable           : in    std_logic;
        iValue            : in    std_logic_vector( 3 downto 0);
        oEncValue         : out   std_logic_vector( 6 downto 0)
    );
end Decoder7seg;

architecture RTL of Decoder7seg is

    --  -- 0 --
    --  5     1
    --  -- 6 --
    --  4     2
    --  -- 3 --
    constant kDigit0 : std_logic_vector(6 downto 0) := "1000000";
    constant kDigit1 : std_logic_vector(6 downto 0) := "1111001";
    constant kDigit2 : std_logic_vector(6 downto 0) := "0100100";
    constant kDigit3 : std_logic_vector(6 downto 0) := "0110000";
    constant kDigit4 : std_logic_vector(6 downto 0) := "0011001";
    constant kDigit5 : std_logic_vector(6 downto 0) := "0010010";
    constant kDigit6 : std_logic_vector(6 downto 0) := "0000010";
    constant kDigit7 : std_logic_vector(6 downto 0) := "1111000";
    constant kDigit8 : std_logic_vector(6 downto 0) := "0000000";
    constant kDigit9 : std_logic_vector(6 downto 0) := "0010000";
    constant kDigitA : std_logic_vector(6 downto 0) := "0001000";
    constant kDigitB : std_logic_vector(6 downto 0) := "0000011";
    constant kDigitC : std_logic_vector(6 downto 0) := "1000110";
    constant kDigitD : std_logic_vector(6 downto 0) := "0100001";
    constant kDigitE : std_logic_vector(6 downto 0) := "0000110";
    constant kDigitF : std_logic_vector(6 downto 0) := "0001110";

    type tDigitSelect is array(0 to 15) of std_logic_vector(6 downto 0);

    constant kDigit : tDigitSelect := ( kDigit0, kDigit1, kDigit2, kDigit3,
                                        kDigit4, kDigit5, kDigit6, kDigit7,
                                        kDigit8, kDigit9, kDigitA, kDigitB,
                                        kDigitC, kDigitD, kDigitE, kDigitF
                                    );
    
    signal sEnable : std_logic;
    signal sValue  : std_logic_vector( 3 downto 0);
    
begin

    -- Retiming Input signal
    process (CLOCK_50, RESET_N) is
    begin
        if RESET_N = '0' then
            sEnable <= '0';
            sValue  <= (others => '0');
        elsif rising_edge(CLOCK_50) then
            sEnable <= iEnable;
            sValue  <= iValue;
        end if;
    end process;

    -- Encode data
    process (CLOCK_50, RESET_N) is
    begin
        if RESET_N = '0' then
            oEncValue <= (others => '1');
        elsif rising_edge(CLOCK_50) then
            if (sEnable = '0') then
                oEncValue <= (others => '1');
            else
                oEncValue <= kDigit(conv_integer(sValue));
            end if;
        end if;
    end process;

end RTL;

(DE0-CV) DE0拡張キットのLCDモジュールを使う (5)


(DE0-CV) DE0拡張キットのLCDモジュールを使う (4)

(4)で作成したVHDLを使ってLCDモジュールを動かしてみました。
これがそのときの写真です。
f:id:m_keishi2006:20170507001545j:plain

( ^ω^)・・・薄くね?
これでもコントラストはGND直結の最大状態……。
まぁ、本来は5V動作のものを3.3Vで動かしているのだから当たり前か……。

一応、Vddを5Vにするとこんな感じ。ちゃんと映る。
(コントラスト調整済) f:id:m_keishi2006:20170507001802j:plain

でも、FPGAのIOは3.3Vなのでさすがに5Vで動かしているモジュールと直結するのは避けたい。
DE0はよくこんなの使ってるなぁ……。Writeだけしかしないこと前提にしてるのかな。

というわけで、LCDモジュールは動いたけど電源電圧に問題ありの状態。
間にレベルシフタなどを入れれば問題ないだろうけど、それは面倒くさい。

そのため、3.3Vで動く別のLCDモジュールを使うことにしました。
マルツ電波のこいつです。
https://www.marutsu.co.jp/pc/i/87076/

RTLはまったくいじっておりません。
f:id:m_keishi2006:20170507002345j:plain

ぶっちゃけLCDモジュールならどれでも同じように使えるので、例えば秋月電子
http://akizukidenshi.com/catalog/g/gP-04794/
とかでも良いです。