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

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

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


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ですね。
 

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

 
LだけではEx-ORは動かないのでInitialize SwitchのLを押してH信号(暗号のSeedという)を与えてください

 
リンクはこちら
 
 
右に再生ボタンがありますがシミュレートを進めないとデータも作られないので再生できません。
最高速にするとすぐ再生できます。 6ステージだと音で繰り返しがわかりますね。
 
PRBSは何ステージでも1ビット表現ですからTR-909もクロックを440Hzにするとループがわからないファミコンのノイズになります。
(月面着陸に使うスラスター音のような)

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

 困ったことにシフトレジスターって4ステージ(4ビット)単位なので抜き取りたい段数が4の倍数だけではないのでDフリップフロップと繋げて設計します。
手元にTR-909で使われているCD4006BE(4ステージ,4+1ステージ)x2が沢山ありますのでそれを使います。
細かくステージの出力を抜き取るにはDフリップフロップ(CD40175BE)を使います。
全部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専用です。
あとは出力電圧が確保出来れば完了です。

コメント