第19回 割込み処理(4)〜タイマープログラム実装〜

タイマープログラムに(無理やり?)割込み処理を実装します。

目次

動作仕様

それでは今までの内容を元に、タイマープログラムに割込み処理を追加しましょう。

タイマー動作としては以下の仕様とします。

  • スタートボタン(RA3)が押されてタイマー動作開始後、タイマー動作中にスタートボタンが押されたら、タイマーカウントをリセットする

この仕様の場合、LEDの点滅中(タイマー時間計測中)の任意のタイミングで強制的にタイマー時間をリセットしてしまいます。

ちょっと乱暴な仕様ですが、実装をシンプルにして割り込み処理を行うプログラム構成をわかりやすくしてみました。

プログラム構成

プログラムを作成する前に、プログラム構成といいますか、アルゴリズムをどうするか検討します。

タイマープログラムに実装する処理内容は、割込みが発生した際にタイマーをリセットする、というものです。

プログラム動作としては、「割込みが発生」 → 「タイマーのカウントを元に戻す」という処理でうまくいきそうな気がします。

でも実際にこのような実装をするにはちょっと工夫が必要です。

最初にその背景を確認しておきます。


最初に割り込み処理の関数の仕様を確認します。

void __interrupt isr(void) {
    // 割り込み処理内容
}

この関数仕様から、とても重要なことがわかります。

それは、割り込み処理関数では、引数で値を渡すこともできないですし、戻り値を返してもらうこともできません!

つまり、main関数と割込処理関数では値のやり取りができない、ということになります。

Pic app 19 interrupt function

割り込みが発生したらタイマー時間をリセットする、ということは、「割り込みが発生したら、main関数で仕様しているタイマーカウント変数の値を、割り込み関数がリセットする」という操作が必要です。

でも、割込み処理関数からmain関数内の変数を直接操作することができないので、ちょっと工夫が必要です。


このような場合は、両方の関数、つまりmain関数と割込み処理関数の両方から参照・操作できるような変数を用意します。

つまり、タイマーカウントの変数をグローバル変数として用意する必要があります。

このように用意したグローバル変数を介して、次のようにmain関数と割込み処理関数の両方から変数を読み書きする、ということになります。

Pic app 19 global variable

割り込み処理を行う場合、グローバル変数を2つの関数で共有することがありますが、この場合少し注意が必要です。

その注意点は少し複雑な内容ですので、これから作成するプログラムは2個作成することにします。

  • グローバル変数が1バイト
    最初に、単純にグローバル変数を介してmain関数と割り込み処理関数で値をやり取りするプログラムを作成します。グローバル変数は1バイト(uint8_t型)で作成します
  • グローバル変数が2バイト
    グローバル変数が2バイト以上の場合、のプログラムは問題が発生するケースがあります。そこでその問題の内容とそれを解決するプログラムを作成します。グローバル変数は2バイト(uint16_t型)で作成します

プログラム動作の検討

これから、main関数と割り込み処理関数の両方がグローバル変数を操作して、スイッチが押されたときの処理を行なっていきます。

現在のプログラムでは、タイマー設定時間はEEPROMに格納されています。プログラム開始時に、EEPROMから設定時間を読み取って、その値の時間計測をしています。

ここで、「タイマー設定時間」と「現在のタイマー計測時間」を次のようにグローバル変数として定義することにします。

uint8_t timerValue; // タイマー設定時間を保持する変数
uint8_t timerCount; // タイマー計測に使用する変数

ここで、main関数と割り込み処理関数の処理をどのようにすれば良いかまとめます。

main関数の処理

main関数では、最初にEEPROMからタイマー設定時間を読み出しています。

この読み出した値をグローバル変数timerValueに保存しておきます。

また、ユーザがスイッチを押したら、タイマー計測を開始しますが、タイマーの計測はグローバル変数timerCountを使用します。

タイマー動作としては、最初にtimerCount変数にタイマー設定時間(timerValue)を代入して、timerCountを1秒に1ずつ減らしながら0になるまでカウントします。

