これまでのプログラムは押しボタンスイッチを押している間だけLEDの点灯状態が変化するものでしたが、ここでは、スイッチを1回チョンと押すだけで出力が変化するプログラムを取り上げます。マイコン回路は「入力スイッチテスト用回路」を使用します。
入力スイッチを1回チョンと押すとLED0が点灯、もう1回チョンと押すと消灯するプログラムです。図に描くと下記のようになります。
これを今までのやり方で書くとすると次のようになります。後から出てくるプログラムの都合上サブルーチンを使って書いていますが、普通の書き方でも動作は同じです。
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
7 | Do | Doループの範囲ここから。 | |
8 | If Pinb.0 = 0 Then | 条件分岐のプログラムここから。 | |
もし「Pinb.0 = 0」ならば次行のプログラムを実行。 | |||
9 | Call Switch | 13行目のサブルーチン「Switch」へジャンプ。 | |
10 | End If | 条件分岐のプログラムここまで。 | |
11 | Loop | Doループの範囲ここまで。7行目へ戻る | |
12 | End | メインプログラム終わり。 | |
13 | Sub Switch | 「Switch」という名前のサブルーチンここから。 | |
14 | Toggle Porta.0 | PA0の出力を反転する。 | |
15 | End Sub | サブルーチン終わり。呼ばれた場所へ戻る。 |
スイッチが押されると「Pinb.0 = 0」になるのでサブルーチン「Switch」へ飛び、14行目「Toggle Porta.0」が実行されます。つまりLEDが消灯していれば点灯、点灯していれば消灯します。スイッチが押されていないときは「Pinb.0 = 1」なので何もしません。次に「Pinb.0 = 0」になるまでLEDの状態はそのままです。
考え方はこれで間違いないのですが、実際にやってみるとこのプログラムは動作が不安定でうまくいきません。これは、スイッチを押したときに生じるバウンス(チャタリング)という現象が原因です。
バウンスというのは、スイッチを押したとき、一度で接点が閉じなくて何度かバタついてしまう現象をいいます。下の図のようなイメージです。
バウンスが起こるのはスイッチを閉じてから数ミリ秒という短い時間内ですが、マイコンICの命令はそれよりもっと短い時間で処理されるので、今回のプログラムの場合、バウンドするたびに出力の反転が起こります。接点がバウンドする回数はスイッチを押すたびに違うため、最終的に出力が「0」になるか「1」になるかは不定です。
バウンスは押しボタンスイッチだけでなく、トグルスイッチやリレーなど、機械的接点を持つ部品すべてで生じます。したがって、これらの部品を入力スイッチとするプログラムでは、バウンスを防ぐための処理が必要になってきます。BASCOM-AVRにはそのための命令「Debounce」(デバウンス)があります。
「Debounce」命令はサブルーチンとセットで用いられます。先のプログラムのように「Pinb.0 = 0」のとき「Switch」という名前のサブルーチンを実行するプログラムの場合は、8行目「If Pinb.0 = 0 Then」と9行目「Call Switch」をまとめて次のように書きます。
このように書かれた場合、スイッチが押されて「Pinb.0 = 0」になっても、プログラムはすぐにはサブルーチンへ飛びません。25ミリ秒後、つまりバウンスが収まった頃を見計らって再びスイッチの接点の状態を調べ、やはり「Pinb.0 = 0」であればそこではじめてサブルーチンのプログラムを実行します。
(以下、2007年1月19日に訂正および追記)
BASCOM-AVRの初期設定ではデバウンスタイムが25ミリ秒になっています。これ以外の時間に設定する場合、例えばデバウンスタイムを10ミリ秒にしたいときは、プログラムで「Debounce」命令が出てくる前に「Config Debounce = 10」と記述します。他のサイトの記事を読むと、まともな品質のスイッチならばバウンスは1ミリ秒以内に収まるようです。デバウンスタイムを必要以上に長く取るとその間プログラムの処理が中断するので動作に支障が出る場合があります。
私が実験に使った押しボタンスイッチは日本開閉器のBB-15AVという型番のもので、通販で1個35円の「特価品」です。試しに「Config Debounce = 1」(デバウンスタイム=1mS)にしてこのページのプログラムを実行してみたところ、まったく問題なく動作しました。安物でもけっこう優秀なんですね。
(追記ここまで)
(以下、2007年2月12日に追記)
上記の記事を書いた時点では「バウンス」と「チャタリング」の違いがよくわかっていませんでした。それで「バウンス(チャタリング)」という曖昧な書き方でごまかしました。後で調べたところ、「オムロン」のホームページに解説が載っていました。それによると、バウンスとは「接点をオンまたはオフさせるときに生じる好ましくない間欠的開閉現象」、チャタリングとは「オンされている接点が外部からの原因により開閉を反復する現象」だそうです。よって、今回問題にしているのはチャタリングではなくて「バウンス」です。
(追記ここまで)
先のプログラムを「Debounce」を使って書き直しました。これなら安定して動作します。このプログラムは押しボタン式の電源スイッチなどに応用できそうです。
プログラムファイル input3a.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
7 | Do | Doループの範囲ここから。 | |
8 | Debounce Pinb.0 , 0 , Switch , Sub | 入力端子PB0の状態を調べる。 | |
もし「Pinb.0 = 0」ならばサブルーチン「Switch」を実行。 | |||
9 | Loop | Doループの範囲ここまで。7行目へ戻る | |
10 | End | メインプログラム終わり。 | |
11 | Sub Switch | 「Switch」という名前のサブルーチンここから。 | |
12 | Toggle Porta.0 | PA0の出力を反転する。 | |
13 | End Sub | サブルーチン終わり。呼ばれた場所へ戻る。 |
最初はLED0だけが点灯しています。入力スイッチをチョンと1回押すとLED0が消えて左隣のLED1が点灯します。もう1回スイッチを押すとLED1が消えて左隣のLED2が点灯します。こうしてスイッチを1回押すたびに点灯するLEDが順次左へ移っていくプログラムです。左端のLED7が点灯している状態でもう1回スイッチを押すとLED0の点灯に戻ります。
プログラムファイル input3b.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
7 | Porta = 1 | PA0を「1」にする。LED0が点灯。 | |
8 | Do | Doループの範囲ここから。 | |
9 | Debounce Pinb.0 , 0 , Switch , Sub | 入力端子PB0の状態を調べる。 | |
もし「Pinb.0 = 0」ならばサブルーチン「Switch」を実行。 | |||
10 | Loop | Doループの範囲ここまで。8行目へ戻る | |
11 | End | メインプログラム終わり。 | |
12 | Sub Switch | 「Switch」という名前のサブルーチンここから。 | |
13 | Rotate Porta , Left | ポートAの状態を1ビット左に回転させる。 | |
14 | End Sub | サブルーチン終わり。呼ばれた場所へ戻る。 |
スイッチが何回押されたかを、LED0〜LED7の8個のLEDで2進数表示するプログラムです。スイッチを1回押すとLED0が点灯、2回押すとLED1が点灯、3回押すとLED0とLED1が点灯、という具合です。スイッチを255回押すと8個のLEDがすべて点灯します。この状態からもう1回スイッチを押すと0に戻り、LEDがすべて消灯します。
プログラムファイル input3c.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
7 | Dim N As Byte | バイト型変数「N」を使用する。 | |
8 | Do | Doループの範囲ここから。 | |
9 | Debounce Pinb.0 , 0 , Switch , Sub | 入力端子PB0の状態を調べる。 | |
もし「Pinb.0 = 0」ならばサブルーチン「Switch」を実行。 | |||
10 | Porta = N | 変数「N」の値をポートAに出力する。 | |
11 | Loop | Doループの範囲ここまで。8行目へ戻る | |
12 | End | メインプログラム終わり。 | |
13 | Sub Switch | 「Switch」という名前のサブルーチンここから。 | |
14 | N = N + 1 | 変数「N」に1を加えて新たな「N」とする。 | |
15 | End Sub | サブルーチン終わり。呼ばれた場所へ戻る。 |
入力スイッチをチョンと1回押すとLEDが2秒間点灯するプログラムです。LEDの点灯中にスイッチを押しても反応しません。2秒を超えてスイッチを長押しすると、スイッチを押されている間ずっとLEDが点灯しています。
プログラムファイル input3d.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
7 | Do | Doループの範囲ここから。 | |
8 | Debounce Pinb.0 , 0 , Switch , Sub | 入力端子PB0の状態を調べる。 | |
もし「Pinb.0 = 0」ならばサブルーチン「Switch」を実行。 | |||
9 | Loop | Doループの範囲ここまで。7行目へ戻る | |
10 | End | メインプログラム終わり。 | |
11 | Sub Switch | 「Switch」という名前のサブルーチンここから。 | |
12 | Porta = 1 | ポートAの出力を「1」にする。LED0が点灯。 | |
13 | Wait 2 | 2秒間そのまま。 | |
14 | Porta = 0 | ポートAの出力を「0」にする。LED0が消灯。 | |
15 | End Sub | サブルーチン終わり。呼ばれた場所へ戻る。 |
このプログラムはスイッチのチョイ押しで動作するということでここに入れましたが、実は「Debounce」を使わなくてもうまくいきます。下記のプログラムになります。
プログラムファイル input3e.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | Config Porta = Output | ポートAを出力に設定する。 | |
4 | Config Portb = Input | ポートBを入力に設定する。 | |
5 | Portb = 1 | PB0をプルアップする。 | |
6 | Do | Doループの範囲ここから。 | |
7 | If Pinb.0 = 0 Then | 条件分岐プログラムここから。 | |
もし「Pinb.0 = 0」ならば8,9行目のプログラムを実行。 | |||
8 | Porta = 1 | ポートAの出力を「1」にする。LED0が点灯。 | |
9 | Wait 2 | 2秒間そのまま。 | |
10 | Else | そうでなければ(Pinb.0 = 1ならば)11行目のプログラムを実行。 | |
11 | Porta = 0 | ポートAの出力を「0」にする。LED0が消灯。 | |
12 | End If | 条件分岐プログラムここまで。 | |
13 | Loop | Doループの範囲ここまで。6行目へ戻る | |
14 | End | 終わり。 |
入力スイッチを押すとLEDが点滅を開始、もう1回押すと点滅が止まるプログラムです。スイッチを1回押すたびに点滅開始、点滅停止を繰り返します。LEDが点灯している瞬間にスイッチを押すと点灯したまま停止、消灯している瞬間にスイッチを押すと消灯したまま停止します。「Wait」時間を短くしたので、一見チョイ押しで動作するように見えます。
プログラムファイル input3f.bas
1 | $regfile = "at26def.dat" | ATtiny26Lを使用する。 | |
2 | $crystal = 1000000 | クロック周波数を1MHzに設定。 | |
3 | |||
4 | Config Porta = Output | ポートAを出力に設定。 | |
5 | Config Portb = Input | ポートBを入力に設定。 | |
6 | Portb = 1 | ポートBを「1」にする。PB0をプルアップ。 | |
7 | Declare Sub Switch | サブルーチン「Switch」を使用する。 | |
8 | Dim N As Bit | ビット型変数「N」を使用する。 | |
9 | N = 0 | 変数「N」の初期値を「0」にする。 | |
10 | |||
11 | Do | Doループの範囲ここから。 | |
12 | Debounce Pinb.0 , 0 , Switch , Sub | 「Pinb.0 = 0」ならばサブルーチン「Switch」を実行。 | |
13 | If N = 1 Then | 条件分岐。「N = 1」ならば、 | |
14 | Toggle Porta.0 | PA0の出力を反転する。 | |
15 | Waitms 100 | 0.1秒間そのまま。 | |
16 | End If | 条件分岐プログラムここまで。 | |
17 | Loop | Doループの範囲ここまで。11行目へ戻る。 | |
18 | |||
19 | End | メインプログラム終わり。 | |
20 | |||
21 | Sub Switch | サブルーチン「Switch」ここから。 | |
22 | Toggle N | 変数「N」の値を反転する。 | |
23 | End Sub | サブルーチン「Switch」ここまで。 |
スイッチが押されると12行目「Debounce Pinb.0, 0, Switch, Sub」によって21行目「Sub Switch」へ飛びます。ここでは変数「N」の値を「0」から「1」にします。「N = 1」になると13行目「If N = 1 Then」の条件に合いますので、LEDの点滅プログラムが実行されます。もう1回スイッチが押されると変数「N」の値が「1」から「0」に戻ります。これは条件に合いませんので点滅は止まります。
このプログラムはスイッチのチョイ押しで動作するように見えますが、実際は12行目「Debounce Pinb.0, 0, Switch, Sub」の時点でスイッチが押されなければ正しく動作しません。もし15行目「Waitms 100」の直前でスイッチが押されたとすると、点滅が停止するまで0.1秒余りスイッチを押し続ける必要があります。このことは、「Waitms 100」を「Wait 1」に変えてみるとよくわかります。