STM32 nucleoを使う (8) 状態遷移表によるSW短押し、長押し判定

はじめに
SWが少ない場合、短押し/長押しで機能を設定することがあります。
状態遷移表を使ったSWの短押し/長押しの実装例です。

#プログラムの内容
周期タイマでSWの状態を読み込み、押されている時間によって短押しと長押しを判別します。
またチャタリング除去としてn回一致の処理を行います。

#チャタリング除去
周期的にSWの状態を読み込み、n回一致でSWの状態を確定します。
SWはONとOFFの1ビットの状態ですので、過去情報としてビット単位で記録します。
n bitのマスク値で過去情報をマスクし、マスク値と一致したらSW ON、0であればSW OFF、どちらでもなければチャタリング発生状態なので前回の値を保持とします。

LSBからSW読み込み値を記録し、マスク値を0x07 （3度読み一致）とした場合の例です。

|SW|過去情報|マスクした値|出力     |
|---|---------|---------|--------------------|
|OFF|0000 0000|0000 0000|OFF|
|ON |0000 0001|0000 0001|OFF (前回の値を保持)|
|ON |0000 0011|0000 0011|OFF (前回の値を保持)|
|ON |0000 0111|0000 0111|ON|
|ON |0000 1111|0000 0111|ON|
|OFF|0001 1110|0000 0110|ON (前回の値を保持)|
|ON |0011 1101|0000 0101|ON (前回の値を保持)|
|OFF|0111 1010|0000 0010|ON (前回の値を保持)|
|OFF|1111 0100|0000 0100|ON (前回の値を保持)|
|OFF|1110 1000|0000 0000|OFF|

```C++
/* SW状態読み込み n回読み一致 */
#define SW_MASK			(0x07u)	/* 3度読み一致 */
typedef enum {
	swStateOff = 0,	/* SW Off */
 	swStateOn,		/* SW On */
} tySwState;

static tySwState swRead(void)
{
	static uint8_t swHistory = 0u;		/* SW 過去情報*/
	static tySwState rslt = swStateOff;	/* SW状態 */
	uint8_t swHistMask;
	GPIO_PinState ptState;

	ptState = HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin);
	swHistory = (swHistory << 1) | ptState;
	swHistMask = swHistory & SW_MASK;
	if (swHistMask == SW_MASK) {
		/* 過去 n回 Hであれば SW OFF */
		rslt = swStateOff;
	} else if (swHistMask == 0u) {
		/* 過去 n回 Lであれば SW ON */
		rslt = swStateOn;
	} else {
		/* その他はSW状態保持 */
	}
	return rslt;
}
```

#短押し/長押し判定
タイマー処理から周期的に判定を行います。4～10ms周期くらいが適当でしょうか。
SWが押されてから離されるまでの時間が、長押し判定時間より短い場合は短押しとします。
SWが押されてから長押し判定時間を超えると長押しとします。
またSWが押された場合と、長押しの後に離された場合の検出もします。


##状態遷移図
状態としてはSW OFF、SW ON、SW HOLDがあります。SW ONはSWが押され続けていて長押し確定前の状態です。SW HOLDは長押し確定後でSWが押され続けている状態です。
SW OFF状態でSWが押されるとタイマーを動かし始めます。SW ON状態でSWが離されると短押し決定です。長押し時間経過すると長押し決定となりSW HOLD状態になります。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68712/a55cff5a-edc8-5cce-21ed-909cc4189eb5.png)

##状態遷移表
状態遷移図から状態遷移表を起こしました。
斜体の〇〇設定は、上位への通知です。
太字・斜体は遷移先です。

|    |*SW OFF* |*SW ON*|*SW HOLD*|
|----|----------|-------|---------|
|ON  |・*SW ON設定*<br>・タイマ開始<br> ***SW ON***|None       |None         |
|OFF |None           |・*短押し設定*<br>・タイマ停止<br>***SW OFF***|・*SW OFF設定*<br>・タイマ停止<br>***SW OFF***
|長押し時間経過|None |・*長押し設定*<br>・タイマ停止<br>***SW HOLD***|None|

##コード
イベントが発生するとモード毎に定義された動作を行い、状態を遷移させます。
動作と遷移先はイベントとモードの2次元配列で定義します。
今回は動作と遷移先を別々の配列で定義します。

```C++
/* SWモード */
typedef enum {
	mdSwOff = 0,	/* SW OFF */
	mdSwOn,			/* SW ON */
	mdSwOnHold,		/* SW HOLD */
	mdSwNoChange,	/* 変化なし */
} tySwMode;

/* SW動作 */
typedef enum {
	actSwOn = 0,	/* SW ON*/
	actSwOff1,		/* SW OFF(短押し)*/
	actSwOff2,		/* SW OFF(HOLD解除)*/
	actSwHold,		/* SW HOLD(長押し)*/
	actSwNoAct,		/* 動作無し*/
} tySwAct;

/* SWモード */
static const tySwMode SwModeTrn[][3] = {
	/* SW OFF		SW ON		 	HOLD */
	{mdSwOn,		mdSwNoChange,	mdSwNoChange},	/* ON */
	{mdSwNoChange,	mdSwOff,		mdSwOff},		/* OFF */
	{mdSwNoChange,	mdSwOnHold,		mdSwNoChange},	/* LONG PUSH TOUT*/
};

/* SW動作 */
static const tySwAct SwModeAct[][3] = {
	/* SW OFF		SW ON		 	HOLD */
	{actSwOn,    	actSwNoAct,		actSwNoAct},	/* ON */
	{actSwNoAct,	actSwOff1,		actSwOff2},		/* OFF */
	{actSwNoAct,	actSwHold,		actSwNoAct},	/* LONG PUSH TOUT*/
};

```

