8.割り込みのプログラムを作ってみよう


これから作るロボットは、ADコンバータからなんらかの値を得て、その値によってモーターを動かすことをします。モーターを動かすにはPWM制御という方法を使いますので、タイマーの値を見ながらこれらの仕事をしなければいけません。一人一人勝手なことをする使用人全てを一気に監督するのはかなりきつい仕事で、これを打開するには賢い使用人を用いて、全責任を負わせ、100%の監督をしないことです。一回使用人に仕事を言いつければ、その使用人はその仕事を勝手にやり続けてくれるようにします。プログラミングも同じようなものです。
 ここでは、ADコンバーターから値を得て、決まった変数に代入するだけの仕事をやらせます。ですから、監督するmainプログラムの方では、ADコンバータのことは忘れて、その変数の値を見るだけでよくなります。

 下の例は多重割り込みの例ですが、ここでは単純に一つの割り込みになります。

割り込み処理
A/Dコンバーターと割り込み処理
A/Dコンバーターと割り込み処理2

割り込み処理

割り込み処理の練習として、1秒間隔でLEDを点滅させてみましょう。mainプログラムの中では単純に0.5秒経ったかどうかを見るだけです。
 割り込み処理ルーチン(上の図ではBの処理とかCの処理)は
void interrupt Intr(void)
{
  割り込み処理の中身、BかCを判別する。
 :
 :
}
のように宣言します。ここではtimerからの割り込みを行ってみます。

以下のプログラムをコピーして、貼り付けて使用してください。

「IntLED」プログラム
// ファイル名 IntLED.c
#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define _XTAL_FREQ 20000000
#define PERIOD 218

/*
20MHz/4 = 5MHz
TMR0 * prescale*counter/5MHZ
= (256-PERIOD) * 256*255/5MHZ 
= (256-218) * 256*255/5MHZ = 0.4961秒
*/

volatile static unsigned char counter=0;
void interrupt Intr(void)
{
  // (256-PERIOD)*256/5MHz = 1.9456ms
  // 約2ms毎に割り込む
  // 割り込みがtimer0の場合
  counter++;
  TMR0 = PERIOD;
  T0IF = 0;                 // 割り込みフラグクリア
  return;
}

main()
{
  PORTA = 0;                // PORTAを0にする
  TRISA = 0;                // PORTAを出力に設定する
  ANSEL = 0;                // アナログ入力をOFF
  
  // Timer0 の設定
  OPTION = 0xd7;            // FOSC/4 プリスケーラ1/256
                            // 20MHz/4/256 = 19.531kHz
  T0IE=1;                   // Timer0 の割り込みON
  T0IF=0;                   // 割り込みフラグクリア
  GIE=1;                    // CPUへの割り込みON
  while(1) {                // 0.5秒経ったら反転させる
    if(counter == 0) {
      counter++;            // ポートが高速on/offするのを防止
      PORTA = ~PORTA;
    }
  }
}


void interrupt Intr(void)
割り込みが起きた場合にこの関数に飛んできます。
この記述はコンパイラの種類によって異なります。

OPTION = 0xd7;
RABPU=1, INTEDG=1, T0SE=0, PSA=0, PS=111(1/256)
タイマーに送られるクロックは20/4/256=19.531kHz と設定しています。他の設定はこの場合関係ありません。下図参照。
optionレジスタの設定
T0IE=1;
T0IF=0;
GIE=1;
Timer0 の割り込みON
割り込みフラグクリア
CPUへの割り込みON
下図参照。

タイマー0からの割り込みはT0IFが0から1になった時に起こります。従って使用する前にT0IFは0にしておかなければいけません。
また、割り込んだ時に次に割り込むようにやはり0にしておかないといけません。
 下図の割り込みロジックのように、T0IF & T0IE & GIE が1になった時に割り込みますから、
T0IEとGIEは1にしておかないといけません。
counter++;
TMR0 = PERIOD;
T0IF = 0;
約2msの割合でカウントするカウンター
2ms毎に割り込ませるための設定値(218)
次に割り込ませるために必要

