<<前回のプロジェクト(生波形を表示するまで)

 マイクで取った音声データをFFTして周波数ごとの成分を得ます.生波形の上にビンをオーバレイで表示します.

 FPGAはMAX10,IDEはVSCodeを使います.Copilot AgentがOllama経由でdeepseek (v4.1-flash) を使います.Copilot AgentはローカルPC内のQuartus Prime Lite をCLIで使います.

 指示するだけで,RTLコードの生成,Questaによるシミュレーション,Quartusでの合成まで行われ,動作するコードが手に入ります(正しく動くとは限らない).

 まさかのWindows10でやります.

110Hz(聞こえない),220Hz,440Hz,880Hz,1760Hz,3520Hz,7040Hz

●作るもの仕様

サンプリングレート:約20kHz

FFTするレンジ:0-10kHz

ビンの数:64

VSCode内のCopilot Agentに指示すると動くデザインが出来上がる

▲readme.md

# microphone - 音声波形 + スペクトラム LCD 表示 (Quartus Prime / MAX 10)

PMOD-Microphone v1.0 で音声を取得し、その波形を PMOD-TFTLCD v1.1
(ILI9341, 320x240) に表示する
**128 点 FFT** で周波数ごとの値を求め、棒グラフを波形に重ねて表示します。

## 構成

```
microphone/
├── microphone.qpf          Quartus Prime プロジェクト
├── microphone.qsf          ピン配置・ファイル登録
├── microphone.sdc          タイミング制約
├── tools/
│   └── gen_tables.ps1      FFT 用 ROM テーブルの生成スクリプト
├── rtl/
│   ├── top.sv              トップモジュール
│   ├── i2s_rx.sv           I2S マイク受信 (FPGA がマスタ)
│   ├── moving_avg.sv       移動平均フィルタ (ノイズ低減)
│   ├── audio_buf.sv        波形表示用デュアルポート RAM
│   ├── fft128.sv           128 点 FFT + パワースペクトル
│   ├── fft_tables.svh      FFT 用 ROM テーブル (自動生成)
│   ├── spectrum.sv         スペクトル -> バーの高さ (log2 圧縮)
│   ├── sw_debounce.sv      タクトスイッチのチャタリング除去
│   └── lcd_wave.sv         ILI9341 SPI 制御 + 波形 / 棒グラフ描画
└── sim/
    ├── tb_i2s_rx.sv        I2S 受信のテストベンチ
    ├── tb_lcd_ctrl.sv      LCD 制御のテストベンチ
    ├── tb_moving_avg.sv    移動平均フィルタのテストベンチ
    ├── tb_fft128.sv        FFT のテストベンチ
    ├── tb_spectrum.sv      スペクトラム変換のテストベンチ
    ├── tb_sw_debounce.sv   スイッチ入力のテストベンチ
    ├── tb_top_sw.sv        top のスイッチ -> 時間軸パスのテストベンチ
    └── run_sim.bat         Questa 実行スクリプト
```

## ハードウェア

| 項目 | 内容 |
|------|------|
| FPGA | 10M08SCE144C8G (MAX 10) |
| マイク | PMOD-Microphone v1.0 (I2S MEMS マイク) |
| LCD | PMOD-TFTLCD v1.1 (ILI9341, 320x240, 4-wire SPI) |
| クロック | 50 MHz (PIN_88) |
| リセット | ボタン PIN_17 (押下 = Low) |

## ピン配置

### PMOD 1 : Microphone

| FPGA | 信号 | 説明 |
|------|------|------|
| 47 | `mic_sck` | ビットクロック (FPGA → MIC) |
| 45 | `mic_ws` | ワードセレクト / LRCLK (FPGA → MIC) |
| 39 | `mic_sd` | シリアルデータ (MIC → FPGA) |

### PMOD 2 : LCD

| FPGA | 信号 | 説明 |
|------|------|------|
| 81 | `lcd_cs` | チップセレクト (Low 有効) |
| 77 | `lcd_dc` | Data/Command (RS) |
| 78 | `lcd_mosi` | SPI MOSI |
| 75 | `lcd_sck` | SPI クロック |

### LED

| FPGA | 信号 | 表示内容 |
|------|------|----------|
| 85 | `led` | フレーム描画ごとにトグル |
| 123 | `led0` | マイク入力検出中に点灯 |
| 122 | `led1` | サンプル受信ごとにトグル |
| 121 | `led2` | FFT フレーム完了ごとにトグル |
| 120 | `led3` | LCD 初期化完了で点灯 |

### タクトスイッチ

| FPGA | 信号 | 機能 |
|------|------|------|
| 62 | `sw_inc` | 波形の時間軸を 1 段上げる |
| 48 | `sw_dec` | 波形の時間軸を 1 段下げる |
| 17 | `btn_rst` | リセット (押下 = Low) |

## 動作

### I2S マイク受信 (`i2s_rx.sv`)

FPGA が I2S マスタとして動作します。

- ビットクロック `SCK` = 1.28 MHz (`CLK_HZ / SCK_HZ ≒ 39`)
- 1 フレーム = 64 SCK (32bit × 2ch) の Philips I2S フォーマット
- サンプリング周波数 `Fs` = 1.28 MHz / 64 = **20.031 kHz**
- WS 変化の 1 SCK 後に MSB が現れるため、`bit_pos = 1..24`
  24bit を取り込み、上位 16bit (符号付き) を出力

### 移動平均フィルタ (`moving_avg.sv`)

受信したサンプルに 8 サンプルの単純移動平均をかけてノイズを抑えます。

- ランニングサム方式のため**乗算器は不要** (加算 2 回のみ)
- `Fs = 20.03 kHz` / `N = 8` のとき、-3dB 遮断周波数は約 1.4 kHz
- ノイズエネルギーは理論上 **1/N = 1/8 (12.5%)** に低減
  (実測 13.7% で理論値とよく一致)

> 移動平均は**波形表示にのみ**使います。FFT には生サンプルを入力します
> (N = 8 では 8 kHz 以上の成分が落ちてしまうため)。

### 波形表示 (`audio_buf.sv` / `lcd_wave.sv`)

- 4096 サンプルのリングバッファにフィルタ後のデータを書き込む
- LCD フレーム開始時に書き込みポインタをラッチし、
  直近 320 サンプルを左から右へ 1 サンプル 1 列で描画
- 振幅は `AMP_GAIN_SHIFT` で調整可能 (現在 = `-1`**1/2**)

#### 振幅スケール `AMP_GAIN_SHIFT`

2 の冪で指定します (符号付き)。

| `AMP_GAIN_SHIFT` | 倍率 | 動作 |
|------------------|------|------|
| `+2` | ×4 | 左シフト |
| `+1` | ×2 | 左シフト |
| `0` | ×1 | 等倍 |
| **`-1`** | **×1/2** | **右シフト (現在)** |
| `-2` | ×1/4 | 右シフト |

```
samp_off = (rdata << AMP_LSH) >>> AMP_RSH
   AMP_LSH = (AMP_GAIN_SHIFT > 0) ?  AMP_GAIN_SHIFT : 0
   AMP_RSH = (AMP_GAIN_SHIFT < 0) ? -AMP_GAIN_SHIFT : 0
```

### スペクトラム表示 (`fft128.sv` / `spectrum.sv`)

周波数ごとの値を棒グラフで波形に重ねて表示します。

```
 moving_avg ──┬─► audio_buf ──► lcd_wave (波形)

              └─► fft128 ──► spectrum ──► lcd_wave (棒グラフ)
```

#### `fft128.sv` : 128 点 FFT

- 基数 2 / DIT (Decimation-In-Time) の in-place バタフライを 7 段
- 入力は 16bit 符号付き (Q0)、Hann 窓 (Q15) を内部で適用
- 回転因子は Q14 の ROM テーブル (`fft_tables.svh`)
- 内部データは 24bit 符号付き (バタフライで 7bit 成長するため)
- 出力は `Re² + Im²` = 34bit のパワースペクトル
- 入力は ping-pong バッファに取り込むため、FFT 実行中も
  サンプルを取りこぼさない

周波数レンジ:

| 項目 | 値 |
|------|-----|
| サンプリング周波数 `Fs` | **20.03 kHz** (`SCK = 1.28 MHz / 64`) |
| ナイキスト周波数 | 10.016 kHz |
| FFT 入力の間引き `DEC_N` | 1 (間引きなし) |
| FFT 入力周波数 `Fse` | 20.03 kHz |
| ポイント数 `N` | 128 |
| 分解能 `df = Fse / N` | **156.5 Hz / bin** |
| **表示範囲** | **bin 0 ~ 63 = 0 ~ 10.0 kHz** |

`Fs` は I2S の `SCK` から `Fs = SCK / 64` で決まります。
0~10 kHz をカバーするため `top.sv``SCK_HZ` を 1 MHz → **1.28 MHz**
に上げています (`Fs = 20.031 kHz``df = 156.5 Hz`)。


> **FFT には移動平均を通さない生サンプルを入力** しています。
> `moving_avg` (N = 8) は `Fs/N` 付近から減衰し始めるため、
> `Fs = 20 kHz` では 8 kHz 以上の成分が落ちてしまうからです。
> 移動平均は波形表示にのみ使います。

#### `spectrum.sv` : 対数圧縮とバーの高さ

パワーは数十 dB のダイナミックレンジがあるため、
そのまま高さにすると弱い成分が潰れます。
最上位ビット位置と 7bit の仮数から `log2` を求め、
`LOG2_LUT` を使って対数圧縮します。

- `q4 = exp*16 + 16*log2(1 + mant/128)`  (log2 の 16 倍 = 0.376 dB/単位)
- しきい値 `LOG_MIN` (既定 = 128 → `mag2 = 256`) 以下はバー高さ 0
- ゲインは `LOG_MUL / 2^LOG_SH` [px / octave]
- **ピークホールド**: 前フレームより小さい値は `DECAY` px/フレーム
  ずつしか減らさない (`DECAY = 0` で最大値保持)


| 項目 | 従来 | 現在 |
|------|------|------|
| `LOG_MUL` | 11 | **22** |
| `LOG_SH` | 4 | 4 |
| ゲイン | 0.69 px / octave | **2.06 px / octave** |
| 1 octave (6.02 dB) あたり | 11 px | **33 px** |

実測の圧縮特性 (1 octave ずつ、`LOG_MUL = 22`, `LOG_MIN = 128`):

#### 棒グラフのジオメトリ (`lcd_wave.sv`)

| 項目 | 既定値 | 説明 |
|------|--------|------|
| `BAR_EN` | 1 | 棒グラフを描く |
| `BAR_N` | 64 | 本数 (= FFT の全 bin) |
| `BAR_W` | 4 | 1 本の横幅 (px) |
| **`BAR_GAIN_SH`** | 1 | **表示高さの左シフト量 (1 で 2 倍、0 で等倍)** |
| `BAR_OVER_WAVE` | 1 | バーを波形より手前に描く |
| 色 | `COL_BAR` = `16'hFD20` | オレンジ |

ピッチ = `320 / 64 = 5 px`、横幅 4 px なので 1 px の隙間が空き、
64 本で **0 ~ 10 kHz** をカバーします。
画面下部 (下辺 = `LCD_H-1`) から表示高さ分上まで描画します。
bin 0 (DC) は Hann 窓の漏れで常に最大になるため `START_BIN = 1` で除外し、
左端の 1 本 (bin 0) だけ空きになります。

