NES(ファミコン)のノイズジェネレーターを設計しよう 解析・設計編

NESのノイズジェネレーターについて 

NESのノイズは計算で説明がつく構造なのである程度の情報で設計が可能です。
音を作っているのはAPUと呼ばれるメインCPUです。 
まず(横に)15ビットPRBS(LFSR)だそうです。これだけで回路が分かります。  


RolandのTR-909のノイズジェネレーターは(横に)17ビットPRBSで出来ています。PRBSの特徴として波形が方形波なんです。0か1かの時間がバラバラなのです。
なので回路上では17ビットPRBSは17ステージの~と表現します。確かに1ビットなのに17ビットPRBSでは話を難しくするだけです。 
バラバラではありますが論理回路ですからどうしても繰り返しのループが発生してしまいます。ですから何クロック目にはビット列はこうなる。と、計算できてしまうのです。
アーケードゲームでもこのタイミングでスタートするとテトリスのブロックの発生パターンは必ずこうなるという攻略法も存在します。
人間にとっては乱数に見えるだけでコンピューターにとっては小さなアルゴリズムで作る大きな分散された表なのです。
話を戻しましょう。
 

疑似乱数の秘密はフィードバックにあり 

 
色々難しい話になるので簡単に説明します。
まず15ビットの乱数(効率よく分散された表)を計算で作るにはDフリップフロップを連結したビットシフターという特殊な論理回路を使います。15ステージ必要ですが、ただシフトするだけではただの1ビットデジタルディレイですのでExclusiveORという論理回路を2つか4つ使い、途中のステージの出力がどちらも0か1の場合1としてフィードバックします。このフィードバックが疑似乱数を作るのです。
 
 

イメージできます?

下の方でシミュレーションしますけど、1ビット15段デジタルディレイの途中の段の2つを取り出してEx-ORしてフィードバック。
「乱数(効率よく分散された表)」と書いたようにここで作られるのは計算された周期のあるデータになります。 その計算式を実現するのが下の表です。
 
bit(ステージ) Ex-ORで抜き取るステージ
3 1, 3
4 3, 4
5 3, 5
6 5, 6
7 6, 7
8 4, 5, 6, 8
9 5, 9
10 7, 10
11 9, 11
12 4, 10, 11, 12
13 8, 11, 12, 13
14 2, 12, 13, 14
15 14, 15
16 4, 13, 15, 16
17 14, 17
このようにするとうまく分散されるそうです。なぜ?と思う方は説明が長くなるので省略します。 
あれ?TR-909は17ステージで13,31抜取です、後でリンクを張りますがそれでも間違ってはいないそうです。
PSG(SN76489)は16ステージで13,16抜取のようです。これで古いアーケードゲームのノイズもOKですね。
 

いざ、シミュレーションしてみましょう。

 
Resetに使うExORは100kΩによってシュミットトリガーとして使われる特殊な方法


 
リンクはこちら
 
 
右に再生ボタンがありますがシミュレートを進めないとデータも作られないので再生できません。
最高速にするとすぐ再生できます。 6ステージだと音で繰り返しがわかりますね。
 
シュミットトリガー部分はたまたまTR-909の回路を使ったので余裕があるなら100kΩ無しでのバッファー1つやNAND回路2つ(NOT+NOT)で置き換え可能です。 
 
PRBSは何ステージでも1ビット表現ですからTR-909もクロックを440Hzにするとループがわからないファミコンのノイズになります。
(月面着陸に使うスラスター音のような)

とりあえずAPUとPSGのノイズはこんな回路で出来ちゃいます。

音は無加工のまま右に進むので音の取り出し口は1つで問題ありません。ループ音も再現され問題ありません。



リンクはこちら