ここではFoscは20MHzです。従ってFosc/4=5MHzとなります。
このクロックがそのまま8bitのプリスケーラに入って1/256になり19.531kHzのタイマー(TMR0)へのクロックになります。
タイマーが実質カウントするのは218から256までで、その数は256-218=38となります。
従って、38/19.531kHzは約2msとなり、この間隔で割り込みます。
割り込まれた関数では8bitのcounterという変数で割り込み数を数えていますから、
この周期は1.9456ms*256=498msとなります。
ですから、LEDは0.5秒ごとに点灯したり消灯したりします。つまり1秒周期で点滅します。

A/Dコンバーターと割り込み処理

次にADコンバーターからの割り込みもサポートするようにします。
このようにすれば、ADコンバーターの面倒を見ることなしに自分の仕事に没頭できます。
ここで用いる入力としてはAN4,AN5,AN6とします。目的のセンサーはCdSなので上位8bitのみを利用することにする。
また、データを得たら直ぐに次のポートの計測を始めるようにします。このようにすれば連続的に計測できます。

以下のプログラムをコピーして、貼り付けてください。

「A/Dコンバーターと割り込み処理」プログラム
// ファイル名 IntADCdSLED.c
// CdSに手をかざすとそれに対応するLEDが点灯する
// これをタイマー0割り込みを用いて実現する
#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define true  1
#define false 0
#define _XTAL_FREQ 20000000
#define ANS4 0x10
#define ANS5 0x20
#define ANS6 0x40
#define AN4  0x11
#define AN5  0x15
#define AN6  0x19
#define PERIOD 218

/*
20MHz/4 = 5MHz
TMR0 * prescale*counter/5MHZ
= (256-PERIOD) * 256*256/5MHZ 
= (256-218) * 256*256/5MHZ = 0.4981秒
*/
typedef unsigned char byte;
volatile static byte counter=0;
byte LeftCdS, CenterCdS, RightCdS;
byte ADch=0;
byte ConvEndFlag;
byte ADC[3]={AN4,AN5,AN6};

void interrupt Intr(void)
{
  // (256-PERIOD)*256/5MHz = 1.9456ms
  // 約2ms毎に割り込む
  if(T0IF) {                  // 割り込みがtimer0からか?
    counter++;
    TMR0 = PERIOD;
    T0IF = 0;                 // 割り込みフラグクリア
    return;
  }
  if(ADIF) {                  // 割り込みがADコンバーターからか?
    if(ADch==0) LeftCdS = ADRESH;
    else if(ADch==1) CenterCdS = ADRESH;
    else if(ADch==2) RightCdS = ADRESH;
    ConvEndFlag=true;         // データの更新あり
    ADIF = 0;                 // 割り込みフラグクリア
  }
}

main()
{
  unsigned int threshold=32;  // 周囲の明るさによって値を調整する
                              // 明るいときは大きくする
  PORTA = 0;                  // PORTAを0にする
  TRISA = 0;                  // PORTAを出力に設定する
  ANSEL = ANS4 | ANS5 | ANS6; // アナログ入力をAN4-6をON
  ADCON0 = AN4;               // ADFM=0(左詰) ADon
  OPTION = 0xd3;              // FOSC/4 プリスケーラ1/16
  PORTC = 0;                  // PORTCを0にする
  TRISC = 0x0f;               // PORTC下4bit入力、上4bit出力
  
  // Timer0 の設定
  OPTION = 0xd7;              // FOSC/4 プリスケーラ1/256
  T0IE=1;                     // Timer0 の割り込みON
  T0IF=0;                     // 割り込みフラグクリア
 
  // ADコンバーターの設定
  ADIE = 1;                   // ADコンバータの割り込み許可
  ADIF = 0;                   // 割り込みフラグクリア
  PEIE = 1;                   // 周辺装置の割り込み許可
  GIE  = 1;                   // CPUへの割り込み許可
  GODONE = 1;                 // AD変換スタート
  
  // 永久ループ
  while(true) {  
    if(ConvEndFlag) { // AD変換が終わったらすぐに次のチャンネルの変換を行う
      ConvEndFlag = false;
      if(++ADch==3) ADch=0;
      ADCON0 = ADC[ADch];     // ADチャンネル変更
      GODONE = 1;             // AD変換スタート
    }
    
    PORTA=0;
    if(RightCdS<threshold)  RA2=1;  // 赤
    if(CenterCdS<threshold) RA1=1;  // 緑
    if(LeftCdS<threshold)   RA0=1;  // 青
  }
}

