##1. はじめに
ソフトウェアテストの小ネタ Advent Calendar 2019の13日の筆者の記事「ストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化する」で修正したバグを供養します。(-人-)ナームー
##2. 修正前のプログラム
- 要件や仕様、修正に至る経緯はストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化するをご覧ください。
- 明示していませんがスイッチは計測開始→計測終了の順に押すものとし、どちらか片方だけや、逆順に押した場合は考慮不要とします。
- Arduinoの組み込み関数millis()の仕様はこちらをご参照ください。
- このプログラムで修正したバグは2つあります。
/***********************************************************************
* 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(double *timestamp, int led)
{
*timestamp = (double)millis()/1000.0;
digitalWrite(led, HIGH);
Serial.println(*timestamp, 3);
}
double calc_duration(double *time_start, double *time_end)
{
double duration = 0;
if(*time_start <= *time_end)
{
duration = *time_end - *time_start;
}
else
{
duration = ( 0xffffffff - *time_start ) + *time_end;
}
return duration;
}
void display_result(double *time_start, double *time_end)
{
double duration = 0;
digitalWrite(LED_START, LOW);
digitalWrite(LED_END, LOW);
duration = calc_duration(time_start, time_end);
Serial.println(duration, 3);
}
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()
{
double time_start = 0;
double 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();
}
}
}
}
以下に実行例を示します。
##3. バグの内訳
###3.1 1つめのバグ
1つめはコメントで教えていただいた件です。筆者は倍精度浮動小数点数のつもりでdoubleを使ったのでコメントを読んだ瞬間ぶへっと吹きました。
####3.1.1 まずいところ
- Arduino UnoやそのほかのATmegaベースのArduinoはdoubleがfloatと同一のためdoubleを指定してもfloat(4バイト)になる
- floatの精度は10進数で7桁
- 下3桁をミリ秒表示に使用しているので9,999.999秒(2時間46分)を過ぎたあたりから誤差の影響が出始める
- floatの変数同士で演算している
- floatの変数同士で演算すること自体は構わないのだけど、今回はわざわざfloatの精度に落として演算する理由がない
####3.1.2 バグ混入の経緯
もともとtime_startやtime_endはunsigned long型にしていたのですが以下の理由でdouble型に変更しました。
- ミリ秒の値をUARTに出力してデバッグしていると桁が多くて見づらい
- 最終段(Excel)では秒で表示するのでArduinoから出力する時点で秒に変換すれば見やすくもなるし一石二鳥
####3.1.3 修正の理由
- doubleを単精度のつもりで使っていない。後日ソースを見た時に倍精度と勘違いする自信あり。
- 2時間46分は筆者自身が使う分には十分長いのだけどストップウォッチとしてはイケていない感。
- 市販のストップウォッチの代替とするなら少なくとも9時間59分59秒999は持って欲しい。
- Arduino Unoは処理系の制約でdouble型の精度を出せない
- もともとのunsigned long型に戻せば2つめのバグ含め根本解決を図れる
####3.1.4 修正方法
- time_start、time_endをもともとのunsigned long型に戻す
- ミリ秒の値をUARTへ出力する
###3.2 2つめのバグ
もう一つのバグは calc_duration() にあります。
double calc_duration(double *time_start, double *time_end)
{
double duration = 0;
if(*time_start <= *time_end)
{
duration = *time_end - *time_start;
}
else
{
duration = ( 0xffffffff - *time_start ) + *time_end;
}
return duration;
}
####3.2.1 まずいところ
millis()は32ビットのカウンタを1ミリ秒ごとに1ずつインクリメントし0xFFFFFFFF(0d4294967295)を超えるとオーバーフローしゼロに戻ってインクリメントを続けます。4,294,967,295ミリ秒は約49.7日になります。
else側は計測開始から計測終了の間にオーバーフローが発生したときの処理ですが引数で与えられるtime_startやtime_endはmeasure_with_stopwatch()でミリ秒を秒に換算した値なのでこのまま計算しても正しい値になりません。
####3.2.2 バグ混入の経緯
unsigned longからdoubleに変えた時の修正漏れです。
##4. 振り返り
###4.1 単精度浮動小数点数、倍精度浮動小数点数
Wikipediaの単精度浮動小数点数から引用します。
- 浮動小数点形式の標準であるIEEE 754では、単精度は32ビット(4オクテット)、倍精度は64ビット(8オクテット)である。
- C言語、C++、C#、Java、Haskellでは単精度のデータ型を float と呼ぶ[1]が、C/C++の規格ではIEEE 754に準拠することは要求されていない。
IEEE 754で単精度、倍精度のサイズは規定されているものの実際のサイズは処理系依存なのですね。処理系依存はintだけではないと。
また、Wikipediaの倍精度浮動小数点数によるとdoubleが8バイトなら精度は10進数で15桁ありmillis()のカウンタがオーバーフローするよりもはるかに長い999,999,999,999.999秒まで行けます1。
###4.2 ユニットテスト
2つめのバグはユニットテストを書けばすぐ見つかるものと思いました。ArduinoUnitでTDDを試してみるで紹介されているArduino用ユニットテストフレームワークArduinoUnitを試してみるのもありかも。
2019/12/31更新
ArduinoUnitを試した記事を作成しましたのでご参考ください。
ArduinoUnitでUnit Testを行う
###4.3 通信仕様を決める
後段のExcelとデータをやり取りする際に時間の単位をミリ秒とするか秒とするか、実現手段はあるか、をあらかじめ検討して決めることでもバグを防げたように思いました。
###4.4 もう一つの修正方法
ストップウォッチにArduino Unoを採用したのはたまたまUnoが積み基板になっていたためです。Unoを使うのはマストではなく、身近なものだとM5Stackに搭載されているESP32がdoubleが8バイトなのでこちらを採用する方法もあります。特にM5Stackは初めからボタンが3つに液晶画面も搭載されているのでストップウォッチに好適です2。
Serial.printf("char : %d\n", sizeof(char));
Serial.printf("short : %d\n", sizeof(short));
Serial.printf("int : %d\n", sizeof(int));
Serial.printf("long : %d\n", sizeof(long));
Serial.printf("long long : %d\n", sizeof(long long));
Serial.printf("float : %d\n", sizeof(float));
Serial.printf("double : %d\n", sizeof(double));
ESP32はprintf()も使えます。便利!
char : 1
short : 2
int : 4
long : 4
long long : 8
float : 4
double : 8
##5. おわりに
- 解決策を思いついた状態ってのは危ないですね。浮かれているし視野も狭くなっているし。
- QAがプログラムを作ればバグがないなんてことはないし自分でプログラムを書いてバグを踏むとこうやってバグができるのだなと実感が伴ってわかるので、QAやテストエンジニアの方も何かしらプログラムを書いてみるのはお勧めです\(^o^)/