lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

Chiselを使ったRISC-Vの勉強(6.パイプライン化とALUを使用する命令の見直し)

突然ですがいままでシングルラインで組んでいたCPUプロジェクトをいきなり5ステージパイプラインにしました。

いきなり!ステージ


f:id:Lynx-EyED:20200524120200j:plain
すまんこれがやりたくてステージングしたわけじゃないんだ信じて欲しい。このクソコラ作るのがこの記事書く時に一番時間かかったけど信じて欲しい。

パイプライン化に向けての準備

本来でしたら、シングルステージでまだ実装が終わっていないMEMステージまで実装後複数段パイプラインにするのが筋だとおもいます。
しかしプロジェクトを進めていく上で問題が発生していました。

  • 機能が増えていくにつれて各ステージの境目がわからなくなってきていた
  • バス上にメモリマップドなメモリがあり、そこから命令の読み込み、データ読み書きをする前提である
  • シングルクロック未満(=非同期)でメモリへの読み込み、書き込みは現実的に不可能。例えばアドレスを与える→データを受け取るを考えても最低2クロック必要。
  • したがってメモリアクセスが終了するまで次の命令に移行しないようストール機能が必須
  • 併せて各機能で分離して動作させるためステージングも必要
  • RV32Iの場合、なお、データメモリは8bit/16bit/32bit単位でアクセスができないといけない
  • データメモリは32bitバスを前提としているのでバス幅調整に1ステージ分追加するかも見積もる必要がある。

したがってこの時点でシングルパイプラインで実装し続けるメリットは皆無であり、一旦パイプライン化してからデータメモリ、その他機能を追加し全容を把握するのが最適と言えます。なおMEMステージで実装するデータメモリは命令メモリと基本的RWは変わらないので32bit幅のアクセスだけできるようかなり大雑把に実装しています。
予定していたマルチパイプライン化にこの段階で取り組むことにしました。

参考にしたコード
rocket/src/main/scala/dpath.scala · fe9adfe71b90046d7d6b84d0c9fe033b06c9229d · CoDEx / rocket-chip · GitLab
本家のRocket-Chipのブランチから探しても同じものが取得できると思います。
ステージごとにかなり簡潔に記述されているので、自分のプロジェクトに導入する際にも分かりやすかったです。

パイプライン化をしたからと言って、いままでの資産が無駄になることはなく、必要とされるデータのレジスタをステージ分用意するだけです。
例えばCPUのパイプラインが5ステージあって、任意のデータが最終ステージまで必要とされているのであれば、データ x5 のレジスタが必要になるだけです。
任意のデータが3段目のステージまで必要とされているのであれば、データ x3のレジスタが必要、といった具合です。

また各ステージで前段の処理が終わっていないと次のステージに移行できないようであれは適宜ストール信号を発行できるようにします。
この部分は未完成な部分があるので、次回以降に取り扱いたいと思います。
行数がいきなり増えるのでびっくりしますが、各ステージで分けて考えられるようになるので、シングルステージよりも考えやすいと思います。

ここでパイプライン化する際に必須ではないですが併せて修正した部分を取り上げます。

ALUの変更

いままで算術、論理命令だけの演算をしていました。しかし条件分岐もおなじALUを使う事ができるはずです。実際同じ演算を行いますし、ALUと別に条件分岐ごとに比較演算機を外部に作るのは論理回路の浪費に思われます。
条件分岐の処理中に算術演算命令を並行して行うようなハザードが発生するステージはないからです。
そういう並行して動作する構成も面白そうですけどそれはまたの機会に。

というわけで、少し処理を追加する必要がありました。
以下の4つの演算です

  • ALU_SEQ (入力された2つの値(in.a, in.b)は等しいか?)
  • ALU_SNE (入力された2つの値(in.a, in.b)は等しくないか?)
  • ALU_SGE (in.a >= in.bか?)
  • ALU_SGEU (UInt(in.a) >= UInt(in.b)か?)


※ALU_SGE (io.op1 >= io.op2か?)、SGEU(SGEの符号なし)はすでに実装済みです。
以下のような実装です。

object ALU {
    def ALU_ADD:    UInt = 0.U(5.W)
    def ALU_SLL:    UInt = 1.U(5.W)
    def ALU_SEQ:    UInt = 2.U(5.W)
    def ALU_SNE:    UInt = 3.U(5.W)
    def ALU_XOR:    UInt = 4.U(5.W)
    def ALU_SRL:    UInt = 5.U(5.W)
    def ALU_OR:     UInt = 6.U(5.W)
    def ALU_AND:    UInt = 7.U(5.W)
    def ALU_COPY1:  UInt = 8.U(5.W)
    def ALU_COPY2:  UInt = 9.U(5.W)
    def ALU_SUB:    UInt = 10.U(5.W)
    def ALU_SRA:    UInt = 11.U(5.W)
    def ALU_SLT:    UInt = 12.U(5.W)
    def ALU_SGE:    UInt = 13.U(5.W)
    def ALU_SLTU:   UInt = 14.U(5.W)
    def ALU_SGEU:   UInt = 15.U(5.W)

    def ALU_X:      UInt = 0.U(5.W) // BitPat("b????")

    def isSub(op: UInt): Bool = op >= ALU_COPY2     // need sub?
    def isCmp(op: UInt): Bool = op >=ALU_SLT        // Compare op?
    def isCmpU(op: UInt): Bool = op >= ALU_SLTU     // Compare unsigned?
    def isCmpI(op: UInt): Bool = op(0)              // need inverse for compare?
    //noinspection ScalaStyle
    def isCmpEq(op: UInt): Bool = !op(3)            // EQ or NEQ compare operation?


}

import ALU._

//noinspection ScalaStyle
class ALU extends Module {
    val io = IO {
        new Bundle {
            val op1: UInt = Input(UInt(32.W))
            val op2: UInt = Input(UInt(32.W))
            val alu_op: UInt = Input(UInt(5.W))
            val out: UInt = Output(UInt(32.W))
            val cmp_out  = Output(Bool())
        }
    }

