FreeRTOS理解その9（ストップウォッチ）

# ストップウォッチ
[FreeRTOS理解その8（タイマー）](https://qiita.com/infinite1oop/items/92d2b253a671601a1628)の続きである。ソフトウェアタイマーを使って、ストップウォッチを実現する。後述するが、状態遷移表やセマフォも使うことになり、FreeRTOSの小さな例題としてふさわしいものになっているかもしれない。

## 環境
Arduino IDEでのM5Stack利用。

## 方針
M5Stackには3つのボタンがあるが、あえてボタンを一つだけ使う。ボタンの短押しと長押しを使い分けて実現する。この手のものは最初に状態遷移表（下記）を書いてみるとよい。理解しやすくなるよう、後述するソースコードの関数やマクロも含めた。

|   |初期状態<br>(ST_INIT)|動作中<br>(ST_RUN)|一時停止中<br>(ST_STOP)|
|---|---|---|---|
|<b>短押し<br>(SHORT_PUSH)</b>|スタート<br>(timer_start())<br> <br><i>動作中へ移行</i>|ラップ表示<br>(show_lap())<br> <br><i>動作中のまま</i>|リスタート<br>(timer_start())<br> <br><i>動作中へ移行</i>|
|<b>長押し<br>(LONG_PUSH)</b>|スタート<br>(timer_start())<br> <br><i>動作中へ移行</i>|ストップ<br>(timer_stop())<br> <br><i>一時停止中へ移行</i>|リセット<br>(init_all())<br> <br><i>初期状態へ移行</i>|

また、カウント（経過時間）表示のためのタスクを設けた。この理由については後述する。

## ソースコード
### マクロ
```c++:cmn.h
// For button
#define L_BTN       39
#define CHATTERING  20    //ms
#define LONG_TIME   300   //ms
enum {SHORT_PUSH, LONG_PUSH};

// For state machine
enum {ST_INIT, ST_RUN, ST_STOP};

// For display
// Position
#define X_POS       10
#define X_POS_MIN   X_POS+115
#define X_POS_SEC   X_POS_MIN+70
#define X_POS_MSEC  X_POS_SEC+70
#define Y_POS       10
#define WIDTH_NUM   70
#define HEIGHT_NUM  40
#define Y_LAP_POS   Y_POS+60
#define HEIGHT_LAP  50
// Type
enum {SHOW_MIN, SHOW_SEC, SHOW_MSEC};
enum {SHOW_TIME, SHOW_LAP};
```

- M5Stackの左ボタンのGPIO番号(39)、短押し/長押しのしきい値(300ms)
- 状態の定義
- LCD表示位置および幅＆高さ（トライ＆エラーで決めた）

などを定義。

### グローバル変数などの定義
```c++
#include <M5Stack.h>
#include "cmn.h" // 上記インクルードファイル

uint32_t  start_time, elapsed_time;
uint8_t   lapped_count;
uint32_t  btn_start;
uint8_t   state;

TimerHandle_t     xTimer;
uint8_t           timer_expired;
xTaskHandle       xTask;
SemaphoreHandle_t xSemaphore;
```

- タイマーがスタートした時間、最初にスタートしたときから経過時間
- ラップカウント、ボタン押下用変数、状態
- タスク関連

### LCD表示関数
```c++
void display_count(uint8_t flag, uint8_t type, uint8_t value) {
  char      buf[4];
  uint16_t  x[] = {X_POS_MIN, X_POS_SEC, X_POS_MSEC};
  uint16_t  y = (flag == SHOW_TIME)? (Y_POS): (Y_LAP_POS + (lapped_count - 1) * HEIGHT_LAP);
  sprintf(buf, ((type == SHOW_MSEC)? ("%02d"): ("%02d:")), value);
  M5.Lcd.fillRect(x[type], y, WIDTH_NUM, HEIGHT_NUM, BLACK);
  M5.Lcd.drawString(buf, x[type], y);
}
```

- 状況に応じた表示位置(x,y)の設定
- LCDへの表示（表示位置の塗りつぶし及び数値表示）

### タイムアウト時に呼ばれるコールバック関数とタスク
```c++
void MsecCallback(TimerHandle_t xTim) {
  timer_expired = 1;
}

void Task(void *arg) {
  while (1) {
    if (timer_expired) {
      if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
        uint32_t  current = millis() - start_time + elapsed_time;
        if ((current%(60*1000) >= 0) && (current%(60*1000) < 10)) {
          display_count(SHOW_TIME, SHOW_MIN, (current/(60*1000))%60);
          display_count(SHOW_TIME, SHOW_SEC, (current/1000)%60);
        } else if ((current%1000 >= 0) && (current%1000 < 10)) {
          display_count(SHOW_TIME, SHOW_SEC, (current/1000)%60);
        }
        display_count(SHOW_TIME, SHOW_MSEC, (current%1000)/10);
        timer_expired = 0;
        xSemaphoreGive(xSemaphore);
      }
    }        
    delay(1); //vTaskDelay(1);
  }
}
```

1. タイムアウト時のコールバック関数（timer_expiredフラグをON）

2. カウント（経過時間）表示のためのタスク（timer_expiredフラグがON時）

- timer_expiredフラグは10ミリ秒毎にONとなる（後述）。
- スタートしてからNN分000ミリ秒からNN分009ミリ秒の間であれば、分表示を行う。
- スタートしてからYY秒000ミリ秒からYY秒009ミリ秒の間であれば、秒表示を行う。
- 10ミリ秒表示は常に行う。
- 表示終了後に、timer_expiredフラグをOFF。

10ms未満の誤差があることになる。[xTimerChangePeriod()](https://www.freertos.org/FreeRTOS-timers-xTimerChangePeriod.html)を使って、この誤差を埋めようとしたがうまく行かず断念。そもそも論として、タイムアウト発生から表示までにも僅かな時間が経過する。そこまでの精度は必要ないと判断。

- 本タスクでも後述するラップ表示でもLCD表示を行う。排他制御を実現するため（同時に呼ばれることを防ぐため）、セマフォを用いる。（セマフォを用いない場合の実験結果も後述する。）

### ラップ表示
```c++
void show_lap(void) {
  char      lap[8];
  uint16_t  y = Y_LAP_POS + (lapped_count - 1) * HEIGHT_LAP;
  sprintf(lap, "Lap%d", lapped_count);
  if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
    uint32_t  current = millis() - start_time + elapsed_time;
    uint8_t   v_min = (current/(60*1000))%60;
    uint8_t   v_sec = (current/1000)%60;
    uint8_t   v_msec = (current%1000)/10;    
    M5.Lcd.drawString(lap, X_POS, y);
    display_count(SHOW_LAP, SHOW_MIN, v_min);
    display_count(SHOW_LAP, SHOW_SEC, v_sec);
    display_count(SHOW_LAP, SHOW_MSEC, v_msec);
    xSemaphoreGive(xSemaphore);
  }
  lapped_count++;
}
```

- ラップ回数により表示位置(Y)を変更。
- LCD表示のためにセマフォを利用。

何回もラップするとY方向の表示が足りなくなるが、スクロールなどの対応はしていない。（ここは本題と関係ないところなので勘弁。）

### 状態ごとの関数
```c++
void init_all(void) {
  M5.Lcd.clear(BLACK);
  M5.Lcd.drawString("TIME", X_POS, Y_POS);
  M5.Lcd.drawString("00:", X_POS_MIN, Y_POS);  // min
  M5.Lcd.drawString("00:", X_POS_SEC, Y_POS);  // sec
  M5.Lcd.drawString("00", X_POS_MSEC, Y_POS);  // msec
  timer_expired = 0;
  elapsed_time = 0;
  lapped_count = 1;
  btn_start = 0;
  state = ST_INIT;  
}

void timer_start(void) {
  start_time = millis();
  xTimerStart(xTimer, 0);
  state = ST_RUN;  
}

void timer_stop(void) {
  elapsed_time += millis() - start_time;
  xTimerStop(xTimer, 0);
  state = ST_STOP;   
}
```

- init_all()：LCD表示および変数初期化、状態変数更新（ST_INIT）
- timer_start()：タイマースタート時間の記録、タイマースタート（[xTimerStart()](https://www.freertos.org/FreeRTOS-timers-xTimerStart.html)）、状態変数変更（ST_RUN）
- timer_stop()：経過時間の更新、タイマーストップ（[xTimerStop()](https://www.freertos.org/FreeRTOS-timers-xTimerStop.html)）、状態変数変更（ST_STOP）

### setup()
```c++
void setup() {
  M5.begin();
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);
  init_all();
  pinMode(L_BTN, INPUT);

  xTimer = xTimerCreate("MsecTimer", pdMS_TO_TICKS(10), pdTRUE, (void*)0, MsecCallback); // every 10 ms
  xTaskCreate(&Task, "Task", 1024, NULL, 0, &xTask);

  xSemaphore = xSemaphoreCreateMutex();
  xSemaphoreGive(xSemaphore);
}
```

- LCD関連およびGPIO初期化
- その他初期化（リセット：init_all()）
- タイマー生成（[xTimerCreate()](https://www.freertos.org/FreeRTOS-timers-xTimerCreate.html)）、10ミリ秒毎にコールバック関数が呼ばれる。
- タスク生成（カウント表示用）
- セマフォ生成（カウント表示とラップ表示の排他制御用）

### loop()
```c++
void loop() {
  void (*func[][2])(void) = {{timer_start, timer_start}, {show_lap, timer_stop}, {timer_start, init_all}};
  if (!digitalRead(L_BTN)) {
    if (!btn_start) {
      btn_start = millis();
    }
  } else if (btn_start) {
    uint32_t push_time = millis() - btn_start;
    if (push_time > CHATTERING) {
      (*func[state][((push_time < LONG_TIME)? SHORT_PUSH: LONG_PUSH)])();
    }
    btn_start = 0;
  }
  // display_count() here is blocked by btn-pushing. ★★★
  delay(1); //vTaskDelay(1);
}
```

- 状態遷移表に沿ったコールバック関数の設定
- ボタンの短押し長押しの判断（含むチャタリング防止）
- 状態およびボタンの短押し長押しに従ったコールバック関数の呼び出し

## 実験＆考察
### 正常時（セマフォ使用：上記のコード）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/3e8ad090-8e81-b939-5b02-c45bb1a74ebe.png", width=40%>
成功。

### セマフォ未使用
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/e9549d59-e96b-a5d0-4dd0-de638671553f.png", width=40%>
カウント表示とラップ表示とが競合し、LCDに正しく表示できていない。この後、リセットしてしまう。

### カウント表示のためのタスクを設けない場合
loop()内で「display_count()」（カウント表示）を呼び出せるが、ボタン押下時にカウント表示が進まなくなる。ソースコードから自明。よって、カウント表示用のタスクを設けるべき。