今回は、Arduinoを「喋る気温・湿度計」にしてみます。
U/I(User Interface)として、スイッチ×3個を持たせ、スイッチを押すと、その時点の気温・湿度データを音声合成で読み上げます。ついでなので、「時計機能」も持たせておいて、別なスイッチを押した時に、その時点の時刻を読み上げる機能も作り込んでみましょう。
まずは、目標仕様からまとめておきます。
以下、時計の実装、数値の読み上げ(数値→変則ローマ字変換)、顔文字表示の順で、考察した結果をご覧に入れます。
RTCを接続して、日時が必要になった時に毎回読み出す、というのも良いのだけど、あんまり芸が無いので、Arduinoでちゃんと時計を実装しようと思います。起動時の日時を知るために、一度だけRTCにアクセスするけど、そこからArduinoがちゃんと時計を刻むので、その後はRTCにはアクセスしません。
Arduinoのコアライブラリでも、「時間」は、刻んでいます。millis()関数とか、micros()関数がそれです。しかし、Arduinoで「時計」を刻むに当たり、millis()関数やmicros()関数では制御しにくいので、16ビットタイマ(TC1)割り込みを使います。TC1を時計用のタイマとして占有し、1秒毎に割り込みをかけるように設定します。
割り込みサービスルーチン(ISR)では、秒カウンタ(clk_sc)をインクリメントして、もし、clk_sc=60になったら、次の桁、分カウンタ(clk_mi)にキャリを上げ、clk_sc=0にリセットします。その結果、clk_mi=60となったら、さらに上の桁にキャリを上げる……、と言う動作を年カウンタ(clk_yr)まで繰り返します。
時計を刻むためのカウンタには、以下の変数を確保して割り当てます。
カウンタ | 変数名 | 動作 |
秒カウンタ | clk_sc |
1秒毎の割り込みでインクリメントclk_sc++ clk_sc=60となったら、clk_mi++,clk_sc=0 |
分カウンタ | clk_mi |
clk_sc=60でインクリメントclk_mi++ clk_mi=60となったら、clk_hr++,clk_mi=0 |
時カウンタ | clk_hr |
clk_mi=60でインクリメントclk_hr++ clk_hr=24となったら、clk_dy++,clk_hr=0 |
日カウンタ | clk_dy |
毎年1月1日からの経過日。1月1日をclk_dy=1とする。 clk_hr=24でインクリメントclk_dy++ clk_dy>365+leapとなったら、clk_yr++,clk_dy=1 leapは、うるう年なら1、そうでなければ0。 |
年カウンタ | clk_yr | clk_dy>365+leapでインクリメントclk_yr++ |
実はこれ、過去にJJYエミュレータを作った時に既に実装済みで、処理に必要な関数群(うるう年の判定、経過日の計算、曜日の計算)の解説が「#5-9 JJYのエミュレーション」にあります。今回は、日カウンタclk_dyから、月/日の情報を逆算する関数が必要だったため、それだけ新規に起こしました。
あと、TC1の割り込み周期を100ms毎から、1s毎に変更しました。JJYエミュレータでは、1秒を10に区切ってデータを送信する必要があったのに対して、時計を刻むためには1s毎の割り込みで十分なためです。
こうしておくと、どこかのタイミングで、「今、hh時mm分ss秒だよ」と、言う情報を一度与えてもらったら、そこからは自分で時計を刻むことができます。そこで、RTCが無くても、シリアルモニタから現在時刻を教えてもらう、【mset】コマンドを用意しておきます。シリアルモニタのコマンドに関しては、後述します。
尚、各カウンタ変数はISR内で非同期に更新されるため、カウンタにアクセス(読み出し/書き込み共)する時には、割り込みを禁止しておく必要があります(以下、読み出し側の例)。
// get clock
noInterrupts ();
yr=clk_yr;
dy=clk_dy;
hr=clk_hr;
mi=clk_mi;
sc=clk_sc;
interrupts ();
clk_days2date (yr,&dy, &mo,&wk);
アクセスしている途中で割り込みがかかって、カウンタ値が更新されてしまうと、割り込みの前後でアクセスしたカウンタが不整合を起こすためです。
音声合成って、使い所が難しいですよね(MCUパワーが非力なArduino等の環境の場合特に)。過去の記事で、WAVファイルの再生を実現した「#7-9 WAVファイルを再生する」の時に、play_wav()関数を作りました。操作の説明とか、状態のレポートなど、決まったセリフ(例えば以下に示すような)であれば、録音してWAVファイルにしておくと再生できるので、その方が自然な音声で聞き取りやすいことでしょう。
例)決まったセリフ
しかし、今回の様に、日時や気温・湿度を読み上げるとなると、WAVファイルの再生だけでは実現困難です。日時や気温・湿度など、取り得る全数値の発声データをWAV化するのは非現実的だからです。→ 音声合成の応用に最適、と言うことです。
音声合成には、「#8-6 音声合成、の、マネ事」で作成したspeak_jp()関数を使います。SDカードに発声データファイルspeak_jp.wavが必要になるので、「#8-6 音声合成、の、マネ事」をご覧になって、ファイルを準備してください。
speak_jp()関数では、「変則ローマ字」の読み上げだけが可能となっています。アルファベットや数字などの発声は一切できません。アルファベットや数字を発声するには、以下の様に、変則ローマ字に変換する必要があります。
例)変則ローマ字への変換
最後の方に示した例の様に、数値 → 変則ローマ字への変換関数を作成する必要がありそうです。
目標仕様に示したように、今回発声すべき数値は、以下の様な範囲を取ると考えられます。
すべてに共通する最大項を取ると、以下の様にまとめることができます。
読み上げに対応すべき数値:[負号−]<最大4桁の数値>[<小数点.><1桁の数字>]
※[ ]内は省略可能/無い場合もある。
小数点以下の数字は、気温・湿度データの時だけ必要なので、別途場合分けして処理することとして、「[符号−]<最大4桁の数値>」を引数にとり、変則ローマ字に変換して読み上げる関数speak_num()を作成します。
日本語の数値表現は「万進法」なので、4桁毎に区切って変換する必要があるのですが、今回必要な数値は最大4桁なので、区切らず一気に変換できます。以下、変換関数のコードを示し、その下に解説を追加して行きます。
void speak_num2str (
int num)
{
// add lbuf[ ] to number num (1 digit) as roman character
switch (num)
{
case 0: strcat (lbuf,FF("rei")); break;
case 1: strcat (lbuf,FF("iti")); break;
case 2: strcat (lbuf,FF("ni")); break;
case 3: strcat (lbuf,FF("sann")); break;
case 4: strcat (lbuf,FF("yonn")); break;
case 5: strcat (lbuf,FF("go")); break;
case 6: strcat (lbuf,FF("roku")); break;
case 7: strcat (lbuf,FF("nana")); break;
case 8: strcat (lbuf,FF("hati")); break;
case 9: strcat (lbuf,FF("kyu-")); break;
}
}
"0"〜"9"までの各数字を変則ローマ字に変換する関数です。以下に解説する数値→変則ローマ字変換関数から使います。
void speak_num (
int num)
{
// speak num by speak_jp()
// num: [-]0~9999
// disturb: lbuf[ ], fbuf[ ]
int sen;
int hya;
int zyu;
int ich;
整数型の引数numを最大4桁の整数値と見て、変則ローマ字に変換し、speak_jp()関数で発声する関数です。
// parameter
if (num==0)
{
speak_jp (FF("rei"));
return;
}
num=0だった場合は、変則ローマ字で"rei"と発声して終了する例外処理です。
// sign
lbuf[0]='\0';
if (num<0)
{
strcat (lbuf,FF("mainasu"));
num=abs (num);
}
ここから先は、文字列バッファlbuf[ ]に変則ローマ字を作成して行きます。num<0だった場合、負号"mainasu"を付加して、num自体は絶対値を取って先に進みます。
// number (4 digits)
sen=(num/1000)%10;
hya=(num/100)%10;
zyu=(num/10)%10;
ich=num%10;
numを最大4桁の整数と見て、各桁毎に数字を分解します。
// conversion
if (sen>0)
{
if (sen>1)
{
speak_num2str (sen);
}
strcat (lbuf,FF("senn"));
}
千の桁(sen)から変則ローマ字を生成します。sen>0の時のみ、発声します。通常、sen=1の時、「イチセン」とは発声しないので、sen>1の時だけ、数字を発声します。
if (hya>0)
{
if (hya>1)
{
speak_num2str (hya);
}
strcat (lbuf,FF("hyaku"));
}
百の桁(hya)の処理、千の桁と同様です。
if (zyu>0)
{
if (zyu>1)
{
speak_num2str (zyu);
}
strcat (lbuf,FF("zyu-"));
}
十の桁(zyu)の処理、百の桁と同様です。
if (ich>0)
{
speak_num2str (ich);
}
一の桁(ich)の処理です。この桁のみ、ich=1の場合も、数字を発声する必要があります。
// speak
speak_jp (lbuf);
}
文字列バッファlbuf[ ]に数値numを変則ローマ字に変換した文字列が格納されているので、speak_jp()関数で読み上げて終了します。
基本的には、これで4桁の整数を変則ローマ字に変換できました。Arduinoでの処理では、ここで止めてあります。
もう少しこだわると、例えばnum=300の時、上の関数では「sann hyaku(サンヒャク)」と変換されてしまいますが、一般的には「サンビャク」ですよね。num=8,000の時も、「hati senn(ハチセン)」と変換されますが、「ハッセン」の方が自然です。
自然な発声にしたい場合には、このようなケースを場合分けして変換することが必要となります。その辺の改造は、モチベーションを感じた皆様へお譲りします。
※単位が絡むとさらに複雑で、「9分:キュウフン⇔(キュウフン)」ですが、「9時:クジ⇔(キュウジ)」が自然だし、「6分:ロップン⇔(ロクフン)」、「30分:サンジュップン⇔(サンジュウフン)」等々、ひと手間かかります。丸カッコ内は、上で示したコードの変換結果です。
Arduinoみたいに、非力なMCUを使う場合、どうしてもU/Iがお座成りになってしまうのですが、今回は、8×8 LEDを使って、機能としては全く意味のない、顔文字表示に対応してみます。
人が何かを喋る時、喋る言葉に応じて、口の形が変化し、そこに表情も重なって、喋った言葉(文字)以上の情報が伝達されています。
口の形は、基本的には喋る文字の母音で決まります。「あ」だと口を大きく開け、「う」だと口をすぼめる、などです。母音の他には、「ん」を発声する時、口を閉じますので、都合、母音「あいうえお」と「ん」の6通りの顔文字があれば、喋る文字に合わせて顔文字を変化させ、「Arduinoが喋っている感」を醸し出すことが可能です。
はい\(1^^;)。機能としては、全く意味がありません。顔文字表示(8×8 LED)が無くても、全く困りません。ただ、「Arduinoが笑顔で喋ってくれている」と、言う情報が発信されることで、U/Iとしては意味のあることだと考えられます。
以下の様に、Excelを使って顔文字フォントを作成しました。
今回は、8×8 LEDを90°回転させて使うつもりなので、顔文字の下に計算したフォントデータ(8ビット16進数×8個)を使用しました。本来の方向で使用する場合は、顔文字の右に計算したフォントデータを使用すれば良いはずです(ただし未確認(1^^;)っ)。
音声合成関数speak_jp()に、8×8 LEDの表示関数を取り込んでおき、発声に同期して顔文字を変化させるようにします。speak_jp()関数内では、発声しようとする文字番号nchr=0〜435を計算しています。文字番号は、5で割った余りが母音を表すので、これを使って、表示する顔文字フォントを切り替えます。
// show face font
if (spk_show_face)
{
// here, nchr%5 shows vowel (0~4) when nchr!=435
switch (nchr%5)
{
case 0:
if (nchr==435)
x88_display ('n');
else
x88_display ('a');
break;
case 1: x88_display ('i'); break;
case 2: x88_display ('u'); break;
case 3: x88_display ('e'); break;
case 4: x88_display ('o'); break;
}
}
厳密には、「ぱぴぷぺぽ」などの破裂音を発声する時、一度口を閉じてから、再び開いて母音を発声するケースなどが存在するのですが、そこまで厳密にはエミュレートしていません。8×8[ドット]と限られた表現力の中では、そもそも、母音の口の形ですら、「何となくそれらしい」程度にしか表示できないため、ある程度、割り切っています。
目標仕様に基づいて回路を構成して行きます。
使用する部品:
いずれも、過去に題材として取り上げた部品ですので、制御方法など詳細については、「逆引き索引」から記事をあたってください。
本ページタイトルの通り、気温・湿度を喋らせるだけなら、8×8 LEDとRTCはオプションです(無くても良い)。
まずは、メインとなる回路を以下に示します。
以下、ポイントを解説しておきます。
上の回路に加えて、音声合成用の発声データファイルspeak_jp.wavを格納するため、SDカードI/F回路が必要です。
「#6-5 microSDカードI/Fを自作する」の使い回しです。
最後に、RTCのネットリストを示しておきます。
Arduino | 接続 | RTC(DS1302) |
5V | → | VCC |
GND | ⇔ | GND |
D3 | → | CLK |
D4 | ⇔ | DAT |
D5 | → | RST |
8×8 LEDとRTCはオプションですが、上の回路をすべて、同時に、Arduinoの周辺に構築する必要があります。オプションを含めてブレッドボード上に構築する場合、結構な規模と配線量になります(1^_^;)💦。後ほど、実験中の写真を示しますのでご確認ください。
完成したプログラムをリンクしておきます。
完成プログラム:speak_th.ino
コンパイルしてUNOに書き込むと、シリアルモニタから以下のコマンドを受け付けるようになっています。
以下、実験中の様子です。
※フレームの外に外れてしまってますが、RTC(DS1302)も接続してあります。
前節で示した回路図と異なり、DA変換器は3.3V駆動にして、Arduinoとの接続にはL/Sを使っています。また、外部電源回路を使って、UNOの5Vから3.3Vを生成し、DA変換器のみに供給しています。一応、最低限のノイズ対策で、デジタル系と電源を分離しています。
※相変わらずノイズはひどくて、電源分離しても気休め程度でしたが……。だったら、電流容量が足りなそうなSDカードI/Fを外部電源にして、UNOからDA変換器へ3.3Vを供給した方が良かったかも??。
では、実際に使ってみましょう。この回路は、シリアルモニタを開かなくても、基本的に自立できるのですが、まずは、シリアルモニタからテスト用機能を使って、動作をチェックして行きます。
実験サンプル:【 】内はシリアルモニタからの送信データ
【num,-1234】
OK: speak_num
…… 「マイナスセンニヒャクサンジュウヨン」と読み上げたら、OKです。
【dht】 …… DHT11からデータを読み出せるかテストします
OK: T=22.6[degC] H=47.0[%RH]
読み出しが成功したら、ブレッドボードに構成したスイッチ1番を押して、気温と湿度を音声合成で読み上げるかどうか、テストしてください。
【mset,20,1,13,14,25,36】
OK: mset,20,1,13,14,25,36
日時を設定しましたので、ブレッドボードに構成したスイッチ2番を押して、今設定した時刻を音声合成で読み上げるかどうか、テストしてください。
うまく行ったら、続いて、スイッチ3番を押して、日付→時刻→気温・湿度の順に音声合成で読み上げるかどうか、テストしてください。
【opt,1】
OK: time signal enable
デフォルトで時報機能を無効にしてあったので、【opt,1】コマンドを実行して、時報機能を有効化しました。【mset】コマンドで時報が発生する直前の時間を設定して、時報が発動するかテストします(以下)。
【mset,20,1,13,9,59,45】
OK: mset,20,1,13,9,59,45
【mset】コマンドを実行したら、およそ15秒後に10時の時報が発動して、「yyyyネン mmガツ ddニチ hhジ tt.tドシー uu.uパーセント」という情報が音声合成で読み上げられるはずです。
あっ、書き忘れていましたが、8×8 LEDを接続している場合には、音声合成の発声に合わせて、顔文字が表示され、「Arduinoが喋っている感」を醸し出しているはずですので、お見逃しなく。
RTCを接続している場合には、一旦Arduino UNOのリセットボタンを押して再起動後、スイッチ2番ないし3番を押して、正しい時刻を読み上げるかどうか、テストしてください。
そのまま20秒(=x88_msec_shutdn)放置すると、8×8 LEDの表示が消えるはずです。その状態でいずれかのスイッチを押すと、再び顔文字が表示されます。
以上で最低限のテストは終了です。実際に動作させた時の動画をご覧に入れます。
いかがですか?Arduinoの音声合成なので、聞き取りにくいのですが、日時や気温・湿度を声で教えてくれています。喋りに合わせて顔文字が変化するのも、意外と良いでしょう?
ちなみに……、気が付かれましたか?動画の中では、2020/01/13は月曜日だけど、「日曜日」と喋っています……。あらら、動画撮影後、回路を崩してしまった後で、曜日計算プログラムのバグに気が付いて修正しました。回路を再構築して動画を撮影し直すのが面倒くさかったので、間違ったまま喋っていますm(1__)m(許して)。
プログラム(speak_th.ino)は修正しておいたので、正しい曜日を喋ります。
※今回に限ったことではないのですが、SDカードが不安定です。電源が弱いのか?ノイズによる誤動作なのか?ブレッドボード+ジャンパ線での実験では、ちょっとしたことで、SDカードにアクセスできなくなるケースがありました。
ある時、プログラム(speak_th.ino)をUNOターゲットでコンパイルした所、ROM/RAMの使用サイズが以下の様に表示されました。
メモリ | 使用量[バイト] | 占有率[%] | 備考 |
ROM(Flash) | 25,530 | 79 | ATmega328では32Kバイト搭載。 |
RAM | 1,533 | 74 | 同じく2Kバイト。 |
スイッチと8×8 LEDだけがU/Iで、スイッチが押された時の気温・湿度を音声合成で読み上げるだけのアプリケーションなのだけど、既にメモリはカツカツの状態です。SDカード関係のライブラリが、結構メモリ喰らい。(まだ少〜し余っているけど、)これ以上の機能を詰め込もうとすると、すぐにメモリが足りなくなる事でしょう。
半導体メーカとしては、ビジネスチャンスで、「お客さん、もっとメモリの大きい上位チップがありますよ(お値段は高くなりますが……)」って、売り込むわけです。どんどんメモリの大きなマイコンが必要になるわけで、当然、コストも上がってしまいます。
よって、MCUとしては、「メモリ効率の良い命令セット」と言うのも重要なファクタになってきます。未だに1バイトコードを持ったMCUが存在しているのも、「メモリ効率」を重視した結果なのかもしれません。ただし、単なる「互換性の産物」になっている場合が多いのも事実です。
最初の思想は「メモリ効率向上」だったのかもしれないけれど、その後、機能を追求する過程で命令が追加され、どんどんオペコードが長くなったにもかかわらず、互換性維持の為に1バイトコードが残っているとか、愚の骨頂でしょ?
その点、ATmega328(AVR系MCU)は良いよな〜。潔く2バイト固定長(時々4バイト)の8ビットRISCとか、キュンキュンします(1^−^)💖。「美しい」とまでは言えないけど、「可愛らしい」、「一緒に居て楽しい」、そんな娘なのよ。
ATmega328の上位には、メモリとI/Oポート数を増加したATmega2560(Arduino MEGAに使われているMCU)なんてのもあるんだけど、がたいのでかいのはちょっと……。ATmega328P-PU(28ピンP-DIP)のフォルムも含めて、超LOVEなのです。
※上図をクリックすると、Xi-TRACEでATmega328P-PUとUNOをモデリングした記事に飛んで行きます。お時間のある時に、是非ご覧ください。
何を言いたかったかと言うと、「何かと手狭にはなって来たけど、暫くはArduino UNOで遊んでみようと、改めて思った」と、言うことをお伝えしたかったのです。
今回のアプリケーションの話に戻ると、8×8 LEDで顔文字を表示しているのだけれど、オプション機能です。基本的には表示装置なしで動作するので、ベッドの枕元に置いて、暗がりでスイッチを押すと時刻を教えてくれるのは、便利かも?眠気眼を無理やり開いて、「今何時?」って、時計を見なくても良いから。そういう用途の場合は、時報をOFFしておかないと、毎正時に一々うるさくて、眠れません(1^^;)。