    // Shift
    val op2_inv: UInt = Mux(isSub(io.alu_op), ~io.op2, io.op2).asUInt()
    val sum: UInt = io.op1 + op2_inv + isSub(io.alu_op)
    val shamt: UInt = io.op2(4,0).asUInt
    val shin: UInt = Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL,io.op1,Reverse(io.op1))
    val shift_r: UInt = (Cat(isSub(io.alu_op) & shin(31), shin).asSInt >> shamt)(31, 0)
    val shift_l: UInt = Reverse(shift_r)
    val slt: Bool = Mux(io.op1(31) === io.op2(31), sum(31), Mux(isCmpU(io.alu_op), io.op2(31), io.op1(31)))

    val cmp = isCmpI(io.alu_op) ^ Mux(isCmpEq(io.alu_op), (io.op1 ^ io.op2) === 0.U, slt)

    val w_out =
        Mux(io.alu_op === ALU_ADD || io.alu_op === ALU_SUB, sum,
        Mux(io.alu_op === ALU_SLT || io.alu_op === ALU_SLTU, cmp,
        Mux(io.alu_op === ALU_SRA || io.alu_op === ALU_SRL, cmp,
        Mux(io.alu_op === ALU_SLL, shift_l,
        Mux(io.alu_op === ALU_AND, io.op1 & io.op2,
        Mux(io.alu_op === ALU_OR,  io.op1 | io.op2,
        Mux(io.alu_op === ALU_XOR, io.op1 ^ io.op2,
        Mux(io.alu_op === ALU_COPY1, io.op1 , io.op2))))))))


    io.out := w_out
    io.cmp_out := cmp


}
}

実装にあたってはいつものriscv-miniを参考にしました。
github.com

今回追加した機能はmini-riscvには含まれてはいませんので、自前で追加実装する必要があります。
ド・モルガンの法則がわかってればなんてことはないでしょ、、、ってここを実装するのすんごい時間かかった。
高専の数学1の最後の方を20年ぶりくらいに必死に勉強し直す始末に。
みんな社会人になっても宇宙人になっても仙人になってもおじいさんおばあさんになっても数学の勉強やろうね。趣味すらできなくなるから。

新編 高専の数学1 第2版・新装版

chisel-iotestersでプローブする信号の追加

今のところ必要としている信号は以下のものです。適宜追加していきます。

  • IF/IDステージ: pc , 機械語
  • EXステージ:rs1, rs2, imm
  • MEMステージ:ALU演算結果
  • WBステージ: ALU演算結果とrd

アセンブラを組む

前回のものに少し手を加えただけです。
今回の動作確認における実質的な内容は変わらないので各行の日本語のコメントは省略します。
test.sに以下を記述します

_start0:
    addi x29, x0, 10    # x29 = x0 + 10 = 10
    addi x31, x29, 0xAA # x31 = x29 + 0xAA = 180 (= 0xB4)
_label1:
    addi  x1,  x0, 1    # x1 = x0 + 1 = 1
    addi  x2,  x0, 2    # x2 = x0 + 2 = 2
    addi  x3,  x0, 3    # x3 = x0 + 3 = 3
    addi  x4,  x0, 4    # x4 = x0 + 4 = 4
    addi  x5,  x0, 5    # x5 = x0 + 5 = 5
    addi  x6,  x0, 6    # x6 = x0 + 6 = 6
    addi  x7,  x6, 7    # x7 = x6 + 7 = 13 (= 0x0D)
    addi  x8,  x0, 8    # x8 = x0 + 8 = 8
    jal   x4,  _label4  # x4 => address(_label2), jump _label4
_label2:
    addi  x9, x0, 9     # x9 = 9
    addi x10, x0, 10    # x10= 0x0A
    addi x11, x0, 11    # x11= 0x0B
_label3:
    jalr  x0, x5,0      # jump to x5 (= _label5)
_label4:
    addi x12, x0, 12      # x12= 0x0C
    jalr x5,  x4, 4      # x5 => _label5, jump x4+4 (= _label2+4)
_label5:
    jal  x0, _label5    # forever loop
    nop

動作確認

make clean
make 

で動作確認を行う事ができます。IntelliJのユーザはTerminalで一旦上記コマンドを実行してからrunしてみてください。(runだけだと機械語ファイルがない場合エラーになる)
ここではおもにアセンブラファイル2行目のaddi x31, x29, 0xAAに注目したいと思います。
x29の値(0xAが格納されている)とimmの値(0xAA)の足し算の結果をx31に書き戻すという命令です。


f:id:Lynx-EyED:20200524145150p:plain
図のように、各ステージごとに命令が処理されWBステージでx31に足し算の結果が書き戻されているのを確認する事ができました。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.8 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

次回はハザードの解決に取り組みます。






















Chiselを使ったRISC-Vの勉強(5. 分岐命令の実装)

前回の記事と分けてしまったら内容がほとんど皆無になってしまいました。



ないようがないよう



(この辺りでみんなブラウザ閉じる)
年取るってこういうことなんだなぁ。



さて、本題です。
前回riscv-asからアセンブルした機械語をChiselプロジェクトにワンストップで導入する仕組みを構築しました。
分岐命令の相対アドレスを計算するのが大変なのと、immが命令タイプに応じて異なるため、ハンドアセンブルが厳しくなったからです。人為的ミスの低減にもなるはずです。

immの種類多すぎでは

今更感が非常に強いですが、immを整理しました。
命令セットのImmに対応するビット位置がまちまちなので、それをここで統一しました。
CSR関係のzimmもここで整理しました。なお、参考にしたのはこちら。

github.com

ざっとこんな感じです。riscv-miniとほとんど変わらない実装

