ストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化する

##1. はじめに
この記事は[ソフトウェアテストの小ネタ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/software-testing-koneta)の13日目の記事です。小ネタということで身近なテストツールであるストップウォッチにフォーカスします。

ストップウォッチといえば開発者、テストエンジニア、QA問わず誰もが使い方を知っていて1,000円出せば1/100秒計測の機種が買えるとってもありふれた存在ですが一方で不便なところもあります。

1. 操作が手作業で自動化できない
2. 値の転記ミスのおそれが付いて回る
2. 計測時間をRS-232CやUSBで自動で読み出せるモデルが意外とない

転記ミスもさることながらソフトウェアの改修のたびに同じチェックを繰り返すような場面で自動化できないのはとっても痛いです。また、スポーツのトレーニング向けの機種でPCにデータを転送できるものがありますが手動による転送のためニーズに合いません。そこで自動化システムに組み込めるようなコマンド操作のストップウォッチとデモを兼ねたテストランナーをサクッと作ります。

![ArduinoUnoStopWatch_topview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/85111ed4-e173-9af5-1105-2ef3ce0dcdfc.jpeg "ツール全景")

ストップウォッチのプログラムは割り込みを使わずポーリングでぐるぐる回しているだけだし回路設計の計算も中学校で習うオームの法則だけ。テストランナーはテスト実行からデータの整理、グラフ作成までをこれ一本でできるとっても身近な統合開発環境 "Excel" で実装します。品質技術開発の敷居は決して高くないので自分も始めてみようかなと思っていただけたら幸いです。

##2. 目標
数秒以下(ミリ秒オーダー)の計測はオシロスコープ(以下、オシロ)で行い、長時間の計測はテスト実行ログのタイムスタンプを使うこととし、オシロと実行ログの補完を目指します。

