today   2019-03-31

access_time  33 mins

ChiselでBF処理系を作る(5)

プロセッサの実装と全体結線

前回はUARTの実装をした。本命のBF Processorを実装する。

graph LR PC -- USB --- USB_Uart USB_Uart --Tx--> UartTxRx UartTxRx--Rx--> USB_Uart UartTxRx -- program/stdin -->FIFO_din FIFO_din -- program --> BF_Processor FIFO_din -- stdin--> BF_Processor BF_Processor -- stdout --> FIFO_dout FIFO_dout -- stdout --> UartTxRx External_SW --program/run -->UntiChatter UntiChatter -->BF_Processor style BF_Processor fill:#f66,stroke:#f33

BFの実装

ChiselでBF処理系を作る(1)で解説したが、しばらく前なので再掲する。

brainfxxk.cpp - Gist

今回は高速化等については触れず、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())
}

制御フロー

個々の要素については順番に解説するが、全体として以下の状態遷移を行うように設計している。

graph TD halted--"io.program == 1"-->program program--"io.program==0"-->halted halted--"posedge io.run"-->run run--"inst == `\s`, `#`"-->halted run--"inst == `>`, `<`, `+`, `-`, `,`, `.`]"-->run run--"inst==`[` and !stackData"-->nest=1 nest=1-->branchJump branchJump--"inst == `[`"-->nest+=1 nest+=1-->branchJump branchJump--"inst == `]`"-->nest-=1 nest-=1--"nest > 0"-->branchJump nest-=1--"nest == 0"-->run branchJump--"other"-->branchJump

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かどうかで分岐する。

  • 0だった場合→branchJumpモードに以降
  • それ以外→現在のアドレスをメモして、プログラムカウンタを一つすすめる

]は、先程メモしたアドレスにプログラムカウンタを戻すだけである。

注意すべき点としてループがネストする場合にアドレスのメモが上書きされてしまうため、アドレスのメモには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分ほど要した。

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)
        }

      }
    }
  }
}

結果はバグが見つかり、修正後ちゃんと出力された。やはりテストは端折ってはいけない、だいたい端折ったところでバグが出る。

result

Verilogコード生成

最後に以下のコードでVerilog HDLファイルを生成した。

object DebugAccessPort extends App {
  chisel3.Driver.execute(Array("--target-dir", "bf"),()=>new DebugAccessPort())
}

結果はDebugAccessPortに関係したModuleすべてのVerilogが連結された1ファイルとして出力されている。

DebugAccessPort.v - Github

Vivadoでの作業

Topモジュール

最後にArty A7ボードにインプリメントするためにVivado 2018.3を使って作業を行った。

Artyのボード上には100MHzのTCXOが実装されていたが、(周波数を変更する兼ね合いもあるので)一旦MMCMに入力して合成した周波数をシステムに供給することとした。

これは明らかにChiselでやるのは冗長(BlackBox機能を使えばできるが)なので、直接Verilogで以下のように記述した。

top.v - Github

`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に適用できる形で編集した。

top.xdc - Github

ILA

万が一動かなかったときのデバッグなどに、ロジアナが使えると便利である。 VivadoにはWebPackライセンスであってもILAという、FPGA内部にロジアナを合成して波形観測する機能が利用できる。

これを使ってデバッグする環境を整えてた(そしてバグの特定に大いに貢献した)。 Vivadoの使い方自体は詳解しないが、簡単に説明するとSynthesized Designを開いた状態でSetup Debugを押す→観測したい波形とキャプチャ長さを選択→xdcファイルを出力。といった手順だ。

参考までに最後動作したときのDebug向けに吐き出したxdcファイルを置いておく。

generated.xdc - Github

ピンクのマーカーが観測するための配線の両端である。右上に見えるのがILAである。

debug

完成

これで合成したビットストリームをFPGAに書き込んで動作することを確認できた。前述したFIFOのバグはあったが、UartTxRxやBF Processorがscalaシミュレーションどおりに一発で動作したことは本当にすごいという感想だった。

設計の反省点

今回完成した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案件に使いたいかと言われると、間違いなく使いたいと言うと思う。