ロウソクの様に瞬くLED照明を作ってみようと思います。
商品化された、LEDキャンドル(別ウインドウで検索)も売られているようですので、わざわざArduinoで作ることも無いのかもしれませんが、筆者なりに原理を考察して、作ってみたいと思います。
LEDをロウソクの様に光らせるには?何が必要でしょうか?やっぱり、明滅のモデルが必要ですよね?「ランダム」にせよ、らしく見せるためのモデルが必要、かつ重要。
明滅モデルができたら、所望の明るさでLEDを光らせる必要がありますが、それはこれまでに何度も使ってきたPWM出力を使うことで解決できます。
インターネットで検索していたところ、1件の論文を発見しました。
電気学会論文誌C(電子・情報・システム部門誌) 照明における1/fゆらぎ制御法の実験的検討 土井 滋貴, 大塚 智仁, 高橋 晴雄 |
なるほど、ロウソクの光は「1/fゆらぎ」なんですね。
1/fゆらぎとは、どういうことかというと、「現象のパワースペクトルが1/fに比例すること」の様です。論文には、1/fゆらぎを持った乱数列の生成方法が記載されています。要約すると、以下。
理屈としてはなんとなく理解できました。が、正規乱数って何?FFTってどうやって計算するんだっけ?と、色々ハードルが高そうです(1^^;)。
いくつかハードルを越えないとならないので、別途調査を進めたところ、全てMicrosoft Excelの機能でクリアできることが分かりました。
解説用に、N=32点のExcelシートをリンクしておきます。
1/fゆらぎ乱数列生成用シート:rand_1_f_32.zip
※クリックすると、拡大してご覧いただけます。もしくは、ZIPファイルをダウンロード→解凍してExcelファイルを参照してください。
Excelシートは、左列から順に処理して、右列に結果が出てくるように作ってありますので、左列から順に解説します。
FFT列と、iFFT列を計算するところだけは、手動で「フーリエ解析」ツールを実行する必要がありますが、それ以外のセルは自動計算になっています。
FFTもiFFTも、32点くらいなら一瞬ですが、数が増えてくると一気に時間がかかるようになります。プログレスバー表示とかは出ないので、「ハングしたか?」と思うくらい。N=4,096で実行したときには、優に数分以上戻ってきませんでした。待ちきれずバックグラウンドで処理させたため、正確な時間は不明です。
最終的に「1/f乱数」列の数値が、生成された「1/fゆらぎ乱数列」です。下図の様に、まんべんなくばらついていた正規乱数(青色プロット)が、1/fフィルタを通したことで、少し偏りのある1/fゆらぎ乱数(オレンジプロット)になっていることが確認できます。
フーリエ変換は、0〜N−1までを波、波形と捉え、0〜N−1までの波形が無限に繰り返していると仮定した場合の周波数空間に変換するので、N−1番目の乱数と、最初に戻って0番目の乱数はつながっています。また、生成したままの順番(上(0)から順に下(N−1)へ、または、下(N−1)から順に上(0)へ)で使わないと、「1/fゆらぎ」になりませんので、使い方にも注意してください。
N=32点だと、N/2=16Hz以上の周波数成分は含まれません。もっと速い周波数まで算出したい場合には、Nの値を大きくとる必要があります。では、今回は、どの程度の周波数まで必要でしょうか?
LEDの明るさを制御するのに、analogWrite()のPWM波形を使おうと考えています。analogWrite()のPWM波形は、周波数が約490Hzですので、これより速い周波数成分があったところで、再現できません。人間の目の時間軸方向解像度も、そんなに高いはずが無いので(例えば、TVのフレームレートは30Hz程度)、一番速い周波数を490Hzにしたとしても、十分でしょう。
では、490Hz波形1個当たりに、1つの1/fゆらぎ乱数を使ったと仮定しましょう。この場合、N点の乱数列が繰り返す周期は、N/490秒となります。例題のN=32点ではたったの32/490≒65msで繰り返してしまいます。これでは、1/fゆらぎどころか、一定の周期で高速に点滅しているだけにしか見えません。
もっとNを大きくとらないと。
どのくらいなら「繰り返している」と、見えなくなるでしょうね?わかりませんが、ここは、Excelのフーリエ解析ツールが許す最大のN=4,096を選択しておくべきなのでしょうね。
まとめると、「どのくらいの周波数まで計算する必要があるか?」よりも、「最速の周波数を490HzのPWMで再現しようとした時、一番遅い周波数(上で計算したのは逆数の周期)がいくらになるか?」の方が重要となります。
※別な観点で、人間の目の時間軸方向解像度から、一番速い周波数を30Hz程度にする場合は、その倍の60Hzで乱数を更新すれば良いので、1/60Hz≒16.7msで更新する様にプログラムすれば良いことになります。その場合、繰り返し周期が伸びるので、もっと乱数の数(N)を減らすことができるかもしれません。
前節のシートを使って、4,096点の1/fゆらぎ乱数列を生成し、これを8ビット符号無(uint8)配列データにしてプログラムに組み込みました。「#4-3 赤外線リモコン(送信)」で解説したPROGMEMを使って、RAM上に領域確保させないようにすることは言うまでもありません。だって、4,096個の8ビット乱数配列は4Kバイトの領域を必要としますので、ATmega328のRAM容量2Kバイトをはるかに超えてしまいます。
const int max_rand_1_f=4096;
PROGMEM const uint8 rand_1_f[max_rand_1_f]=
{
:
};
これで、Arduinoのプログラムで、1/fゆらぎ乱数列を使用する準備が整いました。
赤とか黄色とか普通のLEDでも、光の瞬き方が1/fゆらぎなので、そこそこ雰囲気は出ると思いますが、良い機会なので、スタータキットに入っていたRGB LEDを使って、光の色合いも制御してみたいと思います。
ぱっと見、普通のLEDと同じに見えますが、足が4本あります。型番がわからないので正確なところは不明ですが、ELEGOOスタータキットのCD-ROMに入っていたRGB LEDのデータシートによると、以下の通り:
手持ちのRGB LEDが、同様にカソードコモンなのかどうか?も含めて、端子配置はあらかじめ確認しておいた方が良さそうです。「#2-9 抵抗値を測定する」の抵抗測定回路などで、適切な基準抵抗を使い、各端子間の抵抗を測定すると、順方向だった場合にはピカッと光りますので、その時の色と電圧印加方向を書き留めたら、確認終了です。
※ちなみに、筆者が使用したRGB LEDは、一番長い足を2ピンとカウントした時に、1=Red、2=コモンカソード、3=Green、4=Blueでした。
R,G,Bの各LEDをON/OFFで制御しても、消灯、青、赤、マゼンタ、緑、シアン、黄色、白、の8通りでしか光らせることができません。もう少し炎っぽい色に調整するには、各LED毎にPWM出力を印加して発光強度を調整する必要があります。
Arduino UNOでPWM出力可能なI/Oポートを3端子割り当て、直接駆動することにします。LEDキャンドルの回路図は、以下の如くなります。
回路図って……?ほぼ、繋いだだけですけどね。手持ちRGB LEDの正確な仕様がわからないので、ELEGOOスタータキットのデータシートから読み取ったIf (max)より、マージンを取って電流制限抵抗を330Ωにしてみたのですが、実際に光らせてみたところ、明るすぎ(眩しすぎ)ました(この時のIf=5V/330Ω≒15mA)。
更に付け加えるなら、同じ電流で光らせてしまうと、緑(G)の光が強すぎると感じました。そこで、根拠は特にありませんが、感覚的にGの抵抗を大きく(5.1kΩ)して電流を絞り、RとBの抵抗値を1kΩとしました。
もし、別な抵抗値を使用する場合は、以降、RGB LEDの色合いを調整するR,G,B値が、本ページとずれてくることに留意してください。デューティの値も、ですね。RGB LEDの仕様にも影響を受けるので、各自使用する素子に合わせて微調整が必要になるかもしれません。
※光らせてみた感じですが、まず、本文中でも触れたように、明るさのバランスが取れていない。第二に、発光点の物理的位置が離れているのでしょう、色が分離して見えてしまいます。第三に、GとBのLEDですが、デューティの変化に対して、明るさが急に変化して、微妙な調整はできない様に思いました。
目標仕様を以下とします。
デューティ範囲、色設定、1/fゆらぎ乱数列から、PWM出力のデューティを計算する模式図を示しておきます。
1/fゆらぎ乱数列は、0〜255の範囲に正規化してありますが、元々、正規乱数から作成しているので、真ん中の128辺りの数値の出現確率が高くなっています。それをさらに<duty_min>〜<duty_max>間に変換した上で、<R>、<G>、<B>の強度を係数として掛け算して最終的なデューティを求めます。
これで、RGB LEDの明るさが、1/fゆらぎをもって変化するようになります。
analogWrite()で出力した490Hz PWM波形1個(〜2ms)に対して、1/fゆらぎ乱数1個を割り当て、デューティを設定すれば、終わりなので、そんなに難しいことはありません。
正確には、「そんなに難しくしなくても十分」なはず。
void loop (void)
{
:
// rgb-led drive
uint32 usec;
usec=micros ();
if (candle_e && usec - usec_prev>usec_intvl)
{
// update rand_1_f
uint8 dat;
uint16 duty;
uint16 duty_r;
uint16 duty_g;
uint16 duty_b;
idx_rand += (usec - usec_prev)/usec_intvl;
while (idx_rand>=max_rand_1_f)
idx_rand -= max_rand_1_f;
dat=pgm_read_byte (rand_1_f + idx_rand);
duty=(duty_max - duty_min)*dat/256 + duty_min;
duty_r=col_r*duty/256;
duty_g=col_g*duty/256;
duty_b=col_b*duty/256;
analogWrite (pin_LED_R, duty_r);
analogWrite (pin_LED_G, duty_g);
analogWrite (pin_LED_B, duty_b);
usec_prev=usec;
}
}
loop()関数の中で、micros()を計測しながら、前回デューティを更新してから2ms(usec_itvl)以上経過したときに、1/fゆらぎ乱数列の添え字を1個進めて、新しいデューティを設定するだけのことです。
厳密に言うと、PWMの周波数は、16MHz/64/(2×255)≒490.2Hzであり、周期=2.040msなので、2msという間隔とは微妙にずれがあります。そもそも、ソフトウェアで制御するのに、そんなにピッタリな時間間隔でデューティを更新できるはずがありません。
すなわち、上で示したコードでは、毎回デューティを変更したいところが、どこかでダブったり、1つデータを飛ばしたりという現象が起きます。本来であれば、タイマの割り込み処理中で、タイマカウンタに同期してデューティを更新していくべきところです。
まあ、さすがにそこまでしなくても、「それらしくは見えるはず」と言う、甘えがあるコードであることは覚えておいてください(1^^;)。
完成版プログラムをリンクしておきます。
完成版プログラム:led_candle.ino
前節でリンクした完成版プログラムをArduino UNOへコンパイル&書き込みして実行します。RGB LEDの回路もブレッドボードに構築して、必要な信号線を接続しておいてください。
シリアルモニタを開きます。
実験サンプル:【 】内はシリアルモニタからの送信データ
【cdl,1】 …… RGB LEDが点灯する。既に1/fゆらぎが適用されているはず。
OK: candle on
【col,0,0,255】 …… 青い光に変化
OK: color=(0, 0, 255)
【col,255,128,0】 …… 黄色の光に変化
OK: color=(255, 128, 0)
【duty,160,32】 …… 少し暗くなるが、消灯する時間が無くなる。
OK: duty=32~160
機能としてはOKでした。ただ、最終的な判定は、やはり「ロウソクらしく見えるか?」です。LEDキャンドルが瞬く様子を撮影した動画がこちら(↓)。
※【col,255,96,2】+【duty,160】で撮影。
※1/fゆらぎでロウソクの炎をエミュレートしているので、〜30FPSの再生速度が必要です。PC+有線LANなど、高速インターネット環境推奨です。
きっと、本物のロウソク(の炎)と並べてしまうと、全く違って見えるのではないでしょうか……?しかし、LEDキャンドル単体で眺めていると、それらしく見えてくるので不思議です。RGB LED本体の光より、その光で照らされた背景の方に注目していただいた方が、炎による照明っぽく感じられるかと思います。
「全然ロウソクに見えないよ〜」と言う方は、最初の乱数の発生をもう少し工夫する必要があると思われるので、そこまで遡って再検討をしてみてください。
1/fゆらぎは自然界に、それこそ自然に含まれる現象らしく、気持ちを落ち着かせる効果などがある様です。寝る時に枕元でLEDキャンドルを光らせておいたら、「寝つきが良くなる」、かも?火を使わないので安心ですが、そこそこ電力は食うはずなので、一定時間で電源を遮断できるようにしたいところ。工夫してみてください。