概要
ATtiny5シリーズ(ATtiny85/45/25)のもつPWM機能を使って、家電用の赤外線リモコンのための波形(38kHz、デューティ1/3)を作ってみる。Arduinoで使われているATmega8シリーズでは、タイマーカウンタユニット(TCユニット)がもつPWM機能を使うのが一番簡単だったので、ATtinyでも同じようにできるだろうと期待してデータシートを参考に調べてみた。
赤外線リモコンが送信するデータや波形については以前まとめたものを参照のこと。
PWMでの波形の作成
ATtiny5シリーズには、TC0とTC1の2つのTCユニットがあり、おのおのPWM機能を備えている。各TCユニットのカウンタ(TCNT0およびTCNT1)は8ビット長で、システムクロック(あるいはTCユニットのプリスケーラで分周したクロック)の刻みにより、動作モードに応じてカウントアップされたりカウントダウンされたりする。
前にも書いたようにATtiny5シリーズは少ないピン数(8ピン)の中にいろいろな機能が詰め込まれているから、PWM波形を出力できるピンによって、全体の構成が決まってしまうかもしれない。PWM波形が出力できるピン(TCユニットの比較出力ピン : Output Compare pin )は以下のようになっている。
- PB0 (#5) : OC0A、 /OC1A (OC1Aの反転出力)。
- PB1(#6) : OC0B、 OC1A
- PB3(#2) : /OC1B(OC1Bの反転出力)。
- PB4 (#3) : OC1B
(OCのあとの数字は、TCユニットの番号)
つまり、各TCユニットごとにAとBの2つの比較出力ピンがある。比較出力ピンは、TCユニットのカウンタ値が対応する比較出力レジスタ(Output Compare RegisterA およびB) の値に一致したとき、セット(HIGHを出力)またはリセット(LOWを出力)される。また、カウンタの値が0に戻ったときにもセットされたりリセットされたりする。こういった挙動をプログラミングすることで、任意の波形を出力させることができる。リモコン用の波形を作るときは、Fast PWMモードを使うのが簡単だった。
Fast PWMモードでは、TCユニットのカウンタ(TCNT0またはTCNT1)の値は、クロック(プリスケーラによって分周されたシステムクロック)が刻まれるたびにインクリメントされる(Phase Correct PWMモードの場合、指定期間のインクリメントとデクリメントを繰り返すことになるが、ここでは触れない)。
単純増加するカウンタ値は、比較出力レジスタに設定可能なTOP値に一致すると0に戻り、0からカウントアンプを再開する。このことから、TOPの値によって出力波形の周期を表現することができる。
また、別の比較出力レジスタに波形のHIGH期間を入れておき、対応する比較出力ピンが、
・カウンタが0ならセット(HIGH出力)
・値一致ならリセット(LOW出力)
と動作するよう設定しておけば、任意のデューティー比の波形を作成できる。
PB1ピンに波形を出力する
PB1ピンはTC0のOC0BでありTC1のOC1Aでもある。TC0ユニットを使う場合とTC1ユニットを使う場合それぞれを試してみた。
TC0での方法
- 事前にPB1を出力方向に設定しておく。
- 3ビットある波形生成モードビット(Waveform Generation Mode bits: WGM02-00)を111B とすることで、Fast PWMモードを指定。OCR0Aのもつ値がカウンタのTOP値となる。
WGM01とWGM00はTCCR0Aに、WGM02はTCCR0Bにある。 - 比較出力レジスタA (OCR0A) に出力波形の周期を設定。カウンタTCNT0の値がOCR0Aのもつ値と一致すると、TCNT0は0となりカウントアップをやり直す。つまり、カウンタの値は0~OCR0Aを繰り返すことになる。
- 比較出力レジスタB (OCR0B) に、波形のHIGH期間を設定する。
- TCCR0A (TC0コントロールレジスタA)の、COM0B1ビットを1,COM0B0ビットを0に設定。 これにより、OC0Bピンは、TCNT0 == 0 のときセットされ、TCNT0 == OCR0B のときリセットされる。
- OC0Aピン(PB0)をPWM出力と切り離すため、COM0A1ビットとCOM0A0ビットには0を設定する。
リモコン用に38kHzの波形を作るので周期は約26.3μsecとなり、デューティー1/3なのでH期間は約8μsecということになる。IrLEDを駆動するときにはH期間が短いほど電力的には有利になるし、大きな電流を流せるので、習慣的に6~7μsecにしている。システムクロック1MHz(1周期が1μsec) なので、TCユニットのクロックはシステムクロックそのまま(プリスケーラ x1) とする。
※ 注意点
TC0ユニットはArduinoのコア部分が利用している。TC0のプリスケーラやカウント値などを変える場合、millis()、micros()、delay() といった関数は仕様どおりには動かなくなる。スケッチ内で遅延をおきたいときには、delayMicroseconds() だけが利用できる。
注意:以下のスケッチは8MHz内部発振をクロックプリスケーラで1/8 (FUSEローバイトのCLKDIV8 == 1) したシステクロック1MHz動作のATTinyを用いる前提で書いてあります。クロックが違えば、設定すべきパラメータも変わってきます。
TC0用スケッチ
以下のような短いスケッチをArduino IDEでビルドしてATtiny85に書き込み、USBロジアナ(ZEROPLUS Logic Cube) を使って波形を調べてみた。電源はUSBからの+5V弱、ATtiny自体は前に載せたArduino PRO MINIのISPボードに載せたままで動かした。なお、PB4の立上りをトリガとして利用している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <avr/wdt.h> #define OUTPUT_PIN (_BV(4) | _BV(1)) void pwm_out() { PORTB |= _BV(4); OCR0A = 25; // TOP value : 38kHz --> 26.3us. OCR0B = 6; // H period : 6.4us. TCCR0A = B00100011; // COM0A: 00, COM0B: 10, WGM01-00: 11 TCCR0B = B00001001; // FOC:00, WGM02:1, CS02-01: 001 (system clock x1) TCNT0 = OCR0A; delayMicroseconds(560); TCCR0A = 0; TCCR0B = 0; PORTB &= ~_BV(4); } void setup() { cli(); DDRB = OUTPUT_PIN; // inputは0 PORTB = 0; // 他は0 ACSR |= 0x80; // アナログコンパレータ禁止 ADCSRA &= 0x7f; // disable ADC wdt_disable(); TIMSK = 0; // disable Timer0/1 overflow interrupt. sei(); for(;;) { pwm_out(); delayMicroseconds(560); } } void loop() {} |
void setup()
ポートピンの初期化と省電力化のための定番の設定を行い、タイマー0のオーバーフロー割込みは不要なので禁止している。タイマー0の割込みは、Arduino のコア部分にあり setup()の直前に呼ばれる init(); で許可されている。
void pwm_out()
先に書いたようなことをTC0の各レジスタに対して実施し、赤外線リモコンのNECフォーマットを意識して 560μsecの遅延を置いた。この遅延のあと、TCCR0A/Bを0として波形出力を停止している。
TOP値として OCR0A = 25; としているのは、1周期を約26μsecとするためで、0のとき1クロック、1のとき1クロック… 25のとき1クロックで26μsecになるから。
TCNT0 = OCR0A; とすることで、次のクロックでカウンタとTOP値の一致が認識されカウンタが0に戻る(と思う)。カウンタが0になった時点で、OCR0Bの値が比較のための内部レジスタに転送され、意図した値での比較動作が行われるようになるはず。
TC0のPWMモードでは、比較出力レジスタはプログラムからのアクセス用のレジスタと、TCNT0との比較に使われるレジスタとに二重化されている。波形生成モードビットに応じて、TOPまたは0のとき、プログラムから設定した値が内部レジスタに転送されるようである。
TC0での実行結果
ロジアナで観測した結果は以下のとおり(1MHzサンプリング)。
意図通りの波形ができている。約26μsec周期で約7μsec幅のH期間をもつ波形となった。水色のトリガ信号がHIGHの期間に21周期分出力しているので赤外線リモコンのオン期間として使えるだろう。
9行目を、TCNT0 = OCR0A; ではなく、TCNT = 0; とした場合は以下のようになる。
トリガ信号の立上り時点から、約26μsec後に最初のHパルスが出ており、出力は20周期分となっている。これは、TCNT0がTOP( = 25 )に達したあとでOCR0Bの指定が有効になったことによる。もっとも、これでもリモコンとしては機能するのだが。
pwm_out() 内でdelayMicroseconds(560); としているが、トリガ信号の観測されたHIGH期間がちょっと短いのが気になるところ。数パーセントの相違なので、内蔵RCオシレータの許容範囲内かもしれないが。
TC0でのやり方は、Arduino PRO MINIなどで使われているATmega328pで赤外線リモコン用の波形を出力するときとよく似ており、ほぼ同じやり方を踏襲できると思う。
TC1での方法
TC0を触りたくない(Arduino本来の使い方のままにしたい)ようなときはTC1ユニットを使って出力することになるが、TC0とは若干構成や波形の作り方が違っている。
データシートによれば、非同期モードでは内蔵のPLLオシレータによる最大64MHzのソースクロックを14ビット分解能のプリスケーラで分周することで、正確かつ高速なPWMが実現できるとのことだが、ここではシステムクロック(1MHz)をソースとして用いる同期モードを用いている。
- 比較出力レジスタが3本(OCR1A、OCR1B、 OCR1C)。
- カウンタのTOP値の指定はOCR1Cで行う。TCCR1(TC1コントロールレジスタ)内のCTCビットにより、TCNT1 == OCR1Cのときの挙動(TCNT1を0とするか、何もしないか)を決める。OCR1CをTOP値として使うので”1″を設定。
- OCR1AまたはOCR1Bの値がTCNT1と一致すると、対応する比較出力ピン(OC1A、OC1B)がセットまたはリセットされる。
セットにするかリセットにするかは、TCCR1のCOM1A1ビットとCOM1A0ビット、および、GTCCR (汎用TCコントロールレジスタ ) 内のCOM1B1ビットとCOM1B0ビットに設定する値で決まる。 - A側でPWMを使うときはTCCR1のPWM1Aビットを”1″に、B側では、GTCCRのPWM1Bビットを”1″にする。
- 比較出力ピンは2系統(OC1AとOC1B)だが、それぞれ反転出力用のピンもある。ただし、COM1A1またはCOM1B1を”1″とした場合、対応する反転出力ピンはPWM機能から切り離され、他の用途で利用可能となる。
- プログラムからのOCR1nやTCCR1への書き込みは、即座(2クロック以内)に比較や設定用の内部レジスタに反映される。
TC1用スケッチと実行結果
先のTC0用との相違はpwm_out(); 内だけになる。
1 2 3 4 5 6 7 8 9 10 11 12 |
void pwm_out() { PORTB |= _BV(4); OCR1C = 25; // TOP value : 38kHz --> 26.3us. OCR1A = 6; // H period : 6.4us. TCCR1 = B11010001; // CTC=1, PWM1A=1, COM1A[1-0] = 01, CS13-10: 0001 (x1) GTCCR = 0; TCNT1 = 0; delayMicroseconds(560); TCCR1 = 0; GTCCR = 0; PORTB &= ~_BV(4); } |
TCCR1のCOM1A1を0, COM1A0を1とすることで、TCNT1 == 0のときOC1Aがセットされ、TCNT1 == OCR1Aのときリセットされる。
ロジアナでの観測結果は以下のとおり。
TC0ユニットと違ってOCR1Aが即時反映されているので、TCNT1 = 0から開始してもすぐにHパルスが出ている。
と思っていたら、データシートにTC1のPWMモードでOCR1A/Bの値が確定するのはTCNT1の値がOCR1Cの値と一致したときと書いてあった。波形を見ると、TCNT1 = 0 から開始してもOCR1Bは反映されているように思えるのだが。何か誤読があるのかもしれない。
TC0とTC1のPWM波形の相違
TOP値やHIGH期間の幅は同じように指定したのに、TC0の波形とTC1の波形をよく見ると、TC1の方がHIGH期間の幅が1μsec短い。気になったので、TC0はPB1(OC0B)に、TC1はPB4(OC1B)に同時に出力させ、トリガ信号はPB3から出すようにして波形を観測してみた。
2つの波形を比べてみると周期は一致しているが、TC1から出力した水色の波形はHIGH期間が1μsec短いことが分かる。
スケッチは、TC1の出力先をOC1Bとするための変更を入れ、ほぼ同時に波形を出すための小細工を入れたが、あとはほとんど同じで以下のようになった。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void pwm_out() { // TC0 PB1 (OC0B) and TC1 PB4 DDRB = _BV(4) | _BV(1) | _BV(3); PORTB |= _BV(3); OCR0A = OCR1C = 25; // TOP value : 38kHz --> 26.3us. OCR0B = OCR1B = 6; // H period : 6.4us. TCCR0A = B00100011; // COM0A: 00, COM0B: 10, WGM01-00: 11 TCCR0B = B00001000; // FOC:00, WGM02:1, CS02-01: 000 (stop clock) GTCCR = B01100000; // OC1B cleared on compare match. Set when TCNT1 = $00. TCCR1 = B10000001; // CTC=1. clear TCNT1 on OCR1C==TCNT1. CS13-10 = 0001B (x1) TCNT1 = 0; TCCR0B |= 1; // enable clock (x1). TCNT0 = OCR0A ; delayMicroseconds(560); TCCR0A = 0; TCCR0B = 0; TCCR1 = 0; GTCCR = 0; PORTB &= ~_BV(3); } |
観測波形から分かるように周期は一致しているから、カウンタ(TCNT0/1) は同じように振動していると思われる。波形を出力する際の、OCR1BとOCR1Cの比較のタイミング、あるいは、比較後の出力リセットのタイミングが違うのだろうか。
データシートを読んでも相違の理由はよく分からなかった。TC0ユニットでは期待するHIGH期間を-1した値をセットするが、TC1ではそのまま(7μsecなら7)指定する、と覚えておくしかなさそう。上記スケッチの7行目を、
1 2 3 |
// OCR0B = OCR1B = 6; // H period : 6.4us. OCR0B = 6; OCR1B = 7; |
とすると、同じHIGH期間をもつ波形が生成されることは確認した。
きょうのまとめ
ATtiny85から赤外線リモコン用波形を出力する際は、PB1(OC0BあるいはOC1A)またはPB4(OC1B)を使うことになりそう。同時にA/Dコンバータも使うなら、PB1から出すことになるだろう。
PWM出力用以外にポートピンをあと4本使えるので、スイッチ4つのリモコンの作成は簡単だろう。
誰でも知ってる話に時間かけてどうする的な印象もあるのだけど、これも遊びのうちということです。