2014年 01月 07日

AVR TWI (I2C) 用のライブラリ

そこそこ使いやすい感じなのを学習しながら書いてみた。

使いかた

マスターの場合

割込みを利用はしてますが、APIとしてはブロックする同期なものしか用意してません。

uint8_t data[7];
uint8_t ret;

i2c_set_bitrate(100);
// Set target slave address
i2c_master_init(0x60);
ret = i2c_master_write((uint8_t*)"\x04", 1);
if (ret) goto error;
ret = i2c_master_read(data, 8);
if (ret) goto error;
i2c_master_stop();

error:
    i2c_master_stop();
スレーブの場合

スレーブの場合、特定のメモリ領域を登録すると、そこに対して read も write もできる、という感じの設計になっています。

// Slave memory map (must be smaller than 254 (0xfe) bytes)
uint8_t data[9];
// Enter to slave receive mode with data and size.
// After this operation, data will be changed automatically by TWI interrupt.
i2c_slave_init(0x65, data, 10);

// Access (set or get) to I2C data block
data[0] = 0x10;
// Or more readable code with struct
struct {
    uint8_t foo_flag1;
    uint8_t foo_flag2;
    uint16_t bar_value1;
    uint16_t bar_value2;
    uint16_t bar_value3;
    uint16_t bar_value4;
} data;

i2c_slave_init(0x65, &data, 10);

割込みでいつのまにかデータが変化する感じなので、マルチバイトデータの読み書きではデータが化ける可能性があります。

マルチバイトデータの読み書きを行う場合、

  • 書く場合
    • i2c_state を見て、I2C_STATE_IDLE であることを確認してから
  • 読む場合
    • i2c_state を見て、I2C_STATE_IDLE であることを確認してから
    • cli してから

が必要だと思います。通信中のデータはコピーして別で持っておけばいいんですが、もったいないし、そんなに問題にならなそうだしお手軽なのでこんな感じです。

実装にあたって

I2C は仕様書の日本語訳で公開されている。本家?はi2c-bus.org かな。

ポイントとしては

  • 送信・受信の切替えにあたっては必ず再度アドレスの送信が必要
  • Repeated START というのは単に STOP + START 相当のことを START だけでできるようにしているに過ぎない (バスを連続して占有し続けられるというだけ)
  • NACK は ACK を返さない状態のことを言ってる
    • エラーと区別はない
    • 転送終了とか、受信終了とかの意味付けされてるけど、応答がないのでもう何もしない、って感じ
  • START コンディション、STOP コンディション、クロックは常にマスターが生成する

マルチマスターまわりとゼネラルコール(マルチキャスト)まわりはいまいち理解できてない (今のところ必要性を感じてない)。

I2C 上のプロトコル

I2C 自体は任意長のバイトの送受信しか定義してない (言及はあるけど) ので「特定のアドレスのデータを読みだしたい」みたいな場合は、その上にプロトコルを乗せる必要がある。デファクトスタンダードっぽいのは

  1. アドレスを1バイト送信する
  2. Repeated START (たぶん STOP + START でも同じだけど…)
  3. データをnバイト受信する

というもののようだ。ステートフルなので、読み出される側は読まれようとしているアドレスを記憶しておく必要があり、1バイト読まれるごとにインクリメントする必要がある。送信と受信は別々にアドレスを指定する必要があるので、この場合2度アドレス送信が行われる。

AVR での実装

ポイントは

  • 割込みがかかったら何か必要なことをして TWINT を 1 にする、というのをくりかえす
    • TWINT をクリアしないと無限に割込みが入り続けるし、SCL がローのままになるのでバスが解放されず一切I2Cできなくなる
    • すべきこと (何をしたら次どういう状態になるか) はデータシートにモードごとに書いてある
  • データシートにある "TWINT Flag is set" な状態というは TWINT が 0 の状態のことで、TWINT Flag を clear するというのは TWINT に 1 を書くということらしい。論理逆なの?
  • TWINT Flag が set されている間 SCL はローになる
    • ソフトウェア処理に時間がかかった場合自動でクロックストレッチングされる
  • MR, MT, SR, ST ({Master,Slave} {Receiver,Transmitter}) の定義を先に読んどかないと意味不明

