前回までで回路図と組み立ては完成しましたが、今後回路を発展させていく上でいずれ経験すると思われるスイッチ問題について説明します。
本シリーズ記事の内容を改訂して、基礎編、応用編、実践編として以下のリンクに公開しています。以下のシリーズはさらにいろいろなPICマイコンの機能をご紹介しています!
PICマイコン電子工作入門 〜基礎編〜
PICマイコン電子工作入門 〜応用編〜
PICマイコン電子工作入門 〜実践編〜
スイッチ問題?
根深い問題だとか、スイッチ問題だとか、その中身を全然説明しないでここまできましたが、これからその問題と解決方法を説明します。最後のチャレンジ課題でも必要となる知識ですし、今後スイッチを扱う場合には理解しておく必要のある知識ですので詳しく説明しようと思います。
なおこの説明の中で、実際に前回までに作成した回路をそのまま使って確認してみます。プログラムについては今までの作成したものとはまったく別物になりますので、MPLABX上で別の新しいプロジェクト上で作成します。
これから作成するものですが、
「タクトスイッチを押す度に、発光ダイオードを点灯→消灯→点灯…と制御する」
というものです。なんだか単純で簡単な処理で実現できそうですよね!
ところで、スイッチを押す度に点灯→消灯→点灯…というように、ある状態をいったりきたり繰り返す動作を「トグル動作」と呼びます。このあと「トグル動作」「トグル処理」という言葉をところどころで使うと思います。
これから新規プロジェクトを作成して進めますが、プログラムを作る前に、まずは発光ダイオードのトグル処理をするためにどのような実装をすればよいか考えてみます。
実装方針検討
これから作るプログラムは、発光ダイオードのON/OFF制御、スイッチのON/OFF読み取りだけですので、今までのプログラムのうち、動作処理部分以外はそのまま使います(なおクロック周波数については8MHzに変更します)。ここではどのような動作処理をすればよいか考えてみます。
まず大ざっぱですが、以下のようにすればよさそうですよね。
while(1) {
・スイッチ状態を確認する
・もしスイッチが押されたら発光ダイオードをトグル制御する
}
作成した回路でスイッチはGP4の値を見れば状態がわかりましたよね。上の処理でスイッチの状態を確認するのにはGP4の値を確認します。なお、発光ダイオードの制御ですが、トグル制御するのに非常に簡単な方法があります。発光ダイオードはGP5ピンに接続されていますよね。この場合、
GP5 = !GP5;
とすれば、発光ダイオードが点灯しているときにこれを実行すると消灯、発光ダイオードが消灯しているときにこれを実行すると点灯します。この仕組みについて説明します。まず、GP5は0か1のどちらかの値しか取りません。また、”!”は、NOT(反転)の処理を行います。この文は、!GP5をGP5に代入する、という意味なのですが、!GP5は、GP5が0であれば1、1であれば0となります。つまりこの文を実行すると、点灯していれば消灯、消灯していれば点灯します。これを使って以下のようにしたらどうでしょうか。
while(1) {
・もしスイッチがONだったら(GP4が1だったら)、
・発光ダイオードをトグル処理する(GP5 = !GP5;)
}
もうちょっとプログラムっぽく書くと、
while(1) {
if(GP4) {
GP5 = !GP5;
}
}
ということになります。これ以降、このwhile(1){}の処理を「スイッチ処理」と呼ぶことにします。
このプログラムですが、、、残念ながらこの処理方法ではうまく動きません。これからその理由について詳しく説明します。
まず、このようなwhileループですが、この程度の処理であれば、1回ループするのにせいぜい数マイクロ〜数十マイクロ秒程度です。一方で人間がスイッチを押している時間はどんなに短くても十分の何秒、という程度でしょう。ということは、このwhileループはスイッチがONの間、かなりの回数処理されることになります。これを図にすると以下のようになります。
先ほどのように、「スイッチがONだったら、発光ダイオードをトグル処理する」という動作処理をした場合、以下の点線部分が「スイッチがONだったら」という条件に合致してしまいます。
ということは、点線内の処理ごとに発光ダイオードがトグル処理されますので、スイッチを押している間、発光ダイオードが点滅することになります。
それでは、どのような処理にすれば「スイッチを押したときに(1回だけ)」発光ダイオードをトグル処理できるのでしょうか。もう一度この図で考えてみます。「スイッチを押したときに」発光ダイオードをトグルするということは、以下の処理のうち、青い矢印の処理のところ「だけ」で発光ダイオードをトグル処理すればいいでよすね。
この青いところだけ成り立つ条件ってどのようなものでしょうか。少なくとも「スイッチがON」だけではないですよね。「スイッチがON」の条件を満たすところは、先ほどの点線内のところすべてになります。そこで、青い矢印の一つ前の矢印を見てみると、一つ前の処理ではスイッチがOFFですよね。ということは、今回の処理と、一つ前の処理のときのスイッチの状態を合わせて、「現在のスイッチがONのときで、かつ、一つ前の処理のときのスイッチがOFFだったら」発光ダイオードをトグル処理すればうまくいきそうですよね。
さらに「現在のスイッチがONのときで、かつ、一つ前の処理のときのスイッチがOFF」以外の組み合わせのときは、発光ダイオードの制御はしないことにすれば、上の図の青い矢印だけ、発光ダイオードの点灯状態をトグル処理できそうです。一つ前の状態と今回の状態については、それぞれONとOFFの状態がありますので、すべての組み合わせは、
- 1つ前の処理のときはOFF、今回の処理もOFF(条件1)
- 1つ前の処理のときはOFF、今回の処理はON(条件2)
- 1つ前の処理のときはON、今回の処理はON(条件3)
- 1つ前の処理のときはON、今回の処理はOFF(条件4)
の4通りになります。あとのプログラム説明のためにそれぞれ条件番号をつけておきます。これを図で確認すると以下のようになります。
これをプログラムに書いていけばよさそうです。つまり「条件2」のときのみトグル処理をして、その他の条件のときには何もしない、というプログラムです。
プログラム作成
では早速プログラムを書いてみましょう。まずは言葉で書きます。
「前回のスイッチ状態」 = OFFとしておく
while(1) {
もし今回のスイッチ状態がONだったら(条件2か3に絞られる) {
さらに前回のスイッチ状態を確認してそれがOFFだったら(条件2に絞られる) {
トグル制御する条件2に当てはまったので、発光ダイオードをトグル制御する;
}
条件3は何も処理しない。
ここで今回のループが終わるので、次回処理のために「前回のスイッチ状態」= ONにしておく;
} else {
こちらの条件は条件1か4になり、現在のスイッチ状態はOFFということなので、
前回の状態にかかわらずなにもしなくてもよいのでトグル処理はしない
ここで今回のループが終わるので、次回処理のために「前回のスイッチ状態」= OFFにしておく;
}
う〜ん、なんかかなりややこしいことになってきましたね。ポイントは、最初に書いた4つの条件をすべて処理できているか、という点です。また上のプログラムは、今回のスイッチ状態を判断するところから入っていますが、前回のスイッチ状態を判断するところから入ってもプログラムを書くことができます。
早速新規プロジェクトを作成してプログラムを書いてみます。新規プロジェクトは、ちょっと謎めいていますがプロジェクト名を
“Switch_Bounce” として作成してください。なお、新規プロジェクトの作成の仕方は「第16回 プログラムをコピペして一度動作させてみる」を参考にしてください。
新規プロジェクトができたら、以下が今回のブログラムになりますので、main.cにペーストしてください。
/*
* File: main.c
* Author: Tool Labs
* プログラム内容:
* タクトスイッチ(GP4)が押されたら、
* 発光ダイオードをトグルする。
* これは、うまく動かないバージョン。
*/
// インクルードファイル
#include <stdio.h>
#include <stdlib.h>
#include <xc.h>
// PIC12F683コンフィグレーションビット設定
#pragma config FOSC = INTOSCIO // Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select bit (MCLR pin function is digital input, MCLR internally tied to VDD)
#pragma config CP = OFF // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON // Brown Out Detect (BOR enabled)
#pragma config IESO = OFF // Internal External Switchover bit (Internal External Switchover mode is disabled)
#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
// クロック周波数指定 (ここでは8MHzに設定)
// (__delay_ms()関数が必要としているため)
#define _XTAL_FREQ 8000000
/*
* main()関数
*/
int main(int argc, char** argv) {
// 変数宣言
unsigned char prev; // 前回のスイッチの状態を格納する。OFF=0, ON=1とする。
// マイコン初期化
OSCCON = 0x70; // クロック周波数を8MHzに設定
ANSEL = 0x00; // すべてのピンをデジタルモードに設定
TRISIO = 0x10; // GP4ピンを入力、その他出力(GP3はもともと固定で入力)
// 発光ダイオードのピン(GP5)を1にして、点灯させる
GP5 = 1;
// この後、発光ダイオードのトグル処理をする
// まず、「前回のスイッチ状態」に0(OFF)を入れておく
prev = 0;
// 前回のスイッチ状態がOFFで、今回のスイッチ状態がONのときのみ
// 発光ダイオードのトグル処理をする
while(1){
// 今回のスイッチ状態を確認する
if(GP4){
// こちらは今回のスイッチ状態がONのときの処理
// 今回ONの場合、前回のスイッチ状態がOFFのとき
// 条件に当てはまるので前回のスイッチ状態がOFFかどうか確認する
if(prev == 0){
// ここは前回がOFF、今回がONのときなので、
// トグル処理をする
GP5 = !GP5;
}
// これでループが終わるので、次回の処理のために
// 今回のスイッチ状態(ON)を「前回のスイッチ状態」に入れておく
prev = 1;
} else {
// こちらは今回のスイッチ状態がOFFのときの処理
// 今回OFFの場合は、前回の状態にかかわらずトグル処理はしない
// これでループが終わるので、次回の処理のために
// 今回のスイッチ状態(OFF)を「前回のスイッチ状態」に入れておく
prev = 0;
}
}
// ここには到達しない
return (EXIT_SUCCESS);
}
動作確認する
それでは、プロジェクトをビルド、回路に書き込みして動作確認してみてください。
電源を入れると発光ダイオードが点灯した状態になります。このあと、スイッチを押すたびに発光ダイオードが点灯したり消灯したりするはずですが、、、うまく動きましたか?
実はうまく動きません。私の回路では、点灯しているときにスイッチを押してもそのままだったり、消灯しているときにスイッチを押しても一瞬光ったと思ったら消えてしまったりしました。だいたい10回に3〜4回失敗しました。なお、スイッチの種類や個体によって現象が異なりますので、かなりうまく動いてしまうケースもあるかもしれません。うまく動いてしまう場合は、何回も押してみて下さい。(なんか強引…)
これがスイッチ問題です。
ではなぜこのようなことが発生するのかの説明と、その後対策をします。
スイッチ問題
今まで、スイッチの信号は以下のように説明してきました。
でも、機械的なスイッチの信号はこのようにはなりません。実際の信号はどのようなものかというと、以下のような感じです。
(https://zone.ni.com/reference/en-XX/help/375472F-01/switch/reed_relay_protect_bounce/より引用)
実はこれはタクトスイッチではなく、リレースイッチの信号なんですが、スイッチ問題の典型例としてあげてみました。これは、(リレー)スイッチを何度もON、OFFしているわけではなく、実際には人が1度だけスイッチを押したときのスイッチ信号を記録したものです。これを模式的に表現すると以下のようになります。
どういうことかというと、スイッチは1回しか押してないのに、何度もOFF-ONを繰り返します。スイッチは2つの金属が接したり離れたりすることによりONになったりOFFになったりするのですが、このスイッチを押したとき、金属どうしが1度にピタッとくっつくわけではなく、何度か跳ね返って最終的にくっつきます。またスイッチを離したときも同様に、パッと離れるわけではなく(名残惜しそうに)何度かくっついたり離れたりして最終的に離れます。注意点としては、このパターンは毎回異なり、さらに不安定期間も毎回異なります。
この現象ですが、身近な例では、引き戸(ガラガラっと横方向に開け閉めする扉)を勢いよく閉めると、ピタッと1回で閉まりませんよね。何度かバタンバタンという感じで跳ね返ってから閉まりますよね。スイッチの金属どうしも同じような感じです。ただ小さいので非常に短い時間の繰り返しになります。
スイッチを1回しか押していないのに、実際には何度もON、OFFを繰り返すことを、スイッチの「チャタリング」と呼んでいます。(この「チャタリング」とは和製英語で、英語では”Switch Bounce”(スイッチの飛び跳ね)と呼ばれています)
上で作成したプログラムは実際何が起こっていたかというと、まずそれぞれのスイッチ処理において、スイッチ状態は以下のようになっていました。
上で作成したプログラムは、スイッチ状態がOFF→ONになったときに発光ダイオードのトグル処理をするようにしていましたので、
このように、OFF→ONのところで発光ダイオード点滅をトグル処理するようになってしまっていました。スイッチを1回しか押してないのに、チャタリングが発生し、その部分で何度かトグル処理してしまっていたことが原因です。トグル処理がたまたま奇数回でしたら問題ありませんが、上の図のように偶数回の場合は発光ダイオードに変化がないことになってしまいます。
それでは、このチャタリングを防止するためにはどうしたらよいか考えます。
チャタリング防止方法
スイッチのチャタリング防止方法は、ソフトウエアで解決する方法とハードウエアで解決する方法があります。ここでは電子部品の追加なしに解決できるソフトウエアの方法を説明します。
なお、ハードウエアで解決する方法は、主に以下の方法があります。
- 抵抗とコンデンサをつなげる方法
抵抗とコンデンサをつなげてスイッチの信号を平滑化します。ちょっと難しく言うと、抵抗とコンデンサで積分回路を作って、スイッチが入ったときに電流(電圧)を積分することにより平滑化します。回路自体は簡単なのですが、理屈は結構難しいので、将来機会があればこのブログで説明に挑戦してみたいと思います。 - シュミットトリガをつなげる方法
抵抗とコンデンサの回路にさらに追加して「シュミットトリガ」という回路をつなげてチャタリングを防止します。こちらはヒステリシス特性というものを利用してチャタリングを防止する、というものですが、ちょっと難しい内容ですので、将来機会があればこのブログで説明に挑戦してみたいと思います。
ということで、ハードウエアで解決する方法は、部品を追加する必要があることと、内容的にちょっと難しいのでここでは説明しないことにします。
ではソフトウエアで解決する方法について説明します。ソフトウエアで解決する方法もいくつかありますが、ここでは一番単純な方法について説明します。
基本的な考え方は、
「スイッチ状態が変わったら、チャタリングが収まるまでスイッチ状態を確認しない。つまりチャタリングが収まるまで時間待ちする」
というちょっと乱暴な解決策です。チャタリングの収束時間はスイッチによって大きく異なりますので、かなり余裕をもった時間待つようにしてみたいと思います。ただ人がスイッチを押す間隔よりも長くしてしまうと、操作性が悪くなりますので、ここでは20msにしてみたいと思います。
この基本的な考えを図にすると以下のようになります。
なお非常に重要な注意事項ですが、上の図はスイッチがOFF→ONになったときのものですが、ON→OFFでもチャタリングは発生しますので、その対策も必要となります。
プログラムに対策を入れる
ではプログラムに対策を入れてみます。まずは言葉で書きます。
「前回のスイッチ状態」 = OFFとしておく
while(1) {
もし今回のスイッチ状態がONだったら(条件2か3に絞られる) {
さらに前回のスイッチ状態を確認してそれがOFFだったら(条件2に絞られる) {
トグル制御する条件2に当てはまったので、発光ダイオードをトグル制御する;
スイッチがOFFからONに変わったので、チャタリング対策として20ms待つ
}
条件3は何も処理しない。
ここで今回のループが終わるので、次回処理のために「前回のスイッチ状態」= ONにしておく;
} else {
こちらの条件は条件1か4になり、現在のスイッチ状態はOFFということなので、
前回の状態にかかわらずなにもしなくてもよいのでトグル処理はしない
ただし、前回のスイッチ状態がONの場合はスイッチ状態に変化があったので、
チャタリング対策として20ms待つ
ここで今回のループが終わるので、次回処理のために「前回のスイッチ状態」= OFFにしておく;
}
赤文字部分が変更点です。__delay_ms()関数を使用して20msの時間待ちをしています。
修正したプログラム
上の対策を入れたプログラムです。
/*
* File: main.c
* Author: Tool Labs
* プログラム内容:
* タクトスイッチ(GP4)が押されたら、
* 発光ダイオードをトグルする。
* チャタリング対策をいれたバージョン。
*/
// インクルードファイル
#include <stdio.h>
#include <stdlib.h>
#include <xc.h>
// PIC12F683コンフィグレーションビット設定
#pragma config FOSC = INTOSCIO // Oscillator Selection bits (INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = ON // Power-up Timer Enable bit (PWRT enabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select bit (MCLR pin function is digital input, MCLR internally tied to VDD)
#pragma config CP = OFF // Code Protection bit (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Code Protection bit (Data memory code protection is disabled)
#pragma config BOREN = ON // Brown Out Detect (BOR enabled)
#pragma config IESO = OFF // Internal External Switchover bit (Internal External Switchover mode is disabled)
#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enabled bit (Fail-Safe Clock Monitor is disabled)
// クロック周波数指定 (ここでは8MHzに設定)
// (__delay_ms()関数が必要としているため)
#define _XTAL_FREQ 8000000
/*
* main()関数
*/
int main(int argc, char** argv) {
// 変数宣言
unsigned char prev; // 前回のスイッチの状態を格納する。OFF=0, ON=1とする。
// マイコン初期化
OSCCON = 0x70; // クロック周波数を8MHzに設定
ANSEL = 0x00; // すべてのピンをデジタルモードに設定
TRISIO = 0x10; // GP4ピンを入力、その他出力(GP3はもともと固定で入力)
// 発光ダイオードのピン(GP5)を1にして、点灯させる
GP5 = 1;
// この後、発光ダイオードのトグル処理をする
// まず、「前回のスイッチ状態」に0(OFF)を入れておく
prev = 0;
// 前回のスイッチ状態がOFFで、今回のスイッチ状態がONのときのみ
// 発光ダイオードのトグル処理をする
while(1){
// 今回のスイッチ状態を確認する
if(GP4){
// こちらは今回のスイッチ状態がONのときの処理
// 今回ONの場合、前回のスイッチ状態がOFFのとき
// 条件に当てはまるので前回のスイッチ状態がOFFかどうか確認する
if(prev == 0){
// ここは前回がOFF、今回がONのときなので、
// トグル処理をする
GP5 = !GP5;
// さらにチャタリング防止のウエイトを入れる
__delay_ms(20);
}
// これでループが終わるので、次回の処理のために
// 今回のスイッチ状態(ON)を「前回のスイッチ状態」に入れておく
prev = 1;
} else {
// こちらは今回のスイッチ状態がOFFのときの処理
// 今回OFFの場合は、前回の状態にかかわらずトグル処理はしないが、
// 前回のスイッチ状態がONだったら、スイッチがOFFに変わったということなので
// チャタリング防止のウエイトを入れる
if(prev){
__delay_ms(20);
}
// これでループが終わるので、次回の処理のために
// 今回のスイッチ状態(ON)を「前回のスイッチ状態」に入れておく
prev = 0;
}
}
// ここには到達しない
return (EXIT_SUCCESS);
}
動作確認をする
それでは、上のプログラムを回路に書き込み動作確認してみてください。おそらく問題なく動作すると思います。
ただ、まれにうまく動かないことがあるかもしれません。それはチャタリング問題に起因するものではなく、タクトスイッチ構造に起因するものの可能性があります。具体的には、タクトスイッチのスイッチ部分がメカ的にうまく固定されていないことがあり、その場合、チャタリングではなく、本当に何回かスイッチを押したことになってしまうためです。この場合は、、、しっかりタクトスイッチを押すしかないですかね。。。
タイマーの開始スイッチはチャタリング防止しなくても問題ないのか
ここでちょっと疑問に思うかもしれません。
前回までに完成させたタイマーは、タクトスイッチを押すとタイマーを開始するようにしましたよね。その時のスイッチの処理は、チャタリング対策はしていませんでした。なぜチャタリング対策はいらないのでしょうか。簡単に説明しておきます。
前回のプログラムは、
- 電源投入後はスイッチ状態がONになるまで、ずっとスイッチ状態を監視する
- スイッチ状態がONになったことを一度検知したら、すぐに発光ダイオードの点滅制御にうつるため、スイッチ状態はみなくなる
という処理でした。つまりスイッチ状態が一度でもONになれば、そのあとのスイッチ状態はみないためチャタリングしようがどうでもよかったわけです。
ということで、チャタリング処理が必要な場合をきちんと把握して、適切なプログラムを組む必要があります。最後のチャレンジ課題にこのチャタリング処理が必要なものをつくろうと思います。
なお、次回はチャレンジ課題に向けた補足、そのあとチャレンジ課題をテーマにしていよいよ最終回にしたいと思います。
更新履歴
日付 | 内容 |
---|---|
2015.9.22 | 新規投稿 |
2018.12.3 | 新シリーズ記事紹介追加 |