Finally run the PWM LED on TinyFPGA BX.
前回までの続きです。いつまで立っても犯人の挙がらない刑事ドラマみたいになってきたので、今回は意地でも FPGA ボード上で LED をホワホワさせましょう。
前回は、PwmLed クラスを実装し、シミュレータ(Verilator + GTKwave)で回路の波形出力を確認しました。ところが、PwmLed のような論理回路を実際に FPGA 上で動かすにはそれだけではダメで、他にトップレベル(top level)と呼ばれる回路(ラッパー)を作るのが一般的です。今回はトップレベルの設計をしてみましょう。トップレベルだけ Verilog で書くことも可能ですが、前回に引き続き SpinalHDL で完成させていこうと思います。
前回までの流れ
- 初めての SpinalHDL: デジタル PWM を SpinalHDL で書いてみよう
- Verilog 生成とシミュレーション: 記述した回路をシミュレーションしてみよう
- ソフト屋がハマるところ: if 文と when オブジェクトの違いは?
トップレベルの設計
まず最初に、イメージ的なトップレベルクラスを示します。名前は PwmLed_TinyFPGA_BX としましょう。(これは、このままでは動きません。後で、クロックドメインを定義します。)
class PwmLed_TinyFPGA_BX extends Component {
val io = new Bundle {
val CLK = in Bool
val LED = out Bool
val USBPU = out Bool
}
io.USBPU := False
val pwmled = new PwmLed(divSize = 12, pwmSize = 8)
pwmled.io.output <> io.LED
}
TinyFPGA BX で PwmLed クラスを動作させる時に考えなくてはいけないのは、以下の点です。(USBPU については、こちらを参考にしました。)
- 外部の 16MHz クロック(CLK)を PwmLed クラスのクロック信号に繋ぐ
- PwmLed クラスの出力 io.output を TinyFPGA BX の LED ピンに繋ぐ
- PwmLed を適切にリセットできるようにする
- TinyFPGA BX の USBPU(USB インターフェイスのプルアップ信号)信号 を low(False)にする
クロック信号を繋ぐ
前回までに設計した PwmLed クラスを Verilog に変換した結果を見てみると、こんなふうになっています。
module PwmLed (
output io_output,
input clk,
input reset);
略
endmodule
つまり、PwmLed モジュールの clk にトップレベルの CLK に繋いであげる必要があります。しかし、次のように書こうとしても、PwmLed モジュールのクロック信号名は io.clk ではなく clk なので、うまく記述できません。
pwmled.io.clk <> io.CLK
代わりに pwmled.clk と書くと、そんな信号名は知らないと叱られます。どうしたら良いのでしょう?
結論を言いますと、SpinalHDL の設計ではトップレベルの設計(記述)時に、クロックドメイン(ClockDomain)とクロッキングエリア(ClockingArea)というものを定義しないといけないようです。今回の TinyFPGA BX で動作させるには、先ほどのトップレベルを次のように変更します。
class PwmLed_TinyFPGA_BX extends Component {
val io = new Bundle {
val CLK = in Bool
val LED = out Bool
val USBPU = out Bool
}
val coreClockDomain = ClockDomain(
clock = io.CLK,
frequency = FixedFrequency(16 MHz),
config = ClockDomainConfig(
resetKind = BOOT
)
)
val coreArea = new ClockingArea(coreClockDomain) {
val pwmled = new PwmLed(...)
}
}
coreClockDomain というのがクロックドメインの定義で、coreArea というのがクロッキングエリアの定義です。まず前者から見てみます。
クロックドメインの定義
SpinalHDL のクロックドメインの詳細については、こちらが詳しいです。
クロックドメインの名前は(既存定義と重複しなければ)なんでも良いのですが、ここでは coreClockDomain としましょう。
まず最初に、「clock =」という記述で、そのドメインに入力するクロック信号を指定します。次に frequency です。この記述はオプションですが、定義しておくと、SpinalHDL による設計コード中でこの値を参照できるそうです。FixedFrequency() というのはイディオムで、そういうもんだと覚えます。 🙂
重要なのが config 中で記述している resetKind です。これは、このクロックドメインに属する回路のリセットをどのように扱うか、というものです。利用できる種類としては、ASYNC、SYNC、BOOT があり、それぞれ
- ASYNC: 非同期リセット
- SYNC: 同期リセット
- BOOT: FPGA 用の特殊なリセット
となります。それぞれを試して Verilog コードを覗くのが一番勉強になると思いますが、ASYNC と SYNC の場合は、クロックドメインの定義の中で reset という記述を書かないといけない点に注意してください。たとえば、FPGA ボードに RST というピンがあり、非同期リセット入力としたいならば、トップレベルの io Bundle にval RST = in Bool と記述し、
val coreClockDomain = ClockDomain(
clock = io.CLK,
frequency = FixedFrequency(16 MHz),
reset = io.RST,
config = ClockDomainConfig(
resetKind = ASYNC
)
)
のように記述します。なお、デフォルトではリセット信号の極性は active high になります。active low にしたい場合は、resetKind の記述に加えて「, resetActiveLevel = LOW」と記述します。
一般に FPGA では、ビットストリーム(回路構成用のためのデータで、Flash ROM などに格納される)のダウンロード時に内部状態(フリップフロップやメモリ)を初期化する機能があるので、resetKind に BOOT を選ぶこともできます。それが、一つ前に示したトップレベルのクロックドメインの定義です。
クロッキングエリアの定義
次にクロッキングエリアについてです。上記のトップレベルでは、coreArea という名前で定義しました。
ClockingArea クラスを使う(インスタンス化する)には、カッコの中に定義済みのクロックドメインを指定します。ここでは coreClockDomain を指定しました。そして、そのエリア定義の中に、そのクロックドメインに属する回路を記述します。上記トップレベルでは、エリア coreArea の中に val pwmled = new PwmLed(…) と記述し、PwmLed クラスをインスタンス化しています。なお、PwmLed クラスに含まれる(PwmLed 内でインスタンス化される)pwm インスタンスも、自動的に同じクロッキングエリアに属することになる点に注意してください。
ちなみに、クロックドメインとクロッキングエリアが独立した概念になっているのは、あるクロックドメインを複数のクロッキングエリアから利用できるようにするためと思います。
その他の信号を繋ぐ
最後に、LED と USBPU 信号を繋ぎましょう。冗長ですが、最終的なトップレベル設計、それを Verilog に変換するための main() 関数、コンストレイントファイル(PCF)をまとめて列挙しましょう。
トップレベル(PwmLed_TinyFPGA_BX)と main() 関数
class PwmLed_TinyFPGA_BX extends Component {
val io = new Bundle {
val CLK = in Bool
val LED = out Bool
val USBPU = out Bool
}
val coreClockDomain = ClockDomain(
clock = io.CLK,
frequency = FixedFrequency(16 MHz),
config = ClockDomainConfig(
resetKind = BOOT
)
)
val coreArea = new ClockingArea(coreClockDomain) {
io.USBPU := False
val pwmled = new PwmLed(divSize = 12, pwmSize = 8)
pwmled.io.output <> io.LED
}
}
object PwmLed_TinyFPGA_BX {
def main(args: Array[String]): Unit = {
SpinalVerilog(new PwmLed_TinyFPGA_BX)
}
}
コンストレイントファイル(PCF)
PCF ファイルは、こちらを参考にしています。
# LED
set_io --warn-no-port io_LED B3
# USB
set_io --warn-no-port USBP B4
set_io --warn-no-port USBN A4
set_io --warn-no-port io_USBPU A3
# 16MHz clock
set_io --warn-no-port io_CLK B2 # input
それでは実際に TinyFPGA BX のフラッシュメモリに書き込んでみましょう。
TinyFPGA BX へのフラッシュ書込
SpinalHDL による設計ファイルからフラッシュ書込までには、いくつかのツールが必要です。まとめますと、次の通りです。
- SBT(Scala 言語のためのビルドツール)
- Yosys(論理合成ツール)
- IceStorm
- nextpnr(placing & routing ツール)
- tinyprog
これらを個別に呼び出すのは大変ですので、makefile を書いてみました。こんな感じです。
TOPNAME=pwmled_tinyfpga_bx
CLASSNAME=PwmLed_TinyFPGA_BX
PCF=tinyfpga_bx.pcf
ICEVIEW=$(HOME)/どこか/ice40_viewer/iceview_html.py
$(TOPNAME).asc: $(TOPNAME).json $(PCF)
nextpnr-ice40 \
--lp8k \
--package cm81 \
--asc $@ \
--pcf $(PCF) \
--json $(TOPNAME).json
$(TOPNAME).json: $(CLASSNAME).v
yosys \
-ql yosys.log \
-p 'synth_ice40 -top '$(CLASSNAME)' -json '$@ \
$^
$(CLASSNAME).v: src/main/scala/PwmLed.scala
sbt "runMain $(CLASSNAME)"
$(TOPNAME).html: $(TOPNAME).asc
$(ICEVIEW) $(TOPNAME).asc $@
html: $(TOPNAME).html
$(TOPNAME).bin: $(TOPNAME).asc
icepack $(TOPNAME).asc $@
upload: $(TOPNAME).bin
tinyprog -p $^
clean:
@rm -f $(TOPNAME).asc $(TOPNAME).json $(CLASSNAME).v
なお、これまでに TinyFPGA BX を USB で接続し、tinyprog でプログラムを書き込んだことがあることを前提とします。そうでない方は、こちらも参考にしてください。
make コマンドで「make upload」とすると、フラッシュメモリに書き込まれるはずです。divSize を小さくしてあるので、ブートローダーよりも高速に LED がホワホワしているのがお分かりになる(?)はずです。もっと遅くしたい場合は、12 に代えて 17 くらいにしてみると違いがよく分かると思います。15前後だと、ブートローダーとの違いが分かりにくいです。
ようやく FPGA で動きましたね。最初にお約束したように、全てのコードを GitHub に上げておきました。
今日はここまで! おつかれさまでした。