ブレッドボードラジオAVRマイコン入門

BASCOM-AVR 外部割り込み(その1)

 AVRマイコンの外部割り込み(Int0割り込み)について実験しました。マイコンICの「INT0」という端子に押しボタンスイッチをつないでいろいろな動作をさせます。正直なところ、前にやった入力スイッチのプログラムとどう違うのかまだ十分に理解できていませんが、とりあえず「こんなプログラムを書いたらこう動いた」という具体例を並べていくことにします。

1. テスト用マイコン回路

 プログラムのテスト用の回路は下記の通りです。今回はマイコンICにATtiny2313を使いました。今までのATtiny26Lでもかまわないのですが、後でやる予定のタイマー割り込みの実験に必要なこともあってひとつ購入したので、試しに使ってみました。ATtiny2313は比較的新しいICということもあって、AVRライターやソフトの種類によってはプログラムの書き込みがうまくいかないことがあるそうです。そのときは旧品種でピン接続が同じAT90S2313を使えばいいと思います。ただしクロック用の発振子が必要です。

 ATtiny2313はPD2(6番ピン)が「INT0」端子です。こことGND間に押しボタンスイッチをつなぎます。これが外部割り込み用のトリガスイッチになります。それとリセット端子(1番ピン)にもスイッチを付けました。PB0とPB1には出力用のLEDを接続します。

 第1図

実体図 写真

2. スイッチを押すとLEDが点灯する

 外部割り込み用のスイッチS0をチョンと押すとLED0が点灯するプログラムです。スイッチを放してもそのまま点灯し続けます。消灯するときはリセットスイッチS1を押します。BASCOM-AVRがインストールされたパソコンで下記リンク「int1a.bas」をクリックしてファイルをダウンロードすると、BASCOM-AVR IDEが自動的に立ち上がって画面内にプログラムが表示されます。

 プログラムファイル int1a.bas