基本がわかればあとはそんなに難しくなく、試行錯誤したらできる感じだった。ただ、割込みの中で余計なことをすると、うまく次の割込みが入らなくなるということがあったりするので、動作を観測するのが難しい。LED チカらせてデバッグするしかないことがある。

備考

Linux の I2C まわりを調べていて出てくる SMBus とかいうのはPCの電源管理とかで使われるプロトコルで、基礎プロトコルとしてI2Cを利用している。I2C レイヤーの上に SMBus というレイヤーがあるイメージ。

2013年 12月 23日

AVR のタイマー計算機

というのを作ってみた。

AVR に限らないけど、マイコンで時間を測るにはCPUクロックを数えるわけですが、欲しい時間に対して分周比とかを求めるのが面倒なのでかいた。

F_CPU は CPU クロック数、Seek Freq. のほうに欲しい周波数または時間間隔を入れて、Calculate を押すと、各分周比において CTC でいくつを設定すればいいか、あるいはオーバーフローでいけるかどうかとかを出す。

16MHz で 1msec を測りたい場合、

Pre-scaler:1, 16bit Timer CTC:16000
Pre-scaler:8, 16bit Timer CTC:2000
Pre-scaler:64, 8bit Timer CTC:250
Pre-scaler:256, no result for 62.5
Pre-scaler:1024, no result for 15.625

このようにでる。この場合は 8bit タイマーで 64 分周して CTC を 250 に設定したらよい。

2013年 12月 09日

AVR progmem に long を置く方法

PROGMEM をつけると (const も必須)、その変数の示す先がプログラムメモリー領域になる。そのままだと使えないのでワーキングメモリ (SRAM) にコピーする必要がある、という話。大きい定数は SRAM に置けないので、基本的にプログラムメモリ (フラッシュメモリ) 上に置いて、必要なときだけコピーするようにする。

avr/pgmspace.h に byte やら word を読み出す関数は提供されているけど、long はない。どうするのが正しいのかなあと思ったけどよくわからなかった。

結局以下のように memcopy でうまくいった。

const uint32_t MORSE_CODES[] PROGMEM = {
    ...
};

uint32_t current_sign;
memcpy_P(¤t_sign, &MORSE_CODES[character], 4); 


progmem 調べていると prog_ が prefix された型を使え、って書いてあるのがよく検索ででてくるけど、それは obsolete で、使うとエラーになる。ヘッダを読んだほうが良い。const と PROGMEM をつけるのが正しい。

しかしヘッダ見ると far とか near とかよくわかんない関数がいっぱいあって混乱する。far は 64KB 以上のフラッシュがあるデバイスしか関係ないっぽいので気にしないほうがいいっぽい。つまり near_ だけ使っていれば良い (実際、 near も far もついていない関数は near のエイリアスになっている)

ヘッダファイルの冒頭部分の一部を訳してみた。

\note If possible, put your constant tables in the lower 64 KB and use
pgm_read_byte_near() or pgm_read_word_near() instead of
pgm_read_byte_far() or pgm_read_word_far() since it is more efficient that
way, and you can still use the upper 64K for executable code.
All functions that are suffixed with a \c _P \e require their
arguments to be in the lower 64 KB of the flash ROM, as they do
not use ELPM instructions. This is normally not a big concern as
the linker setup arranges any program space constants declared
using the macros from this header file so they are placed right after
the interrupt vectors, and in front of any executable code. However,
it can become a problem if there are too many of these constants, or
for bootloaders on devices with more than 64 KB of ROM.
All these functions will not work in that situation.

