NES(ファミコン)のノイズジェネレーターを設計しよう クロック分周編

基本仕様

  • NTSC クロック周波数: 3.579545 MHzの半分の1.7897725 MHz。入手は3.5MHzのほうが入手しやすい。

  • 目標分周比: 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068

https://akizukidenshi.com/catalog/g/g101685/ 

https://akizukidenshi.com/catalog/g/g112492/

分周比の実現方法

  • バイナリーカウンターを動かして特定条件をNAND回路で拾ってその信号をフリップフロップに与えて50%なクロックとして使う。 
  • 16あるリセット信号のどれをフリップフロップに与えるかで16種類のクロックを切り替える。この仕組みも必要。  

 バイナリーカウンターTC74HC4040APの仕様(CD4040B)

  • 12ビットバイナリカウンタ (Q1-Q12)

  • 分周範囲: 2~4096

  • 出力: Q1(2^1), Q2(2^2), ..., Q12(2^12=4096)


リセット回路の仕様

96, 160, 202, 254分周以降にはNANDゲートを使ったリセット回路を追加:

  • 所望のカウント値でリセット信号を生成。

     例: 96分周 = 96(10進) = 1100000(2進) → Q6とQ5がHIGHの時にリセット
    分周比2進数値使用ビットRESET信号の取り出し方
    40000 0000 0000 0100Q3RESET_4 = Q3
    80000 0000 0000 1000Q4RESET_8 = Q4
    160000 0000 0001 0000Q5RESET_16 = Q5
    320000 0000 0010 0000Q6RESET_32 = Q6
    640000 0000 0100 0000Q7RESET_64 = Q7
    960000 0000 0110 0000Q6, Q5RESET_96 = NAND(Q6, Q5)
    1280000 0000 1000 0000Q8RESET_128 = Q8
    1600000 0000 1010 0000Q8, Q6RESET_160 = NAND(Q8, Q6)
    2020000 0000 1100 1010Q8, Q7, Q4, Q2RESET_202 = NAND(Q8, Q7, Q4, Q2)
    2540000 0000 1111 1110Q8-Q2RESET_254 = NAND(Q8, Q7, Q6, Q5, Q4, Q3, Q2)
    3800000 0001 0111 1100Q9, Q7-Q3RESET_380 = NAND(Q9, Q7, Q6, Q5, Q4, Q3)
    5080000 0001 1111 1100Q9, Q8-Q3RESET_508 = NAND(Q9, Q8, Q7, Q6, Q5, Q4, Q3)
    7620000 0010 1111 1010Q10, Q8-Q2RESET_762 = NAND(Q10, Q8, Q7, Q6, Q5, Q4, Q3, Q2)
    10160000 0011 1111 1000Q10, Q9-Q4RESET_1016 = NAND(Q10, Q9, Q8, Q7, Q6, Q5, Q4)
    20340000 0111 1111 0010Q11-Q5, Q2RESET_2034 = NAND(Q11, Q10, Q9, Q8, Q7, Q6, Q5, Q2)
    40680000 1111 1110 0100Q12-Q6,Q3RESET_4068 = NAND(Q12, Q11, Q10, Q9, Q8, Q7, Q6, Q3)
    NAND回路のオンパレードだな・・・
