<<前回のプロジェクト(生波形を表示するまで)
マイクで取った音声データをFFTして周波数ごとの成分を得ます.生波形の上にビンをオーバレイで表示します.
FPGAはMAX10,IDEはVSCodeを使います.Copilot AgentがOllama経由でdeepseek (v4.1-flash) を使います.Copilot AgentはローカルPC内のQuartus Prime Lite をCLIで使います.
指示するだけで,RTLコードの生成,Questaによるシミュレーション,Quartusでの合成まで行われ,動作するコードが手に入ります(正しく動くとは限らない).
まさかのWindows10でやります.
●作るもの仕様
サンプリングレート:約20kHz
FFTするレンジ:0-10kHz
ビンの数:64

▲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
ピンバック: deepseek-v4.1-flashでRTL開発 -shader- | FPGAマガジン公式
ピンバック: deepseek-v4.1-flashでRTL開発 -Microphone- | FPGAマガジン公式