可能なら、定数テーブルは 64KB 以下に配置し、pgm_read_byte_far() や pgm_read_word_far() の代わりに pgm_read_byte_near() や pgm_read_word_near() を使ったほうが効率が良いし、なおかつ 64KB 以上は実行コードに使える。

プリフィックスに _P がついている全ての関数はELPM 命令を使わず、フラッシュROMの64KB 以下を引数にとる。これは大抵の場合、このヘッダファイルのマクロを使って定義されたプログラムスペース定数はリンカーが割込みベクターのすぐ後に配置するので、気にすることはない。

しかしながら、もしこのような定数が多すぎる場合や、デバイス上のブートローダーが 64KB 以上の場合問題をひきおこすかもしれない。これらの関数はこのような状況では一切動かないだろう。

2013年 11月 18日

マイコン周りのシリアル通信プロトコルまとめ

なんかいろいろあって??ってなるので特徴を覚え書き。線は信号に関係するものだけ (普通は +VDD が必要)

名前 特徴 速度 備考
1-wire SIGNAL, GND 半二重 1対多 非同期 標準 15.4kbps, オーバードライブ125kbps SIGNAL が VDD を兼ねるので本当に2本だけでOK センサーとかで採用例がある。MAXIM の日本語スライド
I2C SDA, SCL, GND 半二重 多対多 同期 ロースピード 10kbps 標準 100kbps ファーストモード 400kbps 高速モード 3.4Mbps バス中のデバイスのどれでもマスターになれる。マイコンだと400kbpsまでが多い?ここに上げた中では唯一多対多のバスを作れる
SPI SCK, MISO, MOSI, SS, GND 全二重 1対多 同期 クロック依存最大 1Mbps〜2Mbps マスターはスレーブの数だけ SS が必要。1対1 なら SS は必要ない。
RS232 RxD, TxD, GND 全二重 1対1 非同期 9600bps などいろいろ PC でもよく使われていたインターフェイス。今でも USB 変換ケーブルは簡単に手に入る。フロー制御とかしないなら3本で使える
USB1.0 D+, D-, GND 半二重 1対多 非同期 1.5Mbsp, 12Mbps 使うの面倒。USB2.0 は高速化版、USB3.0 はさらに高速化して全二重に

AVR では

  • SPI はほぼどの AVR でも使える
  • I2C は AVR によっては専用ハード (TWI) が用意されてる。専用ハードでなくても USI があれば TWI をハンドリングできる (多少面倒だけどライブラリとして公開している人はいる)。
  • RS232 はロジックレベルをドライバ (MAX232系の) で変換すれば USART で直接扱えるので簡単。
  • USB は一部の AVR で組込み。V-USB を使えばソフトウェアだけでもいける。あるいは USB Serial 変換チップ (FTDIのが有名でクロスプラットフォーム) を使えば USART 経由で使える。変換チップ使うのが確実で汎用性があってよく使われていそう。

略語がいっぱい

  • UART (Universal Asynchronous Receiver Transmitter) は非同期なシリアルを扱う回路の名前。同期なのを統合したものを USART (Universal Synchronous Asynchronous Receiver Transmitter) というらしい
  • TWI (Two Wire Interface) はその名の通り2線式ってことなんだけど、AVR においては普通は I2Cのことを指しているっぽい。I2Cと名乗ってないのはなんか政治的な問題?
2013年 11月 17日

キヤノンのカメラのリモートレリーズ (またはリモコンレリーズ)

そういえばマイコンからカメラのレリーズができたらいいかもなーとなんとなく考えたのでやってみた。タイムラプス的なもの (インターバル撮影) とか、バルブとか自分で制御できたら楽しいかもしれない。

リモートレリーズ端子

リモートレリーズ端子 (有線) にはいくつか種類があるらしく、キヤノンの場合、Kiss とかではステレオミニと同じだけど、5Dとか7Dとかだと、N3型端子とかいう謎の端子になる。N3端子ってのはそこらへんで単体で簡単に手に入らないので、既存のを買って切るしかないっぽい。機能的には同じみたいだけど、なんでこんなことになってるのか謎