74HC00 (NAND×4) 
74HC10 (3入力NAND×2) 
74HC20 (4入力NAND×2) 
74HC30 (8入力NAND

8入力NANDなんて使ったことないな。最適化をAIにお願いしました。
 
// 共通パターンの定義
PATTERN_A = NAND(Q8, Q7, Q6, Q5, Q4, Q3, Q2)  // 254用
PATTERN_B = NAND(Q9, Q8, Q7, Q6, Q5, Q4, Q3)  // 508用

// 最適化リセット式
RESET_96  = NAND(Q7, Q6)
RESET_160 = NAND(Q8, Q6) 
RESET_202 = NAND(PATTERN_A, Q4)    // 254からQ4(16)を除外
RESET_254 = PATTERN_A
RESET_380 = NAND(Q9, PATTERN_A)    // 254 + 256
RESET_508 = PATTERN_B
RESET_762 = NAND(Q10, PATTERN_A)   // 254 + 512
RESET_1016 = NAND(Q10, Q9, PATTERN_A) // 254 + 512 + 256
RESET_2034 = NAND(Q11, Q10, Q9, Q8, Q7, Q6, Q5, Q2)
RESET_4068 = NAND(Q12, Q11, Q10, Q9, Q8, Q7, Q6, Q3) 
 
8入力NANDx4,2入力NANDx5,3入力NANDx1,
このまま作るとNANDだけでICが6つは必要ですね。私はこの構成で作り続けてもよいのですが想像以上に面積をとってしまうので考えを変更します。同じ面積で値段も変わらないのであればより多機能で柔軟なMCU化を選択します。80年代ならPAL/GALですね。
学習目的であれば上の表がほぼ答えなので作れるでしょう。 
 
コスト比較
方式IC数柔軟性コスト
基本NAND方式8-10個低い高い
PAL/GAL方式3-4個中程度中程度
MCU方式2-3個高い安い
CPLD方式2個最高中程度
 精密でなければNE555で抵抗をスイッチで変える方法もあります。
12KHz以上は精密でなくとも問題ありません。

水晶振動子

せっかくのクロック設計なので基本部分は紹介しましょう。
2本足の水晶振動子が基本です。


リンクはこちら

水晶振動子の周波数で33pFや6.8KΩの部分が変わります。1MΩはバイアス抵抗といいます。
水晶振動子は強烈なタンク回路なので上のインバーターが動くアナログ動作な環境で発信してくれます。
つまりCを変えても周波数が殆ど動くことがありません。
気を付けるとしたらインバーターはUB(アンバッファー)型番を使いましょう。型番にUかUBが付いています。
バッファードは内部でしきい値が急峻 → 線形性が悪く発振開始しにくいので適しません。
CMOSならCD4009UBとかTTLなら74HCU04です。
UBならNAND回路でも構いません。
アナログの領域なので回路自体の配線は短くしないと発振しない可能性もあります。


周波数C1,C2(pF)R(Ω)
1 MHz336.8k
2 MHz333.3k
4 MHz332.2k
8 MHz221k
12 MHz22470
16 MHz15470
20 MHz15330
24 MHz10330

あまったインバーターはこう処理します。
ファミリ(例) 未使用入力の処理 出力
CMOS74HC**, 74HCU**, 74AC** など) High か Low に直接接続(または他の入力にリンク) オープン
TTL / LS74LS**, 74ALS** 等) Low に落とすと電流が流れて無駄な消費 → Highにプルアップが安全 オープン

これは入力を明らかにしたままにすれば出力が暴れないという考え方です。
ちなみにインバーターの並列接続は電流が増えますので意図的にそうする事もあります。
オープン(何も接続しない)にすることで電流は増えません。
あと発振するという事はノイズ源でもあります。音関係の回路のそばには置きたくないです。

基本の2本足の次はTTL出力の4本足ですが多くを語らずとも電源をつなげば苦も無く出力が得られるので特に説明も要らないでしょう。
以下ハードウェアでのアプローチではなくソフトウェアでのアプローチで多くのLogicをまとめてしまう話題ですので興味がわかないかもしれません。
 

水晶振動子+CD4024B+NE555

CD4024BはQ1~Q7の分周器なので128までの分周の殆どまで(6出力分)は使えます。残るのは18khz以下ですのでNE555に96分周以下(10出力分)を作っていけば出来そうです。

マイクロコントローラーでのクロック生成

おそらくはPIC12PIC16が安くて早くて安心です。
オルガンみたいに全ピンから各クロックを出力するか、
1ピンだけ出力でスイッチで切り替えるかです。
 
DIP8なPIC12F1822-I/Pで限界まで使ってみますね。
Timer0,2を切替えてクロック生成だけでなく
ノイズジェネレーターを組込んでツマミでZAXXONの出撃音(!!)が鳴ります。
ちなみに64分周より高い周波数はおそらく使いません。人間の耳で判別困難だからです。

