ATtiny13Aを使ってボタン電池で動く シーリングライト リモコン を作ってみた【準備編】
Back to Top
この1年、業務ではC#で開発しており潤沢な開発環境、CPU・メモリリソースの元、速度やメモリ効率を追求することはやめて「富豪的プログラミング」で開発していました。
一方、プライベートではArduinoを使っていろいろな電子工作を楽しんできました。そこで思ったのが、
「マイコンのスペック全然使いきれてないじゃん」
でした。
手のひらに乗っかる1チップマイコンですが今まで自分が作ったものは速度・プログラムサイズ・データサイズなどはあまり気にしていませんでした。利用できるピンも余っておりマイコン内蔵の機能も1つか2つのみで限界まで性能を引き出したという達成感がありませんでした。Arduinoでも「富豪的プログラミング」を実施していたわけです。
そこで今回は、この流れに逆行しますが
「ハードウェアの性能を限界まで引き出し、低予算で何か役に立つものを作る」
をテーマにシーリングライト(LED照明)のリモコン作成にチャレンジしてみました。
記事の文字数が多くなったため【準備編】【開発編】【基板・ケース作成編】に分けて記載します。
どのように取り組んだか備忘録も兼ねてその過程を記事としてまとめました。
ATtiny13A概要
#Arduinoには様々なものありますがArduino Uno R3はATMega328Pというマイコンが利用されています。ATはマイコンを開発したAtmel社[1]、Megaは大きなという意味合いかと思います。
そう、マイクロコントローラとはいうけどCPUはMegaなのです。Arduino開発環境で使えるもっと小さい(Tiny)ものがないかと探したところATtiny13Aがありました。
ATtiny13Aの主なスペック
#ATtiny13Aのデータシートよりいくつか抜粋[2]
項目 | スペック・概要 |
---|---|
CPU | AVR 8Bit マイクロコントローラ |
クロック | 最大20MHz |
動作電圧 | 1.8-5.5V |
フラッシュメモリ | 不揮発性メモリ(高速・大容量) 1024Byte (プログラム保存領域) |
EEPROM | 不揮発性メモリ(低速・小容量) 64Byte (データ保存領域) |
SRAM | 揮発性メモリ(高速) 64Byte (ワーキングメモリ) |
タイマー | 8Bitタイマー1つ |
アナログデジタル変換 | 4チャンネル 10Bit |
AVRのマイコンはハーバードアーキテクチャといってプログラム命令とデータが物理的に分割されて管理されています。そのためプログラムサイズとデータサイズを意識してプログラムを作成する必要があります。
ATtiny13Aは1.8Vで動作するのでボタン電池(3V)駆動が可能です。プログラム容量はフラッシュメモリの容量1024Byte、データ容量はSRAMの容量64Byteまでしか利用できません。この制限内でどのようにプログラムを記述するかがポイントになります。
ちなみにトップ画像はATtiny13Aのサイズ感がわかるように単4電池と一緒に撮影したものです。この画像サイズは135KByte(139190Byte)ありATtiny13Aのフラッシュメモリ1024Byteの約135倍もあります。たったの1024Byteでリモコンのプログラムが書けるのか少し不安がありました。
プログラムはC,C++で記述(アセンブラも可能)します。
SRAMが64Byteしかないので文字列データはASCIIで64文字しか利用できないのでデバッグ文の挿入もプログラム容量とデータ容量が消費するため満足に使えません。そもそもデバッグ出力(UART)もハードウェアではサポートしていません🫠
ピンレイアウト
#ピンは8ピンあり左上から反時計回りに1から8の番号が割り当てられています。
pin8は電源、pin4がGNDにつなげるため1から3と5から7の6ピンに機能が割り当てられています。
1つのピンには入出力ポートを表すPB(0から5の6つある)の他にも上記に記載されている機能から1つ割り当てることができます。
例えばpin6にはINT0が利用できるとありますが、INT0は外部割込みを表します。
pin6にスイッチやボタンを接続し、それらの状態が変化したときに外部割り込みハンドラ(関数)がコールされるといったことができます。
また、ADC0からADC3がそれぞれのピンで利用できます。ADCはアナログ値(電圧値)を読み込んで10bit(1024段階)の値に変換する機能です。
各ピンごとに利用できる機能が異なることに注意が必要です。
ArduinoのAPIを利用すれば簡単に設定ができます。しかしATtiny13AでArduino APIを利用するとプログラムサイズが大きくなり、すぐに1024Byteを超えてしまうため現実的ではありません。そのため直接レジスタを操作して機能を割り当てることになります。
レジスタ操作
#レジスタとはマイコンの状態を保持したり、ピンアサインを変更するときに使われる領域でプログラム(C,C++,アセンブラ)からレジスタの値を参照・設定できます。プログラムから見ると単なる変数(メモリ上のアドレスを指す)のように見えます。
レジスタの例
レジスタ | レジスタ名 | 説明 |
---|---|---|
DDRB | ポートB方向レジスタ | ポートが入力ポートか出力ポートかを設定 |
PORTB | ポートB出力レジスタ | ポートのON/OFFを設定 |
TCCR0A | タイマ/カウンタ0制御レジスタA | タイマ/カウンタを制御する |
OCR0A | タイマ/カウンタ 比較Aレジスタ | タイマ/カウンタの比較値の設定 |
PRR | 電力削減レジスタ | 電力削減に関する設定 |
GIMSK | 一般割り込み許可レジスタ | 割り込み許可に関する設定 |
PCMSK | ピン変化割り込み許可レジスタ | ピン割り込み許可に関する設定 |
直接レジスタを操作するとプログラムサイズを低減できLOWレベルの操作ができます。一方、マイコン依存になってしまうというデメリットがあります。またデータシートを調べながらプログラムするため調べる手間が増えます。
マイコンごとにピンレイアウトやレジスタが異なります。Arduino APIはこれらのレジスタ操作の違いをAPIで吸収しています。そのため様々なマイコンを同じ関数で制御できるようになります。難点としてはヘッダ定義でプリプロセッサ(#if,#ifdef, #define)の分岐が多くてわかりにくいなどあります。
通信フォーマット
#赤外線送信にはいくつか規格があり、各メーカー・企業はその規格に沿って赤外線送信しているのが多いようです。日本ではNECフォーマット/家製協フォーマット/SONYフォーマットがよく使われているそうです。
通信フォーマットについて理解するために以下のサイトを参考としました。
通信フォーマット
赤外線リモコンのフォーマット
赤外線リモコンの信号定義データの合成
38KHz変調のパルス送信イメージ
リビングのシーリングライトのリモコンの型番を見るとPanasonic HK9493とありました。このリモコンがどの通信フォーマットで赤外線送信しているかを調べる必要があります。
開発環境まわりの準備
#Arduino IDEで開発するための設定
#Arduino IDEを利用してATtiny13Aの開発するには以下が必要です。
- ボードマネージャとして
MicroCore
- スケッチ(プログラム)の書き込み装置(私はArduino Unoを利用)
ボードマネージャおよびインストールは下記を参考にしていただければと思います。
Arduino IDEは過去の資産もあるため 1.8.19 を利用しています。
Arduino IDE 2.X.X で動作するかは不明です。
赤外線送信データの解析
#さて赤外線送信データはどのように入手するのでしょうか?メーカーのホームページでは情報は公開されていません。
赤外線リモコン受信モジュールを利用して実際のリモコンから送信される赤外線データを受信し、そこから通信フォーマットを割り出す必要があります。
Arduinoにはオープンソースで素晴らしいライブラリが豊富に提供されています。
IRremoteという赤外線データを送受信するライブラリがあり、これを利用してデータを解析しました。
赤外線リモコン受信モジュールは秋月電子でOSRB38C9AAを購入しました。データシート[3]にある応用例を元に簡易的にブレッドボードで組んでみました。
左の黄色のラインが赤外線受信データを出力するOutputで、右の黄色が電源で、緑がGNDです。
3本足で直立しているのが赤外線リモコン受信モジュールです。
詳細は省きますがArduinoで以下のようなスケッチを作成し先ほどのブレッドボードを繋ぎます。
#include <IRremote.h>
int receiverPin = 8;
void setup() {
Serial.begin(9600);
IrReceiver.begin(receiverPin, true);
}
void loop() {
if (IrReceiver.decode()) {
// 受信した信号を送信するためのコードを表示
IrReceiver.printIRSendUsage(&Serial);
// RAWフォーマットで結果を表示
IrReceiver.printIRResultRawFormatted(&Serial, true);
IrReceiver.resume();
}
}
リモコンを赤外線リモコン受信モジュールに向けて何か赤外線を送信すると下記のように出力されます。
uint32_t tRawData[]={0x9939522C, 0xA0};
IrSender.sendPulseDistanceWidthFromArray(38, 3450, 1700, 450, 1300,
450, 450, &tRawData[0], 40, PROTOCOL_IS_LSB_FIRST,
<RepeatPeriodMillis>, <numberOfRepeats>);
-3276750
+3450,-1700
+ 450,- 400 + 450,- 400 + 450,-1300 + 400,-1300
+ 450,- 400 + 450,-1250 + 450,- 450 + 400,- 450
+ 400,- 450 + 450,-1250 + 450,- 400 + 400,- 450
+ 450,-1300 + 400,- 450 + 450,-1250 + 450,- 400
+ 450,-1250 + 450,- 450 + 400,- 450 + 400,-1300
+ 400,-1300 + 450,-1300 + 400,- 450 + 400,- 450
+ 400,-1300 + 450,- 400 + 450,- 450 + 400,-1300
+ 450,-1250 + 450,- 400 + 450,- 400 + 450,-1300
+ 400,- 450 + 400,- 450 + 400,- 450 + 450,- 400
+ 400,- 450 + 450,-1300 + 400,- 450 + 400,-1300
+ 450
Sum: 53600
uint32_t tRawData[]={0x9939522C, 0xA0};
- 0x9939522Cが受信したデータ
- 0xA0がパリティ
IrSender.sendPulseDistanceWidthFromArray()
の引数の説明void IRsend::sendPulseDistanceWidthFromArray( // 周波数[kHz] uint_fast8_t aFrequencyKHz, // リーダ部 送信ON時間[usec] uint16_t aHeaderMarkMicros, // リーダ部 送信OFF時間[usec] uint16_t aHeaderSpaceMicros, // データ部 送信するbit値が1のときの送信ON時間[usec] uint16_t aOneMarkMicros, // データ部 送信するbit値が1のときの送信後待ち時間[usec] uint16_t aOneSpaceMicros, // データ部 送信するbit値が0のときの送信ON時間[usec] uint16_t aZeroMarkMicros, // データ部 送信するbit値が0のときの送信後待ち時間[usec] uint16_t aZeroSpaceMicros, // 送信データ配列 IRRawDataType *aDecodedRawDataArray, // リーダー部送信ビット数 uint16_t aNumberOfBits, // ビット送信順序 uint8_t aFlags, // リピート時の待ち時間 uint16_t aRepeatPeriodMillis, // リピート回数 int_fast8_t aNumberOfRepeats )
- +-の数値の羅列はビット値(0または1)の受信時間を表しておりIrSender.sendPulseDistanceWidthFromArray関数の引数を算出するときに利用したデータとなっています。
- +がビット値受信時間[usec]
- -がビット値未受信時間[usec]
+と-が交互に一定時間の間隔で繰り返しているのがわかります。
リーダ部のON/OFF時間は +3450(8T), -1700(4T)
、データ部のON時間は +450(1T)
なので 家製協フォーマット
であることがわかりました。
中央部の +と-がペア
となっているところで
+が400から450で、-も400から450のペア
を0とする+が400から450で、-が1250から1300のペア
を1とする
これを2進数にし、LSB(最下位ビット)から読んだときの値を16進数で表すと以下となります。
(+xxx, -xxx)(+xxx, -xxx)(+xxx, -xxx)(+xxx, -xxx) 2進数 16進数
+ 450,- 400 + 450,- 400 + 450,-1300 + 400,-1300 → 0011 C
+ 450,- 400 + 450,-1250 + 450,- 450 + 400,- 450 → 0100 2
+ 400,- 450 + 450,-1250 + 450,- 400 + 400,- 450 → 0100 2
+ 450,-1300 + 400,- 450 + 450,-1250 + 450,- 400 → 1010 5
+ 450,-1250 + 450,- 450 + 400,- 450 + 400,-1300 → 1001 9
+ 400,-1300 + 450,-1300 + 400,- 450 + 400,- 450 → 1100 3
+ 400,-1300 + 450,- 400 + 450,- 450 + 400,-1300 → 1001 9
+ 450,-1250 + 450,- 400 + 450,- 400 + 450,-1300 → 1001 9
+ 400,- 450 + 400,- 450 + 400,- 450 + 450,- 400 → 0000 0
+ 400,- 450 + 450,-1300 + 400,- 450 + 400,-1300 → 0101 A
この値は先ほどの uint32_t tRawData[]={0x9939522C, 0xA0};
のデータと対応していることがわかります。
0x9939522CをLSB(最下位ビット)から順にビット単位で読んだ時と同じになっており、データ送信後にパリティ0xA0をLSBから順に送っていることになります。
最終的には38KHz変調で以下のように送信します。
- リーダ部送信(今からデータを送信するよ)
- IRsend::sendPulseDistanceWidthFromArray()の第2引数の時間で1を、第3引数の時間で0を送信
- データ部送信
- 0x9939522CをIRsend::sendPulseDistanceWidthFromArray()の第4引数から第7引数の時間間隔でLSB(最下位ビット)から送信
- パリティチェック値送信
- 0xA0 をLSBで送信
- 時間間隔はデータ部送信と同じ
- トレーラ部送信(データ送信が終了したよ)
ポイントは1を送信するとき赤外線LEDを450[usec]間点灯し、1300[usec]間消灯するの ではなく 点灯時は38KHzの周波数でLEDを点滅を繰り返すことになります。
つまり 周波数38k[Hz] = 周期約26[usec]で点滅 = 13[usec]間隔で点灯/消灯 を450[usec]間実施することになります。
WindowsやLinuxといったOS上でマイクロ秒単位での制御を実現するのは相当難しい(実現不可?)のではないでしょうか?それが160円[4]のマイコンで実現できるとは感慨深いです。
シリアル通信によるデバッグ
#ATtiny13AはハードウェアでUARTを持っていませんのでプログラムの状態確認が難しいです。簡易的にLEDの点滅で確認するなどします。
ソフトウェアUART通信ライブラリを作るのも手ですがデバッグのためにわずかなピン数から入出力の2つのピンを利用するのはツライです。またUART通信ライブラリでプログラムサイズが圧迫されることにも繋がります。
UARTは非同期・全二重でシリアルデータ通信する方式です。ArduinoはハードウェアでUARTを行っています。マイコンのプログラムをデバッグするときなどはUARTに接続しターミナルソフトでプログラム内容を確認することが多いです。
これらの問題を解決するための素晴らしいライブラリをNerd Ralphさんが公開されています。
ATtiny用に1ピンのみを利用し半二重シリアル通信するアセンブラで書かれたBasicSerial3というライブラリです。わずか62Byteです。しかし、残念なことにリンクが切れていましたので別のサイトからBasicSerial3を入手して利用しました。
- picoUARTなる新しいものが公開されていますがアセンブラからC++に変更されておりROMサイズが倍くらいに増えています
- 私はBasicSerial3で十分でしたのでGithubのプロジェクトからBasicSerial3を利用しているプロジェクトを見つけて利用しました
白いブレッドボードがシリアル通信する回路です。黄色い1本の線をATtiny13AのPB3(pin2)に繋ぎ、ボーレートを115200にします。オレンジと黄色の線をUSB-シリアル変換アダプターのTX/RXに接続し、USBをPCに接続してTera Term等で表示します。
このUSB-シリアル変換アダプターはDTR信号が扱えるので自作のArduino互換機などでスケッチの書き込みなどもでき1本持っていると重宝します。
後は以下のように文字列出力用と数値出力用の関数を作成し、これらをコールすることでUART出力できます。
// 文字列出力
void serOut(const char* str) {
while (*str) {TxByte (*str++);}
}
// 整数を10進数で出力
void OutDEC(uint16_t d) {
int8_t n =-1;
uint16_t v = 10000;
for (uint8_t i=0; i<5; i++) {
if (d >= v) {
TxByte(d/v + '0');
d %= v;
n=i;
} else {
if (n!=-1||i==4) TxByte ('0');
}
v/=10;
}
}
まとめ
#いろいろと調べたり、部品を集めたり、テスト用の回路やプログラムを書いたりと準備に時間が掛かりましたがほぼリモコン制作のためのノウハウは揃いました。
モノはないのですが作れそうだという感触を得ることができました。
次回は【開発編】となります。