割り込み処理関数の処理

ユーザがスイッチを押したとき、タイマーをリセットします。

割り込み処理関数では、timerCount(タイマー計測変数)にtimerValue(タイマー設定時間)を代入します。

なお、割り込み処理関数では、割り込み処理が発生したことを表現するために、LEDを短く3回点滅させてみます。(本来でしたら割り込み発生時は、時間がかかる割り込み処理は避けたほうが良いですが…)


このような考え方で割込み処理によるタイマーリセットプログラムを実装してみます。

❶ グローバル変数が1バイト

先ほど検討したように、グローバル変数を宣言して、main関数と割り込み処理関数でそれぞれ使用するようにしてみました。

/*
 * PICマイコン電子工作入門 応用編 第19回
 *   割り込み処理によりタイマーカウントリセット
 *   グローバル変数が1バイトのケース
 */

#include <xc.h>

// PIC12F1822 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)
// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = OFF      // PLL Enable (4x PLL disabled)
#pragma config STVREN = OFF     // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = OFF        // Low-Voltage Programming Enable (High-voltage on MCLR/VPP must be used for programming)


// クロック周波数指定
// __delay_ms()関数が使用する
#define _XTAL_FREQ 1000000

// 割り込み関数のプロトタイプ宣言
void __interrupt() isr(void);

// EEPROM初期データ
// アドレス0はタイマー設定値の初期値、残りはダミーデータ
__EEPROM_DATA (0x05, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);

// タイマー関連グローバル変数
uint8_t timerValue; // タイマー設定時間 (main関数の最初でEEPROMから設定値を読み出し、この変数に入れておく)
uint8_t timerCount; // 現在のタイマー残り時間 (main関数ではこの値を減らしながらタイマーカウント。割込み処理関数でtimerValueに戻す)

