Posts Tagged with "FPGA"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.

PL UARTの接続とPCでの表示

posted by sakurai on May 17, 2023 #605

PL部UARTのPCへの出力

以前Space Invadersのゲーム実行中のVRAM画面の吸出しを実施しました。過去記事ではこれ等が相当します。これは、メモリダンプモジュールをBSVにより設計し、UARTの送信機能を用いてUART/USB変換ボードを経由してPCとUSBで接続し、PC上のPuttyでメモリダンプ情報を取得するというものです。

ところが、再度実行しようとしたところ、どうしてもUARTの出力の取得ができませんでした。前回は比較的簡単にできてしまったので、記事にはpin設定であるxdcの詳細は書いていませんでした。単にUART_TXに接続するとのみと書いており、端子番号は今は定かではありません。

こうなると基本から調べなければならないので、まずUltra96V2の回路図を見ると、なんとPS部からの接続となっているようです。

図%%.1
図605.1 Ultra96 UART部分回路図

UART/USB変換ボードのJ1の2 pinにUART_TXが接続されていますが、これはPS_MIO0のUART1_TXであり、PLからはPIN配置でエラーになるため、PS部であるBank 500のU4端子に接続することはできません。図605.2にUltra96-V2ハードウェアユーザーズガイドの抜粋を示しますが、やはりBank 500(PS部)のU4端子となっています。

図%%.2
図605.2 Ultra96 UART部分端子表

従って、最後の手段としてボード上に配線をハンダ付けし無理やりJ1に出力することを考えます。

まず図605.3はPL部UART_TXの引き出しを示す回路図です。

図%%.3
図605.3 UART_TX部回路図

このUART_TXを次のxdcによりG6端子に割り当てます。これはPL部の出力(Bank 26)です。

set_property PACKAGE_PIN G6 [get_ports {UART_TX}]

最後にUART_TXを割り着けた端子G6とJ1の2 pinの間に配線をハンダ付けし、ショートしてやります。レベル的に本来はTrのD側ではなくS側に接続すべきですが、3.3Vにpull upされているため、これでもVOHは満足しているようです。

このようにすることで921,600bpsにより、VRAM内容の送信がうまく動作しました。が、以前設定だけでできた理由は不明のままです。

PCへの出力結果の確認

以下に、得られたlogファイルを画像に変換するフィルタを再掲します。

log2ppm.c

#include <stdio.h>
void main() {
      char line[4096];
      char ch;
      printf("P3\n256 256\n255\n");
      for(int y = 0; y <= 255; y++) {
            fgets(line, sizeof(line), stdin);
            for(int x = 0; x <= 255; x++) {
                  ch = line[x] - 0x30;
                  if ((ch & 0x4) != 0) printf("255 ");    // R
                  else printf("0 ");
                  if ((ch & 0x2) != 0) printf("255 ");    // G
                  else printf("0 ");
                  if ((ch & 0x1) != 0) printf("255 ");    // B
                  else printf("0 ");
            }
            printf("\n");
      }
}

以下のコマンドによりフィルタを作成します。

$ gcc -O log2ppm.c -o log2ppm

これを下記のようにフィルターとして実行し、ログデータを画像ファイルに変換します。

$ ./log2ppm <putty.log >putty.ppm

生成されたppm図形を図605.4に示します。

図%%.3
図605.4 メモリダンプ図形

左矢前のブログ 次のブログ右矢

posted by sakurai on December 2, 2022 #561

画面の縦横の入れ替えの変更

過去記事において、本来VRAMの読み出しアドレスのxとyを入れ替えるつもりが、たまたまwrite側のアドレスマルチプレクサが目につき、それを流用しました。が、動作的な欠点が目立ちました。具体的には、画面書き換えの途中で切り替えると、縦図形と横図形が混在したり、初期画面を動的に準備する負担が大きくなっていました。

これを本来のread側アドレスマルチプレクサに変更します。ただし、write側マルチプレクサに追加してread側マルチプレクサの増設が必要になります。

