この記事に関連する山梨大学の授業
- ハードウェア基礎及び実験 ディジタル回路、カウンタ、電子回路
- 計算機アーキテクチャI 論理回路とコンピュータの仕組み
- 実践ものづくり実習 電子工作
こんにちは!山梨大学大学院(医工農学総合教育部修士課程工学専攻コンピュータ理工学コース)1年の木島です。
前回の記事では、この連載で作るルーレットゲームの全体構成を紹介しました。今回からは、実際にFPGAを使って回路を作っていきます。
しかし、最初からすべての機能を一度に作ると、正しく動かなかったときに、どこに原因があるのか分かりにくくなります。また、一度に考えなければならないことが増えるため、設計や動作確認も難しくなります。
そこで今回は、システム全体をいくつかの小さな機能に分け、簡単な部分から順番に作っていきます。最初の一歩として、ボタンを押すと保留が追加され、一定時間が経過すると保留が一つ消化される回路を作ります。
今回作る「保留」とは?
本連載でいう「保留」とは、ボタンが押されたときに決まった抽選結果が、画面上の演出によって表示されるまで、一時的に保存されている状態のことです。
例えば、一つ目の抽選結果を示す演出が行われている途中で、再びボタンが押された場合を考えてみましょう。
新しいボタン入力によって抽選が行われても、実行中の演出を途中で中止したり、その演出で表示する抽選結果を新しい結果へ置き換えたりすることはしません。新しく決まった抽選結果は、次の演出で表示するための保留として保存します。
最終的には、一つの保留に次の二つの情報を保存する予定です。
- あたりまたはハズレの抽選結果
- 抽選結果を表示するまでの演出時間
今回の目標
今回の目標は、次の3点です。
- FPGAに接続したボタンの入力を受け取る
- ボタンを押すたびに、保留を一つ追加してLEDを点灯する
- 一定時間が経過したら、保留を一つ消化してLEDの表示を更新する
今後の記事では、あたり・ハズレの抽選、演出時間の決定、抽選データの保存、PCとの通信などを追加していく予定です。
FPGAボードを準備しよう
今回の回路を実際に動かすために使用するFPGA開発ボードは、Sipeed Tang Primer 20Kです。
FPGA開発ボードの価格は、搭載されているFPGAの規模や周辺機能によって異なり、数万円する製品もあります。
一方、今回のようにボタン入力やLED出力を扱う小規模な回路であれば、5,000円前後の開発ボードでも実装できます。
なお、販売価格や送料は、購入時期、販売店、選択するセットによって変わります。
ピンヘッダを半田付けしよう
ここで、FPGAボードを外部の回路へ接続する準備の一例として、別のFPGA開発ボードであるSipeed Tang Nano 20Kにピンヘッダを取り付けます。
ピンヘッダとは、基板とジャンパ線、ブレッドボードなどを接続するための端子です。
今回用意したTang Nano 20Kには、購入時点ではピンヘッダが取り付けられていませんでした。そこで、ピンヘッダを基板へ半田付けしていきます。
山梨大学の電子工作室
山梨大学には、学生が自主的に電子工作や回路実験を行える 電子工作室があります。
電子工作室には、ハンダゴテなどの工作道具だけでなく、テスタやオシロスコープといった測定機器も用意されています。
電子回路は、外見を確認しただけでは、正しく動作しているか判断できないことがあります。そのようなときは、テスタやオシロスコープを使って電圧や信号を測定すると、回路のどこまで正常に動作しているのかを調べられます。
ピンヘッダを半田付けした後の基板を、図1に示します。
私自身が半田付けをするのは久しぶりでしたが、端子同士がつながらないように注意しながら、無事に取り付けることができました。
なお、ここでは半田付けの例としてTang Nano 20Kを使用しましたが、以降の回路作成と動作確認にはTang Primer 20Kを使用します。
実際に作ろう
ここからは、FPGAを使って保留を管理する回路を作っていきます。
完成時には、ボタンを押したタイミングで、あたり・ハズレの抽選結果と演出時間を決定し、それらを保留として保存します。ただし今回は、最初の試作として抽選結果や演出時間の種類までは扱わず、保留数とLED表示を管理する回路を作ります。
今回の回路は、次のように動作するものとします。
- ボタンが押されたら、新しい保留を一つ追加する
- 現在変動中の1件と保留中の4件を合わせて、最大5件まで管理する
- 新しい保留が追加されたら、対応するLEDを点灯する
- 変動中のデータがある場合は、10秒間の演出時間を数える
- 10秒が経過したら現在の変動を終了する
- 保留が残っている場合は、保留を取り出して次の変動を開始する
- 変動と保留の状態に合わせてLED表示を更新する
以上の動作をフローチャートで表したものを、図2に示します。
ボタンが押されると、新しい保留を一つ追加し、保留数に対応するLEDを点灯します。ただし、現在変動中のデータと保留中のデータが合わせて5件に達している場合は、それ以上追加しません。
現在の変動が始まると、FPGA内部のカウンタを使って経過時間を数えます。10秒が経過したら現在の変動を終了します。
今回は動作を簡単にするため、すべての変動時間を10秒に固定します。今後は、ボタンを押したタイミングに基づいて演出時間を決め、保留ごとに保存できるように拡張する予定です。
まずはVerilogで記述してみよう
それでは、今回考えた動作をVerilogで記述してみましょう。
この連載では、最初から完成したコードだけを紹介するのではなく、開発途中のコードをシミュレーションや実機で確認し、見つかった問題を一つずつ修正しながら完成へ近づけていきます。
Pythonなどで作るソフトウェアでも、文法に誤りがなく、プログラムを実行できたからといって、期待した結果が得られるとは限りません。例えば、処理の順番や条件式を間違えていれば、プログラムは実行できても、想定とは異なる結果になります。
FPGAの開発でも同様に、Verilogの文法に問題がなく、論理合成が完了したからといって、設計者が意図した回路になっているとは限りません。Verilogで記述した内容から論理回路が作られるため、信号のビット数、値を更新するタイミング、複数の処理が同時に起きた場合の動作なども考える必要があります。
そこで、今回はまず開発途中のコードを作成し、どのような問題があるのかを確認してみます。リスト1に動きそうだけど動かないコードを示します。
リスト1:動きそうだけど動かないコード
module roulette(
// クロック
input clock,
// ブレッドボード上に設置したボタン
input button,
// 現在の変動を示すLED
output current_judge_led,
// 保留(次〜4つ以降)
output one_judge_state,
output two_judge_state,
output three_judge_state,
output four_judge_state
);
// LEDの状態
// 1列目LEDの状態 0=点灯 1=消灯
// 2列目変動時間 0=10s 1=20s(今回は未使用)
reg [1:0] judge_led_status[5:0];
// 点灯しているLEDの個数のカウント
reg count_pending=0;
// タイマー用の変数
// 10秒のカウンタ
// 計算式
// 20kHzx10s=27秒
parameter count_ten_value=270000000;
// 20秒のカウンタ
parameter count_twenty_value=540000000;
// カウントアップ
reg [23:0] count_value_reg;
// カウンターを進める
always @(posedge clock)
begin
if (judge_led_status[0][1]==0)
begin
if ( count_value_reg <= count_ten_value )
begin
count_value_reg <= count_value_reg + 1'b1;
end
else
begin
count_value_reg <= 23'b0;
count_pending<=count_pending-1;
end
end
end
// LEDの点灯を5個以内に収める
always @ (posedge clock)
begin
if (count_pending<5)
begin
if (button==1'b1)
begin
judge_led_status[count_pending]<=0;
count_pending<=count_pending+1;
end
end
end
// 結果をLEDに出力する
assign current_judge_led = judge_led_status[0][0];
assign one_judge_led = judge_led_status[1][0];
assign two_judge_led = judge_led_status[2][0];
assign three_judge_led = judge_led_status[3][0];
assign four_judge_led = judge_led_status[4][0];
endmodule
それでは、以上のプログラムを実行してみましょう。
ボードに書き込んで実行してみると、LEDが一つ点灯したままになり、動画1に示したようにボタンを押しても何も反応しません。
論理合成は完了しましたが、期待した動作にはなっていません。つまり、このプログラムには何らかの問題があると考えられます。
実機で動かない原因を調べてみる
Verilogの文法に問題がなく、論理合成が完了した場合でも、設計者が意図した回路になっているとは限りません。
例えば、信号のビット数が処理する値に対して不足していても、記述方法によっては論理合成時にエラーとして扱われないことがあります。そのため、論理合成が完了しただけで正しく動作すると判断せず、実際のコードと実機の動作を見直す必要があります。
今回は、次の点を中心にコードを確認しました。
- 保留数を記録する信号のビット数
- 10秒を数えるカウンタのビット数
- LEDの電源投入直後の状態
- ボタンを押したときの入力信号の値
- LEDへ出力する信号名
保留数を記録する信号のビット数を修正する
リスト1では、保留数を記録する count_pendingを、次のように宣言していました。
reg count_pending = 0;
ビット幅を指定せずに regを宣言した場合、この信号は1ビットになります。1ビットで表現できる値は、0と1の二つだけです。
しかし、今回は count_pendingを使って、現在変動中の1件と保留中の4件を合わせた、最大5件のデータを管理します。
保留がない状態も含めると、 count_pendingには0から5までの値を保存できなければなりません。
0 = 000
1 = 001
2 = 010
3 = 011
4 = 100
5 = 101
0から5までの値を表現するには、少なくとも3ビットが必要です。そこで、リスト2では次のように修正しました。
reg [2:0] count_pending = 3'd0;
[2:0]は、この信号が3ビットであることを示しています。また、 3'd0は、10進数の0を3ビットで表した値です。
3ビットでは0から7までの値を表現できるため、今回必要となる0件から5件までの状態を管理できます。
タイマー用カウンタのビット数を修正する
リスト1では、10秒を数えるためのカウンタを24ビットで宣言していました。
reg [23:0] count_value_reg;
24ビットで表現できる値の個数は2 24個です。0から数え始めるため、24ビットで表現できる最大値は次のようになります。
2^24 - 1 = 16,777,215
一方、今回使用するクロックの周波数は27 MHzです。27 MHzは、1秒間に2,700万回のクロックが入力されることを意味します。
したがって、10秒を数えるために必要なクロック数は、次のようになります。
27,000,000 × 10 = 270,000,000クロック
24ビットのカウンタで表せる最大値は16,777,215であるため、270,000,000まで数えることができません。
そこで、リスト2ではカウンタを29ビットに変更しました。
reg [28:0] count_value_reg = 29'd0;
29ビットで表現できる最大値は、次のとおりです。
2^29 - 1 = 536,870,911
270,000,000は536,870,911以下なので、29ビットのカウンタで10秒分のクロックを数えられます。
LEDの初期状態を設定する
リスト1では、LEDの状態を保存する judge_led_statusに初期値を設定していませんでした。
reg [1:0] judge_led_status[5:0];
FPGA上の初期状態についても、意図した値になっているとは限りません。
今回使用するLEDは、出力が0のときに点灯し、1のときに消灯する負論理です。そのため、電源投入直後にすべてのLEDを消灯するには、LEDへ出力する値を1にしておく必要があります。
そこで、リスト2では initial文を使い、各要素へ 2'b11を設定しました。
initial begin
judge_led_status[0] = 2'b11;
judge_led_status[1] = 2'b11;
judge_led_status[2] = 2'b11;
judge_led_status[3] = 2'b11;
judge_led_status[4] = 2'b11;
end
2'b11は、2ビットの両方を1にすることを表しています。これにより、電源投入直後はすべてのLEDが消灯した状態から始まります。
ボタン入力の論理を修正する
リスト1では、ボタン入力が1のときに、ボタンが押されたと判定していました。
if (button == 1'b1) begin
しかし、今回作成した回路では、ボタンを押していないときに入力が1となり、ボタンを押したときに入力が0となります。
そこで、ボタン入力の判定を次のように修正しました。
if (button == 1'b0) begin
LEDへ出力する信号名を修正する
リスト1では、モジュールの出力信号を次の名前で宣言していました。
output one_judge_state,
output two_judge_state,
output three_judge_state,
output four_judge_state
しかし、LEDへ値を出力する assign文では、異なる名前の信号を使用していました。
assign one_judge_led = judge_led_status[1][0];
assign two_judge_led = judge_led_status[2][0];
assign three_judge_led = judge_led_status[3][0];
assign four_judge_led = judge_led_status[4][0];
one_judge_stateと one_judge_ledは、Verilogでは別の信号として扱われます。そのため、モジュールの出力端子へ期待した値が出力されません。
そこで、リスト2では、出力端子として宣言した信号名と assign文で使用する信号名を統一しました。
コードを修正しよう
以上の内容を反映して修正したコードを、リスト2に示します。
リスト2:実機の動作に合わせて修正したコード
module roulette(
// クロック
input clock,
// ブレッドボード上に設置したボタン
input button,
// 現在の変動を示すLED
output current_judge_led,
// 保留(次から4つ先まで)
output one_judge_state,
output two_judge_state,
output three_judge_state,
output four_judge_state
);
// LEDと変動時間の状態
// ビット0:LEDの状態 0=点灯、1=消灯
// ビット1:変動時間 0=10秒、1=20秒(今回は未使用)
reg [1:0] judge_led_status[4:0];
// 初期値を設定する
// 電源投入後はすべてのLEDを消灯する
initial
begin
judge_led_status[0] = 2'b11;
judge_led_status[1] = 2'b11;
judge_led_status[2] = 2'b11;
judge_led_status[3] = 2'b11;
judge_led_status[4] = 2'b11;
end
// 点灯しているLEDの個数
reg [2:0] count_pending = 3'd0;
// タイマー用の定数とカウンタ
// 27 MHz × 10秒 = 270,000,000クロック
parameter count_ten_value = 270000000;
// 27 MHz × 20秒 = 540,000,000クロック
// 今回は未使用
parameter count_twenty_value = 540000000;
// 10秒を数えるための29ビットカウンタ
reg [28:0] count_value_reg = 29'd0;
// クロックの立ち上がりごとに処理する
always @(posedge clock)
begin
// 管理するデータ数を5件以内に収める
if (count_pending < 5)
begin
// ボタンが押されたときに保留を追加する
if (button == 1'b0)
begin
judge_led_status[count_pending] <= 2'b00;
count_pending <= count_pending + 3'd1;
end
end
// 現在変動中のLEDが点灯している場合
if (judge_led_status[0][1] == 1'b0)
begin
// 規定の変動時間に到達した場合
if (count_value_reg >= count_ten_value - 1)
begin
count_value_reg <= 29'd0;
count_pending <= count_pending - 3'd1;
judge_led_status[count_pending - 3'd1] <= 2'b11;
end
// 規定の変動時間に到達していない場合
else
begin
count_value_reg <= count_value_reg + 29'd1;
end
end
end
// 各LEDへ状態を出力する
assign current_judge_led = judge_led_status[0][0];
assign one_judge_state = judge_led_status[1][0];
assign two_judge_state = judge_led_status[2][0];
assign three_judge_state = judge_led_status[3][0];
assign four_judge_state = judge_led_status[4][0];
endmodule
修正したコードを実行してみよう
修正したコードを論理合成し、FPGAボードへ書き込んで動作を確認します。実際の動作を動画2に示します。
リスト1ではボタンを押しても反応しませんでしたが、リスト2ではボタンを押すとLEDが点灯し、一定時間が経過すると点灯しているLEDが一つ消灯するようになりました。
これで、最初のコードと比べると、目的の動作に近づきました。
しかし、このコードにはまだ問題が残っています。現在のコードは、ボタンを押した瞬間ではなく、ボタンが押されている間、クロックごとに保留を追加しようとします。
FPGAは27 MHzで動作しているため、ボタンを0.1秒間押した場合でも、回路は約270万回ボタンの状態を確認します。
27,000,000 × 0.1 = 2,700,000回
そのため、1回だけボタンを押したつもりでも、複数の保留が追加される可能性があります。
次は、ボタンの現在の値と1クロック前の値を比較し、ボタンが押された瞬間だけを検出する仕組みを追加します。
ボタンを押した瞬間だけを検出しよう
リスト2では、ボタンが押されている間、クロックの立ち上がりごとに保留を追加しようとしていました。
そこで今回は、ボタンを1回押したときに、保留が1件だけ追加されるように回路を修正します。
修正する回路には、主に次の三つの機能を追加します。
- 外部から入力されたボタン信号をFPGA内部のクロックへ同期させる
- ボタンを押したときに、1クロックだけ有効になる信号を作る
- ボタンを離してから20 msの間、入力が安定していることを確認する
外部からの入力をクロックへ同期させる
ブレッドボード上のボタンは、FPGA内部のクロックとは無関係なタイミングで押されます。
このような信号を非同期入力と呼びます。非同期入力が、フリップフロップで値を取り込むタイミングの近くで変化すると、フリップフロップの出力が一時的に0とも1とも判断しにくい状態になることがあります。これをメタステーブル状態と呼びます。
メタステーブル状態が後段の回路へ伝わる可能性を小さくするため、今回は二つのレジスタを直列に接続します。
reg button_sync_1 = 1'b1;
reg button_sync_2 = 1'b1;
always @(posedge clock) begin
button_sync_1 <= button;
button_sync_2 <= button_sync_1;
end
button_sync_1で外部のボタン入力を取り込み、次のクロックで、その値を button_sync_2へ渡します。
以降の回路では、外部入力である buttonを直接使用せず、2段目の button_sync_2を使用します。
ただし、2段の同期回路を追加しても、メタステーブル状態が発生する可能性を完全にゼロにできるわけではありません。後段の回路へ不安定な状態が伝わる可能性を小さくするための回路です。
ボタンを押したときに1クロックだけ有効になる信号を作る
今回使用するボタン入力は負論理です。ボタンを押していないときは1、押したときは0になります。
ボタンが押されたことを検出したら、 button_pressedを1にします。
button_pressed <= 1'b0;
if (button_lock == 1'b0) begin
if (button_sync_2 == 1'b0) begin
button_pressed <= 1'b1;
button_lock <= 1'b1;
end
end
button_pressedは、クロックの立ち上がりごとに、最初に0を代入しています。ボタンの押下を新しく検出したクロックだけ1を代入するため、1クロック幅のパルスになります。
保留を追加する処理では、ボタンの状態そのものではなく、この button_pressedを使用します。
if (button_pressed == 1'b1 && count_pending < 3'd5) begin
judge_led_status[count_pending] <= 2'b00;
count_pending <= count_pending + 3'd1;
end
これにより、ボタンが押され続けていても、 button_pressedが1になる最初の1クロックでだけ保留が追加されます。
ボタン入力をロックする
ボタンの押下を検出した後は、 button_lockを1にします。
button_lockが1の間は、新しいボタン入力を受け付けません。ボタンが一度離され、一定時間にわたって離された状態が続いたことを確認してから、ロックを解除します。
今回のクロック周波数は27 MHzです。20 msの間に入力されるクロック数は、次のように求められます。
27,000,000 × 0.020 = 540,000クロック
そこで、ボタンを離した状態が540,000クロック続いたら、 button_lockを0に戻します。
if (button_sync_2 == 1'b1) begin
if (debounce_counter < 20'd539999) begin
debounce_counter <= debounce_counter + 20'd1;
end
else begin
button_lock <= 1'b0;
debounce_counter <= 20'd0;
end
end
else begin
debounce_counter <= 20'd0;
end
20 msを数えている途中でボタン入力が再び0になった場合は、接点の状態がまだ安定していないと判断し、 debounce_counterを0に戻します。
この処理により、ボタンが十分な時間離されたことを確認してから、次の押下を受け付けます。
ボタン入力回路を追加したコード
以上の処理を追加したコードを、リスト3に示します。
リスト3:ボタン入力の同期処理と連続入力防止を追加したコード
module roulette(
// クロック
input clock,
// ブレッドボード上に設置したボタン
input button,
// 現在の変動を示すLED
output current_judge_led,
// 保留を示すLED
output one_judge_state,
output two_judge_state,
output three_judge_state,
output four_judge_state
);
// LEDと変動時間の状態
// ビット0:LEDの状態 0=点灯、1=消灯
// ビット1:変動時間 0=10秒、1=20秒(今回は未使用)
reg [1:0] judge_led_status[4:0];
// 電源投入後はすべてのLEDを消灯する
initial begin
judge_led_status[0] = 2'b11;
judge_led_status[1] = 2'b11;
judge_led_status[2] = 2'b11;
judge_led_status[3] = 2'b11;
judge_led_status[4] = 2'b11;
end
// 現在変動中のデータと保留中のデータの合計数
reg [2:0] count_pending = 3'd0;
// 27 MHz × 10秒 = 270,000,000クロック
parameter count_ten_value = 270000000;
// 27 MHz × 20秒 = 540,000,000クロック
// 今回は未使用
parameter count_twenty_value = 540000000;
// 10秒を数えるための29ビットカウンタ
reg [28:0] count_value_reg = 29'd0;
// 外部のボタン入力をFPGA内部のクロックへ同期させる
reg button_sync_1 = 1'b1;
reg button_sync_2 = 1'b1;
// ボタンを離してからの安定時間を数える
reg [19:0] debounce_counter = 20'd0;
// 1の間は新しいボタン入力を受け付けない
reg button_lock = 1'b0;
// ボタンを新しく押したとき、1クロックだけ1になる
reg button_pressed = 1'b0;
// ボタン入力の同期処理と連続入力防止
always @(posedge clock) begin
// 外部入力を2段のレジスタへ通す
button_sync_1 <= button;
button_sync_2 <= button_sync_1;
// 通常は0とし、押下を検出したクロックだけ1にする
button_pressed <= 1'b0;
// 新しいボタン入力を受け付けられる場合
if (button_lock == 1'b0) begin
// 負論理なので0のときに押されている
if (button_sync_2 == 1'b0) begin
button_pressed <= 1'b1;
button_lock <= 1'b1;
debounce_counter <= 20'd0;
end
end
// ボタン入力をロックしている場合
else begin
// ボタンが離されている場合
if (button_sync_2 == 1'b1) begin
// 20 msに相当する540,000クロックを数える
if (debounce_counter < 20'd539999) begin
debounce_counter <= debounce_counter + 20'd1;
end
// 20 ms離された状態が続いたらロックを解除する
else begin
button_lock <= 1'b0;
debounce_counter <= 20'd0;
end
end
// 数えている途中で再び押された場合は数え直す
else begin
debounce_counter <= 20'd0;
end
end
end
// 保留数、LEDおよび変動時間を管理する
always @(posedge clock) begin
// ボタンを新しく押したとき、最大5件まで追加する
if (button_pressed == 1'b1 && count_pending < 3'd5) begin
judge_led_status[count_pending] <= 2'b00;
count_pending <= count_pending + 3'd1;
end
// 現在変動中のデータがある場合
if (judge_led_status[0][1] == 1'b0) begin
// 10秒が経過した場合
if (count_value_reg >= count_ten_value - 1) begin
count_value_reg <= 29'd0;
count_pending <= count_pending - 3'd1;
judge_led_status[count_pending - 3'd1] <= 2'b11;
end
// 10秒が経過していない場合
else begin
count_value_reg <= count_value_reg + 29'd1;
end
end
end
// 各LEDへ状態を出力する
assign current_judge_led = judge_led_status[0][0];
assign one_judge_state = judge_led_status[1][0];
assign two_judge_state = judge_led_status[2][0];
assign three_judge_state = judge_led_status[3][0];
assign four_judge_state = judge_led_status[4][0];
endmodule
修正した回路を動かしてみよう
リスト3を論理合成し、FPGAボードへ書き込んで動作を確認します。動作を確認している様子を動画3に示します。
ボタンを押すと、 button_pressedが1クロックだけ1になり、保留が1件追加されます。ボタンを押し続けても、 button_lockが1になっているため、同じ押下によって新しい保留が繰り返し追加されることはありません。
ボタンを離した後、その状態が20 ms続くとロックが解除され、次のボタン入力を受け付けられるようになります。
これにより、ボタンを1回押したときに、保留が1件だけ追加されるようになりました。
まとめ
今回は、FPGAを利用して、ボタンを押すとLEDが点灯する回路を作成しました。
今回は回路を作りながら記事を執筆したため、今後、新たな間違いや改善点が見つかった場合は、後日公開する記事の中で修正していきたいと思います。
今後の連載では、FPGA側で抽選を行う回路、PC側で演出を表示するプログラム、そしてPCとFPGAを接続するUART通信を作っていく予定です。
この記事を書いた人
木島
コンピュータ理工学コース修士1年。アニメを見るのが趣味です。