そしてシフトレジスターって何?1ビットでディレイ作って意味無くね?と思う方はコンピューターのCPUについて学ぶ事になるので省略します。
さらにDフリップフロップって何?甘いの?と思う方はロジックICの種類と記号を学びましょう。回路図を読みたい人は覚えて損はないです。 

 困ったことにシフトレジスターって4ステージ(4ビット)単位なので抜き取りたい段数が4の倍数だけではないのでDフリップフロップと繋げて設計します。
手元にTR-909で使われているCD4006BE(4ステージ,4+1ステージ)x2が沢山ありますのでそれを使います。
細かくステージの出力を抜き取るにはDフリップフロップ(CD40175BEか74HC175)を使います。
全部Dフリップフロップ使ってもいいですよ。ただし面積は4倍以上大きくなります。 

CD4006BEの+1ステージ部分だけDフリップフロップ単独で使えないの?
残念ですが使えません。これがデータシートです。


+1部分のQ(D)に繋がっていれば使えたんですけどね。なので4ステージか5ステージで使うしかありません。
細かく刻むには何も心配しないベーシックなつくりのCD40175BEがおすすめです。

次はクロック部分をAIで生成させましょう。

そしてさらに考えなければいけないのは与えるクロックスピードです。
https://www.nesdev.org/wiki/APU_Noise
NTSC   4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068 (2046 in old)
 
NTSCクロックから分周し、16種類クロックを与えるようです。4068分周は440Hzですね。
はい困りました。
すべてが1/2で出来た世界ではありません。まぁ現代なら小型のMCUを使ってクロック生成してしまいましょう。元々APUの中の出来事です。わざわざロジック回路で作っても面積がもったいないです。
もちろん440Hzから4オクターブの矩形波作ってもいいですよ。
5Vな矩形波のサウンドで良いのです。本物志向から外れますが楽しめます。
 
以下Seeeduino XIAOでのソースです。AIに生成してもらいました。無保証です。
 
 

このコードは、SAMD21マイコンのGCLK (Generic Clock Controller) および TCC (Timer/Counter for Control Applications) という機能を直接利用して、正確な周波数を効率的に生成します。

回路接続

まず、以下のように部品を接続してください。

  • A0ピン: ボリューム(可変抵抗)の真ん中の端子に接続します。ボリュームの両端の端子は、それぞれ 5VGND に接続します。

  • D1ピン: 周波数が可変のクロックが出力されます。

  • D6ピン: 周波数が固定のクロックが出力されます。


Arduino用ソースコード

以下のコードをコピーして、Arduino IDEに貼り付けてXIAO SAMD21に書き込んでください。

/**
 * @file XIAO_SAMD21_Variable_Clock_Generator.ino
 * @author Gemini
 * @brief Seeed Studio XIAO SAMD21で、指定仕様のクロックを生成するコード
 * @version 1.0
 * @date 2025-09-22
 * * @details
 * - D1ピン: A0のAD値に応じて、3.579545 MHzを16段階に分周したクロックを出力
 * - D6ピン: 常に約137kHzのクロックを出力
 * - A0ピン: 分周比を切り替えるためのアナログ入力 (ボリュームを接続)
 */

// --- ピン定義 ---
const int VARIABLE_CLOCK_PIN = D1;  // 可変クロック出力ピン (PA04)
const int ADC_INPUT_PIN = A0;       // AD入力ピン
const int FIXED_CLOCK_PIN = D6;     // 固定クロック出力ピン (PA10)

// --- 定数 ---
const float BASE_FREQ = 3579545.0f; // 基準となる周波数 (Hz)
const uint32_t GCLK0_FREQ = 48000000UL; // SAMD21のメインクロック (48MHz)
const uint32_t FIXED_CLOCK_TARGET_FREQ = 137000UL; // D6から出力する固定クロックの目標周波数 (Hz)

// 16段階の分周比を格納した配列
const uint16_t DIVIDERS[16] = {
  4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068
};

// 前回のADレベルを記憶する変数 (初回更新用)
int last_adc_level = -1;

