FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V ISS Sv32, Sv39の実装

いまいち実装が進んでいないが、Sv32とSv39のISSへの実装しなおしが完了した。

Sv32とSv39については、前回の記事を参考にして欲しい。

msyksphinz.hatenablog.com

github.com

実装方法としては、まずはPTEを参照しながら上位の物理アドレスを生成していき、最後にPTEを参照しないアドレスを付加する。

  for (level = init_level-1; level >= 0; level--) {
    bool is_leaf_achieve = false;
    Byte_t p_pte[4] = {0};
    Addr_t va_vpn_i = static_cast<UWord_t>(vaddr) >> (12 + 10 * level) & 0x3ff;
    pte_addr += va_vpn_i * PTESIZE;
    pte_addr = ExtendSign(pte_addr, 31);

    LoadMemoryDebug (pte_addr, Size_Word, p_pte);
    memcpy (&pte_val, p_pte, 4);

...
    Addr_t mask = ((pte_val >> pte_idx[level]) & ((1 << pte_len[level])-1)) << ppn_idx[level];
    phy_addr |= ((pte_val >> pte_idx[level]) & ((1 << pte_len[level])-1)) << ppn_idx[level];
    pte_addr = ((pte_val >> pte_len[0]) & ((1 << (31-pte_len[0]+1))-1)) * PAGESIZE;
    // str << std::hex << " <Temporary Mask = " << std::hex << mask << ">\n";
    // str << std::hex << " <Temporary PhyAddr = " << std::hex << std::setw(16) << std::setfill('0') << phy_addr << ">\n";
    // DebugPrint ("%s", str.str().c_str());
    if (is_leaf_achieve) break;
  }

  // 下位のアドレスを接続する。
  for (level = level-1; level >= 0; level--) {
    phy_addr |= (vaddr >> vpn_idx[level]) & ((1 << vpn_len[level])-1) << ppn_idx[level];
  }

これで実装をしてみたのだが、テストパタンとしては途中でテーブル参照エラーを起こし、最後は無限ループになってしまった。結局最大サイクル数で停止してしまう。spikeでも同様のようだ。 これで本当にテストパタンはあっているのだろうか?

特権命令の例外権限を委譲するためのRISC-Vシステムレジスタmedeleg, mideleg

テストパタンのチェックをしていて、スーパバイザモードで例外が発生してもスーパバイザモードに留まっているパタンがおり、何が起きているのだろうと調べていると、以下のようなレジスタの仕様を発見した。

目的としては例外が発生した場合のコンテキストスイッチの時間を最小限にし、割り込みの応答性を上げるものらしい。 medelegレジスタmidelegレジスタは、本来スーパーユーザが実行すべき例外プログラムを、別の特権ユーザが変わりに実行するためのレジスタだ。

f:id:msyksphinz:20170215012210p:plain

上記の図だけ見ると何のことだか分からないが、各レジスタのビットフィールドはRISC-Vの割り込み、例外のビットエンコーディングに相当しており、 例えばbit1=命令アクセスフォルトに相当し、bit1=1ならば、この例外が発生した場合にプロセッサはマシンモードに移らず、スーパバイザモードで例外を処理する。

f:id:msyksphinz:20170215012413p:plain

割り込みの場合も同様だ。midelegレジスタを設定することで、割り込みが発生した場合に、マシンモードに遷移せずにより低い特権モードで動作することが出来る。

このようなテストパタンは実際に存在し、例えばriscv-testsのrv32ui-v-addテストパタンでは、最初に以下のような設定をしている。

        42:M:MBar:[8000299c][P8000299c] 30279073 : csrrw      r00,0x302,r15        medeleg=>00000000 r15=>000001a2 medeleg<=000001a2

これは、

  • Instruction Access Fault (bit1)
  • Load Address Fault (bit5)
  • Store/AMO access Fault (bit7)
  • Environment call from U-mode (bit8)

はマシンモードで処理されずに、スーパバイザモードで処理されることを意味する。

Bitnamiで構築したRedmineをBitbucketと同期させる

前回の続きだが、前回はRedmineGithubを同期させた。

msyksphinz.hatenablog.com

同じ仕組みで、RedmineとBitbucketを連携させることが出来る。同じように、bitbucketからdaemonアカウントを使ってgitリポジトリをダウンロードし、Redmineリポジトリを関連付ける。

