3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

聖夜のキラキラ星をBSVで作ろう!

Last updated at Posted at 2025-12-23

はじめに

クリスマスアドベント企画として、Hardware Description Language (HDL) アドベントカレンダー24日目は、Bluespec SystemVerilog (BSV) でキラキラ聖夜を作ります。

グラフィックコントローラやVRAMを使用するので、インベーダーゲーム のリソースを再利用します。解像度は 256×256、色は原色 8 色と、だいたい 45 年前くらいのレトロ感です。

それでも、結果としては 8bit(どこが?)マシンとは思えないくらい立体感のある映像になりました。

……単にもとの画像に立体感があるだけなのですが。

機材

画像のロード

デコーダ関数

まず画像のロード関数です。元画像はこのサイトにあった解像度の高いロゴ画像を256x256に縮め、さらに色数を4色に落としました。ただ単純にパターンROMに画像を置いてVRAMに転送するだけではあまり面白くないので、RLE (run length encoding)を行いました。元画像は Python でエンコードしましたが、単純なベタ塗りパターンだったため、約 1/3 までデータ量を圧縮することができました。

以下にデコーダ部のBSVコードを示します。このデコーダはROMから$color, runlen_\text{high}, runlen_\text{low})$の4bit要素の三つ組みを読み取り、2次元座標$(x, y)$に書いていくものです。

// PROM アドレス(1 nibble 単位)
Reg#(PAddr_t)   rleAddr      <- mkReg(0);

// 展開先の相対座標 (0..255, 0..255)
Reg#(U8)        rleX         <- mkReg(0);
Reg#(U8)        rleY         <- mkReg(0);

// 残りピクセル数 256 * 256
Reg#(UInt#(16)) pixelsLeft   <- mkReg(0);

// 現在のランの残り長さ (1..256)
Reg#(UInt#(9))  runLen       <- mkReg(0);

// 色と長さ nibble
Reg#(Pattern_t) rleColor     <- mkReg(0);
Reg#(Pattern_t) rleLenHiNib  <- mkReg(0);
Reg#(Pattern_t) rleLenLoNib  <- mkReg(0);

// PROM から nibble(Pattern_t) を1つ読み取り、dst に入れ、rleAddr を 1 進める
function Stmt rleNextNibble(Reg#(Pattern_t) dst);
  return (seq
    action
      // PROM アドレスをセット
      p_addr <= rleAddr;
    endaction
    // read timing 調整が必要ならここに noAction を挟む
    noAction;
    action
      // nibble を取り込み
      dst     <= romdata;      // romdata : Pattern_t
      rleAddr <= rleAddr + 1;  // 次の nibble へ
    endaction
  endseq);
endfunction

// PROM 上で RLE が始まるニブルアドレス(行256の先頭なら 128*256)
`define RLE_START_ADDR 128*256
`define VRAM_WIDTH     256
`define VRAM_HEIGHT    256
   
function Stmt rleDecode_org();
  return (seq
    //------------------------------------------------
    // 初期化
    //------------------------------------------------
    action
      // RLE データは PROM の 256 行目から始まる前提
      rleAddr    <= fromInteger(`RLE_START_ADDR);
      // 出力先相対座標
      rleX       <= 0;
      rleY       <= 0;
      pixelsLeft <= fromInteger(`VRAM_WIDTH * `VRAM_HEIGHT -1);
      runLen     <= 0;
    endaction
    //------------------------------------------------
    // 全ピクセルを描き終えるまでループ
    //------------------------------------------------
    while (pixelsLeft != 0) seq
      // --- 新しいランを読み込む必要があるなら ---
      if (runLen == 0) seq
        // color
        rleNextNibble(rleColor);
        // len_hi
        rleNextNibble(rleLenHiNib);
        // len_lo
        rleNextNibble(rleLenLoNib);
        action
          UInt#(4) hi = unpack(rleLenHiNib);
          UInt#(4) lo = unpack(rleLenLoNib);
          // ラン長 L = 16*hi + lo + 1
          runLen <= extend(unpack({pack(hi), pack(lo)})) + 1;
        endaction
      endseq
      // --- このランから 1 ピクセル描画 ---
      setDot(rleX, rleY, rleColor);
      action
        pixelsLeft <= pixelsLeft - 1;
        runLen     <= runLen     - 1;
        // 256 幅で折り返し
        if (rleX == 8'd255) begin
          rleX <= 0;
          rleY <= rleY + 1;
        end
        else begin
          rleX <= rleX + 1;
        end
      endaction
    endseq   // while (pixelsLeft != 0)
  endseq);
endfunction
FSM rle_fsm <- mkFSM(rleDecode_org());