**表示高さの増幅:** `bar_data` (内部の高さ) を `BAR_GAIN_SH` ビット
左シフトして表示します。既定は `BAR_GAIN_SH = 1` なので**内部値の 2 倍の
高さ**で描画します。画面高を超えた分は下辺でクリップされ、その列は
下辺から上端まで塗り潰されます。
内部の高さは 9 bit (`logic [8:0] h`) で計算しているため、
`y + h >= BAR_BOT` の引き算がアンダーフローしません。

#### 描画の重ね順 (重要)

`BAR_OVER_WAVE = 1` (既定) のときの重ね順は:

```
バー > 波形 > 中央線 > グリッド > 背景
```

#### バーの高さのスナップショット (重要)

LCD の走査は**行優先** (`pixel_index = row_cnt*320 + col_cnt`,
`col_cnt` が速い) なので、**1 本のバーのピクセルはフレーム全体に
散らばって描かれます**。列 `c` の row 0 はフレームの最初、row 239 は
フレームの最後に送られます。

そのため `bar_data` を描画中に生で読むと、**1 本のバーがフレームの
途中で何度も高さを変えてしまい、「途中で途切れた」ように見えます**

対策として、`lcd_wave` は**フレーム開始時に全 bin の高さを
スナップショット**し (`bar_snap` 配列)、描画にはその値だけを使います。

```
S_FRAME_START -> S_BAR_LD (bin 0..63 を順に取り込み) -> S_PIX_CALC -> 描画
```

`bar_addr``bar_data` は 2 クロックのレジスタ遅延があるため、
1 bin あたり 3 サイクルかけます

また `col_cnt / BAR_PITCH` の除算と 64 エントリのバー高さ選択は
遅延が大きいため、`S_PIX_CALC``bar_cur` にラッチしてから
`pixel_color` に渡します (経路を 2 サイクルに分割)。

`tb_lcd_ctrl` のフェーズ 5 がこの挙動を検証します
(フレーム途中で `bar_ram` を別の値に変えても、そのフレームのバーが
開始時の値で一貫していること)。

#### 棒グラフの表示時間と消去

**表示時間を変えたいときは `top.sv``BAR_LIFE` を変えます。**

##### 表示時間を決めるのは `BAR_LIFE` (`spectrum.sv`)

各 bin が「あと何フレーム表示するか」のカウンタ `bar_life` を持ちます。

- そのフレームにノイズフロア (`LOG_MIN`) を超える値があれば、
  寿命を `BAR_LIFE` に戻す
- 信号が消えたフレームから 1 つずつ減らし、0 になったらそのビンの
  バーを 0 にする
- **信号が消えてから `BAR_LIFE` フレームの間、そのビンはピーク値を
  保持して表示され続ける**

```
BAR_LIFE : ビンごとの保持時間。単位は FFT フレーム
           1 FFT フレーム = 128 / 20.03 kHz = 6.39 ms
           表示時間 [s] = BAR_LIFE * 6.39 ms

  例)  32 -> 0.20 s  /  156 -> 1.0 s  /  313 -> 2.0 s
```

寿命の判定は LCD ではなく `spectrum` 側で行います。

> **`BAR_LIFE` の制約:** `BAR_LIFE` は FFT フレーム数
> (1 フレーム = 128 サンプル ≒ 6.4 ms) です。LCD は約 148 ms ごとに
> しか `bar_ram` を読まないため、`BAR_LIFE` が 1 LCD フレーム分
> (≒ 23 FFT フレーム) より小さいと、**LCD が読む前に寿命切れで 0 に
> なりバーが点滅・欠けします**。`BAR_LIFE` は 25 以上にしてください。

##### 無音時の一括消去は `SIL_FRAMES``BAR_LIFE` より大きくすること

フレーム全体が無音 (最大 `mag2` < `SIG_TH`) のまま `SIL_FRAMES`
フレーム続くと、全ビンを一度に 0 にします。

> **重要:** `SIL_FRAMES``BAR_LIFE` より小さいと、ビンごとの保持が
> 切れる前に**全部消してしまう**ため、`BAR_LIFE` をいくら上げても
> 表示が伸びません。以前は `SIL_FRAMES = 2` (固定, 約 13 ms) だった
> ため、`BAR_LIFE` を変えても体感の表示時間が変わらない状態でした。
> `top.sv` では `SP_SIL_FRAMES = BAR_LIFE + 8` としています。

##### `BAR_HOLD` (`lcd_wave.sv`) は表示時間に影響しない

`lcd_wave` 側にも `BAR_HOLD` がありますが、これは
**スペクトラム更新 (`bar_fresh`) が完全に止まったときの安全網**です。

実機では `bar_fresh` が約 6.4 ms ごとに来るので、LCD のフレーム開始
(約 148 ms ごと) では必ず `bar_seen = 1` となり `bar_hold`
`BAR_HOLD` に戻り続けます。したがって**通常動作では `BAR_HOLD`
変えても表示時間は変わりません**。表示時間は `BAR_LIFE` で変えます。

| パラメータ | ファイル | 単位 | 役割 |
|-----------|---------|------|------|
| `BAR_LIFE` | `top.sv` | FFT フレーム (6.4 ms) | **表示時間 (これで変える)** |
| `SP_SIL_FRAMES` | `top.sv` | FFT フレーム | 無音時の一括消去 (`BAR_LIFE` より大) |
| `BAR_HOLD` | `top.sv` | LCD フレーム (148 ms) | 更新停止時の安全網 (通常は無関係) |

> `bar_fresh``spectrum` が 1 クロック幅のパルスで出すため、
> フレーム開始でそのままサンプルすると取りこぼします。
> `lcd_wave` では `bar_seen` にラッチし、フレーム開始時に消費する
> 形にしています (`tb_lcd_ctrl` のフェーズ 4 で検証)。

#### 波形の時間軸 (`lcd_wave.sv`)

タクトスイッチで 1 列あたりのサンプル数 `spp` を切り替えます。

| 状態 | `spp` | 表示時間 (320 列) |
|------|-------|-------------------|
| `tb_sel = 0` | 1 | 約 16 ms |
| `tb_sel = 1` | 2 | 約 32 ms |
| `tb_sel = 2` | 4 | 約 64 ms |
| `tb_sel = 3` | 8 | 約 128 ms |

読み出しアドレスは次の式で求めます。

```
rd_idx = rd_base - 1 - (spp * LCD_W) + (col * spp)
```

`tb_sel` はフレーム開始時にラッチするため、切り替えは次フレームから
反映されます。

`spp` は RAM 深さから自動的にクランプされます
(`BUF_AW = 12`, `LCD_W = 320` なら最大 12)。


## カスタマイズ

| 変化内容 | ファイル | パラメータ |
|----------|----------|------------|
| サンプリング周波数 | `top.sv` | `SCK_HZ` (→ `Fs = SCK_HZ/64`) |
| 移動平均のサンプル数 | `top.sv` | `AVG_N` |
| **スイッチのチャタリング除去時間** | `top.sv` | `SW_WAIT_MS` |
| 波形の振幅 | `top.sv` | `AMP_GAIN_SHIFT` (2 の冪。負で縮小) |
| 波形バッファ長 | `top.sv` | `BUF_AW` |
| フレーム更新間隔 | `top.sv` | `FRAME_WAIT` |
| FFT の間引き比 | `top.sv` | `FFT_DEC_N` |
| 表示する最初の bin | `top.sv` | `SP_START` |
| バーの最大高さ | `top.sv` | `SP_MAX` |
| **表示レンジ (本数)** | `top.sv` | `BAR_N` (= bin 数 → 帯域) |
| バーの幅 | `top.sv` | `BAR_W` |
| **バーの表示高さ (倍率)** | `top.sv` | **`BAR_GAIN_SH`** (左シフト量。1 で 2 倍) |
| バーの重ね順 | `lcd_wave.sv` | `BAR_OVER_WAVE` (1 で波形より手前) |
| **バーの表示時間** | `top.sv` | **`BAR_LIFE`** (FFT フレーム数。1.0 s なら 156) |
| 無音時の一括消去 | `top.sv` | `SP_SIL_FRAMES` (`BAR_LIFE` より大) |
| LCD 側の安全網 | `top.sv` | `BAR_HOLD` (LCD フレーム数、通常は無関係) |
| 無音とみなすレベル | `top.sv` | `SP_SIG_TH` |
| **時間軸の段数** | `top.sv` | `TB_N` (1,2,4,... サンプル/列) |
| **表示ゲイン** | `top.sv` | `SP_LOG_MUL` / `SP_LOG_SH` |
| バー 0 になるしきい値 | `top.sv` | `SP_LOG_MIN` |
| ピークホールド | `top.sv``spectrum` | `DECAY` (0 = 最大値保持) |
| 表示色 | `lcd_wave.sv` | `COL_WAVE` / `COL_BAR` など |
| 表示するサンプル数 | `lcd_wave.sv` | `LCD_W` |
| 波形の太さ | `lcd_wave.sv` | `WAVE_THICK` |

> `BUF_AW` を変更する場合は `audio_buf` の深さと
> `lcd_wave` の描画範囲に注意してください。
>
> 窓関数 / 回転因子を変える場合は `tools/gen_tables.ps1` を修正して
> `rtl/fft_tables.svh` を再生成してください。
>
> `SCK_HZ` を変えると `Fs = SCK_HZ / 64``df = Fs / FFT_DEC_N / FFT_N`
> 表示帯域 `= df * BAR_N` がすべて変わります。
> 例) 表示を 0~20 kHz にしたい場合は `SCK_HZ = 2_560_000` (`Fs = 40 kHz`,
> `df = 312.5 Hz`) にするとちょうど `df * 64 = 20 kHz` になります
> (`SP_LOG_MUL` を 22 程度に下げると 240 px に収まります)。

## コンパイル結果

Quartus Prime 25.1std (Lite) / 10M08SCE144C8G

| 項目 | FFT 前 | FFT 後 |
|------|--------|--------|
| Flow Status | Successful (0 errors) | Successful (0 errors) |
| Total logic elements | 803 / 8,064 ( 10 % ) | 2,727 / 8,064 ( 34 % ) |
| Total registers | 439 | 1,354 / 8,542 ( 16 % ) |
| Total pins | 14 / 101 ( 14 % ) | 16 / 101 ( 16 % ) |
| Total memory bits | 65,536 / 387,072 ( 17 % ) | 76,352 / 387,072 ( 20 % ) |
| Worst-case setup slack (Slow 1200mV 85C) | +1.694 ns | **+1.207 ns** |
| Worst-case hold slack | — | +0.341 ns |

> `pixel_color` に 9 bit の加算器 (バーの高さ増幅とクリップ) が入り、
> さらにバーの高さスナップショット (64 エントリの選択) が加わりましたが、
> 選択結果を `bar_cur` にラッチして経路を 2 サイクルに分割しているため
> setup slack は **+1.207 ns** と十分な余裕があります (クロック周期 20 ns)。

> `spectrum.sv` の対数圧縮は組み合わせ遅延が大きいため、
> 「優先エンコーダ → 正規化 → log2 LUT → ゲイン + ピークホールド」の
> 4 段にパイプライン化しています。
広げる

●生成されたRTLコード

top.sv