object ImmGen {
    def apply(sel: UInt, inst: UInt): SInt = {
        val sign = Mux(sel === IMM_Z, 0.S, inst(31).asSInt)
        val b30_20 = Mux(sel === IMM_U, inst(30,20).asSInt, sign)
        val b19_12 = Mux(sel =/= IMM_U && sel =/= IMM_J, sign, inst(19,12).asSInt)
        val b11 = Mux(sel === IMM_U || sel === IMM_Z, 0.S,
            Mux(sel === IMM_J, inst(20).asSInt,
                Mux(sel === IMM_B, inst(7).asSInt, sign)))
        val b10_5 = Mux(sel === IMM_U || sel === IMM_Z, 0.U, inst(30,25))
        val b4_1 = Mux(sel === IMM_U, 0.U,
            Mux(sel === IMM_S || sel === IMM_B, inst(11,8),
                Mux(sel === IMM_Z, inst(19,16), inst(24,21))))
        val b0 = Mux(sel === IMM_S, inst(7),
            Mux(sel === IMM_I, inst(20),
                Mux(sel === IMM_Z, inst(15), 0.U)))

        Cat(sign, b30_20, b19_12, b11, b10_5, b4_1, b0).asSInt
    }

分岐命令

実装した分岐命令は以下に挙げる通りです。WLENは命令1ワードあたりのアドレスバイト長で、今回は4(=32bit)です。imm_XはXタイプの即値immという意味です。

疑似コード

rd = pc + WLEN, pc = pc + imm_J
  • jalr

疑似コード

rd = pc + WLEN, pc = rs1 + imm_I
  • beq

疑似コード

if (rs1 == rs2) then pc = pc + imm_B else pc = pc + WLEN
  • bne

疑似コード

if(rs1 =/= rs2)then pc = pc + imm_B else pc = pc + WLEN
  • blt

疑似コード

if(rs1 < rs2) then pc = pc + imm_B else pc = pc + WLEN
  • bltu

疑似コード

if(rs1.asUInt < rs2.asUInt) then pc = pc + imm_B else pc = pc + WLEN
  • bge

疑似コード

if(rs1 >= rs2)then pc = pc + imm_B else pc = pc + WLEN
  • bgeu

疑似コード

if(rs1.asUInt >= rs2.asUInt)then pc = pc + imm else pc = pc + WLEN

前回のコミットと比較し、すべての分岐命令においてALUを使うように修正しました。回路規模肥大化の抑制を期待できます。

バブルロジックの導入

next_inst_is_validが真の時はそのまま、偽の時は次アドレスの命令をバブルします。
JAL、JALR命令は特に条件なく分岐をしてくれるので、この命令があった場合は次にフェッチしている命令語をバブルします。
Bから始まる分岐命令の場合、条件が真となれば次段でフェッチしている命令をバブルします。偽の場合は分岐しませんので命令はそのまま残します。

// bubble logic
next_inst_is_valid := true.B
switch (id_ctrl.br_type) {

    is( BR_NE ) {
        when(val_rs1 =/= val_rs2) {
            next_inst_is_valid.:=(false.B)} // NEQ = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_EQ ) {
        when(val_rs1 === val_rs2) {
            next_inst_is_valid.:=(false.B)}  // EQ = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_GE ) {
        when(val_rs1 > val_rs2) {
            next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branc
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_GEU ) {
        when(val_rs1.asUInt > val_rs2.asUInt) {
            next_inst_is_valid.:=(false.B)} // GE = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_LT ) {
        when(val_rs1 < val_rs2){
            next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_LTU ) {
        when(val_rs1.asUInt() < val_rs2.asUInt) {
            next_inst_is_valid.:=(false.B)} // LT = true: bubble next inst & branch
        .otherwise {
            next_inst_is_valid.:=(true.B) }
    }
    is( BR_J  ) { next_inst_is_valid.:=(false.B) }  // JAL
    is( BR_JR ) { next_inst_is_valid.:=(false.B) }  // JALR
}

命令語を間違えて解釈していた

Uが末尾につくアセンブラ命令。bltuやbgeu。
immがunsignedだと本気で誤解していました。

逝ってこいレジスタ。負が扱えないので帰ってこれない。すごい。作ってみてもいいかもですね。(ダメだろ)

正確には、rs1、rs2を比較時にunsignedとして扱う命令でした。ですよね。。immだったら、なんのためにauipc命令あるんだよという話になります。

JAL/JALR命令のテスト

そんなこんないろいろ間違いを治しつつ、テストコードを書きました。
(ここに掲載したのはjalとjalrだけです。bから始まる分岐命令も一応動作を確認済みです)
早くriscv-testを走らせられるレベルになりたい。

_start0:
    nop
    nop
_label1:
    addi  x1,  x0, 1    # x1 = x0 + 1 = 1
    addi  x2,  x0, 2    # x2 = x0 + 1 = 2
    addi  x3,  x0, 3    # x3 = x0 + 2 = 3
    jal   x4,  _label4  # x4に次アドレス(_lavel2)を格納し, _label4へジャンプ

_label2:
    addi  x7, x0, 7     # x7 = 7
    addi  x8, x0, 8     # x8 = 8 (RETURN HERE)
    addi  x9, x0, 9     # x9 = 9
    addi x10, x0, 10    # x10= 0x0A
    addi x11, x0, 11    # x11= 0x0B
_label3:
    jalr  x0, x5,0      # x5に格納されているアドレス(= _label5)へジャンプ

_label4:
    addi x12,x0,12      # x12= 0x0C
    jalr x5, x4, 4      # x5に次アドレス(_lavel5)を格納し, x4(= _label2) + 4のアドレス( addi  x8, x0, 8のところ)へジャンプ→(addi x7, x0, 7)は実行されない

_label5:
    jal  x0, _label5    # 無限ループ

結果を見てみます。intelliJであれば画面下部のterminalのタブで

make clean
make test

で動作します。
実行結果。


f:id:Lynx-EyED:20200508144407p:plain
この通り、x7に値は代入されていません。正しく動作していると言えるでしょう。

コード全文

github.com

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10.3 --depth 1 
cd KyogenRV/

macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

makefileでOSによってod/godをスイッチしています。
アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

次回はデータRAMの実装に入ります。

Chiselを使ったRISC-Vの勉強(4. プロジェクトへの機械語の読み込みの自動化)

今更ながら環境を整える

VSCodeにChiselプラグインを入れつつ頑張っていたのですが、IDEがオブジェクトやクラスの階層を把握していないため、補完機能が乏しいことなどから限界を感じてました。

いつも参考にさせていただいている、diningyo氏(id:diningyo-kpuku-jougeki)のブログ

Scalaの勉強 - 統合開発環境IntelliJ IDEAのインストール - ハードウェアの気になるあれこれ

というわけでIntelliJ IDEAに乗り換えました。すごく快適。最初っからこうしていればよかった。


f:id:Lynx-EyED:20200504114858p:plain

機械語の生成

いままでCPUロジックに与えるhexはハンドアセンブルしていましたが、そろそろ限界を感じています。
例えば、JAL命令。


f:id:Lynx-EyED:20200504020102p:plain
相対アドレスに使われるimmはこんなにビットシャッフルしているし、20bit(省略されているがLSBの1bit含めると21bit)長で、2の補数を取ります。
こんなの毎回のように手動アセンブラやってたら脳が停止してしまいます。それに、間違った機械語を記述していたのに気づかずデコーダを作ったら収拾がつかなくなります。

これに関してはみなさん工夫されているようです。

RISC-V(RV32I)のアセンブリから機械語への翻訳(簡易) - Qiita

a163236氏によるscala記述のアセンブラです。機械語がバイナリ文字列で出力されるので、コピペでChiselプロジェクトに貼り付けられるのが魅力。

さて、自分はscalaアセンブラとか記述できないので、実直にriscv64-unknown-elf-asとodコマンドを用いる方向にしました。


参考1:Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (3. CPUのコアのDebug-Trace作成) - FPGA開発日記
参考2:【 od 】コマンド――ファイルを8進数や16進数でダンプする:Linux基本コマンドTips(93) - @IT


Linuxやwsl環境で32bit(=4byte)長ごとに改行された可読できる機械語を出力するには以下のようにします。(アセンブラファイルはtest.s、欲しいファイルはtest.hexという名前とします。)

riscv64-unknown-elf-as test.s -o test.o
riscv64-unknown-elf-objcopy --gap-fill 0 -O binary test.o test.bin
od -An -tx4 -w4 test.bin >> test.hex


test.hexはこんな感じになります。

00000013
00100093
00100113
00200193
00300213
00400293
00500313
00600393
00700413
01b00e13
01c00e93
….

1行あたり32bitで、16進数表記です。
これならコピペしたり、scalaからSource.fromFileでオープンすることも可能です。
このブログの最後に明記しますが、Makefileを記述したので、make asmコマンドでワンストップで生成できます。
Scalaからこのファイルをオープンするにはどうしたらいいでしょう。
以下のようにしました。 間違ってたらご指摘ください。

val s = Source.fromFile("test.hex")
var bufs :Array[String] = _
try {
    bufs = s.getLines.toArray
} finally {
    s.close()
}
// このあと、Arrayから読み出すコードを記述する

ここまでのコード

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。
macOSな方はGNU odを使う関係上以下のようにしてcoreutilsをインストールしてください。

brew install coreutils

BSD odと区別するためなのはわかってるつもりですけど、GNU odをgodってするのどうかと思いますね………。
どうでもいいですけど。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.10 --depth 1 
cd KyogenRV/

アセンブラは(プロジェクトフォルダ)/src/sw/test.sにあります。適宜書き換えてみてください。

nano src/sw/test.s

------- 補足:riscv-gnu-toolchainの導入(まだ導入していない人向け) -------
今回のコードからriscv-gnu-toolchainが必須となります。
こちらからソースからビルドした方がいいかも。
GitHub - riscv/riscv-gnu-toolchain: GNU toolchain for RISC-V, including GCC
パッケージ管理がディストリビューションごとに違うので、依存ファイルの導入はMacの場合を書きます。

brew install python3 gawk gnu-sed gmp mpfr libmpc isl zlib expat coreutils

riscv-gnu-toolchainを導入します。

git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
git submodule update --init --recursive
./configure --prefix=/opt/riscv --enable-multilib
make

PATHを/opt/riscv/binに通します。

nano ~/.bash_profile

# または
nano ~/.bashrc

.bashrcまたは.bash_profileに以下を追加

export PATH=$PATH:/opt/riscv/bin

source ~/.bash_profileまたはsource ~/.bashrcでPATHを通します。
------- 補足終わり -------

保存後、プロジェクトのルートフォルダで

make asm

するとアセンブルがはじまります。
また依存関係の解決をMakefileにさせているのでtest.sに変更があった場合は

make test

したときに同時にアセンブルも行います。(riscv-gnu-toolchainが導入されていて、PATHが通っていることが前提です。)
ビルドが完了すると(project_root)/src/sw/test.hexが生成されます。

話題は変わりますが、上記プロジェクトには分岐命令の一部を実装しています。
この分岐命令の話などは、また次回の記事で。

Chiselを使ったRISC-Vの勉強(3. ALUの実装とIDステージの改良)

githubを巡回してみていろいろな実装を見て作戦を練っていました。
これまでRISCVの命令セットを見る限り、opcode(7bit) + funct3(3bit)で判別して適当に実装できるかなと考えていました。(10bitなら単純計算で1023命令は用意できるし)

  1. ALUへの演算命令、2入力への変数指定、結果の格納先(汎用レジスタ、pc、データメモリなど)指定
  2. データメモリ書き込みイネーブル、アドレス指定、読み込みイネーブル、アドレス指定
  3. CSR周りのゴニョゴニョ
  4. CPUストール制御信号
  5. 分岐制御信号

をIDステージで書けばいいかなと書き始めていました。

マイクロコードの記述方法の調査

IDステージの実装方法を調べているといろいろな実装が見つかります。opcodeから実直にimm,rs1,rs2,funct3を分けて実装しているのが、diningyo氏 (id:diningyo-kpuku-jougeki) のdirv
https://github.com/diningyo/dirv/blob/master/src/main/scala/dirv/pipeline/Idu.scala
とっても簡潔に記述されています。
氏のdirvはステージごとに綺麗に分かれていてパイプラインを組むときにとても作業がしやすそうです。実際、dirvトップモジュールはそれぞれのステージをConnectするだけの記述になっています。

しかし、いろんなrisc-v実装を見てると、マイクロコードはほとんどの実装で示し合わせたようにこんな実装(全く全てが同じではないですが、雰囲気としては同じ)をしてました。

class IDecode  {
    val table: Array[(BitPat, List[BitPat])] = Array(
    LW      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_W) ,
    LB      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_B ),
    LBU     ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_BU ),
    LH      ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_H ),
    LHU     ->  List(Y, BR_N  , OP1_RS1, OP2_IMI , ALU_ADD ,  WB_MEM, REN_1, MEN_1, M_XRD, MT_HU ),
……
)}

