Continue sleepy LED PWM project by SpinalHDL.
前回の続きです。前回までで PWM の基本ロジックが完成したので、今回は LED ホワホワルーチン、じゃなかった、ホワホワロジックを設計していきましょう。
前回までの流れ
前々回に設計したブロック図を再掲します。
PwmLed クラスの設計
今回は、上記「右上の PWM」以外の部分を設計します。最初に、私が試行錯誤して書いたコードを示します。なお、ブロック図に忠実でない部分がいくつかありますが、御容赦ください。
import spinal.lib.fsm._
class PwmLed(divSize: Int, pwmSize: Int) extends Component {
val io = new Bundle {
val output = out Bool
}
val ctDivider = Reg(UInt(divSize bits)) init(0)
val transFsm = Bool
ctDivider := ctDivider + 1
transFsm := ctDivider === U(ctDivider.range -> true) // eg. 'b11111111
val fsm = new StateMachine {
val countingUp = new State with EntryPoint
val countingDown = new State
val curWidth = Reg(UInt(pwmSize bits)) init(0) // current PWM width
countingUp
.whenIsActive {
when(transFsm) {
curWidth := curWidth + 1
when(curWidth === U(curWidth.range -> true) - 1) { // eg. 'b11111110
goto(countingDown)
}
}
}
countingDown
.whenIsActive {
when(transFsm) {
curWidth := curWidth - 1
when(curWidth === 1) { // eg. 'b00000001
goto(countingUp)
}
}
}
}
val pwm = new Pwm(size = pwmSize)
pwm.io.width <> fsm.curWidth
pwm.io.output <> io.output
}
object PwmLed {
def main(args: Array[String]): Unit = {
SpinalVerilog(new PwmLed(divSize = 3, pwmSize = 8))
}
}
コードを少し説明しましょう。divSize というのは PwmLed クラスインタンス化(回路合成時)のパラメタで、上記ブロック図の ClockDivider に使うカウンタのビット数に該当します。つまり、divSize = 4 の場合、1/(1 << 4) == 1/16 の分周器になります。ctDivider が分周器ですね。
pwmSize というのは、PWM の分解能、つまり Pwm クラスの size として使われます。pwmSize が 8 ならば、1 << 8 == 256 レベル(分解能 = 256)の PWM になります。
transFsm は、あまり良いネーミングではありませんが、分周カウンタがラウンドアップするときに 1クロック幅だけ True(真)になり、ステートマシンを動かします。きっと、ScalaHDL の人に聞いたらもっとうまい設計を教えてくれるのかも知れませんが、とりあえず。ちなみに、PwmLed のクロックドメイン自体を分けてしまい、別クロックドメインの Pwm に繋ぐという方法もあるのかも知れませんが、難しそうなのでやめておきます。
val fsm = new StateMachine … というのは、FSM(有限状態マシン)を定義(インスタンス化)しています。こちらが詳しいです。中には curWidth というレジスタを持っていて、「現在の PWM 幅」(LED の明るさ)を保持しています。
when() というのは SpinalHDL の構文で、「これこれの条件のときに、これこれを代入する論理回路を出力しろ」という意味です。これは非常に癖があるので、後でもう一度説明します。
U(ctDivider.range -> true) というのは、レジスタ ctDivider と同じビット幅の符号無しバイナリ値 111…1111 を与えろ、という構文です。この辺りの説明が詳しいのですが、私も理解するまで少し時間がかかりました。いっそのこと、U(ctDivider.max) みたいな構文があれば分かりやすいと思うのですが。でも、(1 << divSize) – 1 などと書くよりは、書き間違いがなくて安心かも知れません。
クラス定義の末尾では Pwm クラスをインスタンス化して、入出力を(演算子 <> で)繋いでいます。
シミュレーションしてみる
前回の PwmLedSim.scala に以下を追加します。
object PwmLedSim {
def main(args: Array[String]): Unit = {
SimConfig.withWave.compile(new PwmLed(divSize = 3, pwmSize = 4)).doSim{ dut =>
dut.clockDomain.forkStimulus(period = 10)
for (i <- 0 until 500) {
dut.clockDomain.waitSampling()
}
}
}
}
前回と同様に sbt “runMain PwmLedSim” を実行し、グラフ出力を GTKwave で見てみましょう。
確認点は次の箇所です。
- ctDivider が 1 << 3 == 8 クロック周期でカウントアップしていること
- ctDiriver の値が 7 のところで transFsm が True に、そうでないところで False になっていること
- ct の 8クロック周期で fsm_curWidth が 1ずつインクリメントしていること
- io_output の幅が fsm_curWidth に比例して広くなっていくこと
次に、グラフをもう少し縮小してみます。
今回の確認点は次の箇所です。
- fsm_stateReg が、01(countingUp)と 10(countingDown)を繰り返していること
- その周期と、それぞれの状態の長さが、常に等しいこと
- io_output の幅が広くなったり狭くなったりしていること
大丈夫そうですね。
最後に if 文と when オブジェクトについて
以前から HDL に親しんでいる方には当然なのかも知れませんが、ソフト屋にとって一番分かりづらいのは、Scala の if 文と SpinalHDL の when オブジェクトの違いではないか、と思います。いや、それ以前に、プログラミング言語として記述する内容がどのようにして論理回路に置き換えられるのか、という部分がもっとも分かりにくいところではないでしょうか。
私も、上記の PwmLed クラスの設計には非常に苦労をしました。それは、プログラミング言語 Scala によるシーケンシャルな処理と、論理回路合成の違いが理解できていないからだと思います。(今でも完全には理解できてません。)
上記のコードでスラっと
val transFsm = Bool
// ...
transFsm := ctDivider === U(ctDivider.range -> true) // 'b11111111
と示してましたが、私は当初、次のように書いていました。
var transFsm = Bool
// ...
if (ctDivider === U(ctDivider.range -> true))
transFsm := True
else
transFsm := False
これは Scala によるコンパイル時にエラーとなります。この「===」による比較がエラーになるのです。どうして上で示した例が OK なのに、下のほうはダメなのでしょうか。おそらくこれは、Spinal HDL による Bool クラスにおける文脈では === が意味を持つのであって、Reg クラスと UInt クラス間の比較としては使えないということです。(たぶん)
それでは、ということで、次のように == を使うように書き直してみます。
if (ctDivider == U(ctDivider.range -> true))
transFsm := True
else
transFsm := False
これは今度はコンパイルは通りますが、正しい Verilog 出力が得られずシミュレーションの結果も期待通りになりません。出力された Verilog コードを覗いてみると、transFsm の演算が期待通りになっておらず、常に 1’b0 になっていることが分かります。
難しいですね。これは、シーケンシャルに解釈されるプログラミング言語(ここでは Scala)と、論理回路合成の仕組は別なのだ、ということを理解しないといけないようです。ソフト屋であるわれわれが、もっとも苦手な部分と言っても良いでしょう。
どういうことかと言うと、最後に示したような if 文の書式は、Scala 言語が論理回路の合成時に解釈する一時的な判断であって、それ自身が論理回路として出力されるのではない、ということです。つまり、ここでの if 文は静的に解釈されるだけで、論理回路には置き換えられないのです。つまり、Verilog コードには、この条件判定は現れません。だから正しく動作しないのです。一方で、最初に示した
transFsm := ctDivider === U(ctDivider.range -> true)
は、SpinalHDL によって正しく解釈され、条件回路(?)として Verilog コードに変換されます。つまり、入力が ctDivider と U(…) で、その論理出力が transFsm となるような回路に変換されるのです。だから正しく動作します。
つまり、SpinalHDL でコードを書く場合には(これは、私の知る限り Verilog HDL でも似たような問題に遭遇することがあるのですが)、それがプログラム言語としてコンパイル時点で解釈されるものなのか、論理回路の合成出力として解釈されるのかについて、常に注意を払わなくてはいけない、ということになります。これは多くの HDL 設計屋さんや SpinalHDL 言語設計者さんには自明なのでしょうが、一般のソフト屋さんに鬼門、ということになります。
そこで冒頭に述べた「if 文と when オブジェクトの違いについて」という話に戻ります。
私の理解を簡単に述べますと、if 文は Scala 言語のプリミティブであり、条件判断のためのコードです。C や Python の if 文と同じです。if 文は論理回路の合成時に解釈され、その結果が真(Scala における true)であるか偽(false)であるかが判断(演繹?)された時点で消えてしまうのです。もし条件判断を論理回路して出力したいのであれば、if 文ではなく SpinalHDL の when オブジェクトを使わないといけません。上記のコードを、when オブジェクトを使って書き直してみます。(== でなく === に戻した点に注意してください。)
when(ctDivider === U(ctDivider.range -> true)) {
transFsm := True
}.otherwise {
transFsm := False
}
これなら正しく動作するはずです。なお、transFsm への代入は Scala の「=」ではなく SpinalHDL の「:=」でないといけない点にも注意してください。また、SpinalHDL のサンプルコードを見ると、if 文でも when オブジェクトも似たような書き方をしているものがありますが、この点を正しく理解した暁には、if 文では直後にスペースを空け、when では空けないほうが正しい書き方なのではないか、と思います。繰り返しますが、if 文は Scala 言語のプリミティブであり、when は ScalaHDL で定義したオブジェクトなのです。
私もようやく少しだけ、これらの違いが分かり始めたところです。
今日は難しい話になりましたね。本来は、実際に TinyFPGA BX に書き込んで動かすところまで行きたかったのですが、今日はこの辺にしておきましょう。次回に御期待! おつかれさまでした。
御質問などありましたら、お気軽に!