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をまとめてしまう話題ですので興味がわかないかもしれません。
 
 

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

おそらくはPIC12PIC16が安くて早くて安心です。
オルガンみたいに全ピンから各クロックを出力するか、
1ピンだけ出力でスイッチで切り替えるかです。
 
まず全ピン出力から
 
 /**
 * PIC16F1823 クロックジェネレータ
 * 1.7897725MHzを様々な分周比で出力
 */

#include <xc.h>

// コンフィギュレーションビットの設定
#pragma config FOSC = INTOSC  // 内部クロック使用
#pragma config WDTE = OFF     // ウォッチドッグタイマ無効
#pragma config PWRTE = OFF    // パワーアップタイマ無効
#pragma config MCLRE = ON     // MCLR有効
#pragma config CP = OFF       // コードプロテクション無効
#pragma config CPD = OFF      // データメモリプロテクション無効
#pragma config BOREN = OFF    // ブラウンアウトリセット無効
#pragma config CLKOUTEN = OFF // CLKOUT無効
#pragma config IESO = OFF     // 内部/外部スイッチオフ
#pragma config FCMEN = OFF    // フェイルセーフクロックモニタ無効

// 16MHz内部クロック設定
#define _XTAL_FREQ 16000000

// 分周比と対応するピンの定義
typedef struct {
    unsigned int divider;
    unsigned char pin_mask;
    unsigned char timer_config;
} ClockOutput;

// 分周比リスト: 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068
const ClockOutput clock_outputs[] = {
    {64,   0x01, 0x00},  // RA0 (GPIO0)
    {96,   0x02, 0x01},  // RA1 (GPIO1)
    {128,  0x04, 0x02},  // RA2 (GPIO2)
    {160,  0x08, 0x03},  // RA3 (GPIO3)
    {202,  0x10, 0x04},  // RA4 (GPIO4)
    {254,  0x20, 0x05},  // RA5 (GPIO5)
    {380,  0x40, 0x06},  // RA6 (GPIO6)
    {508,  0x80, 0x07},  // RA7 (GPIO7)
    {762,  0x01, 0x08},  // RB4 (GPIO11)
    {1016, 0x02, 0x09},  // RB5 (GPIO12)
    {2034, 0x04, 0x0A},  // RB6 (GPIO13)
    {4068, 0x08, 0x0B}   // RB7 (GPIO14)
};

// 基本クロック周波数 (1.7897725 MHz)
#define BASE_FREQ 1789772.5

void setup_oscillator(void);
void setup_timer2(unsigned char timer_config);
void setup_ccp(unsigned char pin_index, unsigned char timer_config);
void setup_pins(void);
void calculate_frequencies(void);

void main(void) {
    // オシレーター設定
    setup_oscillator();
    
    // ピン設定
    setup_pins();
    
    // 周波数計算と表示用(デバッグ)
    calculate_frequencies();
    
    // メインループ
    while(1) {
        // 必要に応じて他の処理を追加
        __delay_ms(1000);
    }
}

void setup_oscillator(void) {
    // 内部クロックを16MHzに設定
    OSCCON = 0b01111000;  // IRCF=1111 (16MHz), SCS=00 (内部クロック)
    
    // 安定するまで待機
    while(!OSCSTATbits.HFIOFR);
}

void setup_pins(void) {
    // PORT A 全ピンをデジタル出力に設定
    ANSELA = 0x00;      // アナログ入力無効化
    TRISA = 0x00;       // 全て出力モード
    
    // PORT B の対応するピンをデジタル出力に設定
    ANSELB = 0x00;      // アナログ入力無効化
    TRISB &= 0xF0;      // RB4-RB7を出力モード(下位4ビットは入力のまま)
    
    // 最初は全てのピンをLOWに
    LATA = 0x00;
    LATB &= 0xF0;
}