// --- プロトタイプ宣言 ---
void setupFixedClock();
void setupVariableClock();
void updateVariableClock(int level);

//================================================================
//  セットアップ関数
//================================================================
void setup() {
  // D6ピンに固定周波数クロックを出力する設定
  setupFixedClock();

  // D1ピンに可変周波数クロックを出力する設定
  setupVariableClock();
  
  // A0ピンを入力に設定
  pinMode(ADC_INPUT_PIN, INPUT);

  // 初回実行時にデフォルトのクロック(分周比4)を設定
  updateVariableClock(0); 
}

//================================================================
//  メインループ
//================================================================
void loop() {
  // A0ピンからAD値を取得 (0-1023)
  int adc_value = analogRead(ADC_INPUT_PIN);
  
  // AD値を16段階のレベル (0-15) に変換
  int current_level = map(adc_value, 0, 1023, 0, 15);

  // レベルが変更された場合のみ、クロックの周波数を更新
  if (current_level != last_adc_level) {
    updateVariableClock(current_level);
    last_adc_level = current_level;
  }
  
  // 15ms待機して、処理負荷を軽減
  delay(15);
}

//================================================================
//  D1ピン: 可変クロックの周波数を更新する関数
//================================================================
void updateVariableClock(int level) {
  // 配列から現在の分周比を取得
  uint16_t divider = DIVIDERS[level];
  
  // 目標の周波数を計算
  float target_freq = BASE_FREQ / divider;
  
  // TCC0の周期レジスタに設定する値を計算
  // 周波数 = GCLK周波数 / (PER + 1) => PER = (GCLK周波数 / 周波数) - 1
  uint32_t period_value = (uint32_t)(((float)GCLK0_FREQ / target_freq) - 1.0f);
  
  // TCC0の周期(PER)とデューティ比(CC)を更新
  // バッファレジスタ(PERBUF, CCBUF)を使うことで、出力波形にグリッチが発生するのを防ぎます
  TCC0->PERBUF.reg = period_value;
  TCC0->CCBUF[4].reg = period_value / 2; // デューティ比50%
}

//================================================================
//  D6ピン: 固定クロック(約137kHz)の初期設定
//================================================================
void setupFixedClock() {
  // 1. GCLKジェネレータ5を有効化し、48MHzをソースに設定
  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |           // デューティ比を50%に補正
                      GCLK_GENCTRL_GENEN |         // ジェネレータを有効化
                      GCLK_GENCTRL_SRC_DFLL48M |   // ソースとして48MHz DFLLを選択
                      GCLK_GENCTRL_ID(5);          // GCLKジェネレータ5を選択
  while (GCLK->STATUS.bit.SYNCBUSY);               // 同期完了を待つ

  // 2. GCLK5の分周比を設定
  // 48,000,000 Hz / 351 = 136,752 Hz (約137kHz, 誤差 -0.18%)
  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(351) |        // 分周比を351に設定
                     GCLK_GENDIV_ID(5);            // GCLK5に適用
  while (GCLK->STATUS.bit.SYNCBUSY);               // 同期完了を待つ

  // 3. D6ピン(PA10)をGCLK5の出力として設定 (ピンマルチプレクサ機能)
  pinMode(FIXED_CLOCK_PIN, OUTPUT);
  PORT->Group[g_APinDescription[FIXED_CLOCK_PIN].ulPort].PINCFG[g_APinDescription[FIXED_CLOCK_PIN].ulPin].bit.PMUXEN = 1;
  // D6は偶数ピンなのでPMUXE、ペリフェラル機能"H"(7)でGCLK_IO[5]に接続
  PORT->Group[g_APinDescription[FIXED_CLOCK_PIN].ulPort].PMUX[g_APinDescription[FIXED_CLOCK_PIN].ulPin >> 1].reg |= PORT_PMUX_PMUXE_H;
}