手元にあるのはN3型のものなので、別にレリーズ別途買ってまで試すまでのモチベーションはわかなかったので、とりあえずリモートレリーズ端子を使うのは諦めた。(ただのスイッチなので難しいことはなさそう)

赤外線リモコンのレリーズ

他にレリーズを切る方法としては、赤外線リモコンを使うほうほうがあって、これなら赤外線 LED チカらせるだけでよく、端子とか関係ないのでお手軽そうだった。赤外線は昔昔買ったLEDジャンク詰め合せに無駄にいっぱい入っていて腐っている。

ググるとリモコンの解析結果を公開している人がいるので、その通りやったらいけた。

あたり前だけどドライブモードをリモコンのモードにしないとシャッター切れない。解析結果をそのままコードにしただけだけど (PB0 に LED が接続されている前提)

void shutter () {
	int i;

	for (i = 0; i < 23; i++)  {
		set_bit(PORTB, PB0);
		_delay_us(13);
		clear_bit(PORTB, PB0);
		_delay_us(13);
	}
	_delay_us(7200);
	for (i = 0; i < 23; i++)  {
		set_bit(PORTB, PB0);
		_delay_us(13);
		clear_bit(PORTB, PB0);
		_delay_us(13);
	}
}

ただ、赤外線の場合、設定済みの露出でしかシャッターを切れないので、バルブとかはできない。インターバル撮影とか、センサー連動のシャッターとかはこれで十分できそう。花火の撮影とか自動化できそう。

2013年 11月 10日

ATTiny2313 ステップ速度調節のエレキー

最初に作ったやつを少し作りなおした。ケースを加工するのが面倒だったので適当な状態で放置されていたんだけど、おさまりの良さそうな方法を思いついたので、基板から作りなおした。

回路図

殆ど前のと変わってない。ESD 保護ダイオードと、プルアップを変更しただけ

基板図

ファームウェア

ピンの配置の関係でアサインだけ変えてある。

ケース

秋月で売ってる電池ボックス 単3×4本用(フタ付プラスチック・スイッチ付)というのを使ってる。

単3は2本分の領域をエネループ用に使って、残り半分のスペースに回路を入れてる。もとからあるでっぱりとかはニッパーとかカッターで切った。電池の金具は加工する必要はなくて、ハメこまれてる位置を変えてあげるだけでよかった。元からあるスイッチはとってしまっている。

結構狭いので、圧電ブザーを削ったり基板をやすりで削ったりしてギリギリで入れてる。今回は部品配置の検討をするのに EAGLE が大変役に立った。ISP 部分以外はほぼジャンパなしでいけてる。IC ソケットをつけなければもっと高さに余裕があったんだけど、一応つけた。

スイッチがあった部分は少し広げて、タクトスイッチを入れてる。固定する方法で困るけど、グルーガンでタクトスイッチを固めてしまった。グルーガンでも結構固めになるので、力を入れても大丈夫なくらいにはなるっぽい。

ref.

2013年 11月 08日

MOS-FET 2N7000

VGS(th) に結構バラつきがあるみたいで、データシートだと 0.8〜3V となっている。VGS = VDS ID = 1mA の条件が書いてあるから、たぶん温度変化について書いてあるんだと思うけど、よくわからない。

VDS= 5V, ID = 1mA ぐらいで、手元のをいくつか試すと。室温20度で、約2.28〜2.32V付近でスイッチする。指でつまんで温めると0.1Vぐらい下がったりする。おもしろい。

2.1V ぐらいまでスイッチできてほしいんだけど、なんかうまい手はないんだろうか。負荷側の電圧は決まってしまっているし、ほかの素子を探すしかないのかな。すごい困るというほどではないから、そこまではしたくない。