1 $regfile = "attiny2313.dat" ATtiny2313を使用する。
2 $crystal = 1000000 クロック周波数を1MHzに設定。
3
4 Config Portb = Output ポートBを出力に設定する。
5 Config Portd = Input ポートDを入力に設定する。
6 Portd = 127 PD0〜PD6をプルアップする。
7
8 On Int0 Int0_int 割込みが起きたら「Int0_int」を実行する。
9 Enable Interrupts 割込み全般を許可する。
10 Enable Int0 Int0割込みを許可する。
11
12 Do Doループの範囲ここから。
13  Toggle Portb.1 PB1の出力を反転する。
14  Wait 1 1秒間そのまま。
15 Loop Doループの範囲ここまで。12行目へ戻る。
16
17 End メインプログラム終わり。
18
19 Int0_int: 割込みプログラム「Int0_int」ここから。
20  Portb.0 = 1 PB0の出力を「1」にする。LED0が点灯。
21 Return 割込みプログラム「Int0_int」ここまで。

 別に外部割り込み機能を使わずとも同じ動作をさせることは可能ですが、外部割り込みプログラムの書き方を勉強するために最初は簡単な例でやってみました。1行目「$regfile = "attiny2313.dat"」はマイコンICにATtiny2313を使うときの記述です。AT90S2313を使うときは「$regfile = "2313def.dat"」と書きます。プログラムの構成は、各端子と外部割り込みの設定、メインプログラム、外部割り込みプログラムの順になっています。

 ICのINT0端子すなわちPD2に入力スイッチをつなぐので、ポートDを入力に設定します(5行目「Config Portd = Input」)。そしてポートD全体をプルアップします。ATtiny2313のポートDは0〜6までしかないので、6行目「Portd = 127」と書けばすべてプルアップされます。外部割り込み自体はポートが入力であっても出力であっても関係なく発生します。しかしスイッチをつなぐときは入力に設定してプルアップしないと正しく動作しません。

 8行目「On Int0 Int0_int」はInt0割り込み(外部割込み0)が発生したら割り込みプログラム「Int0_int」を実行するという意味です。割り込みが起きたとき何をさせるかはサブルーチンとして記述します。「Int0_int」というのは私が付けた名前です。予約語以外なら自分の好きな名前が付けられます。9行目「Enable Interrupts」はすべての割り込みを許可する命令、10行目「Enable Int0」はInt0割り込みを許可する命令です。割り込み機能を使うときは、割り込みが起きる前にこれらの記述で割り込みを許可しておかなければなりません。8〜10行目は順番が入れ違ってもOKです。ATtiny2313にはInt0の他にInt1(外部割込み1)というのもありますので、両者を区別する必要があります。

 割り込み機能を使うためには割り込まれる側のメインプログラムが必要です。メインプログラムは必ず無限ループ(Do〜Loop, Main:〜Goto Mainなど)の形になっていなければなりません。今回の例はあまりに単純すぎてメインプログラムでやることが何もありませんが、これがないと割り込みが起きる間もなくあっという間にプログラムが終了してしまうので、PB1にLEDをつないで1秒間隔で点滅させることにしました。本来は、割り込みによってメインプログラムの動作が変化するような使い方をします。

 19行目「Int0_int:」から21行目「Return」までが割り込みプログラムです。スイッチS0が押されるたびにInt0割り込みが発生し、このサブルーチンが実行されます。今回の例では「 Portb.0 = 1」とあるので、PB0の出力が「1」になり、LED0が点灯します。なお、今回は割り込みプログラムを「End」の後に書きましたが、順序を逆にしても動作は同じでした。

 ところで、通常の入力スイッチプログラムで同じ動作をさせるとするなら、メインプログラム内に「If Pind.2 = 0 Then Portb.0 =1」と書くと思います。しかしこの場合、メインプログラム内に「Wait 1」がありますので、スイッチを押してからLED0が点灯するまで最大1秒間待たされる可能性があります。一方外部割込みは、メインプログラムの進行状況に関係なく、スイッチが押されれば瞬時に発生します。仮に「Wait 1」の実行中であっても割り込みが生じ、すぐにLED0が点灯します。これは外部割込みの利点と言えるでしょう。

 上記のプログラムではスイッチを押している間じゅう、割り込みが連続して発生します。LED0は最初にスイッチが押されたときに点灯してあとはそのままなので、スイッチを長押ししても何も変化がありませんが、割り込みプログラムが実行されている間はメインプログラムの処理が止まるので、スイッチを押し続けるとLED1の点滅が非常にゆっくりになります。最初14行目が「Wait 1」だったので、LED1の点滅が完全に停止するのかと思ったのですが、ここを「Waitms 100」に変えてスイッチを長押ししてみたところ、約6秒ごとに点灯、消灯を繰り返しました。60分の1のスピードです。

 「Int0_int:」内を「Portb.0 = 1 : Wait 2 : Portb.0 = 0」と書くと、割り込みスイッチが押されるとLED0が点灯し2秒後に消灯する、という動作になります。LED0が点灯している間、LED1の点滅はストップしています。LED0が消灯するとLED1は点滅を再開します。スイッチを2秒間以上押し続けると、押されている間ずっとLED0は点灯しています。割り込みスイッチをチョンと押してから放し、LED0が点灯しているときにリセットスイッチを押す(押して放す)とLED0は消灯します。実はリセット動作というのも一種の割り込みで、しかも外部割込みよりも動作が優先されます。したがって外部割込みプログラムの実行中でもリセットが利きます。

3. スイッチを押している間だけLEDが点滅する

 スイッチを押している間だけLED0が0.5秒周期で点滅し、スイッチを放すと放した瞬間の状態のまま点滅が停止するプログラムです。先のプログラムと違うのは「Int0_int:」内のプログラムだけです。

 プログラムファイル int1b.bas