write側アドレスマルチプレクサは名前をWriteMuxに変更しましたが、元の単なるセレクタに戻しました。新たにReadMuxモジュールを設置し、読み出し側アドレスのxとyを入れ替えます。実際にはyはx(a[7:0])をそのまま用い、xはy(a[15:8])を256から引いたものを用います。

$$ \begin{eqnarray} \left\{ \begin{array}{l} x&\Leftarrow&256-y \\ y&\Leftarrow&x \end{array} \right. \end{eqnarray} $$

図561.1に改造後のブロック図を示します。

図%%.1
図561.1 VRAMモジュール

以下にBSVソースを示します。処理はwrite側で実施した入れ替えとほとんど同様です。

ReadMux.bsv:

// アドレス型の定義
typedef Bit#(16) Addr_t;

// マルチプレクサのインタフェース定義
interface ReadMux_ifc;
   (* prefix="" *)
   // 出力アドレスを生成するメソッド
   method Addr_t outp(Bool sel, Addr_t a);
endinterface

// マルチプレクサの実装
(* synthesize, always_ready = "outp", no_default_clock, no_default_reset *)
module mkReadMux(ReadMux_ifc);

   // 出力アドレスを生成するメソッド
   method Addr_t outp(Bool sel, Addr_t a);
      // xa: aの下位8ビット
      Bit#(8) xa = a[7:0];
      // yax: aの上位8ビットから算出
      Int#(10) yax = 256 - signExtend(unpack(a[15:8]));
      // ya: yaxを8ビットにトリミング
      Bit#(8) ya = truncate(pack(yax));

      // selがTrueなら加工したアドレスを返す
      if (sel) return {xa, ya};
      // selがFalseなら元のアドレスをそのまま返す
      else return a; 
   endmethod

endmodule

この変更により、ボード上のdip swを切り替えることで、リアルタイムに画面の縦横変換ができるようになりました。


左矢前のブログ 次のブログ右矢

posted by sakurai on September 27, 2022 #517

完成したゲームのオープニングからのゲーム開始画面です。動画変換フレームレートの関係で、ゼロの点滅がハッキリと再生されませんが、実際にはきれいに点滅しています。

図%%.1
図517.1 オープニングアニメーションシーケンス

実行のシーケンス

  • 得点表(Score Advance Table)アニメーション表示
  • Fボタンを押す
  • "PUSH ONLY 1PLAYER BUTTON"を表示、CREDIT=01
  • Sボタンを押す
  • "PLAY PLAYER<1>"を表示、CREDIT=00、SCORE<1>をゼロにし、規定回数点滅
  • ゲームスタート

図513.5
図513.5 ボタン配置図

左矢前のブログ 次のブログ右矢

posted by sakurai on September 26, 2022 #516

Y字リプレースアニメーションのソース

Y字リプレースアニメーションのソースを示します。Y字リプレースアニメーションもFボタンにより中断するため、各所でFボタンを見ています。

function Stmt replaceY;
   return (seq
      // from right to left
      for (i <= 228; i >= 142; i <= i - 2) seq
         copyArea((pack(i)[1] == 1'b1) ? 68 : 84 , 32, i, 67, 10, 8);
         wait_timer(`TICK_WAIT3);
         if (fbutton) break;
      endseq // for
      if (fbutton) break;
      // from left to right
      for (i <= 136; i <= 226; i <= i + 2) seq
         copyArea((pack(i)[1] == 1'b1) ? 75 : 91 , 107, i, 67, 16, 8);
         wait_timer(`TICK_WAIT3);
         if (fbutton) break;
      endseq // for
      eraseArea(226, 67, 16, 8);
      wait_timer(`TICK_WAIT32);
      if (fbutton) break;
     // from right to left
     for (i <= 226; i >= 136; i <= i - 2) seq
        copyArea((pack(i)[1] == 1'b1) ? 77 : 93 , 117, i, 67, 16, 8);
        wait_timer(`TICK_WAIT3);
        if (fbutton) break;
      endseq // for
      wait_timer(`TICK_WAIT32);
      if (fbutton) break;
      eraseArea(141, 67, 9, 8);
      wait_timer(`TICK_WAIT32);
      if (fbutton) break;
   endseq);
endfunction

これだけでなく、タイマールーチンの中でもFボタンによる中断を見ていますが、ちょっとやり過ぎのようです。実際には多少間引いても体感に影響しないと思います。


左矢前のブログ 次のブログ右矢

posted by sakurai on September 23, 2022 #515

オープニングアニメーションのソース

オープニングアニメーションのソースを示します。オープニングアニメーションはFボタン(コイン投入の模擬)により中断するため、各所でFボタンを見ています。

function Stmt openingAnimation;
   return (seq
      // Opening Animation
      foa <= True;
      eraseArea( 0, 41, 255, 199); // erase screen
      eraseArea(25,242, 5, 7); // erase zanki
      stringS1; // PLAY ...
      if (fbutton) break;
      wait_timer(`TICK_WAIT64);
      if (fbutton) break;
      stringS2; // *SCORE ...
      if (fbutton) break;
      wait_timer(`TICK_WAIT32);
      if (fbutton) break;
      stringS3; // =? MYSTERY ...
      if (fbutton) break;
      wait_timer(`TICK_WAIT64);
       if (fbutton) break;
      replaceY; // ^ -> Y
      if (fbutton) break;
      wait_timer(`TICK_WAIT64);
      if (fbutton) break;
      foa <= False;
   endseq);
endfunction

左矢前のブログ 次のブログ右矢

posted by sakurai on September 22, 2022 #514

オープニングアニメーションの追加

テスト用のソースを示します。コンパイル時間短縮のため、ゲーム部分をカットしています。

// メインフロー
Stmt main = seq
   while (True) seq
      while (!fbutton) seq
         openingAnimation; // Fボタンによりブレーク
      endseq // while
      openingDisplay;      // 表示のみ、ボタンを待たない
      await(sbutton);        // Sボタンによりブレーク
      openingDisplay2;    // タイマーによりブレーク
      // game start
   endseq // while
endseq;

// CREDIT 01, "PUSH ONLY 1PLAYER BUTTON"
function Stmt openingDisplay;
   return (seq
      eraseArea( 0, 41, 255, 199); // erase screen
      stringS5; // PUSH ONLY ...
      copyArea(10, 162, 217, 241, 5, 7); // CREDIT 00->01
   endseq);
endfunction

// CREDIT 00, "PLAY PLAYER<1>, 00000"
function Stmt openingDisplay2;
   return (seq
      eraseArea( 0, 41, 255, 199); // erase screen
      stringS6; // PLAY PLAYER<1>
      copyArea(2, 162, 217, 241, 5, 7); // CREDIT 01->00
      for (i <= 1; i < 15; i <= i + 1) seq
         // erase zero
         eraseArea(40, 25, 37, 7);
         wait_timer(`TICK_WAIT4); // wait 66.66msec
         stringS7; // 00000
         wait_timer(`TICK_WAIT4); // wait 66.66msec
      endseq // for
   endseq);
endfunction

左矢前のブログ 次のブログ右矢

posted by sakurai on September 21, 2022 #513

オープニングアニメーションの追加

この動画の最初の部分を参考にして、オープニングアニメーションを作成します。

  1. インベーダの種類や点数の紹介、と同時に逆さYを引っ張って行き正立Yに入れ替えます。アニメーションがメインなので、これをopeningAnimationシーケンスと呼び、同名の関数により実行します。Fボタンによりコイン投入を模擬します。
    図%%.1
    図513.1 openingAnimation画面
  2. コインを投入すると、"PUSH ONLY 1PLAYER BUTTON"と表示され、CREDITが+1されます。アニメーションは無いため、これをopeningDisplayシーケンスと呼び、同名の関数により実行します。Sボタンを待ちます。
    図%%.2
    図513.2 openingDisplay画面
  3. Sボタンを押すと、"PLAY PLAYER<1>"と表示され、CREDITが-1されます。同時に得点が"00000"となり、点滅します。これをopeningDisplay2シーケンスと呼び、同名の関数により実行します。
    図%%.3
    図513.3 openingDisplay画面1
    図%%.4
    図513.4 openingDisplay画面2
  4. ゼロ点滅を規定回数実行すると自動的にゲームを開始します。

図513.5にSボタンとFボタンの配置を示します。

図%%.5

図513.5 ボタン配置図

左矢前のブログ 次のブログ右矢

posted by sakurai on September 20, 2022 #512

テストベンチ

テストベンチは変わりません。

Tb.bsv

import StmtFSM::*;
import Uart::*;

(* synthesize *)
module mkTb();
      Uart_ifc uart <- mkUart();

      Stmt s = seq
            delay(8);
            uart.load(8'h55);
            uart.load(8'haa);
            uart.load(8'hc3);
            uart.load(8'h3c);
            await(uart.done());
            $finish;
      endseq;
      
      mkAutoFSM(s);

endmodule

Bsimシミュレーション

Bsimシミュレーションのコマンドは次のとおりです。

$ bsc -u -sim Tb.bsv; bsc -sim -e mkTb -o Tb.exec; ./Tb.exec -V bsim.vcd; gtkwave -A bsim.vcd

以下にBsimシミュレーション結果を示します。意外なことに明示的にdone信号を書いたにも関わらず、シミュレーションのダンプの中にdone信号がありませんでした。

図%%.1
図512.1 Bsimシミュレーション波形

テストベンチ内でレジスタにuart.doneを格納するようにしたら、インタフェースにuart.doneが現れました。awaitで使用するくらいでは削除され、レジスタに取って初めて残すようです。

図%%.2
図512.2 Bsimシミュレーション波形

Verilogシミュレーション

Verilogシミュレーションにおいては、モジュール(mkUart.v)、それをドライブするテストベンチ(mkTb.v)の上位に最上位(top.v)を配備します。これはクロックやリセットを供給するモジュールですが、Bsimの場合はシステムから暗黙にクロックやリセットが供給される一方、Verilogでは供給されないためです。

top.v

`timescale 1ns/1ns
module top();
      /*AUTOREGINPUT*/
      // Beginning of automatic reg inputs (for undeclared instantiated-module inputs)
      reg      CLK;        // To mkTb_inst of mkTb.v
      reg      RST_N;        // To mkTb_inst of mkTb.v
      // End of automatics
      /*AUTOWIRE*/
      mkTb mkTb_inst(/*AUTOINST*/
      // Inputs
      .CLK    (CLK),
      .RST_N    (RST_N));

      initial begin
            RST_N = 1'b0;
            #10;
            RST_N = 1'b1;
      end
      initial begin
            CLK = 1'b0;
            forever begin
        #5 CLK = ~CLK;
            end
      end
      initial begin
            \$dumpfile("verilog.vcd");
            \$dumpvars;
      end
endmodule // top

Verilogシミュレーションのコマンドは次のとおりです。

$ bsc -u -verilog Tb.bsv; iverilog top.v mkTb.v mkUart.v -o ./mkTb.exev; ./mkTb.exev; gtkwave -A verilog.gtkw

Verilogシミュレーションのほうには当然ですが、uart.done信号が存在します。

図%%.3
図512.3 Verilogシミュレーション波形

左矢前のブログ 次のブログ右矢

posted by sakurai on September 19, 2022 #511

UARTの改良点

過去記事においてUARTを設計しましたが、見直したところ改良点が見つかりました。 改良点はdoneフラグの生成です。実はハンドシェークは自動的にenableとreadyの2線で行われるので、doneが無くても良いのですが、test benchで終了を知りたい場合には必要です。

Uart.bsv

import StmtFSM::*;

interface Uart_ifc;
      method Bit#(1) read();
      method Action load(Bit#(8) newdata);
      method Bool done();
endinterface

(* synthesize, always_ready="read, done" *)
module mkUart(Uart_ifc);
      Reg#(Bit#(8)) data <- mkRegU;
      Reg#(Bit#(1)) odata <- mkReg(1'h1); // stop bit

      Stmt s= seq
            odata <= 1'h0; // start bit
            repeat (8) action
                  odata <= data[0];
                  data <= (data >> 1);
            endaction
            odata <= 1'h1; // stop bit
      endseq;

      FSM fsm <- mkFSM(s);

      method Bit#(1) read();
            return odata;
      endmethod
      method Bool done();
            return fsm.done();
      endmethod
      method Action load(Bit#(8) newdata);
            action
                  data <= newdata;
                  fsm.start();
            endaction
      endmethod
endmodule

この記述のように、従来設けてあったレジスタのdoneフラグを削除し、ステートマシンのdoneを上位に返すことで実現します。さらに、インタフェースのreadとdoneは常にreadyであるため、それらのready信号は不要なので削除しています。


左矢前のブログ 次のブログ右矢

posted by sakurai on September 16, 2022 #510

Graphic Controlerの再設計

完成したIP diagramを510.1に示します。3個のサブモジュールをまとめたので、graphic階層はなくなりました。

図%%.1
図510.1 BSVによるグラフィックコントローラ

BSVソース

GraphicFSM.bsv:

// グラフィックディスプレイコントローラー、BSVによる実装
import StmtFSM::*;
// 各種タイミングパラメータの定義
`define HD  800  // 水平解像度
`define HFP 16   // 水平フロントポーチ
`define HSP 80   // 水平同期期間
`define HBP 160  // 水平バックポーチ
`define HO  `HFP + `HSP + `HBP  // 水平オフセット
`define HL  `HD + `HO  // 一行当たりのピクセル数

`define VD  600  // 垂直解像度
`define VFP 1    // 垂直フロントポーチ
`define VSP 3    // 垂直同期期間
`define VBP 21   // 垂直バックポーチ
`define VO  `VFP + `VSP + `VBP  // 垂直オフセット
`define VL  `VD + `VO  // 一画面当たりの行数

// その他の定義
`define EHD 512  // 有効水平解像度
`define EVD 512  // 有効垂直解像度

// ログ幅の定義
`define HW  11  // log2(1056) = 10.04439
`define VW  10  // log2(628) = 9.294621

// アドレス型の定義
typedef Bit#(16) Addr_t;

// インターフェースの定義
interface GraphicFSM_ifc;
   method Bool xhs();      // 水平同期信号出力
   method Bool xvs();      // 垂直同期信号出力
   method Addr_t address();  // VRAMアドレス出力
   (* prefix="" *)
   method Action idata(Bit#(4) indata); // VRAMデータ入力
   (* prefix="" *)
   method Action expl(Bool exp);  // 爆発入力
   method Bit#(1) rd();  // 赤出力
   method Bit#(1) gd(); // 緑出力
   method Bit#(1) bd();  // 青出力
endinterface

// モジュールの定義
(* synthesize,always_ready,always_enabled *)
module mkGraphicFSM(GraphicFSM_ifc);

   Reg#(UInt#(`HW)) x <- mkRegU;     // 水平方向のカウンタ
   Reg#(UInt#(`VW)) y <- mkRegU;     // 垂直方向のカウンタ
   Reg#(Bool) in_xhs <- mkReg(False),   // 水平同期信号フラグ
             in_xvs <- mkReg(False),  // 垂直同期信号フラグ
              in_hdt <- mkReg(False),
              in_vdt <- mkReg(False);
   UInt#(`HW) ehoff = (`HD-`EHD)/2;
   UInt#(`VW) evoff = (`VD-`EVD)/2;
   Reg#(Bit#(4)) in_data <-mkRegU;  // VRAMデータ
   Reg#(Bool) in_exp <- mkReg(False); // 爆発フラグ

// メインループの定義
   Stmt main = seq
      while(True) seq
//       for (y <= 0; y < `VL; y <= y+1) seq
         y <= 0;
         while (y < `VL) seq
//          for (x <= 0; x < `HL; x <= x+1) action  --- for consumes two cycles, then we like to use while
            x <= 0;
            while (x < `HL) action
               if (((`HD+`HFP)<=x)&&(x<(`HD+`HFP+`HSP))) in_xhs <= False;
               else in_xhs <= True;
               if ((ehoff<=x)&&(x<ehoff+`EHD)) in_hdt <= True;
               else in_hdt <= False;
               x <= x + 1;
               if (((`VD+`VFP)<=y)&&(y<(`VD+`VFP+`VSP))) in_xvs <= False;
               else in_xvs <= True;
               if ((evoff<=y)&&(y<evoff+`EVD)) in_vdt <= True;
               else in_vdt <= False;
            endaction // for -> while
            y <= y + 1;
         endseq // for -> while
         $display("%3d %3d", y, x);
      endseq // while(True)
   endseq; // Stmt       // xから水平オフセット(ehoff)を減算してパックし、右に1ビットシフトする。
   Bit#(`HW)xx = pack(x-ehoff)>>1;

   // yから垂直オフセット(evoff)を減算してパックし、右に1ビットシフトする。
   Bit#(`VW)yy = pack(y-evoff)>>1;
    // xxとyyを8ビットに切り詰める。
   Bit#(8)xxx = truncate(xx);
   Bit#(8)yyy = truncate(yy);
   // xxxとyyyを合成して16ビットのアドレスを作成。
   Bit#(16) in_addr = {yyy, xxx};
   // 水平データタイミング(in_hdt)と垂直データタイミング(in_vdt)をANDで合成。
   Bool in_dt = in_hdt && in_vdt;
   // 爆発フラグ(in_exp)に基づいて赤色成分のデータを処理。
   Bit#(1) in_rd = !in_exp ? in_data[2] & pack(in_dt) : (in_data[2] | in_data[1] | in_data[0]) & pack(in_dt);
   // 爆発フラグ(in_exp)に基づいて緑色成分のデータを処理。
   Bit#(1) in_gd = !in_exp ? in_data[1] & pack(in_dt) : 1'b0;
   // 爆発フラグ(in_exp)に基づいて青色成分のデータを処理。
   Bit#(1) in_bd = !in_exp ? in_data[0] & pack(in_dt) : 1'b0;

   // ステートマシン生成
   mkAutoFSM(main);

   // メソッド定義
   method Bool xhs();
      return in_xhs;
   endmethod
   method Bool xvs();
      return in_xvs;
   endmethod
   method Addr_t address();
      return in_addr;
   endmethod
   method Action idata(Bit#(4) indata);
      in_data <= indata;
   endmethod
   method Action expl(Bool exp);
      in_exp <= exp;
   endmethod
   method Bit#(1) rd();
      return in_rd;
   endmethod
   method Bit#(1) gd();
      return in_gd;
   endmethod
   method Bit#(1) bd();
      return in_bd;
   endmethod

endmodule: mkGraphicFSM
  • actionからendactionまでは1サイクル実行です。
  • verilogと同様、"<="はノンブロッキング代入でDFFが、"="はブロッキング代入で組み合わせ回路がそれぞれ生成されます。

当初、水平のオフセットを表す定数EHOFFは、上記のような

UInt#(`HW) ehoff = (`HD-`EHD)/2;

という変数ではなく、define文により

`define EHOFF      (`HD-`EHD)/2

のように定義していたのですが、defineの中でカッコや乗除算は使用できないようなので、変数としました。

ところが、生成されたVerilogを確認したところ、bscの最適化により定数となっており、レジスタは存在しませんでした。結論として、マクロで定数定義してもレジスタ宣言しても、オーバヘッドは変わりません。


左矢前のブログ 次のブログ右矢


ページ: