Link命令をどう実装するか(SCP vs. Pipelined)

jump and link

「31番」レジスタ$ra = リターンアドレス)に「PC+4」を格納しつつ、即値で与えられたアドレスにPCを切り替える。という命令

SCPの場合

f:id:varmil:20171211215027p:plain http://meseec.ce.rit.edu/eecc550-winter2005/550-chapter5-exercises.pdf

上記のように、 RegDstMemToReg のselectorを拡張する実装が多い。

Pipelinedの場合

    /*** EX Rs Forwarding Mux ***/
    Mux4 #(.WIDTH(32)) EXRsFwd_Mux (
        .sel  (EX_RsFwdSel),
        .in0  (EX_ReadData1_PR),
        .in1  (M_ALUResult),
        .in2  (WB_WriteData),
        .in3  (EX_RestartPC),
        .out  (EX_ReadData1_Fwd)
    );

    /*** EX Rt Forwarding / Link Mux ***/
    Mux4 #(.WIDTH(32)) EXRtFwdLnk_Mux (
        .sel  (EX_RtFwdSel),
        .in0  (EX_ReadData2_PR),
        .in1  (M_ALUResult),
        .in2  (WB_WriteData),
        .in3  (32'h00000008),
        .out  (EX_ReadData2_Fwd)
    );

    /*** EX ALU Immediate Mux ***/
    Mux2 #(.WIDTH(32)) EXALUImm_Mux (
        .sel  (EX_ALUSrcImm),
        .in0  (EX_ReadData2_Fwd),
        .in1  (EX_SignExtImm),
        .out  (EX_ReadData2_Imm)
    );

    /*** EX RtRd / Link Mux ***/
    Mux4 #(.WIDTH(5)) EXRtRdLnk_Mux (
        .sel  (EX_LinkRegDst),
        .in0  (EX_Rt),
        .in1  (EX_Rd),
        .in2  (5'b11111),
        .in3  (5'bxxxxx),
        .out  (EX_RtRd)
    );

https://github.com/grantae/mips32r1_core/blob/master/mips32r1/Processor.v#L497-L533

この例では、 RegDst の方(31番のレジスタを指定する、5bitアドレスの方)は同じように実装している。が、「PC+4」はフォワーディングの方を絡めて実装している。Link命令の際は、ハザード制御ユニットを経由して

  • Rs へ現在のPCをフォワード
  • Rt へ即値「8」をフォワード
  • ALUに計算させた結果をレジスタファイルに書き込み(MemToRegはmux2のまま拡張なし)

という感じで実装しているようだ。こちらがスタンダード?

FPGAにbootcodeやkernelをどう転送するか(SDRAMやUFMとやりとりするには)

いま使用しているDE−10 Liteは残念ながらSRAMを装備しておらず

[方法1]Qsys & System Consoleを使う

Qsys > SDRAM Controller
  • bit幅を16にする
  • wire」を右クリックして「Exported」にする
  • メニューの Generate > Show Instantiation Templateの結果をコピーする
Quartusに戻って
  • DE10_LITE_Golden_Top.v にペースト
  • wireを置き換えていく
参考:

f:id:varmil:20171205172440p:plain

System Console

あとは適当にデータを読み書きすればよい。

f:id:varmil:20171205172550p:plain

[方法2]DE-10 Lite Control Panelを使う

ここから落とすだけ。

Questions

Nios 2 を使わずにやりとりできる?(MIPS CPUからSDRAMやUFMをwrite, readできるか)

SDRAM
  • de10_lite_golden_top.v のようなテンプレートデザインを使用すると、DRAMピンが露出している。
UFM
  • PC --> UFMへのrwは可能っぽいが、CPUから直接は難しい?素直にNiosを使おう。

PCからJTAGで直接書き込みするには?

SDRAM
  • System Console > Tcl Console で可能(Avalon MM Bridgeが必要)
UFM
  • Qsysで何とかなりそう?

PCからkernel imageを直接SDRAMに転送するのがベストかも

  1. SDRAMの適当な番地(0x80000 = 8MB)とかにkernelmemfsを転送
  2. bootcode(カーネル展開コード)をROM(といいつつ実態は回路にする。あるいはUFMにload memory fileを指定してそっちを使っても良さそう)に直接書き込んでおく。リセットしたらここから命令フェッチを始める。
  3. memide.cを参考に、カーネル本体を物理メモリ --> 物理メモリへ再配置する。

ELFヘッダのパース

  • kernelmemfs、こいつをSDRAMにそもそもそのまま配置できるのか?
    • 割と余裕でできた。

【ELF形式】ELFヘッダ(2) - ゆずさん研究所

xv6のfs.imgはルートファイルシステム

kernelmemfsでビルドする場合、出力ファイルにfs.imgがくっついて完成するイメージ。ので特に気にせずELFヘッダに沿ってメモリ展開すれば良さそう。

Soc周辺機器

UART (8250, RS-232規格を満たしていればよい)

説明
実装例

PIC (i8259A)

説明
実装例

Timer (8253/8254)

説明
実装例

※GAIAでは自前のtimerを実装しており、kernelの機能は使ってない。PICに至っては使用すらしていない(CPUに直でインタラプトする)

MIPS Interruptのハードウェア実装(CP0)

CPU受付完了時にassertするIACKピンの実装

We also want to have to ability to service external interrupts. This is useful if a device external to the processor needs attention. To do this, we'll add 2 pins to the processor. The first pin, called IRQ (interrupt request), will be an input that will allow an external device to interrupt the processor. Since we don't want the processor to service any external interrupts before it is finished executing the current instruction, we may have to make the external device wait for several clock cycles. Because of this, we need a way to tell the external device that we've serviced its interrupt. We'll solve this problem by adding the second pin, called IACK (interrupt acknowledge), that will be an output. The following waveform defines the timing for external interrupt requests.

f:id:varmil:20171204155204p:plain

Exceptions and Interrupts for the MIPS architecture

これによってPICのIRR->ISRへの切替が行われるようになる。

CP0のverilog実装

mips32r1_xum/CPZero.v at master · grantae/mips32r1_xum · GitHub

非常に分かりやすい。が、MIPS Release 1では Cause.Ⅳ Cause.WP など使用しないフラグもあるので、その辺は自分で作る時は排除しても良さそう。また、このリポジトリではCPUのInt(0)をUART用、Int(4:1)をSwitch用に使用している(PICは使っていない。のでIACK pinも存在しない)

// これがコア側のInt*(4:0)に対応していて、CP0内部でクロック立ち上がり時に、
// Cause_IP[6:2] <= Int[4:0]; でIP部分に代入している
//-- Hw Interrupts --//
input  [4:0] Int,               // Five hardware interrupts external to the processor

.....

// IE, IP, IMを見て割り込みを起こすかどうか決める
// 2行目はEXL, ERLを見ているので、既に例外処理中だったら割り込みを起こさないということか
wire   Enabled_Interrupt = EXC_NMI | (Status_IE & ((Cause_IP[7:0] & Status_IM[7:0]) != 8'h00));
assign EXC_Int = Enabled_Interrupt & ~Status_EXL & ~Status_ERL & ~ID_IsFlushed;

.....

// この実装では割り込みはIDステージで検知する。stall中はReadyしない。
assign  ID_Exception_Detect = EXC_Sys | EXC_Bp | EXC_RI | EXC_CpU | EXC_Int;
assign  ID_Exception_Ready =  ~ID_Stall & ID_Exception_Detect & ~ID_Exception_Mask;

.....

// ここもクロック立ち上がり時に判定、つまりIPに代入した次のクロックでここへ入ってくる。
// ID stage
else if (ID_Exception_Ready) begin
    Cause_BD <= (Status_EXL) ? Cause_BD : ID_IsBD;
    Cause_CE <= (COP3) ? 2'b11 : ((COP2) ? 2'b10 : ((COP1) ? 2'b01 : 2'b00));
    Cause_ExcCode30 <= Cause_ExcCode_bits;
    Status_EXL <= 1;
    EPC <= (Status_EXL) ? EPC : ID_RestartPC;
    BadVAddr <= BadVAddr;

.....

/*** Cause Register ExcCode Field ***/
 always @(*) begin
     // Ordered by Pipeline Stage with Interrupts last
     if      (EXC_AdEL) Cause_ExcCode_bits <= 4'h4;     // 00100
     else if (EXC_AdES) Cause_ExcCode_bits <= 4'h5;     // 00101
     else if (EXC_Tr)   Cause_ExcCode_bits <= 4'hd;     // 01101
     else if (EXC_Ov)   Cause_ExcCode_bits <= 4'hc;     // 01100
     else if (EXC_Sys)  Cause_ExcCode_bits <= 4'h8;     // 01000
     else if (EXC_Bp)   Cause_ExcCode_bits <= 4'h9;     // 01001
     else if (EXC_RI)   Cause_ExcCode_bits <= 4'ha;     // 01010
     else if (EXC_CpU)  Cause_ExcCode_bits <= 4'hb;     // 01011
     else if (EXC_AdIF) Cause_ExcCode_bits <= 4'h4;     // 00100
     else if (EXC_Int)  Cause_ExcCode_bits <= 4'h0;     // 00000     // OK that NMI writes this.
     else               Cause_ExcCode_bits <= 4'bxxxx;
 end

EXC_Sys(システムコール例外に関して)

コア側の制御ユニットで、命令デコード時に「syscall」だったらEXC_Sys wireをassertする。それをCP0がInputとして受取り、検知する。ソフトウェア例外用に用意されている IP[1:0] は特に使用していないようなので無視して良いだろう。

TLB例外を追加する場合

  • TLBL(code=2), TLBS(code=3) を上記CP0クラスに追加する必要あり。input wireとして EXC_TLBL EXC_TLBS を追加して中のコードもいじる必要があるだろう

f:id:varmil:20171204204836p:plain

xv6 bootmain

0x7c00x86BIOSMBRを読み取って、物理メモリに展開するときの位置(仕様)

0x10000 はプログラム側でディスク読み取って、物理メモリに展開する際のdstアドレス。これはリンカスクリプトの設定と一致させればよい。

つまり、自作する際はリセットベクタ付近に、

  • bootasm.S(MIPSならレジスタ初期化とbootmain()へのjumpくらいしかやらなくてよい。)
  • bootmain.c(まず、ディスクからカーネル本体のELFヘッダを読み取り、それに従って本体を物理メモリ上に順次展開していく)

の双方を含むコードを配置しておけば良い。ブートローダ自体はコンフィギュレーションROM等に配置できそうだが、数百KBはある、カーネル本体(ELFイメージ)をどうやってFPGAに転送するのか要調査。

xv6-mipsリンカスクリプトとentry

リンカスクリプト

OUTPUT_FORMAT("elf32-tradlittlemips", "elf32-tradlittlemips", "elf32-tradlittlemips")
OUTPUT_ARCH(mips)
ENTRY(_start)

この _start はentry.Sに定義されている。

# By convention, the _start symbol specifies the ELF entry point.
.globl _start
_start:
  mfc0   $t0, $COP0_STATUS
  # Disable interrput
  li     $t1, STATUS_IE
  or     $t0, $t1, $t1
  xor    $t0, $t0, $t1
  # Setup IM registers preparing for enabling interrput
  ori    $t0, $t0, STATUS_IM
  # COP0
  lui    $t1, STATUS_CU0 >> 16
  or     $t0, $t0, $t1
  mtc0   $t0, $COP0_STATUS
  
  # Set up the stack pointer.
  la  $sp, stack + KSTACKSIZE

  # Jump to main()
  la     $t0, main
  jr     $t0
  nop

ここの最後で main() へ飛ぶ。memfsの仕組みがよくわかってないが、

xv6のI/O, PIC, IRQ, uart

プロセッサ側とポートを合わせなきゃいけないものと、kernel(ソフト側)で好き勝手に決めればいいものの境界線を探っている。xv6-mipsで定義している

mips.h

static int io_port_base = 0xb4000000;

これはなぜこのアドレスなのかわからない。inb outbuchar *へキャストしているのが関係ある? →QEMUのISAバスが Phys 0x1400_0000 に紐付いているから。 0xb400_0000 - 0x1400_0000 = 0xA0000000 (kseg1の先頭)となる。

uart 0x3F8

COM (hardware interface) - Wikipedia

これは仕様で決まってるっぽい

mips interrupt mask i/O

CPUで、「一体どのハードウェアからinterruptがきたのか」をどうやって判別するのか調べている。ついでにMemory-mapped I/Oの理解もきちんと出来ていないので。「こっからここまではデバイス用I/Oメモリ!」って決めるのは誰?CPUなのかkernelなのか、はたまたデバイスなのか。

f:id:varmil:20171113150735p:plain

https://courses.cs.washington.edu/courses/cse378/07au/lectures/L20-Interrupt.pdf

f:id:varmil:20171113150716p:plain

http://jjc.hydrus.net/cs61c/handouts/interrupts1.pdf

メモリマップドI/Oの入出力ポートとアドレスの割付はハードウェア(address decoder, address code decoderなどと呼ばれる)で決め打ち

このへんを見ると、わかるがI/OメモリマップはCPUの外にあるハードウェア(address decoderという回路)で制御しているっぽい。CPUが出力する物理アドレスを見て、それが特定のアドレス(I/Oにマップされている)の場合、向き先をデバイスに向ける。

CPUに内蔵されているデバイスであれば、ボードが違っても同じアドレスに見えるはずだよね。でも、デバイスを外付けした場合は、ボードの設計によるんだ。この場合は、ボードの仕様書にメモリマップが書かれているはずだよ。

I/Oポートベース(qemuだとPhys0x1400_0000)もハードウェアで好きに設定してよい

(xv6-mipsだと)ベースアドレスからのオフセットで、各周辺デバイスへのメモリマップを行う。COM1を 0x3f8 にしているのもPC/AT互換機のボード設定であり、MIPSおれおれボードで必ずしもこのアドレスに設定する必要はない。(あくまで、address decoderの回路とkernelの設定を一致させればよいだけ)

https://i.stack.imgur.com/rBbh1.png

cpu - Difference between port mapped and memory mapped access? - Super User

The hardware of the system is arranged so that devices on the address bus will only respond to particular addresses which are intended for them, while all other addresses are ignored. This is the job of the address decoding circuitry, and that establishes the memory map of the system.

Memory-mapped I/O - Wikipedia

自作CPUではPICはいらない

パタヘネ本にある通り、ハードウェア側でgeneral exception handler entry point addressを決め打ちする(0x8000_0180)。例外ベクタは使用しない。PICも必要ない。

DMA (I/O) Controller

I/Oを特に工夫しない通常のデータの流れだと、I/Oデバイスからレジスタファイルへデータ転送(lw)→レジスタファイルからRAMへデータ転送(sw) というのが読み出したいbyte分繰り返されることになり、その間CPUは他の仕事をできない。

これを、I/Oデバイス(DMA Controller)⇔RAM間で直接データ転送できるようにするのがDMAである。DMA Controller or I/O Controllerは回路であり、いわゆるマザボ等のハードウェア側で対応しなければ使用できない。

バスとは

単なる概念。Data Busは、ReadData[32], WriteData[32]に分かれていたりする。その集合概念