1 $regfile = "attiny2313.dat" ATtiny2313を使用する。
2 $crystal = 1000000 クロック周波数を1MHzに設定。
3
4 Config Portb = Output ポートBを出力に設定する。
5 Config Portd = Input ポートDを入力に設定する。
6 Portd = 127 PD0〜PD6をプルアップする。
7
8 On Int0 Int0_int 割込みが起きたら「Int0_int」を実行する。
9 Enable Interrupts 割込み全般を許可する。
10 Enable Int0 Int0割込みを許可する。
11
12 Do Doループの範囲ここから。
13  Toggle Portb.1 PB1の出力を反転する。
14  Wait 1 1秒間そのまま。
15 Loop Doループの範囲ここまで。12行目へ戻る。
16
17 End メインプログラム終わり。
18
19 Int0_int: 割込みプログラム「Int0_int」ここから。
20  Toggle Portb.0 PB0の出力を反転する。
21  Waitms 250 250ミリ秒間そのまま。
22 Return 割込みプログラム「Int0_int」ここまで。

 このプログラムでは、スイッチを押し続けると外部割込みが連続して発生し、「Toggle Portb.0」と「Waitms 250」が繰り返し実行されます。その結果LED0が0.5秒周期で点滅します。先のプログラムと違って、スイッチを押している間LED1の点滅は停止するようです。14行目「Wait 1」の時間を短くして観察してみましたが、点滅する気配はありませんでした。スイッチを押してから250ミリ秒以内にすぐ放すと、「Toggle Portb.0」と「Waitms 250」が1回だけ実行されます。この場合はスイッチのチョイ押しでLED0の点灯状態が反転する動作のように見えます。

 割り込みプログラムの実行中にスイッチを押しても新たな割り込みは発生しません。このことは、21行目「Waitms 250」を「Wait 2」とかに書き換えて実験してみるとわかります。LED0の点灯中にいくらスイッチを押しても反応しません。

4. スイッチを押している間だけLEDが点灯、放すと消灯する

 スイッチを押している間ずっとLED0が点灯し続け、スイッチを放すと消灯するプログラムです。メインプログラム内でLED0を消灯させておき、割り込みプログラム内に点灯命令を書きました。

 プログラムファイル int1c.bas

1 $regfile = "attiny2313.dat" ATtiny2313を使用する。
2 $crystal = 1000000 クロック周波数を1MHzに設定。
3
4 Config Portb = Output ポートBを出力に設定する。
5 Config Portd = Input ポートDを入力に設定する。
6 Portd = 127 PD0〜PD6をプルアップする。
7
8 On Int0 Int0_int 割込みが起きたら「Int0_int」を実行する。
9 Enable Interrupts 割込み全般を許可する。
10 Enable Int0 Int0割込みを許可する。
11
12 Do Doループの範囲ここから。
13  Portb.0 = 0 PB0の出力を「0」にする。LED0が消灯。
14 Loop Doループの範囲ここまで。12行目へ戻る。
15
16 End メインプログラム終わり。
17
18 Int0_int: 割込みプログラム「Int0_int」ここから。
19  Portb.0 = 1 PB0の出力を「1」する。LED0が点灯。
20 Return 割込みプログラム「Int0_int」ここまで。

5. 割り込みプログラム内で変数を使う

 上記と同じ動作を変数を使ってやってみました。割り込みプログラム内で変数「Flag」の値を「1」にします。メインプログラムでは「Flag」が「1」ならLEDを点灯、「0」ならLEDを消灯させます。

 プログラムファイル int1d.bas

