シーリングライトの選定
使用開始から10年弱経過した部屋のLEDシーリングライトが勝手に消えるようになった
Hotalux社のHLDC08301SGを購入し交換した
選んだ理由は幾つかあるが
- 前の家で使っていた蛍光灯のホタルックが便利だった
- NECからの独立でリモコンが多分NECフォーマットなのでリモコン製作入門に丁度いいと判断
というわけでリモコン製作入門をする
NECフォーマット
まず最初にNECフォーマットについて理解する必要がある
elm氏の解説記事が非常に参考になった
T=562us
$=PWM( frequency=38kHz, duty=1/3, duration=T )
_=LOW( duration=T )
としたとき、
- リーダー
- 信号の開始を表す
$$$$$$$$$$$$$$$$________
- データ
- 32bitの固定長のデータを含むことができる
-
$_
が0、$__
が1に対応
- ストップビット
- データの末尾のビットを決定するための
$
- データの末尾のビットを決定するための
の順に構成される
$$$$$$$$$$$$$$$$________$___$___$_$___$___$___$___$_$___$_$___$_$___$___$_$___$___$_$___$___$___$___$___$_$___$___$___$_$___$___$___$___$
これのデータを読むと11011110101011011011111011101111
となり、今回はこれを0b11011110101011011011111011101111
==0xdeadbeef
のように読むことにする
正しい読み順が決まっているようだが今回は外部ライブラリを使うこともないので不自由ない
データ部32bitはカスタマーコード16bit、データ本体8bit、その反転8bitと言われているが、実際に読むと最後の8bitがデータ本体の一部になっていたりするので、とりあえず32bitのデータがあるんだな程度の認識で良いと思う
リモコンの解析
付属のリモコンの型番はRE0212
手持ちの赤外線リモコン受信モジュール GP1UXC41QSとハンドヘルドオシロ HDS272S
で波形をCSVファイルとして取得した
受光モジュールのV_outは基本HIGHで受光するとLOWになる
チャンネルやボタンを変えながら色々試した
以下は中央ボタンch1の波形データである
Channel :,CH1
Frequency :,F= ?
Period :,T= ?
PK-PK :,Vpp=4.640V
Average :,V=3.383V
Vertical pos :,0.00mV
Probe attenuation :,10X
Voltage per ADC value:,80.00mV
Time interval :,40.00uS
index,CH1_Voltage(mV)
0,4480.00
1,4560.00
2,4560.00
3,4560.00
4,4480.00
5,4640.00
6,4560.00
...
890,4560.00
891,4480.00
892,4480.00
893,4560.00
894,160.00
895,80.00
896,0.00
897,0.00
898,0.00
899,0.00
900,0.00
901,0.00
902,0.00
903,0.00
...
1118,0.00
1119,0.00
1120,0.00
1121,4480.00
1122,4400.00
1123,4400.00
CSVを読んでデータをデコードするスクリプトを書いた
#!/usr/bin/bun
const
t=562,
b2u=w=>w.reduce((a,x,i)=>a<<1|x,0).toString(16).padStart(Math.ceil(w.length/4),0),
parse=w=>(
w=w.match(/\d+,\d+\.\d+/g).reduce((a,x,i,_)=>(
x=+x.split(',')[1]<1000,
a.x!=x&&(
a.p&&(
_=Math.round((i-a.p)*a.t/t),
a.x?a.a.push([_]):a.a[a.a.length-1].push(_),
a.b.push({l:_,i:[a.p,i],x:a.x})
),
a.p=i,a.x=x
),
a
),{
a:[],b:[],x:1,p:0,t:+w.match(/Time interval\s*:,(\d+\.\d+)uS/)[1]
}),
w=w.a.slice(1,33).map(x=>+(2<x[1])),
[
b2u(w.slice(0,16)),
b2u(w.slice(16,24)),
b2u(w.slice(24,32))
]
);
console.log(
(
await Promise.all(Bun.argv.slice(2).map(async x=>[x,
x.slice(-4).toLowerCase()=='.csv'&&parse(await Bun.file(x).text())
]))
).reduce((a,[i,x])=>(x&&(a[i]=x),a),{})
);
これに先程のCSVを渡した
csvの名前は ( Center, Up, Down, Left, Right, FnLeft, FnRight ) + チャンネル番号
~ % ./parse *
{
"c1.CSV": [ "41b6", "fd", "02" ],
"c2.CSV": [ "41b6", "fc", "03" ],
"d1.CSV": [ "41b6", "dd", "22" ],
"fl!1.CSV": [ "41b6", "8e", "44" ],
"fl1.CSV": [ "41b6", "f5", "0a" ],
"fr!1.CSV": [ "41b6", "65", "9a" ],
"fr1.CSV": [ "41b6", "8e", "98" ],
"l1.CSV": [ "41b6", "e5", "1a" ],
"r1.CSV": [ "41b6", "15", "ea" ],
"u1.CSV": [ "41b6", "5d", "a2" ],
}
32bitのデータが取れた
一応16,8,8bitで区切ったが、反転部分の8bitがデータ本体の拡張になっているボタンがちらほら
AVRに実装
ボタン電池(CR2032)、ATtiny202、IRLED、タクトスイッチ、念の為のFET(BSS138) だけでなんとかする
PWM( f=38k d=1/3 ), LOW()
まず38kHzのPWMを出せないとHIGHの表現すらできない
PWMにはTCA0を使う
クロック周波数の計算
ここで省電力化のためになるべくcpuのクロックを下げたい
クロックは20MHzか16MHz、
クロックの分周は2 4 8 16 32 64 6 10 12 24 48のいずれか、
TCA0のTOP値が適当なuint8_tで
(20M 16M)/(2 4 8 16 32 64 6 10 12 24 48)/(適当なuint8_t)≒38k
を作る必要がある
自由度の低い部分から計算して素因数分解をして様子を見る
20M/38k≒526=2*263
16M/38k≒421
突然素数で困惑
値を少し上下して素因数分解しやすい値を探すと
16M/420=38.095k
420=2*2*3*5*7=12*35
おそらく正しい探し方は
38k=2*19*1k
16M=20*20*40k
=(19*21+1)*40k
16M/38k≒19*21*40k/(2*19*1k)
=21*40/2
=3*7*2*2*5
=12*35
以上より12分周、TOP値は34(35回で1周期なので-1)とする
16MHzが扱いやすいというのはこういうことかと実感した
ところでもっと大胆に
20M/512=39.063k
512=2**8
というのも完成したあとに試したが、64分周だとcpuの処理が間に合わず、32分周だとなんとか動くがシーリングライトの反応が鈍かった
実装
PA7、GND間に繋がれたタクトスイッチを押している間38kHzを出すサンプル
押していない間はスリープして電力消費を抑える
CMP2を使ったがこちらは前のプロジェクトの使い回しで特に理由はない
CMPnBUFを使うことで突然のCMP値変更によるduty比や周波数の乱れを防ぐことができる
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
// 割り込みを有効にしてスリープ
static void sleep(){sei();SLPCTRL.CTRLA=SLPCTRL_SMODE_PDOWN_gc|SLPCTRL_SEN_bm;sleep_cpu();cli();}
// 割り込みリセット
ISR(PORTA_PORT_vect){PORTA.INTFLAGS=PORT_INT7_bm;}
void main(){
// クロックを 16M/12=1333k Hz に
_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB,CLKCTRL_PDIV_12X_gc|CLKCTRL_PEN_bm);
// TCA0 IR 1333kHz/35=38.095kHz
// TCA0有効化
TCA0.SINGLE.CTRLA=TCA_SINGLE_ENABLE_bm;
// CMP2有効化 単傾斜PWM
TCA0.SINGLE.CTRLB=TCA_SINGLE_CMP2EN_bm|TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
TCA0.SINGLE.PER=34;// TOP 35-1
// PA2を出力に
PORTA.DIRSET=0b100
// PA7をプルアップ付き入力に スリープ復帰用の割り込みもつける
PORTA.PIN7CTRL=PORT_PULLUPEN_bm|PORT_ISC_LEVEL_gc;
while(1){
sleep();
TCA0.SINGLE.CMP2BUF=11 // 35/3-1
while(~VPORTA.IN&(1<<7));
TCA0.SINGLE.CMP2BUF=0
}
}
[env:t202]
platform = atmelmegaavr
targets = upload
board_build.f_cpu = 1333333L
board_hardware.bod = 0
upload_protocol = serialupdi
upload_flags = -xrtsdtr=high
board = ATtiny202
pio run -t fuses
でヒューズ書き換え、pio run
でプログラム書き込み
ヒューズを書き込まないとRC発振器が20MHzで動いて指定した周波数で動作しないので注意
T=562us
正確な周期で何かを実行したい場合に<avr/delay.h>
を使うのはあまり良くない
命令A,B,C,Dがあってwhile(1){A;B;C;D;}
、かつDがdelayの場合、指定するべき値はABCの実行時間を考慮する必要がある
タイマーを使うことでこれを回避する
TCBが余っているのでこれをdelayに用いる
//...
void wait(){while(!(TCB0.INTFLAGS&TCB_CAPT_bm));TCB0.INTFLAGS=1;}
//...
void main(){
//...
// TCB0 delay 1333kHz/749=1780.151Hz
TCB0.CTRLA=TCB_ENABLE_bm;
TCB0.CCMP=748;// TOP 749-1
//...
}
タイマーはcpuとは関係なく動くのでwhile(1){A;B;C;wait();}
でABCの実行がなされている間もタイマーは進む
結果、wait()
~wait()
間の処理時間がタイマーの1周期より短い場合はタイマーの周期がそのままdelayとして機能する
ところで562us
も1/562u=1779.36
もあまりに中途半端な数字なのでもしかすると1780Hz
が先に決まっている?
完成
Lチカの延長線といった感じ
動作確認用にLEDをつけたり、ボタンをもう一つつけたり
/*
IR: PA2
LED: PA3
SW_A: PA7
SW_B: PA1
*/
#define CODE_A 0x41b6fd02
#define CODE_B 0x41b68e44
#define IR_HZ 38000
#define T 562
#define LEADER_ON 16
#define LEADER_OFF 8
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#define IR_TOP (F_CPU+IR_HZ/2)/IR_HZ
#define IR_ON TCA0.SINGLE.CMP2BUF=(IR_TOP+3/2)/3-1// duty=1/3
#define IR_OFF TCA0.SINGLE.CMP2BUF=0
#define LED_ON PORTA.OUTSET=1<<3
#define LED_OFF PORTA.OUTCLR=1<<3
#define FOR(X) for(uint8_t i=0;i<X;++i)
void wait(){while(!(TCB0.INTFLAGS&TCB_CAPT_bm));TCB0.INTFLAGS=1;}// TCB0 T us
static void sleep(){sei();SLPCTRL.CTRLA=SLPCTRL_SMODE_PDOWN_gc|SLPCTRL_SEN_bm;sleep_cpu();cli();}
ISR(PORTA_PORT_vect){PORTA.INTFLAGS=PORT_INT7_bm|PORT_INT1_bm;}
static void send(uint32_t x){
IR_ON;FOR(LEADER_ON)wait();IR_OFF;FOR(LEADER_OFF)wait();
FOR(32){IR_ON;wait();IR_OFF;wait();if(x>>(31-i)&1)FOR(2)wait();}
IR_ON;wait();IR_OFF;
}
void main(){
// TCA0 IR 1333kHz/35=38.095kHz
TCA0.SINGLE.CTRLA=TCA_SINGLE_ENABLE_bm;
TCA0.SINGLE.CTRLB=TCA_SINGLE_CMP2EN_bm|TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
TCA0.SINGLE.PER=IR_TOP-1;// TOP 35-1
// TCB0 delay 1333kHz/749=1780.151Hz T us
TCB0.CTRLA=TCB_ENABLE_bm;
TCB0.CCMP=(F_CPU*T+500000)/1000000-1;// TOP 749-1
_PROTECTED_WRITE(CLKCTRL.MCLKCTRLB,CLKCTRL_PDIV_12X_gc|CLKCTRL_PEN_bm);// CLR_PER=16M/12=1333k Hz
PORTA.DIRSET=0b1100;// out: PA2,3
PORTA.PIN1CTRL=PORT_PULLUPEN_bm|PORT_ISC_LEVEL_gc;// BOTHEDGES|LEVEL
PORTA.PIN7CTRL=PORT_PULLUPEN_bm|PORT_ISC_LEVEL_gc;// BOTHEDGES|LEVEL
LED_ON;FOR(255)wait();LED_OFF;
while(1){
sleep();
if(~VPORTA.IN&(1<<7))send(CODE_A);
if(~VPORTA.IN&(1<<1))send(CODE_B);
LED_ON;wait();LED_OFF;
while(~VPORTA.IN&(1<<7|1<<1))FOR(18)wait();// about 10ms
}
}
re0212 % pio run
Processing t202 (board: ATtiny202; platform: atmelmegaavr)
----------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/atmelmegaavr/ATtiny202.html
PLATFORM: Atmel megaAVR (1.9.0) > ATtiny202
HARDWARE: ATTINY202 1MHz, 128B RAM, 2KB Flash
PACKAGES:
- tool-avrdude @ 1.70100.0 (7.1.0)
- toolchain-atmelavr @ 1.70300.191015 (7.3.0)
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 0 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Compiling .pio/build/t202/src/main.o
Linking .pio/build/t202/firmware.elf
Checking size .pio/build/t202/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [ ] 0.0% (used 0 bytes from 128 bytes)
Flash: [== ] 22.0% (used 450 bytes from 2048 bytes)
Building .pio/build/t202/firmware.hex
Configuring upload protocol...
AVAILABLE: serialupdi
CURRENT: upload_protocol = serialupdi
Looking for upload port...
Auto-detected: /dev/ttyUSB0
Uploading .pio/build/t202/firmware.hex
avrdude warning: forcing serial DTR/RTS handshake lines HIGH
avrdude: AVR device initialized and ready to accept instructions
avrdude: device signature = 0x1e9123 (probably t202)
avrdude: Note: flash memory has been specified, an erase cycle will be performed.
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file .pio/build/t202/firmware.hex for flash
with 450 bytes in 1 section within [0, 0x1c1]
using 8 pages and 62 pad bytes
avrdude: writing 450 bytes flash ...
Writing | ################################################## | 100% 0.28s
avrdude: 450 bytes of flash written
avrdude: verifying flash memory against .pio/build/t202/firmware.hex
Reading | ################################################## | 100% 0.12s
avrdude: 450 bytes of flash verified
avrdude warning: releasing DTR/RTS handshake lines
avrdude done. Thank you.
================================ [SUCCESS] Took 1.27 seconds ================================
Environment Status Duration
------------- -------- ------------
t202 SUCCESS 00:00:01.266
================================ 1 succeeded in 00:00:01.266 ================================
ユニバーサル基板に実装した
コードはこちら