void setup_timer2(unsigned char timer_config) {
    // Timer2停止
    T2CONbits.TMR2ON = 0;
    
    // プリスケーラーとポストスケーラー設定
    T2CONbits.T2CKPS = (timer_config >> 2) & 0x03;  // プリスケーラー
    T2CONbits.T2OUTPS = timer_config & 0x0F;        // ポストスケーラー
    
    // PR2レジスタ設定(周期設定)
    // 基準周波数: 16MHz / 4 = 4MHz (Timer2クロック)
    // 分周比に応じてPR2を計算
    unsigned int pr2_value;
    
    switch(timer_config) {
        case 0x00: pr2_value = 63; break;    // 64分周
        case 0x01: pr2_value = 95; break;    // 96分周
        case 0x02: pr2_value = 127; break;   // 128分周
        case 0x03: pr2_value = 159; break;   // 160分周
        case 0x04: pr2_value = 201; break;   // 202分周
        case 0x05: pr2_value = 253; break;   // 254分周
        case 0x06: pr2_value = 379; break;   // 380分周
        case 0x07: pr2_value = 507; break;   // 508分周
        case 0x08: pr2_value = 761; break;   // 762分周
        case 0x09: pr2_value = 1015; break;  // 1016分周
        case 0x0A: pr2_value = 2033; break;  // 2034分周
        case 0x0B: pr2_value = 4067; break;  // 4068分周
        default:   pr2_value = 63; break;
    }
    
    PR2 = pr2_value;
    
    // Timer2開始
    T2CONbits.TMR2ON = 1;
    
    // Timer2がリセットされるのを待つ
    while(!PIR1bits.TMR2IF);
    PIR1bits.TMR2IF = 0;
}

void setup_ccp(unsigned char pin_index, unsigned char timer_config) {
    // CCPモジュールを比較モードで設定
    // ピンに応じてCCPモジュールを選択
    if(pin_index < 8) {
        // PORT Aピン: CCP1を使用
        APFCONbits.CCP1SEL = 0;  // CCP1をRC5に割り当て
        
        // CCP1を比較モードで設定 (ピン状態を切り替え)
        CCP1CON = 0b00001010;    // CCPxM = 1010 (特殊イベントトリガ)
        
        // CCPR1L/H設定 (50%デューティ)
        unsigned int compare_value = PR2 / 2;
        CCPR1L = compare_value;
        CCPR1H = compare_value >> 8;
    } else {
        // PORT Bピン: CCP2を使用
        APFCONbits.CCP2SEL = 0;  // CCP2をRC1に割り当て
        
        // CCP2を比較モードで設定
        CCP2CON = 0b00001010;    // CCPxM = 1010 (特殊イベントトリガ)
        
        // CCPR2L/H設定 (50%デューティ)
        unsigned int compare_value = PR2 / 2;
        CCPR2L = compare_value;
        CCPR2H = compare_value >> 8;
    }
}

void calculate_frequencies(void) {
    // 各分周比の実際の周波数を計算(参考用)
    for(unsigned char i = 0; i < sizeof(clock_outputs)/sizeof(ClockOutput); i++) {
        double frequency = BASE_FREQ / clock_outputs[i].divider;
        
        // ここで周波数を表示または使用できます
        // 実際のハードウェアではシリアル通信等で出力
        
        // タイマー2とCCPを設定
        setup_timer2(clock_outputs[i].timer_config);
        
        // CCPモジュール設定
        setup_ccp(i, clock_outputs[i].timer_config);
        
        // 対応するピンをHIGHに設定(実際の出力はCCPモジュールが制御)
        if(clock_outputs[i].pin_mask & 0x0F) {
            LATA |= clock_outputs[i].pin_mask;
        } else {
            LATB |= (clock_outputs[i].pin_mask >> 4);
        }
        
        __delay_ms(10);  // 設定間の遅延
    }
}
 
これで分周が難しい96分周以降をすべて出力できます。このソースの変更で残り4種類出力も可能です。
 
次にGPIO1~4を4bit入力(10進数で0~15)として使ってGPIO5に全16種類クロック出力する例です。 
 
 /**
 * PIC16F1823 可変分周クロックジェネレータ
 * 入力値に応じて1.7897725MHzを分周してGPIO1に出力
 */

#include <xc.h>

// コンフィギュレーションビットの設定
#pragma config FOSC = INTOSC  // 内部クロック使用
#pragma config WDTE = OFF     // ウォッチドッグタイマ無効
#pragma config PWRTE = OFF    // パワーアップタイマ無効
#pragma config MCLRE = ON     // MCLR有効
#pragma config CP = OFF       // コードプロテクション無効
#pragma config CPD = OFF      // データメモリプロテクション無効
#pragma config BOREN = OFF    // ブラウンアウトリセット無効
#pragma config CLKOUTEN = OFF // CLKOUT無効
#pragma config IESO = OFF     // 内部/外部スイッチオフ
#pragma config FCMEN = OFF    // フェイルセーフクロックモニタ無効