Watch Dog Timer

ワッチドッグタイマー。基本はプログラムが意図しない挙動をしたときにリセットをかけるためのタイマー。

例えば、「常に1秒後にリセットをかけるワッチドッグタイマー」をセットして、ワッチドッグタイマーによってリセットされる前にメインプログラム側からワッチドッグタイマーをリセットする。もしプログラムが暴走してタイマーが初期化されなくなると、ワッチドッグタイマーがリセットをかけてくれる。

用途上、メインのクロックとは別のクロックで動いている。

Brown Out Detection

電源電圧を監視して、一定より下がったらリセットをかけてくれる。

もちろん瞬間的な電源電圧変動では反応しないようにヒステリシスな感じになってる。

ボタン電池で動く小型・低消費電力 AVR エレキー (50円 ワンチップマイコン ATTiny13A)

とりあえず完成なので、今まで書いたやつのまとめで整理。

要求

  • 外に持ちだせるように小型で電池駆動できること
  • 電池交換をあまり気にしなくてもいいこと
  • 実用的であること

仕様

  • 短点・長点それぞれ1点のメモリー
  • スクイーズ
  • インヒビット
  • 無段階スピードコントロール (10wpm〜30wpmまで想定)
インヒビットについて

エレキーは片方のパドルを押したままだと、ずっとその符号が出続けるので、いいタイミングで手を離す必要がある。

このとき、どのタイミングで符号メモリーを効かせるかで使い勝手が大きく異なる。

  • 前の符号が終わった瞬間にパドルが押されていたら即メモリーさせるか
    • 前の符号が鳴っている間にパドルから手を離す必要がある
    • 前のめったキーイングが必要
  • 前の符号の直後のスペースが終わった瞬間にメモリーさせる
    • 前の符号が鳴り終わってからも多少時間がある
    • 落ちついたキーイングが必要

後者のほうが基本的に落ちついて打てる感じだけど、パドルを早く動かしすぎると変な符号が出たりする。一長一短な感じ。

このエレキーでは、これらの中間として、前の符号が終わってから、短点の長さ * 0.3 の間は「インヒビット」として押し続けていてもメモリーされない挙動にした。これは単に好みなので、ソースコードの定数で変えれるようにした。

回路図

前回とピンの使いかたが変わっているけど、あまり変更はない。

パドル入力には静電気サージ (ESD) 用にツェナーダイオードを入れた。AVR の仕様上「All I/O pins have protection diodes to both VCC and Ground」となっていてクランプダイオードが入っているみたいだけど、クランプされたのを消費する部分がどこにもないので前段で入れたほうがよさそう。

FET のゲートについてる 150Ω は電流制限用だけど、一応なくても動く。AVR のポートの絶対定格が 40mA なので 3V/20mA=150Ω。流れるとしても瞬間だけなんだけど、絶対定格を守るなら必要そう。

FET の GS 間の抵抗は入力オープン時に GS を確実に 0V にするためで、普通入れるみたい。AVR の出力ポートってローのときは吸い込み動作するしいらないんじゃ? と思ったけど、電源 OFF->ON からのタイミングとかいろいろなタイミングで入力オープンのとき不意にONになるとそのままになってしまうらしい。一応入れたほうがよさそう。

2.2MΩ の抵抗はパドルのプルアップ抵抗で、省電力のため外部に大きなのをつけてる。ただ、人体アースの電圧降下でもキーイングとみなされる(接点を触れるとキーイングされてしまう)ようになるので、善し悪しがある。

いろいろ突き詰めると部品点数が減らせない。

基板図

ステレオミニジャックでパドルと繋ぐようにしてるけど、別に直結でもいいと思う。その場合ジャック部分はいらない。

ボリュームは半固定として書いてあるけど、普通のやつのほうがいいと思う。基板取り付けの小型ボリュームだとお手軽だけどあまり手に入らないみたい。なんでもいいけど場所をとる。