//================================================================
//  D1ピン: 可変クロックの初期設定
//================================================================
void setupVariableClock() {
  // 1. TCC0タイマーにクロック(GCLK0=48MHz)を供給
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // クロックを有効化
                      GCLK_CLKCTRL_GEN_GCLK0 |     // ソースとしてGCLK0(48MHz)を選択
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // TCC0とTCC1に供給
  while (GCLK->STATUS.bit.SYNCBUSY);               // 同期完了を待つ

  // 2. D1ピン(PA04)をTCC0の波形出力(WO[4])として設定
  pinMode(VARIABLE_CLOCK_PIN, OUTPUT);
  PORT->Group[g_APinDescription[VARIABLE_CLOCK_PIN].ulPort].PINCFG[g_APinDescription[VARIABLE_CLOCK_PIN].ulPin].bit.PMUXEN = 1;
  // D1は偶数ピンなのでPMUXE、ペリフェラル機能"E"(5)でTCC0/WO[4]に接続
  PORT->Group[g_APinDescription[VARIABLE_CLOCK_PIN].ulPort].PMUX[g_APinDescription[VARIABLE_CLOCK_PIN].ulPin >> 1].reg |= PORT_PMUX_PMUXE_E;
  
  // 3. TCC0タイマーを設定
  TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV1;     // プリスケーラ(分周)を1に設定
  
  // 波形生成モードをNormal PWM (NPWM)に設定
  TCC0->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;
  while(TCC0->SYNCBUSY.bit.WAVE);                  // 同期完了を待つ

  // 4. TCC0を有効化
  TCC0->CTRLA.reg |= TCC_CTRLA_ENABLE;
  while (TCC0->SYNCBUSY.bit.ENABLE);               // 同期完了を待つ
}
 
 

コードの解説

  • setupFixedClock() 関数:

    • XIAOのメインクロックである48MHzを、GCLK(汎用クロック生成器)を使って 351分周 します。

    • Hz となり、目標の137kHzに対して誤差約-0.18%の安定したクロックを生成します。

    • 生成したクロックをD6ピンに出力するよう、ピンの機能を切り替えています(ピンマルチプレクサ設定)。

  • setupVariableClock() 関数:

    • より高度なPWM生成が可能なTCC(タイマー/カウンター)を使用します。

    • TCC0のクロック源として48MHzを直接入力します。

    • D1ピンをTCC0の波形出力ピンとして設定します。

    • TCC0をPWMモードで有効にします。

  • updateVariableClock(int level) 関数:

    • A0ピンから読み取ったAD値のレベル(0〜15)に応じて、DIVIDERS配列から分周比を取得します。

    • 3.579545 MHz / 分周比 で目標周波数を計算します。

    • その目標周波数を48MHzのクロックから生成するために必要なタイマーの周期 (period_value) を逆算します。

    • 計算した値をTCC0の周期レジスタ(PERBUF)とデューティ比レジスタ(CCBUF)に書き込むことで、出力クロックの周波数が変更されます。

  • loop() 関数:

    • A0ピンの電圧を常に監視し、ボリュームが回されて値が変化した時だけ updateVariableClock 関数を呼び出して周波数を更新します。これにより、無駄な計算を省いています。


ツマミでファミコンノイズが「チクガガゴー」と雷として鳴らせると思いますよ。最高ですね。
さて、クロックもステージも何とか設計できたとします。
まだ足りない部分がありますね。
イニシャライズ部分です。 
5V電源をONしたら一定時間5Vを出力して0Vになる回路。
TR-909だとここの赤丸部分ですね。


まぁTR-909のこの部分は1円玉ほどの 大きさですから何も問題ないですね。
Seeeduino XIAOのソース中に137kHzクロックも出力するようにしていますが、これはTR-909専用です。
あとは出力電圧が10Vppに出来ればEurorackモジュールのPseudo Noise Generatorの完成です。

ちなみにTR-909のクロックの周波数は?

R = 33kΩ(フィードバック抵抗)
R = 10kΩ(直列抵抗)
C = 100pF

