スイッチ入力
スイッチ入力回路
もっとも一般的なスイッチ入力回路は下図のとおりです。プルアップであるためスイッチオフでHigh、スイッチオンでLowと、イメージと信号が逆転します。これが嫌でプルダウンにしても良いのですが、そうするとスイッチの先まで電源ラインを伸ばさなければなりません。これを回路設計者は嫌います。基板上を引き回すのはグランドラインにしたいのです。
(a) スイッチオフ:IN1 = H | (b) スイッチオフ:IN1 = L |
チャタリング
スイッチ入力において最も気を付けなければならないのが、チャタリングとその対策です。 機械的なスイッチを押した瞬間または放した瞬間、極短い間に接点が振動を起こします。それが入力端子に入ると、下図のようなオン/オフを繰り返した挙動となることがあり、これをチャタリングといいます。
チャタリングが発生すると、1回しか押されていないスイッチを複数回押されたものとして捉えてしまう場合があります。これが誤作動に繋がるため、対策が必要です。チャタリングには下表のようにハード(回路)で対策する方法と、ソフトで対策する方法があります。余力のある方で対策を検討してください。
No. | 分類 | 内容 |
---|---|---|
1 | ハード | CR回路とシュミットトリガ―の組み合わせで、反応を遅らせます。 |
2 | ソフト | 10~50ms毎にスイッチの状態を取得し、3回連続で一致した場合のみ採用します。 |
キーマトリクス
安価で小型なマイコンはI/Oポートが少なく、多数のスイッチ入力を扱うことができません。I/Oエキスパンダーで増設することもできますが、メンブレンスイッチで用いられることも多いキーマトリクス方式というものがあります。 キーマトリクスは下図のように格子状にスイッチを配置した構成で、出力ポートと入力ポートを使うのが特徴です。キーマトリクスで制御できるスイッチ数は、入力ポート×出力ポートです。下図の例では出力3と入力3の6ポートで、9個のスイッチを制御できます。
キーマトリクスの制御は少し複雑です。下図のように出力ポートを1つずつ順にHighにしたとき、入力ポートがHighであれば、その交点にあるスイッチがオンであることを表します。
[サンプルコード]
/** ----------------------------------------------------------------------------
* @brief キーの状態を確認する (キーマトリクス)
*
* @return キーの状態 [0=ON / 1=OFF]
*/
static uint32_t Read_KeyMatrix(void)
{
uint8_t pin;
uint32_t port = 0;
GPIO_WritePin(GPIOA, GPIO_PIN_0, 1);
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_MENU);
if (pin != 0) {
port |= KEY_MENU_BIT;
}
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_UP);
if (pin != 0) {
port |= KEY_UP_BIT;
}
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_DOWN);
if (pin != 0) {
port |= KEY_DOWN_BIT;
}
GPIO_WritePin(GPIOA, GPIO_PIN_0, 0);
GPIO_WritePin(GPIOA, GPIO_PIN_1, 1);
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_LEFT);
if (pin != 0) {
port |= KEY_LEFT_BIT;
}
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_RIGHT);
if (pin != 0) {
port |= KEY_RIGHT_BIT;
}
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_ENTER);
if (pin != 0) {
port |= KEY_ENTER_BIT;
}
GPIO_WritePin(GPIOA, GPIO_PIN_1, 0);
GPIO_WritePin(GPIOA, GPIO_PIN_2, 1);
pin = GPIO_ReadPin(GPIOA, KEY_MATRIX_IN_CANCEL);
if (pin != 0) {
port |= KEY_CANCEL_BIT;
}
GPIO_WritePin(GPIOA, GPIO_PIN_2, 0);
return ~port;
}
スイッチ入力処理例
スイッチ入力処理としてよく求められるのは、次の3点です。
- チャタリング対策
- 長押し判定
- 長押しによる連打
これらを満たしたサンプルコードを下に掲載していますが、その前に動作を簡単に紹介しておきます。なお以降の図(タイミングチャート)中の項目が何を表しているかは下表を参照ください。
項目 | 内容 |
---|---|
Scan_Key | スイッチの入力を監視する関数です。 |
Get_KeyState | Scan_Key関数による監視結果を取得します。結果はedgeとholdとして出力します。 |
入力 | スイッチの入力状態を表します。 |
KEY_STATE.edge | Scan_Key関数の結果、スイッチがオフからオンになったことを検知するとHighとなります。edgeはGet_KeyState関数を呼び出すとクリアーします。 |
KEY_STATE.hold | スイッチが長押しされると、その間holdはHightとなります。 |
サンプルコードの解説
チャタリング対策
- Scan_Key関数をDECISION_TIMES回呼び出す間スイッチ入力に同じ状態が続くと、スイッチのオンまたはオフが確定します。
- スイッチがオフからオンに切り替わるタイミングでedgeをHighに、オンからオフに切り替わるタイミングでedgeをLowにします。
- Get_KeyState関数を呼び出すと、現在のedgeとholdの状態を取得します。このときedgeがHighならLowにします。そしてスイッチがオフからオンに切り替わるタイミングでのみedgeはHighになるため、1回のスイッチ操作でedgeをHighにするのは1度のみです。
長押し判定
- Scan_Key関数をHOLD_TIMES回呼び出す間スイッチがオンであり続けたとき、holdをHighにします。
- スイッチがオンであり続ける間、holdはHighを維持します。
- スイッチがオフになるとholdをLowにします。
長押しによる連打
- Scan_Key関数をHOLD_TIMES回呼び出す間スイッチがオンであり続けたとき、edgeとholdをHighにします。
- スイッチがオンであり続ける間、holdはHighを維持します。
- 長押し判定後、Scan_Key関数をBARRAGE_TIMES回呼び出す毎にedgeをHighにします。
- スイッチがオフになるとedgeとholdをLowにします。
サンプルコード
[key.h]
#ifndef KEYH
#define KEYH
#ifdef __cplusplus
extern "C" {
#endif
//------------------------------------------------------------------------------
// include
//------------------------------------------------------------------------------
#include <stdint.h>
//------------------------------------------------------------------------------
// define
//------------------------------------------------------------------------------
//! @enum KEY_LIST
//! @details キーの種類
typedef enum
{
KEY_MENU,
KEY_UP,
KEY_DOWN,
KEY_LEFT,
KEY_RIGHT,
KEY_ENTER,
KEY_CANCEL,
KEY_MAX
} KEY_LIST;
#define KEY_MENU_BIT (1UL << KEY_MENU) //!< メニューキー
#define KEY_UP_BIT (1UL << KEY_UP) //!< 上キー
#define KEY_DOWN_BIT (1UL << KEY_DOWN) //!< 下キー
#define KEY_LEFT_BIT (1UL << KEY_LEFT) //!< 左キー
#define KEY_RIGHT_BIT (1UL << KEY_RIGHT) //!< 右キー
#define KEY_ENTER_BIT (1UL << KEY_ENTER) //!< エンターキー
#define KEY_CANCEL_BIT (1UL << KEY_CANCEL) //!< キャンセルキー
//! @brief キーの判定回数 (チャタリング対策)
//! @details DECISION_TIMES回同じ状態が続くと採用してedgeを立てる
//! 2~255の値を設定する
#define DECISION_TIMES (3U)
//! @brief 長押し判定回数
//! @details HOLD_TIME回押し続けると長押し判定してholdを立てる
//! 0~65535の値を設定する
#define HOLD_TIMES (200U)
//! @brief 長押し連打判定回数
//! @details BARRAGE_NEXT_TIME毎に連打判定してedgeを立てる
//! 0~65535の値を設定する
#define BARRAGE_TIMES (20U)
//! @brief キーの状態
//! @details 条件を満たすと、キーの割り当てに該当するビットに1が立つ
typedef struct
{
//! @brief キーが押された [bit:0=No / 1=Yes]
//! @details キーがOFFからONになると1が立つ @n
//! Get_KeyState関数を呼ぶと0にクリアーする
uint32_t edge;
//! @brief キーが長押しされた [bit:0=No / 1=Yes]
//! @details キーをHOLD_TIMES時間押し続けると1が立つ @n
//! キーを放すと0になる
uint32_t hold;
} KEY_STATE;
//! キーの動作モード
typedef struct
{
//! @brief 長押し連打機能の設定 [bit:0=無効 / 1=有効]
//! @details 長押し後、BARRAGE_TIMES毎にedgeに1が立つ
uint32_t barrage;
} KEY_MODE;
//------------------------------------------------------------------------------
// function
//------------------------------------------------------------------------------
void Set_KeyMode(const KEY_MODE *key_mode);
void Get_KeyState(KEY_STATE *key_state);
void Scan_Key(void);
#ifdef __cplusplus
}
#endif
#endif
[key.c]
/**
* @file key.c
* @brief キー入力処理
*/
//------------------------------------------------------------------------------
// include
//------------------------------------------------------------------------------
#include "key.h"
#include "gpio.h"
//------------------------------------------------------------------------------
// private variable
//------------------------------------------------------------------------------
static KEY_MODE m_key_mode;
//! 現在のキーの状態 [bit:0=Off / 1=On]
static uint32_t m_key;
//! キーが押された [bit:0=No / 1=Yes]
static uint32_t m_key_edge;
//! キーが長押しされた [bit:0=No / 1=Yes]
static uint32_t m_key_hold;
static uint8_t m_key_count;
static uint32_t m_key_fixed;
static uint32_t m_key_old;
//------------------------------------------------------------------------------
// private function
//------------------------------------------------------------------------------
static uint32_t KeyChatter(uint32_t key_port);
static void KeyEdgeHold(uint32_t key);
//static uint32_t Read_KeyMatrix(void);
/** ----------------------------------------------------------------------------
* @brief キーの動作モードを設定する
*
* @param [in] key_mode : 動作モード
*/
void Set_KeyMode(const KEY_MODE *key_mode)
{
m_key_mode = *key_mode;
m_key_edge = 0;
m_key_hold = 0;
m_key_fixed = 0;
m_key_old = 0;
m_key_count = DECISION_TIMES - 2;
}
/** ----------------------------------------------------------------------------
* @brief キーの状態を取得する
*
* @param [out] key_state : キーの状態
*/
void Get_KeyState(KEY_STATE *key_state)
{
key_state->edge = m_key_edge;
key_state->hold = m_key_hold;
// 読みだすとエッジをクリアーする
m_key_edge = 0;
}
/** ----------------------------------------------------------------------------
* @brief キーの確認
* @details 10ms周期ぐらいで呼び出す
*/
void Scan_Key(void)
{
uint32_t port;
uint32_t key;
port = GPIO_ReadPin(GPIOA, GPIO_PIN_All);
port = (uint32_t)(~port) & 0x000000ff;
key = KeyChatter(port);
KeyEdgeHold(key);
m_key = key;
}
/** ----------------------------------------------------------------------------
* @brief キーの状態を確認する
* @details DECISION_TIMES回数同じ状態を維持すれば確定する
*
* @param port : キー入力 [0=OFF / 1=ON]
* @return キーの状態 [0=OFF / 1=ON]
*/
static uint32_t KeyChatter(uint32_t port)
{
if (port == m_key_old) {
if (m_key_count == 0) {
m_key_fixed = port;
} else {
m_key_count--;
}
} else {
m_key_old = port;
m_key_count = DECISION_TIMES - 2;
}
return m_key_fixed;
}
/** ----------------------------------------------------------------------------
* @brief キーの押下・長押しを確認する
* @details HOLD_TIMES回数同じ状態を維持すれば確定する
*
* @param key : キー入力 [0=OFF / 1=ON]
*/
static void KeyEdgeHold(uint32_t key)
{
static uint16_t count = 0;
if (key == 0) {
m_key_edge = 0;
m_key_hold = 0;
count = HOLD_TIMES - 2; // キーの長押しカウント開始
return;
}
// エッジ判定
m_key_edge |= (uint32_t)(~(key & m_key)) & key;
// 前とキーの状態が変わると長押し判定をやり直し
if (key != m_key) {
count = HOLD_TIMES - 1; // キーの長押しカウント開始
m_key_hold = 0;
}
// 長押し・連打判定
if (count == 0) {
m_key_hold = key;
// 連打判定
m_key_edge |= key & m_key_mode.barrage;
count = BARRAGE_TIMES - 1; // キーの連打カウント開始
} else {
count--;
}
}
サンプルコードの使用例
使用例代わりに、CppUTestによるテストコードを掲載しておきます。
#include "CppUTest/TestHarness.h"
#include "CppUTest/TestHarness_c.h"
#include "CppUTestExt/MockSupport_c.h"
#include "gpio.h"
#include "key.h"
static int equalMethod(const void *object1, const void *object2);
static const char *toStringMethod(const void *);
// clang-format off
TEST_GROUP(key){
TEST_SETUP(){
}
TEST_TEARDOWN(){
mock_c()->clear();
}
};
// clang-format on
TEST(key, Test_KeyEdge)
{
KEY_MODE key_mode;
KEY_STATE key_state;
// 連打なし
key_mode.barrage = 0;
Set_KeyMode(&key_mode);
mock_c()->installComparator("GPIO_TypeDef", equalMethod, toStringMethod);
mock_c()
->expectNCalls(DECISION_TIMES, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
// 2回までは反映しない
for (uint8_t i = 0; i < (DECISION_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
}
// 3回目で押下判定するか?
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, KEY_UP_BIT);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
// 押下され続けてもエッジは立たない
mock_c()
->expectOneCall("GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
mock_c()->removeAllComparatorsAndCopiers();
mock_c()->checkExpectations();
}
TEST(key, Test_KeyHold)
{
uint16_t i;
KEY_MODE key_mode;
KEY_STATE key_state;
// 連打なし
key_mode.barrage = 0;
Set_KeyMode(&key_mode);
mock_c()->installComparator("GPIO_TypeDef", equalMethod, toStringMethod);
mock_c()
->expectNCalls(DECISION_TIMES - 1, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
for (i = 0; i < (DECISION_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
}
// 長押し判定開始
mock_c()
->expectNCalls(HOLD_TIMES, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
for (i = 0; i < (HOLD_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
}
// 長押し判定するか?
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
UNSIGNED_LONGS_EQUAL(key_state.hold, KEY_UP_BIT);
// 長押し判定解除の開始
mock_c()
->expectNCalls(DECISION_TIMES, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~0x00);
for (i = 0; i < (DECISION_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
UNSIGNED_LONGS_EQUAL(key_state.hold, KEY_UP_BIT);
}
// 長押し判定を解除するか?
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
mock_c()->removeAllComparatorsAndCopiers();
mock_c()->checkExpectations();
}
TEST(key, Test_KeyBarrage)
{
uint16_t i, n;
KEY_MODE key_mode;
KEY_STATE key_state;
// 連打あり
key_mode.barrage = KEY_UP_BIT | KEY_DOWN_BIT | KEY_LEFT_BIT | KEY_RIGHT_BIT;
Set_KeyMode(&key_mode);
mock_c()->installComparator("GPIO_TypeDef", equalMethod, toStringMethod);
mock_c()
->expectNCalls(DECISION_TIMES - 1, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
for (i = 0; i < (DECISION_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
}
// 長押し判定開始
mock_c()
->expectNCalls(HOLD_TIMES, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
for (i = 0; i < (HOLD_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.hold, 0);
}
// 長押し判定と同時にエッジが立つか?
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, KEY_UP_BIT);
UNSIGNED_LONGS_EQUAL(key_state.hold, KEY_UP_BIT);
// 連打テスト
for (n = 0; n < 5; n++) {
mock_c()
->expectNCalls(BARRAGE_TIMES, "GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", GPIOA)
->withUnsignedIntParameters("gpio_pin", GPIO_PIN_All)
->andReturnUnsignedIntValue(~KEY_UP_BIT);
for (i = 0; i < (BARRAGE_TIMES - 1); i++) {
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, 0);
}
// 連打判定するか?
Scan_Key();
Get_KeyState(&key_state);
UNSIGNED_LONGS_EQUAL(key_state.edge, KEY_UP_BIT);
}
mock_c()->removeAllComparatorsAndCopiers();
mock_c()->checkExpectations();
}
/** ----------------------------------------------------------------------------
* @brief モック関数
*/
void GPIO_WritePin(GPIO_TypeDef *gpio, uint8_t gpio_pin, uint8_t pin_state)
{
mock_c()
->actualCall("GPIO_ReadPort")
->withParameterOfType("GPIO_TypeDef", "gpio", gpio)
->withUnsignedIntParameters("gpio_pin", gpio_pin)
->withUnsignedIntParameters("pin_state", pin_state);
}
uint8_t GPIO_ReadPin(GPIO_TypeDef *gpio, uint8_t gpio_pin)
{
mock_c()
->actualCall("GPIO_ReadPin")
->withParameterOfType("GPIO_TypeDef", "gpio", gpio)
->withUnsignedIntParameters("gpio_pin", gpio_pin);
return mock_c()->unsignedIntReturnValue();
}
static int equalMethod(const void *object1, const void *object2)
{
return object1 == object2;
}
static const char *toStringMethod(const void *)
{
return "string";
}