【15】割り込み処理あれこれ・・・
前回は割り込みの基本的な説明をしました。今回は割り込み処理をどう使うのか、もう少し具体的な内容に進めます。
PICにはたくさんの割り込みが存在します。例えば、前回出て来たタイマー0がオーバーフローしたら割り込み開始とか、RB0のポートに信号が入ったとき割り込み開始とかありますが、他にも・・・。
・タイマー1がオーバフローしたら割り込み開始
・タイマー2がオーバフローしたら・・・
・RB4~RB7が変化したら・・・
・A/D変換が終了したら・・・
・EEPROMへデータを書き込み終了したら・・・
・シリアル通信の送受信が完了したとき・・・
まだまだありますが、これはらはみんな割り込み開始のトリガー(きっかけ)を何にするかだけのことです。また、どの割り込みが発生したのかは、PICの中に点在する割り込み要因フラグを調べるとわかります。問題は〝何をさせるか〟のほうだと思います。
割り込み処理の流れを客観的に眺めて見るとこんな感じです。
PICは割り込みをトリガーすると、処理中のプログラムを中断してアドレス0004番地へ飛ぶ。そして割り込み処理を終了させるときは〝 retfie (リターン フロム インタラプト)〟という命令を使って元の処理へ戻させる。
割り込み処理の基本的な流れはこんな簡単なことです。
▽ 外部割り込み と タイマー割り込み の違い △
割り込みを大きく分けていくと、結局、外部割り込みとタイマー割り込みになります。どういうものかは前回説明していますが、このふたつ、どこが大きく異なるのかというと、それは反応速度です。
外部割り込みは入力端子にトリガーが掛けられたときに発生するものですから、ハードウエアと密接に関係してきます。反応速度は数百nS(nS=10億分の一秒)でも反応します。コレぐらいの速度になるとノイズと信号との識別がし難くなりますので、しっかりとしたハードウエアが必要になってきます。そうでないとノイズに反応してしまい、予期しない動きになります。
反応速度の限界値をカタログやインターネットで調べましたが見当たりませんでした。次回機会があれば実験してみます。いままでの経験では500nSでもじゅうぶん反応していましたが、ノイズとの兼ね合いで、私は1μSほどを最速値としています。
対して、タイマー割り込みは周期的に割り込みをトリガーさせるのが目的です。そのため周期の最短は50~100μS以上にしなければ、割り込みが連続してしまい、肝心の処理が進まなくなります。そのような理由から私の場合はもっと遅く、1~2mSほどにしています。
先ほどから〝mS〟とか〝μS〟や〝nS〟とか出てきますが、これらはミリセック、マイクロセック、ナノセックと読みます。昔はミリセカンドとかでしたが、最近はセックと読むそうです。1mSは千分の一秒で、1μSは百万分の一秒、1nSは10億分の一秒を表します。PICなどの回路設計をするときやプログラム処理の速度を重視する場合などに頻繁に出てくる時間の単位ですので覚えておくと便利です。
これらの単位は一般生活の中では使われませんので、まったくピンとこないと思います。そこで非常にアバウトな実測値ですが、これらの単位がどれほどの時間のモノか実感してみてください(だいたいこれぐらいです)
○時計の1秒は1000mSです。
○途中で止まらないほどに傾けたレールに硬貨を転がしたとき、ある一点をフォトセンサーで検知した場合、角度にもよりますが30mS~100mS(0.03秒~0.1秒)です。
○人間が押しボタンスイッチを最も速く押そうと意識してON、OFFさせたときのON時間が約20~50mSで、さらに強く意識して指で弾くように叩くと、たまに10mS以下の短いON期間が計測されます。
○垂直落下する硬貨をマイクロスイッチで検知した場合、約30~50mSです。
○垂直落下する硬貨をフォトセンサーで検知した場合、約10~20mSです。
○スイッチをONにした時に発生するチャタリング(接点のバウンド現象)期間が、2~10mSです。
○スイッチをONからOFFにした時に発生するチャタリング(OFF時にもチャタリングが出ます)期間が、500μS~2mSです。
人間が機械を使わず出せる速度なんてこんなものです。μSやnSなどの時間枠はコンピュータの中だけのものです。ちなみにメモリアクセスの世界ではpS(ピコセック=1兆分の1秒)というのが出てきます。
10MHzのクロックでPICを走らせると、ひとつの命令を処理するのに0.4μSの時間が掛かります。人間が押しボタンやレバーをON、OFFしたときにONを維持する時間は、先ほど例を出しましたが速くても50mSです。いいかえれば、50mSの期間、ON信号が続くといえます。それだけあればPICは、50000μS÷0.4=12万5000個の命令をこなしています。(50mS=50,000μS)
ゲーム機並みの感度でボタン検知をさせる場合でも1~2mSでじゅうぶん対応できます。2mSだと5000個の命令をこなしています。いかにPICの速度がすごいかというのがわかります。ミッドレンジPICを最大速度の20MHzのクロックで走らせると、さらにこの倍の命令をこなすことになります。
もちろん外部割り込みでボタン検知をしても何の問題はありませんが、タイマー割り込みは他にも利用価値があります。周期割り込みということは、プログラマーが意識しなくても周期的に処理をやってくれるということになります。これだけでメイン処理が非常に楽になります。メイン処理がいくら込み入った処理をしていようとも、時間が来れば割り込みが掛かり、処理が自動的に切り替わってくれるからです。
タイマー割り込みでよく行われる処理例を書くと・・・。
①ボタン/スイッチ類のダイナミック スキャン処理
②7セグメントLED表示器のダイナミック 点灯処理
③多チャンネルに接続したLEDの輝度コントロール
④多チャンネルに接続したフルカラーLEDのコントロール
⑤メイン処理で使用するタイミング フラグの更新
タイマー割り込み処理がリアルタイムプログラムに最適な理由は、メイン処理とは別にこれらの処理が同時進行で可能になることです。
①ボタン/スイッチ類のダイナミックスキャンというのは、ボタンやスイッチを直接ポートに接続すると、ひとつのポートに8個までしか接続できません。そこでボタン類をグル-プに分け、グループ別にコモン信号を切り替えて、今現在どのボタンが押されているか検知する方法です。この方法だと、ひとつのグループに8個のボタンを接続して、4つのグループに分けると、ポートの消費は8と4で12ポートですが、接続できるボタンの数は8×4で32個にもなります。
回路は次のようになります。
RB0~RB3を出力ポートに設定したグループ切り換えポートとして、RC0~RC7を入力ポートにします。
まずRB0を最初のグループとしてLにします。そして他のグループRB1~3をHにして、RC0~RC7の入力状況を調べます。何も押されていないときは、プルアップ抵抗4.7KΩの働きで、RC0~RC7はすべてH(=0xFF)ですが、図のように、S8とS4を押すと、RC0とRC4だけがRB0に接続されますので、Lになります。結果、RC0~RC7は"HHHL_HHHL"=0xEEに変化します。
もし別グループのボタンを押してしまったとしても、コモン出力は H なので何も変化がありません。それと各ボタンにダイオードが入っていますが、これはHになっている他のグループの出力が押されたボタンを通して、Lになっているグループのポートへ流れ込むのを防いでいます。流れるとショートしてPICのポートを傷めることになります。
このグループの切り替えを1mS周期のタイマー割り込みで行い、その時のボタンの状況をグループに対応した4つのユーザーメモリのひとつに書き込みます。メイン処理はそのユーザーメモリを見てどのボタンが押されているかを判断して処理を進めるということになります。要するに割り込み処理がボタン状況を1mSおきに読み取って、ユーザーメモリに更新しているわけですが、メイン処理から見たら単にメモリの中を覗けば、ボタンの状況が分かることになり非常に処理が楽になります。
1グループ8個、4グループで計32個のボタンの情報が更新されるのに4mS掛かることになりますが、人間はそれ以上速くボタンを押すことができませんので、じゅうぶんな結果になります。
出力に対してもダイナミックスキャンと同じような回路を作ると少ないポートでたくさんの出力が出せるようになります。それが ②7セグメントLEDのダイナミック点灯です。原理はダイナミックスキャンとまったく同じです。7セグメントLEDのセグメント側の8本をポートに接続して、コモンをグループ別のポートに接続します。4ポートを利用すれば、12ポートで4桁の7セグメントLEDが接続できます。
7セグメント(以下7セグと書きます)表示器は小数点のLED(Dp)を含めて8個のLEDがひとつのパッケージに入っている部品ですので、結果的には32個のLEDをコントロールしていることになります。
上の回路ではRB0~RB3を各桁のコモン出力として割り当て、RC0~RC7を出力ポートにして7セグの"a"~"Dp"の各セグメントに対応させています。例えば上の場合は、RB0をLに他をHにしていますので、左端のトランジスタのベースだけがLになり、そのトランジスタがONになって5Vが7セグのコモンへ流れ込んできます。セグメントデータを"HHHH_HLLH"としてRC0~RC7に出力しています。これはセグメントの"b"と"c"だけをLにしますので、そのラインだけに電流が流れてLEDが点灯します。その結果、数字の"1"を表示することになります。
ボタンスキャンのときと同じように、割り込み処理でコモンを切り替えて、そのコモンに対応したユーザメモリを順に読み込んで、その内容をセグメントデータとして、RC0~RC7へ出力する処理を繰り返します。
メイン処理から見れば、表示したい数値を7セグメントLED用のデータに変換して、各桁のユーザーメモリへ書き込んでおくだけで、あとは割り込み処理が順に点灯させてくれます。
ただ、ボタンのスキャン処理と違って、ダイナミック点灯はすこしシビアです。それは人間の眼の残像現象を利用して全部のLEDに数値が表示されているように見せていますが、瞬間的には4個のうちどれかひとつだけしか点灯していません。なので点灯の切り替え速度が遅くなるとチラチラしてきます。逆に速くなると暗くなってきます。ひとつのLEDが放つ光がしっかり人間の眼に焼きつく前に次の桁へ移動していまうからです。私の経験では、ひと桁2mSで、8桁までが限界だと思います。この場合、ひと桁だけを見てみますと、16mSおきに2mSの期間点灯することになります。これ以上遅くなるとチラつき出します。逆にひと桁を1mSにして、8mSで1周するようにすると、チラつきは完全に無くなりますが、すこし暗くなります。
また、コモンの切り換え時は一瞬でもすべてのLEDが消えている時間を与えてやるほうがメリハリが付いて綺麗に点灯します。次のコモンにしてから現在のコモンを消灯させると、一瞬ですが、2個の7セグが点灯することになり、ゴーストが出て見にくくなります。またベースと電源を繋いでいる4.7KΩを大きくするとトランジスタがOFFになるのに時間が掛かるようになり、やはりボケた感じになります。
次は ③多チャンネルに接続したLEDの輝度コントロール です。
通常LEDは光るか消えるかの二通りでしか使用しませんが、暗く光らせたり、消えている状態から〝ふぁ~〟と点灯したり、アナログ的なコントロールをさせようというものです。
単純に1個のLEDをアナログ的に光らせるのなら、PICに内蔵されているPWM機能を使えば割り込みを使用しなくてもこと足ります。
しかし、メイン処理でたくさんの仕事をしながら、8個とか16個のLEDに対して行うとなるとたいへんなことになってきます。このようなときにタイマー割り込みを使ったPWMを作れば可能になります。
割り込み処理内で行うPWMですが、LEDの輝度コントロール程度でしたらそれほど難しくありません。まず、PWMの説明ですが・・・。
PWMは〝 Pulse Width Modulation 〟の略でパルスのHとLの幅を可変させて擬似的にアナログ出力する方式です。例えば、ある周期(=PERIOD)を決めておいて、Hの幅をその周期の50%(半分)にして残りの50%をLにするという出力を繰り返し出した場合と、Hの幅を100%、Lの幅を0%、ようするにHにしたままで出力した場合では、前者のほうが平均電圧が半分になり、LEDなら暗く、モーターなら回転が遅くなる、という方法で擬似的にアナログのようにコントロールする方式です。
上の回路でRC7をLにするとLED1が点灯しますが、ある周期(=PERIOD)で、点灯時間を短くすると暗く、長くすると明るくなります。
この回路では L にすると点灯するタイプのPWMになります。
他の処理と平行してリアルタイムにコレを実現するには割り込み処理を利用するのが妥当です。
割り込み周期とPWMの周期の関係ですが、最低輝度(消灯)から最大輝度までの段階数がPWMの周期になります。そしてその段階を切り替える時間が割り込み周期になります。よって 段階×割り込み周期 が全体時間になり、あまり長いとダイナミック点灯のようにチラつきだします。これがモーターだと振動になります。
逆に速いほど輝度の段階数を多くできますが、頻繁に割り込みが掛かることになり、メイン処理の割り当て時間が減っていきますので、私は割り込み周期を1~2mS、輝度は16段階ぐらいが適度だと思っています。あまり細かくしても輝度の差が人間には判断できないからです。ここでは輝度16段階、割り込み周期 1mS にしてみました。
8個のLEDをコントロールするとしたら、それに対応した輝度情報を保存するメモリーとして、8個の連続したユーザーメモリーを確保します。このような同じ目的のために順に並んだメモリーを〝ルックアップテーブル〟あるいは、略して〝テーブル〟などと呼びます。
割り込み処理内では、割り込みが発生するたびにこのテーブルを読み込んでそれに応じた出力をするようにします。こうすると、メイン処理はテーブル内のデータを変更するだけで済み、そのあたりのことを一切考えなくていいことになり非常に楽になります。
16段階にした場合、8個の輝度テーブルは 0 から 0x0F までの数値を書き込むことになります。割り込み処理側では割り込みごとに0~15を数えるカウンター(PERIOD COUNTER)を拵えます。そして割り込みごとに現在のカウンターの数値と輝度テーブルの内容を順に8個比較します。その結果、カウンターの数値よりテーブルの内容のほうが等しいか小さければテーブルに対応するLEDをOFFにします。テーブルの内容が大きい場合はONにします。
単純に分解して考えてみますと、16mSという周期で8個のLEDの点灯時間を、輝度テーブルに書き込まれたデータに沿って変化させているだけのことですが、人間の目では識別できる速度ではありませんので、擬似的なアナログ点灯が可能になります。
④多チャンネルに接続したフルカラーLEDのコントロール も、この延長として考えられます。フルカラーLEDは、赤・緑・青の三色のLEDがひとつのパッケージに押し込まれているだけです。輝度コントロールのテーブルを三色分に拡張するだけです。例えばひとつのLEDに対応した赤のテーブルの数値を 0x0F に、緑を 0x08 に、青を 0x00 と書き込めば、そのLEDは黄色より少し暗いオレンジ色に光ります。
実際にPIC16F877A、クロック10MHzで、2チャンネルのアナログ信号をA/D変換して、32個のフルカラーLEDを音楽に合わせて光らせるという実験を成功させています。
消えている状態から〝ふぁ~〟とアナログ的にゆっくりと明るくさせるには、メイン処理で任意の輝度テーブルの数値を50mSほどの周期で、0 から 0x0F まで順に変化させると 50×16=800mS(0.8秒)掛かって〝ふぁ~〟と点灯するはずです。
ここでいう50mSというのは変化していく速度です。長くすればゆっくりと変化し、短くすれば速く変化します。しかし極端に速くするとアナログ的な変化ではなくなりますのであまり意味がありません。さらにPWMの周期は16mSですので、これ以下の速度で輝度テーブルを急速に変化させると割り込み処理が追いつかなくなって、おかしな光り方になる場合があります。そのようなときは、割り込み処理内で使用する PERIOD COUNTER をリセットする、あるいは輝度テーブルを変更するときは割り込みを停止させるなどの工夫が必要です。
ところで、メイン処理では他の処理も平行して走っています。これらに影響が出ないように50mSという時間を拵えるにはどうしたらいいのでしょうか。これには割り込み処理と同期を取って対処する方法があります。
⑤メイン処理で使用するタイミング フラグの更新 を利用すると簡単に割り込み処理と同期を取ることができます。これを利用するとメイン処理が非常に簡単になります。
本などで割り込み処理を説明するプログラムを見ると下記のような記序がよくあります。
|
このプログラムはリセットから立ち上がると、初期化を通って割り込みの設定などを経て、割り込み開始にしたあとメイン処理は無限ループに入っています。
割り込み処理の説明ですので間違ってはいませんが、実際に何かを作るとき、メイン処理を何も行わない無限ループにすることはほとんどありません。
メイン処理の中でも何らかの処理が繰り返し行われているはずです。
私の場合、全体の処理はだいたいこんな感じに記序します。
|
メイン処理の中にループを作ってたくさんの処理を並べて繰り返します。ただ、処理のどこかで無駄なループに入り処理が止まると、その瞬間、リアルタイムなプログラムではなくなってしまいますので、【13】リアルタイムなプログラム 1(ループ)で書きましたが、
その中の方法② 決められた時間が来るたびに、仕事を切り替える をここで利用ます。
短い時間で切り替えて仕事を割り振る方法をタイムシェアリングといいますが、この切り換えのトリガーとしてこの1mSの割り込みを利用します。
切り換えのタイミングを知るために、ユーザメモリに FLG という名前のメモリを確保して、これを割り込み発生済みの同期フラグとして利用します。
割り込み処理内で割り込みが掛かるたびにこれをHにセットします。H になれば 割り込みが発生したという状態がメイン処理に知らされるわけです。これで割り込み処理と同期が取れることになります。メインではコレを利用したということで L に戻します。すると次の割り込みで再び H になって返ってくるということになります。
割り込み処理ではこのフラグの内容をHにするだけですので、単純に FLG に 0xFF を書き込んで8個分のフラグをいちどに更新してもよいわけです。これだけの簡単な処理で8個分の同期フラグができます。たったの2命令ですので、7セグのダイナミック点灯処理などの片隅に記序してもかまいません。
この同期フラグをどのように利用するかというと・・・。
上記のプログラムをもう一度ご覧ください。各処理は決められた時間が来るまでは何もせずに処理の流れをメインへ戻します。決められた時間が来たら、なるべく短時間で処理を次に進めてメインに戻すという流れを繰り返すような仕組みになっています。CALL命令が並んで無駄のように見えますが、処理全体が、各処理へ入るか、メインへ戻るかの二通りの流れになり、見通しのよいプログラムになりますので私はこの方法を多用しています。
決められた時間が来たか判断する部分は至って簡単、たとえばメイン処理1の先頭の部分をご覧ください。
|
サブルーチンの先頭でFLGというメモリのbit0がHになるのを待って、Hになれば、それをLに戻してからCNT1というメモリを-1しています。そしてCNT1が0になれば、0x64(10進で100)をCNT1に書き込んでから処理に入っています。それ以外はすべてメイン処理に戻って(return)います。
FLG の bit0 を H にしているのは割り込み処理内で FLG に 0xFF を書き込んだときだけです。割り込みは1mS周期ですので、メイン処理でLにすればそれ以降、次の割り込みが発生するまではこの処理に進入することはありません。そして1mS後に割り込み処理で H に変更されます。
ということでメイン処理が割り込みを待っているあいだはほとんど何もされることもなく空でメインループを廻っていることになります。そして、フラグが H になったときだけサブルーチンに進入して来ますが、もうひとつの CNT1 というメモリの内容が0になるまでは再び何もしません。0になってやっとサブルーチンが動きます。このときに CNT1 を 100 という数値にしますので、1mSが100回=100mS周期でこのサブルーチンが動くことになります。
メイン処理2では CNT2 を使って、0を書き込んでいます。CNT2 はバイトサイズですので、0から1ずつ減算して256回目で再び 0 になります。処理2は256mS周期で処理が進むことになります。
50mSという時間が必要なら、CNT1 を 0x32 (10進で50)にすればよいだけです。
先ほどの輝度コントロールをメイン処理1(JOB1)で行ったとしたら・・・
|
輝度を変更している部分は赤字の部分です。これで他の処理に影響なく50mSおきに TBL+1 の内容が 0x0F まで変化します。0x0Fを越えると最大輝度に達した処理に分岐しますので、もういちど 0x0F に戻しています。これは、再び50mS後に "輝度を上げる" に入ってきたときに再び "最大輝度に達した" に飛ばして、0x0Fを維持させる対処方法です。実際は必要に応じてここに最大輝度になったあとの処理の初期化を記序します。
タイミングを取る同期フラグは8個まで使えますので、8つのサブルーチンのコントロールができることになります。各処理内で使用されている CNT1 や CNT2 に書き込む数値を変更するだけで、1~256mSの周期でいろいろな処理が可能になります。さらに割り込みごとに FLG を H にするのではなく、2回に1回 H にするようにプログラムを変更すると、2mS周期で H になりますので、メイン処理のスピードを簡単に下げることができます。
1mSという割り込み周期は人間を相手するのでしたらじゅうぶんな速度ですが、機械相手になると問題が発生します。そこで周期を速くすることになるのですが、割り込み周期をあまり速くすると別の問題が発生します。
例えば周期をぐんと上げて50μSにしたとします。50μSだと、50÷0.4μS=約125個の命令が割り込みと割り込みのあいだで処理できます。このとき忘れがちなのが、この数は割り込み処理の先頭(0004番地)から〝 retfie (リターン フロム インタラプト)〟まで、割り込み処理内の命令も含めているということです。
メイン処理がその個数内でも、割り込み処理が複雑でそれを越えているという場合もあります。そうなるとメイン処理がまったく進まなくなります。その理由は、割り込み処理から戻った時にもう次の割り込み周期がやって来ており、再び割り込み処理へ飛ばされます。この繰り返しになりますのでメイン処理が進まなくなります。
プログラム内で使用していた、処理進入コントロール用の CNT1, CNT2 というカウンターですが、通常はメイン処理の初期化部分で必要な初期値に予め設定しておくものですが、今回のようなLED輝度コントロールの場合、初期値不定のままプログラムをスタートさせましても、最大で256mSのズレが出ますが、それは最初の一度だけで、各処理内で固定値が上書きされて、2回目以降は正しい動作に戻りますので今回は省略しています。
また、カウンターの設定値をプログラム内で直接記序していますが、実際は擬似命令(equ など)を利用して、一括変換したほうが効率的です。
CNT1_INI equ 0x32
CNT2_INI equ 0x00
と、ソースファイルの先頭あたりにまとめて書いておき、プログラム内では・・・。
movlw CNT1_INI
movwf CNT1
などと記序します。
もし、プログラムの何ヶ所かで書き込んでいても、先頭の
CNT1_INI equ 0x32
だけを変更するだけで、すべての変更が自動的に行われます。
_______________________________________________________
次回は "ウォッチドッグタイマー" についてです。
2011.01.10
Copyright(C) 2004. D-Space Keyoss. All rights reserved