ATtiny202で赤外線リモコン製作

# シーリングライトの選定
使用開始から10年弱経過した部屋のLEDシーリングライトが勝手に消えるようになった
Hotalux社の[HLDC08301SG](https://www.hotalux.com/products/_1833.html?p_encd=06434477)を購入し交換した
選んだ理由は幾つかあるが
- 前の家で使っていた蛍光灯のホタルックが便利だった
- NECからの独立でリモコンが多分NECフォーマットなのでリモコン製作入門に丁度いいと判断

というわけでリモコン製作入門をする

# NECフォーマット

まず最初にNECフォーマットについて理解する必要がある

[elm氏の解説記事](http://elm-chan.org/docs/ir_format.html)が非常に参考になった

```js
T=562us

$=PWM( frequency=38kHz, duty=1/3, duration=T )
_=LOW( duration=T )
```

としたとき、

- リーダー
    - 信号の開始を表す
    - `$$$$$$$$$$$$$$$$________`
- データ
    - 32bitの固定長のデータを含むことができる
    - `$_`が0、`$__`が1に対応
- ストップビット
    - データの末尾のビットを決定するための`$`

の順に構成される

```js
$$$$$$$$$$$$$$$$________$___$___$_$___$___$___$___$_$___$_$___$_$___$___$_$___$___$_$___$___$___$___$___$_$___$___$___$_$___$___$___$___$
```
これのデータを読むと`11011110101011011011111011101111`となり、今回はこれを`0b11011110101011011011111011101111`==`0xdeadbeef`のように読むことにする
正しい読み順が決まっているようだが今回は外部ライブラリを使うこともないので不自由ない

データ部32bitはカスタマーコード16bit、データ本体8bit、その反転8bitと言われているが、実際に読むと最後の8bitがデータ本体の一部になっていたりするので、とりあえず32bitのデータがあるんだな程度の認識で良いと思う

# リモコンの解析

付属のリモコンの型番は[RE0212](https://www.hotalux.com/products/_1833.html?p_encd=A0391038)

手持ちの[赤外線リモコン受信モジュール GP1UXC41QS](https://akizukidenshi.com/catalog/g/g106487/)と[ハンドヘルドオシロ HDS272S
](https://akizukidenshi.com/catalog/g/g116701/)で波形をCSVファイルとして取得した

受光モジュールのV_outは基本HIGHで受光するとLOWになる

チャンネルやボタンを変えながら色々試した
以下は中央ボタンch1の波形データである

```:c1.CSV
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を読んでデータをデコードするスクリプトを書いた

```js: parse.mjs
#!/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 ) + チャンネル番号`

```console
~ % ./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比や周波数の乱れを防ぐことができる

```c:src/main.c
#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
	}
}
```

```ini:platformio.ini
[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に用いる
```c
//...

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をつけたり、ボタンをもう一つつけたり

```c:src/main.c
/*
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
	}
}
```

```console
 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 ================================
```

ユニバーサル基板に実装した

https://misskey.io/notes/a2hhdcd32r1s08rr

コードはこちら

https://github.com/McbeEringi/avr-stuff/tree/main/re0212