分岐制御、ALUの入力指定、IMMの種類、ALUでの演算指定、メモリ書き戻し、レジスタ書き戻し、などの指定がListで格納されています。
本家Rocket-chipなどがこの実装だから、なのはわかってたのですが、いろいろ調べてました。

lowRISCというプロジェクトがあり、メモリなどのカスタマイズができるようです。その際にこの体裁で拡張やカスタマイズを進めていくようなのでこの手順を真似しておいた方が今後新たにSoCの設計するときに有利だと思いました。
参考:Adding HW/SW support for the load and store tag instructions · lowRISC: Collaborative open silicon engineering

命令デコード(ID)ステージの実装

自分が参考するにあたって、上記のような手法でIDステージを記述しており、かつChisel3.2以降*1に対応したものはないか、さがしていました。

これが良さげ。
GitHub - ucb-bar/riscv-mini: Simple RISC-V 3-stage Pipeline in Chisel

Chisel3で記述されていて、かなりシンプルでした。これを参考に組み立てていきます。
方針としては、

  • RV32Iに対応しているアセンブラのIDステージのみ移植し、いま自作しているプロジェクトにフィットするよう書き換える。
  • ALUは32bit固定にして、わかりやすく記述。今後対応しきれないようであれば都度書き換え。

という感じで行くことににしました。