// rleDecodeを起動するラッパ
function Stmt rleDecode();
  return (seq
    `RUN_FSM(rle_fsm)
  endseq);
endfunction

上記最後の rleDecode 関数は、本体 rleDecode_org を起動するだけの薄いラッパです。これは HDL アドベントカレンダー 12 月 5 日の「巨大 FSM のダイエット計画」で紹介した方法で、重いシーケンスを小 FSM に切り出し、メインからはラッパを呼び出して終了を待つだけにするものです。これにより、メインシーケンス側の状態爆発を抑えられます。

さらに私は BSV の rule が苦手なので、HDL アドベントカレンダー 12 月 17 日の「Rule を用いない BSV の設計手法」で解説した手法を用いています。従って C のように while ループが書いてあるだけなので、読みやすいと思います。

キラキラ聖夜

星座標

星の座標は $(x, y)$ではなく$(x, y, P, \phi)$の4つ組で表します。周期 $P$、位相$\phi$を各星に持たせることで、ランダムに点滅しているように見せます。以下はそのテーブルの一部です。

// loaded 76 stars from stars_xy.csv
// ps(x, y, P, phi) 形式
  ps(  0,   0, 64, 19),
  ps(204,   1, 16,  7),
  ps( 63,   2, 16, 11),
:
  ps( 33, 251, 32,  8),
  ps(165, 254,  8,  0),
  ps(232, 254, 64, 33)

このテーブルの作成にあたっては、原画像の白点を星と見立て、その抽出から座標テーブル生成までを行う Python スクリプトを ChatGPT に作ってもらいました。

アルゴリズム

ランダムに点滅するアルゴリズムは

案3:座標から「周期と位相」を決めるだけの簡単版

擬似乱数すら使わず、各星に違う周期と位相を与えるだけでも、それなりに「ランダムっぽく」見えます。

  1. フレームカウンタ$t$を用意。

  2. 星の座標$(x,y)$から、その星固有の周期$P$と位相$\phi$を決める。
    例:
    $$
    P = 8 + ((x + 2y) \bmod 5) \quad (8 \sim 12; \text{フレーム周期})
    $$
    $$
    \phi = (3x + y) \bmod P
    $$

  3. 星は次の条件で ON にする。
    $$
    \text{star_on} = 1 \iff ((t + \phi) \bmod P) < \frac{P}{2}
    $$

  • つまり各星は周期$P$で点滅し、そのうち半分だけ点灯。
  • 星ごとに$P$と$\phi$が違うため、全体としてはかなりバラバラに光って見える。

ハード実装としては

  • $P$を事前に ROM に入れておくか、座標から組み合わせ回路で計算、
  • Pを2の冪にすれば、$\bmod P$の演算も不要

ご覧のとおり、このアルゴリズムも ChatGPT に教えてもらったものです。

聖夜表示関数

以下は、星の点滅を 1 フレーム分更新する BSV コードです。

// pack ∘ st のラッパ (packed star)
function Bit#(SizeOf#(StarTwinkle)) ps(U8 x, U8 y, U8 p, U8 phi);
  return pack(st(x, y, p, phi));
endfunction

// 星テーブル (packed ビット列)
Bit#(SizeOf#(Vector#(NumStars, StarTwinkle))) starTwinkleBits = {
// ps(x, y, P, phi) 形式
`include "stars_xy.vh"
};

// Vector#(NUM_STARS, StarTwinkle) に変換
Vector#(NumStars, StarTwinkle) starTwinkleTable =
  reverse(unpack(starTwinkleBits));

// 1フレーム分、星の点滅を更新する Stmt
//   count : グローバルフレームカウンタ (60Hzで+1される想定)
function Stmt stepStarField();
  let s = starTwinkleTable[i];
  let phaseIdx = (truncate(counter) + s.phase) % s.period;
  let on = (phaseIdx < (s.period >> 1));
  return (seq
    for (i <= 0; i < fromInteger(valueOf(NumStars)); i <= i + 1) seq
      if (on)
        setDot(s.x, s.y, 7);
      else
        setDot(s.x, s.y, 0);
    endseq
  endseq);
endfunction

4 つ組を配列にする部分は少し凝った書き方をしています。配列を並べて初期化したかったので、いったんビット列に pack しています。実際に使うときは、そのビット列に対してあらかじめ unpackreverse を行ったテーブルを普通に引くだけです。

完成

お待ちかねの完成結果をご紹介します。GIF だと圧縮の都合で少し粗く見えますが、実機ではもう少し滑らかにキラキラして見えます。

My Project_0.GIF

さいごに

今回は ChatGPT 5.1 に各種サポートプログラムを Python で作成してもらったほか、BSV についても 9 割方は Chat によるもので、私はほぼデバッグ係になっていました。

ChatGPT 5.1 は、慣れていない BSV では時々文法誤りのコードを吐くものの、Python については一度もバグを入れることなく動くコードを出してくれました。Python の方が得意なのは、世の中にあるコードベースの量を考えれば当然かもしれません。

これほど Python が得意なら、今後は人間が Python を書く機会はどんどん減り、Python は機械語のような位置付けになるかもしれません。昔は機械語のバイナリを覚えたものですが(C3,00,80,...)、それと同じような存在になりそうです。

幸いChatGPTは、BSVはそこまで得意ではないので、人間の書く楽しみがまだ辛うじて残されているのかもしれません。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?