today 2019-03-31
access_time 33 mins
プロセッサの実装と全体結線
前回はUARTの実装をした。本命のBF Processorを実装する。
ChiselでBF処理系を作る(1)で解説したが、しばらく前なので再掲する。
今回は高速化等については触れず、C++で記述したこの処理をそのままハードウェアに落とし込む形で実装する。 scalaやchiselに関して目新しい要素はないので、それぞれのコードブロックごとに簡単に解説する。
高速化するのであれば、例えばbranch([
, ]
)のジャンプテーブルの事前作成や同一命令の一括実行、パイプライン化など様々な余地があると思う。プロセッサ設計にはあまり詳しくないのでなんとも言えないところではある。
ともあれ、今回完成したコードは以下の通り。
BrainfuckProcessor.scala - Github
package bf
import chisel3._
import chisel3.util._
import scala.math.pow
class BrainfuckProcessor(instMemWidth: Int = 14, stackMemWidth: Int = 10, branchStackMemWidth: Int = 4) extends Module {
val io = IO(new Bundle {
val run = Input(Bool())
val program = Input(Bool())
val programData = Input(UInt(8.W))
val programValid = Input(Bool())
val programReady = Output(Bool())
val programAck = Output(Bool())
val pc = Output(UInt(instMemWidth.W))
val inst = Output(UInt(8.W))
val stackPtr = Output(UInt(stackMemWidth.W))
val stackData = Output(UInt(8.W))
val branchStackPtr = Output(UInt(branchStackMemWidth.W))
val branchStackData = Output(UInt(instMemWidth.W))
val halted = Output(Bool())
val errorCode = Output(UInt(3.W))
val stdinData = Input(UInt(8.W))
val stdinValid = Input(Bool())
val stdinReady = Output(Bool())
val stdinAck = Output(Bool())
val stdoutData = Output(UInt(8.W))
val stdoutValid = Output(Bool())
val stdoutReady = Input(Bool())
val stdoutAck = Input(Bool())
})
val programReady = RegInit(Bool(), false.B)
io.programReady := programReady
val programAck = RegInit(Bool(), false.B)
io.programAck := programAck
val halted = RegInit(Bool(), true.B)
io.halted := halted
val errorCode = RegInit(UInt(4.W), 0.U)
io.errorCode := errorCode
val stdinReady = RegInit(Bool(), true.B)
io.stdinReady := stdinReady
val stdinAck = RegInit(Bool(), true.B)
io.stdinAck := stdinAck
val stdoutData = RegInit(UInt(8.W), false.B)
io.stdoutData := stdoutData
val stdoutValid = RegInit(Bool(), false.B)
io.stdoutValid := stdoutValid
val instMemSize: Int = pow(2, instMemWidth).asInstanceOf[Int]
val instMem = Mem(instMemSize, UInt(8.W))
val programAddr = RegInit(UInt(instMemWidth.W), 0.U)
val stackMemSize: Int = pow(2, stackMemWidth).asInstanceOf[Int]
val stackMem = Mem(stackMemSize, UInt(8.W))
val stackPtr = RegInit(UInt(stackMemWidth.W), 0.U)
val stackData = RegInit(UInt(8.W), 0.U)
io.stackPtr := stackPtr
io.stackData := stackData
val branchStackMemSize: Int = pow(2, branchStackMemWidth).asInstanceOf[Int]
val branchStackMem = Mem(branchStackMemSize, UInt(instMemWidth.W))
val branchStackPtr = RegInit(UInt(branchStackMemWidth.W), 0.U)
val branchStackData = RegInit(UInt(instMemWidth.W), 0.U)
io.branchStackPtr := branchStackPtr
io.branchStackData := branchStackData
when(stdinAck) {
stdinAck := (false.B)
}
when(stdoutValid) {
when(io.stdoutAck) {
stdoutData := (0.U)
stdoutValid := (false.B)
}
}
val branchJump = RegInit(Bool(), false.B)
val branchJumpNest = RegInit(UInt(branchStackMemWidth.W), 0.U)
val inst = RegInit(UInt(8.W), 0.U)
val pc = RegInit(UInt(instMemWidth.W), 0.U)
io.inst := inst
io.pc := pc
when(!halted) {
printf(p"[process] branchJump:$branchJump pc:$pc inst:${Character(inst)} ($inst) stackPtr:$stackPtr stackData:$stackData\n")
when(branchJump) {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
switch(inst) {
is(0.U, '#'.U) {
halted := true.B
errorCode := 1.U
}
is('['.U) {
when(branchJumpNest === (branchStackMemSize - 1).U) {
halted := true.B
errorCode := 2.U
} .otherwise {
branchJumpNest := (branchJumpNest + 1.U)
}
}
is(']'.U) {
when(branchJumpNest > 0.U) {
branchJumpNest := (branchJumpNest - 1.U)
} .otherwise {
branchJump := (false.B)
branchJumpNest := (0.U)
}
}
}
} .otherwise {
switch(inst) {
is(0.U, '#'.U) {
halted := true.B
errorCode := 0.U
}
is('>'.U) {
when (stackPtr === (stackMemSize - 1).U) {
halted := true.B
errorCode := 3.U
} .otherwise {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackPtr := (stackPtr + 1.U)
stackData := stackMem.read(stackPtr + 1.U)
}
}
is('<'.U) {
when (stackPtr === 0.U) {
halted := true.B
errorCode := 4.U
} .otherwise {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackPtr := (stackPtr - 1.U)
stackData := stackMem.read(stackPtr - 1.U)
}
}
is('+'.U) {
stackMem.write(stackPtr, stackData + 1.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (stackData + 1.U)
}
is('-'.U) {
stackMem.write(stackPtr, stackData - 1.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (stackData - 1.U)
}
is('.'.U) {
when(io.stdoutReady && !stdoutValid) {
printf(p"[stdout] ${Character(stackData)} $stackData\n")
stdoutData := (stackData)
stdoutValid := (true.B)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
}
}
is(','.U) {
when(io.stdinValid && !stdinAck) {
stdinAck := (true.B)
stackMem.write(stackPtr, io.stdinData)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (io.stdinData)
}
}
is('['.U) {
when(stackData === 0.U) {
branchJump := (true.B)
branchJumpNest := (0.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
} .otherwise {
branchStackMem.write(branchStackPtr, pc)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
branchStackPtr := (branchStackPtr + 1.U)
branchStackData := (pc)
}
}
is(']'.U) {
pc := (branchStackData)
inst := ('['.U)
when(branchStackPtr > 1.U) {
branchStackPtr := (branchStackPtr - 1.U)
branchStackData := branchStackMem.read(branchStackPtr - 2.U)
}.otherwise {
branchStackPtr := (0.U)
branchStackData := (0.U)
}
}
is('\r'.U, '\n'.U, ' '.U, 'X'.U, 'x'.U) {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
}
}
}
} .otherwise {
branchJump := (false.B)
branchJumpNest := (0.U)
pc := (0.U)
inst := instMem.read(0.U)
stackPtr := (0.U)
stackData := stackMem.read(0.U)
branchStackPtr := (0.U)
branchStackData := (0.U)
}
val run = RegInit(Bool(), false.B)
val run2 = RegInit(Bool(), false.B)
run := (io.run)
run2 := (run)
when(halted) {
when((!run2 && run) && !io.program) {
printf(p"[run] Trigger!\n")
errorCode := 0.U
halted := (false.B)
}
}
val programDelay = RegInit(Bool(), false.B)
when(!io.program || !halted) {
programAddr := 0.U
programReady := false.B
programDelay := false.B
} .otherwise {
programReady := true.B
when(programDelay) {
programAck := false.B
programDelay := false.B
} .elsewhen(io.programValid) {
printf(p"[program] Write programAddr:$programAddr data:${Character(io.programData)} (${io.programData})\n")
programAck := (true.B)
programDelay := true.B
instMem.write(programAddr, io.programData)
programAddr := programAddr + 1.U
stdoutData := io.programData
stdoutValid := true.B
}
}
}
object BrainfuckProcessor extends App {
chisel3.Driver.execute(args,()=>new BrainfuckProcessor())
}
個々の要素については順番に解説するが、全体として以下の状態遷移を行うように設計している。
runモードに移行するためには、外部ポートのrunに立ち上がりエッジを入力する。><+-,.
の命令群に関しては1cycで実行できるようになっている。
少し特殊なのが[]
の命令でループを抜ける場合である。inst==[ and !stackData
と記載したが、ループを抜ける際は対応する]
にプログラムカウンタを進める必要があるため、branchJump
モードに移行する。
このモードでは順番に命令を舐めていき、対応するカッコが見つかった場合にrun
モードに制御を戻す設計とした。
ただし、カッコについてはネストする可能性があるのでnest
の変数を持って対応するカッコの判断を行う。
今回作成したBF Processorをまっさきに高速化するのであればここのジャンプテーブルを事前に作成しておくことが必要だと考えている。
各要素について以下に解説する。
halted
の状態でio.program
がアサートされた場合にプログラム書き込みモードに移行する。
FIFOから受信したデータを順番にinstMem
に書き込んでいる。アドレスのオートインクリメントはprogramモードではない時点で初期化されるので、programスイッチを有効にした後にデータ転送を行えば必ず0番地から書かれるようになっている。
val programDelay = RegInit(Bool(), false.B)
when(!io.program || !halted) {
programAddr := 0.U
programReady := false.B
programDelay := false.B
} .otherwise {
programReady := true.B
when(programDelay) {
programAck := false.B
programDelay := false.B
} .elsewhen(io.programValid) {
printf(p"[program] Write programAddr:$programAddr data:${Character(io.programData)} (${io.programData})\n")
programAck := (true.B)
programDelay := true.B
instMem.write(programAddr, io.programData)
programAddr := programAddr + 1.U
stdoutData := io.programData
stdoutValid := true.B
}
}
run入力の立ち上がりエッジを検出している。検出した場合、halted
をfalseにセットして実行開始ステートに遷移している。
val run = RegInit(Bool(), false.B)
val run2 = RegInit(Bool(), false.B)
run := (io.run)
run2 := (run)
when(halted) {
when((!run2 && run) && !io.program) {
printf(p"[run] Trigger!\n")
errorCode := 0.U
halted := (false.B)
}
}
現在のアドレスを一つ進める(もしくは一つ戻す。その際に次のアドレスにあるデータをstackData
に予め読み込んでおく。
また、アドレスのオーバーフローとアンダーフロー時には動作を停止させるようなコードも追加。1cycで動作可能。
is('>'.U) {
when (stackPtr === (stackMemSize - 1).U) {
halted := true.B
errorCode := 3.U
} .otherwise {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackPtr := (stackPtr + 1.U)
stackData := stackMem.read(stackPtr + 1.U)
}
}
is('<'.U) {
when (stackPtr === 0.U) {
halted := true.B
errorCode := 4.U
} .otherwise {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackPtr := (stackPtr - 1.U)
stackData := stackMem.read(stackPtr - 1.U)
}
}
現在のアドレスにあるデータに1を足した(もしくは1を引いた)値を現在のstackMem[stackPtr]
に書き込む。特に特別な操作はなく、1cycで動作可能。
is('+'.U) {
stackMem.write(stackPtr, stackData + 1.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (stackData + 1.U)
}
is('-'.U) {
stackMem.write(stackPtr, stackData - 1.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (stackData - 1.U)
}
現在のstackData
の値を、出力FIFOに流しているだけである。命令実行の処理を止めないために、Ackの検知とValidの取り下げはプログラムをデコードしている回路ブロックとは別の場所に記述してある。
これも出力先のFIFOがReadyでありつづけるならば、1cycで処理できる。そうでない場合はReadyがアサートされるまで待機。
when(stdoutValid) {
when(io.stdoutAck) {
stdoutData := (0.U)
stdoutValid := (false.B)
}
}
// 中略
is('.'.U) {
when(io.stdoutReady && !stdoutValid) {
printf(p"[stdout] ${Character(stackData)} $stackData\n")
stdoutData := (stackData)
stdoutValid := (true.B)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
}
}
FIFOから受信したデータを現在のstackMem[stackPtr]
に書き込む。すでにデータが来ているのであれば1cycで動作する。
データを受け取った場合、今回のオレオレFIFOインターフェースではAckを返すことになっていたのでstdinAck
も立てている。上げ続けているとFIFOのデータがどんどん流れていしまうので、次のサイクル時にAckを取り下げている。
when(stdinAck) {
stdinAck := (false.B)
}
// 中略
is(','.U) {
when(io.stdinValid && !stdinAck) {
stdinAck := (true.B)
stackMem.write(stackPtr, io.stdinData)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
stackData := (io.stdinData)
}
}
まず、[
については、現在のstackData
が0かどうかで分岐する。
]
は、先程メモしたアドレスにプログラムカウンタを戻すだけである。
注意すべき点としてループがネストする場合にアドレスのメモが上書きされてしまうため、アドレスのメモにはStack構造を用いる。
is('['.U) {
when(stackData === 0.U) {
branchJump := (true.B)
branchJumpNest := (0.U)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
} .otherwise {
branchStackMem.write(branchStackPtr, pc)
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
branchStackPtr := (branchStackPtr + 1.U)
branchStackData := (pc)
}
}
is(']'.U) {
pc := (branchStackData)
inst := ('['.U)
when(branchStackPtr > 1.U) {
branchStackPtr := (branchStackPtr - 1.U)
branchStackData := branchStackMem.read(branchStackPtr - 2.U)
}.otherwise {
branchStackPtr := (0.U)
branchStackData := (0.U)
}
}
branchJumpモードの動作は以下の通りである。最初にお見せしたフローの通り、多重ループの検出のために[
, ]
の有無でbranchJumpNest
の値を増減させている。もし0になったらbranchJumpモードを抜け、]
のあとにある命令を実行する。
when(!halted) {
printf(p"[process] branchJump:$branchJump pc:$pc inst:${Character(inst)} ($inst) stackPtr:$stackPtr stackData:$stackData\n")
when(branchJump) {
pc := (pc + 1.U)
inst := instMem.read(pc + 1.U)
switch(inst) {
is(0.U, '#'.U) {
halted := true.B
errorCode := 1.U
}
is('['.U) {
when(branchJumpNest === (branchStackMemSize - 1).U) {
halted := true.B
errorCode := 2.U
} .otherwise {
branchJumpNest := (branchJumpNest + 1.U)
}
}
is(']'.U) {
when(branchJumpNest > 0.U) {
branchJumpNest := (branchJumpNest - 1.U)
} .otherwise {
branchJump := (false.B)
branchJumpNest := (0.U)
}
}
}
}
}
ともあれこれで一通りのモジュールが完成したので、頭の図にあるようにすべてのモジュールを接続する。
DebugAccessPort.scala - Github
package bf
import chisel3._
class DebugAccessPort(
instMemWidth: Int = 14, stackMemWidth: Int = 10, branchStackMemWidth: Int = 4,
freq: Double = 50e6, baud: Double = 115200,
depthWidth: Int = 4,
captureFreq: Double = 100, averageWidth: Int = 8, isPositive: Boolean = true
) extends Module {
val io = IO(new Bundle{
val uartTx = Output(Bool())
val uartRx = Input(Bool())
val switches = Input(Vec(4, Bool()))
val leds = Output(Vec(4, Bool()))
val triLed0 = Output(UInt(3.W))
val triLed1 = Output(UInt(3.W))
val triLed2 = Output(UInt(3.W))
val triLed3 = Output(UInt(3.W))
})
val uart = Module(new UartTxRx(freq, baud))
val bf = Module(new BrainfuckProcessor(instMemWidth, stackMemWidth, branchStackMemWidth))
val chatterProgram = Module(new UntiChatter(freq, captureFreq, averageWidth, isPositive))
val program = Wire(Bool())
io.switches(0) <> chatterProgram.io.din
program <> chatterProgram.io.dout
bf.io.program <> program
val chatterRun = Module(new UntiChatter(freq, captureFreq, averageWidth, isPositive))
val run = Wire(Bool())
io.switches(1) <> chatterRun.io.din
run <> chatterRun.io.dout
bf.io.run <> run
val statusInst = RegInit(UInt(8.W), 0.U)
statusInst := bf.io.inst
io.leds(0) <> program
io.leds(1) <> run
io.leds(2) <> bf.io.stdoutValid
io.leds(3) <> bf.io.halted
io.triLed0 <> statusInst(2,0)
io.triLed1 <> statusInst(5,3)
io.triLed2 <> statusInst(7,6)
io.triLed3 <> bf.io.errorCode
uart.io.tx <> io.uartTx
uart.io.rx <> io.uartRx
val fifoUartToBf = Module(new Fifo(8, depthWidth))
uart.io.rxData <> fifoUartToBf.io.inData
uart.io.rxReady <> fifoUartToBf.io.inReady
uart.io.rxValid <> fifoUartToBf.io.inValid
uart.io.rxAck <> fifoUartToBf.io.inAck
bf.io.programData <> fifoUartToBf.io.outData
bf.io.stdinData <> fifoUartToBf.io.outData
bf.io.programValid := Mux(program, fifoUartToBf.io.outValid, false.B)
bf.io.stdinValid := Mux(!program, fifoUartToBf.io.outValid, false.B)
fifoUartToBf.io.outReady := Mux(program, bf.io.programReady, bf.io.stdinReady)
fifoUartToBf.io.outAck := Mux(program, bf.io.programAck, bf.io.stdinAck)
val fifoBfToUart = Module(new Fifo(8, depthWidth))
uart.io.txData <> fifoBfToUart.io.outData
uart.io.txReady <> fifoBfToUart.io.outReady
uart.io.txValid <> fifoBfToUart.io.outValid
uart.io.txAck <> fifoBfToUart.io.outAck
bf.io.stdoutData <> fifoBfToUart.io.inData
bf.io.stdoutReady <> fifoBfToUart.io.inReady
bf.io.stdoutValid <> fifoBfToUart.io.inValid
bf.io.stdoutAck <> fifoBfToUart.io.inAck
}
<>
演算子は双方の信号のバルク接続を示している。他のコードはそのまま見てもらえればわかるがModule(new MyModule())
でインスタンシェートしてそれぞれ結線している。
またstdinとprogramDataのFIFOはio.program
によってどちらの信号を使うか選択式にしている。
実は最初は全体を結線したテストを行っていなかった。ところが以下ツイートにあるようにprogramDataの転送が3重に発生するバグが有ったため、シミュレーションによるものなのか論理合成やその後の処理が原因なのか切り分けるために簡単な全体テストを記述し実行した。Hello world!を出すのにMacBook Airで15分ほど要した。
あ~(受信したデータを二回書いている音がする pic.twitter.com/Il1SrHiiEo
— kamiya (@kamiya_owl) 2019年3月25日
DebugAccessPortSpec.scala - Github
package bf
import chisel3._
import chisel3.util._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
class DebugAccessPortSpec extends ChiselFlatSpec {
def calcDuration(freq: Double, baud: Double): Int = {
(freq / baud).toInt
}
"DAP" should "Hello world!" in {
val src = ">+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[-]>++++++++[<++++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.+++.------.--------.[-]>++++++++[<++++>-]<+.[-]++++++++++."
val expect = "Hello world!"
val freq = 50e6
val baud = 115200
val duration = calcDuration(freq, baud)
val captureFreq = 25e6
val incCount = (freq / captureFreq).toInt
val average = 2
val swDelay = (incCount * average * 1.1).toInt
val result = Driver(() => new DebugAccessPort(freq = freq, baud = baud, captureFreq = captureFreq, averageWidth = average)) {
c => new PeekPokeTester(c) {
println(s"[Initialize]")
poke(c.io.uartRx, true.B)
poke(c.io.switches(0), false.B)
poke(c.io.switches(1), false.B)
step(swDelay)
println(s"[Start Program]")
poke(c.io.switches(0), true.B)
step(swDelay)
for(d <- src) {
println(s"[Write Program] Data:$d")
val sendData = Seq(
false,
(d & 0x01) != 0x00,
(d & 0x02) != 0x00,
(d & 0x04) != 0x00,
(d & 0x08) != 0x00,
(d & 0x10) != 0x00,
(d & 0x20) != 0x00,
(d & 0x40) != 0x00,
(d & 0x80) != 0x00,
true,
)
for(s <- sendData) {
poke(c.io.uartRx, s.B)
step(duration)
}
}
poke(c.io.switches(0), false.B)
step(swDelay)
println(s"[Run]")
poke(c.io.switches(1), true.B)
step(1000)
while(peek(c.io.leds(3)) == BigInt(0)) {
step(1)
}
}
}
}
}
結果はバグが見つかり、修正後ちゃんと出力された。やはりテストは端折ってはいけない、だいたい端折ったところでバグが出る。
最後に以下のコードでVerilog HDLファイルを生成した。
object DebugAccessPort extends App {
chisel3.Driver.execute(Array("--target-dir", "bf"),()=>new DebugAccessPort())
}
結果はDebugAccessPortに関係したModuleすべてのVerilogが連結された1ファイルとして出力されている。
最後にArty A7ボードにインプリメントするためにVivado 2018.3を使って作業を行った。
Artyのボード上には100MHzのTCXOが実装されていたが、(周波数を変更する兼ね合いもあるので)一旦MMCMに入力して合成した周波数をシステムに供給することとした。
これは明らかにChiselでやるのは冗長(BlackBox機能を使えばできるが)なので、直接Verilogで以下のように記述した。
`timescale 1ns / 1ps
module top(
input clock_in,
output io_uartTx,
input io_uartRx,
input io_switches_0,
input io_switches_1,
input io_switches_2,
input io_switches_3,
output io_leds_0,
output io_leds_1,
output io_leds_2,
output io_leds_3,
output [2:0] io_triLed0,
output [2:0] io_triLed1,
output [2:0] io_triLed2,
output [2:0] io_triLed3
);
wire sys_clk;
wire locked;
clk_wiz_0 clk0(
.clk_out1(sys_clk), // 50MHz
.locked(locked),
.clk_in1(clock_in) // 100MHz single-ended pin
);
reg reset;
always @ (posedge sys_clk) begin
if((io_switches_3 == 1'b1) || !locked) begin
reset <= 1'b1;
end else begin
reset <= 1'b0;
end
end
DebugAccessPort dap(
.clock(sys_clk),
.reset(reset),
.io_uartTx(io_uartTx),
.io_uartRx(io_uartRx),
.io_switches_0(io_switches_0),
.io_switches_1(io_switches_1),
.io_switches_2(io_switches_2),
.io_switches_3(io_switches_3),
.io_leds_0(io_leds_0),
.io_leds_1(io_leds_1),
.io_leds_2(io_leds_2),
.io_leds_3(io_leds_3),
.io_triLed0(io_triLed0),
.io_triLed1(io_triLed1),
.io_triLed2(io_triLed2),
.io_triLed3(io_triLed3)
);
endmodule
ピンアサインとクロック指定などはDigilent社が配布しているxdcファイルを、top.vに適用できる形で編集した。
万が一動かなかったときのデバッグなどに、ロジアナが使えると便利である。 VivadoにはWebPackライセンスであってもILAという、FPGA内部にロジアナを合成して波形観測する機能が利用できる。
これを使ってデバッグする環境を整えてた(そしてバグの特定に大いに貢献した)。 Vivadoの使い方自体は詳解しないが、簡単に説明するとSynthesized Designを開いた状態でSetup Debugを押す→観測したい波形とキャプチャ長さを選択→xdcファイルを出力。といった手順だ。
参考までに最後動作したときのDebug向けに吐き出したxdcファイルを置いておく。
ピンクのマーカーが観測するための配線の両端である。右上に見えるのがILAである。
これで合成したビットストリームをFPGAに書き込んで動作することを確認できた。前述したFIFOのバグはあったが、UartTxRxやBF Processorがscalaシミュレーションどおりに一発で動作したことは本当にすごいという感想だった。
やったー!scalaでhdlを記述できるchiselを使って、brainfuck言語処理系をFPGAにオフロードできた!
— kamiya (@kamiya_owl) 2019年3月26日
動画ではSW0でUART経由でプログラム転送、SW1で実行しています。
ボードはArty A7を使っていて、合成レポートを見ると最大139MHzで動かせそうです。たのしいhttps://t.co/tWLOakh06r pic.twitter.com/VcENSczvN8
今回完成したBF Processorを見てみると、まだVerilog HDLをScalaで書き下したに過ぎない程度の物となっている。 以下のページに有るような、Arbiter, Counter, Decoupled, BitPatなどを見るともっと手続きをTraitやModuleクラスの継承、Bundleを活用して抽象化している。
また、モジュールの設定もConfigをうまく活用して切り替えることも可能であるようだった。
最初のVerilog HDLからの移行お題としては適切であったが、Chiselならではの記述についても理解を深めていく必要がある。
freechipsproject/chisel3 chisel3.util - Github
これでchiselでBF処理系を設計する内容は終了した。反省点にもある箇所はあるが、ともあれchiselが全くわからない状態から多少は記述できるようになっただけでも大きな進歩だと思う。
scalaとhdlどちらに対してもある程度の理解が必要なことなど学習コストの高さが目立つが、かなり実用性のあるaltHDLとして機能している、と私は率直に感じている。
今後のFPGA案件に使いたいかと言われると、間違いなく使いたいと言うと思う。