1 $regfile = "attiny2313.dat" ATtiny2313を使用する。
2 $crystal = 1000000 クロック周波数を1MHzに設定。
3
4 Config Portb = Output ポートBを出力に設定する。
5 Config Portd = Input ポートDを入力に設定する。
6 Portd = 127 PD0〜PD6をプルアップする。
7 Dim Flag As Bit ビット型変数「Flag」を使用する。
8
9 On Int0 Int0_int 割込みが起きたら「Int0_int」を実行する。
10 Enable Interrupts 割込み全般を許可する。
11 Enable Int0 Int0割込みを許可する。
12
13 Do Doループの範囲ここから。
14  If Flag = 1 Then Portb.0 = 1 「Flag=1」なら「PB0=1」に(LEDを点灯)する。
15  If Flag = 0 Then Portb.0 = 0 「Flag=0」なら「PB0=0」に(LEDを消灯)する。
16  Flag = 0 「Flag」の値を「0」にする。
17 Loop Doループの範囲ここまで。13行目へ戻る。
18
19 End メインプログラム終わり。
20
21 Int0_int: 割込みプログラム「Int0_int」ここから。
22  Flag = 1 「Flag」の値を「1」にする。
23 Return 割込みプログラム「Int0_int」ここまで。

 16行目に「Flag = 0」という命令があります。押されていたスイッチが元に戻ったとき、変数「Flag」の値を「0」に戻すためです。通常の入力スイッチプログラムでは、スイッチを押したとき「Flag=1」になり、放すと「Flag=0」に戻るという動作にできます。しかし外部割込みのスイッチは「Flag」が「1」に変わるきっかけを与えるだけで、「Flag」の値そのものを操作しているわけではありません。このことを理解するのにずいぶん時間がかかりました。

 Do〜Loop間は下記のように書いてもOKです。「 : 」(コロン)が必要です。

Do
 If Flag = 1 Then : Portb.0 = 1
 Else : Portb.0 = 0
 End If
 Flag = 0
Loop

6. スイッチを押している間だけLEDの点滅が停止する

 通常はLEDが1秒周期で点滅しています。スイッチを押すとその間だけ点滅を停止するというプログラムです。LED点灯時にスイッチを押せば点灯したままで点滅が停止、消灯時にスイッチを押せば消灯したままで点滅が停止します。押していたスイッチを放せば点滅を再開します。

 プログラムファイル int1e.bas

1 $regfile = "attiny2313.dat" ATtiny2313を使用する。
2 $crystal = 1000000 クロック周波数を1MHzに設定。
3
4 Config Portb = Output ポートBを出力に設定する。
5 Config Portd = Input ポートDを入力に設定する。
6 Portd = 127 PD0〜PD6をプルアップする。
7 Dim Flag As Bit ビット型変数「Flag」を使用する。
8
9 On Int0 Int0_int 割込みが起きたら「Int0_int」を実行する。
10 Enable Interrupts 割込み全般を許可する。
11 Enable Int0 Int0割込みを許可する。
12
13 Do Doループの範囲ここから。
14  Toggle Portb.0 PB0の出力を反転する。
15  Waitms 500 0.5秒間そのまま。
16  If Flag = 1 Then 条件分岐。「Flag=1」ならば、
17   Exit Do Doループから抜ける。
18  End If 条件分岐プログラムここまで。
19  Flag = 0 「Flag」の値を「0」にする。
20 Loop Doループの範囲ここまで。13行目へ戻る。
21
22 Int0_int: 割込みプログラム「Int0_int」ここから。
23  Flag = 1 「Flag」の値を「1」にする。
24 Return 割込みプログラム「Int0_int」ここまで。
25
26 End 終わり。

 このプログラムでは「End」が最後に来ています。今までのように「Int0_int」の前に「End」を書いた場合は、押していたスイッチを放してもLEDの点滅は停止したままで再開しません。スイッチを押した時点でプログラムが終了してしまうようです。それではなぜ、「End」を最後に書くとDoループを抜けてもまた元に戻るのでしょうか。シミュレーターで動きをたどって行ったところ、「Flag=1」になってループを抜けると22行目「Int0_int:」へ進み、24行目「Return」でプログラムの先頭へ戻りました。このときになぜか「Flag」も「0」に戻るので、プログラムをリセットしたような形になります。「Int0_int」の前に「End」があると、ループを抜けた後「End」へ行き、そこで終わってしまいます。