ファームウェア

これだけしか書いてないけど、ATTiny13A だと 96.7% Full (990 bytes) なので、結構ギリギリです

CrossPack-AVR-20130212 の avr-gcc (GCC) 4.6.2 で開発

AVR ISP Mark II なら、Makefile 変更なしに make install で書きこみが走る。

ISP も回路を切断せずに一応動いて、書きこみもできるけど、キーイング出力にノイズが入るので注意が必要 (ブレークインを切っておくか、リグとは切断して書きこみする必要がある)。

電源と実測消費電流

  • CR2032
  • eneloop lite

あたりを想定

CR2032 (3V) は公称 3V、終止電圧 2〜2.5V 程度。約 225mAh 使い捨てだけど、よく売ってる。使い捨てなので限界まで動いて電池しぼりとって動かなくなるほうが効率的。

eneloop lite 2本だと公称は 2.4V (1.2*2)、終止電圧 2.0V (1.0 * 2) 程度で、単4で550mAh、コイン電池に比べると体積は大きい。lite 版のほうが用途的にはいいだろうと思う。エネループの場合普通のニッケル水素より自然放電が少ないのと、比較的高い電圧が長く続く。ただし過放電すると致命的なので、2.1V程度で動かなくなるのが理想。

最低動作電圧

本体の最低動作電圧は 1.8V 程度なので、スイッチング用の FET の Vth に支配される。

実測すると、最低動作電圧は室温20℃で2.3V 程度 (≒ FET Vth)

CR2032 で動かすなら、もっと低い電圧でも動いてほしいけど、もっとぎりぎりの別のFETにするほかない。

エネループの場合2本で2.3Vを切る (1本で1.15V) になるのは、だいたい70%〜90%ぐらい放電したあたりなようなので、これでも悪くはなさそう。

消費電流

消費はいろいろ工夫してみたので、結構減らせてる。

  • キーイング中: 96μA (約 0.1mA 実測)
  • パワーダウン中: 0.15μA
    • (測定不可なので、ATTiny13A の仕様書から。実測では 0.0μA未満の表示だった)

電源に CR2032 (225mAh) を使った場合、1日に2時間キーイングすると仮定すると、約1152日ほど持つ。1日8時間フルタイムでキーイングしても 292日ほど持つ。1日16時間キーイングし続ける廃人でも146日ほど持つ。

eneloop lite (550mAh) の場合、容量の70%程度使うまででも、1日2時間のキーイングで1971日ほど持つ計算になる。自然容量減を考えてもそこそこ持ちそう。

消費電流減の対策

具体的には以下のような対策をしてる

  • 動作周波数は内蔵 128kHz
  • FET でスイッチング (なのでキーイング端子に高電圧がかっているリグでは使えない。5Vとか12Vとかなら全然大丈夫なはず)
  • スピード調節用のボリュームにできるだけ通電しない
  • キーイング用のプルアップ抵抗を高抵抗に
  • できるだけアイドルモードで動くように
  • 10秒経過ですぐパワーダウンモードに移行
    • キー入力割込みで復帰するので特に違和感はないと思う

まとめ

かなり単純なものをしっかり各所考えながら作ってみたけど、いろいろと発見があった。ソフトウェアの変更がダイレクトに消費電力減に繋がったりするのは新鮮で面白い。

また、この程度の回路だと全素子の動作を考えながら作れるので、基本的なことを理解するきっかけになった。

次はもうすこし複雑なものを作りたい。

ref.

2013年 11月 02日

ATTiny13A を使った低消費電力エレキー

まとめを後日書きました [tech][avr][arduino] ボタン電池で動く小型・低消費電力 AVR エレキー (50円 ワンチップマイコン ATTiny13A) | Fri, Nov 8. 2013 - 氾濫原

前書いたのの続き

さらにもっと消費電力を減らせないだろうか? と考えた。パワーダウンモードの消費が計測不可能なので、支配的なのは特にキーイング中と、アイドル中の消費になっている。

