LoginSignup
0
1

More than 1 year has passed since last update.

Longan Nanoを使ってみる 11 ~ボタンの入力

Last updated at Posted at 2022-03-02

前の記事

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もボタンに使用する。

sch2.png

このポートを読み取り、ボタンの状態を取得する。

注意

このページは、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が繰り返されることになる
chatter1.png

マイコンは十分早いため、この瞬間的なオン/オフをすべて読み取ってしまうことで、誤動作となってしまう。

チャタリングを防ぐには、アナログ回路(CR回路)による方法や、論理回路(RSラッチ)による方法があるが、今回は部品点数を減らすため、ソフトウェアによる方法を使う。ソフトウェアによる方法は低速で低信頼だが安価(追加部品なし)で簡単な方法になる。

ソフトウェアによるチャタリング防止

ソフトウェアでチャタリング防止を行う定番の処理は、「nマイクロ秒間隔でボタンのチェックを行い、m回連続してオンの場合にのみ、オンと扱う」という処理になる。チャタリングは、ボタンが完全に押されてしまった後は起こらない(ボタンが劣化すると起こるが、それはまた別の話)ので、一定の時間以上、連続して押されていることを判断すればよい。

下の図は、チェックの様子を示したもの。この例では、4回連続してオンの時にのみ、オンと扱う、と考えた。 最初の1でスイッチがオンになったことを知るが、ソフトウェア上ではこれをオンとは扱わない。その後、2で2回目もオンだが、3回目にオフが来たので、カウンタはリセットする。その後、再度スイッチがオンになり、カウントを1から再開、その後、スイッチは安定してオンのままをキープできるので、4回目で実際にオンとなる。

chatter2.png

(この例では、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!と表示される。

console.png

動作確認が終わったら、テスト用のコードは削除しておく。

ここまででできたこと

接続したボタンの状態を読み込み、チャタリングを除去してプログラムから利用できるようになった。

次の記事に進む

Longan Nanoを使ってみる 12 ~ボールのロス~

今回の追加を反映したソースコード

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;
}
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1