Arduinoを使った電卓の製作(6)

このページをスマホなどでご覧になる場合は、画面を横長にする方が読みやすくなります。
目次へ  前のページへ (1) (2) (3) (4) (5) (6) (7) 次のページへ
2015年09月14日 公開。
2016年03月18日 「最後に」の章を7ページ目に移動した。

11.電卓スケッチの解説

4ページで紹介した電卓のスケッチの仕組みについて説明します。比較的細かいコメントをスケッチ内に書いたので、ここでは、概略だけ説明する事にします。

たかが電卓のスケッチですが、行数にして400行弱あります。行数が多くなった原因は、数値を表わす文字列とfloat型の間で変換をおこなう関数を自前で作った事と、エラー処理が複雑だった事にあります。このくらいの規模のスケッチになると、スケッチだけを見たのでは、初心者には理解に時間がかかると思われますので、以下の説明と、スケッチのリストを読み合わせてください。

まず、文字列とfloat型の数値との間の変換関数の説明から始め、次にその他の関数の説明をして、最後にメインルーチン(loop関数)の処理について説明するという具合に、ボトムアップ式に電卓のスケッチの解説を行います。

11-1.数値を表わす文字列からfloat型への間の変換

電卓のスケッチでは、キーパッドで数値を入力し、その入力した文字列をfloat型に変換してから計算し、計算した結果を文字列に再変換して、液晶画面に表示しています。

標準のC++を使っている場合、数値を表わす文字列(例:"123.45")をfloat型に変換するにはsscanf関数を使うのが一般的です。例えば、sを文字列型(String型ではなくchar型の配列)の変数、fをfloat型の変数とすると、sからfへの変換は、次の様に書きます。

リスト3、sscanf関数の使用例COPY
sscanf(s,"%f",&f);

しかしながら、ArduinoのC++(正確にはAVR用のC++)では、%fの変換指定子の処理が実装されていません。(ただし整数を変換するために%dの変換指定子を使うのなら、正常に動作する) よって、sscanf関数は、文字列をfloat型に変換する用途には使えません。

他にも、atof関数を使って変換する方法があります。この場合は、次の様なコードで文字列変数sからfloat型変数fへの変換ができます。

リスト4、atof関数の使用例COPY
f=atof(s);

ただし、この場合、sに数値とはみなされない文字が含まれているかどうかを、判定する方法がないのが問題です。例えば、リスト5を実行すると、何事もなかったかのように、fに123が代入されてしまいます。

リスト5、atof関数に、数値とはみなされない文字が含まれる文字列を渡した例COPY
f=atof("123abc");

そういう訳で、文字列からfloat型への変換の関数を自前で作りました。それがStringToFloatという関数です。

StringToFloat関数の関数プロトタイプ(書式)は次の様になります。

リスト6、StringToFloat関数の関数プロトタイプCOPY
float StringToFloat(String s);

この関数は、古典的なchar型の配列の文字列ではなく、String型の引数を取ります。String型は、文字列を易しく取り扱える様にしたオブジェクト型(クラス)です。古典的な文字列(char型の配列)とは違い、最大の文字数をあらかじめ想定する必要がなかったり、文字列を結合する場合にstrcat関数などを呼ばなくても+演算子で記述できるなど、初心者にも文字列処理がわかりやすくなっています。メモリを多く消費するという欠点はありますが、電卓のスケッチではメモリに余裕があるため、スケッチを読みやすくする意味で、スケッチ中で文字列を扱うところは、基本的にString型を使っています。

StringToFloat関数は、引数sをfloatに変換して返しますが、もしsが、数値に変換できない文字列であれば、ErrorFloatを返すように作ってあります。ErrorFloatは、float型の定数ですが、1.0E30と、電卓で扱える範囲の数字(999999~−999999)を大きく超えた数に設定しています。

ところで余談になりますが、この記事を書いている最中に、strtod関数の存在を知りました。この関数を使えば、Arduinoで文字列をfloat型に変換できますし、文字列中に、数値とみなされない文字が含まれていても、それを検出できます。(CやC++の標準関数には、よく似た機能の関数が多くあるので、ややこしいですね)

