モノ創りで国造りを

ハード/ソフト問わず知見をまとめてます

Verilogでゲームを作る --Designing Video Game Hardware in Verilogの簡易和訳-- 目次

Verilogビデオゲーム

Verilogの参考書を探してるとアマゾンで以下の本に出会った。

Designing Video Game Hardware in Verilog

Designing Video Game Hardware in Verilog

プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
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

Designing Video Game Hardware in Verilog

プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
Verilogでは同様のコンセプトの書籍が見つからなかったので、さっそく購入。
まだ全部読んでいないため、良書かどうかの判断はできないが、
せっかくなので、重要な箇所だけかいつまんで和訳しようと思う。
全部翻訳すると著作権に抵触するらしいので。
なるべく最後まで完結できるように頑張る。

目次はこちら

yuji2yuji.hatenablog.com

12章 動くボール

アニメーション表示の最初の電子ゲームの一つは1958年にBrookhaven National Laboratoryで開発されたTennis of Twoである。
f:id:yuji2yuji:20190819110322p:plain

このアイデアは、いかにして跳ねるボールをオシロスコープ上に表示するかというアナログコンピューターの参考書に例示された回路から生まれた。

後に、Magnavox Odysseyコンソールがタ級をtVスクリーンに映し出した、Pongがアーケードとして誕生した。
f:id:yuji2yuji:20190819110647p:plain
プレーヤーはノブを回転させスクリーン上のパドルを動かし、スクリーン上の動いている四角い玉を跳ね返す。

絶対座標

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信号である。
f:id:yuji2yuji:20190819113053p:plain

より大きなボールを作りたい場合、イコールではなく比較を行う必要がある。
ボール位置をビーム位置から引き、両者の値をボールサイズと比較する。

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章 スイッチとパドル

ほぼ回路の説明なので省略。
スイッチとパドルの回路の模式図は以下。
f:id:yuji2yuji:20190820082317p:plain
f:id:yuji2yuji:20190820082407p:plain

Verilogでゲームを作る --Designing Video Game Hardware in Verilogの簡易和訳-- part1

Verilogビデオゲーム

Verilogの参考書を探してるとアマゾンで以下の本に出会った。

Designing Video Game Hardware in Verilog

Designing Video Game Hardware in Verilog

プログラミング言語の入門書の多くに、ゲーム作製をテーマにしているものがある。
Verilogでは同様のコンセプトの書籍が見つからなかったので、さっそく購入。
まだ全部読んでいないため、良書かどうかの判断はできないが、
せっかくなので、重要な箇所だけかいつまんで和訳しようと思う。
全部翻訳すると著作権に抵触するらしいので。
なるべく最後まで完結できるように頑張る。

目次はこちら

yuji2yuji.hatenablog.com

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の基本文法の説明なので省略。以下の表だけ頭に入れておけば十分。
f:id:yuji2yuji:20190818003243p:plain

6章 IDE

The 8bitworkshop IDEなるものがあることの紹介なので省略。
実際に画面に表示するためのシミュレーターも含まれている。実機ない人でもデモできる。
8bitworkshop

7章 シンプルなクロック分配器

最初のVerilogモジュールとして、クロック分配器を作ろう。
クロック分配器はクロック信号の入力を受けて、整数で分割されたクロックを生成する。
例えば、10MHzのクロックを2で分割して5MHzのクロックを生成する。

一般的に水晶発振子はマスタークロックを生成する。クロック分配回路はシグナルを方形波にし、回路の様々な目的に合わせた低い周波数を生成する。
以下にクロック分配器のタイミングダイアグラムを示す。

f:id:yuji2yuji:20190818004318p:plain

フリップフロップを直列に接続して回路を実現する。

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;

この方法は同期回路になっていないので、常に正しいといえる方法ではない。
本書のデモではこれを使用する。
回路図は以下となる。
f:id:yuji2yuji:20190818004843p:plain

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では左から右へ移動するスキャンする電子銃が各ピクセルの明暗を作る。
f:id:yuji2yuji:20190818084721p:plain
同期シグナルはCRTのスキャン信号のストップとスタートを決める。同期シグナルは2つある。
水平同期(horizontal sync)は新しいスキャンラインを生成する。
vertical sync(垂直同期)は新しいfieldを生成する、つまり電子ビームを左上に移動させる。

8bitworkshop IDEにはリアルタイムで信号を表示するCRTシミュレーションがある。
CRTシミュレーションのディスプレイは256x240のピクセルで、各ピクセルはクロック1サイクルの幅を持つ。
最初に、水平と垂直の同期信号を生成するモジュールを作成する。
f:id:yuji2yuji:20190818085409p:plain

同期信号発生器は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章 テストパターン

f:id:yuji2yuji:20190818094816p:plain 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を使うものもあった。
f:id:yuji2yuji:20190818160830p:plain

7セグ Digit

f:id:yuji2yuji:20190818160920p:plain
1970年代から、特にLEDとLCDが広く利用可能になったときに、7セグメントディスプレイはハイテクと同義語になった。

我々が作る回路はseven_segment_decorderモジュールである。
これは4bitの数値を7bitの配列に変換し、各々のビットを異なるセグメントで表現する。
segments_to_bitmapモジュールは7ビット配列と現在のスキャンラインを取得し、それをスキャンライン用に出力されるビットを表す5ビット配列に変換する。
f:id:yuji2yuji:20190818161709p:plain
ここでは、7セグメントのビデオ生成方法について詳しく説明しませんが、参考文献で引用されたWilliam Arkushの本で詳しく説明されている。

ビットマップ化されたDigitsとROM

ビットマップフォントを使用する事で、我々は任意の形を表現する事が出来る。
各文字や数字のビットパターンはROMを使った2次元配列に保存される。一般的に1は明るいピクセルで0は暗いピクセルである。
ROMは変化しないビットパターンを保存するチップであり。本質的にルックアップテーブルである。
アドレスによってアクセスされる。
例えばROMが10bitのアドレスを持っていれば、210の1024種類のデータを提供可能である。
ほとんどのROMは複数のbitを同時に提供できる。例えば8bits ROMは各々のアドレスに8bitのデータを保存している。
VerilogはROMの場所を明確化しない。むしろ合成ツールはいくつかの異なる構成からROMを推測する。
f:id:yuji2yuji:20190818162657p:plain

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通りのプロトコル(モード)がある。

f:id:yuji2yuji:20190813171657p:plain

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. スタートビットを立下りのエッジで検出する
  2. スタートビットを検出したら、クロックのカウントを開始する。
  3. ボーレートの半周期だけ待つ。
  4. ボーレートの1周期毎にデータを取得する。
  5. 上記を9回(データ8bit + stop bit)繰り返す。
  6. 9個目のデータ(ストップビット)がHの場合にバッファにデータを送る。
  7. レジスタを初期化する。

ソースコード

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ボードで画像処理

IntelFPGAボードとイメージセンサ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ファイルがわんさか出てきた・・・
f:id:yuji2yuji:20190809143109p:plain

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を作成(作成中)

HDMIVGA、USBと方法は色々あれど、どのIFを使うにしても方法が謎。
ちょこちょこ勉強するしかない。

イメージセンサ2つのデータを処理するモジュールを作成(作成中)

トラ技かInterfaceにそれっぽい記事があったので、なんとかなると楽観視している。