today 2019-03-28
access_time 11 mins
チャタリング除去回路の実装と構文の解説
前回はBF処理系の説明と全体構成について解説した。次からはいよいよ実装を行う。
今回は赤で示すUntiChatterモジュールを実装する。
プッシュスイッチ、スライドスイッチなどは機械入力を電気信号に変換するが実際の信号ではOff->Onとはキレイに遷移してくれない。具体的にはOn/Offを短期間内で繰り返すような波形となる。
波形で書くとbutton_inのようになっているため、後の回路が誤動作してしまうことは避けたい。目標としてはdoutのような応答を作りたい。
チャタリング防止で調べるといろいろ出てくるが、ボタン入力の高周波成分を取り除ければよいのである。主には以下のあたりが主流だと思う。
今回は2を採用するが、以下の点から2.3.併用とする。
(先の図ではslow_clkで書いてしまったが)ボタン入力に100MHzほどスピードを上げる必要がなく、移動平均の値保存にLEを使用しすぎるので100Hz~1kHz程度でキャプチャする。
おそらく普通にやろうと思ったら、以下のようなコード(架空)になると思われる。入力値を保持しておいて平均値を出せば良い。
queue<int> buf;
int dout;
while(true) {
buf.enqueue(get_input());
if (buf.size() > BUF_SIZE) {
buf.dequeue()
}
dout = buf.sum() / buf.size();
}
論理回路で実装するなら(定石ではあるが)簡略化できる点がある。一つはtrue/falseの2値であること、もう一つはボタンの入力が安定したら平均値は最大値に張り付くはずなので平均値計算は不要である。
これを踏まえると以下のような回路を考えるのが妥当である。
クロックについては省略したが、立ち上がりが来るたびにButton->BUF_0, BUF_0->BUF_1…と代入することでBUFの長さで過去クロック分のデータを蓄積することができる。
あとは保持されている値がすべてTrueであることを確認して出力してあげれば良い。
大体の方式が決まったところで実装に移る。ソースコード全体を示す。
package bf
import chisel3._
import scala.math.{ log10, ceil }
class UntiChatter(sysFreq: Double, captureFreq: Double = 100, averageWidth: Int = 8, isPositive: Boolean = true) extends Module {
val io = IO(new Bundle {
val din = Input(Bool())
val dout = Output(Bool())
})
val duration: Int = (sysFreq / captureFreq).toInt
val log2: Double => Double = (x: Double) => log10(x)/log10(2.0)
val counterWidth: Int = ceil(log2(duration)).toInt + 1
val counter = RegInit(UInt(counterWidth.W), 0.U)
val trigger = RegInit(Bool(), false.B)
when(counter < duration.U) {
counter := counter + 1.U
trigger := false.B
} .otherwise {
counter := 0.U
trigger := true.B
}
val dout = RegInit(Bool(), false.B)
io.dout := dout
val captureData = RegInit(UInt(averageWidth.W), 0.U)
when (trigger) {
captureData := (captureData << 1).asUInt + Mux(io.din, 1.U, 0.U)
dout := { if (isPositive) captureData.andR else captureData.orR }
}
}
おそらくscalaがわかるがHDLがわからない人よりも、HDLはわかるがChisel(及びScala)がわからないという方のほうが多いと思うので、最初なのでscalaの構文にも触れながら解説する。
package bf
これは名前空間とほぼ相違ない。package宣言下のclassなどはこの名前空間に属する。つまり今回の場合はbf.UntiChatter
になるというわけである。小規模な場合省略も可能。
import chisel3._
import scala.math.{ log10, ceil }
先のパッケージの説明であったとおり、特定のパッケージに属するクラスなどを呼び出す場合はインポートが必要である。includeだと思ってもらえれば良い。
chisel3._
とアンダーバーがあるが、これはワイルドカードと等価でchisel3下にあるものすべてが利用可能になる。
一方{log10, ceil}
のように特定の物のみをインポートすることも可能。
先に例をいくつか出して解説する。
class MyClass {
}
scalaのクラス宣言はシンプルである。
class MyClass(a: Int, b: String) {
}
コンストラクタに引数をもたせたい場合はこう
class MyClass(a: Int, b: String = "Hello") {
}
デフォルト引数も指定可能
class MyClass(a: Int, b: String = "Hello") {
def this() = {
this(100, "Overload")
}
}
コンストラクタのオーバーロードがしたい場合は、thisメソッドを定義する。
class MyClass() extends BaseClass {
}
継承はextendsを使う。
これらを踏まえた上でUntiChatterの定義を見る。細かいことを言うとtraitとかcase classとかあるので一旦放置。
class UntiChatter(sysFreq: Double, captureFreq: Double = 100, averageWidth: Int = 8, isPositive: Boolean = true) extends Module {
}
sysFreq
, captureFreq
, averageWidth
, isPositive
をコンストラクタ引数に持ち、Module
を継承したクラスであることがわかる。
Chiselで自作のモジュールを作る際は、基本的にchisel3.Module
を継承する。またchiselではコンストラクタに指定した変数をもとにロジックの動作を決めても構わない。例えばデータのビット幅であったり定数であったりなど(Verilogでいうところのlocalparamと同じ扱い)
一応パラメータの値を解説しておく。
名前 | 概要 | 想定(デフォルト引数) |
---|---|---|
sysFreq |
システムクロック周波数 | 100e6 |
captureFreq |
入力を受け取る周期 | 100 |
averageWidth |
入力値を保持する数 | 8 |
isPositive |
入力値が負論理だった場合のオプション | true |
val io = IO(new Bundle {
val din = Input(Bool())
val dout = Output(Bool())
})
chisel3.Module
ではフィールド変数にio
をオーバーライドする必要がある。ここにはBundle
というcで言うところの構造体(struct)にあたるものをIOに投げたものを代入してあげれば良い。
※scalaチックなことを気にする必要はないので、今はこのような構文で描けば良いと理解してもらって差し支えない。
Bundle
の中にはIOポートとして外部に公開したいポートと、その種類について記述する必要がある。
例えば8bitの数値を受け取るのであればval num = Input(UInt(8.W))
と宣言する。
ちょっとややこしいので3つほど解説する。
scalaの変数宣言には、val
とvar
がある。val
は一度代入した値を変更することができない。基本的にはval
を使うことが推奨される。
val a = 5
var b = "Hello"
b = "World" // エラーなし
a = 10 // valは不変なので後から値を変更できない
また型を明示する場合は以下のように書く。
val a: Int = 5
UInt(8.W)
と書いた.W
について。chiselにおける型(UInt, SInt, Width, Bits, Bool…)はscalaのプリミティブ型(Int, Boolean, …)とは別物として処理され暗黙のキャストもなされない(scalaの言語仕様上は可能なので、おそらく意図しないバグ混入対策かと)
なので、scalaプリミティブ型からchisel型に変換するために以下のメソッドが用意されている
プリミティブ | chisel | メソッド名 | 例 | 使い所 |
---|---|---|---|---|
Int | UInt | .U |
15.U |
整数を符号なし整数にしたいとき |
Int | SUInt | .S |
-20.S |
符号付き整数にしたいとき |
Int | Width | .W |
8.W |
ビット幅の指定をしたい場合 |
Boolean | Bool | .B |
true.B |
真偽値を使いたいとき |
ちなみにchisel内での型変換(例えばSInt
->UInt
など)は、.asUInt
, asSInt
などが用意されているので必要な場面で参照してほしい。
Input
, Bool
にnewがいらないのを気持ち悪く感じるかもしれないので追記する。気にならない人は読み飛ばしてもらって構わない。
scalaがscala言語上にDSLを作るにあたって素晴らしい言語であることは自明なのだが、その特徴らしい構文がいくつか出てきていたので紹介する。
val a = new MyClass("Hello")
これはコンストラクタを呼び出している。
val a = MyClass("Hello")
実はこれはMyClass.apply("Hello")
と等価。scalaでstaticメソッドのようなものを作る場合はclassではなくobjectで宣言した以下のようなクラスを用意する。
object MyClass {
def apply(str: String) = {
???
}
}
c++などでは演算子はオーバーロードしていたが、scalaの場合は引数が一つのメソッドを空白を開けて演算子のように扱うことができる。つまり
println(3 + 5)
println(3.+(5))
が等価である。更に言うなら記号である必要もなく、PythonでいうところのRangeに相当するものは以下のようにかける。
0 to 10 map(_ * 2) foreach(println)
0.to(10).map(x => x * 2).foreach(println)
20.U
でInt
->chisel3.UInt
に変換できることはわかるがどうしてプリミティブ型にchisel変換用のメソッドが生えているのか疑問に思った方がいるかも知れない。
実はこれにはカラクリがあって、C#やKotlinでは拡張関数(メソッド)と呼ばれる機構と同じようなものが実装されている。
例えば"Hello".myPrintln
を使えるようにしたいとする。その場合以下のようなクラスを定義しその名前空間が見える場所が見えるようにimportしてあれば使えるようになる。
implicit class MyStringExtension(val src: String) {
def myPrintln: Unit = println("!!!!!" + src + "!!!!!")
}
chiselはこの機構を利用してscalaプリミティブ型との相互運用を図っている。
と、脱線が過ぎたのでここから本題に戻る。
val duration: Int = (sysFreq / captureFreq).toInt
val log2: Double => Double = (x: Double) => log10(x)/log10(2.0)
val counterWidth: Int = ceil(log2(duration)).toInt + 1
システムクロックからキャプチャする周波数に変換するにあたって、カウンタに必要なビット幅とカウンタをリセットする周期を計算している。 verilog HDLのlocalparamだとどうしてもここの柔軟性が確保できずモヤモヤしながらコメントを書いていたので、私的にはかなり嬉しい機能である。
ちなみにこの計算はすべてscalaの機能だけで構成されているので、verilogに変換する段階で定数に置き換えられる。(公式ドキュメントではscala land, chisel landと呼称している)
val counter = RegInit(UInt(counterWidth.W), 0.U)
val trigger = RegInit(Bool(), false.B)
when(counter < duration.U) {
counter := counter + 1.U
trigger := false.B
} .otherwise {
counter := 0.U
trigger := true.B
}
これはverilogで書き下すなら(ビット幅などは正確な構文ではない)以下の回路と同じになる
reg [counterWidth-1:0] counter;
reg trigger;
always @ (posedge clk) begin
if(reset) begin
counter <= 0;
trigger <= 1'd0;
end else if(counter < duration) begin
counter <= counter + 1
trigger <= 1'd1
end else begin
counter <= 0
trigger <= 1'd0
end
end
きっと読者の多くはverilog HDLで記述されたほうを見て理解できるのではないかと思う。それはさておき順番に解説する。
Reg(<type>)
及びRegInit(<type>, <initial value>)
で初期化することができる。
val counter = RegInit(UInt(10.W), 0.U)
例えば上記は10bitの幅を持つレジスタを初期値0で宣言している。この初期値宣言はresetがアサートされているときに初期値に設定されるようなverilogを生成してくれるようになる。
またここまで触れてこなかったがCLK、RESETについては開発者側が明示しなくても自動的にchiselが生成してくれるので基本的に記述する必要はない。(複数クロックドメインを扱う場合は別)
chiselではModuleクラスのフィールドに記述した内容はそのままverilogに変換される。(chiselに絡んだ変数が含まれる場所のみ。先の事前計算などはverilog上には残らない)
ただしif文などはchisel landで条件分岐に使う構文が用意されている。
例えば今回when
, .otherwise
句を使っているが、これはif
, else
のchisel land版である。
when(counter < duration.U) {
counter := counter + 1.U
trigger := false.B
} .otherwise {
counter := 0.U
trigger := true.B
}
:=
演算子があるが、これは代入演算子である。
これが組み合わせ論理回路になるか同期回路になるかは代入先にレジスタがあるかどうかであるようだ。(※要調査)
たったこれだけでcaptureFreq
で設定した周期ごとにtrigger
がアサートされる回路が完成した。
いよいよ本命の実装をする。先の回路でtrigger信号を受け取れるようになったので、triggerが来たときに値を取り込むシフトレジスタを実装してみる。(※Vecでやっても良かったが値シフトでやった)
val dout = RegInit(Bool(), false.B)
io.dout := dout
val captureData = RegInit(UInt(averageWidth.W), 0.U)
when (trigger) {
captureData := (captureData << 1).asUInt + Mux(io.din, 1.U, 0.U)
dout := { if (isPositive) captureData.andR else captureData.orR }
}
captureDataはaverageWidthで指定された幅のレジスタで初期化し、triggerの値が来るたびに値を左シフトして一番下に現在の値を取り込む。
verilogで書くなら以下のようになる。(※isPositive=trueの場合。後述)
reg dout;
assign io_dout = dout;
reg [averageWidth - 1:0] captureData;
always @ (posedge clk) begin
if (reset) begin
captureData <= 0;
dout <= 1'd0;
end else if(trigger) begin
captureData <= { capturedata[averageWidth - 1:1], din};
dout <= &captureData;
end
end
※dout <= &captureData
はverilog HDLにあるリダクション演算子というものである。意味はすべてのビットのandした結果を返す。以下と等価。
dout <= captureData[averageWidth - 1] & captureData[averageWidth - 2] & ..中略.. captureData[0];
val dout = RegInit(Bool(), false.B)
io.dout := dout
io.dout
は最初に宣言した出力ポートなので何もアサインされてない(エラーになる)
なので、何かしらの組み合わせ論理回路かレジスタの値を:=
を使って設定する。
3項演算子と等価。型変換が面倒だったので。以下のような記述をしているだけである。
io.din ? 1'd1 : 1'd0
先程から注釈として何度も入れてきたが、scala landの構文が事前処理されることをマクロとして利用できる。
またscalaの構文はすべて値を返すので、if~else
を利用してメタ的に処理切り替えが実装できる(なので3項演算子は存在しない)
具体的にはコンストラクタでisPositive
というボタン入力が正論理/負論理であるパラメータを受け取り、生成する回路を動的に変更している。
if (isPositive) captureData.andR else captureData.orR
andR
はリダクション演算子のand、orR
はリダクション演算子のorである。チャタリングが収まっていればすべてand(もしくはor)を満たすことができるのでdout
がアサートされる仕掛けである。
あっちこっち説明してまとまりがなかったが、これで目的のチャタリング除去回路を実装することができた。
テストについては簡単なものしか書いていないので詳解しないが、UntiChatterSpec.scala -Githubを参照していただきたい。
scalaの構文についてもすこし触れたが、chiselとして使うだけであれば詳しく知らなくても十分使えると思う。ただテストを書く場合などもちろん知っておくと得する要素は大きい。
次は同期FIFOを実装する。次以降は一度説明したところを端折れるのでもっと大雑把に説明する予定。