unsigned int threshold=160;
暗くなったかどうかを判断する数値。これより小さくなったら暗くなったことになるが、絶対値を用いているので、環境の明るさによって左右される。

ADIE = 1;
ADIF = 0;
PEIE = 1;
GIE = 1;
GODONE = 1;
ADコンバータの割り込み許可
割り込みフラグクリア
周辺装置の割り込み許可
CPUへの割り込み許可
AD変換スタート
割り込みロジック
ADコンバータの場合は割り込みをする条件としてさらにPEIEが1でなければいけません。
割り込みはADコンバーターの変換終了時に起こりますから、GODONEを1にてスタートさせないと割り込みません。
割り込むとADIFが1となり、次の部分が実行されます。
if(ADch==0) LeftCdS = ADRESH;
else if(ADch==1) CenterCdS = ADRESH;
else if(ADch==2) RightCdS = ADRESH;
ConvEndFlag=true;
ADIF = 0;
ADchはmainプログラムの方で変換終了毎に0,1,2,0,1,2と繰り返すようになっています。
変換値は10bit精度で左詰です。ここでは細かい精度は必要としないので上位8bitのADRESHのみを利用します。プログラムコードの縮小及び高速化も関係あります。
ADchの値に応じて、LeftCdS,CenterCdS,RightCdSというグローバル変数に代入されます
1回の変換終了をmainに知らせるためにConvEndFlagをtrueにします。
次のAD変換割り込みをさせるためにADIFを0にします。

A/Dコンバーターと割り込み処理2

上記のプログラムは閾値を固定したので、周囲の明るさによって動作が異なってしまいます。
これを打開するために電源投入直後に周囲の明るさを計測し、その平均の明るさより暗くなったら 手がかざされたと判断するように変更します。
また、色々な設定でmainプログラムがゴチャゴチャしてきたので、設定の部分をinitialize()という 関数にまとめてすっきりさせます。

以下のプログラムをコピーして、貼り付けてください。

「A/Dコンバーターと割り込み処理2」プログラム
// ファイル名 IntADCdSLED2.c
// CdSに手をかざすとそれに対応するLEDが点灯する
// これをタイマー0割り込みを用いて実現する
// 関数にして機能分担化させる
// 自動閾値設定
#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

// マクロ宣言
#define true  1
#define false 0
#define _XTAL_FREQ 20000000
#define ANS4 0x10
#define ANS5 0x20
#define ANS6 0x40
#define AN4  0x11
#define AN5  0x15
#define AN6  0x19
#define PERIOD 218

/*
20MHz/4 = 5MHz
TMR0 * prescale*counter/5MHZ
= (256-PERIOD) * 256*256/5MHZ 
= (256-218) * 256*256/5MHZ = 0.4981秒
*/
typedef unsigned char byte;

// プロトタイプ宣言
void initialize();
byte checkCdS();
void setThreshold(byte th);

// グローバル変数の宣言
volatile static byte counter=0;
byte LeftCdS, CenterCdS, RightCdS;
byte ADch=0;
byte ConvEndFlag;
byte ADC[3]={AN4,AN5,AN6};
byte threshold[4];

