今回もATtiny2313を使って、とても小さな(tiny)アプリケーションを作って見たいと思います。
アプリケーションは「暗くなったら蝋燭を灯して」と、言う事で、周囲の暗さを判定して、暗くなったら自動的に点灯するLEDキャンドル🔥です。
メモリも少ないし、搭載しているI/Oユニットにも制限のあるATtiny2313で実現するには、いくつか超えなければならないハードルがあります。
最初のハードルは、ATtiny2313(下図)のメモリが足りないことです。
「#4-6 LEDキャンドル」で作成した1/fゆらぎ乱数列は、0〜255(8ビット)の4,096点でしたので4Kバイトのメモリを要します。一方、ATtiny2313のFlashは2Kバイトしかありません。Flashメモリには他に、LEDキャンドルのプログラム自体も記憶する必要があるため、作成した乱数列の半分も載せることが出来ない、と言う事です。
さて、直交座標系変換(FFT/iFFT)を使って作成した1/fゆらぎ乱数列は、末尾値と先頭値が繋がって、リング状になっています。従って、4,096点の乱数列からN点の乱数列に圧縮する時は、先頭からN点取るのではなく、4,096/N点飛びにデータを間引いて拾っていけば1/fゆらぎの性質を損なうことなく、圧縮することができるのです。
既に手元に4,096点の1/fゆらぎ乱数列があるので、後はNを決めれば、乱数列を間引いて終了、です。
では、Nはいくらにできるでしょうか?なるべく大きな乱数列を残したいので、以下手順でNを決定します。
Nは理論的にはどのような数値でも構わないのですが、整数飛びに間引くことができる様、4,096/Nが整数となるように選ぶのがベストです。素因数分解すると4,096=212ですので、Nは2冪数となります(それ以外では整数にならない)。
※間引く数が整数にならなかった場合、厳密に見ると、一番近い整数に丸めることで周波数にブレが生じてしまいます。これを「誤差範囲」と割り切れるなら、Nは自由に決めて構いません。どちらかと言うとFlashの容量で決まってくるので、2冪数に拘ると返ってサイズが小さくなってしまって不利かもしれません。
「#4-6 LEDキャンドル」で作成したオリジナル(元祖)のプログラム(led_candle.ino)では、4,096点の1/fゆらぎ乱数列を2ms間隔でPWMデューティに設定(更新)しました。LEDをPWMで光らせるので、デューティの変化によってLEDが1/fゆらぎ乱数列で瞬きます。これがLEDキャンドルの原理となっていました。
今回、1/fゆらぎ乱数列を4,096点 → N点へ間引いて圧縮しますので、PWMデューティの更新間隔を2ms×4,096/Nに変更することで、オリジナルプログラム(led_candle.ino)と同じ瞬きに近づけることが出来ます。
なお、厳密には4,096点 → N点に圧縮したことで高周波数寄りの乱数が失われているので、その周波数帯の挙動は、オリジナルと異なります。ただし、高周波数側は周期にしてmsオーダ、周波数にしてkHzオーダなので、人間の目で見てその差をはっきりと認識することは、かなり難しいと思います。……つまり、圧縮しても問題なし、と考えられる。
※N=10とか、小さくし過ぎた場合はダメです(目で見て明らかに差が解る)ので、程度問題となります(1^^;)。
「暗くなったら蝋燭を灯して」と言うアプリケーションを実現するため、周囲の暗さを検知可能なセンサを接続してください。手持ちセンサの中では、フォトレジスタ(CdSセル、下図)が該当します。
この素子は周囲の暗さによって抵抗値を変化させるので、一般的には以下図のような回路構成で使用します。
UNO等を使う場合には、上図左側の様な直列抵抗との抵抗分圧で生じた電圧Voをアナログ端子に接続し、AD変換器でアナログ電圧として取り込み、適切な閾値電圧との比較(ソフトウェア)によって、暗さを判定します。
2つ目のハードルは、ATtiny2313にはアナログ電圧Voを読み取るためのAD変換器が搭載されていないことです。
ATtiny2313には、アナログ比較ユニットが搭載されていますので、これを使うと、上図右側の様なリファレンス電圧生成回路による出力電圧VrとVoを比較することで、閾値電圧Vrとの大小を比較することが可能です。R3=R1に設定しておいて、抵抗R4を、LEDキャンドルを点灯すべき暗さになった時のCdSセル抵抗値に設定することで、暗さを判定することが出来ます。
これで良いのですが、この回路構成だと、閾値の調整をするのに一々抵抗値R4(すなわち回路定数)を変更する必要があって、面倒です。
そこで今回はもう少しひねって、PWM出力を使った簡便なDA変換器によってリファレンス(閾値)電圧Vrを生成しようと思います。PWMデューティで出力電圧を制御できるので、ソフトウェアで閾値を調整できることが利点です。
パルス幅変調型(以降、PWM型)DA変換器と言うのは、パルス幅変調(PWM;Pulse Width Modulation)と低域通過フィルタ(LPF;Low Pass Filter)を組み合わせて、アナログ電圧を出力するDA変換器です。
PWMと言うのは、二値(LOW/HIGH)のデジタル信号を使って、その平均値として中間電圧を出力する技術なのですが、波形そのものは単なるデジタル矩形波でしかありません。そこで、PWM出力を低域通過フィルタ(LPF)に通して、高周波数成分を除去してやると、以下図の様に、平均的な電圧値(青線)を取り出すことが出来ます。
上の図は、デューティ80%(=51.2μs/64μs)のPWM波形(緑線)と、LPF通過後の波形(青線)をLTspiceでシミュレーションしたものです。
青線はギザギザになっていますが、300〜500[μs]範囲の平均値は約4Vを示しており、5Vのデューティ80%、すなわち5V×80%=4Vと一致しています。
上図のシミュレーションを実行した時の回路図は以下図の通りです。
LPF通過後波形(青線)のギザギザが気になる場合には、LPFのカットオフ周波数fc=1/(2π×C1×R1)を小さくすれば、次第に平らになって行きます。その場合、時定数C1×R1が大きくなるため、PWMデューティを変更した時の出力電圧安定時間が伸びることになります。
出力電圧の安定時間を取るか、平滑度を取るか、の取捨選択です。
今回は、一度決めた閾値(デューティ)をダイナミックに変更する用途は考えていないので、多少安定時間が伸びても、出力電圧が平坦である方が良いのですが、将来の応用で少し考えるところがあり、その試行も兼ねて上回路図の時定数(64μs)を選択しました。
この値(64μs)はPWM周期(255×2×1/(fcpu/1)=63.75μs)とほぼ等しく、従って出力電圧波形には充放電に起因する大きなギザギザが残ってしまいます。……平滑度より安定時間短縮の方を選択した。
手持ちのコンデンサにはバリエーションが少ないので、その中から100nFのセラミックコンデンサを使うことに決めました。時定数を64μsにするためには抵抗値が640Ωです。そんな中途半端な抵抗は手持ちに無いのですが、「#13-6 赤外線リモコンを作ろう」で提供したスクリプトを使ったところ、1kΩ//2kΩ≒667Ωの組み合わせが比較的偏差も小さく使えそうです。
PWM周期とぴったり合わせる必要は無いので、なるべく近い値を使います。ただし、時定数は出力電圧の安定時間を決める重要なパラメータなので、あまり偏差が大きくならないように注意してください。
※今回の使い方では、時定数のパラメータは重要ではないので、少し大きめですが1kΩ程度の抵抗を1個だけ使って実装しても良いです。
PWM型DA変換器で出力した閾値電圧と、CdSセルが出力する電圧をアナログ比較ユニットで比較して、LEDのON/OFFをコントロールするのですが、PWM型DA変換器のアナログ出力は前節で示したシミュレーション波形(青線)の様にギザギザです。
シミュレーション波形から読み取ってみると、ギザギザの山と谷の電位差は0.8Vほどもありますので、PWM波形と非同期にアナログ比較結果を読み出してしまうと、比較結果がばらついてしまいます。
ひっくり返すと、PWM波形に同期してアナログ比較結果を読み出せば、常にギザギザ波形の同じタイミングで読み出すことになるので、安定した結果を得ることが出来るのです(↓)。
static volatile bool acmp_posi; // acmp: AIN0>AIN1
ISR (TIMER1_OVF_vect)
{
// TOI1 interrupt service routine
acmp_posi=(ACSR&0x20)? true: false;
}
上のコードは、PWMを発生するタイマ(TC1)のオーバフロウ割り込みサービスルーチンです。この中で、アナログ比較ユニットのACSRレジスタから比較結果(第5ビット、0x20)を読み出して、acmp_posi変数に設定しています。
アナログ比較ユニットは、非反転入力AIN0、反転入力AIN1を比較して、AIN0>AIN1の場合に、ACSRレジスタの第5ビットに1が立つ様になっています。
※ATtiny2313の端子配置表はこちら。
loop()関数内など通常処理中でacmp_posiを参照すると、アナログ比較結果を使って明るさを検知することができます。
※割り込みISR内で値が更新される変数acmp_posiはvolatile修飾してあるので、通常処理関数内から問題なくアクセスすることが出来ます。
PWMを発生するタイマの設定としては位相補償モードを使いました。位相補償モードは、カウンタが0から255までカウントアップしたら、ダウンカウントに切り替わって254から0まで変化するモードです。このモードでは、カウンタが0になった時にオーバフロウ割り込みが発生します(↓)。
// TC1 setting
// OC1A pwm output, pwm phase correct 8 bit mode 1, fcpu/1, TOI1 enable
TCCR1A=0x80 + 0x01;
TCCR1B=0x00 + 0x01;
noInterrupts ();
OCR1A=dty_thresh;
TIMSK |= 0x80;
interrupts ();
本モードでオーバフロウ割り込み(TOI1)が起きるのは、PWM波形でHIGHを出力する期間のちょうど真ん中です(下図)。
HIGH期間のちょうど真ん中なので、LPF通過後のギザギザ波形でも、山と谷の電位差のほぼ中央辺りで比較することになって、Goodタイミングです。
デューティは定数dty_threshで設定してコンパイルするようになっています。
const int dty_thresh=64; // led on/off threshold duty
CdSセル出力信号Voの電位をテスタなりLogiscopeなりで測定して、LEDをON/OFFすべき電位になる様にdty_threshを決定してください。閾値電圧Vothとすると、dty_thresh=Voth/5V×256で求められます。
ついでなので、アナログ比較ユニットのレジスタ設定も以下に示しておきます。
// analog comparator setting
ACSR=0x00;
DIDR=0x03;
と、言っても、元々デフォルトでアナログ比較ユニットは有効になっていて、常にAIN0とAIN1の値を比較しているみたいです。アナログ比較ユニットを無効化するビットがACSRレジスタにあるのですが、そのビットはリセット解除後の初期値で「有効」側になっています。
電流消費を削減したい時のため、アナログ比較ユニットを無効化する設定が可能になっている様です。
DIDRレジスタは、AIN0およびAIN1のデジタル入力バッファを無効にするためのレジスタです。リセット解除後の初期値はデジタル入力が有効になっています。従って、digitalRead()関数でAIN0,AIN1端子を読み出すことが出来ます。
実は、デジタル入力バッファと言う回路は、デジタルHIGHとLOWの中間付近の電圧が入力されてしまうと、NMOSとPMOSが同時にONしてしまい、貫通電流が流れて電流消費量が跳ね上がってしまう特性を持っています。従って、アナログ端子(AIN0およびAIN1)をアナログ入力として使用する場合には、デジタル入力バッファを無効にしておくことが望ましいのです。
※ふと思いました、「ATmega328はどうなっているの?」。ATmega328にもDIDR1/DIDR2と言うレジスタが有って、アナログ入力端子のデジタル入力を禁止することが出来るみたい(今まで気づかなかったよ……)。
今回の回路は、ATtiny2313とLEDキャンドルの周辺回路すべてを170穴ミニブレッドボード(下図)上に載せたいと思います。
従って、「#13-6 赤外線リモコンを作ろう」等で工夫した様に、なるべく部品点数を減らして実装する必要があります。と言っても、今回はATmega328(28DIP)と違ってATtiny2313(20DIP)なので、同じミニブレッドボードに載せるにも、多少スペースに余裕があります。
もし、328duinoでLEDキャンドルを作るなら、「#13-6 赤外線リモコンを作ろう」と同じ様にI/Oポートに内蔵されたプルアップ抵抗で光らせようと思っていたのですが、ATtiny2313は20DIPと、少し小さなパッケージになったおかげでミニブレッドボードでも十分なスペースがあります。
また、後ほど実験する時にご確認いただきますが、今回は3V電源を使いますので、低電圧(3V)でも明るく光る様に、外部に適切な抵抗を接続して光らせようと思います(↓)。
LED(赤)のVf≒2V
LEDに流す電流(目標)If=500μA
電源電圧Vcc=3V
電流制限抵抗Rfとして、
(Vcc−Vf)=If×Rfより、
Rf≒2kΩ
この回路定数だと、電源電圧Vcc=5Vで光らせる時はIf≒1.5mAと、少し明るくなりますので、適切に抵抗値を調整してください。
下図左側回路(再掲)は、CdSセルを使って暗さを検知する一般的な回路です。
この回路では、抵抗分圧で電圧Voを出力するため、CdSセルと直列に抵抗R1を接続してください。抵抗R1は直接Vccに吊られているので、形の上ではCdSセルをプルアップしていることになります。つまり、このR1はATtiny2313内蔵のプルアップ抵抗Rpuに置き換えることができます。
上図右側回路は、こちらで検討した通り、PWM型DA変換器に置き換えます。
前節のLEDは、低電圧(3V)で十分な明るさを確保するため適切な抵抗を外付けにしましたが、こちらは特段の事情がありませんでしたので、I/Oポート内蔵プルアップ抵抗を使って部品点数を削減しました。
完成した回路図は以下の通りです。
赤外線リモコン化328duinoなどと異なり、ATtiny2313を使ったことでスペースに余裕があります。この程度であれば、配置配線図無しで構築することができます。
プログラムはオリジナル(led_candle.ino)からの抜粋だし、ここまでの解説の中で、ポイントとなるコードは示して解説済みなので、以下に完成したプログラムをリンクしておきます。
完成プログラム:tiny_candle.ino
※Arduino as ISPを使ったATtiny2313へのプログラム書き込みについてはこちらを参照してください。
すべてのプログラムを組んだ後で、ATtiny2313ターゲットでコンパイルしたところ、幸いにも1,024バイト以上のFlash空き領域を確保することができました。よって、こちらで検討した1/fゆらぎ乱数列はN=1,024点としました。1/fゆらぎ乱数列は1点当たり1バイトの記憶領域を必要とするので、最終的に手元のコンパイル結果は1,024バイト増加して「Flash 1,970バイト使用」と、なりました。
もし、GCCやコアライブラリのバージョンによって、2Kバイトを超えてしまった場合は、何とか自力でプログラムを詰めるなり、1/fゆらぎ乱数列をN=512点に減らすなりして対処してくださいm(1__)m。
以下、実験中の様子です。
電源は、CR2025ボタン電池(3V)を使いました。ATtiny2313が小さかった(20DIP)おかげで、ボタン電池ホルダまで含めて全てミニブレッドボードに載りました。ATmega328(28DIP)では、ボタン電池ホルダと干渉してしまって1つのミニブレッドボードには載りません。
以下、動作させた時の様子を撮影した動画です。暗くなると自動的にLEDキャンドルが点灯して、「暗くなったら蝋燭を灯して」となっていることをご確認ください。
1/fゆらぎ乱数列を×1/4サイズに圧縮したのですが、特に問題ないと思われませんか?
今回のお買い物は、以下の通りです。
誤植じゃなくて、セールで9円でした(1^^)。追加購入したATmega328と一緒に購入しておいた部品の一つです。ブレッドボードに挿さる程度の足の長さと、ピンピッチがGoodでした。
ボタン電池ホルダの型名はBS2032-Cですが、実験ではCR2025というボタン電池を使いました。ボタン電池の型番は、最初の2桁が直径(20mm)、後ろの2桁が厚さ(2.5mm)だそうです。普通は厚さを合わせておかないと、うまく電極と接触できず、通電しません。今回購入したボタン電池ホルダは、下図の様に+電極がばねの様になって電池と接触するので、多少の厚さの違いは吸収できます。
ボタン電池ホルダのお陰で、ミニブレッドボード上に独立動作可能なアプリケーションを作り込むことが出来るようになりました。ボタン電池は出力電圧3Vなので、ATmega328でもATtiny2313でも、電源電圧に10%マージンを持たせた上でfMAX=10MHz動作可能です。