// 16MHz内部クロック設定
#define _XTAL_FREQ 16000000

// 基本クロック周波数 (1.7897725 MHz)
#define BASE_FREQ 1789772.5

// 分周比テーブル (入力値0-15に対応)
const unsigned int divider_table[16] = {
    4068,  // 0: 4068分周
    2034,  // 1: 2034分周
    1016,  // 2: 1016分周
    762,   // 3: 762分周
    508,   // 4: 508分周
    380,   // 5: 380分周
    254,   // 6: 254分周
    202,   // 7: 202分周
    160,   // 8: 160分周
    128,   // 9: 128分周
    96,    // 10: 96分周
    64,    // 11: 64分周
    32,    // 12: 32分周
    16,    // 13: 16分周
    8,     // 14: 8分周
    4      // 15: 4分周
};

// ピン定義
#define CLOCK_OUT_PIN  LATA1    // GPIO1 (出力ピン)
#define INPUT_PORT     PORTA    // 入力ポート
#define INPUT_MASK     0x3C     // GPIO2-5 (0b00111100)

// 現在の入力値と分周比を保存
volatile unsigned char current_input = 0;
volatile unsigned int current_divider = 4068;
volatile unsigned char input_changed = 0;

// 関数プロトタイプ
void setup_oscillator(void);
void setup_pins(void);
void setup_timer1(void);
void setup_timer2(void);
unsigned char read_input(void);
void update_divider(unsigned char new_input);
void calculate_timer_values(unsigned int divider);
void toggle_clock_output(void);

void main(void) {
    unsigned char new_input;
    
    // オシレーター設定
    setup_oscillator();
    
    // ピン設定
    setup_pins();
    
    // 初期入力値の読み取り
    current_input = read_input();
    update_divider(current_input);
    
    // Timer1設定 (周期測定用)
    setup_timer1();
    
    // Timer2設定 (ソフトウェアPWM用 - 初期設定)
    setup_timer2();
    
    // メインループ
    while(1) {
        // 入力値を読み取る
        new_input = read_input();
        
        // 入力値が変化したかチェック
        if (new_input != current_input) {
            current_input = new_input;
            input_changed = 1;
        }
        
        // 入力変化があった場合は分周比を更新
        if (input_changed) {
            update_divider(current_input);
            input_changed = 0;
            
            // 分周比に応じた遅延で出力をトグル
            // 実際のハードウェアタイマを使用する場合は以下を変更
            __delay_us(10);  // 短い遅延
        }
        
        // タイマーベースのクロック生成(別の方法)
        // この例ではソフトウェアで制御します
        __delay_us(1);  // メインループの速度調整
    }
}

void setup_oscillator(void) {
    // 内部クロックを16MHzに設定
    OSCCON = 0b01111000;  // IRCF=1111 (16MHz), SCS=00 (内部クロック)
    
    // 安定するまで待機
    while(!OSCSTATbits.HFIOFR);
}

void setup_pins(void) {
    // PORT A 設定
    ANSELA = 0x00;        // 全てデジタルI/O
    
    // GPIO1: 出力 (クロック出力)
    // GPIO2-5: 入力 (分周比選択)
    TRISA = 0x3C;         // RA2-RA5を入力、RA1を出力、RA0,RA6,RA7も必要に応じて
    
    // 初期状態
    LATA = 0x00;          // 全てLOW
    CLOCK_OUT_PIN = 0;    // クロック出力初期値LOW
    
    // 弱いプルアップ無効
    OPTION_REGbits.nWPUEN = 1;
    
    // PORT B 設定 (使用しない場合)
    ANSELB = 0x00;
    TRISB = 0xFF;         // 全て入力
}

void setup_timer1(void) {
    // Timer1停止
    T1CONbits.TMR1ON = 0;
    
    // Timer1設定: プリスケーラー1:1, 内部クロック
    T1CON = 0b00000000;   // T1CKPS=00 (1:1), T1OSCEN=0, T1SYNC=0, TMR1CS=0 (内部クロック)
    
    // Timer1割り込み有効 (オプション)
    PIE1bits.TMR1IE = 1;
    INTCONbits.PEIE = 1;
    INTCONbits.GIE = 1;
    
    // Timer1開始
    T1CONbits.TMR1ON = 1;
}

