NESのノイズジェネレーターについて
疑似乱数の秘密はフィードバックにあり
イメージできます?
| 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 |
いざ、シミュレーションしてみましょう。
![]() |
| 音は無加工のまま右に進むので音の取り出し口は1つで問題ありません。ループ音も再現され問題ありません。 |
次はクロック部分をAIで生成させましょう。
NTSC 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068 (2046 in old)
このコードは、SAMD21マイコンのGCLK (Generic Clock Controller) および TCC (Timer/Counter for Control Applications) という機能を直接利用して、正確な周波数を効率的に生成します。
回路接続
まず、以下のように部品を接続してください。
A0ピン: ボリューム(可変抵抗)の真ん中の端子に接続します。ボリュームの両端の端子は、それぞれ 5V と GND に接続します。
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関数を呼び出して周波数を更新します。これにより、無駄な計算を省いています。
もっと安いNE555で考えてみる。
NES APU クロック分周テーブル
ベースクロック:1.789773 MHz
| 分周数 | 周波数 | 備考 |
|---|---|---|
| ÷4 | 447.443 kHz | |
| ÷8 | 223.722 kHz | |
| ÷16 | 111.861 kHz | ← 発振回路(~106kHz)に近い |
| ÷32 | 55.930 kHz | |
| ÷64 | 27.965 kHz | |
| ÷96 | 18.643 kHz | |
| ÷128 | 13.983 kHz | |
| ÷160 | 11.186 kHz | |
| ÷202 | 8.860 kHz | |
| ÷254 | 7.046 kHz | |
| ÷380 | 4.710 kHz | |
| ÷508 | 3.523 kHz | |
| ÷762 | 2.349 kHz | |
| ÷1016 | 1.761 kHz | |
| ÷2034 | 0.880 kHz = 880 Hz | A4(ラ)×2 |
| ÷4068 | 0.440 kHz = 440 Hz | A4(ラ) |
447kも223kも要らないと言えば要りません。ホワイトノイズですから。
安く作るとしたら500kHzまで発信できるNE555の出番ですかね。
NE555 定数算出(RA・RB共にE24系列最適化)
RA+2RBの合計値をE24の組み合わせで目標に最も近づける方針で算出。
定数表
| 目標周波数 | C | RA (E24) | RB (E24) | RA+2RB | 実現周波数 | 誤差 |
|---|---|---|---|---|---|---|
| 111.861 kHz | 100pF | 1.0kΩ | 6.2kΩ | 13.4kΩ | 107.5 kHz | -3.9% |
| 55.930 kHz | 100pF | 2.0kΩ | 12kΩ | 26.0kΩ | 55.4 kHz | -0.9% |
| 27.965 kHz | 100pF | 1.5kΩ | 25kΩ | 51.5kΩ | 27.96 kHz | 0.0% |
| 18.643 kHz | 100pF | 3.0kΩ | 36kΩ | 75.0kΩ | 19.2 kHz | +3.0% |
| 13.983 kHz | 100pF | 2.0kΩ | 51kΩ | 104.0kΩ | 13.85 kHz | -1.0% |
| 11.186 kHz | 100pF | 3.0kΩ | 62kΩ | 127.0kΩ | 11.34 kHz | +1.4% |
| 8.860 kHz | 100pF | 1.6kΩ | 82kΩ | 165.6kΩ | 8.70 kHz | -1.8% |
| 7.046 kHz | 1nF | 1.2kΩ | 100kΩ | 201.2kΩ | 7.16 kHz | +1.6% |
| 4.710 kHz | 1nF | 2.0kΩ | 150kΩ | 302.0kΩ | 4.77 kHz | +1.3% |
| 3.523 kHz | 1nF | 2.7kΩ | 200kΩ | 402.7kΩ | 3.57 kHz | +1.5% |
| 2.349 kHz | 1nF | 3.3kΩ | 300kΩ | 603.3kΩ | 2.39 kHz | +1.7% |
| 1.761 kHz | 1nF | 1.8kΩ | 400kΩ | 801.8kΩ | 1.796 kHz | +2.0% |
| 0.880 kHz | 10nF | 1.6kΩ | 81kΩ | 163.6kΩ | 0.880 kHz | 0.0% |
| 0.440 kHz | 10nF | 1.6kΩ | 160kΩ | 321.6kΩ | 0.448 kHz | +1.8% |
備考
- 1.761 kHz と 18.643 kHz がやや誤差大きめ。トリマ併用を推奨
- RB≫RAを維持しているためデューティ比は概ね 52〜55% 程度に収まります
- 400kΩはE24に存在しないため、390kΩ+10kΩ直列で構成してください
- ホワイトでない色のあるノイズ(8kHz以下)が3%以内であれば音程としては優秀です。気になるなら半固定抵抗でもつけて下さい。おそらくその価値は無いでしょう。


コメント
コメントを投稿