ALUの実装はこんな感じです。へんなところで頑張らない。足し算引き算、(排他的)論理和論理積、シフトくらい。
ざっと見てVerilogコードがなんとなくでも想像できる形。(一部抜粋)

class ALU extends Module {
    val io = IO (new Bundle 
    {
        val op1     = Input(UInt(32.W))
        val op2     = Input(UInt(32.W))
        val alu_op  = Input(UInt(4.W))
        val out     = Output(UInt(32.W))
    })
    val shamt = io.op2(4,0).asUInt
    val w_out = Wire(UInt(32.W))

    w_out := MuxLookup(io.alu_op, io.op2, Seq(
      ALU_ADD  -> (io.op1 + io.op2),
      ALU_SUB  -> (io.op1 - io.op2),
      ALU_SRA  -> (io.op1.asSInt >> shamt).asUInt,
      ALU_SRL  -> (io.op1 >> shamt),
      ALU_SLL  -> (io.op1 << shamt),
      ALU_SLT  -> (io.op1.asSInt < io.op2.asSInt),
      ALU_SLTU -> (io.op1 < io.op2),
      ALU_AND  -> (io.op1 & io.op2),
      ALU_OR   -> (io.op1 | io.op2),
      ALU_XOR  -> (io.op1 ^ io.op2),
      ALU_COPY1 -> io.op1))
    
    io.out := w_out
}

30行足らず。わーい。
このALUをCPU側のモジュールにインスタンシエートし、入力出力をIDステージのフラグから判別してやればOK
ALUの入力セレクタは今のところこんな実装。I-type命令じゃない即値immなどは真面目に実装していないのでこれから頑張る。

 // ALU OP1 selector
    val ex_op1 = MuxLookup(id_ctrl.alu_op1, 0.U(32.W),
        Seq(
            OP1_RS1 -> rv32i_reg(idm.io.inst.rs1),
            OP1_IMU -> 0.U(32.W),   // DUMMY 
            OP1_IMZ -> 0.U(32.W)    // DUMMY
        )
    )
    // ALU OP2 selector
    val ex_op2 = MuxLookup(id_ctrl.alu_op2, 0.U(32.W),
        Seq(
            OP2_RS2 -> rv32i_reg(idm.io.inst.rs2),
            OP2_IMI -> idm.io.inst.imm,   
            OP2_IMS -> 0.U(32.W)    // DUMMY
        )
    )


// ALUの入力にConnectし、ALUのオペレーションを4bit長で指示
import ALU._
    val alu = Module(new ALU)
    alu.io.alu_op := id_ctrl.alu_func
    alu.io.op1 := ex_op1
    alu.io.op2 := ex_op2

ALUの出力先はpcとかデータメモリとか色々あるはずですが、今回はまだ汎用レジスタだけ実装しています。

when (rf_wen === REN_1){ // 汎用レジスタ書き込みイネーブルであれば
        when (rd_addr =/= 0.U){
            rv32i_reg(rd_addr) := alu.io.out // 汎用レジスタに書き込み
        }.otherwise { // rd_addr = 0
            rv32i_reg(0.U) := 0.U(32.W) // ゼロレジスタが指定されていたら、強制的にゼロを書き込む(nop命令などの用途)
        }  
    }

ALUを使い、かつ汎用レジスタのみで読み書きが完結する命令はI,およびR-typeの命令です。


f:id:Lynx-EyED:20200428205047p:plain
その中から3タイプに分けました。

  • rd,rs1,imm(12bit)を使う命令
  • rd,rs1,imm(5bit)を使う命令
  • rd,rs1,rs2を使う命令

それぞれから1命令づつピックアップしてハンドアセンブルして、結果が期待された値となっているか確認。

  • addi
  • slli
  • add

をピックアップ。
下記のように、バイナリを記述して、汎用レジスタの結果を見ます。

(中略)
0x00A00593L, // addi x11,x0,10 (x11 = 0 + 10 = 10)
0x00259093L, // slli x1,x11,2 (x1 = x11 << 2 = 40)
0x00B00613L, // addi x12,x0,11 (x12 = 11)
0x00C581B3L, // add x3,x11,x12 (x3 = x11 + x12 = 10 + 11 = 21 = 0x15)

期待される結果は、
x1 = 40(= 0x28)
x3 = 21(= 0x15)
x11 = 10(= 0x0A)
x12 = 11( = 0x0B)
です。
結果:


f:id:Lynx-EyED:20200428205023p:plain

コード全文

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.9.1 --depth 1 

*1:初期の頃に記述されたものはChisel2以前のバージョンで記述しているサンプルも多いため

Chiselを使ったRISC-Vの勉強(2)

汎用レジスタ(x0-x31)の実装

RISC-V(RV32I)の汎用レジスタは32bit長で32個あります。


f:id:Lynx-EyED:20200423201049j:plain
下記Manualのpp.10(Fig2.1)参照
https://content.riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf

まずRISC-Vの命令デコードには必須なのでこちらを用意します。
Chiselだとレジスタ宣言、ゼロ初期化まで1行でかけます。x0 = zero registerで固定ですが、めんどくさいのでx1-x31とまとめて確保しました。

val rv32i_reg   = RegInit(VecInit(Seq.fill(32)(0.U(32.W)))) // x0 - x31:All zero initialized

デバッグトレース