void main(void) {

    // PICマイコン設定
    OSCCON = 0b01011010;  // 内部クロック周波数を1MHzに設定
    ANSELA = 0b00000100;  // RA2ピンをアナログ、それ以外のピンはデジタルに設定
    TRISA  = 0b00001100;  // RA2とRA3を入力、それ以外は出力に設定(RA3ピンは設定にかかわらず入力ピン)

    // ADコンバータ設定
    ADCON0 = 0b00001001;  // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
    ADCON1 = 0b10000000;  // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD

    // ブザーをOFFにする
    LATA4 = 0;

    // タイマー設定時間をEEPROMから読み出す
    //   timerValueは1バイトにしています
    timerValue = eeprom_read(0);

    // タイマー時間をLEDの点滅回数で表現する
    //
    // 動作開始後少し待つ
    __delay_ms(500);

    // 設定時間分LEDを点滅する
    for(uint8_t i=0; i<timerValue; i++) {
        LATA5 = 0;
        __delay_ms(200);
        LATA5 = 1;
        __delay_ms(200);
    }

    // 少しの間LEDをOFFにする少しの間LEDをOFFにする
    LATA5 = 0;
    __delay_ms(800);

    // LEDを点灯する
    LATA5 = 1;            
    
    // スイッチが押されるまで、タイマー時間増減スイッチ処理を行う
    while( RA3 ) {
        // ADコンバータ読み取り
        GO = 1;     // 読み取り依頼
        while(GO) { // 読み取り完了待ち
        }
        
        // ADRESの数値に応じてタイマー時間の増減を行う
        //
        // タイマー時間を減らすスイッチの処理(ADRESが250未満の場合)
        if( ADRES < 250 ) {
            // タイマー時間を減らす(マイナスにならないようにする)
            if( timerValue > 0 ) {
                timerValue--;
            }

            // タイマー時間をLEDの点滅回数で表現する
            //
            // 少しの間LEDをOFFにする
            LATA5 = 0;
            __delay_ms(500);
            
            // 設定時間分LEDを点滅する
            for(uint8_t i=0; i<timerValue; i++) {
                LATA5 = 0;
                __delay_ms(200);
                LATA5 = 1;
                __delay_ms(200);
            }

            // 少しの間LEDをOFFにする少しの間LEDをOFFにする
            LATA5 = 0;
            __delay_ms(800);

            // LEDを点灯する
            LATA5 = 1;            
        } else {
            // タイマー時間を増やすスイッチの処理(ADRESが250以上750未満)
            if( ADRES < 750 ) {
                // タイマー時間を増やす(上限チェックはしない)
                timerValue++;
                
                // タイマー時間をLEDの点滅回数で表現する
                LATA5 = 0;
                __delay_ms(500);
                
                for(uint8_t i=0; i < timerValue; i++) {
                    LATA5 = 0;
                    __delay_ms(200);
                    LATA5 = 1;
                    __delay_ms(200);
                }
                
                LATA5 = 0;
                __delay_ms(800);
                
                LATA5 = 1;
            }
        }
    }

    // タイマー設定値をEEPROMに書き込む
    eeprom_write(0, timerValue);

    // timerCountをタイマー設定時間(timerValue)に設定
    timerCount = timerValue;

    // RA3割り込み設定
    IOCAF3 = 0;  // RA3割り込みフラグをクリアしておく
    IOCIE  = 1;
    IOCAN3 = 1;
    GIE    = 1;

    // LEDをtimerValue分点滅する (timerCountが1以上であれば継続、0であればカウント終了)
    while( timerCount ) {
        timerCount--;
        // LEDを950ms消灯する
        LATA5 = 0;
        __delay_ms(950);            
        // LEDを50ms点灯する
        LATA5 = 1;
        __delay_ms(50);
    }

    // 割り込み禁止 (割り込みはRA3ピンのみなので、GIEのみで禁止する)
    GIE = 0;

    // アラーム音を3回鳴らす
    for(uint8_t i=0; i<3; i++) {
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピーッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        // 1回分の音パターンの間を少し空ける
        __delay_ms(800);
    }

    // LEDをPWM制御してスムーズな点滅制御をする
    //
    // PWM機能のピン割り当て設定
    APFCONbits.CCP1SEL = 1;     // PWM機能をRA5ピンに設定
    CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
    CCP1CONbits.P1M = 0b00;     // RA2ピンはGPIOに設定

    // 周期とデューティーサイクルの設定
    //   周期は1ms
    //   デューティーサイクルは100%から開始するので1msに設定
    T2CONbits.T2CKPS = 0b00;    // プリスケーラ値を1に設定
    PR2 = 249;                  // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
    CCPR1L = 1000/4;            // デューティーサイクルを0.5msに設定
    CCP1CONbits.DC1B = 1000 & 0b11;

    // PWM制御スタート
    T2CONbits.TMR2ON = 1;

    // LEDをPWM制御
    // デューテー比は10%〜100%の制御にする
    while(1) {
        // 100% -> 5%の制御
        for(uint16_t duty=1000; duty>=50; duty--) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
        // 5% -> 100%の制御
        for(uint16_t duty=50; duty<=1000; duty++) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
    }

    // 以下の命令は実行されない
    return;
}

// 割り込み処理関数
void __interrupt() isr(void)
{
    // タイマー測定時間リセット
    timerCount = timerValue;

    // LEDが点灯中に割り込みが発生することもあるので、
    // 少しの間LEDをOFFにする
    // ただし、割り込み処理内で時間がかかる処理を入れるのは本来は良くないです
    LATA5 = 0;
    __delay_ms(300);
    
    // 短く3回点滅
    for( uint8_t i=0; i<3; i++) {
        LATA5 = 0;
        __delay_ms(100);
        LATA5 = 1;
        __delay_ms(100);
    }

    // 少しの間LEDをOFFにする
    // この処理も時間がかかるので本来は避けるべきです
    LATA5 = 0;
    __delay_ms(300);

    // 割り込み処理フラグクリア
    IOCAF3 = 0; 

}