Bitbucketにdaemonsshキーを登録しておけば、同様にbitbucketにプッシュしただけでRedmineリポジトリを反映させることが出来た。

f:id:msyksphinz:20170212120859p:plain

f:id:msyksphinz:20170212121127p:plain

Bitnamiで設定したRedmineをgithubと同期させる

個人の開発を記録するためにRedmineを導入したが、githubとの連携をまだ実現していなかった。

githubとの連携を実現するための情報はたくさんあるが、bitnamiで構築したRedminegithubはどのように連携すればよいのだろうか?調査してみた。

実現したいこととしては、githubに対してpushすると、自動的にRedmineリポジトリもアップデートして欲しい。

GithubRedmineを連携させる仕組み

ここでは、redmine_github_hookというプラグインを使用した。その名のとおりRedmineGithubを連携させるためのプラグインだ。

github.com

CentOS 6.8にインストールした場合は、Rubyのバージョンなどを上げないとうまく行かないようだったが、基本的にマニュアルどおりにインストールすることが出来る。 インストールが完了すると、Redmineを立ち上げて、http://redmineのページ/github_hookにアクセスしてきちんとメッセージが出るかを確認する。

f:id:msyksphinz:20170212025002p:plain

redmine_github_hookの仕組みはこうだ。まずgithubに対してpushが実行されると、webhookを通じて対象Redminegithub_hookに通知が発行される。 すると、Redmineの存在するサーバにgitリポジトリのクローンが作られており、それをアップデートさせる、という仕組みだ。

https://cloud.githubusercontent.com/assets/6480/3311503/3a789390-f6c5-11e3-804d-d5ca2562799f.png