今のところ、デバッグトレースで必要なものは何かを考え、テストオブジェクトに入出力できるようにしました。

  • 汎用レジスタ (x0-x31)
    • x0は要らないと思ったのですが、論理回路の設計ミスでx0にゼロ以外が書き込めちゃったりしたのでしばらくの間監視できるようにします。アセンブラによるエラーを期待してもいいのですが、そうすると悪意あるコードからHWを守れないので。

  • PC
    • Program counterの読み書きができるようにしました。halt信号(後述)をtrueにしてから書き換え推奨。
  • halt信号
    • 前回の記事で既に実装済み。命令メモリへの書き込みや汎用レジスタの読み書き、PC書き換え、その他デバッグの時にこの信号をtrueにしてCPUを停止させます。
  • 命令メモリ

(ID)命令デコードの実装

力弱く、addiだけ実装しました。これを実装すると、上記で用意したx0-x31までのテストができるからです。

以下のようなアセンブラを記述します。

addi x1, x0, 0; // x1 = x0 + 0 (=0) 
addi x2, x0, 1; // x2 = x0 + 1 (=1) 
addi x3, x0, 2; // x3 = x0 + 2 (=2) 
addi x4, x0, 3; // x4 = x0 + 3 (=3) 
….
addi x31, x0, 1E; x31 = x0 + 0x1E (=30) 

ここまで説明しておいてアレですけどアセンブラ流し込むスクリプトを記述してなかったので(おい)、、機械語で書きましょう。
AddiはRISC-Vの中でI-Typeの命令語で12bitの即値(imm)、1つの代入元(ソース)レジスタ(rs1)、1つの代入先(デスティネーション)レジスタ(rd)から構成されます。rs1はx0~x31、rdはx1~x31上にあるいずれかのレジスタ*1となります。


f:id:Lynx-EyED:20200423203344j:plain
ADDIのopcodeはb0010011、funct3はb000です。

f:id:Lynx-EyED:20200423203521j:plain
よって、addiの機械語b????_????_????_????_?000_????_?001_0011であれば良いことがわかります。16進数で表記すると、

  0x00000093, // addi x1,x0,0 (x1 = x0 + 0 = 0)
  0x00100113, // addi x2,x0,1 (x2 = x0 + 1 = 1)
  0x00200193, // addi x3,x0,2 (x3 = x0 + 2 = 2)
  0x00300213, // addi x4,x0,3 (x4 = x0 + 3 = 3)
  ....
  0x01E00F93  // addi x31,x0,30

となります。うっひょ、マシン語とか超楽しいマジ無理。早くアセンブラからバイナリ流し込む機能実装しないと。
Chiselで機械語の判定はBitPatを使うと簡単にできます。

when (inst_code === BitPat("b????_????_????_????_?000_????_?001_0011")){ 
        // ADDI = imm12=[31:20], src=[19:15], funct3=[14:12], rd=[11:7], opcode=[6:0]
        val dest = inst_code(11,7) // rd-pointer
        val imm  = inst_code(31,20) // imm
        val src  = inst_code(19,15)  // src-pointer
        val dsrc = rv32i_reg(src)  //rs
        when (0.U < dest && dest < 32.U){ //アドレスは1~31か?
            rv32i_reg(dest) :=  dsrc + imm
        }
}

と言った感じです。
テストコードを書き直します。1部抜粋です。

poke(c.io.sw.halt, true.B) // CPUを停止
step(1)
for (addr <- 0 to (memarray.length * 4 - 1) by 4){    
  poke(c.io.sw.wAddr, addr)
  poke(c.io.sw.wData, memarray(addr/4)) // 指定アドレスに機械語をロード
  println(f"write: addr = 0x${addr}%08X, data = 0x${memarray(addr/4)}%08X")
  step(1)
}
poke(c.io.sw.w_pc, 0)   // プログラムカウンタをゼロに戻す=reset
step(1)                 // fetch pc  

poke(c.io.sw.halt, false.B) // CPUを再開させる
 step(1)

for (lp <- 0 to (memarray.length - 1) by 1){
 val a = peek(c.io.sw.addr)
 val d = peek(c.io.sw.data)

  println(f"read : addr = 0x$a%08X, data = 0x$d%08X") // メモリ上の機械語のダンプ
  step(1)
}

step(1)
poke(c.io.sw.halt, true.B)
step(1)
for (lp <- 0 to 31 by 1){

  poke(c.io.sw.gAddr, lp)
  step(1)
  val d = peek(c.io.sw.gData)

  println(f"read : x$lp%2d = 0x$d%08X") //x0 ~ x31レジスタの内容をダンプ
  step(1)
}

動作確認


f:id:Lynx-EyED:20200423205112p:plain
うん、うごいた。

コード全文

コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.4 --depth 1 

きょうはここまで。

*1:x0はゼロレジスタなので代入先(デスティネーション)レジスタにはなれない

Chiselを使ったRISC-Vの勉強(1)

前回紹介したようにプロトタイプ用のFPGAができて, その後USB Type-C化などもいたしました。
これで電力問題が解決したため、FPGA基板からRaspiに給電かつ給電時の衝突防止までできるようFETを追加する改造もしました。


f:id:Lynx-EyED:20200420161916j:plain

さて、とても使いやすいRISCVのSCR1を紹介する予定でしたが、こちらはまた後での紹介にしようかなと思います。
なおgithubにSCR1のCyclone10LP向けの修正は置いてありますので参照ください。
GitHub - panda5mt/fpga-sdk-prj: FPGA-based SDK projects for SCRx cores

Chiselやるか

あまり気乗りはしてなかったのですが、昨今のコロナの影響で、思うように部品調達ができず仕事が従来の半分くらいの稼働率なのでChiselを勉強しています。動機としては

  • UC BerkeleyでもRISC-Vの実装に使われている
  • HDLのようにとりあえずシミュレーションのために合成するという手間が省けそう
  • 高位合成言語ではない(抽象度は同じ)
  • 自前のRISC-Vが必要になってきた