void setup_timer2(void) {
    // Timer2停止
    T2CONbits.TMR2ON = 0;
    
    // Timer2設定: プリスケーラー1:1, ポストスケーラー1:1
    T2CON = 0b00000000;
    
    // 初期PR2値 (後で変更)
    PR2 = 255;
    
    // Timer2割り込み有効
    PIE1bits.TMR2IE = 1;
    
    // Timer2開始
    T2CONbits.TMR2ON = 1;
}

unsigned char read_input(void) {
    // GPIO2-5から4ビット入力を読み取る
    // 入力は反転されているかもしれないので注意
    unsigned char raw_input = (INPUT_PORT & INPUT_MASK) >> 2;
    return raw_input;
}

void update_divider(unsigned char new_input) {
    // 入力値が範囲内かチェック
    if (new_input > 15) {
        new_input = 15;  // 安全のため最大値に制限
    }
    
    // 新しい分周比を取得
    current_divider = divider_table[new_input];
    
    // 分周比に応じたタイマー値を計算
    calculate_timer_values(current_divider);
    
    // 出力周波数を計算 (デバッグ用)
    double output_freq = BASE_FREQ / current_divider;
    
    // ここで実際のタイマー設定を更新
    // 例: Timer2の周期を変更
    unsigned int timer_period;
    
    // 分周比に基づいてTimer2のPR2を設定
    // 注: これは単なる例です。実際の計算は必要に応じて調整してください
    if (current_divider <= 16) {
        timer_period = current_divider * 4;
    } else {
        timer_period = current_divider / 4;
    }
    
    // 範囲チェック
    if (timer_period < 1) timer_period = 1;
    if (timer_period > 255) timer_period = 255;
    
    PR2 = timer_period;
    
    // Timer2をリセットして新しい周期で開始
    TMR2 = 0;
    T2CONbits.TMR2ON = 1;
}

void calculate_timer_values(unsigned int divider) {
    // 分周比に応じたタイマー値を計算
    // この関数では、必要なタイマー設定を計算します
    
    // 例: 出力周波数と周期の計算
    double frequency = BASE_FREQ / divider;
    double period_us = 1000000.0 / frequency;
    
    // 50%デューティサイクルのハイ/ロー時間
    double half_period_us = period_us / 2.0;
    
    // ここで実際のタイマー設定を計算
    // 例: NOP命令の数やタイマープリスケーラー値など
    
    // デバッグ用: LED点滅などで確認可能
}

// Timer2割り込みサービスルーチン
void __interrupt() isr(void) {
    // Timer2割り込み
    if (PIR1bits.TMR2IF) {
        PIR1bits.TMR2IF = 0;  // 割り込みフラグクリア
        
        // クロック出力をトグル
        CLOCK_OUT_PIN = ~CLOCK_OUT_PIN;
        
        // 分周比に応じてタイマー周期を動的に変更する場合はここで処理
        // 注: 単純なトグルでは正確な周波数にならない場合があります
        // 正確な周波数が必要な場合は、より複雑な制御が必要です
    }
    
    // Timer1割り込み (オプション)
    if (PIR1bits.TMR1IF) {
        PIR1bits.TMR1IF = 0;  // 割り込みフラグクリア
        
        // Timer1オーバーフロー時の処理
        // 例えば、長時間のタイミング測定など
    }
}

// 別の方法: ソフトウェア遅延を使用するシンプルな実装
/*
void generate_clock(void) {
    double half_period_us;
    
    while(1) {
        // 現在の分周比に基づいて半周期を計算
        double frequency = BASE_FREQ / current_divider;
        double period_us = 1000000.0 / frequency;
        half_period_us = period_us / 2.0;
        
        // 入力チェック (簡易版)
        unsigned char input = read_input();
        if (input != current_input) {
            current_input = input;
            update_divider(current_input);
            continue;  // 新しい設定で再計算
        }
        
        // クロック出力をトグル
        CLOCK_OUT_PIN = 1;
        __delay_us(half_period_us);
        
        CLOCK_OUT_PIN = 0;
        __delay_us(half_period_us);
    }
}
*/
 
これでUp/Downボタン操作や外部信号で切り替え出来ます。
 
ツマミで操作したい人向けに10KΩのポテンションメーターをA/Dに繋げて0~15の値として計測し、GPIO1から分周クロックを出力させてみます。
 
/**
 * PIC16F1823 可変分周クロックジェネレータ
 * 1.7897725MHzをポテンショメータで選択した分周比で出力
 */