❷ グローバル変数が2バイト

先ほどのプログラムは、タイマー設定時間やタイマー計測用の変数は8ビットですので、255までのカウントになります。

秒単位のタイマーにしたい場合、255秒=4分15秒までしか計測できないタイマーになってしまいます。

計測時間を伸ばすためには、タイマー設定時間変数のtimerValueとタイマー計測用変数のtimerCountを16ビットに拡張する対応が考えられます。

でも、先ほどのプログラムでそれぞれの変数をuint16_t型に変更するだけですと、割り込み処理で問題が発生する可能性があります。

この問題は、PICマイコンに限らず、他のマイコンでも同じです。

そこで、割り込み処理を行うときの変数型について補足しておきたいと思います。

問題点

プログラムでは、タイマー時間計測用としてuint8_t timerCount;で変数宣言をして、タイマー計測の処理では次のようにしています。

    // LEDをtimerValue分点滅する (timerCountが1以上であれば継続、0であればカウント終了)
    while( timerCount ) {
        timerCount--;
        // LEDを950ms消灯する
        LATA5 = 0;
        __delay_ms(950);            
        // LEDを50ms点灯する
        LATA5 = 1;
        __delay_ms(50);
    }

割り込み処理を行わないプログラムでは、timerCountを16ビットにしたい場合は単にuint16_t timerCount;と変数宣言をすれば問題ありません。(EEPROMの16ビット読み出しの対応などは必要ですが…)

ところが、割り込み処理を行う場合、これにより問題が発生する可能性があります。


最初にその問題と原因を説明します。

タイマー時間計測では、timerCount変数を次のように1ずつ減らしながら0になるまでカウントしています。

timerCount--;

また、このように処理しているtimerCount変数は、whileの条件で0かそうでないかを判断しています。

while( timerCount )

C言語では、このように1行で書かれていますが、ビルド後の機械語(PIC12F1822が直接理解できる実行コード)では、複数の処理から構成されているんです。

PIC12F1822は8ビット単位で処理をしている関係から、timerCountを一気に計算したり比較しているわけではないんです。

実際には、「timerCountの下位1バイトを取り出す」「timerCountの上位1バイトを取り出す」「取り出した16ビットのデータに対して計算や比較を行う」というように何回かの操作に分けて処理をしています。

ところで、割り込み処理はどのタイングでも発生します。

つまり、上のtimerCountを操作するとき、例えば「timerCountの下位1バイトを取り出す」「timerCountの上位1バイトを取り出す」の間で割り込みが発生する可能性もあります。

timerCountの16ビットの値を知ろうとしているのに、その途中でtimerCountの値が変更されてしまいますので、どこかで矛盾が発生する可能性が出てきてしまうんです。

解決策

このように、C言語では1行の処理(一回のまとまった処理)に見える場合でも、機械語レベルで複数の処理に分かれる場合、その1行の中身の処理で割り込みが発生しないようにする必要があります。

つまり、そのような処理の前で割り込みを禁止、処理後に割り込みを許可する必要があります

例えば、timerCount--;の処理では、次のように割り込みを禁止してから操作、その後割り込みを許可する、という処理を追加する必要があります。

GIE = 0;  // 割り込みを禁止 (複数種類の割り込みがある場合は、対応する割り込みスイッチをOFFにします)
timerCount--;
GIE = 1; // 割り込みを許可

また、whileの比較でも前後で割り込み禁止、許可をする必要がありますが、whileではそのような記述はできません。