と言った感じでしょうか。
合成時間中にいろいろ閃いたことを忘れたり、ロジックが複雑になると合成時間だけでかなりもったいない。
あと、自分の開発の仕方に問題があるとは思うのですが、複雑なロジックの場合、単純なロジックから組み立ててシミュレーションするのですが、それでも結構作り込んでからシミュレーションしてしまう(FPGA実機でうごいてるし、わざわざめんどくさいとか。そんなの)。複雑化していくと指数関数的にシミュレーションも遅くなっていきます。
あと、この期に及んでフルスクラッチする理由としては入手できるRISC-V実装がバスがオリジナル(おそらく学習用)、またはAXIベース(その方が汎用性は高い)であり、SCR1でもAvalon-MMへはバスブリッジを利用していてQsysで細かい設定をしたい自分としては不便ということがあります。
またSCR1はパイプラインが最大4段となっていて、CPUストール頻度が5段に比べてかなり高い。

などなどがあります。intel PSG FPGAに実装する前提で、はじめからCPUバスを意識して実装してフルスクラッチで実装していきたい。

参考となったブログ、サイト

  • Chisel handbook

https://github.com/schoeberl/chisel-book/wiki/chisel-book.pdf

  • Test(PeekPokeTester)の書き方

Chiselで作ったモジュールをPeekPokeTesterでテストするまでの流れのまとめ - ハードウェアの気になるあれこれ

  • 初めはここを見ながら書きました。とても丁寧なまとめ

ChiselのBundleの使い方をまとめてみる - ハードウェアの気になるあれこれ

  • IFステージの実装はここを参考に

Chisel-Templateを使ってオリジナルデザインを作ってみるチュートリアル (1. デザインの作成) - FPGA開発日記

方針

基本的にCPUは以下のステージ構成を取るはずです。命令が複数存在しているCPUであることが前提ですが。。

(IF)命令フェッチ
(ID)命令デコード
(EX) 演算
(MA)メモリアクセス
(WB)レジスタ書き込み

  • それぞれのステージをChiselで記述、テスト
  • 上記ステージを接続
  • シングルパイプラインCPUとしてテスト
  • 5ステージパイプライン化
  • 64bit(RV64I)化

とりあえず、命令をフェッチする部分を記述します。
今の状態ですとはっきり言ってVerilog/VHDLで記述した方が10倍速で組めそうですが、我慢して組んでいきます。上記参考ブログでも書かれていますが、本当にBundleは慣れると武器になるはず。
まず、CPUのPCとかメモリの前にインターフェースを記述しておきます。後でAvalon-MMに移行しやすいように。
一部抜粋になります。

// address channel bundle
class AddressChannel extends Bundle {
    val req     = Output(Bool())        // request signal
    val addr    = Output(UInt(32.W))   // address (32bit)
}

// data channel bundle
class DataChannel extends Bundle {
    val ack = Output(Bool())        // data is available ack
    val data = Output(UInt(32.W))   // data (32bit)
}


// HOST :read only(IMem)
// HOST :read/Write(Dmem)
class HostIf extends Bundle {
    // IO definition
    val r_ach = new AddressChannel
    val r_dch = Flipped(new DataChannel)    // flipped I/O
    // write operation
    val w_ach = new AddressChannel   
    val w_dch = new wDataChannel	
//(中略)
}

// Memory-Mapped Slave IF
// Slave :read/Write(IMem)
// Slave :read/Write(Dmem)
class SlaveIf extends Bundle {
    // IO definition
    // read operation
    val r_ach = Flipped(new AddressChannel) // flipped I/O
    val r_dch = new DataChannel	
    // write operation
    val w_ach = Flipped(new AddressChannel) // flipped I/O
    val w_dch = Flipped(new wDataChannel)	

}

基本的インターフェース(read/write)を記述し、ホスト側スレーブ側に割り当てます。
Flippedを使うとIn/Out logicが反転するので記述が楽になります。

CPU側。PCをカウントアップし、アドレスをインターフェースに与えるだけ。
こちらも一部抜粋

class Cpu extends Module {
    val io = IO(new HostIf)
    
    // initialization    
    val r_addr  = RegInit(0.U(32.W))
    val r_data  = RegInit(0.U(32.W))    
    val r_req   = RegInit(true.B)       // fetch signal
    val r_rw    = RegInit(false.B)
    val r_ack   = RegInit(false.B)

    val w_req   = RegInit(true.B)
    val w_ack   = RegInit(false.B)
    val w_addr  = RegInit(0.U(32.W))
    val w_data  = RegInit(0.U(32.W)) 
    
    when (io.sw.halt === false.B){     
        when(r_ack === true.B){
            r_addr := r_addr + 4.U(32.W)    // increase program counter
        }
    }

// (中略)
    
    io.r_ach.addr   := r_addr
    io.r_ach.req    := r_req
    
    // write process
    io.w_ach.addr   := w_addr
    io.w_dch.data   := w_data
    io.w_ach.req    := w_req
    //io.w_dch.ack := w_ack
    
    // read process
    r_ack  := io.r_dch.ack
    r_data := io.r_dch.data 
    

}

メモリ側は割愛。必要であればこの記事最後にあるGithubからCloneしてください。

トップモジュールでCPUとmemoryをコネクトします。

class CpuBus extends Module {

    val cpu     = Module(new Cpu)
    val memory  = Module(new IMem)
    
  //(中略)

    // Read memory
    memory.io.r_ach.req     <> cpu.io.r_ach.req
    memory.io.r_ach.addr    <> cpu.io.r_ach.addr
    cpu.io.r_dch.data       <> memory.io.r_dch.data 
    cpu.io.r_dch.ack        <> memory.io.r_dch.ack

    // write memory
    memory.io.w_ach.req     <> cpu.io.w_ach.req
    memory.io.w_ach.addr    <> cpu.io.w_ach.addr
    memory.io.w_dch.data    <> cpu.io.w_dch.data
    cpu.io.w_dch.ack        <> memory.io.w_dch.ack

}

この後、テストオブジェクトからメモリへダミーデータを与え、CPUがフェッチできているか確認します。

// CPUを停止させ、メモリに書き込む
 poke(c.io.sw.halt, true.B)
// メモリへ書き込み            
for (addr <- 0 to (memarray.length * 4 - 1) by 4){    
	poke(c.io.sw.wAddr, addr)
	poke(c.io.sw.wData, memarray(addr/4))                
	step(1)     
}
step(1)
poke(c.io.sw.halt, false.B) // CPUを再開させる