#include <xc.h>

// コンフィギュレーションビット
#pragma config FOSC = INTOSC  // 内部クロック
#pragma config WDTE = OFF     // ウォッチドッグ無効
#pragma config PWRTE = OFF    // パワーアップタイマ無効
#pragma config MCLRE = ON     // MCLR有効
#pragma config CP = OFF       // コードプロテクション無効
#pragma config CPD = OFF      // データプロテクション無効
#pragma config BOREN = OFF    // ブラウンアウトリセット無効
#pragma config CLKOUTEN = OFF // CLKOUT無効
#pragma config IESO = OFF     // 内部/外部スイッチオフ
#pragma config FCMEN = OFF    // フェイルセーフクロック無効

// 16MHz内部クロック
#define _XTAL_FREQ 16000000

// 入力値と分周比の対応テーブル
const unsigned int divider_table[16] = {
    4068,  // 0
    2034,  // 1
    1016,  // 2
    762,   // 3
    508,   // 4
    380,   // 5
    254,   // 6
    202,   // 7
    160,   // 8
    128,   // 9
    96,    // 10
    64,    // 11
    32,    // 12
    16,    // 13
    8,     // 14
    4      // 15
};

// 基本クロック周波数 (Hz)
#define BASE_FREQ 1789772.5

// 関数プロトタイプ
void setup_oscillator(void);
void setup_adc(void);
void setup_timer2(void);
void setup_ccp(void);
unsigned char read_adc_value(void);
void update_clock_divider(unsigned char adc_value);

// グローバル変数
unsigned char current_divider_index = 0;

void main(void) {
    unsigned char adc_value;
    
    // 初期設定
    setup_oscillator();
    setup_adc();
    setup_timer2();
    setup_ccp();
    
    // 初期分周比設定 (4分周)
    update_clock_divider(15);
    
    while(1) {
        // ADC値を読み取り
        adc_value = read_adc_value();
        
        // 分周比が変更された場合
        if(adc_value != current_divider_index) {
            update_clock_divider(adc_value);
            current_divider_index = adc_value;
            
            // 変化を確認するための小さな遅延
            __delay_ms(50);
        }
        
        // 次の読み取りまでの待機
        __delay_ms(10);
    }
}

void setup_oscillator(void) {
    // 内部クロックを16MHzに設定
    OSCCON = 0b01111000;  // IRCF=1111 (16MHz), SCS=00
    
    // 安定待機
    while(!OSCSTATbits.HFIOFR);
}

void setup_adc(void) {
    // AN0 (RA0)をアナログ入力に設定
    TRISAbits.TRISA0 = 1;      // RA0を入力
    ANSELAbits.ANSA0 = 1;      // アナログモード
    
    // ADC設定
    ADCON0 = 0b00000001;       // CHS=0000 (AN0), ADON=1
    ADCON1 = 0b10110000;       // FOSC/64, VDD参照, 右詰め
    
    // ADCコンバージョン時間待機
    __delay_us(10);
}

unsigned char read_adc_value(void) {
    unsigned int adc_result;
    
    // ADC変換開始
    ADCON0bits.GO = 1;
    while(ADCON0bits.GO);      // 変換完了待機
    
    // 結果読み取り (10ビット)
    adc_result = (ADRESH << 8) | ADRESL;
    
    // 0-1023を0-15に変換
    return (adc_result >> 6);  // 1024/16 = 64, なので6ビット右シフト
}

void setup_timer2(void) {
    // Timer2停止
    T2CONbits.TMR2ON = 0;
    
    // プリスケーラー: 1:16、ポストスケーラー: 1:1
    T2CONbits.T2CKPS = 0b11;   // 1:16 プリスケーラー
    T2CONbits.T2OUTPS = 0x0;   // 1:1 ポストスケーラー
    
    // PR2初期値 (4分周用)
    PR2 = 3;  // 初期値
    
    // Timer2開始
    T2CONbits.TMR2ON = 1;
    
    // 最初の周期完了待機
    while(!PIR1bits.TMR2IF);
    PIR1bits.TMR2IF = 0;
}

