ダイソーLEDキャンドルにタイマー&調光機能を追加する~ソフトウェア編~

ダイソーのLEDキャンドルにPICマイコンを使用してタイマー&調光機能を追加してみた。
後編となる今回は、PICマイコンのプログラミングを行う。




<はじめに>
前々回、前回の記事はこちら。
monaruka.hatenablog.com
monaruka.hatenablog.com




<プログラムについて>
今回使用するコンポーネントはTimer1。
16bitタイマーでクロック周波数は31,000Hzのため、1秒毎に割り込みを発生させ、24時間タイマーを作る。
そして、タイマーとロータリースイッチの状態に合わせてRA5の出力を変化させ、LEDのON/OFFを制御する。


プログラムはこちら。

// PIC12LF1822 Configuration Bit Settings
// 'C' source line config statements

// 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 = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#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)

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

#include <xc.h>

#define _XTAL_FREQ 31000

unsigned char timer_s;
unsigned char timer_m;
unsigned char timer_h;

void main(void) {

  // Clockコンフィグ
  OSCCONbits.SCS  = 0b10;   // 内部発振
  OSCCONbits.IRCF = 0b0000; // 31kHz

  // IOコンフィグ
  TRISA  = 0x0F; // RA0~3は入力、RA4~5は出力
  ANSELA = 0x00; // 全部デジタル
  nWPUEN = 0;    // 内部プルアップ有効化
  WPUA   = 0x0F; // RA0~3を内部プルアップにする
  LATA   = 0x00; // 出力はLow

  // Timer1コンフィグ
  T1CONbits.TMR1CS  = 0b01; // TimerクロックをFoscにする
  T1CONbits.T1CKPS  = 0b00; // プリスケールを1/1にする
  T1GCONbits.TMR1GE = 0;    // Timer1ゲートを無効化
  TMR1   = 34536; // 65536-31000
  TMR1ON = 1;     // タイマー1スタート

  // 割り込みコンフィグ
  TMR1IF = 0; // Timer1割り込みフラグの解除
  TMR1IE = 1; // Timer1割り込みの有効化
  PEIE   = 1; // 周辺機能割り込みの有効化
  GIE    = 1; // グローバル割り込みの有効化

  while(1){ }

  return;
}

void __interrupt() isr(void){
  static unsigned char timer_s = 0;
  static unsigned char timer_m = 0;
  static unsigned char timer_h = 0;
  static unsigned char rotary_sw = 0;

  if(TMR1IF == 1){
    TMR1   = 34536; // Timer1の再セット
    TMR1IF = 0;     // Timer1の再スタート
    // timerカウンタ
    timer_s++;
    if(timer_s == 60){
      timer_s = 0;
      timer_m++;
      if(timer_m == 60){
        timer_m = 0;
        timer_h++;
        if(timer_h == 24){
          timer_h = 0;
        }
        // 出力
        rotary_sw = 0x0F & PORTA;
        if(rotary_sw == 0){
          LATA5 = 0;
        }
        else{
          if(rotary_sw > timer_h){
            LATA5 = 0;
          }
          else{
            LATA5 = 1;
          }
        }
      }
    }
  }
}




プログラムについて簡単に解説すると、

  • #pragma config ○○○○はPICマイコンの初期設定。FOSC = INTOSCでクロックを内部発振にして、他は基本的に使用しないのでOFFにした。
  • #include は必須。マイコンの定義ファイル等を呼び出している。
  • #define _XTAL_FREQ ○○○○はディレイ関数を使用するのに必須で、○○○○の部分にクロック周波数を入力する。今回はディレイ関数を使用していないのでなくてもいい。
  • 各種コンフィグはコメントに書いた通り。PICマイコンは1行ずつレジスタを叩くようなプログラミングになりがちなので、データシートとにらめっこしながら設定していこう。
  • (レジスタ名)bits.(ビット名)はビット単位でレジスタを設定するのに使用する。筆者個人としては、ビット毎に役割が変わるものはこれで設定した方が変更しやすくていいと思う。
  • while(1){ }がメインループで、今回はタイマー割り込みで動かすのでただの無限ループにした。
  • void __interrupt() isr(void){ }がタイマー割り込み関数。まずif(TMR1IF == 1)でタイマー割り込みであることを確定して、割り込みの再セットを行う。
  • 次に24時間タイマーをカウントアップする。タイマー用変数は分かりやすさとデバッグのしやすさを考慮し、timer_s(秒)、timer_m(分)、timer_h(時間)の3次元カウンタにした。変数はstaticで定義しないと値を保持できないので注意。
  • カウントアップ後はロータリースイッチの値をrotary_swに代入する。rotary_swが0だったら常時点灯(RA5を'0')、1~15だったらtimer_hと比較して、timer_h未満の間だけ点灯する。


これを前回製作した基板のマイコンに書き込み動作確認をしたところ、設定時間通りにLEDをON/OFFできるようになった。
ただし、TMR1の設定は65536-31000より34536としているが、RC発振のため誤差がある。
データシートによると±2%のようで、時間にすると1日約30分もずれる可能性がある。
筆者の場合は10分ほどずれていたし、日によって誤差が変化していた。
精度が必要なやつに内部発振はダメだね。




<改善案>
内部発振だと誤差が大きいため、水晶発振子を使用するというのも有効。
水晶発振子は多くの時計に使用されているクロック源で、月差30秒以内の高精度を誇る(その前に電池が切れるだろうけど)。
今回はピン数が足りなくて使用しなかったが、もっとピン数の多いマイコンを使用するか、ロータリースイッチを1ビット減らせばPIC12F1822でも使用できる。
どうせ8~15時間タイマーなんて使わないだろうし、最初からそうすればよかったかも。
クロックだけでなくピンアサインも変わるので、プログラムはそれに合わせて修正が必要。


また、マイコンの消費電流はデータシートを見たところ1mA弱だった。
ダイソーのLEDキャンドルが単四×3仕様のため5V対応マイコンを使用しているが、ニトリのLEDキャンドルは単三×2のため、低電圧版のPIC12LF1822を使用すれば消費電流が1/5以下になる見込み。




<おわりに>
タイマーや調光のなかったダイソーLEDキャンドルが、今回の改造によりタイマーと調光機能が追加され、利便性が向上した。
これで練習は完了のため、ニトリのLEDキャンドルを買ったら改造しアップしようと思う。
まぁ近所にニトリがないので、いつ買うかは未定。