// 割り込み処理ルーチン
void interrupt Intr(void)
{
  // 約2ms毎に割り込む
  if(T0IF) {       // 割り込みがtimer0からか?
    counter++;
    TMR0 = PERIOD;
    T0IF = 0;                 // 割り込みフラグクリア
    return;
  }
  if(ADIF) {      // 割り込みがADコンバーターからか?
    if(ADch==0) LeftCdS = ADRESH;
    else if(ADch==1) CenterCdS = ADRESH;
    else if(ADch==2) RightCdS = ADRESH;
    ConvEndFlag=true;         // データの更新あり
    ADIF = 0;                 // 割り込みフラグクリア
  }
}
  
main()
{
  initialize();               // 各種初期設定
  // 永久ループ
  while(true) {  
    checkCdS();               // CdSの状態をチェックする
  }  
}

void initialize()
{
  PORTA = 0;                  // PORTAを0にする
  TRISA = 0;                  // PORTAを出力に設定する
  ANSEL = ANS4 | ANS5 | ANS6; // アナログ入力をAN4-6をON
  ADCON0 = AN4;               // ADFM=1(右詰) ADon
  PORTC = 0;                  // PORTCを0にする
  TRISC = 0x0f;               // PORTC下4bit入力、上4bit出力
  
  // Timer0 の設定
  OPTION = 0xd7;              // FOSC/4 プリスケーラ1/256
  T0IE=1;                     // Timer0 の割り込みON
  T0IF=0;                     // 割り込みフラグクリア
 
  setThreshold(65);           // 平均の明るさの65%の位置に閾値を設定する
  
  // ADコンバーターの設定
  ADIE = 1;                   // ADコンバータの割り込み許可
  ADIF = 0;                   // 割り込みフラグクリア
  PEIE = 1;                   // 周辺装置の割り込み許可
  GIE  = 1;                   // CPUへの割り込み許可
  GODONE = 1;                 // AD変換スタート
}

byte checkCdS()
{
  byte status;

  if(ConvEndFlag) { // AD変換が終わったらすぐに次のチャンネルの変換を行う
    ConvEndFlag = false;
    if(++ADch==3) ADch=0;
      ADCON0 = ADC[ADch];     // ADチャンネル変更
      GODONE = 1;             // AD変換スタート
  }
    
  PORTA=0;
  if(RightCdS<threshold[2])  RA2=1;  // 赤
  if(CenterCdS<threshold[1]) RA1=1;  // 緑
  if(LeftCdS<threshold[0])   RA0=1;  // 青
  
  status = RightCdS<threshold[2];
  status += (CenterCdS<threshold[1])?2:0;
  status += (LeftCdS<threshold[0])?4:0;
  return status;
}

void setThreshold(byte th)    // 平均の指定%を閾値にする
{
  byte i, j, x, g;
  int average[3]={0,0,0};
  g = GIE;                    // 割り込み状態の保存
  GIE=0;                      // 割り込みは使わない
 // 明るいときの値 明るいと大きい値
  ADC[0]=AN4;
  ADC[1]=AN5;
  ADC[2]=AN6;
  for(j=0; j<40; j++) {       // 平均の明るさを求める
    for(i=0; i<3; i++) {
      ADCON0 = ADC[i];
      GODONE = 1;
      while(GODONE);
      x = ADRESH;
      average[i] = (average[i]*9+x)/10;
    }
  }
  for(i=0; i<3; i++)
    threshold[i]=average[i]*th/100;
  GIE = g;                    // CPUへの割り込み状態を戻す
}
void setThreshold(byte th)    // 平均の指定%を閾値にする
の中の計算について。
average[i] = (average[i]*9+x)/10;
これは、 average[i] = average[i]*0.9+0.1*x;を整数型で行っています。
完全な平均を計算しているのではなく、過去の最近の値を早い変動分を取り除いて平均らしい値を求めています。パソコンで平均を計算するのならば、データをメモリに入れて後で計算するか、データを全部加算して、その後データ個数で割れば平均は求まりますが、PICのようにメモリに限界がある場合、メモリは多く使えない、全部加算すると変数がオーバーフロー起こすことがあり得ます。従って、それらしい平均を求めるにはこの方法が有効です。

目次へ戻る