void setup_ccp(void) {
    // GPIO1 (RA1)を出力ピンとして設定
    TRISAbits.TRISA1 = 0;      // RA1を出力
    ANSELAbits.ANSA1 = 0;      // デジタルモード
    LATAbits.LATA1 = 0;        // 初期状態LOW
    
    // CCP1を特殊イベントトリガモードで設定
    CCP1CON = 0b00001010;      // CCPxM = 1010 (特殊イベント)
    
    // CCP1出力をRA1に設定
    APFCONbits.CCP1SEL = 0;    // CCP1をRA1に割り当て
    
    // 比較値初期設定 (50%デューティ)
    CCPR1L = PR2 / 2;
    CCPR1H = 0;
}

void update_clock_divider(unsigned char adc_value) {
    unsigned int divider;
    unsigned int pr2_value;
    
    // 分周比取得
    divider = divider_table[adc_value];
    
    // Timer2停止
    T2CONbits.TMR2ON = 0;
    
    // PR2値を計算
    // 基準: Timer2クロック = 16MHz / 4 = 4MHz
    // 周期 = (PR2 + 1) * 4 * プリスケーラー / 4MHz
    // 目標周期 = 分周比 / BASE_FREQ
    
    // 簡略化計算:
    // PR2 = (分周比 * 4MHz / BASE_FREQ) - 1
    
    // ただし、分周比が小さい場合は別の方法で計算
    if(divider <= 32) {
        // 小さい分周比の場合はプリスケーラーを変更
        switch(divider) {
            case 4:
                T2CONbits.T2CKPS = 0b00;  // 1:1
                pr2_value = 3;
                break;
            case 8:
                T2CONbits.T2CKPS = 0b00;  // 1:1
                pr2_value = 7;
                break;
            case 16:
                T2CONbits.T2CKPS = 0b01;  // 1:4
                pr2_value = 3;
                break;
            case 32:
                T2CONbits.T2CKPS = 0b01;  // 1:4
                pr2_value = 7;
                break;
            case 64:
                T2CONbits.T2CKPS = 0b10;  // 1:16
                pr2_value = 3;
                break;
            default:
                T2CONbits.T2CKPS = 0b10;  // 1:16
                pr2_value = 7;
                break;
        }
    } else {
        // 大きい分周比の計算
        // PR2 = (divider * 1000000 / 447443) - 1  (概算)
        // より正確な計算:
        double period_seconds = (double)divider / BASE_FREQ;
        double timer2_ticks = period_seconds * 1000000.0;  // 4MHzクロック
        
        // プリスケーラー選択
        if(timer2_ticks <= 256) {
            T2CONbits.T2CKPS = 0b00;  // 1:1
            pr2_value = (unsigned int)(timer2_ticks / 4) - 1;
        } else if(timer2_ticks <= 1024) {
            T2CONbits.T2CKPS = 0b01;  // 1:4
            pr2_value = (unsigned int)(timer2_ticks / 16) - 1;
        } else if(timer2_ticks <= 4096) {
            T2CONbits.T2CKPS = 0b10;  // 1:16
            pr2_value = (unsigned int)(timer2_ticks / 64) - 1;
        } else {
            T2CONbits.T2CKPS = 0b11;  // 1:64
            pr2_value = (unsigned int)(timer2_ticks / 256) - 1;
        }
        
        // PR2の最大値制限 (8ビットなので0-255)
        if(pr2_value > 255) pr2_value = 255;
    }
    
    // PR2設定
    PR2 = pr2_value;
    
    // CCP比較値更新 (50%デューティ)
    CCPR1L = pr2_value / 2;
    CCPR1H = 0;
    
    // Timer2リセット
    TMR2 = 0;
    
    // Timer2開始
    T2CONbits.TMR2ON = 1;
    
    // CCP1CONを再設定(必要に応じて)
    CCP1CON = 0b00001010;  // 特殊イベントトリガ
    
    // 新しい設定が確立されるまで待機
    __delay_us(10);
}

// オプション: 分周比に応じたプリスケーラー最適化関数
unsigned char get_prescaler_for_divider(unsigned int divider) {
    if(divider <= 8) return 0b00;     // 1:1
    if(divider <= 32) return 0b01;    // 1:4
    if(divider <= 128) return 0b10;   // 1:16
    return 0b11;                      // 1:64
}
 
このようにマイクロコントローラーは多くのLogicICやA/Dそしてクロックや記憶装置を備えているので、ハードに詳しい人が適切に使うと全てにおいてパフォーマンスが高いです。別のマイクロコントローラーへの移植はAIにソースを読み込ませて相談してみましょう。良い結果が出るはずです。
 

コメント