// ============================================================
// top.sv
//   マイクで取り込んだ音声波形を LCD に表示する
//
//   [PMOD 1] PMOD-Microphone v1.0 (I2S MEMS マイク)
//     FPGA が I2S マスタとして SCK / WS を生成し SD を受信
//
//   [PMOD 2] PMOD-TFTLCD v1.1 (ILI9341, 320x240, SPI)
//     音声波形を 1 サンプル 1 列で描画。
//     さらに周波数ごとの値 (スペクトラム) を棒グラフで重ねて表示する。
//
//   構成:
//     i2s_rx     : I2S 受信      -> sample (16bit 符号付き)
//     moving_avg : 移動平均      -> ノイズ低減 (波形表示のみ)
//     audio_buf  : 波形用 RAM    -> 4096 サンプルのリングバッファ
//     fft128     : 128 点 FFT    -> ビンごとのパワー (mag2)
//     spectrum   : log2 圧縮     -> バーの高さ (0..239)
//     lcd_wave   : ILI9341 制御 + 波形 / 棒グラフ描画
//
//   周波数レンジ:
//     Fs  = SCK_HZ / 64 = 1.28 MHz / 64 = 20.03 kHz
//     Fse = Fs / DEC_N  = 20.03 kHz / 1
//     df  = Fse / 128   = 156.5 Hz / bin
//     表示範囲 = bin 0..63 = 0 .. 10.0 kHz  (64 本の棒グラフ)
//
//   ボタン:
//     btn_rst   (PIN_17) : リセット (押下 = Low)
//     sw_inc    (PIN_62) : 波形の時間軸を 1 段上げる (1 列あたりのサンプル数 x2)
//     sw_dec    (PIN_54) : 波形の時間軸を 1 段下げる
//     時間軸は 1 -> 2 -> 4 -> ... -> 12 サンプル/列 (BUF_AW で上限が決まる)。
//     押下のたびに変化し、押しっぱなしでは進まない。
// ============================================================

