YM2151 (OPM) の音声出力はアナログではなく、専用 DAC「YM3012」に渡すための シリアルデジタルです。実チップの YM3012 を入手せずに、ESP32-S3 でこのシリアル ストリームを直接デコードしてステレオ PWM 再生する手法をまとめます。
実装は nes-rom-reader の feature/ym2151 ブランチ (ファミコンカセット吸い出し機 + 改造カートリッジに載せた YM2151 を駆動する構成)。
YM2151 から出る信号は4本:
| 信号 | 内容 |
|---|---|
| SO | シリアルデータ。1本に L/R が時分割で乗る |
| SH1 | RIGHT ch ワードの区切りパルス |
| SH2 | LEFT ch ワードの区切りパルス |
| φ1 | ビットクロック (= φM/2。φM=4MHz なら 2MHz) |
- サンプルレートは φM/64(4MHz 駆動で 62.5kHz/ch)
- 1チャンネルスロット = φ1 × 16クロック。先頭3ビットは捨てビット、 続いて 仮数10bit(B0 が LSB、オフセットバイナリ)+ 指数3bit(S0 が LSB) の13bit
- データは φ1 の立ち下がりで変化するので立ち上がりでサンプルする
- ワードは SH1/SH2 の立ち下がり時点の直前13bitとして確定する
- 指数
000は仕様上の禁止値(これが化けワード検出に使える。後述)
φ1=2MHz のエッジを1つも落とさず拾うのがキモ。
- Dedicated GPIO を使う。通常の
GPIO.inレジスタ読みは APB 経由で 約150nsかかり取りこぼすが、Dedicated GPIO バンドルは CPU 命令で 1サイクル読みできる。専用コア(Core 0)のタイトループでポーリング- 注意: バンドルは「配列順=ビット順」。GPIO 番号昇順で作らないと ビット対応がズレる
- 割り込みは止めない(止めると割り込みウォッチドッグでパニックする)。 tick 等で稀に落ちるビットは後段の検証で捨てる
- φ1 立ち上がりで SO をシフトイン、SH1/SH2 立ち下がりでワード確定
- エッジ数検証: 前回のラッチからの φ1 エッジ数がハーフフレーム分
(16±α)にならないフレームは破棄して直前値を保持
- 先頭3ビットは捨てビットなので 13〜16 は無条件で採用
- 17 は余分な1ビットがワード後に入った状態なので、抽出位置を 1ビット補正して採用(補正せずデコードすると化ける)
- SH2 未配線ならモノラル(SH1→SH1 の 32±α 窓)として動く
// 13bit ワード → 16bit 相当 PCM
m = (sr >> 19) & 0x3FF; // 仮数 (シフトレジスタの最新13bitから)
e = (sr >> 29) & 0x07; // 指数
if (e == 0) reject(); // 禁止値 = 化けワード。実測で毎秒数百個ブロックできた
v = 2*m - 1023; // ±1023 (半LSB中心化)
pcm = (v * 32) >> (7 - e); // 負数の << は UB なので乗算で- LEDC で 78.125kHz / 9bit の PWM を2チャンネル(L/R独立)
- SH ラッチのたびにデューティ更新 = チップのサンプルレートに完全同期 (リサンプリング不要、ピッチずれゼロ)
- 1次ノイズシェーピング: 16bit→9bit の量子化誤差を次サンプルへ繰り越し。 クリップ時のワインドアップ防止に誤差を±1量子へクランプ
- median-3 フィルタ: EMI 等で稀に化ける1サンプルのスパイク (「ぶちぶち」ノイズ)を除去。音楽波形への影響は実質なし
- 後段に RC ローパス(例: 2.4kΩ + 4.7nF、fc≒14kHz)+ DC カット 1µF → アンプ。 LPF なしだとキャリアとシェーピングノイズが素通しになり逆効果
- ラッチ処理(デコード+ PWM レジスタ直叩き)は次スロットの捨てビット区間
(φ1×3 ≒ 1.5µs @4MHz)に収める。関数呼び出し(
ledcWrite)は遅すぎるので LEDC の LL レジスタを直接叩く - 検証窓を「処理遅延で落ちる捨てビット」まで許容しないと、ステレオ化した 途端に採用率が半減して音がガタガタになる(実体験)
- LEDC は全タイマーがクロック源を共有 — 別用途(φM 生成)と音声 PWM で
ソースが衝突すると片方が黙って壊れる。Arduino の
ledcAttachChannelは ch/2 でタイマーが決まる点にも注意 - ESP32 の USB-Serial-JTAG は受信バッファが溢れると黙って捨てる — 曲データのストリーミングにはアプリレベルのクレジット式フロー制御が必須
- 未使用の /OE 系ピンをフローティングにしない(CMOS 入力が不定になり バスを壊す)。必ず H/L に固定
- 電源: FM 8ch フル演奏+アンプでピーク電流が跳ね、
esp_reset_reason()が Brownout を返す落ち方をする。バルクコンデンサで解消 - 検証だけでは守れない化け方(データビットの化け)は、指数禁止値チェックと median-3 の二段構えで潰す
- 採用率 ~99%、62.5kHz/9bit ステレオ、X68000 ネイティブピッチ(φM=4MHz)
- MDX(portable_mdx の再生ロジックで レジスタイベント列に変換)と VGM(レジスタログをそのまま変換)を実チップ再生
- 34秒の曲を65周(約37分)連続再生できる安定度
デジタル段の S/N は実 YM3012 + アナログ段より素直なくらいで、 「YM3012 が手に入らないから鳴らせない」は過去の話になりました。