特にキーイング中の消費が比較的多い (200uA近く) ので、どうにかできないかと考えた。キーを押しているとき、内蔵プルアップを通して GND に電流が流れるので、そこの消費が結構多い。

今まで何も考えず AVR 組込みのプルアップ抵抗を使っていたけど、ここでちょっと考えてみることにした。

プルアップの調整

内部プルアップは、実測からすると3V/100uA=30kΩ になってるようだ (スペック上は 20k〜50k)。とりあえず AVR 内部のプルアップ抵抗をオフに。

プルアップ抵抗に許される電圧降下を考える。電源電圧3Vで、入力 H レベルは VCC * 0.6 (スペックから)、ノイズ回避用に 0.4V を足すと

  • ピンの必要入力電圧 Vin = 3 * 0.6 + 0.4 = 2.2V
  • プルアップでの電圧降下最大 V = 3 - Vin = 0.8V

となった。

マイコン側は十分大きい入力抵抗があると考えると、かなり大きな抵抗を入れても大丈夫そう? なのかな。

試しに 2.2MΩでプルアップしてみると、ICの足にかかる電圧は2.5V程度になった。2.2MΩ で0.5V電圧降下しているので、0.23uA 程度流れてる。3V / 0.23uA でプルアップと内部抵抗の合成値は 13.2MΩ、つまり内部抵抗は11MΩぐらい……? よく「入力抵抗は非常に大きい」といわれるのを見る割に、いまいちどの程度かわからなかったけど、こんなもんなのかな。

この状態で、消費電力をはかる

  • before: 内蔵プルアップ: 198uA
  • after: 2.2MΩプルアップ: 113uA

とりあえず減ったし、普通に動いてはいる。しかしこれであっているのかさっぱりわからない。

これで、毎日2時間使うケースだと (CR2032/225mAh, 1日2時間, パワーダウン中 0.15e-6mA, キーイング中 113e-3mA) 995日持つ計算になった。

delay_ms のさらなる見直し

128kHz で動かすにあたり delay_ms を以下のようにしていた。

void delay_ms(uint16_t t) {
	uint16_t end;
	cli();
	timer = 0; TCNT0 = 0;
	end = NOW + DURATION(t);
	sei();
	while (NOW <= end) { nop; }
}

timer は 2msec ごとのオーバーフロー割込みでインクリメントされているけど、これだとちょっと精度が悲しいような気がするので、カウンタそのもの (TCNT0) も見ている。オーバーフローでやっているので、単純に timer はカウンタの桁あがりとして扱える。NOW は ((timer<<8)|TCNT0) という定義

while の中を アイドルではなく nop; にしているのは、ここでアイドルに入ってしまうと、次に起きるのが 2msec 後とかになるので、せっかく TCNT0 を見ている意味がなくなるから。

ただ、まだここは精度を維持しつつも最適化の余地があって、以下のようにした。

void delay_ms(uint16_t t) {
	uint16_t end;
	uint16_t end0;
	cli();
	timer = 0; TCNT0 = 0;
	end = NOW + DURATION(t);
	end0 = end - 0x100;
	sei();
	while (NOW <= end0) {
		set_sleep_mode(SLEEP_MODE_IDLE);
		sleep_mode();
	}
	while (NOW <= end) { }
}

少なくとも、最後の TCNT0 分 (8bit) になるまでは、普通に 2msec ごとに寝ていても大丈夫なはずなので、それまではスリープを使い、最後のカウンタ分はビジーループにするようにした。nop; は別にいらなそうなのでとっただけ。

これでキーイング中でも 96uA まで消費電流を落とせた。上と同じように、毎日2時間使うケースだと (CR2032/225mAh, 1日2時間, パワーダウン中 0.15e-6mA, キーイング中 96e-3mA) 1171日持つ計算になった。1000日超え! もうこれでいいかな。