/*
 * PIC12F1822 マルチクロック/LFSR出力ファームウェア (仕様変更版)
 * ------------------------------------------------------------
 * Fosc = 32MHz (内蔵HFINTOSC 8MHz + 4xPLL)
 *
 * RA5 (2pin) : LFSR1 最終ステージbit出力
 * RA4 (3pin) : リセット直後100ms間だけHigh、以降Low
 * RA3 (4pin) : デジタル入力。High=RA2を137kHz固定に強制。Low=AN0選択の分周比。
 *              (この入力による切替はリセット動作を伴わない)
 * RA2 (5pin) : リセット直後100ms間は固定8分周のクロックを出力。
 *              その後はAN0(16段階)で選んだ分周比、またはRA3=Highなら137kHz固定。
 * AN1 (RA1/6pin) : LFSR更新レート可変(440Hz〜14080Hz)
 * AN0 (RA0/7pin) : RA2の分周比を16段階選択
 *
 * --- 実装方式 ---
 * RA2の高速な波形は CCP1 + Timer2 のハードウェアPWM(50%デューティ)で生成する。
 * これによりCPU/割り込みのオーバーヘッドなしに高周波数(最大約447kHz)まで
 * 安定して出力できる。ただしTimer2の周期レジスタ(PR2)は8bit(最大255)しか
 * ないため、下限は約1953Hz程度までしかカバーできない。
 * それより低い周波数(1016/2034/4068分周)は、負荷が軽く正確に出せる
 * Timer0の割り込みトグル方式にフォールバックする(モードを動的に切り替える)。
 * ------------------------------------------------------------
 */

#include <xc.h>
#include <stdint.h>

// ---------------- CONFIG BITS ----------------
#pragma config FOSC     = INTOSC
#pragma config WDTE     = OFF
#pragma config PWRTE    = ON
#pragma config MCLRE    = OFF   // RA3をデジタル入力として使用するため
#pragma config CP       = OFF
#pragma config CPD      = OFF
#pragma config BOREN    = ON
#pragma config CLKOUTEN = OFF
#pragma config IESO     = OFF
#pragma config FCMEN    = OFF

#pragma config WRT      = OFF
#pragma config PLLEN    = ON    // 4xPLL常時有効(8MHz -> 32MHz)
#pragma config STVREN   = ON
#pragma config BORV     = LO
#pragma config LVP      = OFF

#define _XTAL_FREQ 32000000UL

// ---------------- 出力モード ----------------
typedef enum { MODE_TIMER0 = 0, MODE_PWM = 1 } clkmode_t;

typedef struct {
    clkmode_t mode;
    // Timer0モード用
    uint8_t   t0_option;  // OPTION_REG下位4bit(PSA,PS2,PS1,PS0)
    uint8_t   t0_reload;  // TMR0リロード値(256-N)
    // PWMモード用 (CCP1 + Timer2)
    uint8_t   t2ckps;     // T2CON<T2CKPS1:0> (00=1:1,01=1:4,10/11=1:16)
    uint8_t   pr2;        // PR2値
    uint16_t  duty10;     // 10bit デューティ値(概ね(pr2+1)相当で50%)
} div_entry_t;

// index0(=4068分周,約440Hz,最遅) 〜 index15(=4分周,約447kHz,最速)
static const div_entry_t g_divTable[16] = {
    /* 0: 4068分周 (~439.9Hz)   Timer0 */ {MODE_TIMER0, 0x05, 114, 0,   0,  0},
    /* 1: 2034分周 (~880.3Hz)   Timer0 */ {MODE_TIMER0, 0x04, 114, 0,   0,  0},
    /* 2: 1016分周 (~1760.6Hz)  Timer0 */ {MODE_TIMER0, 0x03, 114, 0,   0,  0},
    /* 3:  762分周 (~2347.4Hz)  PWM    */ {MODE_PWM,    0,    0,   0b10, 212, 213},
    /* 4:  508分周 (~3521.1Hz)  PWM    */ {MODE_PWM,    0,    0,   0b10, 141, 142},
    /* 5:  380分周 (~4717.0Hz)  PWM    */ {MODE_PWM,    0,    0,   0b10, 105, 106},
    /* 6:  254分周 (~7042.3Hz)  PWM    */ {MODE_PWM,    0,    0,   0b10,  70,  71},
    /* 7:  202分周 (~8849.6Hz)  PWM    */ {MODE_PWM,    0,    0,   0b01, 225, 226},
    /* 8:  160分周 (~11173.2Hz) PWM    */ {MODE_PWM,    0,    0,   0b01, 178, 179},
    /* 9:  128分周 (~13986.0Hz) PWM    */ {MODE_PWM,    0,    0,   0b01, 142, 143},
    /*10:   96分周 (~18691.6Hz) PWM    */ {MODE_PWM,    0,    0,   0b01, 106, 107},
    /*11:   64分周 (~27777.8Hz) PWM    */ {MODE_PWM,    0,    0,   0b01,  71,  72},
    /*12:   32分周 (~55944.1Hz) PWM    */ {MODE_PWM,    0,    0,   0b00, 142, 143},
    /*13:   16分周 (~111111Hz)  PWM    */ {MODE_PWM,    0,    0,   0b00,  71,  72},
    /*14:    8分周 (~222222Hz)  PWM    */ {MODE_PWM,    0,    0,   0b00,  35,  36},
    /*15:    4分周 (~444444Hz)  PWM    */ {MODE_PWM,    0,    0,   0b00,  17,  18},
};