そこで、次のように処理している内容を分解して、いったん割り込み処理で操作されない仮の変数に代入してから比較する、という操作が必要になります。

    // 無限ループにして、内部の処理でif文で残り時間が0になったら抜ける、という処理に分解します 
    while(1) {
        uint16_t current_local_count;        // タイマーカウントのローカル保存用変数
        uint8_t perform_blink_operation = 0; // 点滅するかどうかの判定フラグ

        // timerCountの読み取りと変更は割り込み禁止状態で行う
        GIE = 0; // グローバル割り込み禁止
        current_local_count = timerCount; // 実際の機械語は複数の処理に分かれるので割り込み禁止状態で行います
        if (current_local_count > 0) {
            timerCount--;
            perform_blink_operation = 1; // 点滅を継続する場合はフラグを立てておく (1にしておく)
        }
        GIE = 1; // グローバル割り込み許可

        // カウント残り時間が1秒以上の時に点滅処理、そうでない場合(0の場合)はwhileを抜ける
        if (perform_blink_operation) {
            // LEDを950ms消灯する
            LATA5 = 0;
            __delay_ms(950);
            // LEDを50ms点灯する
            LATA5 = 1;
            __delay_ms(50);
        } else {
            // timerCountが0になったらwhileループを抜ける
            break;
        }
    }

上のように変更すると、割り込み許可中はグローバル変数の操作や参照がされないようになります。

メイン処理と割り込み処理の双方から利用するグローバル変数を扱う場合、特にメイン関数での変数の操作や参照は、このように割り込み禁止状態で行う必要がある、という注意点があります。なんだかちょっと大変ですね…

とはいっても、最近はAIのプログラミングの能力がかなり高くなってきていますので、AIにプログラムを変更してもらうのも一つの方法かな、と思います。

実は、❷の変数が16ビットのプログラムはAIに作成してもらいました。

AIに❶の8ビットのプログラムを提示して、「timerValuetimerCountを16ビットに変更してください」とお願いして作成してもらったものです。(Gemini 2.5 Proを使用しました。ただ、EEPROM読み出しと書き込みは16ビット対応してくれませんでした。そのあたりはプロンプトで明確にお願いする必要がありそうです)

プログラム

少し複雑になってしまいますが、次のようにまとめてみました。(Gemini 2.5 Pro作成プログラムをベースに、このシリーズ合わせて変更したものです)

/*
 * PICマイコン電子工作入門 応用編 第19回
 *   割り込み処理によりタイマーカウントリセット
 *   グローバル変数が2バイトのケース
 */

#include <xc.h>

// PIC12F1822 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF        // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = ON       // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)
// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = OFF      // PLL Enable (4x PLL disabled)
#pragma config STVREN = OFF     // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = OFF        // Low-Voltage Programming Enable (High-voltage on MCLR/VPP must be used for programming)


// クロック周波数指定
// __delay_ms()関数が使用する
#define _XTAL_FREQ 1000000

// 割り込み関数のプロトタイプ宣言
void __interrupt() isr(void);

// EEPROM初期データ
// アドレス0にタイマー設定値の下位バイト、アドレス1に上位バイトの初期値を設定
// 次のデータは5の例
__EEPROM_DATA (0x05, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF);

// タイマー関連グローバル変数
uint16_t timerValue; // タイマー設定時間 (main関数の最初でEEPROMから設定値を読み出し、この変数に入れておく)
uint16_t timerCount; // 現在のタイマー残り時間 (main関数ではこの値を減らしながらタイマーカウント。割込み処理関数でtimerValueに戻す)

