前の記事
Longan Nanoを使ってみる 10 ~パドルを動かす~
全体の目次
Longan Nanoを使ってみる 1 ~ビルド環境の構築~
Longan Nanoを使ってみる 2 ~デバッガの環境設定~
Sipeed RISC-V Debugger
Longan Nanoを使ってみる 3 ~デバッガの使用方法~
Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~
Longan Nanoを使ってみる 5 ~ゲームのプロジェクトを作成~
Longan Nanoを使ってみる 6 ~文字出力~
Longan Nanoを使ってみる ~FONTX2ファイルを作る~
Longan Nanoを使ってみる 7 ~外枠とブロックを書く~
Longan Nanoを使ってみる ~謎の画像表示関数~
Longan Nanoを使ってみる 8 ~ボールを動かす~
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~
Longan Nanoを使ってみる 10 ~パドルを動かす~
Longan Nanoを使ってみる 11 ~ボタンの入力
Longan Nanoを使ってみる 12 ~ボールのロス~
Longan Nanoを使ってみる 13 ~ステージの遷移とゲームオーバー~
Longan Nanoを使ってみる 14 ~PWMとサウンド~
Longan Nanoを使ってみる 15 ~音楽を鳴らす~
Longan Nanoを使ってみる 16 ~とりあえずのまとめ~
はじめに
Longan Nanoを使ってみる 9 ~A/Dコンバータから入力~で作った入力装置は、ボリュームとボタンを用意してある。パドルの動きにボリュームを使ったので、次はボタンの入力処理を行う。
作った回路では、スイッチはGPA6に接続した。 下の絵では、 A6、の部分にボタンが接続されている。また、将来、コントローラを増やすときのため、A7もボタンに使用する。
このポートを読み取り、ボタンの状態を取得する。
注意
このページは、quiita.com で公開されています。URLがqiita.com以外のサイト、例えばjpdebug.comなどのページでご覧になっている場合、悪質な無許可転載サイトで記事を見ています。正しい記事は、https://qiita.com/BUBUBB/items/7ce85ada67a3f6d1944d からリンクしています。
無許可転載サイトでの権利表記(CC BY SA 2.5、CC BY SA 3.0とCC BY SA 4.0など)は、不当な表示です。
正確な内容は、qiitaのページで参照してください。
チャタリング
こうしたマイコンで機械的なボタンを入力するとき、チャタリングを考慮する必要がある。チャタリングとは、ボタンを押した瞬間に、ボタンの電極がバウンドしたり、電気接点の接触などが原因で、オンとオフが短い間で繰り返される現象。
下の図は、人間がスイッチを押したときの様子。理想的には、スイッチ状態がOFF/ONにすぐに切り替わるが、実際にはそうならない。スイッチがオンになった後、機械的に跳ね返りが起こったり、スイッチ端子が完全に接触する直前に、電流が漏れて流れる、など不安定な要素がある。その結果、実際のOn/OFFは、スイッチが入る(あるいは切れる)瞬間にON/OFFが繰り返されることになる
マイコンは十分早いため、この瞬間的なオン/オフをすべて読み取ってしまうことで、誤動作となってしまう。
チャタリングを防ぐには、アナログ回路(CR回路)による方法や、論理回路(RSラッチ)による方法があるが、今回は部品点数を減らすため、ソフトウェアによる方法を使う。ソフトウェアによる方法は低速で低信頼だが安価(追加部品なし)で簡単な方法になる。
ソフトウェアによるチャタリング防止
ソフトウェアでチャタリング防止を行う定番の処理は、「nマイクロ秒間隔でボタンのチェックを行い、m回連続してオンの場合にのみ、オンと扱う」という処理になる。チャタリングは、ボタンが完全に押されてしまった後は起こらない(ボタンが劣化すると起こるが、それはまた別の話)ので、一定の時間以上、連続して押されていることを判断すればよい。
下の図は、チェックの様子を示したもの。この例では、4回連続してオンの時にのみ、オンと扱う、と考えた。 最初の1でスイッチがオンになったことを知るが、ソフトウェア上ではこれをオンとは扱わない。その後、2で2回目もオンだが、3回目にオフが来たので、カウンタはリセットする。その後、再度スイッチがオンになり、カウントを1から再開、その後、スイッチは安定してオンのままをキープできるので、4回目で実際にオンとなる。
(この例では、4回連続としたが、時間を十分長くとれば2回連続でも使い物になる。)
ボタン関連の処理を、button.cとbutton.hに実装する。
このbutton.cでは、 IsButtonPushedとIsButtonReleasedの2つの関数を実装している。これらの関数は、引数にコントローラー番号として0か1を受け取り、PortTbl配列で定義されたポート、それぞれ A6、A7からデータを受け取る。
チェックする間隔は、この関数を呼び出す間隔であり、50回連続してボタンが押されて(あるいは離されて)いたらtrueを返すようになっている。
FlagStatus fr = gpio_input_bit_get (GPIOA,PortTBL[PlayerNo]);
の部分が、実際にGPIOのA6から、値を読み出す部分で、これはAPI GD32V Firmware Library User Guide の中で説明されている。
ここから読みだした、ポートの生の状態は、チャタリングを考えるとそのままでは使えない。そこで、Counterという配列の変数で、連続してオンになっている数を数え、50を超えたら初めて trueを返すようにしている。
重要なのは(当たり前だが)この関数は、1回読んだだけでtrueを返すことはない。一回ボタンが押されたことを検知するためには、この関数は最低でも51回は呼ばれる必要がある。ボタンを検知する必要がない場合でも、常にこの処理は定期的に呼び出す必要があるということ。
button.h
#ifndef __button_h__
#define __button_h__
bool IsButtonReleased(int PlayerNo);
bool IsButtonPushed(int PlayerNo);
#endif
button.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
// ボタンが押され続けた
static const uint32_t PortTBL[] = {GPIO_PIN_6,GPIO_PIN_7};
bool IsButtonPushed(int PlayerNo)
{
static u8 Counter[2];
FlagStatus fr = gpio_input_bit_get (GPIOA,PortTBL[PlayerNo]);
if (!fr) {
if (Counter[PlayerNo] == 0xFF) Counter[PlayerNo]=0xFE;
Counter[PlayerNo]++;
} else {
Counter[PlayerNo]=0;
}
if (Counter[PlayerNo] >= 50) {
return TRUE;
}
return FALSE;
}
// ボタンが離され続けた
bool IsButtonReleased(int PlayerNo)
{
static u8 Counter[2];
FlagStatus fr = gpio_input_bit_get (GPIOA,PortTBL[PlayerNo]);
if (fr) {
if (Counter[PlayerNo] == 0xFF) Counter[PlayerNo]=0xFE;
Counter[PlayerNo]++;
} else {
Counter[PlayerNo]=0;
}
if (Counter[PlayerNo] >= 50) {
return TRUE;
}
return FALSE;
}
ボタン押下を検出する
実際の、ユーザーインタフェースのボタンの多くは、押した時ではなく話したときに反応する。そのため、このプログラムも同じように処理をする。ボタンは、基本的に離された状態にある。そのため、単に IsButtonReleasedを呼び出すだけだと、いきなり最初からボタンが「押された」(実際には離された)という状態になってしまう。
そのため、「ボタンが押されたあとに離された」という実装が必要になる。
ここで実装する CheckP1Buttonは、定期的に呼び出され、ボタンが押された後に離されたら戻り値で trueを返すと同時に、 グローバル変数 p1ButtonPushedをtrueにする。この、グローバル変数がtrueになっているのは、一瞬だけで、次回CheckP1Buttonが呼び出されるとクリアされる。
button.h
:
extern bool CheckP1Button();
extern bool p1ButtonPushed;
extern bool p2ButtonPushed;
:
button.c
bool p1ButtonPushed = FALSE;
bool p2ButtonPushed = FALSE;
// 一定時間間隔で呼び出され、ボタンが押されたかを監視する。
// 一般的なボタンは、押したときに反応するのではなく、離した時に反応する。
// そのため、この処理では、ボタンの状態を示す p1ButtonPushed グローバル変数、関数の戻り値とも、
// ボタンが押された時ではなく、離されたときにtrueとなる
bool CheckP1Button()
{
p1ButtonPushed = RESET; // ボタンの変化は一回だけトリガーされれば良い
static bool isButton1 = FALSE;
if (isButton1 == FALSE) {
if (IsButtonPushed(0)) { //ボタンがOFF→ONに変化
isButton1 = TRUE; // ボタンが押された
}
} else {
if (IsButtonReleased(0)) {//ボタンがON→OFFに変化
isButton1 = FALSE;
p1ButtonPushed = TRUE;
return TRUE;
}
}
return FALSE;
}
動作を確認する
ゲームのメインプログラムから、この関数を呼び出して、実際に期待する動作をするか、確認する。チェックするポイントは、次の通り
- 一回押しただけなのに、複数回押された、という状態にならない
- ボタンは押した時ではなく、離したときに反応する
メインプログラムに、printfを入れて動作を確認する。ソースコードレベルのデバッガでもよいが、ボタンを押しながらマウスでデバッグというのも不便。
LEDを点灯させるのも、こうした「繰り返される一瞬のイベント」をテストするには不便。やはり原始的なprintfデバッグ(Longan Nanoを使ってみる 4 ~printfを使ったデバッグ~)が便利。
メインループのウエイトを抜けた直後(タイマー割込みにより、ループから抜けた直後)に、ボタンのチェックを行い、printfを追加する。
game.c
:
while (gameState != STATE_GAMEOVER) { //ゲームオーバーになるまで繰り返す
timer_enable(TIMER5); // タイマーを有効にする
// タイマーのウェイト処理。wakeFlagが割り込みルーチン内で1になるまで無限ループする
while(WakeFlag == 0) {
delay_1ms(10);
break;
}
if (CheckP1Button()) {
printf("pushed!\r\n\r\n");
}
:
モニターターミナルには、ボタンを押す(離す)たびに、pushed!と表示される。
動作確認が終わったら、テスト用のコードは削除しておく。
ここまででできたこと
接続したボタンの状態を読み込み、チャタリングを除去してプログラムから利用できるようになった。
次の記事に進む
今回の追加を反映したソースコード
button.h
#ifndef __button_h__
#define __button_h__
bool IsButtonReleased(int PlayerNo);
bool IsButtonPushed(int PlayerNo);
extern bool CheckP1Button();
extern bool p1ButtonPushed;
extern bool p2ButtonPushed;
#endif
button.c
#include "lcd/lcd.h"
#include "led.h"
#include "memory.h"
// ボタンが押され続けた
static const uint32_t PortTBL[] = {GPIO_PIN_6,GPIO_PIN_7};
bool IsButtonPushed(int PlayerNo)
{
static u8 Counter[2];
FlagStatus fr = gpio_input_bit_get (GPIOA,PortTBL[PlayerNo]);
if (!fr) {
if (Counter[PlayerNo] == 0xFF) Counter[PlayerNo]=0xFE;
Counter[PlayerNo]++;
} else {
Counter[PlayerNo]=0;
}
if (Counter[PlayerNo] >= 50) {
return TRUE;
}
return FALSE;
}
// ボタンが離され続けた
bool IsButtonReleased(int PlayerNo)
{
static u8 Counter[2];
FlagStatus fr = gpio_input_bit_get (GPIOA,PortTBL[PlayerNo]);
if (fr) {
if (Counter[PlayerNo] == 0xFF) Counter[PlayerNo]=0xFE;
Counter[PlayerNo]++;
} else {
Counter[PlayerNo]=0;
}
if (Counter[PlayerNo] >= 50) {
return TRUE;
}
return FALSE;
}
bool p1ButtonPushed = FALSE;
bool p2ButtonPushed = FALSE;
// 一定時間間隔で呼び出され、ボタンが押されたかを監視する。
// 一般的なボタンは、押したときに反応するのではなく、離した時に反応する。
// そのため、この処理では、ボタンの状態を示す p1ButtonPushed グローバル変数、関数の戻り値とも、
// ボタンが押された時ではなく、離されたときにtrueとなる
bool CheckP1Button()
{
p1ButtonPushed = RESET; // ボタンの変化は一回だけトリガーされれば良い
static bool isButton1 = FALSE;
if (isButton1 == FALSE) {
if (IsButtonPushed(0)) { //ボタンがOFF→ONに変化
isButton1 = TRUE; // ボタンが押された
}
} else {
if (IsButtonReleased(0)) {//ボタンがON→OFFに変化
isButton1 = FALSE;
p1ButtonPushed = TRUE;
return TRUE;
}
}
return FALSE;
}