strtod関数を使うと、StringToFloat関数は、次の様に簡単になります。

リスト7、strtod関数を使って書き直したStringToFloat関数COPY
float StringToFloat(String s)
{
  float result;
  char *ErrPtr;
  
  if(s=="") return ErrorFloat;
  result=strtod(s.c_str(),&ErrPtr);
  if(*ErrPtr=='\0') { // 正常に変換できた
    return result;
  } else { // 変換できなかった
    return ErrorFloat;
  } // if
} // StringToFloat

元もとのStringToFloat関数は52行ありましたが、strtod関数を使って書き直すと13行に短くなりました。ただし、コンパイル後のオブジェクトコードは、元もとのStringToFloat関数の方が小さいみたいです。

広告

11-2.float型数値から文字列への変換

標準のCやC++を使う場合、float型の数値を文字列に変換するには、sprintf関数をよく使います。例えばfをfloat型の変数、sをchar型の配列だとすると、fをsに変換するには、次の様にします。

リスト8、sprintf関数の使用例COPY
sprintf(s,"%f",f);

しかしながら、ArduinoのC++(正確にはAVR用のC++)では、%fの変換指定子の処理が実装されていません。(ただし整数を変換するために%dの変換指定子を使うのなら、正常に動作する) よって、float型の数値を文字列に変換するのに、sprintf関数は使えません。

AVRのライブラリにはdtostrfという、float型(厳密にはdouble型)を文字列に変換する関数があり、これをArduinoのスケッチに使えるのですが、小数点以下の桁数を指定しなければならないので、そのままでは電卓の数値表示には使えません。

そういう訳で、float型から文字列への変換関数も、自作しました。それがFloatToStringという関数です。

FloatToString関数の関数プロトタイプは次の様になります。

リスト9、FloatToString関数の関数プロトタイプCOPY
String FloatToString(float f);

float型の数値fをFloatToString関数に渡すと、数値を表わすString型の文字列が返ってきます。この際、6桁電卓の表示に都合のよい桁数になるように、調整して文字列化します。

例えば、123.4567という数をFloatToString関数で文字列に変換すると、"123.456"と、数字部分が6桁の文字列に変換されます。また0.012345を文字列に変換すると、"0.01234"(先頭の0を含めて数字部分が6桁)になります。12.3と、数字部分の桁数が6桁より少ない数字を渡すと、内部で一旦"12.3000"という具合に、数字部分が6桁の文字列に変換され、その後不要な末尾の0を除いて、最終的な変換結果は"12.3"となります。

FloatToString関数に絶対値の大きな数(例えば1234567)を渡すと、数字部分が6桁の文字列に変換できない場合があります。この場合は、"?"という文字列を返す仕様にしています。

この関数で工夫しているところは、丸め誤差の影響が表面化しにくいように、f(厳密に言えばfの絶対値)に小さな数を足してから、10進数に変換し、それを文字列化している事です。

前のページの計算精度についての項目でも説明しましたが、10進数から2進数に変換する際に、丸め誤差が発生します。さらに、2進数同士で計算する際にも、計算の内容によっては、丸め誤差が発生します。

そのため、例えば本来の計算結果(丸め誤差が生じない数学的な計算結果)が0.1ちょうどになるような計算でも、float型で計算した結果は、0.099999の様に、0.1よりも小さい数になっている可能性があります。そのままで数字部分が6桁の文字列に変換したのでは、7桁目以降を切り捨てる処理をしたと仮定すると、"0.09999"と丸め誤差が表面化してしまいます。

そこで、元の数に例えば0.000002という小さな数を足してから処理を行うことで、丸め誤差の表面化を防ぎます。この場合、元の数0.099999に補正値0.000002を足すと0.100001となります。この数を数字部分が6桁になるように文字列化すると"0.10000"となります。さらに末尾の不要な0を取り除く処理をすると、ちゃんと"0.1"という文字列に変換されます。