- 利用シーンとして、ストップウォッチで測定を行うような数秒～数分程度の測定を想定します。
  - Arduino Uno Rev.3のシステムクロック[CSTCE16M0V53-R0](https://www.murata.com/ja-jp/products/productdetail?partno=CSTCE16M0V53-R0)の周波数許容偏差は±0.50%以内です。±0.50%というと1秒に対して±5ミリ秒、200秒に対して±1秒、1時間に対して±18秒の誤差に相当しますので、この誤差を許容できるような用途でご使用ください。
  - CI/CDや回帰テストに本システムを組み込んで処理時間がやけに長い(短い)といった異常をスクリーニングし、異常を検出したら改めて精密に調べるような使い方が向いていると思います。
- 操作手段として、1. スイッチ押下、2. テスト対象の機器を線出しして接続、3. UARTのコマンド操作、の3種類を想定します。
- ストップウォッチの計測時間はUARTに出力します[^1]。

また、小ネタ縛りで以下の制約を入れます。

- はんだ付けはしない
- プログラムは割り込みを使わない

##3. ストップウォッチの製作
[Arduino](https://www.arduino.cc/)の組み込み関数でミリ秒単位のカウントを行う[millis()](https://www.arduino.cc/reference/en/language/functions/time/millis/)を利用します。2つの変数time_start、time_endを用意し、計測開始時にtime_startへ、計測終了時にtime_endへmillis()の値をストアし、time_endとtime_startの差分から経過時間を求めます。

###3.1 部品表

|部品|数量|
|----|----|
|Arduino UNO Rev.3|1個|
|ブレッドボード|1枚|
|タクタイルスイッチ|3個|
|LED(赤色)|1個|
|LED(黄色)|1個|
|3kΩのカーボン抵抗|2個|
|ブレッドボード用ジャンパー線|6本|
|ブレッドボード用ジャンパーワイヤー|適宜|

###3.2 実体配線図
 [Fritzing](https://fritzing.org/home/)で描いた実体配線図を以下に示します。
![ArduinoUnoStopWatch_breadboard_m.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/a0768658-41f4-f91e-6906-d36275688df7.png "実体配線図")

|Digital Pin #|Pin Mode|役割|
|----|----|----
| 2 |INPUT_PULLUP|計測開始スイッチ
| 3 |INPUT_PULLUP|計測終了スイッチ
| 4 |INPUT_PULLUP|経過時間出力スイッチ
| 5 |OUTPUT|計測開始を示すインジケータ(黄色LED)
| 6 |OUTPUT|計測終了を示すインジケータ(赤色LED)

Digital Pin #2～#4の入力ポートのプルアップはAVRマイコンの機能を利用します。テスト対象の機器を線出しして接続する場合はプルアップの設定や電圧レベルは適宜合わせてください。LEDは極性があり足の長い方がアノード(+側)、短い方がカソード(-側)です。LEDの足をニッパーで切るときはアノード側をちょっと長くしておくと極性を迷わずに済みます。

###3.3 LEDの電流制限抵抗の選定
ArduinoのDigital Pinの出力電圧は5V、LEDの順方向電圧Vfをざっくり2Vとすると電流制限抵抗Rの両端の電圧Vrは3Vとなります。LEDに流す電流Iを1mAとし、オームの法則から電流制限抵抗の値を求めます。

![ArduinoUnoStopWatch_LED.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/90ec4a8f-1c15-3a0d-7dc3-24f1470bccbe.png "LEDの電流制限回路")

$オームの法則：V = RI$
$オームの法則を変形：R = V / I$
$ここでVr=3V、I = 0.001Aとすると$
$R = 3/0.001$
$  =3000Ω$
$  =3kΩ$

3kΩは許容差5%の[E24系列](https://ja.wikipedia.org/wiki/標準数#JIS_C_60063の標準数列)にちょうどありますので3kΩを使用します。

併せて抵抗Rが消費する電力Prも確認します。

$Pr = Vr * I$
$Vr=3V、I=0.001Aとすると$
$Pr=3 * 0.001$
$  = 0.003W$
$  = 3mW$

3kΩ(橙黒赤金)、誤差5%の一般的なカーボン抵抗で1/4Wや1/6Wなど入手しやすいものを用意すればOKです。なお、今時のLEDは少ない電流でも明るく光るのでE12系列の3.3kΩを選定して電流をもうちょっと絞ってもよいかもしれません。

###3.4 プログラム

|操作|動作|
|----|----|
|・計測開始スイッチの押下<br>・UARTコマンド"s"の受信|・変数time_startへmillis()の値をストア<br>・計測開始インジケータを点灯<br>・ストアした値をUARTへ出力
|・計測終了スイッチの押下<br>・UARTコマンド"e"の受信|・変数time_endへmillis()の値をストア<br>・計測終了インジケータを点灯<br>・ストアした値をUARTへ出力
|・経過時間出力スイッチの押下<br>・UARTコマンド"d"の受信|・経過時間を計算しUARTへ出力<br>・計測開始インジケータと計測終了インジケータを消灯
|・UARTコマンド"?"の受信|・ヘルプメッセージをUARTへ出力

- UARTの通信速度は115200bpsを設定しています。
- スイッチのチャタリング除去として5msの待ち時間を設けています[^2]。
- 筆者が使用しているArduino IDEはVer 1.6.4です[^3]。

``` c:ArduinoUnoStopWatchV2.ino
/***********************************************************************
 * Copyright 2019 ka's@pbjpkas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***********************************************************************/

/* pin assign */
#define PUSH_SW_START      2
#define PUSH_SW_END        3
#define PUSH_SW_RESULT     4
#define LED_START          5
#define LED_END            6

/* push switch anti-chattering delay */
#define ANTI_CHATTER_DELAY 5 //msec

void measure_with_stopwatch(unsigned long *timestamp, int led)
{
    *timestamp = millis();
    digitalWrite(led, HIGH);
    Serial.println(*timestamp);
}

unsigned long calc_duration(unsigned long *time_start, unsigned long *time_end)
{
    unsigned long duration = 0;
    
    if(*time_start <= *time_end)
    {
        duration = *time_end - *time_start;
    }
    else
    {
        duration = ( 0xffffffff - *time_start ) + *time_end;
    }
    
    return duration;
}

void display_result(unsigned long *time_start, unsigned long *time_end)
{
    unsigned long duration = 0;
    digitalWrite(LED_START, LOW);
    digitalWrite(LED_END,   LOW);
    duration = calc_duration(time_start, time_end);
    Serial.println(duration);
}

void print_help(void)
{
    Serial.print( F("This is ") );
    Serial.print( F(__FILE__) );
    Serial.print( F(" ") );
    Serial.print( F("Build at ") );
    Serial.print( F(__DATE__) );
    Serial.print( F(" ") );
    Serial.print( F(__TIME__) );
    Serial.print( F("\r\n") );
    Serial.print( F("s : Start measurement\r\n") );
    Serial.print( F("e : End of measurement\r\n") );
    Serial.print( F("d : Display of measurement results\r\n") );
}

void setup()
{
    pinMode(PUSH_SW_START,  INPUT_PULLUP);
    pinMode(PUSH_SW_END,    INPUT_PULLUP);
    pinMode(PUSH_SW_RESULT, INPUT_PULLUP);
    pinMode(LED_START,      OUTPUT);
    pinMode(LED_END,        OUTPUT);
    digitalWrite(LED_START, LOW);
    digitalWrite(LED_END,   LOW);
    Serial.begin(115200);
}

void loop()
{
    unsigned long time_start = 0;
    unsigned long time_end   = 0;

    int port_state_start  = HIGH;
    int port_state_end    = HIGH;
    int port_state_result = HIGH;
    int port_state;
    char buf;

    while(1)
    {
        port_state = digitalRead(PUSH_SW_START);
        if(port_state_start != port_state)
        {
            if(port_state == LOW)
            {
                measure_with_stopwatch(&time_start, LED_START);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_start = port_state;
        }
        
        port_state = digitalRead(PUSH_SW_END);
        if(port_state_end != port_state)
        {
            if(port_state == LOW)
            {
                measure_with_stopwatch(&time_end, LED_END);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_end = port_state;
        }
    
        port_state = digitalRead(PUSH_SW_RESULT);
        if(port_state_result != port_state)
        {
            if(port_state == LOW)
            {
                display_result(&time_start, &time_end);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_result = port_state;
        }
    
        if(Serial.available())
        {
            buf = Serial.read();
            if(buf == 's')
            {
                measure_with_stopwatch(&time_start, LED_START);
            }
            if(buf == 'e')
            {
                measure_with_stopwatch(&time_end, LED_END);
            }
            if(buf == 'd')
            {
                display_result(&time_start, &time_end);
            }
            if(buf == '?')
            {
                print_help();
            }
        }
    }
}
```

``` txt:ビルドメッセージ
(略)
スケッチが プログラムストレージ領域の 3,530バイト (10%) を使用しています。最大は 32,256バイト です。
グローバル変数が 182バイト (8%) の 動的メモリを使用しており、ローカル変数に 1,866 バイトが残っています。最高は 2,048バイトです。
```
Arduino UNO Rev.3に載っているAVRマイコン[ATmega328P](http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf)はFlash ROM 32KB、RAM 2KBですがプログラムがコンパクトなのでメモリ空間がとっても広く感じますね。

###3.5 動作確認
オシロ([RIGOL](https://jp.rigolna.com)・[DS1104Z](https://jp.rigolna.com/products/digital-oscilloscopes/1000z/)、100MHz/4ch)で動作確認を行いました。

![inspection_s.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/53b360ca-1ab3-5faa-2373-10f8f86133d5.jpeg "テストベンチ")

####3.5.1 計測開始スイッチの押下からLEDが点灯するまでの遅延時間
オシロのCH1はDigital Pin #2(計測開始スイッチの入力ポート)、CH2はDigital Pin #5(計測開始を示すインジケータLED)です。スイッチ押下からLED点灯までの遅延時間を数十回観測したところ約10μs～約25μsでした。変数time_startへmillis()の値をストアするのはLED点灯よりも前のため、スイッチ押下から遅くとも約25μs以内に値がストアされます。ストップウォッチの用途としては十分な速さです。

![スイッチ押下の応答時間測定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/18690f0d-edd9-f19f-6800-6d633b7b4974.png "スイッチ押下の応答時間測定")

####3.5.2 チャタリング
こちらはオシロのCH1はDigital Pin #2(計測開始スイッチの入力ポート)、CH2はDigital Pin #3(計測終了スイッチの入力ポート)です。予想に反しチャタリングは見られませんでした。このタクタイルスイッチを使う場合は5ミリ秒のdelay()で十分そうです。

![計測開始スイッチのチャタリング](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/79d45be3-3f1f-ab17-678c-22d526115b22.png "計測開始スイッチのチャタリング")
![計測終了スイッチのチャタリング](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/57b72f09-1f65-5df8-7c97-3b0d89dd2ae1.png "計測終了スイッチのチャタリング")

####3.5.3 計測時間
1ミリ秒から120秒まで時間を変えながら約130点の測定を行いオシロと比較しました。

![オシロの測定例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/6aef91d8-c56c-6d02-4817-9f7981255a6c.png "オシロの測定例")

![ds1104z-ArduinoUno_V2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/1b4e47db-0002-f751-1b72-27a1950c8274.png)

オシロの測定値に対してオシロとArduino Unoの測定値のずれがどれくらいの割合となるかを図示したのが以下のグラフです。1点目(500.0%)、2点目(177.78%)、3点目(87.50%)、5点目(6.67%)はグラフからはみ出していてプロットされていません。N=11(計測時間としては約40ms)以降±1%、N=30(計測時間としては約200ms)以降±0.5%に収まっていてストップウォッチの代替として十分と思います。

![ds1104z-ArduinoUno-gosa_V2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/225c83d0-59ab-a3dd-e593-9ecb5b88bc90.png)

<details><summary>測定結果表</summary><div>

2019/12/15 Ver. 2版

| N   | DS1104Z | Arduino Uno | 差分    | 単位 | DS1104Zの測定値に対する差分の割合 |
|-----|---------|-------------|---------|------|-----------------------------------|
| 1   | 0.0010  | 0.006       | 0.0050  | [s]  | 500.00%                           |
| 2   | 0.0018  | 0.005       | 0.0032  | [s]  | 177.78%                           |
| 3   | 0.0032  | 0.006       | 0.0028  | [s]  | 87.50%                            |
| 4   | 0.0048  | 0.005       | 0.0002  | [s]  | 4.17%                             |
| 5   | 0.0075  | 0.008       | 0.0005  | [s]  | 6.67%                             |
| 6   | 0.0094  | 0.009       | -0.0004 | [s]  | -4.26%                            |
| 7   | 0.0141  | 0.014       | -0.0001 | [s]  | -0.71%                            |
| 8   | 0.0192  | 0.019       | -0.0002 | [s]  | -1.04%                            |
| 9   | 0.0243  | 0.025       | 0.0007  | [s]  | 2.88%                             |
| 10  | 0.0294  | 0.029       | -0.0004 | [s]  | -1.19%                            |
| 11  | 0.0379  | 0.038       | 0.0001  | [s]  | 0.26%                             |
| 12  | 0.0392  | 0.039       | -0.0002 | [s]  | -0.51%                            |
| 13  | 0.0454  | 0.045       | -0.0004 | [s]  | -0.88%                            |
| 14  | 0.0564  | 0.056       | -0.0004 | [s]  | -0.71%                            |
| 15  | 0.0636  | 0.064       | 0.0004  | [s]  | 0.63%                             |
| 16  | 0.0684  | 0.068       | -0.0004 | [s]  | -0.58%                            |
| 17  | 0.0804  | 0.080       | -0.0004 | [s]  | -0.50%                            |
| 18  | 0.0884  | 0.089       | 0.0006  | [s]  | 0.68%                             |
| 19  | 0.0766  | 0.077       | 0.0004  | [s]  | 0.52%                             |
| 20  | 0.0912  | 0.091       | -0.0002 | [s]  | -0.22%                            |
| 21  | 0.0952  | 0.095       | -0.0002 | [s]  | -0.21%                            |
| 22  | 0.1004  | 0.100       | -0.0004 | [s]  | -0.40%                            |
| 23  | 0.1056  | 0.106       | 0.0004  | [s]  | 0.38%                             |
| 24  | 0.1128  | 0.112       | -0.0008 | [s]  | -0.71%                            |
| 25  | 0.1240  | 0.124       | 0.0000  | [s]  | 0.00%                             |
| 26  | 0.1352  | 0.134       | -0.0012 | [s]  | -0.89%                            |
| 27  | 0.1372  | 0.137       | -0.0002 | [s]  | -0.15%                            |
| 28  | 0.1496  | 0.149       | -0.0006 | [s]  | -0.40%                            |
| 29  | 0.1726  | 0.173       | 0.0004  | [s]  | 0.23%                             |
| 30  | 0.1924  | 0.192       | -0.0004 | [s]  | -0.21%                            |
| 31  | 0.2010  | 0.201       | 0.0000  | [s]  | 0.00%                             |
| 32  | 0.2320  | 0.232       | 0.0000  | [s]  | 0.00%                             |
| 33  | 0.2680  | 0.268       | 0.0000  | [s]  | 0.00%                             |
| 34  | 0.2920  | 0.293       | 0.0010  | [s]  | 0.34%                             |
| 35  | 0.3220  | 0.321       | -0.0010 | [s]  | -0.31%                            |
| 36  | 0.3370  | 0.336       | -0.0010 | [s]  | -0.30%                            |
| 37  | 0.3500  | 0.349       | -0.0010 | [s]  | -0.29%                            |
| 38  | 0.3730  | 0.373       | 0.0000  | [s]  | 0.00%                             |
| 39  | 0.4030  | 0.402       | -0.0010 | [s]  | -0.25%                            |
| 40  | 0.4320  | 0.431       | -0.0010 | [s]  | -0.23%                            |
| 41  | 0.4590  | 0.458       | -0.0010 | [s]  | -0.22%                            |
| 42  | 0.4800  | 0.480       | 0.0000  | [s]  | 0.00%                             |
| 43  | 0.4940  | 0.494       | 0.0000  | [s]  | 0.00%                             |
| 44  | 0.5400  | 0.539       | -0.0010 | [s]  | -0.19%                            |
| 45  | 0.5780  | 0.579       | 0.0010  | [s]  | 0.17%                             |
| 46  | 0.6340  | 0.634       | 0.0000  | [s]  | 0.00%                             |
| 47  | 0.6720  | 0.671       | -0.0010 | [s]  | -0.15%                            |
| 48  | 0.7400  | 0.739       | -0.0010 | [s]  | -0.14%                            |
| 49  | 0.7740  | 0.772       | -0.0020 | [s]  | -0.26%                            |
| 50  | 0.8380  | 0.837       | -0.0010 | [s]  | -0.12%                            |
| 51  | 0.9100  | 0.908       | -0.0020 | [s]  | -0.22%                            |
| 52  | 0.9800  | 0.980       | 0.0000  | [s]  | 0.00%                             |
| 53  | 1.034   | 1.034       | 0.000   | [s]  | 0.00%                             |
| 54  | 1.066   | 1.065       | -0.001  | [s]  | -0.09%                            |
| 55  | 1.050   | 1.048       | -0.002  | [s]  | -0.19%                            |
| 56  | 1.074   | 1.073       | -0.001  | [s]  | -0.09%                            |
| 57  | 1.094   | 1.092       | -0.002  | [s]  | -0.18%                            |
| 58  | 1.138   | 1.139       | 0.001   | [s]  | 0.09%                             |
| 59  | 1.166   | 1.164       | -0.002  | [s]  | -0.17%                            |
| 60  | 1.204   | 1.203       | -0.001  | [s]  | -0.08%                            |
| 61  | 1.246   | 1.241       | -0.005  | [s]  | -0.40%                            |
| 62  | 1.250   | 1.249       | -0.001  | [s]  | -0.08%                            |
| 63  | 1.274   | 1.273       | -0.001  | [s]  | -0.08%                            |
| 64  | 1.306   | 1.303       | -0.003  | [s]  | -0.23%                            |
| 65  | 1.358   | 1.358       | 0.000   | [s]  | 0.00%                             |
| 66  | 1.390   | 1.389       | -0.001  | [s]  | -0.07%                            |
| 67  | 1.418   | 1.416       | -0.002  | [s]  | -0.14%                            |
| 68  | 1.574   | 1.574       | 0.000   | [s]  | 0.00%                             |
| 69  | 1.670   | 1.670       | 0.000   | [s]  | 0.00%                             |
| 70  | 1.766   | 1.766       | 0.000   | [s]  | 0.00%                             |
| 71  | 1.886   | 1.882       | -0.004  | [s]  | -0.21%                            |
| 72  | 1.974   | 1.971       | -0.003  | [s]  | -0.15%                            |
| 73  | 2.006   | 2.008       | 0.002   | [s]  | 0.10%                             |
| 74  | 2.166   | 2.168       | 0.002   | [s]  | 0.09%                             |
| 75  | 2.625   | 2.618       | -0.007  | [s]  | -0.27%                            |
| 76  | 2.885   | 2.877       | -0.008  | [s]  | -0.28%                            |
| 77  | 3.185   | 3.185       | 0.000   | [s]  | 0.00%                             |
| 78  | 3.435   | 3.431       | -0.004  | [s]  | -0.12%                            |
| 79  | 3.845   | 3.837       | -0.008  | [s]  | -0.21%                            |
| 80  | 3.875   | 3.865       | -0.010  | [s]  | -0.26%                            |
| 81  | 4.125   | 4.124       | -0.001  | [s]  | -0.02%                            |
| 82  | 4.405   | 4.403       | -0.002  | [s]  | -0.05%                            |
| 83  | 4.685   | 4.685       | 0.000   | [s]  | 0.00%                             |
| 84  | 5.025   | 5.024       | -0.001  | [s]  | -0.02%                            |
| 85  | 5.410   | 5.412       | 0.002   | [s]  | 0.04%                             |
| 86  | 5.910   | 5.902       | -0.008  | [s]  | -0.14%                            |
| 87  | 6.250   | 6.243       | -0.007  | [s]  | -0.11%                            |
| 88  | 6.750   | 6.760       | 0.010   | [s]  | 0.15%                             |
| 89  | 7.000   | 6.990       | -0.010  | [s]  | -0.14%                            |
| 90  | 7.510   | 7.492       | -0.018  | [s]  | -0.24%                            |
| 91  | 7.910   | 7.897       | -0.013  | [s]  | -0.16%                            |
| 92  | 8.410   | 8.409       | -0.001  | [s]  | -0.01%                            |
| 93  | 9.030   | 9.017       | -0.013  | [s]  | -0.14%                            |
| 94  | 9.630   | 9.624       | -0.006  | [s]  | -0.06%                            |
| 95  | 10.01   | 9.992       | -0.018  | [s]  | -0.18%                            |
| 96  | 10.09   | 10.117      | 0.027   | [s]  | 0.27%                             |
| 97  | 10.65   | 10.662      | 0.012   | [s]  | 0.11%                             |
| 98  | 10.90   | 10.873      | -0.027  | [s]  | -0.25%                            |
| 99  | 11.54   | 11.492      | -0.048  | [s]  | -0.42%                            |
| 100 | 12.06   | 12.059      | -0.001  | [s]  | -0.01%                            |
| 101 | 12.94   | 12.922      | -0.018  | [s]  | -0.14%                            |
| 102 | 14.06   | 14.021      | -0.039  | [s]  | -0.28%                            |
| 103 | 15.10   | 15.054      | -0.046  | [s]  | -0.30%                            |
| 104 | 16.10   | 16.071      | -0.029  | [s]  | -0.18%                            |
| 105 | 16.94   | 16.938      | -0.002  | [s]  | -0.01%                            |
| 106 | 18.14   | 18.140      | 0.000   | [s]  | 0.00%                             |
| 107 | 19.06   | 19.048      | -0.012  | [s]  | -0.06%                            |
| 108 | 20.02   | 20.030      | 0.010   | [s]  | 0.05%                             |
| 109 | 25.15   | 25.120      | -0.030  | [s]  | -0.12%                            |
| 110 | 30.05   | 30.049      | -0.001  | [s]  | 0.00%                             |
| 111 | 34.85   | 34.818      | -0.032  | [s]  | -0.09%                            |
| 112 | 40.05   | 40.040      | -0.010  | [s]  | -0.02%                            |
| 113 | 45.05   | 45.031      | -0.019  | [s]  | -0.04%                            |
| 114 | 49.75   | 49.764      | 0.014   | [s]  | 0.03%                             |
| 115 | 54.65   | 54.643      | -0.007  | [s]  | -0.01%                            |
| 116 | 60.40   | 60.346      | -0.054  | [s]  | -0.09%                            |
| 117 | 64.30   | 64.213      | -0.087  | [s]  | -0.14%                            |
| 118 | 70.10   | 70.046      | -0.054  | [s]  | -0.08%                            |
| 119 | 75.20   | 75.108      | -0.092  | [s]  | -0.12%                            |
| 120 | 79.90   | 79.713      | -0.187  | [s]  | -0.23%                            |
| 121 | 85.50   | 85.480      | -0.020  | [s]  | -0.02%                            |
| 122 | 90.10   | 90.136      | 0.036   | [s]  | 0.04%                             |
| 123 | 95.10   | 95.087      | -0.013  | [s]  | -0.01%                            |
| 124 | 99.90   | 99.913      | 0.013   | [s]  | 0.01%                             |
| 125 | 103.7   | 103.705     | 0.005   | [s]  | 0.00%                             |
| 126 | 108.9   | 108.664     | -0.236  | [s]  | -0.22%                            |
| 127 | 118.3   | 118.376     | 0.076   | [s]  | 0.06%                             |

</div></details>

###3.6 基本的な使い方

1. 計測開始スイッチを押下する
2. 計測終了スイッチを押下する
3. UARTコマンド"d"を送信し戻り値(=経過時間)を受信する

※時間はミリ秒表記です。
![example-of-use_v2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/5d96c511-2fc7-6ad9-458d-8b6a94075509.png)


##4. テストランナーの製作
テストツール(ストップウォッチ)をコマンドで操作してテスト結果を取得し、整理(最大値、最小値、平均値、中央値、標準偏差を求める)し、グラフを描くといった性能テストの一連のアクティビティを自動化するExcel VBAのサンプルです。[Raspberry Pi、Jenkins、Pythonでテストランナーを作って](https://qiita.com/pbjpkas/items/c33c6c55a927eccc4d85)テストウェアをGitHubやSubversionで管理するのは今後のネタに取っておき、まずはExcelでスモールスタートします。

今回は5回の測定を行って結果の整理とグラフ作成を行うテストランナーを製作します[^4]。

![excel-vba-demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221758/58a51abb-9529-e322-cb8f-ed73869c1f92.png "テストランナーのサンプル")

|ボタン|動作|
|----|----|
|Start|・コマンド"s"を送信し戻り値をアクティブセルに格納する<br>・アクティブセルを右に1つ移動する<br>(SetStart()を登録しています)|
|End|・コマンド"e"を送信し戻り値をアクティブセルに格納する<br>・アクティブセルを右に1つ移動する<br>(SetEnd()を登録しています)|
|Duration|・コマンド"d"を送信し戻り値をアクティブセルに格納する<br>・アクティブセルを左に2つ、下に1つ移動する<br>(GetDuration()を登録しています)|

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">5回の測定を行って結果の整理とグラフ作成を行う自動テストのデモ動画です。 <a href="https://t.co/UG9FI6FGwp">pic.twitter.com/UG9FI6FGwp</a></p>&mdash; ka’s (@pbjpkas) <a href="https://twitter.com/pbjpkas/status/1205146459919663104?ref_src=twsrc%5Etfw">December 12, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

```vb:ArduinoStopWatchTestRunnerV2.xlsx
Option Explicit

Private Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long)

Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" _
   (ByVal lpFileName As String, _
    ByVal dwDesiredAccess As Long, _
    ByVal dwShareMode As Long, _
    ByVal lpSecurityAttributes As Long, _
    ByVal dwCreationDisposition As Long, _
    ByVal dwFlagsAndAttributes As Long, _
    ByVal hTemplateFile As Long) As Long

Private Declare Sub CloseHandle Lib "kernel32" _
   (ByVal handle As Long)

Private Declare Sub ReadFile Lib "kernel32" _
   (ByVal hFile As Long, _
    lpBuffer As Any, _
    ByVal nNumberOfBytesToRead As Long, _
          lpNumberOfBytesRead As Long, _
    ByVal lpOverlapped As Long)

Private Declare Sub WriteFile Lib "kernel32" _
   (ByVal hFile As Long, _
    lpBuffer As Any, _
    ByVal nNumberOfBytesToWrite As Long, _
          lpNumberOfBytesWritten As Long, _
    ByVal lpOverlapped As Long)

'SetCommState
Private Type DCB
    DCBlength As Long
    BaudRate As Long
    Fields As Long
    wReserved1 As Integer
    XonLim As Integer
    XoffLim As Integer
    ByteSize As Byte
    Parity As Byte
    StopBits As Byte
    XonChar As Byte
    XoffChar As Byte
    ErrorChar As Byte
    EofChar As Byte
    EvtChar As Byte
    wReserved2 As Integer
End Type
Private Declare Function SetCommState Lib "kernel32" _
   (ByVal hFile As Long, _
    ByRef lpDCB As DCB) As Long
Private Declare Function GetCommState Lib "kernel32" _
   (ByVal hFile As Long, _
    ByRef lpDCB As DCB) As Long

'SetCommTimeouts
Private Type COMMTIMEOUTS
    ReadIntervalTimeout As Long
    ReadTotalTimeoutMultiplier As Long
    ReadTotalTimeoutConstant As Long
    WriteTotalTimeoutMultiplier As Long
    WriteTotalTimeoutConstant As Long
End Type
Private Declare Sub SetCommTimeouts Lib "kernel32" _
   (ByVal hFile As Long, _
          lpCommTimeouts As COMMTIMEOUTS)

'SetupComm
Private Declare Function SetupComm Lib "kernel32" _
   (ByVal hFile As Long, _
    ByVal dwInQueue As Long, _
    ByVal dwOutQueue As Long) As Long

'PurgeComm
Private Declare Function PurgeComm Lib "kernel32" _
   (ByVal hFile As Long, _
    ByVal dwFlags As Long) As Long

'CreateFile
Const GENERIC_READ = (&H80000000)
Const GENERIC_WRITE = (&H40000000)
Const OPEN_EXISTING = 3

'PurgeComm
Const PURGE_TXABORT As Long = &H1
Const PURGE_RXABORT As Long = &H2
Const PURGE_TXCLEAR As Long = &H4
Const PURGE_RXCLEAR As Long = &H8

'Settings
Const COM_PORT = "C4"

Function ArduinoStopWatchReadValue(strCmd As String)
    Dim comPort As String
    Dim hFile As Long
    Dim cs As DCB 'CommState
    Dim ct As COMMTIMEOUTS 'CommTimeouts
    Dim length As Long
    Dim txBuf() As Byte
    Dim rxBuf() As Byte
    Dim i As Long
    Dim char As String
    Dim rxStr As String
    Dim asciiCode As Integer
    Dim strLength As Long

    'COM port open
    comPort = ActiveSheet.Range(COM_PORT)
    hFile = CreateFile(comPort, GENERIC_READ Or GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)

    If hFile = -1 Then
        MsgBox ("Can't Open " & comPort)
        Exit Function
    End If

    On Error GoTo hClose

    'RS232C通信設定
    GetCommState hFile, cs
    cs.BaudRate = 115200
    cs.ByteSize = 8
    cs.Parity = 0
    cs.StopBits = 0
    cs.Fields = 0 '&H11 'To avoid reset M5Stack at begining serial-communicaton, cs.Fields = &H11
    SetCommState hFile, cs

    'RS232Cタイムアウト設定
    ct.ReadIntervalTimeout = 10
    ct.ReadTotalTimeoutMultiplier = 0
    ct.ReadTotalTimeoutConstant = 500
    ct.WriteTotalTimeoutMultiplier = 0
    ct.WriteTotalTimeoutConstant = 500
    SetCommTimeouts hFile, ct
    
    SetupComm hFile, 256, 256
    PurgeComm hFile, PURGE_TXCLEAR
    PurgeComm hFile, PURGE_RXCLEAR
    
    strLength = Len(strCmd)
    ReDim txBuf(strLength)
    
    For i = 0 To strLength - 1
        char = Mid(strCmd, i + 1, 1)
        asciiCode = Asc(char)
        txBuf(i) = CByte(asciiCode)
        'Debug.Print Chr(txBuf(i))
    Next
    txBuf(i) = &HD 'add 0x0D(CR)
    
    WriteFile hFile, txBuf(0), strLength + 1, length, 0
    Sleep 1 'To avoid reset M5Stack
    
    ReDim rxBuf(256)
    ReadFile hFile, rxBuf(0), 256, length, 0
    For i = 0 To 255
        If rxBuf(i) = &HD Or rxBuf(i) = &HA Then
            Exit For
        Else
            rxStr = rxStr + Chr(rxBuf(i))
        End If
    Next
    
    ActiveCell.Value = CDbl(rxStr) / 1000# '2019/12/14 update
    
    CloseHandle hFile
    
    Exit Function
    
hClose:
    CloseHandle hFile

End Function

Sub SetStart()
    Call ArduinoStopWatchReadValue("s")
    ActiveCell.Offset(0, 1).Select
End Sub

Sub SetEnd()
    Call ArduinoStopWatchReadValue("e")
    ActiveCell.Offset(0, 1).Select
End Sub

Sub GetDuration()
    Call ArduinoStopWatchReadValue("d")
    ActiveCell.Offset(1, -2).Select
End Sub
```

##5. ステップ数
[かぞえチャオ！Ver.1.68](http://ciao-ware.c.ooco.jp/)でステップ数を数えました(ファイル名はかぞえチャオで開けるよう適当に変更しています)。

| パス名                            | モジュール名                       | 種類 | 総ステップ数 | 実ステップ数 | コメント[％] |
|-----------------------------------|------------------------------------|------|--------------|--------------|--------------|
| ArduinoUnoStopWatch.c             | モジュール外                       | c    | 26           | 6            | 81.4         |
| ArduinoUnoStopWatch.c             | void measure_with_stopwatch        | c    | 7            | 6            | 0            |
| ArduinoUnoStopWatch.c             | double calc_duration               | c    | 16           | 13           | 0            |
| ArduinoUnoStopWatch.c             | void display_result                | c    | 9            | 8            | 0            |
| ArduinoUnoStopWatch.c             | void print_help                    | c    | 15           | 14           | 0            |
| ArduinoUnoStopWatch.c             | void setup                         | c    | 12           | 11           | 0            |
| ArduinoUnoStopWatch.c             | void loop                          | c    | 69           | 63           | 0            |
| ArduinoUnoStopWatchTestRunner.bas | モジュール外                       | bas  | 89           | 71           | 2.7          |
| ArduinoUnoStopWatchTestRunner.bas | Function ArduinoStopWatchReadValue | bas  | 84           | 62           | 11.1         |
| ArduinoUnoStopWatchTestRunner.bas | Sub SetStart                       | bas  | 5            | 4            | 0            |
| ArduinoUnoStopWatchTestRunner.bas | Sub SetEnd                         | bas  | 5            | 4            | 0            |
| ArduinoUnoStopWatchTestRunner.bas | Sub GetDuration                    | bas  | 4            | 4            | 0            |
| 全ステップ数                      |                                    |      | 341          | 266          | 12           |

実ステップ数はストップウォッチ121行、テストランナー145行、トータル266行でした。

##6. おわりに
- ストップウォッチの代替となるものをArduinoと数点のパーツでサクッと作ることができました＼(^o^)／
- 実ステップ300行に満たないテスト自動化システムとはいえ投入している技術を並べてみると抽象的なソフトウェアのレイヤから具象的な物理レイヤまでQAはフルスタックだなあと改めて思いました[^5]。
  - 組込みCプログラミング
      - ポインタ
      - LEDで動作タイミング確認
  - Excel VBAプログラミング
      - Windows API呼び出し
  - 回路設計
      - オームの法則
      - 部品選定
      - チャタリング除去(今回はソフトウェアで実装;delay())
      - オシロで測定
  - テスト結果の整理
      - 最大値、最小値、平均値、中央値、標準偏差
- プログラム更新に伴い測定をやり直しましたが、Excelからオシロとストップウォッチにコマンドを送って値を自動取得するようにしていなかったら手作業での転記で心が折れるところでした。自動化しておいて良かったです。
- 時間を計るだけの単機能なテストツールにもかかわらず記事にまとめたら結構なボリュームになってしまいました。最後までご覧いただきありがとうございます。

##7. 付録：Unit Testのテストコードをマージしたスケッチ
- [ArduinoUnitでUnit Testを行う](https://qiita.com/pbjpkas/items/d10a2d0a1af68aa6efdc)で作成したテストコードをマージしたスケッチです。詳しくは[こちら](https://qiita.com/pbjpkas/items/d10a2d0a1af68aa6efdc)をご参照ください。
- Arduino IDE 1.8.9でビルドを確認しています[^6]。

```c:ArduinoUnoStopWatchV3.ino
/***********************************************************************
 * Copyright 2019 ka's@pbjpkas
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ***********************************************************************/
#define ENABLE_UT 0
#if ENABLE_UT
  #line 19 "ArduinoUnoStopWatchV3.ino"
  #include <ArduinoUnit.h>
#endif

/* pin assign */
#define PUSH_SW_START      2
#define PUSH_SW_END        3
#define PUSH_SW_RESULT     4
#define LED_START          5
#define LED_END            6

/* push switch anti-chattering delay */
#define ANTI_CHATTER_DELAY 5 //msec

void measure_with_stopwatch(unsigned long *timestamp, int led)
{
    *timestamp = millis();
    digitalWrite(led, HIGH);
    Serial.println(*timestamp);
}

unsigned long calc_duration(unsigned long *time_start, unsigned long *time_end)
{
    unsigned long duration = 0;
    
    if(*time_start <= *time_end)
    {
        duration = *time_end - *time_start;
    }
    else
    {
        duration = ( 0xffffffff - *time_start ) + *time_end;
    }
    
    return duration;
}

void display_result(unsigned long *time_start, unsigned long *time_end)
{
    unsigned long duration = 0;
    digitalWrite(LED_START, LOW);
    digitalWrite(LED_END,   LOW);
    duration = calc_duration(time_start, time_end);
    Serial.println(duration);
}

void print_help(void)
{
    Serial.print( F("This is ") );
    Serial.print( F(__FILE__) );
    Serial.print( F(" ") );
    Serial.print( F("Build at ") );
    Serial.print( F(__DATE__) );
    Serial.print( F(" ") );
    Serial.print( F(__TIME__) );
    Serial.print( F("\r\n") );
    Serial.print( F("s : Start measurement\r\n") );
    Serial.print( F("e : End of measurement\r\n") );
    Serial.print( F("d : Display of measurement results\r\n") );
#if ENABLE_UT
    Serial.print( F("t : run Unit Test\r\n") );
#endif
}

void setup()
{
    pinMode(PUSH_SW_START,  INPUT_PULLUP);
    pinMode(PUSH_SW_END,    INPUT_PULLUP);
    pinMode(PUSH_SW_RESULT, INPUT_PULLUP);
    pinMode(LED_START,      OUTPUT);
    pinMode(LED_END,        OUTPUT);
    digitalWrite(LED_START, LOW);
    digitalWrite(LED_END,   LOW);
    Serial.begin(115200);
}

void loop()
{
    unsigned long time_start = 0;
    unsigned long time_end   = 0;

    int port_state_start  = HIGH;
    int port_state_end    = HIGH;
    int port_state_result = HIGH;
    int port_state;
    char buf;

    while(1)
    {
        port_state = digitalRead(PUSH_SW_START);
        if(port_state_start != port_state)
        {
            if(port_state == LOW)
            {
                measure_with_stopwatch(&time_start, LED_START);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_start = port_state;
        }
        
        port_state = digitalRead(PUSH_SW_END);
        if(port_state_end != port_state)
        {
            if(port_state == LOW)
            {
                measure_with_stopwatch(&time_end, LED_END);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_end = port_state;
        }
    
        port_state = digitalRead(PUSH_SW_RESULT);
        if(port_state_result != port_state)
        {
            if(port_state == LOW)
            {
                display_result(&time_start, &time_end);
                delay(ANTI_CHATTER_DELAY);
            }
            port_state_result = port_state;
        }
    
        if(Serial.available())
        {
            buf = Serial.read();
            if(buf == 's')
            {
                measure_with_stopwatch(&time_start, LED_START);
            }
            if(buf == 'e')
            {
                measure_with_stopwatch(&time_end, LED_END);
            }
            if(buf == 'd')
            {
                display_result(&time_start, &time_end);
            }
#if ENABLE_UT
            if(buf == 't')
            {
                while(1){ Test::run(); }
            }
#endif
            if(buf == '?')
            {
                print_help();
            }
        }
    }
}

#if ENABLE_UT
test(calc_duration_start_lt_end)
{
    unsigned long test_time_start = 1400;
    unsigned long test_time_end   = 1800;
    unsigned long duration = calc_duration(&test_time_start, &test_time_end);
    assertEqual(400, duration);
}

test(calc_duration_start_eq_end)
{
    unsigned long test_time_start = 1400;
    unsigned long test_time_end   = 1400;
    unsigned long duration = calc_duration(&test_time_start, &test_time_end);
    assertEqual(0, duration);
}

test(calc_duration_start_gt_end)
{
    unsigned long test_time_start = 0xffffffff-1200;
    unsigned long test_time_end   = 1200;
    unsigned long duration = calc_duration(&test_time_start, &test_time_end);
    assertEqual(2400, duration);
}
#endif
```

## 更新履歴

|No.|日付|理由|内容|
|----|----|----|----|
|1|2019/12/14|・Arduino Uno Rev.3のシステムクロックの周波数許容偏差が±0.50%以内あり、本システムでの時間計測が適するもの、適さないものがある|・2章の想定する利用シーンに補足を追記
|2|2019/12/14|・Arduino Unoでunsigned long型のmillis()の戻り値をdouble型にキャストすると精度が[float型](https://ja.wikipedia.org/wiki/単精度浮動小数点数)に落ちてしまう([Unoやその他ATMEGAベースのボードのdouble型の実装はfloat型と同一](https://www.arduino.cc/reference/en/language/variables/data-types/double/)のため)<br>・float型の変数同士で演算を行うことによる計算誤差を避けたい|・Arduino側はunsigned longのまま処理を行いミリ秒の値をUARTへ出力<br>・Excel側で[Double型](https://docs.microsoft.com/ja-JP/office/vba/language/reference/user-interface-help/double-data-type)にキャストして秒に変換<br>※プログラムと3.6基本的な使い方の図を更新
|3|2019/12/15|・プログラム更新に伴う再測定|以下の章、節を更新<br>・3.5.1 計測開始スイッチの押下からLEDが点灯するまでの遅延時間<br>・3.5.3 計測時間<br>・6 おわりに
|4|2019/12/31|・Unit Testのテストコードを移植|・7.付録を追加

[^1]: UARTに出力すればExcel VBAでもPythonでも拾えるしいったんCI/CDに組み込んだらストップウォッチの画面は見ないと思うので。
[^2]: 待ち時間を数十ミリ秒より大きくしてもチャタリングが発生する場合はスイッチを別のものに交換することをお勧めします。チャタリング除去をdelay()で済ませているためこのストップウォッチはスイッチの品質に強く依存します。
[^3]: 筆者はVer 1.6.4はArduino用、Ver 1.8.xはM5Stack用と分けています。
[^4]: テストベンチにテキストデータで制御する機器とバイナリデータで制御する機器が混在しても対応できるよう、[バイナリ通信対応版](https://qiita.com/pbjpkas/items/271a60e057fff1ba9dd8)を移植しています。
[^5]: テストツールの開発はソフトウェアのほかにも無鉛はんだでの実装やケース加工などが伴うこともあります。「テスト（QA）エンジニアであっても、開発の知識がフルスタックで必要になる」([テスト設計コンテスト'20 OPENクラス チュートリアル p.42](http://aster.or.jp/business/contest/doc/2020_OPEN_V1.0.0.pdf))という話がありますが開発者が専門分野を深掘りするのに対してQAは横に広げていく印象があります。
[^6]: Arduino IDE 1.6.4では[ArduinoUnit](https://github.com/mmurdoch/arduinounit/)を組み込んだスケッチのビルドが通りませんでした。