(https://github.com/koppen/redmine_github_hook より画像抜粋)

これらの操作は、詰まるところがRedmineが実施している。

多くのredmine_github_hooksの解説記事では、Redmineサーバ上に配置するgitリポジトリのクローンを、マシン内でgitを引くための専用アカウントや、redmineを実行しているアカウントであるapacheを設定している。 しかし、Bitnamiで設定したRedmineの場合はどうだろうか?

ps aux | grep httpd
daemon    1976  0.0  0.3 394960  1756 ?        S    02:48   0:00 /opt/redmine-3.3.1-0/apache2/bin/httpd.bin -f /opt/redmine-3.3.1-0/apache2/conf/httpd.conf

daemonというユーザが実行している。さらにdaemonについて調べると、

less /etc/passwd
...
daemon:x:2:2:daemon:/sbin:/sbin/nologin
...

ホームディレクトリが/sbinか、、、なかなか難しいな。/sbin/だとsshキーを作ったとしても/sbin/.ssh/を置くのはちょっと厳しいかもしれない。別の場所を作っておこう。ここでは/home/daemon/とした。

less /etc/passwd
...
daemon:x:2:2:daemon:/home/daemon:/sbin/nologin
...

/home/daemon/.sshgithubとの連携を行うためのsshキーを作成し、githubと連携させた。このためには、いったんdaemonユーザに切り替わる必要がある。

sudo -s -u daemon
(daemonユーザにて) ssh-keygen -t rsa

Redmine用サーバにgitリポジトリのミラーをcloneする

Redmineサーバにgitリポジトリのミラーをcloneし、Redmineと同期させる。ここでは、まずいったんdaemonユーザにsuし、リポジトリをクローンした。

sudo -s -u daemon
cd /opt/repos
git clone --mirror git@github.com:msyksphinz/swimmer_iss.git
bash-4.1$ pwd
/opt/repos
bash-4.1$ ls -ltr
total 4
drwxr-xr-x 7 daemon daemon 4096 Feb 12 01:20 swimmer_iss.git

Redmineを設定して、リポジトリを参照させる。

f:id:msyksphinz:20170212031100p:plain

GithubのWebhookに、リポジトリのpushを通知させる

最後に、gitリポジトリにpushがなされた場合、対象のRedmineに通知が発行されるようにしたい。 これにより、Redmineサーバ側のリポジトリがfetchされ、情報がアップデートされる仕組みだ。

githubリポジトリ設定欄から「Webhook」の項目を設定する。http://redmineサーバ名/redmine/github_hook?project_id=xxxというフォーマットで指定する。xxxの部分は、Redmineのプロジェクトの識別子を記入する。

f:id:msyksphinz:20170212031411p:plain

f:id:msyksphinz:20170212031633p:plain

これでRedmineとのリポジトリ連携が完了した。ためしに、githubに向けてpushを通知してみよう。

echo test >> test && git commit -a -m "Func: add test5" && git push origin temp

雑なコミットだな!Redmine側には何か通知が行っているだろうか?Redmineのproduction.logを確認しよう。

Started POST "/redmine/github_hook?project_id=magnetor-1" for 192.30.252.34 at 2017-02-12 01:20:46 +0900
Processing by GithubHookController#index as */*
  Parameters: {"ref"=>"refs/heads/temp", "before"...
...
  Current user: anonymous
  GithubHook: Redmine repository updated: swimmer_iss (Git: 5311.4ms, Redmine: 456.5ms)
Completed 200 OK in 5794ms (Views: 2.0ms | ActiveRecord: 71.8ms)

GithubHookが働いて、リポジトリのアップデートがうまく行ったようだ。

f:id:msyksphinz:20170212032147p:plain

RISC-Vのメモリ仮想化の方式について (v1.9.1編)

自作ISSのテストパタンが通らなくなってきた!

特に仮想メモリを使っているパタンが通らなくなった。大昔に自作ISSに仮想アドレスから物理アドレスの変換を実装していたのだが、どうやら仕様がv.1.9.1で変わっているらしい。 もう一度チェックしてみよう。

ちなみに、前回の解説はこちら。

msyksphinz.hatenablog.com

ここから、いろんな仕様が変更されている。

sptbr(Supervisor Page-Table Base Register)の仕様について

f:id:msyksphinz:20170210005726p:plain

v1.9.1には、sptbrの仕様変更が行われている。これまでは上位ビットにアドレス変換のベースアドレスが格納されていたが、sptbrのベースアドレスは下位(PPN)に移動されている。 Sv32の仕様ならば、PAGESIZE=2^12を乗算してベースアドレスとしている。

ページテーブルエントリの仕様について

ページテーブルの変換では、メモリ上(あるいはTLB)上に存在するアドレス情報を探索していくが、ページテーブルのエントリの仕様も変更されている。

f:id:msyksphinz:20170210010955p:plain

属性ビットがv.1.9.1では増強されている。

  • V=Valid Bit. ページテーブルエントリが有効であることを示す。
  • R=Read Bit. ページが読み込み可能であることを示す。
  • W=Write Bit. ページが書き込み可能であることを示す。
  • X=Executable Bit. ページがフェッチ可能であることを示す。 ちなみに、RWXがどれも1がセットされていない場合は、次のページテーブルへのポインタページとなっている。
  • U=UserMode Bit. ユーザモードのソフトウェアはU=1のページテーブルでないとアクセスできない。
  • G=Global Mapping Bit. グローバルマッピング。すべてのアドレス空間に存在していることを示す。
  • A=Access Bit. アクセス済みビット
  • D=Dirty Bit. 書き込み済みビット

アドレス変換処理の仕様変更について

v.1.7の場合

  1. i=LEVELS-1 (LEVEL=2)からスタートする。つまり、上位の変換ブロックから変換を始める。
  2. va.vpn[i]×PTSIZE(=4)+sptbr のアドレスを計算し(これが変換テーブルのアドレスとなる。つまり、va.vpn[i]をインデックスとして変換テーブルを参照している)、ページテーブルエントリ(=pte)を参照する。
  3. pte.Vビットを確認し、0であれば無効テーブルエントリなのでアドレスエラーを出力する。
  4. pte.type が2以上であれば、テーブルの参照の終端まで行ったことになるので、ステップ5に進む。ここでpte.typeの意味は以下のように定義されているようだ (Privileged Reference Manual参照)。

f:id:msyksphinz:20160115022437p:plain

つまり、Type=0 or 1であれば次のテーブルへの参照となるが、それ以外は最終的なページテーブルとなる。 ここでテーブルのアクセスエラーを確認する。 Type=0,1のときは次のアドレスを計算する。a=pte.ppn×PAGESIZEとし(つまりベースアドレスを更新する)、再度ステップ2に進む。また、i<0となるとこれもテーブル不良のためアドレスエラーとなる。

  1. ステップ5まで来たということは、i>=0の間にページテーブルの終端に到達したことを意味する。ここでpte.typeのアクセス権限を確認し、アクセス権限が足りなければエラーとなる。 そうでなければ、以下の方針でアドレス変換を実行する。

  2. オフセットはそのまま利用する。 pa.pgoff = va.pgoff

  3. i>0(=つまり、インデックスの参照を0まで使い切らない)場合はi-1から0まで(つまり到達しなかったレベル)は、仮想アドレスをそのまま物理アドレスとして利用する。 pa.ppn[i-1:0]=va.vpn[i-1:0]。
  4. それ以外のインデックスはページテーブルを参照して変換する。pa.ppn[LEVELS-1:i] = pte.ppn[LEVELS-1:i]

v.1.9.1の場合

  1. i=LEVELS-1 (LEVEL=2)からスタートする。つまり、上位の変換ブロックから変換を始める。
  2. va.vpn[i]×PTSIZE(=4)+sptbr.PPN×PAGESIZE(=212) のアドレスを計算し(これが変換テーブルのアドレスとなる。つまり、va.vpn[i]をインデックスとして変換テーブルを参照している)、ページテーブルエントリ(=pte)を参照する。
  3. pte.Vビット、pte.Rビット、pte.Wビットを確認し、pte.V=0、もしくはpte.R=0&pte.W=1であれば無効テーブルエントリなのでアドレスエラーを出力する。
  4. pte.R=1, pte.X=1であれば、テーブルの参照の終端まで行ったことになるので、ステップ5に進む。そうでなければ、a=pte.ppn×PAGESIZEとして2.に戻る。

  5. ステップ5まで来たということは、i>=0の間にページテーブルの終端に到達したことを意味する。以下の方針でアドレス変換を実行する。

  6. オフセットはそのまま利用する。 pa.pgoff = va.pgoff

  7. i>0(=つまり、インデックスの参照を0まで使い切らない)場合はi-1から0まで(つまり到達しなかったレベル)は、仮想アドレスをそのまま物理アドレスとして利用する。 pa.ppn[i-1:0]=va.vpn[i-1:0]。
  8. それ以外のインデックスはページテーブルを参照して変換する。pa.ppn[LEVELS-1:i] = pte.ppn[LEVELS-1:i]

組み込みシステムにおけるアドレスの考え方 (VMA, LMAについて)

ちょっと話が逸れるが、今回は組み込みシステムにおけるVMAとLMAの考え方についてまとめておく。

例えばリンカスクリプトなどを組み上げるとき、「CPUから見たアドレス」と「外部からデータをRAMに配置するときにみる領域」が違うことがある。 例えば、こんな場合はどうだ。まずは0x0000_0000のROMにCPUが実行するべき命令とデータを置き、CPUがまずはそれらの情報を0x3000_0000から始まるRAMにコピーし、そこからフェッチして実行するとしたら? プログラムのアドレスはどこに設定して置けばよいのだろう?

  • マイコンの外部からデータを流し込む時:流し込みアドレスを0x0000_0000から始めたい(外部バスから見た場合、RAMが0x0000_0000にあるので)
  • プログラムをCPUが実行するとき:RAMにコピーされているので0x3000_0000から始まるアドレスで進めたい

f:id:msyksphinz:20170209005337p:plain

  • VMA(Virtual Memory Address) : プログラムが実際に実行するときに参照されるアドレス。通常はリンカスクリプトを記述するときは、このアドレスをベースに記述する。上記の例だと0x3000_0000として配置される。
  • LMA(Load Memory Address) : プログラムをロードするときに参照されるアドレス。外部からプログラムをロードするときに参照されるアドレス。上記の例だと0x0000_0000に相当する。

実際にはどのようなことが起きるのか(RISC-Vプログラムの例)

例えば、RISC-Vのプログラムを考えてみよう。RISC-Vはリセット時のフェッチアドレスが0x8000_0000に設定されている。このため、普通にプログラムをコンパイルしてobjdumpすると、0x8000_0000から配置されるが、 外部にRAMを配置している場合、0x8000_0000(つまり2GB空間から!)のRAMなんてものは存在しない。だいたいは0x8000_0000を先頭にRAMが置かれており、プログラムをロードするバスからは0x0000_0000から見えていたりする。

今自分が作っているRISC-V自作CPUだってそうだ。最初にフェッチリクエストがくるのは0x8000_0000だけれども、そんな高い領域まで存在するRAMは置けないので、0x8000_0000にマップする形で0x0000_0000からRAMを配置している。

riscv_cpu (
 .fetch_addr(w_fetch_addr),
 ...
);

assign ram_addr = w_fetch_addr - 0x8000_0000;   // Offsetを切り落とす。

ram_16k (
 .addr(ram_addr),  // アドレスは8000_0000のアクセスが0x0000_0000になるように
 ...
);

この場合、リンカスクリプトVMA=0x8000_0000, LMA=0x0000_0000を設定してみよう。

SECTIONS
{
        .text.reset 0x80000000 : AT(0x00000000) {
                *(.text.reset)
        }
        .text   0x80001000 : {
                *(.text)
        }
        .rodata : {
                *(.rodata.*) *(.rodata*)
...

AT(...)で記述されているのがLMAだ。これでコンパイルしてみる。objdumpしても、VMAで表記されているのが分かるだろう。

Disassembly of section .text.reset:

80000000 <_start>:
80000000:       00007197                auipc   gp,0x7
80000004:       90818193                addi    gp,gp,-1784 # 80006908 <_gp>
80000008:       0000a117                auipc   sp,0xa
8000000c:       11010113                addi    sp,sp,272 # 8000a118 <_sp>

80000010 <format_regs>:
80000010:       000000b3                add     ra,zero,zero
80000014:       00000233                add     tp,zero,zero
...

ところが、objdump -hで実行してみると、LMAとVMAが異なっていることが分かる。

$ riscv64-unknown-elf-objdump -h coremark.bin

coremark.bin:     file format elf32-littleriscv

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text.reset   000000a4  80000000  00000000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .text         00003784  80001000  00001000  00002000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  2 .text.startup 0000078c  80004784  00004784  00005784  2**2
...

下記のエントリを使ってLMAをベースにreadmemh用のファイルを作ってみると、以下のようになる。

sourceware.org

@00000000 110101130000a1179081819300007197   // 0x8000_0000
@00000001 00000333000002b300000233000000b3   // 0x8000_0010
@00000002 00000533000004b300000433000003b3   // 0x8000_0020
@00000003 00000733000006b300000633000005b3   // 0x8000_0030
@00000004 00000933000008b300000833000007b3   // 0x8000_0040
@00000005 00000b3300000ab300000a33000009b3   // 0x8000_0050
@00000006 00000d3300000cb300000c3300000bb3   // 0x8000_0060
@00000007 00000f3300000eb300000e3300000db3   // 0x8000_0070
...

RAMに置かれる場合は0x0000から始まるように配置されるが、CPUからフェッチされるときは0x8000_0000になっている。絶対アドレスのジャンプとかは、0x8000_0000をベースにして計算されるようになっている。

Intelの2.5D FPGA @ ISSCC

Intelがチップ間2.5D通信を採用したFPGAISSCCで発表した。

www.eetimes.com

まだ詳細は出て来ていないが、基本技術はEMIB(Embedded Multi-die Interconnect Bridge)というものらしい。

【後藤弘茂のWeekly海外ニュース】超広帯域メモリの採用を可能にするIntelの新パッケージング技術「EMIB」 - PC Watch

チップ間を2.5D接続で実現するためには、一般的にはインターポーザーを介して接続する必要がある。しかしインターポーザーでの接続は一般的には難しく、 HBMなどの2.5D接続技術がなかなか安定して供給できないのは、インターポーザーの設計が難しいとも言われている(らしい)。

www.4gamer.net

Intelの技術は、インターポーザーを使わずにパッケージの上位層を使ってダイ同士を接続するものだ。

f:id:msyksphinz:20170208213310p:plain

(http://www.eetimes.com/document.asp?doc_id=1331317&image_number=1 の画像より抜粋)

ISSCCの記事によると、ダイ同士を24個のトランシーバーで接続し、1ピンあたり2Gbit/sで動作、ビットあたり1.2pJの電力効率ということらしい。 4つの28GHzのSERDESを積んでいるようだ。