ただし、丸め誤差の表面化を防ぐために足す補正値は、常に0.000002でいい訳ではありません。処理したい元の数の大きさによって、補正値も調整する必要があります。

例えば、本来の計算結果が123.4になるはずの数を文字列に変換したいと仮定します。実際の計算結果は丸め誤差の影響により、123.4より少し小さい数になっている可能性があります。この丸め誤差の影響は、元の数が大きいほど大きく出るので、123.4という(先の例の0.1と比較して)大きな数では、例えば123.3999という具合に、小数点以下4桁のオーダーで影響が出てくると考えられます。(float型の有効桁数が6桁強なので、数字全体で7桁目、すなわち小数点以下4桁目が怪しくなってくる)

小数点以下4桁目に影響が出てくるなら、0.000002というように小数点以下6桁目に数を足しても、補正不足になります。(123.3999+0.000002=123.39992となり、123.4未満になる) この様に、補正値は、元の数の整数部(元の数が123.4なら、整数部は123)の桁数が問題になってきます。

そこで、FloatToString関数では、引数fの絶対値の整数部が何桁あるかをまず数え、その桁数に応じて、補正値を調整しています。

11-3.画面表示

今回作成した電卓では、16桁×2行の液晶を用い、上の行には入力履歴、下の行には入力中の数または計算結果を表示するようにしました。

写真23、液晶画面の構成
↑ 画像をクリックすると拡大
写真23、液晶画面の構成

この画面表示は、DisplayStringsという関数で行っています。DisplayStrings関数の関数プロトタイプは次の通りです。

リスト10、DisplayStrings関数の関数プロトタイプCOPY
void DisplayStrings(String hist, String num,int DelayTime=0);

第1引数のhistは、液晶の上の行に表示するString型の文字列です。この文字列は、右詰で表示されます。

第2引数のnumは、液晶の下の行に表示するString型の文字列です。この文字列も、右詰で表示されます。

第3引数のDelayTimeは、画面を消去してから、histやnumを表示するまでの時間を表わします。単位はmsです。DisplayStrings関数では、一旦画面を消去した後、DelayTimeで指定した時間が経過した後に、histやnumの文字列を表示します。DelayTimeに0を指定すると、画面はすぐに書き換わりますが、0より大きい数を指定すると、一瞬画面が消えた後に新しい表示が出るようになります。四則演算のボタンや=のボタンを押した時に、画面を一瞬消えるようにすると、電卓がキーを受け付けた事をユーザーが理解しやすいので、DelayTimeを指定できる仕様にしています。

11-4.主なグローバル変数

スケッチ中に出てくる主なグローバル変数の名称と型と意味を、次の表に示します。

表5、電卓スケッチの主なグローバル変数
名称 意味
keypad ResKeypad キーパッドを読むための、ResKeypad型の変数。ResKeypad型については、I/Oピン一つで読める4X5キーパッドキットサポートページ(4)を参照。
lcd LiquidCrystal 液晶に表示を行うための、LiquidCrystal型の変数。
MaxNum float この電卓で、扱える数の上限。 999999.4が、setup関数の中で代入される。MaxNumを定数にせずに変数にしたのは、電卓の桁数(columns定数で指定)に連動して変わるようにしたかったため。
NumStr String 液晶の下の行に表示する文字列。=ボタンを押した直後は計算結果を表わす文字列になり、それ以外のときは、入力中の数字を表わす文字列になる。
history String 液晶の上の行に表示する文字列。(入力履歴の文字列)
LastNum float 最後に入力した数。(もしくは最後の計算結果)
cleared boolean =ボタンを押した直後で計算結果を表示しているときはtrue、そうでない時はfalseになる。
LastChar char 最後に入力されたボタンの文字。

11-5.loop関数の動作

電卓のスケッチのメインになる処理はloop関数内で行っています。loop関数では、最初にc=keypad.WaitForChar();と、キーパッドから1文字読んで、char型のローカル変数cに代入します。WaitForChar関数は、キーパッドのボタンが押されるまで待ち、ボタンが押されたら、それに関連付けられた文字を返す関数です。(詳しくはI/Oピン一つで読める4X5キーパッドキットサポートページ(4)を参照。)