#define DIV8_INDEX      14u   // 起動直後固定「8分周」に対応するテーブル番号

// 137kHz固定(RA3=High時): prescale1, PR2=57 -> 約137931Hz
static const div_entry_t g_entry137k = {MODE_PWM, 0, 0, 0b00, 57, 58};

// 起動100ms判定用(divisor8のPWM周波数 ~222222Hz、1周期ごとにTMR2IF発生)
#define STARTUP_TICKS   22222UL

// ---------------- グローバル変数 ----------------
static volatile uint8_t  g_startupDone = 0;
static volatile uint32_t g_startupTickCount = 0;

static volatile uint32_t g_lfsr1 = 0x12345678UL;
static volatile uint16_t g_tmr1Reload = 0;
static volatile uint8_t  g_tmr0Reload = 114;

static int8_t g_lastAppliedIdx = -1;   // -2:137kHz固定, -1:未設定, 0-15:通常テーブル

// ---------------- ADC ----------------
static uint16_t ADC_Read(uint8_t channel)
{
    ADCON0 = (uint8_t)((channel << 2) | 0x01);
    __delay_us(5);
    ADCON0bits.GO_nDONE = 1;
    while (ADCON0bits.GO_nDONE) {
        ;
    }
    return (uint16_t)((((uint16_t)ADRESH) << 8) | ADRESL);
}

// ---------------- 初期化 ----------------
static void OSC_Initialize(void)
{
    OSCCONbits.IRCF = 0b1110; // 8MHz -> PLLで32MHz
    OSCCONbits.SCS  = 0b00;
    while (!OSCSTATbits.PLLR) {
        ;
    }
}

static void IO_Initialize(void)
{
    ANSELA = 0x03;  // AN0,AN1のみアナログ
    TRISA  = 0x0B;  // RA0,RA1,RA3=入力 / RA2,RA4,RA5=出力
    LATA   = 0x00;
    LATAbits.LATA4 = 1; // 起動直後100msだけHigh
}

static void ADC_Initialize(void)
{
    ADCON1 = 0xE0; // ADFM=1, ADCS=110(FRC), ADPREF=00(VDD)
    ADCON0bits.ADON = 1;
}

static void Timer1_Initialize(void) // LFSR (RA5) 用
{
    T1CONbits.TMR1CS = 0b00;
    T1CONbits.T1CKPS = 0b00;
    g_tmr1Reload = 65536u - (8000000UL / 440UL);
    TMR1H = (uint8_t)(g_tmr1Reload >> 8);
    TMR1L = (uint8_t)(g_tmr1Reload & 0xFF);
    PIR1bits.TMR1IF = 0;
    PIE1bits.TMR1IE = 1;
    T1CONbits.TMR1ON = 1;
}

// RA2出力を指定テーブルエントリで駆動する(Timer0/PWM切替を含む)
static void ApplyClockEntry(const div_entry_t *e)
{
    if (e->mode == MODE_TIMER0) {
        // PWM(CCP1)を止めてピンをLATA制御に戻す
        CCP1CON = 0x00;
        T2CONbits.TMR2ON = 0;
        TRISAbits.TRISA2 = 0;

        INTCONbits.T0IE = 0;
        OPTION_REG = (OPTION_REG & 0xF0) | e->t0_option;
        OPTION_REGbits.T0CS = 0;
        g_tmr0Reload = e->t0_reload;
        TMR0 = g_tmr0Reload;
        INTCONbits.T0IF = 0;
        INTCONbits.T0IE = 1;
    } else {
        // Timer0トグルを止めてCCP1 PWMに切替
        INTCONbits.T0IE = 0;

        T2CONbits.TMR2ON = 0;
        T2CONbits.T2CKPS = e->t2ckps;
        PR2 = e->pr2;

        uint16_t duty = e->duty10; // 10bit値。下位2bitをCCP1CON<5:4>へ
        CCPR1L = (uint8_t)(duty >> 2);
        CCP1CONbits.DC1B = (uint8_t)(duty & 0x03);
        CCP1CON = (CCP1CON & 0xCF) | 0b00001100; // CCP1M=1100(PWMモード)

        TRISAbits.TRISA2 = 0;
        T2CONbits.TMR2ON = 1;
    }
}

