ハードウェアの気になるあれこれ

技術的に興味のあることを調べて書いてくブログ。主にハードウェアがネタ。

Chiselのユーティリティ - log2Up / log2Down / log2Ceil / log2Floorについて

スポンサーリンク

Twitterでつぶやいてた以下のスレッドに書いてたやつをまとめてみた話。

Chisel.util.log2xx

Chiselにはその値を表現するのに必要なビット幅を算出するためのユーティリティとして以下の4種類が用意されている。 System Verilog$clog2みたいなの。

  • log2Up
  • log2Down
  • log2Ceil
  • log2Floor

上記のメソッドはChisel3.2のSNAPSHOT時点からは各メソッドごとに以下のようにScaladocがちゃんと入っていてソースを見ればどういうものかわかるようになっている(v3.1.8では詳細な説明は存在しない)

/** Compute the log2 of a Scala integer, rounded up.
  * Useful for getting the number of bits needed to represent some number of states (in - 1).
  * To get the number of bits needed to represent some number n, use log2Ceil(n + 1).
  *
  * Note: can return zero, and should not be used in cases where it may generate unsupported
  * zero-width wires.
  *
  * @example {{{
  * log2Ceil(1)  // returns 0
  * log2Ceil(2)  // returns 1
  * log2Ceil(3)  // returns 2
  * log2Ceil(4)  // returns 2
  * }}}
  */
object log2Ceil {
  def apply(in: BigInt): Int = {
    require(in > 0)
    (in-1).bitLength
  }
  def apply(in: Int): Int = apply(BigInt(in))
}

ただ毎回ソース見るのも面倒なのでまとめておこう、、というのが冒頭のつぶやきであり本記事になる。

サンプルコード書いて調べてみる

だいたい使うときに気にするのって以下の2点くらいなのでiを1 ~ 16の範囲で変化させて確認した。

  • 1を入れた時にどうなるか
  • 2のべき乗でどうなるか

その際のコードは以下。

import chisel3._
import chisel3.util._

/**
 * ただ単にlog2xxを呼んで値をチェックするメインオブジェクト
 */
object Main extends App {

  for (i <- 0x1 to 0x10) {
    println(
      f"| $i%2d | " +
      f"${log2Up(i)} | ${log2Up(i + 1)} | " +
      f"${log2Down(i)} | ${log2Down(i + 1)} | " +
      f"${log2Ceil(i)} | ${log2Ceil(i + 1)} | " +
      f"${log2Floor(i)} | ${log2Floor(i + 1)} | " +
      f"${isPow2(i)} |")
  }
}

各々のメソッドについてiを入れた場合とi+1を入れた場合も一緒に確認しておく。というのも3.2-SNAPSHOT時点でlog2Up/log2Downに以下のように「0bitのwireが動くまでは廃止しないけど1より大きくなるのがわかってるならlog2Ceilを使って」的なコメントが入っているからだ。

/** Compute the log2 of a Scala integer, rounded up, with min value of 1.
  * Useful for getting the number of bits needed to represent some number of states (in - 1),
  * To get the number of bits needed to represent some number n, use log2Up(n + 1).
  * with the minimum value preventing the creation of currently-unsupported zero-width wires.
  *
  * Note: prefer to use log2Ceil when in is known to be > 1 (where log2Ceil(in) > 0).
  * This will be deprecated when zero-width wires is supported.
  *
  * @example {{{
  * log2Up(1)  // returns 1
  * log2Up(2)  // returns 1
  * log2Up(3)  // returns 2
  * log2Up(4)  // returns 2
  * }}}
  */```

log2xxの計算結果一覧

結果は以下のようになった(表がでかくなるのでlog2Up等のlog2は省きました)

i Up(i) Up(i+1) Down(i) Down(i+1) Ceil(i) Ceil(i+1) Floor(i) Floor(i+1)
1 1 1 1 1 0 1 0 1
2 1 2 1 1 1 2 1 1
3 2 2 1 2 2 2 1 2
4 2 3 2 2 2 3 2 2
5 3 3 2 2 3 3 2 2
6 3 3 2 2 3 3 2 2
7 3 3 2 3 3 3 2 3
8 3 4 3 3 3 4 3 3
9 4 4 3 3 4 4 3 3
10 4 4 3 3 4 4 3 3
11 4 4 3 3 4 4 3 3
12 4 4 3 3 4 4 3 3
13 4 4 3 3 4 4 3 3
14 4 4 3 3 4 4 3 3
15 4 4 3 4 4 4 3 4
16 4 5 4 4 4 5 4 4

回路のパラメタライズをやるときにlog2Ceilを使った結果ビット幅がゼロになるというケースに何度か出くわした。先ほどのソースのコメントにもあるとおり、現状ビット幅が0のwireは作れないので注意が必要だ。

改めてまとめてみての自分的使い分けは以下の感じ。

  • アドレスのビット幅→log2Ceil(N)
    • これは容量1KBのRAMと言った時には1024の状態があればよくて、その場合は0~1023の範囲で表せるから
  • ポート数やらモジュールの数→log2Up(N)もしくはlog2Ceil(N+1)
    • こっちはNという数字自体に意味があるので、その数字自体を表現できるビット幅が必要になる
    • ソースのコメント的にはlog2Upはいずれ消えそうなので、log2Ceil(N+1)で揃えておくのが良さそう

ということで、これからは迷ったらこの表を参照すればOK!という自分メモでした。