定周期タイマからSW判定処理(SwProc())を呼び出します。判定結果は戻り値で返します。
定周期タイマが動作開始前に初期化処理(SwInit())を呼び出します。

SwProc()ではSW状態の読み取りとSWタイマー処理を行い、結果をEventに変換します。
現在のモードとEventを基にSW動作配列(SwModeAct)からSw動作を取り出し実行します。
次に現在のモードとEventを基に状態遷移配列(SwModeTrn)から、遷移先を取り出します。
遷移先が遷移無し(mdSwNoChange)以外であれば現在のモードを取り出した遷移先に置き換えます。

```C++
/* SW Event */
typedef enum {
	evSwOn = 0,		/* SwOn */
	evSwOff,		/* SwOff */
	evLongPushTime,	/* TimeOut */
	evNoEvent		/* None */
} tySwEvent;

void SwInit(void)
{
	SwTimerCounter = 0;
}

tySwOut SwProc(void)
{
	static tySwMode swMode = mdSwOff;
	tySwState swState;			/* SW状態 */
	char timerOut;				/* Timer結果 */
	tySwEvent swEvent;			/* Event */
	tySwAct actNo;				/* 処理番号 */
	tySwOut swOut;				/* SW出力 */
	tySwMode modeNo;			/* モード番号*/

	/* 入力処理 */
	swState = swRead();			/* SW状態 読み込み*/
	timerOut = swTimerProc();	/* SW Timer処理実行 */

	/* Eventに変換 */
	swEvent = evNoEvent;
	if (swState == swStateOff) {
		swEvent = evSwOff;
	} else if (swState == swStateOn){
		swEvent = evSwOn;
	}
	if (timerOut == TRUE) {
		swEvent = evLongPushTime;
	}

	/* SW動作 */
	actNo = SwModeAct[swEvent][swMode];
	swOut = swActProc(actNo);

	/* 状態遷移 */
	modeNo = SwModeTrn[swEvent][swMode];
	if (modeNo != mdSwNoChange) {
		swMode = modeNo;
	}

	return swOut;
}


/* SW動作 */
static tySwOut swActProc(tySwAct actNo)
{
	tySwOut rslt;

	rslt = outNone;

	switch (actNo) {
	case actSwOn:				/* SW ON*/
		swTimerStart();			/* SW Timer Start */
		rslt = outSwOn;			/* SW ON設定 */
		break;
	case actSwOff1:				/* SW OFF(短押し)*/
		rslt = outSwShortPush;	/* 短押し設定 */
		swTimerStop();			/* SW Timer Stop */
		break;
	case actSwOff2:				/* SW OFF(HOLD解除)*/
		rslt = outSwOff;		/* SW OFF設定 */
		swTimerStop();			/* SW Timer Stop */
		break;
	case actSwHold:				/* SW HOLD(長押し)*/
		rslt = outSwLongPush;	/* 長押し設定 */
		swTimerStop();			/* SW Timer Stop */
		break;
	case actSwNoAct:			/* 動作無し*/
	default:
		break;
	}

	return rslt;
}

static void swTimerStart(void)
{
	SwTimerCounter = LONG_PUSH_TIME;
}

static void swTimerStop(void)
{
	SwTimerCounter = 0;
}

static char swTimerProc(void)
{
	char rslt;

	rslt = FALSE;
	if (SwTimerCounter !=0) {
		SwTimerCounter--;
		if (SwTimerCounter == 0) {
			rslt = TRUE;
		}
	}
	return rslt;
}
```
##メイン処理
タイマー割り込みでSW判定処理(SwProc())を実行し、結果を変数に格納します。
メインループでは変数をチェックし、SW状態に応じた文字列をUARTで送信します。

```C++
/* USER CODE BEGIN PV */
tySwOut SwEvent;
long tm;
/* USER CODE END PV */
/* USER CODE BEGIN 0 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
	if(htim == &htim7) {
		tm++;
		SwEvent = SwProc();
	}
}

void sendMsg(char *msg)
{
	char buff[100];
	int	length;

	sprintf(buff, "%08ld %s\r", tm, msg);
	length = strlen(buff);
	HAL_UART_Transmit(&huart2, (uint8_t *)buff, length, 0xFFFF);
}

/* USER CODE END 0 */
int main(void)
{
  /* USER CODE BEGIN Init */
  SwEvent = outNone;
  tm = 0L;
  SwInit();
  /* USER CODE END Init */

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  if (SwEvent != outNone) {
		  switch (SwEvent) {
		  case outSwOn:				/* SW ON */
			  sendMsg("SW ON");
			  break;
		  case outSwShortPush:		/* 短押し? */
			  sendMsg("Short Push");
			  break;
		  case outSwLongPush:		/* 長押し? */
			  sendMsg("Long Push");
		  break;
		  case outSwOff:			/* SW OFF*/
			  sendMsg("SW OFF");
			  break;
		  case outNone:				/* None */
		  default:
			  break;
		  }
		  SwEvent = outNone;
	  }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
```


STM32の使い方を学習した内容をまとめています。内容に不備等ありましたら連絡お願いします。