キーパッドから文字を読み込んだら、switch-case文で、読み込んだ文字に応じた処理を行います。ここで行う処理については、後で説明します。

その後、画面を更新し、LastChar変数(最後に入力されたボタンの文字)を更新します。

基本的にはたったこれだけの処理です。後は、押されたボタンの種類と、過去の状態に応じて、switch-case文の中で、画面の更新やら、計算やらを行います。

押されたボタンの種類と、switch-case文の中で行う処理の関係について、次の表に示します。

表6、押されたボタンの種類とswitch-case文の中で行う処理の関係
押されたボタンの種類 switch-case文の中で行う処理
数字(0~9) 入力中の数字(NumStr変数)に1桁追加する。
小数点(.) 入力中の数字(NumStr変数)に小数点を追加する。
+/− 入力中の数字の正負を入れ替える。NumStr変数の先頭の文字が'-'なら、それを取り除く。NumStr変数の先頭の文字が'−'でなければ、'−'を追加する。
入力中の数字(NumStr変数)の末尾の文字を削除する。その結果、NumStr変数がヌル文字列(長さ0の文字列)になれば、"0"に置き換える。
CE 入力中の数字(NumStr変数)を"0"にする。
四則演算ボタン(+、−、×、÷) 入力履歴がない場合(history==""):
入力中の数字を数値化し、LastNum変数に代入する。また、入力中の数字と、四則演算の種類を表わす文字('+'、'-'、'*'、'/')を結合し、history変数(入力履歴)に代入する。NumStr変数は"0"にする。

入力履歴がある場合(history!=""):
LastNum変数と現在入力中の数の間で計算を行い、その結果をLastNum変数に代入する。また計算結果と四則演算の種類を表わす文字を結合し、history変数に代入する。NusStr変数は"0"にする。
= 入力履歴がない場合(history==""):
入力中の数字(NumStr変数)を一旦float型に変換し、さらにそれを文字列に変換しなおして、NumStr変数に代入する。(この操作により、例えばNumStr変数が"12.300"ならば、"12.3"に置き換えられる)

入力履歴がある場合(history!=""):
history変数に""を代入して入力履歴をクリアする。また、LastNum変数と入力中の数字で計算を行い、その結果を文字列化したものをNumStr変数に代入する。
C history変数に""を代入して入力履歴をクリアする。またNumStr変数に"0"を代入して、現在入力中の数の表示を"0"に初期化する。

この表6に示した処理は概略ですから、実際のスケッチには各種のエラー処理が付いています。色々なエラーが起こりえますが、例えば、0で割り算を行った、計算結果が6桁を超えた、小数点ボタンを2回押した、四則演算ボタンを続けて2回押したなどが考えられます。エラー処理に抜けがないようにするには、それなりにスケッチが長くなりますし、それ以上に、動作チェックをしっかりする必要があります。

次のページでは、Arduino互換機と液晶モジュールを1枚のユニバーサル基板に組み込んで、コストダウンする方法を説明します。

目次へ  前のページへ (1) (2) (3) (4) (5) (6) (7) 次のページへ

このページで使われている用語の解説

関連ページ

関連製品

I/Oピン一つで読める4X5キーパッドキット 商品名 I/Oピン一つで読める4X5キーパッドキット
税抜き小売価格 2400円
販売店 スイッチサイエンス
サポートページ
Arduino用ブートローダ/スケッチライタキット 商品名 Arduino用ブートローダ/スケッチライタキット
税抜き小売価格 3000円
販売店 スイッチサイエンス
サポートページ
Arduino 電子工作
このサイトの記事が本になりました。
書名:Arduino 電子工作
ISBN:978-4-7775-1941-5
工学社の書籍の内容の紹介ページ
本のカバーの写真か書名をクリックすると、Amazonの書籍購入ページに移動します。