module top #(
    // シミュレーションで高速に回せるようパラメータにしてある。
    // 既定値は実機と同じなので合成結果は変わらない。
    parameter int CLK_HZ     = 50_000_000,  // 入力クロック
    parameter int SW_WAIT_MS = 10           // スイッチのチャタリング除去時間 (ms)
) (
    // --- クロック / リセット ---
    input  logic       clk,        // PIN_88 : 50 MHz
    input  logic       btn_rst,    // PIN_17 : 押下 = Low

    // --- タクトスイッチ ---
    input  logic       sw_inc,     // PIN_62 : 時間軸を上げる
    input  logic       sw_dec,     // PIN_54 : 時間軸を下げる

    // --- PMOD 1 : Microphone ---
    output logic       mic_sck,    // PIN_47 : SCK
    output logic       mic_ws,     // PIN_45 : WS
    input  logic       mic_sd,     // PIN_39 : SD

    // --- PMOD 2 : LCD ---
    output logic       lcd_cs,     // PIN_81 : CS
    output logic       lcd_dc,     // PIN_77 : RS
    output logic       lcd_mosi,   // PIN_78 : MOSI
    output logic       lcd_sck,    // PIN_75 : CLK

    // --- LED ---
    output logic       led,        // PIN_85 : フレーム描画ごとにトグル
    output logic       led0,       // PIN_123: マイク入力検出
    output logic       led1,       // PIN_122: サンプル受信ごとにトグル
    output logic       led2,       // PIN_121: FFT フレーム完了ごとにトグル
    output logic       led3        // PIN_120: LCD 初期化完了
);

    // ========================================================
    // パラメータ
    // ========================================================
    // CLK_HZ は上でパラメータとして宣言している
    // I2S は 1 フレーム = 64 SCK なので Fs = SCK_HZ / 64。
    // 1.28 MHz -> Fs = 20031.25 Hz (ナイキスト = 10.016 kHz)
    localparam int SCK_HZ  = 1_280_000;    // I2S ビットクロック (Fs = 20.03 kHz)
    localparam int BUF_AW  = 12;           // 波形 RAM アドレス幅 (4096 サンプル)

    localparam int AVG_N         = 8;      // 移動平均のサンプル数 (波形表示用)
    //   波形の振幅スケール (2 の冪で指定)。
    //     > 0 : 2^AMP_GAIN_SHIFT 倍に拡大 (左シフト)
    //     = 0 : 等倍
    //     < 0 : 1/2^|AMP_GAIN_SHIFT| に縮小 (右シフト)
    //   現在は 1/2 (-1)。
    localparam int AMP_GAIN_SHIFT = -1;    // 波形表示スケール (-1 = 1/2)

    // ---- FFT / スペクトラム ----
    localparam int FFT_N     = 128;        // FFT 点数
    localparam int FFT_DEC_N = 1;          // din_valid の分周比 (1 = 間引きなし)
    localparam int FFT_HALF  = FFT_N / 2;  // 出力するビン数 (64)
    localparam int SP_START  = 1;          // 表示する最初の bin (DC を除外)
    localparam int SP_MAX    = 239;        // バーの内部上限 (px)

    // ---- 棒グラフの表示高さ ----
    //   表示高さ = bar_data << BAR_GAIN_SH。
    //   1 にすると従来の 2 倍の高さで表示する
    //   (画面上端を超えた分はクリップされ、下辺から塗られる)。
    localparam int BAR_GAIN_SH = 1;        // 2 倍

    // ---- 表示レンジ ----
    //   Fse = Fs / FFT_DEC_N = 20.031 kHz
    //   df  = Fse / FFT_N    = 156.5 Hz / bin
    //   表示帯域 = df * FFT_HALF = 10.0 kHz (bin 0..63)
    localparam int BAR_N     = 64;         // 表示する本数 = 全 bin (0..63)
    localparam int BAR_W     = 4;          // 1 本の横幅 (px)
                                           // pitch = 320 / 64 = 5 px
                                           // なので 1 px の隙間が空く

    // ---- 棒グラフの表示時間 ----
    //   「信号が消えてからバーが消えるまで」の時間は
    //   spectrum 側の BAR_LIFE で決まる。
    //
    //     BAR_LIFE : ビンごとの保持時間。単位は FFT フレーム。
    //                1 FFT フレーム = FFT_N / Fse = 128 / 20.03 kHz
    //                               = 6.39 ms
    //                表示時間 [s] = BAR_LIFE * 6.39 ms
    //
    //   例) 32 -> 0.20 s  /  156 -> 1.0 s  /  313 -> 2.0 s
    //
    //   ◆ 25 以上にすること ◆
    //   LCD は 1 フレーム約 148 ms = 約 23 FFT フレームごとにしか
    //   bar_ram を読まない。BAR_LIFE が 25 未満だと LCD が読む前に
    //   寿命切れで 0 になり、バーが点滅・欠けする。
    localparam int BAR_LIFE = 64;         // ビンの保持時間 (FFT フレーム)
                                           // 156 * 6.39 ms ≒ 1.0 s

    //   無音が続いたときに全ビンを一括で消すまでのフレーム数。
    //   ◆ BAR_LIFE 以上にすること ◆
    //   小さいとビンごとの保持より先に全部消えてしまい、
    //   BAR_LIFE をいくら上げても表示が伸びない。
    localparam int SP_SIL_FRAMES = BAR_LIFE + 8;

    //   BAR_HOLD : lcd_wave 側の安全網。単位は LCD フレーム。
    //     スペクトラム更新 (bar_fresh) が完全に止まったときに
    //     バーが残り続けないようにするためのもので、
    //     **通常動作では表示時間に影響しない**。
    //     実機で bar_fresh は 6.4 ms ごとに来るので、
    //     LCD フレーム開始 (約 148 ms ごと) には必ず
    //     bar_seen = 1 となり BAR_HOLD に戻り続けるため。
    //     表示時間を変えたい場合は BAR_LIFE を変えること。
    localparam int BAR_HOLD = 3;           // LCD 側の安全網 (LCD フレーム数)

    // ---- 時間軸 ----
    //   tb_sel を上げると 1 列あたりのサンプル数が 1,2,4,... と増える
    //   = 表示する時間が長くなる。
    //   spp * LCD_W (320) がバッファ長 (4096) を超えないよう段数を決める。
    //     1,2,4,8 -> 最大 8 * 320 = 2560 (< 4096)
    localparam int TB_N      = 4;          // 段数 (1, 2, 4, 8 サンプル/列)
    localparam int TB_SPP0   = 1;          // tb_sel = 0 のサンプル数

    // ---- スペクトラム ----
    //   無音とみなす mag2 のしきい値
    localparam int SP_SIG_TH = 1024;
    //   ゲイン = SP_LOG_MUL / 2^SP_LOG_SH [px / octave]
    //   1 octave = 6.02 dB。
    //   従来 (LOG_MUL=11, LOG_MIN=128) に対し LOG_MUL を 33 にしたので
    //   ちょうど 3 倍 (33/11 = 3)。
    //   11/16 = 0.69 px/octave -> 33/16 = 2.06 px/octave (約 0.34 px/dB)
    //   画面 240 px で表示レンジは約 70 dB。
    localparam int SP_LOG_MIN = 64;       // バー 0 になる q4 (ノイズフロア)
    localparam int SP_LOG_MUL = 22;        // ゲイン分子 (従来 11 の 3 倍)
    localparam int SP_LOG_SH  = 4;         // ゲイン分母 (2^SP_LOG_SH = 16)

    // ========================================================
    // 電源投入時リセット + ボタン 2 段同期化
    //   rst_n : Low 有効の非同期リセット
    // ========================================================
    logic [15:0] por_cnt;
    logic        btn_r1, btn_r2;
    logic        rst_n;

    always_ff @(posedge clk) begin
        if (por_cnt != 16'hFFFF) por_cnt <= por_cnt + 16'd1;
    end

    always_ff @(posedge clk) begin
        btn_r1 <= btn_rst;
        btn_r2 <= btn_r1;
    end

    assign rst_n = (por_cnt == 16'hFFFF) & btn_r2;

    // ========================================================
    // タクトスイッチ (時間軸の切り替え)
    //   チャタリング除去し、押下のたびに tb_sel を 1 段変える。
    //   上限 / 下限に達したら、そのままにする。
    // ========================================================
    logic swi_press, swd_press;

    sw_debounce #(
        .CLK_HZ     (CLK_HZ),
        .WAIT_MS    (SW_WAIT_MS),
        .ACTIVE_LOW (1'b1)
    ) u_sw_inc (
        .clk    (clk),
        .rst_n  (rst_n),
        .sw_in  (sw_inc),
        .level  (),
        .press  (swi_press)
    );

    sw_debounce #(
        .CLK_HZ     (CLK_HZ),
        .WAIT_MS    (SW_WAIT_MS),
        .ACTIVE_LOW (1'b1)
    ) u_sw_dec (
        .clk    (clk),
        .rst_n  (rst_n),
        .sw_in  (sw_dec),
        .level  (),
        .press  (swd_press)
    );

    // tb_sel : 0 .. TB_N-1 (0 が最短の時間軸)
    logic [2:0] tb_sel;
    localparam logic [2:0] TB_LAST = TB_N - 1;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            tb_sel <= 3'd0;
        end else if (swi_press) begin
            if (tb_sel != TB_LAST) tb_sel <= tb_sel + 3'd1;
        end else if (swd_press) begin
            if (tb_sel != 3'd0)    tb_sel <= tb_sel - 3'd1;
        end
    end

    // ========================================================
    // I2S マイク受信
    // ========================================================
    logic signed [15:0] sample;
    logic               sample_valid;

    i2s_rx #(
        .CLK_HZ (CLK_HZ),
        .SCK_HZ (SCK_HZ)
    ) u_i2s_rx (
        .clk          (clk),
        .rst_n        (rst_n),
        .mic_sck      (mic_sck),
        .mic_ws       (mic_ws),
        .mic_sd       (mic_sd),
        .sample       (sample),
        .sample_valid (sample_valid)
    );

    // ========================================================
    // 移動平均フィルタ (ノイズ低減)
    //   AVG_N サンプル分の平均をとることで、波形に乗る
    //   高周波ノイズを抑える。
    // ========================================================
    logic signed [15:0] avg_sample;
    logic               avg_valid;

    moving_avg #(
        .N  (AVG_N),
        .DW (16)
    ) u_moving_avg (
        .clk       (clk),
        .rst_n     (rst_n),
        .valid_in  (sample_valid),
        .data_in   (sample),
        .data_out  (avg_sample),
        .valid_out (avg_valid)
    );

    // ========================================================
    // 波形用リングバッファ
    // ========================================================
    logic [BUF_AW-1:0] rd_addr;
    logic [BUF_AW-1:0] wr_ptr;
    logic signed [15:0] rdata;

    audio_buf #(
        .AW (BUF_AW),
        .DW (16)
    ) u_audio_buf (
        .clk      (clk),
        .we       (avg_valid),
        .wdata    (avg_sample),
        .wr_ptr   (wr_ptr),
        .rd_addr  (rd_addr),
        .rdata    (rdata)
    );

    // ========================================================
    // FFT (128 点) + スペクトラム変換
    //   FFT には移動平均を通さない生サンプルを入力する。
    //   移動平均 (N = 8) は Fs / N 付近から減衰し始めるため、
    //   Fs = 20 kHz では 8 kHz 以上の成分が落ちてしまう。
    //   Hann 窓は fft128 の内部で掛ける。
    // ========================================================
    logic [6:0]  mag_addr;
    logic [33:0] mag2;
    logic        mag_we;
    logic [7:0]  fft_frame;
    logic        fft_done;
    logic        fft_busy;

    fft128 #(
        .N      (FFT_N),
        .DEC_N  (FFT_DEC_N),
        .OUT_SH (8)
    ) u_fft128 (
        .clk         (clk),
        .rst_n       (rst_n),
        .din         (sample),        // 生サンプル (移動平均前)
        .din_valid   (sample_valid),
        .mag_addr    (mag_addr),
        .mag2        (mag2),
        .mag_we      (mag_we),
        .frame_index (fft_frame),
        .frame_done  (fft_done),
        .busy        (fft_busy)
    );

    logic [5:0] bar_addr;
    logic [7:0] bar_data;
    logic       sp_busy;
    logic       bar_fresh;

    spectrum #(
        .N_BINS    (FFT_HALF),
        .START_BIN (SP_START),
        .MAX_VAL   (SP_MAX),
        .LOG_MIN   (SP_LOG_MIN),
        .LOG_MUL   (SP_LOG_MUL),
        .LOG_SH    (SP_LOG_SH),
        .DECAY     (0),                // 0 = ピークホールド
        .SIG_TH    (SP_SIG_TH),
        .BAR_LIFE  (BAR_LIFE),
        .SIL_FRAMES (SP_SIL_FRAMES)
    ) u_spectrum (
        .clk        (clk),
        .rst_n      (rst_n),
        .mag_addr   (mag_addr),
        .mag2       (mag2),
        .frame_done (fft_done),
        .bar_addr   (bar_addr),
        .bar_data   (bar_data),
        .busy       (sp_busy),
        .bar_fresh  (bar_fresh)
    );

    // ========================================================
    // LCD コントローラ + 波形描画
    // ========================================================
    logic init_done;
    logic frame_tick;

    lcd_wave #(
        .CLK_HZ         (CLK_HZ),
        .BUF_AW         (BUF_AW),
        .LCD_W          (320),
        .LCD_H          (240),
        .AMP_GAIN_SHIFT (AMP_GAIN_SHIFT),
        .FRAME_WAIT     (2_500_000),   // 50 ms
        .BAR_EN         (1'b1),
        .BAR_W          (BAR_W),
        .BAR_N          (BAR_N),
        .BAR_HOLD       (BAR_HOLD),
        .BAR_GAIN_SH    (BAR_GAIN_SH),
        .TB_N           (TB_N),
        .TB_SPP0        (TB_SPP0)
    ) u_lcd_wave (
        .clk        (clk),
        .rst_n      (rst_n),
        .lcd_cs     (lcd_cs),
        .lcd_dc     (lcd_dc),
        .lcd_mosi   (lcd_mosi),
        .lcd_sck    (lcd_sck),
        .rd_addr    (rd_addr),
        .rdata      (rdata),
        .wr_ptr     (wr_ptr),
        .bar_addr   (bar_addr),
        .bar_data   (bar_data),
        .tb_sel     (tb_sel),
        .bar_fresh  (bar_fresh),
        .init_done  (init_done),
        .frame_tick (frame_tick)
    );

    // ========================================================
    // LED インジケータ
    // ========================================================
    logic led_frame;
    logic led_sample;
    logic led_fft;
    logic [22:0] mic_act_cnt;

    // フレーム描画ごとにトグル
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)       led_frame <= 1'b0;
        else if (frame_tick) led_frame <= ~led_frame;
    end

    // サンプル受信ごとにトグル
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)             led_sample <= 1'b0;
        else if (sample_valid)  led_sample <= ~led_sample;
    end

    // FFT フレーム完了ごとにトグル (スペクトラムが更新されている)
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)            led_fft <= 1'b0;
        else if (fft_done)     led_fft <= ~led_fft;
    end

    // マイク入力がある間点灯 (約 0.17 秒間保持)
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)            mic_act_cnt <= '0;
        else if (sample_valid) mic_act_cnt <= '0;
        else if (mic_act_cnt != 23'h7FFFFF) mic_act_cnt <= mic_act_cnt + 23'd1;
    end

    assign led  = led_frame;
    assign led0 = (mic_act_cnt != 23'h7FFFFF);
    assign led1 = led_sample;
    assign led2 = led_fft;
    assign led3 = init_done;

endmodule
広げる

lcd_wave.sv

// lcd_wave.sv
//  ILI9341 SPI LCD コントローラ + 波形描画PMOD-TFTLCD v1.1
//
//  画面構成 (横向き: MADCTL = 0x28)
//
//   描画方式:
//     フレーム開始時に書き込みポインタ (wr_ptr) をラッチし、
//     その (LCD_W * TB_SPP) サンプル前から現在までのデータを
//     左→右に描画する。tb_sel で 1 列あたりのサンプル数 (TB_SPP) を切り替えることで、波形の時間軸を動的に変えられる。
//
//   時間軸 (tb_sel):
//     tb_sel を上げるほど 1 列あたりのサンプル数が増える
//     = より長い時間を表示する (波形は細かく見える)。
//     フレーム開始時にラッチするので、切り替えは次フレームから反映される。
//
//   棒グラフ:
//     bar_n 番目のバーを横幅 BAR_W px で描く。
//     bar_ram は spectrum.sv が持つため、ここでは
//       アドレスを出して 1 クロック後に値を読む (bar_data)。
//     バーの高さは下辺 (LCD_H-1) から上方向に bar_data px。
//
//     棒グラフの表示時間は spectrum 側の BAR_LIFE が決める。
//     ここ (lcd_wave) の BAR_HOLD は、スペクトラム更新 (bar_fresh) が
//     完全に止まったときだけバーを消すための安全網であり、
//     通常動作では表示時間に影響しない。
//     実機では bar_fresh が 6.4 ms ごとに来るので、LCD のフレーム
//     開始 (約 148 ms ごと) には必ず bar_seen = 1 となり
//     bar_hold は BAR_HOLD に戻り続ける。
//
//     bar_fresh はspectrum 側が1クロック幅のパルスで出すため、
//     フレーム開始でサンプルすると取りこぼす。bar_seen に
//     ラッチしてフレーム開始時に見る。
//
//   SPI : Mode 0, SCK = CLK_HZ / 4 (= 12.5 MHz @ 50 MHz)

module lcd_wave #(
    parameter int CLK_HZ = 50_000_000,
    parameter int BUF_AW = 12,           // 波形 RAM アドレス幅
    parameter int LCD_W  = 320,
    parameter int LCD_H  = 240,
    parameter int AMP_GAIN_SHIFT = 0,    // 振幅スケール (2 の冪)
                                         //   > 0 : 2^n 倍に拡大
                                         //   = 0 : 等倍
                                         //   < 0 : 1/2^|n| に縮小
    parameter int FRAME_WAIT = 2_500_000, // フレーム間隔 (50MHz -> 50ms)
    parameter int POWERON_WAIT = 7_500_000, // 電源ON後待機 (50MHz -> 150ms)
    parameter int SWRST_WAIT   = 250_000,   // SWRESET 後待機 (5ms)
    parameter int SLPOUT_WAIT  = 6_000_000, // SLPOUT 後待機 (120ms)
    parameter bit BAR_EN  = 1'b1,       // 棒グラフを描く
    parameter int BAR_W   = 5,          // 1 本の横幅 (px)
    parameter int BAR_N   = 64,         // 表示する本数
    parameter int BAR_HOLD = 20,        // 棒グラフの寿命 (LCD フレーム数)
    parameter int TB_SEL  = 0,          // 時間軸の初期値 (0..TB_N-1)
    parameter int TB_N    = 8,          // 時間軸の段数
    parameter int TB_SPP0 = 1,          // tb_sel = 0 のサンプル数/列
    parameter int BAR_GAIN_SH = 1       // バー表示高さの左シフト量
                                        // (1 で表示高さ 2 倍、0 で等倍)
                                        // 画面高を超えた分はクリップされる
) (
    input  logic               clk,
    input  logic               rst_n,   // 非同期リセット (Low 有効)

    // --- PMOD-TFTLCD ---
    output logic               lcd_cs,  // CS (Low 有効)
    output logic               lcd_dc,  // RS (1=Data, 0=Command)
    output logic               lcd_mosi,// MOSI
    output logic               lcd_sck, // SCK

    // --- 波形 RAM 読み出し ---
    output logic [BUF_AW-1:0]  rd_addr,
    input  logic signed [15:0] rdata,
    input  logic [BUF_AW-1:0]  wr_ptr,  // RAM 書き込みポインタ

    // --- スペクトラム RAM 読み出し ---
    output logic [5:0]         bar_addr, // 表示する bin
    input  logic [7:0]         bar_data, // バーの高さ (1 クロック遅れ)

    // --- 時間軸 / 棒グラフの制御 ---
    input  logic [2:0]         tb_sel,      // 波形の時間軸 (0..TB_N-1)
    input  logic               bar_fresh,   // スペクトラムが更新された (パルス)

    // --- ステータス ---
    output logic               init_done,
    output logic               frame_tick    // 1 フレーム描画完了パルス
);

    // ========================================================
    // 定数
    // SPI バイト送信器は 1 ビットあたり 4 クロック使うため
    // SPI クロック = CLK_HZ / 4 = 12.5 MHz となる。
    localparam int TOTAL_PIX = LCD_W * LCD_H;      // 76800
    localparam int CNT_W     = $clog2(TOTAL_PIX);  // 17

    localparam int Y_CENTER = LCD_H / 2;       // 120
    localparam int Y_OFFSET = Y_CENTER - 1;    // 119 (振幅 0の行)
    localparam int Y_MAXROW = LCD_H - 1;       // 239

    // 定数 (比較用は int のままにしておくとビット幅の切捨て警告が出ない)
    localparam int LAST_PIX = TOTAL_PIX - 1;
    localparam int LAST_COL = LCD_W - 1;
    localparam int LAST_ROW = LCD_H - 1;

    // サイズ付き定数 (演算に使うものだけ幅を明示する)
    localparam logic [23:0]        POWERON_CNT  = POWERON_WAIT;
    localparam logic [23:0]        FRAME_WAIT_C = FRAME_WAIT;
    localparam logic [23:0]        SWRST_CNT    = SWRST_WAIT;
    localparam logic [23:0]        SLPOUT_CNT   = SLPOUT_WAIT;
    localparam logic [8:0]         Y_OFFSET_C   = Y_OFFSET;
    localparam logic [7:0]         Y_MAXROW_C   = Y_MAXROW;

    localparam logic [15:0] COL_BG   = 16'h0000;   // 黒
    localparam logic [15:0] COL_GRID = 16'h1082;   // 暗い緑 (グリッド)
    localparam logic [15:0] COL_AXIS = 16'h4208;   // グレー (中央線)
    localparam logic [15:0] COL_WAVE = 16'h07E0;   // 緑 (波形)
    localparam logic [15:0] COL_BAR  = 16'hFD20;   // 橙 (スペクトラム)

    // ---- 棒グラフのジオメトリ ----
    localparam int BAR_PITCH = (BAR_W > LCD_W / BAR_N) ? BAR_W : LCD_W / BAR_N;
    localparam int BAR_BOT   = LCD_H - 1;      // バーの下辺

    localparam logic [8:0] BAR_W_C  = BAR_W;
    localparam logic [8:0] BAR_P_C  = BAR_PITCH;
    localparam logic [8:0] BAR_BOT_9 = BAR_BOT[8:0];
    localparam logic [7:0] BAR_SHF_C = BAR_GAIN_SH[7:0];
    localparam logic [5:0] BAR_LAST_C = (BAR_N - 1);   // 最後に読み込む bin

    // バーを波形 / 中央線より手前に描くか。
    //   1 : バーが最優先。バーが途切れない (高さを上げても崩れない)
    //   0 : 従来の順 (波形 > 中央線 > バー)。バーの高さが中央線を
    //       超えると波形と中央線がバーを横切って見える
    localparam bit BAR_OVER_WAVE = 1'b1;

    // ---- 時間軸 ----
    //   tb_sel は 0..TB_N-1。値が大きいほど 1 列あたりのサンプル数が
    //   増える (= 長い時間を表示する)。
    //     spp = TB_SPP0 << tb_sel
    //   spp は RAM 深さ / LCD_W 以下にクランプする
    //   (BUF_AW = 12, LCD_W = 320 -> 最大 12)。
    localparam int MAX_SPP_I = (1 << BUF_AW) / LCD_W;
    localparam logic [7:0] MAX_SPP_C = MAX_SPP_I[7:0];

    // ---- 棒グラフの寿命 ----
    localparam int BAR_HOLD_C = BAR_HOLD;

    // ==================================================
    // 初期化シーケンス ROM
    //   bit[8] = 0 : CMD (DC = 0)
    //   bit[8] = 1 : DAT (DC = 1)

    localparam [6:0] ROM_DEPTH = 7'd64;

    localparam [8:0]
        R00=9'h001, R01=9'h011,                // SWRESET, SLPOUT
        R02=9'h03A, R03=9'h155,                // COLMOD  = 0x55
        R04=9'h036, R05=9'h128,                // MADCTL  = 0x28 (横)
        R06=9'h0B1, R07=9'h100, R08=9'h118,    // FRMCTR1
        R09=9'h0C0, R10=9'h123,                // PWCTR1
        R11=9'h0C1, R12=9'h110,                // PWCTR2
        R13=9'h0C5, R14=9'h13E, R15=9'h128,    // VMCTR1
        R16=9'h0C7, R17=9'h186,                // VMCTR2
        R18=9'h026, R19=9'h101,                // GAMMASET = 1
        R20=9'h0E0,                            // GMCTRP1
        R21=9'h10F, R22=9'h131, R23=9'h12B, R24=9'h10C,
        R25=9'h10E, R26=9'h108, R27=9'h14E, R28=9'h1F1,
        R29=9'h137, R30=9'h107, R31=9'h110, R32=9'h103,
        R33=9'h10E, R34=9'h109, R35=9'h100,
        R36=9'h0E1,                            // GMCTRN1
        R37=9'h100, R38=9'h10E, R39=9'h114, R40=9'h103,
        R41=9'h111, R42=9'h107, R43=9'h131, R44=9'h1C1,
        R45=9'h148, R46=9'h108, R47=9'h10F, R48=9'h10C,
        R49=9'h131, R50=9'h136, R51=9'h10F,
        R52=9'h02A, R53=9'h100, R54=9'h100,    // CASET (0..319)
        R55=9'h101, R56=9'h13F,
        R57=9'h02B, R58=9'h100, R59=9'h100,    // PASET (0..239)
        R60=9'h100, R61=9'h0EF,
        R62=9'h029,                            // DISPON
        R63=9'h02C;                            // Memory Write

    function automatic [8:0] rom_read(input [6:0] addr);
        case (addr)
            7'd0:  rom_read = R00;  7'd1:  rom_read = R01;
            7'd2:  rom_read = R02;  7'd3:  rom_read = R03;
            7'd4:  rom_read = R04;  7'd5:  rom_read = R05;
            7'd6:  rom_read = R06;  7'd7:  rom_read = R07;
            7'd8:  rom_read = R08;  7'd9:  rom_read = R09;
            7'd10: rom_read = R10;  7'd11: rom_read = R11;
            7'd12: rom_read = R12;  7'd13: rom_read = R13;
            7'd14: rom_read = R14;  7'd15: rom_read = R15;
            7'd16: rom_read = R16;  7'd17: rom_read = R17;
            7'd18: rom_read = R18;  7'd19: rom_read = R19;
            7'd20: rom_read = R20;  7'd21: rom_read = R21;
            7'd22: rom_read = R22;  7'd23: rom_read = R23;
            7'd24: rom_read = R24;  7'd25: rom_read = R25;
            7'd26: rom_read = R26;  7'd27: rom_read = R27;
            7'd28: rom_read = R28;  7'd29: rom_read = R29;
            7'd30: rom_read = R30;  7'd31: rom_read = R31;
            7'd32: rom_read = R32;  7'd33: rom_read = R33;
            7'd34: rom_read = R34;  7'd35: rom_read = R35;
            7'd36: rom_read = R36;  7'd37: rom_read = R37;
            7'd38: rom_read = R38;  7'd39: rom_read = R39;
            7'd40: rom_read = R40;  7'd41: rom_read = R41;
            7'd42: rom_read = R42;  7'd43: rom_read = R43;
            7'd44: rom_read = R44;  7'd45: rom_read = R45;
            7'd46: rom_read = R46;  7'd47: rom_read = R47;
            7'd48: rom_read = R48;  7'd49: rom_read = R49;
            7'd50: rom_read = R50;  7'd51: rom_read = R51;
            7'd52: rom_read = R52;  7'd53: rom_read = R53;
            7'd54: rom_read = R54;  7'd55: rom_read = R55;
            7'd56: rom_read = R56;  7'd57: rom_read = R57;
            7'd58: rom_read = R58;  7'd59: rom_read = R59;
            7'd60: rom_read = R60;  7'd61: rom_read = R61;
            7'd62: rom_read = R62;  7'd63: rom_read = R63;
            default: rom_read = 9'h000;
        endcase
    endfunction

    // ================================================
    // 電源 ON 後 150ms 待機
    // ================================================
    logic [23:0] poweron_cnt;
    logic        init_start;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            poweron_cnt <= '0;
            init_start  <= 1'b0;
        end else if (poweron_cnt == POWERON_CNT) begin
            init_start <= 1'b1;
        end else begin
            poweron_cnt <= poweron_cnt + 24'd1;
        end
    end

    // ================================================
    // SPI バイト送信器 (Mode 0, MSB ファースト)
    //   tx_cnt[1:0] : 0=MSB 出力, 1=SCK 立上, 2=保持, 3=SCK 立下
    //   tx_cnt[5:2] : ビット番号 0..7
    // ================================================
    logic [7:0] tx_byte;
    logic       tx_dc_reg;
    logic       tx_start;
    logic       tx_busy;
    logic [5:0] tx_cnt;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            lcd_cs   <= 1'b1;
            lcd_sck  <= 1'b0;
            lcd_mosi <= 1'b0;
            lcd_dc   <= 1'b0;
            tx_busy  <= 1'b0;
            tx_cnt   <= '0;
        end else if (tx_start && !tx_busy) begin
            tx_busy  <= 1'b1;
            tx_cnt   <= '0;
            lcd_cs   <= 1'b0;
            lcd_dc   <= tx_dc_reg;
            lcd_sck  <= 1'b0;
            lcd_mosi <= tx_byte[7];
        end else if (tx_busy) begin
            tx_cnt <= tx_cnt + 6'd1;
            case (tx_cnt[1:0])
                2'b00: lcd_mosi <= tx_byte[7 - tx_cnt[5:2]];
                2'b01: lcd_sck  <= 1'b1;
                2'b10: ; // 半周期保持
                2'b11: begin
                    lcd_sck <= 1'b0;
                    if (tx_cnt[5:2] == 4'd7) begin
                        tx_busy <= 1'b0;
                        lcd_cs  <= 1'b1;
                    end
                end
            endcase
        end
    end

    // ===============================================
    // ステートマシン
    // ===============================================
    // ステートマシン (typedef enum古いシミュレータで扱いにくいため
    // localparam の整数で表現する)
    localparam [3:0]
        S_WAIT       = 4'd0,
        S_INIT_TX    = 4'd1,
        S_INIT_WAIT  = 4'd2,
        S_SWRST_DLY  = 4'd3,
        S_SLPOUT_DLY = 4'd4,
        S_FRAME_START= 4'd5,
        S_PIX_CALC   = 4'd6,
        S_PIX_RD     = 4'd7,
        S_PIX_HI_TX  = 4'd8,
        S_PIX_HI_W   = 4'd9,
        S_PIX_LO_TX  = 4'd10,
        S_PIX_LO_W   = 4'd11,
        S_FRAME_END  = 4'd12,
        S_NEXT_FRAME = 4'd13,
        S_BAR_LD     = 4'd14;

    logic [3:0]  state;
    logic [6:0]  init_idx;
    logic [23:0] dly_cnt;
    logic [23:0] frm_cnt;
    logic [CNT_W-1:0] pix_cnt;
    logic [8:0]  col_cnt;
    logic [7:0]  row_cnt;
    logic [15:0] cur_color;
    logic [BUF_AW-1:0] rd_base;

    // ---- 時間軸 ----
    logic [7:0]  spp_r;        // ラッチした 1 列あたりのサンプル数
    logic [2:0]  tb_r;         // 前フレームの tb_sel (変化検出用)
    logic        tb_clr;       // 時間軸が変わった直後の 1 フレームだけ 1

    // ---- 棒グラフの寿命 ----
    logic [7:0]  bar_hold;
    logic        bar_seen;     // bar_fresh をラッチした (更新あり)

    // ---- 棒グラフの高さのスナップショット ----
    //   bar_ram は spectrum 側が約 6.4 ms ごとに書き換えるが、
    //   LCD のフレームは約 148 ms かかる上に走査が行優先
    //   (col_cnt が速い) のため、1 本のバーのピクセルは
    //   フレーム全体に散らばって描かれる。
    //   bar_data を描画中に生で見ると、1 本のバーがフレーム中に
    //   何度も高さを変えて「途中で途切れた」ように見えてしまう。
    //   -> フレーム開始時に全 bin の高さを取り込み、描画はその値だけを使う。
    logic [7:0]  bar_snap [0:BAR_N-1];
    logic [7:0]  bar_cur;      // この列で使うスナップショット値
    logic [5:0]  ld_bin;       // スナップショット読み込み中の bin
    logic [1:0]  ld_ph;        // 0 = アドレス設定, 1 = 待ち, 2 = 取り込み

    // フレーム開始 (S_FRAME_START) の 1 クロック
    wire frame_start_evt = (state == S_FRAME_START);

    // bar_fresh は 1 クロックパルスなので、LCD のフレーム開始
    // (約 160 ms 間隔) でそのままサンプルすると取りこぼす。
    // ここでラッチし、フレーム開始時に消費する。
    //   ※ 消費と同じクロックで来た bar_fresh は残す
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n)               bar_seen <= 1'b0;
        else if (frame_start_evt) bar_seen <= bar_fresh;  // 消費
        else if (bar_fresh)       bar_seen <= 1'b1;       // ラッチ
    end

    // フレーム開始時に更新する時間軸 (1 列あたりのサンプル数)
    //   tb_sel = 0 -> 1, 1 -> 2, 2 -> 4, 3 -> 8, ...
    wire [7:0] spp_calc = (TB_SPP0 > 0) ? (8'd1 << tb_sel) : 8'd1;
    wire [7:0] spp_next = (spp_calc > MAX_SPP_C) ? MAX_SPP_C : spp_calc;

    // 読み出したサンプルから波形の Y 座標を求める
    //  16bit 符号付き-> 振幅スケールを掛け、振幅1ビット = 1 px
    //  として中央 (Y_OFFSET) からの変位に変換する。
    //
    //   スケールは 2 の冪で指定する:
    //     AMP_GAIN_SHIFT > 0 : 左シフト (拡大)
    //     AMP_GAIN_SHIFT < 0 : 右シフト (縮小、算術シフトで符号を保持)
    //
    //   左シフト量と右シフト量を分けておくことで、負のシフト量という
    //   未定義動作 (Verilog では禁止) を避けている。
    localparam int AMP_LSH = (AMP_GAIN_SHIFT > 0) ?  AMP_GAIN_SHIFT : 0;
    localparam int AMP_RSH = (AMP_GAIN_SHIFT < 0) ? -AMP_GAIN_SHIFT : 0;

    wire signed [31:0] samp_gain = $signed(rdata) <<< AMP_LSH;
    wire signed [31:0] samp_off  = samp_gain >>> AMP_RSH;
    wire signed [31:0] y_raw    = $signed({23'd0, Y_OFFSET_C}) - samp_off;
    wire        [7:0]  y_wave   = (y_raw < 32'sd0)                        ? 8'd0 :
                                  (y_raw > $signed({24'd0, Y_MAXROW_C}))  ? Y_MAXROW_C :
                                                                           y_raw[7:0];

    // 波形は 1 サンプル = 1 列で描画する。
    // 隣接列とのつながりを良くするため WAVE_THICK px の太さで描く。
    localparam int WAVE_THICK = 3;    // 奇数 (1 = 1px 幅)
    localparam int WAVE_HALF  = WAVE_THICK / 2;
    localparam logic [7:0] WAVE_HALF_C = WAVE_HALF;
    wire [7:0] y_lo = (y_wave < WAVE_HALF_C) ? 8'd0
                                             : (y_wave - WAVE_HALF_C);
    wire [7:0] y_hi = ((y_wave + WAVE_HALF_C) > Y_MAXROW_C) ? Y_MAXROW_C
                                                            : (y_wave + WAVE_HALF_C);

    // 現在の列を描画するサンプル位置
    //   rd_base は「最新サンプル + 1」の位置。
    //   spp = 1 列あたりのサンプル数 (時間軸により変わる)
    //
    //     rd_idx = rd_base - 1 - (spp * LCD_W) + (col * spp)
    //
    //   つまり「最新の (LCD_W * spp) サンプル」を左→右に描画する。
    //   spp = 1 のときは従来と同じ 1 サンプル = 1 列になる。
    //   1 列に複数サンプルがある場合は先頭サンプルを代表値として使う。
    //
    //   col_cnt は 9bit 固定なので BUF_AW 幅へゼロ拡張する。
    //   spp * LCD_W が RAM の深さを超えないよう TB_SEL を選ぶこと
    //   (BUF_AW = 12 -> 4096, LCD_W = 320 なので spp <= 12)。
    wire [BUF_AW-1:0] col_off;
    generate
        if (BUF_AW <= 9) begin : g_col_off
            assign col_off = col_cnt[BUF_AW-1:0];
        end else begin : g_col_off
            assign col_off = {{(BUF_AW-9){1'b0}}, col_cnt};
        end
    endgenerate

    // 描画範囲の幅 (サンプル数) と、列ごとの読み出し間隔
    wire [15:0] spp_span_w = spp_r * LCD_W;
    wire [15:0] col_step_w = col_off * spp_r;

    wire [BUF_AW-1:0] spp_span = spp_span_w[BUF_AW-1:0];
    wire [BUF_AW-1:0] col_step = col_step_w[BUF_AW-1:0];

    wire [BUF_AW-1:0] rd_idx = rd_base - 1'b1 - spp_span + col_step;

    assign rd_addr = rd_idx;

    // ---- 棒グラフの位置 ----
    //   col_cnt は 0..319 なので、商と剰余からバー番号とバー内位置を得る。
    //   BAR_PITCH は 2 の冪でなくても動くよう除算器を使う (合成時に
    //   定数除算として最適化される)。
    wire [8:0] bar_off = col_cnt % BAR_P_C;   // バー内の x 位置
    wire [8:0] bar_n   = col_cnt / BAR_P_C;   // バー番号 (0..)

    // 棒グラフは bar_hold フレームの寿命を持つ。
    //   bar_seen (スペクトラム更新をラッチ) で BAR_HOLD にリセットし、
    //   フレームごとに 1 つ減らして 0 になったら表示しない (消去)。
    //   ※ どのバーを描くかは spectrum 側の bar_ram が 0 かどうかで決まる。
    wire       bar_alive = (bar_hold != 8'd0);
    wire       bar_vis   = BAR_EN && bar_alive && (bar_n < BAR_N);

    // スナップショット配列の安全なインデックス
    //   bar_n が BAR_N 以上のときは 0 を見る (bar_vis が 0 なので使われない)
    wire [5:0] bar_ix = bar_vis ? bar_n[5:0] : 6'd0;

    // ピクセル色 (S_PIX_RD で rdata / bar_data が有効)
    //   重ね順 : BAR_OVER_WAVE = 1 なら
    //              バー > 波形 > 中央線 > グリッド > 背景
    //            BAR_OVER_WAVE = 0 なら
    //              波形 > 中央線 > バー > グリッド > 背景
    //   b_data = 0 のビンはバーを描かない。
    function automatic [15:0] pixel_color(
        input [8:0] x,
        input [7:0] y,
        input [7:0] wy_lo,
        input [7:0] wy_hi,
        input       in_bar,
        input [7:0] b_data
    );
        logic [8:0] h;          // 表示する高さ (BAR_GAIN_SH で増幅)
        logic       in_bar_region;
        begin
            // 内部の高さを表示用に増幅し、画面高以上はクリップする
            //   9bit で扱うことで y >= (BAR_BOT - h) の引き算が
            //   アンダーフローしないようにする。
            h = b_data << BAR_SHF_C;
            if (h > BAR_BOT_9) h = BAR_BOT_9;

            // このピクセルがバー本体の範囲に入っているか
            //   y + h >= BAR_BOT ならバーの中 (下辺からの高さ h 以内)
            in_bar_region = in_bar && (b_data != 8'd0) &&
                            (({1'b0, y} + h) >= BAR_BOT_9);

            if (BAR_OVER_WAVE && in_bar_region)             pixel_color = COL_BAR;
            else if ((y >= wy_lo) && (y <= wy_hi))          pixel_color = COL_WAVE;
            else if (y == Y_OFFSET_C[7:0])                  pixel_color = COL_AXIS;
            else if (in_bar_region)                         pixel_color = COL_BAR;
            else if (x[4:0] == 5'd0)                        pixel_color = COL_GRID;
            else if (y[4:0] == 5'd0)                        pixel_color = COL_GRID;
            else                                             pixel_color = COL_BG;
        end
    endfunction

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state      <= S_WAIT;
            init_idx   <= '0;
            dly_cnt    <= '0;
            frm_cnt    <= '0;
            pix_cnt    <= '0;
            col_cnt    <= '0;
            row_cnt    <= '0;
            cur_color  <= '0;
            tx_start   <= 1'b0;
            tx_byte    <= '0;
            tx_dc_reg  <= 1'b0;
            rd_base    <= '0;
            spp_r      <= 8'd1;
            tb_r       <= '0;
            tb_clr     <= 1'b0;
            bar_hold   <= '0;
            bar_addr   <= '0;
            bar_cur    <= '0;
            ld_bin     <= '0;
            ld_ph      <= 2'd0;
            // bar_seen は専用の always_ff でリセットする
            // (ここで駆動すると multiple driver になる)
            init_done  <= 1'b0;
            frame_tick <= 1'b0;
        end else begin
            tx_start   <= 1'b0;
            frame_tick <= 1'b0;

            case (state)

                // ---- 電源 ON 待機 ----
                S_WAIT: begin
                    if (init_start) begin
                        init_idx <= '0;
                        state    <= S_INIT_TX;
                    end
                end

                // ---- 初期化コマンド/データ送信 ----
                S_INIT_TX: begin
                    if (init_idx < ROM_DEPTH) begin
                        if (!tx_busy && !tx_start) begin
                            {tx_dc_reg, tx_byte} <= rom_read(init_idx);
                            tx_start <= 1'b1;
                            state    <= S_INIT_WAIT;
                        end
                    end else begin
                        init_done <= 1'b1;
                        state     <= S_FRAME_START;
                    end
                end

                S_INIT_WAIT: begin
                    if (!tx_busy && !tx_start) begin
                        case (tx_byte)
                            8'h01: begin dly_cnt <= '0; state <= S_SWRST_DLY;  end
                            8'h11: begin dly_cnt <= '0; state <= S_SLPOUT_DLY; end
                            default: begin
                                init_idx <= init_idx + 7'd1;
                                state    <= S_INIT_TX;
                            end
                        endcase
                    end
                end

                // SWRESET 後 5ms
                S_SWRST_DLY: begin
                    if (dly_cnt == SWRST_CNT) begin
                        init_idx <= init_idx + 7'd1;
                        state    <= S_INIT_TX;
                    end else begin
                        dly_cnt <= dly_cnt + 24'd1;
                    end
                end

                // SLPOUT 後 120ms
                S_SLPOUT_DLY: begin
                    if (dly_cnt == SLPOUT_CNT) begin
                        init_idx <= init_idx + 7'd1;
                        state    <= S_INIT_TX;
                    end else begin
                        dly_cnt <= dly_cnt + 24'd1;
                    end
                end

                // ---- フレーム開始 ----
                // 書き込みポインタをラッチして描画範囲を固定する。
                // 時間軸 (spp_r) もここでラッチするので、tb_sel の変更は
                // 次フレームから反映される。棒グラフの寿命もここで減らす。
                S_FRAME_START: begin
                    rd_base <= wr_ptr;
                    spp_r   <= spp_next;
                    // 時間軸が変わったら表示が乱れるので、このフレームは
                    // 画面全体をクリアする (tb_clr は 1 フレームだけ 1)
                    tb_clr  <= (tb_r != tb_sel);
                    tb_r    <= tb_sel;

                    // スペクトラムが新しければ寿命を延ばし、
                    // そうでなければ 1 フレーム分減らす。
                    //   bar_seen は bar_fresh をラッチしたものなので、
                    //   FFT (約 5.6 ms) の更新を取りこぼさない。
                    if (bar_seen)       bar_hold <= BAR_HOLD_C[7:0];
                    else if (bar_alive) bar_hold <= bar_hold - 8'd1;

                    pix_cnt <= '0;
                    col_cnt <= '0;
                    row_cnt <= '0;
                    // このフレームで使うバーの高さを取り込む
                    ld_bin   <= '0;
                    ld_ph    <= 2'd0;
                    bar_addr <= '0;
                    state    <= S_BAR_LD;
                end

                // ---- バーの高さのスナップショット ----
                //   bar_addr も bar_data も 1 クロック遅れのレジスタなので、
                //   アドレスを出してから値が返るまで 2 サイクルかかる。
                //     位相 0 : bar_addr に bin を出す
                //     位相 1 : bar_addr -> bar_ram の読み出しを待つ
                //     位相 2 : bar_data が有効。配列に取り込む
                //   1 bin あたり 3 サイクル。64 bin でも 192 サイクル
                //   (≒ 3.8 us) なのでフレーム時間には影響しない。
                //
                //   ここで取り込んだ値をこのフレームの描画にだけ使う。
                //   これでフレーム中に bar_ram が変わっても、1 フレーム内
                //   では 1 つの値だけが使われ、バーが途中で途切れない。
                S_BAR_LD: begin
                    case (ld_ph)
                        2'd0: begin
                            bar_addr <= ld_bin;
                            ld_ph    <= 2'd1;
                        end
                        2'd1: begin
                            ld_ph <= 2'd2;
                        end
                        default: begin
                            bar_snap[ld_bin] <= bar_data;
                            ld_ph <= 2'd0;
                            if (ld_bin == BAR_LAST_C)
                                state <= S_PIX_CALC;
                            else
                                ld_bin <= ld_bin + 6'd1;
                        end
                    endcase
                end

                // ---- 読み出しアドレス設定 ----
                // 時間軸が変わった直後の 1 フレームは画面全体をクリアする。
                //
                // この列で使うバーの高さもここでラッチする。
                //   bar_n = col_cnt / BAR_P_C は除算 (定数除算だが
                //   パイプライン化される) で遅延が大きく、さらに
                //   64 エントリのバー高さ選択が続くため、これらを
                //   pixel_color と同じサイクルに入れるとタイミングが
                //   厳しくなる。ここで 1 度レジスタに取ることで
                //   経路を 2 サイクルに分割する。
                S_PIX_CALC: begin
                    bar_cur <= bar_snap[bar_ix];
                    state   <= S_PIX_RD;
                end

                // ---- 波形 RAM 読み出し待ち (1 サイクル) ----
                S_PIX_RD: begin
                    if (tb_clr) cur_color <= COL_BG;
                    else        cur_color <= pixel_color(col_cnt, row_cnt,
                                                         y_lo, y_hi,
                                                         bar_vis && (bar_off < BAR_W_C),
                                                         bar_cur);
                    state <= S_PIX_HI_TX;
                end

                // ---- 上位バイト送信 ----
                S_PIX_HI_TX: begin
                    if (!tx_busy && !tx_start) begin
                        tx_byte   <= cur_color[15:8];
                        tx_dc_reg <= 1'b1;
                        tx_start  <= 1'b1;
                        state     <= S_PIX_HI_W;
                    end
                end

                S_PIX_HI_W: begin
                    if (!tx_busy && !tx_start) state <= S_PIX_LO_TX;
                end

                // ---- 下位バイト送信 ----
                S_PIX_LO_TX: begin
                    if (!tx_busy && !tx_start) begin
                        tx_byte   <= cur_color[7:0];
                        tx_dc_reg <= 1'b1;
                        tx_start  <= 1'b1;
                        state     <= S_PIX_LO_W;
                    end
                end

                // ---- カウンタ更新 ----
                S_PIX_LO_W: begin
                    if (!tx_busy && !tx_start) begin
                        if (col_cnt == LAST_COL) begin
                            col_cnt <= '0;
                            row_cnt <= row_cnt + 8'd1;
                        end else begin
                            col_cnt <= col_cnt + 9'd1;
                        end

                        if (pix_cnt == LAST_PIX) begin
                            frame_tick <= 1'b1;
                            frm_cnt    <= '0;
                            state      <= S_FRAME_END;
                        end else begin
                            pix_cnt <= pix_cnt + 1'b1;
                            state   <= S_PIX_CALC;
                        end
                    end
                end

                // ---- フレーム間待機 ----
                S_FRAME_END: begin
                    if (frm_cnt == FRAME_WAIT_C)
                        state <= S_NEXT_FRAME;
                    else
                        frm_cnt <= frm_cnt + 24'd1;
                end

                // ---- Memory Write 再発行 -> 次フレーム開始 ----
                // ILI9341 は 0x2C を送るとアドレスポインタが先頭に戻る
                S_NEXT_FRAME: begin
                    if (!tx_busy && !tx_start) begin
                        tx_byte   <= 8'h2C;
                        tx_dc_reg <= 1'b0;
                        tx_start  <= 1'b1;
                        state     <= S_FRAME_START;
                    end
                end

                default: state <= S_WAIT;
            endcase
        end
    end

endmodule
広げる

spectrum.sv

// spectrum.sv
//   パワースペクトル (mag2) を LCD 表示用の棒グラフ高さに変換する
//
//   動作:
//     1. fft128 が 1 フレーム分 (64 bin) の mag2 を書き終えると
//        frame_done が 1 クロックパルスする。
//     2. それを合図に bin 0..63 を順に読み出し、log2 圧縮して
//        バーの高さ (0..MAX_VAL px) に変換する。
//     3. bar_ram に書き込み、LCD 側は bar_addr で読み出す。
//
//   圧縮方法 (log2 圧縮):
//     mag2 (34bit) の最上位 1 の位置 exp と、その直後の 7bit 仮数
//     mant を取り出し、LOG2_LUT を使って
//         q4 = exp*16 + 16*log2(1 + mant/128)
//     を求める。16 = 1 オクターブなので q4 は log2 を 16 倍した値。
//     これを線形に 0..MAX_VAL へ写像する。
//
//       オクターブ = 6.02 dB なので、q4 1 単位 ≒ 0.376 dB
//
//     LOG_MIN はバー高さ 0 になるしきい値 (ノイズフロア)。
//     LOG_MUL / LOG_SH がゲイン (LOG_MUL / 2^LOG_SH)。
//
//     q4 は最大 544 (msb_pos=33, LUT=16) 程度になるので、
//     LOG_MUL との積は 15bit 以上必要。ここでは 18bit で受ける。
//
//         height = clip( (q4 - LOG_MIN) * LOG_MUL / 2^LOG_SH )
//
//     ゲインの単位は px / octave (1 octave = 6.02 dB)。
//     例) LOG_MUL=11, LOG_SH=4 -> 0.69 px/octave
//         LOG_MUL=33, LOG_SH=4 -> 2.06 px/octave (= ちょうど 3 倍)
//
//   ピークホールド (減衰):
//     前フレームより小さい値は DECAY px/フレームずつ減らす。
//     値が急に消えずスペクトラムアナライザらしい見た目になる。
//
//   棒ごとの表示時間 (BAR_LIFE):
//     各 bin は「あと何フレーム表示するか」のカウンタを持つ。
//     信号があるフレームは寿命を BAR_LIFE に戻し、
//     信号が消えたフレームから 1 つずつ減らして 0 になったら
//     そのビンのバーを 0 にする。
//
//     -> **これがバーの表示時間を決める本命のカウンタ**。
//        信号が消えてから BAR_LIFE フレームの間、
//        そのビンはピーク値を保持して表示され続ける。
//
//     この方式なら LCD 側のフレーム位相に依存しない
//     (LCD の寿命カウンタだと、FFT の 1 クロックパルスを
//      フレーム開始で取りこぼして即座に消えてしまう)。
//
//     ◆ 単位は FFT フレーム (128 サンプル = 6.4 ms) ◆
//     LCD は約 148 ms ごとにしか bar_ram を読まないので、
//     BAR_LIFE が 1 LCD フレーム分 (約 23 FFT フレーム) より
//     小さいと、LCD が読む前に寿命切れで 0 になりバーが点滅する。
//     BAR_LIFE は 25 以上にすること。
//
//  無音時の一括消去 (SIL_FRAMES):
//     フレーム全体が無音 (最大 mag2 < SIG_TH) のまま SIL_FRAMES
//     フレーム続いたら、全ビンを 0 にする。
//     ◆ SIL_FRAMES は BAR_LIFE 以上にすること ◆
//     小さいとビンごとの寿命より先に全部消えてしまい、
//     BAR_LIFE をいくら上げても表示が伸びない (以前は 2 で、
//     これが「表示がすぐ消える」原因だった)。
//
//   fft128 側の mag2 読み出しは 1 クロック遅れなので、
//   アドレスを出してから 2 クロック待ってから値を使う。
//
//   対数圧縮は組み合わせ遅延が大きいため、以下の 4 段にパイプライン化
//   している (50 MHz / 20 ns に収めるため)。
//     1. 最上位ビット位置 (優先エンコーダ)
//     2. 正規化シフト (仮数部の抽出)
//     3. log2 LUT + 加算
//     4. ゲイン + クリップ + ピークホールド判定
// ==================================================

`include "fft_tables.svh"

module spectrum #(
    parameter int N_BINS    = 64,    // 変換するビン数 (= fft128 の HALF)
    parameter int START_BIN = 1,     // この bin から処理する (0 = DC は除外)
    parameter int MAX_VAL   = 239,   // バーの最大高さ (px)
    parameter int LOG_MIN   = 128,   // バー 0 になる q4 (= 2^8 -> mag2 256)
    parameter int LOG_MUL   = 11,    // ゲイン分子
    parameter int LOG_SH    = 4,     // ゲイン分母 (2^LOG_SH)
    parameter int DECAY     = 2,     // 1 フレームあたりの減衰量 (px)
    parameter int SIG_TH    = 1024,  // 無音とみなす mag2 のしきい値
    parameter int BAR_LIFE  = 6,     // バーの表示時間 (FFT フレーム数)
    parameter int SIL_FRAMES = 2     // 全ビン一括消去までの無音フレーム数
) (
    input  logic        clk,
    input  logic        rst_n,

    // --- fft128 との接続 ---
    output logic [6:0]  mag_addr,      // 読み出す bin
    input  logic [33:0] mag2,          // パワー (1 クロック遅れ)
    input  logic        frame_done,    // 1 フレーム完了パルス

    // --- LCD 側の読み出し ---
    input  logic [5:0]  bar_addr,      // 表示したい bin
    output logic [7:0]  bar_data,      // バーの高さ (1 クロック遅れ)

    // --- ステータス ---
    output logic        busy,          // 変換中
    output logic        bar_fresh      // バーを更新した (1 フレーム 1 パルス)
);

    // =============================================
    // 定数
    // =============================================
    localparam int LAST_BIN = N_BINS - 1;

    // 型付き定数 (古いツールはサイズ付きキャストを持たないため
    // localparam で幅を明示する)
    localparam logic [5:0]  LAST_BIN_C  = LAST_BIN[5:0];
    localparam logic [6:0]  START_BIN_C = START_BIN[6:0];
    localparam logic [10:0] MAX_VAL_C   = MAX_VAL;
    localparam logic [9:0]  LOG_MIN_C   = LOG_MIN;
    localparam logic [7:0]  DECAY_C     = DECAY;
    localparam logic [33:0] SIG_TH_C    = SIG_TH;
    //   表示時間は ms 単位で 1 秒以上にもなるため 16bit
    localparam logic [15:0] BAR_LIFE_C  = BAR_LIFE;
    localparam logic [15:0] SIL_FRAMES_C = SIL_FRAMES;

    // ==============================================
    // バーの高さ RAM (LCD 読み出しポート)
    // ==============================================
    logic [7:0] bar_ram [0:N_BINS-1];

    // 各 bin の寿命カウンタ (FFT フレーム数)。0 = もう表示しない
    //   数秒の保持を扱えるよう 16bit
    logic [15:0] bar_life [0:N_BINS-1];

    always_ff @(posedge clk) begin
        bar_data <= bar_ram[bar_addr];
    end

    // ==============================================
    // mag2 -> バーの高さ
    //  パイプライン各段の組み合わせ演算
    // ==============================================

    // --- 段 1 : 最上位ビット位置 (優先エンコーダ) ---
    logic [5:0] msb_pos;
    integer     idx;

    always_comb begin
        msb_pos = 6'd0;
        // 小さい方から見て「あと勝ち」にすることで最上位が残る
        for (idx = 0; idx <= 33; idx = idx + 1)
            if (mag2[idx]) msb_pos = idx[5:0];
    end

    // --- 段 2 : 正規化 (仮数部 = 最上位 1 の直後 7bit) ---
    logic [33:0] m2_r;      // 段 1 で取り込んだ mag2
    logic [5:0]  exp_r;     // 段 1 で取り込んだ msb_pos
    logic [6:0]  mant_r;    // 段 2 の結果

    wire [5:0]  shamt    = 6'd33 - exp_r;
    wire [33:0] norm     = m2_r << shamt;
    wire [6:0]  mant_now = norm[32:26];

    // --- 段 3 : q4 = 16*log2(mag2) ---
    logic [9:0] q4_r;

    wire [9:0] q4_now = {exp_r, 4'b0} + {5'b0, LOG2_LUT[mant_r]};

    // --- 段 4 : 高さへの写像 ---
    logic [7:0] h_r;        // 段 4 の結果 (しきい値クリップ済み)
    logic [7:0]  old_val;   // 前フレームのバー高さ
    logic [15:0] life_r;    // 前フレームの寿命カウンタ

    //   q4 <= LOG_MIN なら 0、それ以外はゲインを掛けてクリップする。
    //   積は (q4 - LOG_MIN) <= 544, LOG_MUL <= 255 なので 18bit。
    wire [10:0] rel = (q4_r > LOG_MIN_C) ? (q4_r - LOG_MIN_C) : 11'd0;
    wire [17:0] gain = rel * LOG_MUL;
    wire [17:0] gsh  = gain >> LOG_SH;
    wire [7:0]  height_now = (gsh > MAX_VAL_C) ? MAX_VAL_C[7:0] : gsh[7:0];

    // ピークホールド : 前フレームより低い値は DECAY ずつしか減らさない
    wire [7:0] decayed = (old_val > DECAY_C) ? (old_val - DECAY_C) : 8'd0;

    // 寿命の判定
    //   has_sig : このフレームにノイズフロアを超える値があった
    //   expired : 寿命カウンタが 0 -> もう表示しない (バーは 0)
    //
    //   信号があれば毎フレーム寿命をリセットするので、表示中は消えない。
    //   信号が無くなると BAR_LIFE フレーム後にそのビンだけ 0 になる。
    //   信号があるフレームは寿命に関係なく即座に表示する
    //   (クリア直後の 1 フレーム目から見えるようにするため)。
    wire        has_sig   = (height_now != 8'd0);
    wire        expired   = (life_r == 16'd0);
    wire [15:0] life_next = has_sig    ? BAR_LIFE_C
                          : expired    ? 16'd0
                                       : (life_r - 16'd1);
    // バーの高さ : ピークホールド (信号が無いときは DECAY で減衰)
    wire [7:0]  held      = (height_now >= old_val) ? height_now : decayed;
    wire [7:0]  bar_next  = (has_sig || !expired)   ? held : 8'd0;

    // ==============================================
    // ステートマシン
    // ==============================================
    localparam [2:0]
        S_CLR  = 3'd0,   // 起動時に bar_ram を 0 で埋める
        S_IDLE = 3'd1,   // frame_done 待ち
        S_W1   = 3'd2,   // mag_addr を出して 1 クロック待つ
        S_ENC  = 3'd3,   // mag2 が有効 : 最上位ビット位置を求める
        S_NORM = 3'd4,   // 仮数部を抽出
        S_LOG  = 3'd5,   // log2 LUT + 加算
        S_GAIN = 3'd6,   // ゲイン + クリップ
        S_HOLD = 3'd7;   // ピークホールド判定

    logic [2:0] state;
    logic [5:0] bin;
    logic [5:0] clr_bin;

    // ---- 無音検出 ----
    //   フレーム中に読み出した mag2 の最大値が SIG_TH 未満なら無音。
    //   無音が SIL_FRAMES フレーム続いたら、その次のフレームで
    //   全バーを 0 に消去する (消去は 1 フレームで完了する)。
    //
    //   判定は「読み出しパスが終わった時点」で行う。フレーム開始時に
    //   見ると、読み出し前の pk (= 0) で判断してしまい、信号が現れた
    //   最初のフレームを消してしまうため。
    //
    //   消去後は sil_cnt を 1 に戻す (ヒステリシス)。これで無音が
    //   ずっと続いても消去は SIL_FRAMES ごとに 1 回で済む。
    logic [33:0] pk;            // このフレームの最大 mag2
    logic [15:0] sil_cnt;       // 無音が連続したフレーム数
    logic        wipe;          // このパスは消去のみ行う

    wire        silent   = (pk < SIG_TH_C);
    wire [15:0] sil_inc  = (sil_cnt == 16'hFFFF) ? 16'hFFFF : (sil_cnt + 16'd1);
    wire        wipe_go  = silent && (sil_inc >= SIL_FRAMES_C);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state    <= S_CLR;
            bin      <= '0;
            clr_bin  <= '0;
            mag_addr <= '0;
            m2_r     <= '0;
            exp_r    <= '0;
            mant_r   <= '0;
            q4_r     <= '0;
            h_r      <= '0;
            old_val  <= '0;
            life_r   <= '0;
            pk        <= '0;
            sil_cnt   <= '0;
            wipe      <= 1'b0;
            bar_fresh <= 1'b0;
        end else begin
            bar_fresh <= 1'b0;
            case (state)

                // ---- 起動時のクリア (配列を for で消すと
                //      ラッチ推論の警告が出るため 1 個ずつ消す) ----
                S_CLR: begin
                    bar_ram[clr_bin]  <= 8'd0;
                    bar_life[clr_bin] <= 16'd0;
                    if (clr_bin == LAST_BIN_C) begin
                        state <= S_IDLE;
                    end else begin
                        clr_bin <= clr_bin + 6'd1;
                    end
                end

                // ---- FFT 1 フレーム完了待ち ----
                S_IDLE: begin
                    if (frame_done) begin
                        // DC (bin 0) は Hann 窓の漏れで常に最大に
                        // なってしまうため START_BIN から始める。
                        // bar_ram[0] はクリア時の 0 のまま残る。
                        bin      <= START_BIN_C[5:0];
                        mag_addr <= START_BIN_C;
                        pk       <= '0;
                        state    <= S_W1;
                    end
                end

                // ---- mag_addr 出力後 2 クロック待つ ----
                S_W1: state <= S_ENC;

                // ---- 段 1 : mag2 が有効。エンコーダと一緒に取り込む ----
                S_ENC: begin
                    m2_r    <= mag2;
                    exp_r   <= msb_pos;
                    old_val <= bar_ram[bin];
                    life_r  <= bar_life[bin];
                    if (!wipe && (mag2 > pk)) pk <= mag2;   // 最大値を記録
                    state   <= S_NORM;
                end

                // ---- 段 2 : 仮数部 ----
                S_NORM: begin
                    mant_r <= mant_now;
                    state  <= S_LOG;
                end

                // ---- 段 3 : log2 ----
                S_LOG: begin
                    q4_r  <= q4_now;
                    state <= S_GAIN;
                end

                // ---- 段 4 : ゲインとクリップ ----
                S_GAIN: begin
                    h_r   <= height_now;
                    state <= S_HOLD;
                end

                // ---- ピークホールド判定 + 書き込み ----
                S_HOLD: begin
                    // 消去フレームは強制的に 0 を書く
                    if (wipe) begin
                        bar_ram[bin]  <= 8'd0;
                        bar_life[bin] <= 16'd0;
                    end else begin
                        // 寿命が切れていれば 0、まだあれば寿命内の高さを書く
                        bar_ram[bin]  <= bar_next;
                        bar_life[bin] <= life_next;
                    end

                    if (bin == LAST_BIN_C) begin
                        // 読み出しパス終了。ここで無音を判定して次の
                        // フレームの動作を決める。
                        if (wipe) begin
                            // 消去し終わった -> ヒステリシスのため 1 に戻す
                            sil_cnt <= 16'd1;
                            wipe    <= 1'b0;
                        end else begin
                            sil_cnt <= silent ? sil_inc : 16'd0;
                            wipe    <= wipe_go;   // 次のフレームで消去する
                        end
                        bar_fresh <= 1'b1;
                        state     <= S_IDLE;
                    end else begin
                        bin      <= bin + 6'd1;
                        mag_addr <= {1'b0, bin} + 7'd1;
                        state    <= S_W1;
                    end
                end

                default: state <= S_IDLE;
            endcase
        end
    end

    assign busy = (state != S_IDLE);

endmodule
広げる

参考文献

(1)GitHubリポジトリ

https://github.com/Lathe-Mariel/PCB_CQmax10/tree/main/CQmax10/sample/microphone

2件のコメントがあります

コメントを残す