void main(void) {

    // PICマイコン設定
    OSCCON = 0b01011010;  // 内部クロック周波数を1MHzに設定
    ANSELA = 0b00000100;  // RA2ピンをアナログ、それ以外のピンはデジタルに設定
    TRISA  = 0b00001100;  // RA2とRA3を入力、それ以外は出力に設定(RA3ピンは設定にかかわらず入力ピン)

    // ADコンバータ設定
    ADCON0 = 0b00001001;  // RA2(AN2)をADコンバータピンに設定し、ADコンバータ機能をEbableにする
    ADCON1 = 0b10000000;  // 結果数値は右寄せ、ADコンバータクロックはFOSC/2、基準電圧はVDD

    // ブザーをOFFにする
    LATA4 = 0;

    // タイマー設定時間をEEPROMから読み出す (2バイトに変更)
    uint8_t timerValueLower = eeprom_read(0); // アドレス0から下位バイトを読み出し
    uint8_t timerValueUpper = eeprom_read(1); // アドレス1から上位バイトを読み出し
    timerValue = ((uint16_t)timerValueUpper << 8) | timerValueLower; // 2バイトを結合して16ビットのtimerValueへ代入

    // タイマー時間をLEDの点滅回数で表現する
    //
    // 動作開始後少し待つ
    __delay_ms(500);

    // 設定時間分LEDを点滅する
    for(uint16_t i=0; i<timerValue; i++) {
        LATA5 = 0;
        __delay_ms(200);
        LATA5 = 1;
        __delay_ms(200);
    }

    // 少しの間LEDをOFFにする少しの間LEDをOFFにする
    LATA5 = 0;
    __delay_ms(800);

    // LEDを点灯する
    LATA5 = 1;            
    
    // スイッチが押されるまで、タイマー時間増減スイッチ処理を行う
    while( RA3 ) {
        // ADコンバータ読み取り
        GO = 1;     // 読み取り依頼
        while(GO) { // 読み取り完了待ち
        }
        
        // ADRESの数値に応じてタイマー時間の増減を行う
        //
        // タイマー時間を減らすスイッチの処理(ADRESが250未満の場合)
        if( ADRES < 250 ) {
            // タイマー時間を減らす(マイナスにならないようにする)
            if( timerValue > 0 ) {
                timerValue--;
            }

            // タイマー時間をLEDの点滅回数で表現する
            //
            // 少しの間LEDをOFFにする
            LATA5 = 0;
            __delay_ms(500);
            
            // 設定時間分LEDを点滅する
            for(uint8_t i=0; i<timerValue; i++) {
                LATA5 = 0;
                __delay_ms(200);
                LATA5 = 1;
                __delay_ms(200);
            }

            // 少しの間LEDをOFFにする少しの間LEDをOFFにする
            LATA5 = 0;
            __delay_ms(800);

            // LEDを点灯する
            LATA5 = 1;            
        } else {
            // タイマー時間を増やすスイッチの処理(ADRESが250以上750未満)
            if( ADRES < 750 ) {
                // タイマー時間を増やす(上限チェックはしない)
                timerValue++;
                
                // タイマー時間をLEDの点滅回数で表現する
                LATA5 = 0;
                __delay_ms(500);
                
                for(uint8_t i=0; i < timerValue; i++) {
                    LATA5 = 0;
                    __delay_ms(200);
                    LATA5 = 1;
                    __delay_ms(200);
                }
                
                LATA5 = 0;
                __delay_ms(800);
                
                LATA5 = 1;
            }
        }
    }

    // タイマー設定値をEEPROMに書き込む (2バイトに変更)
    timerValueLower = (uint8_t)(timerValue & 0xFF);        // timerValueの下位バイト
    timerValueUpper = (uint8_t)((timerValue >> 8) & 0xFF); // timerValueの上位バイト
    eeprom_write(0, timerValueLower); // アドレス0に下位バイトを書き込み
    eeprom_write(1, timerValueUpper); // アドレス1に上位バイトを書き込み

    // timerCountをタイマー設定時間(timerValue)に設定
    timerCount = timerValue;

    // RA3割り込み設定
    IOCAF3 = 0; // RA3割り込みフラグをクリアしておく
    IOCIE  = 1;
    IOCAN3 = 1;
    GIE    = 1;

    // LEDをtimerValue分点滅する
    while(1) {
        uint16_t current_local_count;        // タイマーカウントのローカル保存用変数
        uint8_t perform_blink_operation = 0; // 点滅するかどうかの判定フラグ

        // timerCountの読み取りと変更は割り込み禁止状態で行う
        GIE = 0; // グローバル割り込み禁止
        current_local_count = timerCount;
        if (current_local_count > 0) {
            timerCount--;
            perform_blink_operation = 1;
        }
        GIE = 1; // グローバル割り込み許可

        // カウント残り時間が1秒以上の時に点滅処理、そうでない場合(0の場合)はwhileを抜ける
        if (perform_blink_operation) {
            // LEDを950ms消灯する
            LATA5 = 0;
            __delay_ms(950);
            // LEDを50ms点灯する
            LATA5 = 1;
            __delay_ms(50);
        } else {
            // timerCountが0になったらループを抜ける
            break;
        }
    }

    // 割り込み禁止 (割り込みはRA3ピンのみなので、GIEのみで禁止する)
    GIE = 0;

    // アラーム音を3回鳴らす
    for(uint8_t i=0; i<3; i++) {
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピーッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        // 1回分の音パターンの間を少し空ける
        __delay_ms(800);
    }

    // LEDをPWM制御してスムーズな点滅制御をする
    //
    // PWM機能のピン割り当て設定
    APFCONbits.CCP1SEL = 1;     // PWM機能をRA5ピンに設定
    CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
    CCP1CONbits.P1M = 0b00;     // RA2ピンはGPIOに設定

    // 周期とデューティーサイクルの設定
    //   周期は1ms
    //   デューティーサイクルは100%から開始するので1msに設定
    T2CONbits.T2CKPS = 0b00;    // プリスケーラ値を1に設定
    PR2 = 249;                  // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
    CCPR1L = 1000/4;            // デューティーサイクルを0.5msに設定
    CCP1CONbits.DC1B = 1000 & 0b11;

    // PWM制御スタート
    T2CONbits.TMR2ON = 1;

    // LEDをPWM制御
    // デューテー比は10%〜100%の制御にする
    while(1) {
        // 100% -> 5%の制御
        for(uint16_t duty=1000; duty>=50; duty--) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
        // 5% -> 100%の制御
        for(uint16_t duty=50; duty<=1000; duty++) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
    }

    // 以下の命令は実行されない
    return;
}