f=1/2.2xRxC

ここでRは支配的な抵抗。この構成では R1とR2の直列合成 が時定数を決めます:

R=33k+10k=43kΩ

f=1/9.46x10の-6乗=106kHz

なんだかネット上では値がおかしいような・・・まぁ実際は20%程度誤差があってもちゃんとノイズですから。

もっと安いNE555で考えてみる。

確かに。その考えは正しい。ただしロータリースイッチを付けた時点で値段は高くなります。
とりあえず考えてみましょう。

NESのクロック表がこちら

NES APU クロック分周テーブル

ベースクロック:1.789773 MHz

分周数周波数備考
÷4447.443 kHz
÷8223.722 kHz
÷16111.861 kHz← 発振回路(~106kHz)に近い
÷3255.930 kHz
÷6427.965 kHz
÷9618.643 kHz
÷12813.983 kHz
÷16011.186 kHz
÷2028.860 kHz
÷2547.046 kHz
÷3804.710 kHz
÷5083.523 kHz
÷7622.349 kHz
÷10161.761 kHz
÷20340.880 kHz = 880 HzA4(ラ)×2
÷40680.440 kHz = 440 HzA4(ラ)

÷16 が発振回路の ~106 kHz に最も近い値です。というか誤差の範囲内なのでこだわる必要もありません。
447kも223kも要らないと言えば要りません。ホワイトノイズですから。
安く作るとしたら500kHzまで発信できるNE555の出番ですかね。

NE555 定数算出(RA・RB共にE24系列最適化)

f=1.44(RA+2RB)×Cf = \frac{1.44}{(R_A + 2R_B) \times C}

RA+2RBの合計値をE24の組み合わせで目標に最も近づける方針で算出。


定数表

目標周波数CRA (E24)RB (E24)RA+2RB実現周波数誤差
111.861 kHz100pF1.0kΩ6.2kΩ13.4kΩ107.5 kHz-3.9%
55.930 kHz100pF2.0kΩ12kΩ26.0kΩ55.4 kHz-0.9%
27.965 kHz100pF1.5kΩ25kΩ51.5kΩ27.96 kHz0.0%
18.643 kHz100pF3.0kΩ36kΩ75.0kΩ19.2 kHz+3.0%
13.983 kHz100pF2.0kΩ51kΩ104.0kΩ13.85 kHz-1.0%
11.186 kHz100pF3.0kΩ62kΩ127.0kΩ11.34 kHz+1.4%
8.860 kHz100pF1.6kΩ82kΩ165.6kΩ8.70 kHz-1.8%
7.046 kHz1nF1.2kΩ100kΩ201.2kΩ7.16 kHz+1.6%
4.710 kHz1nF2.0kΩ150kΩ302.0kΩ4.77 kHz+1.3%
3.523 kHz1nF2.7kΩ200kΩ402.7kΩ3.57 kHz+1.5%
2.349 kHz1nF3.3kΩ300kΩ603.3kΩ2.39 kHz+1.7%
1.761 kHz1nF1.8kΩ400kΩ801.8kΩ1.796 kHz+2.0%
0.880 kHz10nF1.6kΩ81kΩ163.6kΩ0.880 kHz0.0%
0.440 kHz10nF1.6kΩ160kΩ321.6kΩ0.448 kHz+1.8%

備考

  • 1.761 kHz18.643 kHz がやや誤差大きめ。トリマ併用を推奨
  • RB≫RAを維持しているためデューティ比は概ね 52〜55% 程度に収まります
  • 400kΩはE24に存在しないため、390kΩ+10kΩ直列で構成してください
  • ホワイトでない色のあるノイズ(8kHz以下)が3%以内であれば音程としては優秀です。気になるなら半固定抵抗でもつけて下さい。おそらくその価値は無いでしょう。
555部分を切り替えるのはアナログスイッチで大丈夫でしょう。







コメント