for (lp <- 0 to (memarray.length - 1) by 1){
	val a = peek(c.io.sw.addr)
	val d = peek(c.io.sw.data)	
	println(f"addr =  0x$a%08X, data = 0x$d%08X") // メモリアドレス、データを16進表記
	step(1)
}

動作確認。make testで動作するようMakefileに記述してあります。


f:id:Lynx-EyED:20200420161633p:plain
一応動いてそう。。


ここまでのコード全文は以下で。コードは今後も漸進的に更新されるので、以下のように-bでタグを指定してcloneするのがいいと思います。

git clone http://github.com/panda5mt/KyogenRV -b 0.0.3 --depth 1 

sbtやjava導入後、上記Githubからcloneして,make testすると動作します。
きょうはここまで。

Cyclone10LP基板ができたのでLチカ

基板到着

新型コロナの影響なのか国内の基板製造にみなさん移行しているようで、いつもより時間がかかりました。


f:id:Lynx-EyED:20200314105024j:plain
2層基板、L/S=5milです。Raspberry Pi Zeroにスタックできる設計。
特に実装エラーもなかったようで安心。基板製造前の指示で認識マークを基板本体にも追加しています。
こんな感じでラズパイにスタックして使います。とてもコンパクト。

f:id:Lynx-EyED:20200314113714j:plain
基板検査がてらラズパイと一緒にいじりました。
ラズパイ経由でFPGASRAMを書き換え、デバッグバウンダリスキャンができます。HDLコンパイルは自宅のWindowsで行い、遠隔地に仕掛けたラズパイからFPGAをコンフィグし、結果を取得するといった完全リモートでFPGAを扱うことができます。

f:id:Lynx-EyED:20200314111852p:plain
この基板は遠隔でソフトコアCPUを実装する必要がある時に活用できるかもしれません。
ロスコンパイラとJTAGデバッガをラズパイにインストールしておき、

  • 低消費電力要求時:動作周波数を下げたソフトコアCPU、最低限のペリフェラル
  • 処理性能要求時:高速ソフトコアCPUと差動バス

などと状況に応じた機能を遠隔でコンフィグするなどの動作ができます。その後ラズパイ上のコンパイラ(もちろん遠隔からでも)からバイナリを流し込めます。

全日本Lチカ人権宣言

基板を作ってもLチカができないと人権を剥奪されるおそれがあるらしいようなのでLチカ人権宣言を行いました。
一番簡単なのはHDLでクロックを分周して下位のビットをLEDを接続した端子に出力する方法です。



めんどくさいので今回はやりません。ぼくFPGAとかHDLとかわかんないし。



Raspberry Pi ZeroからCyclone10LPへバウンダリスキャンレジスタからLED端子に1/0を書き込みます。
ラズパイにUrJTAGを使います。GPIOを使うビルドが必要になるので、以前のブログの方法でビルドするといいと思います。
Raspberry Pi 3でUrJTAGを使う - lynxeyedの電音鍵盤
インテル(アルテラ)のFPGAではJTAGモードではTDO/TDI/TCK/TMSの4端子を使います。それぞれがラズパイのGPIOに接続されている必要があります。
UrJTAGにラズパイのどの端子が、FPGAのTDO/TDI/TCK/TMSのどこに対応するか伝えます。
なおラズパイの40pinの端子番号ではなく、マイコン(BCM283x)のどのGPIOに対応しているか、で伝えます。
今回は

TDO=20, 
TDI=21, 
TCK=26,
TMS=19

に対応して接続しています。ラズパイのピン番号とBCM283xの対応が知りたい時は、以下のようにします。

sudo apt update
sudo apt install python3-gpiozero

$pinout

UrJTAGに認識させます。

sudo jtag

cable gpio tdo=20 tdi=21 tck=26 tms=19   
detect

おそらく、以下のメッセージになると思います。

IR length: 10
Chain length: 1
Device Id: 00000010000011110011000011011101 (0x020F30DD)
  Manufacturer: Altera (0x0DD)
  Unknown Part! (001000001111001) (/usr/local/share/urjtag/altera/PARTS)

バイスが不明です。BSDLファイルが必要になります。
インテルのページから取得できます。必要なのはCyclone10LP:10CL025U256のBSDLファイルです。
Intel Cyclone 10 LP Device BSDL Files
UrJTAGへBSDLファイルを導入する方法は多くのブログで取り上げられているので扱いません。
参考になるブログ

BSDLファイルを導入できると、先程のメッセージが変わると思います。

IR length: 10
Chain length: 1
Device Id: 00000010000011110011000011011101 (0x020F30DD)
  Manufacturer: Altera (0x0DD)
  Part(0):      10CL025U256 (0x20F3)
  Stepping:     0
  Filename:     /usr/local/share/urjtag/altera/10cl025u256/10cl025u256

さてLチカです。バウンダリスキャンレジスタにピンの状態を書き込んで点滅させます。
この基板ではLEDはCyclone10LPのIO84(PIN_L15)/IO85(PIN_L16)に接続されています。
先ほどのBSDLを見ると、

--I/O Pins
......(中略), 
       "IOL16   : L16  , IOL15     : L15  , IOK16     : K16  ,
..............

とありました。IOL15,IOL16の状態をバウンダリスキャンレジスタ経由で書き換えます。
以下のスクリプトhoge.txtという名前で保存します。

cable gpio tdo=20 tdi=21 tck=26 tms=19
detect

instruction EXTEST
shift ir
set signal IOL15 out 0
set signal IOL16 out 1
shift dr
usleep 500000
shift ir
set signal IOL15 out 1
set signal IOL16 out 0
shift dr
usleep 500000

shift ir
set signal IOL15 out 0
set signal IOL16 out 1
shift dr
usleep 500000
shift ir
set signal IOL15 out 1
set signal IOL16 out 0
shift dr
usleep 500000
shift ir
set signal IOL15 out 0
set signal IOL16 out 1
shift dr
usleep 500000
shift ir
set signal IOL15 out 1
set signal IOL16 out 0
shift dr
usleep 500000
shift ir
shift ir
set signal IOL15 out 1
set signal IOL16 out 1
shift dr

以下のコマンドで実行します。

sudo jtag hoge.txt

動作風景
youtu.be

次回

RISC-V界隈で完成度がすごく高く、フリーで商用利用OKとホットなSyntacore社のRISC-VコアSCR1をCyclone10LPに移植したので、紹介いたいます。
お楽しみに。