Verilogでゲームを作る --Designing Video Game Hardware in Verilogの簡易和訳-- 目次
Verilogでビデオゲーム
Verilogの参考書を探してるとアマゾンで以下の本に出会った。
Designing Video Game Hardware in Verilog
- 作者: Steven Hugg
- 出版社/メーカー: Independently published
- 発売日: 2018/12/15
- メディア: ペーパーバック
- この商品を含むブログを見る
プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
Verilogでは同様のコンセプトの書籍が見つからなかったので、さっそく購入。
まだ全部読んでいないため、良書かどうかの判断はできないが、
せっかくなので、重要な箇所だけかいつまんで和訳しようと思う。
全部翻訳すると著作権に抵触するらしいので。
なるべく最後まで完結できるように頑張る。
この書籍に書かれているソースコードは、以下に全て記載されている。
PLATFORMSをHardware > Verilogを選択する。
8bitworkshop
CRTのシミュレータ付きで、ソースコードを改変したら即時CRTに反映される。
この著者まじすげー。
part1(1章~11章)はこちら
yuji2yuji.hatenablog.com
poart5(12章~15章)はこちら yuji2yuji.hatenablog.com
Verilogでゲームを作る --Designing Video Game Hardware in Verilogの簡易和訳-- part2
Verilogでビデオゲーム
Verilogの参考書を探してるとアマゾンで以下の本に出会った。
Designing Video Game Hardware in Verilog
- 作者: Steven Hugg
- 出版社/メーカー: Independently published
- 発売日: 2018/12/15
- メディア: ペーパーバック
- この商品を含むブログを見る
プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
Verilogでは同様のコンセプトの書籍が見つからなかったので、さっそく購入。
まだ全部読んでいないため、良書かどうかの判断はできないが、
せっかくなので、重要な箇所だけかいつまんで和訳しようと思う。
全部翻訳すると著作権に抵触するらしいので。
なるべく最後まで完結できるように頑張る。
目次はこちら
12章 動くボール
アニメーション表示の最初の電子ゲームの一つは1958年にBrookhaven National Laboratoryで開発されたTennis of Twoである。
このアイデアは、いかにして跳ねるボールをオシロスコープ上に表示するかというアナログコンピューターの参考書に例示された回路から生まれた。
後に、Magnavox Odysseyコンソールがタ級をtVスクリーンに映し出した、Pongがアーケードとして誕生した。
プレーヤーはノブを回転させスクリーン上のパドルを動かし、スクリーン上の動いている四角い玉を跳ね返す。
絶対座標
9章で水平/垂直同期パルスの時間を計るカウンターを使って、同期生成モジュールを作成した。それはまた現在の電子銃の位置を表していた。
動くボールをスクリーンに置くために、これをどう使えばよいだろうか。
抜け目のないデザイナーは、特にソフトウェアのバックグラウンドを持っている場合、「ボールの水平位置と垂直位置を追跡する2つのレジスターを持たせよう。これらを水平および垂直ビーム位置から引き算し、水平/垂直の引き算した結果ががいずれもNピクセル未満だった場合はボールを表示する。連続する各ビデオフレームで、ボールの位置をある方向に数ピクセルだけ動かす。」
1ピクセルのボールを描くところから始めよう。
これはとても簡単、ボールの水平位置と垂直位置をビームポジションと比較して、水平位置と垂直位置の両方が等しいときにボールを描けばよい。
wire ball_hgfx = ball_hpos == hpos; // horizontal wire ball_vgfx = ball_vpos == vops;// vertial wire ball_gfx = ball_hgfx && ball_vgfx; // AND operator
もしball_hgfx
信号をCRTに表示すれば、水平ラインが表示される。
ball_vgxf
信号は垂直方向のラインを表示する。
両者が交わるところがボールであり、白いドットでであり、ball_gfx信号である。
より大きなボールを作りたい場合、イコールではなく比較を行う必要がある。
ボール位置をビーム位置から引き、両者の値をボールサイズと比較する。
wire [8:0] ball_hdiff = hpos - ball_hpos; wire [8:0] ball_vdiff = vpos - ball_vpos; wire ball_hgfx = ball_hdiff < BALL_SIZE; wire ball_vgfx = ball_vdiff < BALL_SIZE; wire ball_gfx = ball_hgfx && ball_vgfx;
Verilogはデフォルトでunsinegが数字に割り当てられるため、引き算結果が負数の場合はオーバーフローする点に注意して欲しい。
これは良いことで、なぜならオーバーフローした数値は常にBALL_SIZEより大きいからだ。
ただし、ボールのサイズがばかげた大きさでなければ。
ボールをフレームごとに動かしたい。そのためにvsync信号をトリガーとする、これはフレームの終わりに発生する。
always @(posedge vsync or posedge reset) begin if (reset) begin ball_hpos <= ball_horiz_initial; ball_vpos <= ball_vert_initial; end else begin ball_hpos <= ball_hpos + ball_horiz_move; ball_hpos <= ball_vpos + ball_vert_move; end end
壁での跳ね返り
スクリーンの端っこでボールが跳ね返るようにしたい。
最初にボールが端にヒットしたかをどうかを定義する必要がある。
賞取ると検出したときに、左右の壁ならボールの水平方向の速度を、上下の壁ならボールの垂直方向の速度を反転させる、もし隅っこなら水平/垂直両方の速度を反転させる。
wire ball_horiz_collide = ball_hpos >= 256 - BALL_SIZE; wire ball_vert_collide = ball_vpos >= 240 - BALL_SIZE;
ボールサイズを考慮して、右辺からボールサイズを引き算した。
ボールが左の壁に当たった場合または上の壁に当たった場合については、
先ほど述べたように負数はオーバーフローして数値が大きくなるため、式には記載しなくてよい。
衝突をトリガーとしたイベントを記載する。
always @(posedge ball_horiz_collide) ball_horiz_move <= -ball_horize_move; always @(posedge ball_vert_collide) ball_vert_move <= -ball_vert_move;
posedgeでトリガーするため、イベントは信号がHになったときに一度だけ発生する。信号がフレームごとに1パルスのみ発生する限り、フレームごとに1度だけ速度反転する。
13章 スリッピングカウンター
スリッピングカウンター法
絶対座標のカウンターはソフトウェアでゲームを作成する人になじみ深い方法だ。
しかし最初のハードウェアによるビデオゲームに用いられた方法ではない。
彼らはもう少し賢い方法、スリッピングカウンターと呼ばれる方法を用いた。
歴史的な観点から、このスリッピングカウンターについて少し記載しよう。
(時間がない人は本章は飛ばしても良い)
X/Y座標を表すレジスタの代わりにスリッピングカウンター法は水平と垂直の同期カウンタを並行してカウントする2つのレジスタを保持する。
ボールの絶対位置ではなく、ボール位置とビーム位置の差分を追跡し続けると考えてほしい。
もしボールが定常的であれば、ボールのカウンターが正確にビデオの同期カウンターと同期する、ボールのオフセット位置を除いて。
しかし、もしボールが動いていれば、それらは1フレームに一回だけある決まった量をスリップすることが許される。
スリップの方向と大きさはボールの速さと向きを決める。
例えば、もしボールが右に動いていれば、ボールの水平カウンターは1フレームに数サイクルカウントする 。もし上に動いていれば、ボールの垂直カウンターは数サイクル少なくカウントされる。
なぜこのような方法をとったのだろうか。なぜなら、ハードウェアを少なくできるからだ。
この方法は、4つの9316 4ビットカウンターチップ(水平に2つ、垂直に2つ)、追加のフリップフロップ、および少数のゲートで実装できる。
これは、ビデオ同期ジェネレーターの実装方法に似ている。
もしボールの絶対位置を保持すれば、2つの8btiレジスタと2つの8bitの引き算回路、そして、同期ジェネレーターからレジスターに位置をラッチするためのロジックが必要になる。
* 詳細は省略。
14章 RAM
アーケードゲーム初期のころは、基板上のフリップフロップやカウンターにデータを保持しており、CPUとビデオフレームバッファを除いて、RAMの必要性は少なかった。
ブロック崩してRAMチップが使われていた。ICが一つ$5~$10。
当時のRAMは1bit幅だった。各アドレスが1bitのデータを保持していた。
スペースインベーダーは8つの1bitRAMで構成される。
後期には4bitのRAMが利用可能になり、実装チップ数が減った。
VerilogのRAM
読み書きできる大きなRAMを作りたい。
Verilogは明確なRAMのコンセプトを持たないが、インデックス配列でRAMを模擬できる。
例えば1024byteの容量を持つアドレスバスとwrite-enable信号(we)で読み書きできる"mem"というRAMは以下のように宣言できる。
reg [7:0] mem [1024]; dout <= mem[addr]; if (we) mem[addr] <= din;
ビット幅、アドレスをparamで設定することで、可用性の高いRAMモジュールを作成できる。
module RAM_sync(clk, addr, din, dout, we); parameter A = 10; //address bits parameter D = 8; //data bits input clk; input [A-1:0] addr; input [D-1:0] din; output [D-1:0] dout; input we; reg [D01:0] mem [0:(1<<A)-1]; always @(posedge clk) begin if(we) mem[addr] <= din; dout <= mem[addr]; end endmodule
多くのFPGAはこの手のモジュールを自動で名部のblockRAMに変換し、リソースを開放する。
Tri-stateバスと入出力ポート
RAMをコモンバスに接続したいかもしれない。そのバスを入力と出力両方に使用したい。そのような場合にTri-stateロジックを使用できる。
0と1に加えて、ハイインピーダンスのZが使用できる。
上記のRAMモジュールでは、書き込みしていない場合に、シグナルパスをオープンにしたい。
入出力ポートの宣言とデータの読み出しは以下のように書ける。
inout [D-1:0] data; assign data = we ? {D{1'bz}} : mem[addr];
書き込み中だとweがHなので、dataは入インピーダンス状態。
読それ以外ではdata = mem[addr]となる。
*このDはparameterで設定された8を意味する。
tri-stateバスへの書き込み
一方バスへの書き込みは以下となる。
always @(posedge clk)begin if(we) mem[addr] <= data; end
15章 タイルグラフィック
RAMとCPUの価格が下がり続けると、ビデオゲームはCRT端末と同じラインに沿って設計され、均一サイズのセルの行と列が表示されるようになった。
このスキームは現在、タイルグラフィックスとして一般に知られている。
タイルグラフィックシステムは、特定のセルの行と列に表示する文字を指定するRAMの値を検索する。
次に、各文字のビッチマップを含むROM内のタイルデータを検索する。
11章で作成したROMを使って、単純なRAMベースのタイルグラフィック生成器を作成する。
wire [9:0] ram_addr; wire [7:0] ram_read; reg [7:0] ram_write; reg ram_wrireenable = 0; RAM_sync ram( .clk(clk), .dout(ram_read), .din(ram_write), .addr(ram_addr), .we(ram_writeenable) );
次にXとYの位置をデコードし行と列のデータに変換する。
wire [4:0] row = vpos[7:3]; wire [4:0] col = hpos[7:3]; wire [2:0] rom_yofs = vpos[2:0]; wire [4:0] rom_bits;
10ビットアドレスを得るために、rowとcolを連結する。
またdigitデータをramと紐づける。
assign ram_addr = {row, col}; wire [3:0] digit = ram_read[3:0];
ROMと結び付ける。
wire [2:0] xofs = hpos[2:0]; wire digit_gfx = rom_bits[~xofs];// inverts xofs. digits10_case numbers( .digit(digit), .yofs(rom_yofs), .bits(rom_bits) );
xofsを反転させる理由は、ROMに保存されているbitデータと画面に表示させるbitの方向が左右逆なため。
アニメーション
上に書いたのは変化のない表示には適している。しかし、アニメーションも動作させたい。そのために、RAMに書き込む必要がある。
我々の単純なアニメーションは、すべてのメモリーセルをフレームごとに1ずつ増やしていく。
各行の最後のスキャンラインの間で、そして各文字の最後の2クロックの間だけ書き込みを行う。
セルは8x8で数字は5x5なので、書き込みが読み出しと衝突しないだけの十分なブランクがある。
always @(posedge clk)begin case (hpos[2:0]) // on 7th pixel of cell 6:begin //increment RAM cell ram_write <= (ram_read + 1); //only enable write on last scanline of cell ram_writeenable <= (vpos[2:0] == 7); end 7:begin //disable write ram_writeenable <= 0; end endcase end
これはとても単純なアニメーションで、updateブロックはRAMのアドレスに触れていない。
16章 スイッチとパドル
ほぼ回路の説明なので省略。
スイッチとパドルの回路の模式図は以下。
Verilogでゲームを作る --Designing Video Game Hardware in Verilogの簡易和訳-- part1
Verilogでビデオゲーム
Verilogの参考書を探してるとアマゾンで以下の本に出会った。
Designing Video Game Hardware in Verilog
- 作者: Steven Hugg
- 出版社/メーカー: Independently published
- 発売日: 2018/12/15
- メディア: ペーパーバック
- この商品を含むブログを見る
プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
Verilogでは同様のコンセプトの書籍が見つからなかったので、さっそく購入。
まだ全部読んでいないため、良書かどうかの判断はできないが、
せっかくなので、重要な箇所だけかいつまんで和訳しようと思う。
全部翻訳すると著作権に抵触するらしいので。
なるべく最後まで完結できるように頑張る。
目次はこちら
1章 Bool 論理
基本的な内容なので省略!! 内容は以下。
- ブール代数
- 真理値表
- ビットとデータ数
- 2の補数
- 桁上げ
- signedとunsigned
- 10進数と16進数
2章 ディスクリートハードウェア
基本的な内容なので省略!! 内容は以下。
- ロジックゲートの歴史
- TTL
- 7400シリーズ
- 回路図
3章 クロックとフリップフロップ
シーケンシャルロジック(順序回路)
過去の入力ではなく、現在の入力のみを考慮する組み合わせ回路について既に説明した。
面白い事をするには、回路にメモリが必要。
順序回路は現在を考慮した回路ですが、過去を記憶することも可能。
この種の回路はクロック信号によって駆動される。
全ての動作がクロックに同期しているので、同期回路と呼ばれている。
ほとんどの回路は組み合わせ回路と順序回路で構成される。
ただ一つの例外として、ROMはその状態が入力に関わらず変化しないので、組み合わせ回路とみなされる事がある。
クロック
同期回路において、すべての動作はクロックに同期する。
クロックは決まった周波数でLowとHighを交互に繰り返す。
例えば1MHzのクロック 信号は1秒間に100万回LowとHighを繰り返す。
クロックシグナルは水晶発振子やクロック分配機によってい生成される。
回路はしばしば異なる周波数/位相のクロックを含む。
フリップフロップ
フリップフロップは順序回路に不可欠な構成要素だ。フリップフロップはクロックでのみ変化する1つのビットを記憶する。
フリップフロップはエッジでトリガーされる。状態は常にクロック信号の立ち上がりor立下りのエッジでのみ変化し得る。
出力信号はクロックエッジの前と後で常に安定である。この規約を乱すと結果としてセットアップタイムバイオレーションまたはホールドタイムバイオレーションが生じ、メタステーブル(準不安定)な状態になる。
ラッチ
ラッチはフリップフロップに似ているが非同期である。クロックのエッジではなく入力信号に反応する。
一般的にラッチは使用せずフリップフロップのみを使用するのが良い。なぜなら、非同期な回路はメタステーブルな状態を招くからだ。
4章 HDL(Hardware Description Language)
省略。
- 歴史
- シミュレーションについて
- モデリングについて
5章 Verilogの初歩
Verilogの基本文法の説明なので省略。以下の表だけ頭に入れておけば十分。
6章 IDE
The 8bitworkshop IDEなるものがあることの紹介なので省略。
実際に画面に表示するためのシミュレーターも含まれている。実機ない人でもデモできる。
8bitworkshop
7章 シンプルなクロック分配器
最初のVerilogモジュールとして、クロック分配器を作ろう。
クロック分配器はクロック信号の入力を受けて、整数で分割されたクロックを生成する。
例えば、10MHzのクロックを2で分割して5MHzのクロックを生成する。
一般的に水晶発振子はマスタークロックを生成する。クロック分配回路はシグナルを方形波にし、回路の様々な目的に合わせた低い周波数を生成する。
以下にクロック分配器のタイミングダイアグラムを示す。
フリップフロップを直列に接続して回路を実現する。
module clock_divider( input clk, input reset, output reg clk_div2, output reg clk_div4, output reg clk_div8, output reg clk_div16 ); always @(posedge clk) clk_div2 <= ~clk_div2; always @(posedge clk_div2) clk_div4 <= ~clk_div4; always @(posedge clk_div4) clk_div8 <= ~clk_div8; always @(posedge clk_div8) clk_div16 <= ~clk_div16;
この方法は同期回路になっていないので、常に正しいといえる方法ではない。
本書のデモではこれを使用する。
回路図は以下となる。
8章 バイナリカウンター
Verilogなら簡単にバイナリカウンターが作成できる。
非同期リセットもついでに記載。
assign cntr_div2 = counter[0]; assign cntr_div4 = counter[1]; assign cntr_div8 = counter[2]; assign cntr_div16 = counter[3]; always @(posedge clk, posedge reset)begin if(reset) counter <= 0; else counter <= counter + 1; end
9章 ビデオ信号発生器
CRTディスプレイへの描画と同様にビデオ信号を生成する。
CRTでは左から右へ移動するスキャンする電子銃が各ピクセルの明暗を作る。
同期シグナルはCRTのスキャン信号のストップとスタートを決める。同期シグナルは2つある。
水平同期(horizontal sync)は新しいスキャンラインを生成する。
vertical sync(垂直同期)は新しいfieldを生成する、つまり電子ビームを左上に移動させる。
8bitworkshop IDEにはリアルタイムで信号を表示するCRTシミュレーションがある。
CRTシミュレーションのディスプレイは256x240のピクセルで、各ピクセルはクロック1サイクルの幅を持つ。
最初に、水平と垂直の同期信号を生成するモジュールを作成する。
同期信号発生器は262スキャンライン各々について309クロックをカウントする。
フレームレートを60fpsとするとクロックは
262x309x60 = 4.875MHzとなる。
モジュールの入出力を書いてみる。
module hvsync_generator(clk, reset, hsync, vsync, display-on, hpos, vpos); input clk; input reset; output hsync, vsync; output display_on; output [8:0] hpos; output [8:0] vpos;
モジュールの入力はclkとresetの二つだけ。reset信号はモジュールの初期状態を保証する。
hsyncとvsyncは水平と垂直の同期信号。
display_onは現在のピクセルが表示可能エリアにある場合にセットする。
hposとvposは9bitカウンターで、現在の水平・垂直の位置を与える。
ディスプレイの表示可能領域のピクセルは(0, 0)~(255, 239)である。全領域(0, 0)~(308, 261)である。
8bitだと255までしかカウントできないので、hposとvposは9bit必要になる。
コードを分かりやすくするために、localparamを設定する。
localparam H_DISPLAY = 256; localparam H_BACK = 23;//back porch localparam H_FRONT = 7;//front porch localparam H_SYNC = 23;//horizontal sync width localparam V_DISPLAY = 240; localparam V_TOP = 4;//top border localparam V_BOTTOM = 14;//bottom border localparam V_SYNC = 4;vertical sync line
localparamは算術演算ができる。新たに同期信号のスタートとストップを示すカウンターと0に戻るためのMAXカウンターを定義する。
localparam H_SYNC_START = H_DISPLAY + H_FRONT; localparam H_SYNC_END = H_DISPLAY + H_FRONT + H_SYNC -1; localparam H_MAX = H_DISPLAY + H_BACK + H_FRONT + H_SYNC -1; localparam V_SYNC_START = V_DISPLAY + V_BOTTOM; localparam V_SYNC_END = V_DISPLAY + V_BOTTOM + V_SYNC -1; localparam V_MAX = V_DISPLAY + V_TOP +V_BOTTOM + V_SYNC -1;
hposを扱うカウンターをalwaysで記載。
//jorizontal position counter wire hmaxxed = (hpos == H_MAX) || reset; always @(posedge clk) begin hsync <= (hpos >= H_SYNC_START && hpos<=H_SYNC_END); if(hmaxxed) hpos <= 0; else hpos <= hpos + 1; end
水平同期中ならhsyncは1とする。加えてH_MAX又はリセットが押された場合はhposは0、そうでないならhposは1インクリメントする。
垂直同期についても別のalwaysブロックで記載する。
always @(posedge clk) begin vsync <= (vpos >= V_SYNC_START && vpos <= V_SYNC_END); if(hmaxxed) begin if(vmaxxed) vpos <= 0; else vpos <= vpos + 1; end end
display_onシグナルは以下で計算する。
assign display_on = (hpos < H_DISPLAY) && (vpos < V_DISPLAY);
10章 テストパターン
9章で作成したモジュールをインクルードする。
`include "hvsync_generator.v"
新しいテストモジュールを作成する。モジュール名にtopを付けることでIDEがtopモジュールであると認識する。
module test_hvsync_top(clk, reset, hsync, vsync, rgb); input clk, reset; output hsync, vsync; output [2:0] rgb; wire display_on wire [8:0] hpos; wire [8:0] vpos;
前回同様のモジュールを作成する。hsyncとvsynの2つとrgbの3bitを出力する。
次にhvsync_generatorのインスタンスを生成する。
hvsync_generator hvsync_gen( .clk(clk), .reset(reset), .hsync(hsync), .vsync(vsync), .display_on(display_on), .hpos(hpos), .vpos(vpos) );
次にRGBを定義する。
wire r = display_on && (((hpos & 7)) == 0 ||| (vpos & 7) == 0); wire g = display_on && vpos[4]; wire b = display_on && hpos[4]; assign rgb = {b, g, r};
11章 Digit
ビデオゲームがやってくるまでは、ピンボールが王様で、プレーヤーのスコアを表示する必要があった。機械式のスコアリールが一般的だったが、多くの摩耗が見られた。
いくつかのゲームはネオンが充填されたガラスの内側に10本の数字のワイヤを重ね合わせたNixie tubeを使うものもあった。
7セグ Digit
1970年代から、特にLEDとLCDが広く利用可能になったときに、7セグメントディスプレイはハイテクと同義語になった。
我々が作る回路はseven_segment_decorderモジュールである。
これは4bitの数値を7bitの配列に変換し、各々のビットを異なるセグメントで表現する。
segments_to_bitmapモジュールは7ビット配列と現在のスキャンラインを取得し、それをスキャンライン用に出力されるビットを表す5ビット配列に変換する。
ここでは、7セグメントのビデオ生成方法について詳しく説明しませんが、参考文献で引用されたWilliam Arkushの本で詳しく説明されている。
ビットマップ化されたDigitsとROM
ビットマップフォントを使用する事で、我々は任意の形を表現する事が出来る。
各文字や数字のビットパターンはROMを使った2次元配列に保存される。一般的に1は明るいピクセルで0は暗いピクセルである。
ROMは変化しないビットパターンを保存するチップであり。本質的にルックアップテーブルである。
アドレスによってアクセスされる。
例えばROMが10bitのアドレスを持っていれば、210の1024種類のデータを提供可能である。
ほとんどのROMは複数のbitを同時に提供できる。例えば8bits ROMは各々のアドレスに8bitのデータを保存している。
VerilogはROMの場所を明確化しない。むしろ合成ツールはいくつかの異なる構成からROMを推測する。
ROMモジュールのインターフェースを示す。
module digits10_case(digit, yofs, bits); input [3:0] digit;//digit 0-9 input [2:0] yofs;//vertial offset. 0-4 output reg [4:0] bits; wire [6:0] address = {digit, yofs};
解説
ディスプレイに表示する数字の縦は5ピクセルとするので、yofsは0-4の5値が取れれうように、3bitとする。
次にそれぞれのアドレス固有の出力データを示す。
always @(*)begin case (address) //digit "0" 7'o00: bits = 5'b11111; // ***** 7'o01: bits = 5'b10001; // * * 7'o02: bits = 5'b10001; // * * 7'o03: bits = 5'b10001; // * * 7'o04: bits = 5'b11111; // ***** //digit "1" 7'o10: bits = 5'b01100; // ** 7'o11: bits = 5'b00100; // * 7'o12: bits = 5'b00100; // * 7'o13: bits = 5'b00100; // * 7'o14: bits = 5'b11111; // ***** // ... etc ... endcase end
caseステートメントでは8進数を使用していることに注意。 セレクターの1つは3ビットであるため、8進表記により少し読みやすくなる。
解説
addressの下3bitはyofsである。8進数表記した場合の1桁は3bitに相当するので、case文中のセレクタの下一桁はまさにyofsそのものを示している。
イニシャルブロック
initialキーワードを使用して配列を設定することで、このモジュールを作成することもできる。 caseステートメントの代わりに、assignステートメントを使用して配列にインデックスを付け、ビット出力を駆動する。
reg [4:0] bitarray[0:15][0:4]; assign bits = bitarray[digit][yofs]; initial begin //digit "0" bitarray[0][0] = 5'b11111; bitarray[0][1] = 5'b10001; bitarray[0][2] = 5'b10001; bitarray[0][3] = 5'b10001; bitarray[0][4] = 5'b11111; //digit "1" bitarray[1][0] = 5'b01100; bitarray[1][1] = 5'b00100; bitarray[1][2] = 5'b00100; bitarray[1][3] = 5'b00100; bitarray[1][4] = 5'b11111; // ... etc ... end
配列は2次元で、1次元が数字(16エントリ)、1次元がYオフセット(5エントリ)。したがって、各桁には5つのエントリがあり、各エントリは5ビット(reg [4:0]で示される)。
verilogではfor文も使えるけど、おすすめしない。
外部ファイル
Verilogでは外部ファイルを読み込むことができる。
initial begin $readmemb("ditigs5x5.txt", bitarray); end
ただし8bitworkshop IDEは外部ファイルの読み込みをサポートしていないので、ROMの初期化には上述のいずれかの方法を使用する。
テストモジュール
数字を表示するため、hvsync_generatorモジュールを使って簡単なテストモジュールを作ってみる。
wire [3:0] digit = hpos[7:4]; // selected digit 0-9 wire [2:0] xofs = hpos[3:1]; // horiz. offset (2x size) wire [2:0] yofs = vpos[3:1]; // vert. offset (2x size) wire [4:0] bits; //output bits from ROM digits10_array numbers( //ROM module .digit(digit), .yofs(yofs), .bits(bits) );
解説
wire [3:0] digit = hpos[7:4];
hposが16の倍数になるたびにdigitが1インクリメントされる。
wire [2:0] yofs = vpos[3:1];
垂直方向のオフセットvposの0,2,4,6,8, ...14を2で割ったもののいずれか。
ただしROMにはyofsは0-4までしか対応していない。つまりvposの0-8に相当する。
従ってwire [3:0] digit = hpos[7:4];
から16ピクセルごとに数字が変わるので、数字がダブルことはない。
wire r = 0; wire g = display_on && bits[xofs ^ 3'b111]; wire b = 0; assign rgb = {b,g,r};
解説
bits[xofs ^ 3'b111]
と上記のwire [2:0] xofs = hpos[3:1];
について、
bitsはROMからの出力。例えばアドレスがdigitが0でyofsが0のとき、bitsは5'b11111。
xofsは水平方向のオフセットhposの下4bitのデータ0,2,4,6,8, ...14を2で割ったもののいずれか。
bitsデータのMSb(先頭bitなのであえて小文字で記載)から順番にデータを取り出したいので、xofsを反転させている。
xofs ^ 3'b111はxofs[2:0]の全ビットを反転させたもの。ビットを反転させると数字の大小も反転することを利用している。
例えば、
hposが3'b000(=0)を反転させると3'b111(=7)
hposが3'b001(=1)を反転させると3'b110(=6)
hposが3'b010(=2)を反転させると3'b101(=5)
hposが3'b011(=3)を反転させると3'b100(=4)
VerilogでSPI通信
前置き
Verilogの勉強としてSPI通信のIFを作成する。
SPI通信は送受信が一つのクロックで同時に行われる。
実装が難しそうな気がするが頑張る。
SPIのプロトコル
通信線は全部で4本。
No | 名称 | 用途 |
---|---|---|
1 | SPICLK | クロック |
2 | CSn | チップセレクト、負論理 |
3 | MISO | マスターからスレーブへの信号 |
4 | MOSI | スレーブからマスターへの信号 |
チップセレクトが負論理。
MISO、MOSIは正論理。
ただしデータの取得タイミングはデバイスによって異なり、クロックの立上がりor立下りいずれかとなる。
加えてデフォルト状態でHかLかもデバイスにより異なる。
さらにLSb(bitなのであえて小文字) FirstかMSb FIrstかもデバイスによって頃なる。
従って、SPIの通信は8通りのプロトコル(モード)がある。
Mode0と2における受信信号とクロックのタイミングが謎。
実装の進め方
方針
今回はMode3かつ、MSb Firstに対応させる。
Mode3はSPICLKがidle時に0、立下りで信号送受信する。
クロックはパラメータで可変にする。
*いずれパラメーターの設定で各モード切り替えられるようにしたい。
手順
スタート信号を受信すると、送信データをバッファからコピーする。
信号とクロックの送信を開始する。
クロックをカウントする。
一定カウント毎にクロックを反転する。
一定カウント毎に信号を送信する。
一定カウント毎に受信信号を受信バッファに入力。
既定のデータ数の送受信後に受信バッファを出力用バッファに代入。
初期化。
クロックを反転する時間間隔はデータの送受信タイミングの2倍。
ソースコード
Verilog-HDL
module spi_msb( input CLK, input RST, input START, input[7:0] TXDATA, input RXSIG, output reg TXSIG = 0, output reg[7:0] RXDATA, output reg SPICLK = 1//Mode3 ); reg[7:0] txbuff = 8'b0; reg[7:0] rxbuff = 8'b0; reg spien = 0; reg[8:0] clkCnt = 9'b0; reg[3:0] dataCnt = 4'b0; parameter CLKFLAG = 9'd125;//CLK/(SPICLK*2) ex)50MHz/(200kHz*2) parameter DATAFLAG = 9'd250;//CLK/SPICLK ex)50MHz/200kHz*2 always@(posedge CLK or negedge RST)begin if(!RST)begin txbuff <= 8'b0; rxbuff <= 8'b0; spien <= 0; SPICLK <= 1;//Mode3 clkCnt <= 9'b0; end else begin if(spien)begin clkCnt <= clkCnt + 9'b1; if(clkCnt == CLKFLAG)begin SPICLK <= ~SPICLK; if(!SPICLK)begin rxbuff[7:0] <= {rxbuff[6:0],1'b0}; rxbuff[0] <= RXSIG; dataCnt <= dataCnt + 4'b1; end end else if(clkCnt == DATAFLAG)begin if(dataCnt < 4'b1000)begin clkCnt <= 9'b0; SPICLK <= ~SPICLK; TXSIG <= txbuff[7]; txbuff <= {txbuff[6:0], 1'b0}; end else begin RXDATA <= rxbuff; txbuff <= 8'b0; rxbuff <= 8'b0; spien <= 0; clkCnt <= 9'b0; dataCnt <= 4'b0; SPICLK <= 1;//Mode3 end end end else if(START)begin txbuff <= TXDATA; rxbuff <= 8'b0; spien <= 1; clkCnt <= 9'b0; dataCnt <= 4'b0; SPICLK <= 0;//Mode3 TXSIG <= txbuff[7]; txbuff <= {txbuff[6:0], 1'b0}; end end end endmodule
テストベンチ
`timescale 1 ns/ 100 ps module spi_msb_vlg_tst(); // constants // general purpose registers reg eachvec; // test vector input registers reg CLK; reg RST; reg RXSIG; reg START; reg [7:0] TXDATA; // wires wire [7:0] RXDATA; wire SPICLK; wire TXSIG; // assign statements (if any) spi_msb i1 ( // port map - connection between master ports and signals/registers .CLK(CLK), .RST(RST), .RXDATA(RXDATA), .RXSIG(RXSIG), .SPICLK(SPICLK), .START(START), .TXDATA(TXDATA), .TXSIG(TXSIG) ); always #10 CLK = ~CLK; initial begin CLK = 1; RST = 1; START = 0; TXDATA = 8'b01010011; RXSIG = 0; #200 START = 1; #100 RXSIG = 1; START = 0; #100 RXSIG = 1;//1bit #5000 RXSIG = 0;//2bit #5000 RXSIG = 0;//3bit #5000 RXSIG = 1;//4bit #5000 RXSIG = 1;//5bit #5000 RXSIG = 0;//6bit #5000 RXSIG = 0;//7bit #5000 RXSIG = 1;//8bit #5000 RXSIG = 0;//default $display("Running testbench"); end always begin @eachvec; end endmodule
気づき
if文を入れ子構造にしている場合でも、条件式を判定するタイミングは同じ。
if(条件1)begin 式1 if(条件2)begin 式2 end end
としている場合、条件1と条件2の判定は同じ同一のタイミングで行われる。
例えば以下において、SPICLK = 1
の時にclkCnt
が100になると、
SPICLK
は0になり、jikkou
は1になる(はず)。
if(clkCnt == 100)begin SPICK <= 0; if(SPICLK == 1)begin jikkou <= 1; end end
通常のプログラミング言語は上から順次演算されるが、HDLは条件式の中が同時に判定されるために、このような結果になる。
if... else...
の場合も、条件式は同時に判定される(はず)。
VerilogでUART送信
前置き
先日UARTの受信を作成した。今回は送信。
方針
START信号を検出する。
外部データをBuffに入力。
スタートビットを出力。
クロックをカウントし、ボーレートのタイミングで信号を1bitずつ送信する。
信号送信回数をカウントする。
9回目のカウントで、出力を終えて初期化。
ソースコード
Verilog-HDL
module uart_tx( input[7:0] UARTDATA, input CLK, input RST, input START, output reg UARTTX ); reg[7:0] uartTxBuff = 8'b0; reg[8:0] clkCnt = 9'b0; reg[3:0] dataCnt = 4'b0; reg sendEn = 0; parameter UARTTIMING = 9'd434;//CLK/Baudrate ex)5oMHz/115200 always@(posedge CLK or negedge RST)begin if(!RST)begin clkCnt <= 9'b0; dataCnt <= 4'b0; end else if(sendEn)begin if(clkCnt != UARTTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCnt == UARTTIMING clkCnt <= 9'b0; dataCnt <= dataCnt + 4'b1; UARTTX <= uartTxBuff[7]; uartTxBuff[7:0] <= {uartTxBuff[6:0], 1'b1};//"1" means stop bit if(dataCnt == 4'b1001)begin sendEn <= 0; clkCnt <= 9'b0; dataCnt <= 4'b0; end end end else if(START)begin sendEn <= 1; uartTxBuff[7:0] <= UARTDATA[7:0];//Copy Data UARTTX <= 0;//start bit end end endmodule
テストベンチ
`timescale 1 ns/ 100 ps module uart_tx_vlg_tst(); // constants // general purpose registers reg eachvec; // test vector input registers reg CLK; reg RST; reg START; reg [7:0] UARTDATA; // wires wire UARTTX; // assign statements (if any) uart_tx i1 ( // port map - connection between master ports and signals/registers .CLK(CLK), .RST(RST), .START(START), .UARTDATA(UARTDATA), .UARTTX(UARTTX) ); always #10 CLK = ~CLK; initial begin CLK = 0; RST = 1; START = 0; UARTDATA = 8'b0101_1100; #200 RST = 0; #200 RST = 1; #200 START = 1; #200 START = 0; #100 UARTDATA = 8'b1100_1001; #90000 START = 1; #500 START = 0; $display("Running testbench"); end always begin @eachvec; end endmodule
気づき
verilog-HDLのソースを変更すると、Modelsimを再度立ち上げ直さないと反映されない。
なにか方法あるのかも?
VerilogでUART受信
前置き
Verilogと開発ツールの使い方の勉強のため、Verilogであれこれ作成・シミュレーションしようと思う。
UARTの受信
通信仕様は
- データ8bit
- Parityなし
Stop1bit
今までこれ以外のUART通信をみた事がないので、拡張性も持たせない。 ボーレートはparameterで可変とする。 今回は115200bpsで作成。
大方針
スタートビットの検出方法
一般的なスタートビットの検出方法として、
UARTのボーレート用の逓倍のクロックを生成し、数回連続でLだったらスタートビットとみなす方法と、
UARTデータを基板の元クロックでモニターし、数回連続でLだったらスタートビットとみなす方法がある。
今回は後者で検出する。後者のメリットは。ボーレートが多少ずれても許容幅が大きいこと。
手順
- スタートビットを立下りのエッジで検出する
- スタートビットを検出したら、クロックのカウントを開始する。
- ボーレートの半周期だけ待つ。
- ボーレートの1周期毎にデータを取得する。
- 上記を9回(データ8bit + stop bit)繰り返す。
- 9個目のデータ(ストップビット)がHの場合にバッファにデータを送る。
- レジスタを初期化する。
ソースコード
Verilog-HDL
module uart_rx( input wire CLK, input wire RST, input wire UARTRX, output reg[7:0] UARTRXBUFF ); parameter UARTTIMING = 9'd434;//CLK/Baudrate ex)50MHz/115200 parameter WAITTIMING = 9'd217;//CLK/Baudrate/2 ex)50MHz/115200/2 reg[8:0] clkCnt = 9'b0; reg[8:0] rxData = 9'b0;//8bit + stop bit reg[3:0] dataCnt = 4'b0; reg[3:0] startEdge = 4'b1111; parameter WAITSTATE = 2'b01, RDSTATE = 2'b10, IDLESTATE = 2'b00; reg[1:0] state = 2'b00; //UART CLK always@(posedge CLK or negedge RST)begin if(!RST)begin clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end else begin if(state == IDLESTATE)begin startEdge[3:0] <= {startEdge[2:0], UARTRX}; if(startEdge == 4'b1000)begin state <= WAITSTATE; end end else if(state == WAITSTATE)begin if(clkCnt != WAITTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCounter == WAITTIMING clkCnt <= 9'b0; state <= RDSTATE; end end else if(state == RDSTATE)begin if(clkCnt != UARTTIMING)begin clkCnt <= clkCnt + 9'b1; end else begin//clkCounter == UARTTIMING clkCnt <= 9'b0; rxData[8:0] <= {rxData[7:0], UARTRX}; dataCnt <= dataCnt + 4'b1; end if(dataCnt == 4'b1001)begin if(rxData[0])begin UARTRXBUFF[7:0] <= rxData[8:1]; end clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end end else begin//state == OTHER clkCnt <= 9'b0; state <= IDLESTATE; rxData <= 9'b0; dataCnt <= 4'b0; startEdge <= 4'b1111; end end end endmodule
テストベンチ
`timescale 1 ns/ 100 ps module uart_rx_vlg_tst(); // constants // general purpose registers reg eachvec; // test vector input registers reg CLK; reg RST; reg UARTRX; // wires wire [7:0] UARTRXBUFF; // assign statements (if any) uart_rx i1 ( // port map - connection between master ports and signals/registers .CLK(CLK), .RST(RST), .UARTRX(UARTRX), .UARTRXBUFF(UARTRXBUFF) ); always #10 CLK = ~CLK; initial begin CLK = 0; RST = 1; UARTRX = 1; #100 RST = 0; #100 RST = 1; #1000 UARTRX = 0;//start #8680 UARTRX = 1;//1bit #8680 UARTRX = 0;//2bit #8680 UARTRX = 1;//3bit #8680 UARTRX = 1;//4bit #8680 UARTRX = 0;//5bit #8680 UARTRX = 0;//6bit #8680 UARTRX = 1;//7bit #8680 UARTRX = 0;//8bit #8680 UARTRX = 1;//stop #20000 UARTRX = 0;//start #8680 UARTRX = 1;//1bit #8680 UARTRX = 1;//2bit #8680 UARTRX = 1;//3bit #8680 UARTRX = 0;//4bit #8680 UARTRX = 0;//5bit #8680 UARTRX = 0;//6bit #8680 UARTRX = 1;//7bit #8680 UARTRX = 0;//8bit #8680 UARTRX = 1;//stop // --> end $display("Running testbench"); end always begin @eachvec; end endmodule
気づき
4bitのレジスタに3bit と1bitのレジスタを連結させ代入する場合、わざわざ4bitであることを明記しないとデータが正しく代入できなかった。
手元にある書籍には明確な記載はないので、別に原因があったのかも?
//正しい startEdge[3:0] <= {startEdge[2:0], UARTRX}; //間違い startEdge <= {startEdge[2:0], UARTRX};
テストベンチで、不要と思っていた以下の記述を削除すると、波形が表示されなかった。
always begin @eachvec; end
身についたこと
- parameterの使い方
- テストベンチの書き方
- シミュレーションの内部信号の表示方法
FPGAで画像処理
DE10-Nanoボードで画像処理
IntelのFPGAボードとイメージセンサ2つで、深度カメラを構築しようと思います。
大まかな手順は以下を想定。
No | To Do | How |
---|---|---|
1 | イメージセンサとFPGA間の制御信号のIFを作成 | I2C, SCCB |
2 | FPGAとPC間の通信用IFを作成 | UART |
3 | イメージセンサの制御ソフトを作成 | ? |
4 | イメージセンサとFPGA間の画像データ信号のIFを作成 | ? |
5 | FPGAとPC or Display間の画像データ信号用IFを作成 | HDMI, VGA, USB |
6 | イメージセンサ2つのデータを処理するモジュールを作成 | ? |
部品の選定
FPGAボードはDE10-Nano-SoCを使用。理由は、現在手元にあるから。
MAX10の評価ボードもあるので、ひょっとしたらMAX10にするかも。
今回はverilogの勉強を兼ねているので、ARMもNIOSⅡも使わないで行けるところまで行く予定。
イメージセンサは何が適切か不明。なのでとりあえず秋月で安く売っていたOV7675http://akizukidenshi.com/catalog/g/gM-13201/を使用予定。
SCCB(I2C)IFの作成(作成中)
SCCBはオムニビジョン社のイメージセンサの制御によく用いられるIF。基本はI2Cのはず。
I2Cは昔ながらのIFなのでFPGAでも簡単に記述できるでしょう
という考えはちょっと甘く、I2Cはデバイスによって微妙に動作が異なる。
各デバイスに応じてverilogの記述を変える必要がある。
Intel提供のI2CのIPはコードが膨大。
QsysでI2C搭載してHDL生成したら、verilogファイルがわんさか出てきた・・・
UART IFの作成(作成中)
I2Cとは打って変わって、UARTは楽。
QsysでUARTのIP搭載してHDL生成しても、ソースコード1つしか出てこない。
920行もあるけど、それはハードウェア言語の宿命。
イメージセンサの制御ソフトを作成(作成中)
FPGAとPCが通信できたので、PCからFPGAのI2C IFを制御するためのソフトを作成。
主要なレジスタの設定だけできるようにする。
FPGA内部でも、PCから送られるUARTのデータとI2Cのデータを連携させる必要がある。
C言語なら簡単なのにと思いながらもverilogでせっせと作成。
イメージセンサとFPGA間の画像データ信号のIFを作成(作成中)
イメージセンサから送信される画像データは、D0-D8のデータをVSYC、HSYNCによって適切なレジスタに格納する。
難しいかと思ったが、他のIFより簡単かも。
FPGAとPC or Display間の画像データ信号用IFを作成(作成中)
HDMI、VGA、USBと方法は色々あれど、どのIFを使うにしても方法が謎。
ちょこちょこ勉強するしかない。
イメージセンサ2つのデータを処理するモジュールを作成(作成中)
トラ技かInterfaceにそれっぽい記事があったので、なんとかなると楽観視している。