// 割り込み処理関数
void __interrupt() isr(void)
{
    // タイマー測定時間リセット
    timerCount = timerValue;

    // LEDが点灯中に割り込みが発生することもあるので、
    // 少しの間LEDをOFFにする
    // ただし、割り込み処理内で時間がかかる処理を入れるのは本来は良くないです
    LATA5 = 0;
    __delay_ms(300);
    
    // 短く3回点滅
    for( uint8_t i=0; i<3; i++) {
        LATA5 = 0;
        __delay_ms(100);
        LATA5 = 1;
        __delay_ms(100);
    }

    // 少しの間LEDをOFFにする
    // この処理も時間がかかるので本来は避けるべきです
    LATA5 = 0;
    __delay_ms(300);

    // 割り込み処理フラグクリア
    IOCAF3 = 0; 

}

更新履歴

日付内容
2017.10.4新規投稿
2018.12.2プログラムテンプレートをMPLABX IDE v5.10のものに変更
2019.2.8割り込み処理関数宣言をXC8コンパイラVersion2.00以降の書式に変更
2025.5.25プログラム仕様変更(割込み処理内はタイマー時間をリセットのみ)
uint16_tなどの、2バイトデータを扱う場合の注意点追加
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
弓田 尚志
弓田 尚志
5 年 前

unsigned char timerCount; // 現在のタイマー残り時間(main関数ではこの値を減らしながらタイマーカウント。割込み処理関数では呼ばれたらtimerValueに戻す)
と書かれていますが、「完成した割り込みプログラム」のどこに書かれているのでしょうか。
どこにも見えませんが?

管理者
管理者
返信  弓田 尚志
5 年 前

ご指摘どうもありがとうございました。
早速表示を修正いたしました。ご確認いただければと思います。

原因ですが、このサイトはWordPressを使用しており、仕様が変わったようでソースコードが正しく表示されなくなってしまいました。記事数が多いため一通り変更することが困難で、ご指摘いただいた記事をその度に修正しております。

ご迷惑をおかけしました。

目次