static void Timer0_Initialize(void)
{
    // 起動直後は固定8分周(DIV8_INDEX = PWMモード)で開始
    ApplyClockEntry(&g_divTable[DIV8_INDEX]);
    g_lastAppliedIdx = DIV8_INDEX;
}

// ---------------- 割り込みサービスルーチン ----------------
void __interrupt() ISR(void)
{
    // --- Timer0モード時: RA2トグル(低周波数域のみ) ---
    if (INTCONbits.T0IE && INTCONbits.T0IF) {
        TMR0 = g_tmr0Reload;
        LATAbits.LATA2 ^= 1;
        INTCONbits.T0IF = 0;
    }

    // --- Timer2 (PWM周期一致): 起動100ms計測用 ---
    if (PIE1bits.TMR2IE && PIR1bits.TMR2IF) {
        if (!g_startupDone) {
            g_startupTickCount++;
            if (g_startupTickCount >= STARTUP_TICKS) {
                g_startupDone = 1;
                LATAbits.LATA4 = 0;
            }
        }
        PIR1bits.TMR2IF = 0;
    }

    // --- Timer1: LFSR更新 (RA5) ---
    if (PIE1bits.TMR1IE && PIR1bits.TMR1IF) {
        TMR1H = (uint8_t)(g_tmr1Reload >> 8);
        TMR1L = (uint8_t)(g_tmr1Reload & 0xFF);

        g_lfsr1 = (g_lfsr1 >> 1) ^ (-(g_lfsr1 & 1u) & 0xD0000001u);
        LATAbits.LATA5 = (uint8_t)(g_lfsr1 & 1u);

        PIR1bits.TMR1IF = 0;
    }
}

// ---------------- メイン ----------------
void main(void)
{
    OSC_Initialize();
    IO_Initialize();
    ADC_Initialize();
    Timer1_Initialize();

    PIE1bits.TMR2IE = 1; // 起動100ms計測に使うため、PWM開始前に有効化しておく
    Timer0_Initialize();  // 起動時8分周(PWM)を適用、Timer2始動

    INTCONbits.PEIE = 1;
    INTCONbits.GIE  = 1;

    while (1) {
        if (g_startupDone) {
            if (PORTAbits.RA3 == 1) {
                // RA3=High: 137kHz固定に強制
                if (g_lastAppliedIdx != -2) {
                    ApplyClockEntry(&g_entry137k);
                    g_lastAppliedIdx = -2;
                }
            } else {
                uint16_t an0 = ADC_Read(0);
                uint8_t idx = (uint8_t)(an0 >> 6); // 上位4bit(0-15)
                if (g_lastAppliedIdx != (int8_t)idx) {
                    ApplyClockEntry(&g_divTable[idx]);
                    g_lastAppliedIdx = (int8_t)idx;
                }
            }
        }

        // AN1: 440Hz〜14080HzでLFSR更新レートを線形補間
        uint16_t an1 = ADC_Read(1);
        uint32_t freq = 440UL + ((13640UL * (uint32_t)an1) / 1023UL);
        uint32_t n = 8000000UL / freq;
        if (n < 2UL) {
            n = 2UL;
        }
        if (n > 65535UL) {
            n = 65535UL;
        }
        g_tmr1Reload = (uint16_t)(65536UL - n);
    }
}

このようにマイクロコントローラーは多くのLogicICやA/Dそしてクロックや記憶装置を備えているので、ハードに詳しい人が適切に使うと全てにおいてパフォーマンスが高いです。別のマイクロコントローラーへの移植はAIにソースを読み込ませて相談してみましょう。良い結果が出るはずです。
最後のコードならLogicとPICで2つのLFSRノイズがとれるわけですから(RA3に5VいれるとTR-909用クロック)
面白いですよ。NESの独特なループを無くして64分周より高い周波数が不要なら贅沢なステレオLFSRも可能です。
こんな方法もあります。

NE555による固定周波数発信器



この方法だとR,Cをアナログスイッチで切り替えるだけですがスイッチングにコストがかかります。
でもモノシンセみたいにとってもアナログですね。好きな方は挑戦してみましょう。

コメント