2018年10月14日日曜日

BluePill に USB 経由でプログラムを転送する

1. 概要

STM32 マイコンボードの BluePill は STM32F103C8 を搭載していて、とても安く (海外からだと200円以下で) 購入できる。

STM32 Blue Pill perspective.jpg
BluePill


ここでは、STM32duino (STM32 用の Arduino 環境) ではなく、MDK-ARM(Keil) や GCC 等の開発環境を使って C言語で書いたプログラムを BluePill に USB 経由で書き込む方法を説明する。

最初に一回 SWD (Derial Wire Debug) かシリアル通信を使って BluePill に STM32duino 用のブートローダを書き込んでおけば、それ以降は USB 経由で、コマンドライン版の DFU (Device Firmware Upgrade) ツールを使ってプログラムを書き込める。ただし、ブートローダはプログラムを 0x2000 番地から書き始めるため、ブートローダ経由で書き込みたいプログラムは、開始アドレスと割り込みベクタのオフセットを 0x0000(0x8000000) から 0x2000(0x8002000)  にしておく必要がある。

※ 手順を細かく書いていますが、要点は文中の赤字の部分のみです。

2. 準備するもの (64bit Windows 10 の場合)

 

1. ブートローダのファイルと書き込み装置


■ STM32duino 用のブートローダのコンパイル済みバイナリファイル (generic_boot20_pc13.bin)
https://github.com/rogerclarkmelbourne/STM32duino-bootloader/

※ 書き込み装置は、今回は SEGGER JLINK-LITE + J-Flash Lite v6.34g を使った。これを使わずにシリアル通信で書き込む方法もあるようだ。

2. プログラムの開発環境


■ プログラムのひな形を作成するツール: STM32CubeMX 4.27.0 (下記のページの一番下のリンクからダウンロードできる。ダウンロード時にはメールアドレスの登録が必要)。インストール後に必要なライブラリが自動でダウンロードされる。

https://www.st.com/ja/development-tools/stm32cubemx.html

■ 開発環境: MDK-ARM(Keil) Version 5.26。インストール後に必要なライブラリが自動でダウンロードされる。

http://www2.keil.com/stmicroelectronics-stm32/mdk/

※ FreeBSD 上で GCC を使って開発する方法は後述

■ OBJCOPY ツール (arm-none-eabi-objcopy.exe)
MDK-ARM(Keil) が生成した HEX ファイルを BIN ファイルに変換する際に使う。GNU Arm Embedded Toolchain に含まれている。

https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads

3. DFU ツール


■ DFU-UTIL 0.9

https://sourceforge.net/projects/dfu-util/

■ Windows 用 DFU のデバイスドライバ

https://github.com/rogerclarkmelbourne/Arduino_STM32/archive/master.zip


3. ブートローダの書き込み

BluePill に STM32duino 用のブートローダのコンパイル済みバイナリファイル (generic_boot20_pc13.bin) を書き込む。確実に書き込むには下記の手順で。 書き込み時には BluePill に USB 端子から電源を供給する必要があることにも注意。

(1) BluePill と SEGGER JLINK-LITE を接続。SWD の4ピンを接続して J-Flash Lite v6.34g を起動する。マイコンの種類は STM32F103C を選択する。
(2) BOOT0 ピンを "1" にしてリセットボタンを押す。
(3) J-Flash Lite の "Erase Chip" をクリックする。
(4) BOOT0 ピンを "0" にして J-Flash Lite のファイル選択で "generic_boot20_pc13.bin" を選択し "Program Device" をクリックする。
(5) リセットボタンを押す。

ブートローダの書き込みに成功すると、リセット直後に短時間緑色の LED が高速で点滅し、その後低速で点滅するようになる。
  

4. テスト用プログラムの作成


ボード上の緑色の LED (GPIOの PC13 に接続されている) を1秒間隔で点滅させるソフト LEDTEST.bin を作る。

(6) STM32CubeMX を起動し "New Project" でプロジェクトを新規作成する。チップの種類は "STM32F103C8" を選ぶ。ピンの PC13 をGPIO_Output にする。

STM32CubeMXで PC13 を GPIO_Output に設定する


なお、STM32CubeMX を使う時は、SYS→Debug を "Serial Wire" にして、SWD ピンを有効にしておかないと、後にデバッガが使えなくなったり、プログラムの書き込みができなくなるので注意。

STM32CubeMX では SWD ピンを有効にする


(7) "Project" → "Settings" でToolChain/IDE を "MDK-ARM V5" に変更する。"Project" → "Generate Code" を実行すると、指定したフォルダ以下に、ソースファイルのテンプレートが生成する。 

STM32CubeMXで開発環境を MDK-ARM V5(Keil) にする



(8) MDK-ARM(Keil) Version 5.26 の Keil uVision5 を起動して、(7) で生成したフォルダの中にあるプロジェクトファイルを開く。main.c の main() 関数の中に書いてある while(){} の無限ループの中に下記の2行を追加する。

HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // LED を点滅
HAL_Delay(1000);

ここで一度 "Project" → "Build target" でビルドしておく。

(9) "Project" → "Option for Target" のリンカのオプションで、*.sct ファイルを使うようにするため、次のように指定する。

プログラムの開始アドレスの変更: "EDIT" をクリックして LEDTEST.sct ファイルを編集し、ファイル中に書かれているアドレス 0x8000000 を 0x8002000 に変更する(2カ所)。

Keil uVision5 でリンカの設定を変更



プログラムの開始アドレスの変更

割り込みベクタのオフセットの変更:"system_stm32f1xx.c" の VECT_TAB_OFFSET の定義を変更して

#define VECT_TAB_OFFSET  0x00002000U 

に書き換える。

割り込みベクタのオフセットの変更

 "Project" → "Build target" でビルドすると、HEX ファイル LEDTEST.hex が生成する。ここで生成した LEDTEST.hex を開き、アドレス (赤で囲んだ箇所) が 2000 になっていることを確認する。

※ このアドレスが 0000 になっている場合は、上記 (9) の *.sct ファイルの設定が正しく反映していないのでやり直す。

生成した HEX(Intel HEX) ファイル。赤文字の部分が 2000 になっていないといけない

(10) 下記のコマンドで HEX ファイルを BIN ファイルに変換する。
> arm-none-eabi-objcopy.exe -I ihex -O binary LEDTEST.hex LEDTEST.bin

5. DFU ツールでの書き込み


(11) DFU デバイスドライバのインストール

https://github.com/rogerclarkmelbourne/Arduino_STM32/archive/master.zip

から master.zip をダウンロードして、Arduino_STM32-master → drivers → win にある install_drivers.bat を実行すると、ドライバがインストールされる。ドライバをインストールすると、BluePill を デバイスマネージャで見たときに "Maple DFU" として表示されるようになる。

ドライバのインストール

ドライバのインストール前


ドライバのインストール後

(12) 書き込みと実行: BOOT0 ピンが "0" に設定されていることを確認する。BluePill を Windows PC に USB 接続する。リセットボタンを押し、緑色の LED が点滅している間に次のコマンドを実行すると、ファームウェアが転送される。

> dfu-util-static.exe -D LEDTEST.bin -a 2



※ dfu-util-static.exe には 32bit 版と 64bit 版がある。OS に合った方を実行しないと書き込めない。
※ "Error sending completion packet" と表示されるが、これで正常に転送されている。

書き込み直後にリセットボタンを押すと、正常に書き込まれていれば、書き込んだプログラムが起動して LED が 1秒間隔で点滅する(明らかに1秒以下の間隔で点滅していたり、つきっぱなし、消えっぱなしになった場合は失敗)。次回以降は、リセット後に2秒間ファームウェアの転送を待ち、その後にプログラムが実行される。

6. FreeBSD + GCC で開発する場合


Windows 版の MDK-ARM(Keil uVision5) の代わりに、FreeBSD 上で GCC を使って開発することもできる。

(13) FreeBSD に次のパッケージをインストールする
■ gcc-arm-embedded-7.3.20180627
■ dfu-util-0.9

(14) 上記 (6) (7) の手順でプログラムのひな形を作成する。(7) で STM32CubeMX で "Project" → "Settings" で ToolChain/IDE を "Makefile" にすると、GCC 用のひな形を生成できる。

(15) プログラムの開始アドレスの変更: Makefile と同じディレクトリにある STM32F103C8Tx_FLASH.ld の、次の赤文字の箇所を書き換える。

/* Specify the memory areas */
MEMORY
{
RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 20K
FLASH (rx)      : ORIGIN = 0x8002000, LENGTH = 128K
}

(16) 割り込みベクタのオフセットの変更: Src/system_stm32f1xx.c 中の VECT_TAB_OFFSET の定義を次のように変更する。

/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET  0x00002000U

(17)  Src/main.c の main() 内に (8) に書いた LED を点滅させるための 2行を加える。
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // LED を点滅
HAL_Delay(1000);
(18) make コマンドを実行するとコンパイルされ、 build/LEDTEST.bin が生成する。
(19) リセットボタンを押し、緑色の LED が点滅している間に、次のコマンドで BluePill に書き込む。
# dfu-util -D build/LEDTEST.bin -a 2

2017年9月13日水曜日

Digispark+gcc USB HID 通信のチュートリアル


概要

 


Digispark+gcc チュートリアルの Part3 です(Part1, Part2)。

Digispark にファームウェアを書き込んで、HID デバイス (HID ベンダ定義デバイス) として認識させ、PC 側で動いているプログラムとの間で USB 経由で任意のデータの送受信をする方法を説明します。

サンプルでは PC 側のプログラムから送信された1バイトの値を Digispark が受信して、同じ値を送り返します。 このとき Digispark が受信した値が '1' の文字コードの時は、LED を点灯します。 '0' の文字コードの時は、LED を消灯します。 それ以外の場合は何もせず送り返すのみの動作をします。

Digispark 用のファームウェアは C言語で書いて avr-gcc でコンパイルし、USB で接続した Digispark に書き込みます (Arduino の開発環境を使いません)。 また PC 用の通信プログラムも C で書いて gcc でコンパイルします。

Raspberry Pi, FreeBSD で動作確認しました。

※ Windows のコマンドラインから Digispark に生のファームウェアを書き込む手順は、1回目の記事 を参照。


1. 開発環境の準備


以前の記事 「Digispark+gcc Lチカチュートリアル(RaspberryPi, Linux, FreeBSD 版)」 の 1. 2. を実行して、必要なツールをインストールし、Digispark への書き込みソフト micronucleus を実行できるようにしておく。

2. ソースコードのダウンロードとコンパイル 


(1) Digispark_VendorHID.zip をダウンロードして(「エラー」と表示された場合もそのままダウンロードできる)、ローカルに展開する。
(2) ファームウェアをコンパイルする。make コマンド(Raspberry Pi では make コマンド、FreeBSD では gmake コマンド) を実行すると main.c がコンパイルされてバイナリ (Intel HEX) 形式のファームウェア sample1.hex が生成する。
(3)  PC 側で動かす通信プログラムもコンパイルしておく。"COMMANDLINE" ディレクトリの中で make コマンドを実行すると vusb.c がコンパイルされて、実行ファイル vusb が生成する。

3. ファームウェアの書き込み


1. で準備した書き込みツール micronucleus をパスの通った場所に置き
sample1.hex  があるディレクトリで

# micronucleus --run sample1.hex

を実行する(管理者権限が必要)。

> Please plug in the device ...
> Press CTRL+C to terminate the program

 と表示されるので、この状態で USB 端子に Digispark を挿入するとファームウェアが書き込まれる。

4. 動作確認 


(1) ファームウェアを書き込んだ Digispark を PC の USB 端子に挿入すると、5-6 秒後に HID (ヒューマンインターフェースデバイス) として認識される。Raspberry Pi, FreeBSD では dmesg コマンドを実行すると、USB デバイスの名前が確認できる(ベンダID=0x16c0, デバイスID=0x05dc)。
(2) PC 側で 2. でコンパイルしたコマンド vusb を実行する (管理者権限が必要)。

vusb コマンドの引数には、次のように何か1文字を指定する。


# ./vusb A
libusb_set_configuration OK
libusb_claim_interface OK
A


正常に動作終了した場合は、引数に指定した文字が Digispark に送信され、そのまま送り返されて、上記のように画面に表示される。

文字 '0' と '1' を受信したときは Digispark 側で特殊処理を行っている。
 # ./vusb 1
とすると、Digispark 上の LED が点灯する。
# ./vusb 0
とすると、Digispark 上の LED が消灯する。

5. PC 側プログラムの説明


PC 側のプログラム vusb.c は、ライブラリ libusb-0.1 を使って HID デバイスとの通信を行う。このサンプルでは、通常のエンドポイント (通信用のバッファメモリ) を使用する通信ではなく、コントロール転送という通信方法で、単純にデータを1バイトずつ USB デバイスとの間で送受信している。

USB による通信では一般に、USB デバイスが自発的に送受信を要求することはない。送信も受信もホスト側(PC側) が API を呼び出すことによって行う。

送信(PC→外部USB機器): PC 側のプログラムで 「(5) ホスト→デバイス: 1バイト送信」 を実行するたびに、後述の Digispark 側のファームウェアで usbFunctionWrite() が呼び出される。Digispark 側ではこの関数内で受信を行う。

受信外部USB機器PC: PC 側のプログラムで 「 (6) デバイス→ホスト: 1バイト受信」 を実行するたびに、Digispark 側のファームウェアで usbFunctionRead() が呼び出される。Digispark 側ではこの関数内で送信を行う。

今回プログラム内で使用した関数 (libusb0.1 の API) を解説する。


(1) USB の初期化
r = libusb_init(NULL);

USB デバイスの使用を開始する。 最初に呼び出す必要がある。
成功したら 0、失敗したらエラーコードを返す。エラーコードは負の値。
今回はエラーコードを libusb_error_text() 関数で文字列に変換している。

(2) USB デバイスの検索
devh = libusb_open_device_with_vid_pid(NULL, VENDOR_ID, PRODUCT_ID);

ベンダID とプロダクトID を指定して USB デバイスを1個検索する。
デバイスハンドル (libusb_device_handle 型構造体) を返す。
今後の USB デバイスの操作にはこのデバイスハンドルを指定する。

(3) USB デバイスのコンフィグレーション(configration) の設定
r = libusb_set_configuration(devh, 1);

USB デバイスが内部に持つコンフィグレーションの設定を行う。
インタフェースの要求の前に実行する必要がある。
成功したら 0、失敗したらエラーコードを返す。

(4) インタフェースの要求
r = libusb_claim_interface(devh, 0);

USB デバイスとの通信に使用するインタフェースを要求する。
成功したら 0、失敗したらエラーコードを返す。

(5) ホスト→デバイス: 1バイト送信
r = libusb_control_transfer(devh, CTRL_OUT, HID_SET_REPORT,
     (HID_REPORT_TYPE_FEATURE<<8) | 0x00, 0, buf,  PACKET_CTRL_LEN, TIMEOUT);
  
buf[0] に送信する値を入れて実行する。
(今回は PACKET_CTRL_LEN を 1 にしているので、1バイトだけが送信される)。
送信に成功したバイト数を返す。エラーの場合はエラーコードを返す。

(6) デバイス→ホスト: 1バイト受信
r = libusb_control_transfer(devh,CTRL_IN,HID_GET_REPORT,
     (HID_REPORT_TYPE_FEATURE<<8) | 0x00, 0, buf,  PACKET_CTRL_LEN, TIMEOUT);

実行すると buf[0] に USB デバイスから受信したデータが格納される。
(今回は PACKET_CTRL_LEN を 1 にしているので、1バイトだけが受信される)。
受信に成功したバイト数を返す。デバイス側で送信するデータがなかった場合は 0 を返す。
エラーの場合はエラーコードを返す。

(7) インタフェースの解放
libusb_release_interface(devh, 0);

インタフェースの使用を終了する。

(8) USB デバイスのクローズ
libusb_close(devh);

USB デバイスの使用を終了する。


 

6. Digispark 側プログラムの説明


ソースファイル
usbconfig.h で HID デバイス用の設定を行っている。
ファームウェアの実体は main.c に書かれている。

メインルーチン
main() 内ではまず USB の初期化を行う。
メインループ内では 50ms 以内の間隔で usbPoll() を実行する必要がある。

USB 通信の処理
USB に関するリクエストが発生したときには usbFunctionSetup() が呼ばれる。
その後、デバイス→ホストの通信 (クラスリクエストの GET_REPORT) だった場合は usbFunctionRead() が呼ばれる。この関数内に USB デバイス側の送信処理を記述する。
ホスト→デバイスの通信 (クラスリクエストの SET_REPORT) だった場合は usbFunctionWrite() が呼ばれる。この関数内に USB デバイス側の受信処理を記述する。

割り込み
USB の通信のために、外部割り込み(PCINT4) のみを使っている (Digispark では PCINT4 端子が USB コネクタの D+ に接続されている)。これ以外の割り込みは自由に使用できる。タイマ割り込みも使用できるが、正確な間隔で割り込むためには、一時的に外部割り込みを禁止する必要がある(その間は通信に応答しなくなる)。
  
USB 通信中かの確認
USB 通信中に cli() で割り込みを禁止すると通信エラーが発生するため、一時的に割り込みを禁止する場合は、事前に main.c 中の USBNOTBUSY() マクロで、通信中かを確認して、通信中の場合は待つ必要がある。

レポートディスクリプタ
HID デバイスに必要なレポートディスクリプタを main.c 内で定義している。このレポートディスクリプタのサイズが usbconfig.h 内の USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH に書いてある。


補足、要調査
  • Digispark のウォッチドッグリセットがなぜかうまく動作しない。
  • PC 側ライブラリに libusb-1.x というバージョンもあるが、使い方が異なる模様
  • PC 側ソフトウェアで、該当するUSB デバイスが1つだけしかないと分かっている場合は今回の方法が使えるが、同一 ID のデバイスが複数の場合は列挙する必要がある。  
  • Linux でも動作するはずだが、 CentOS7 ではライブラリのバージョンが異なるらしくコンパイルできない。
  • PC 側プログラムは Windows(MinGW環境でコンパイル) でも動作するように書いてあるが、まだ手順を書いていない。





2017年9月3日日曜日

Digispark+gcc USB キーボードデバイス作成チュートリアル

下の赤色LEDはCAPSランプになっていて、他のキーボードの CAPS LOCK キーにも連動して点滅する

概要


Digispark+gcc チュートリアルの Part2 です。 

Digispark にファームウェアを書き込んで、USB キーボードとして動作させる方法を説明します。これは Code and Life さんの記事 USB HID keyboard with V-USB を参考にして Digispark  で動作するようにアレンジしたものです。

ファームウェアを C言語で書いて avr-gcc でコンパイルし、USB  で接続した Digispark に書き込みます (Arduino の開発環境を使いません)。

Raspberry Pi, FreeBSD で動作確認しました。

※ Windows のコマンドラインから Digispark に生のファームウェアを書き込む手順は、1回目の記事 参照

1. 開発環境の準備


以前の記事 「Digispark+gcc Lチカチュートリアル(RaspberryPi, Linux, FreeBSD 版)」 の 1. 2. を実行して、必要なツールをインストールし、Digispark への書き込みソフト micronucleus
を実行できるようにしておく。

2.  ソースコードのダウンロードとコンパイル


(1) Digispark_kbd.zip をダウンロードして(「エラー」と表示された場合もそのままダウンロードできる)、ローカルに展開する。
(2) make コマンド(Raspberry Pi では make コマンド、FreeBSD では gmake コマンド) を実行すると main.c がコンパイルされてバイナリ (Intel HEX) 形式のファームウェア sample1.hex が生成する。

3. ファームウェアの書き込み


1. で準備した書き込みツール micronucleus をパスの通った場所に置き
# micronucleus --run sample1.hex
を実行する(root 権限が必要)。

> Please plug in the device ...
> Press CTRL+C to terminate the program.

と表示されるので、この状態で USB 端子に Digispark を挿入するとファームウェアが書き込まれる。

4. 動作確認


(1) ファームウェアを書き込んだ Digispark を PC の USB 端子に挿入すると、5-6 秒後に USB キーボードとして認識される。Raspberry Pi, FreeBSD では dmesg コマンドを実行すると、USB デバイスの名前が確認できる(ベンダID=0x16c0, デバイスID=0x05dc)。

Raspberry Pi の例

% dmesg
[522793.926120] usb 1-1.3: New USB device found, idVendor=16c0, idProduct=05dc
[522793.926166] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[522793.926185] usb 1-1.3: Product: V-USB
[522793.926201] usb 1-1.3: Manufacturer: KBD
[522793.945119] input: KBD V-USB as /devices/platform/soc/20980000.usb/usb1/1-1/1-1.3/1-1.3:1.0/0003:16C0:05DC.0005/input/input4
[522794.000287] hid-generic 0003:16C0:05DC.0005: input,hidraw0: USB HID v1.01 Keyboard [KBD V-USB] on usb-20980000.usb-1.3/input0

FreeBSD の例

% dmesg
ugen0.5: <KBD> at usbus0
ukbd0: <KBD V-USB, class 0/0, rev 1.10/1.00, addr 12> on usbus0
kbd2 at ukbd0

Windows の例

Windows PC に挿した場合は HID キーボードデバイスとして認識されたことが、デバイスマネージャから確認できる。

 

(2) 基板上の LED (PB1 に接続) は CAPS LOCK ランプとして機能している。Digispark を接続した PC のキーボードの SHIFT+CAPSLOCKキーを押すと、本来のキーボードの CAPS LOCK ランプと同時に、Digispark 上の LED も点滅する。

(3) Digispark の P0 端子(PB0)と GND 端子をショートさせると、キーボードの "x" キーを押して離す動作が実行されて、画面上に x と表示される。




2017年8月21日月曜日

Digispark+gcc Lチカチュートリアル(RaspberryPi, Linux, FreeBSD 版)

 

概要


Digispark+gcc チュートリアルの Part1 です。 

Digispark 用のプログラムを C言語で書いて gcc でコンパイルし、USB  で接続した Digispark に書き込んで実行する方法を説明します(Arduino の開発環境を使いません)。以前の記事では手順が分かりにくかったので、あらためてまとめます。

Raspberry Pi, Linux, FreeBSD で動作確認しました。

動作確認したOS
  • FreeBSD: FreeBSD freebsd11 11.1-RELEASE i386
  • Raspberry Pi: Linux raspberrypi 4.4.11+ #888 Mon May 23 20:02:58 BST 2016 armv6l GNU/Linux
  • Linux (CentOS7): Linux localhost.localdomain 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
WindowsPC から書き込む方法
 
Windows でも https://github.com/digistump/DigistumpArduino/releases/download/1.6.7/Digistump.Drivers.zip のドライバをインストールすれば、同梱の  micronucleus.exe で書き込めるようになります。

Windows で micronucleus.exe が動作しない場合は、スタティックリンク版 を作成しましたので、こちらを利用して下さい (その場合も、上記の Digistump.Drivers.zip のドライバのインストールは必要)。

1. 必要なツールをパッケージインストール


(1)  以下のパッケージをインストールする。

○ RaspberryPi の場合["apt-get install" コマンドでインストール]
  • libusb-dev
  • gcc
  • gcc-avr 
  • avr-libc
FreeBSD の場合 ["pkg install" コマンドでインストール]
  • gmake-4.2.1_1
  • gcc-5.4.0_2
  • avr-gcc-5.4.0
  • avr-libc-2.0.0_1,1
Linux (centOS) の場合 ["yum install" コマンドでインストール]

※ yum の epel リポジトリの登録が必要
  • libusb-devel.x86_64
  • gcc.x86_64
  • avr-libc.noarch
  • avr-gcc.x86_64

2. ファームウェアの書き込みツールをコンパイル


コマンドラインから Digispark に USB 経由でファームウェアを書き込むために Micronucleus V2.03 という書き込みツールをコンパイルしておく。

(1) https://github.com/micronucleus/micronucleus/ から [Clone or Download] ボタンをクリックして zip ファイルをダウンロードし、ローカルに展開する。
(2) micronucleus-master/commandline/ に移動する。
(3) make コマンド(Raspberry Pi では make コマンド、FreeBSD では gmake コマンド) を実行すると、実行ファイル micronucleus が生成する。



3. ファームウェアのソースコードの作成


(1) 次のファイルを main.c として保存する。


// main.c: LED の点滅 
#include <avr/io.h>
#include <util/delay.h> //_delay_ms() を使うために必要
int main(void) {
  DDRB  |= _BV(PB1);    // PB1 を出力ポートにする
  while(1) {
    PORTB |=  _BV(PB1); // LED 点灯
    _delay_ms(100);     // 100ms ディレイ
    PORTB &= ~_BV(PB1); // LED 消灯
    _delay_ms(200);     // 200ms ディレイ
  }
  return 0;
}


4. ファームウェアのコンパイル


(1) 次のファイルを Makefile として保存する。3カ所行頭から字下げしている箇所があるが、スペース文字でなくタブで字下げすること。


# Makefile
CC  = avr-gcc
MCU = attiny85
TRG = sample1
OBJ = main.o

# Options
ASFLAGS   = -gstabs -mmcu=$(MCU) -DF_CPU=16500000L -DDEBUG_LEVEL=0
OPTIMIZE  = -Os
CFLAGS    = -g -Wall $(OPTIMIZE) -mmcu=$(MCU) -DF_CPU=16500000L
LDFLAGS   = -Wl,-Map,$(TRG).map
OBJCOPY   = avr-objcopy
OBJDUMP   = avr-objdump

all:  $(TRG).hex $(TRG).lst

%.hex:  %.elf
  $(OBJCOPY) -j .text -j .data -O ihex $< $@

$(TRG).elf: $(OBJ)
  $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^

%.lst:  %.elf
  $(OBJDUMP) -h -S $< > $@


(2) make コマンド(Linux/Raspberry Pi では make コマンド、FreeBSD では gmake コマンド) を実行すると main.c がコンパイルされてバイナリ (Intel HEX) 形式のファームウェア sample1.hex が生成する。
 

5. ファームウェアの書き込み


(1) 2. で作成した書き込みツール micronucleus をパスの通った場所に置き、
# micronucleus --run sample1.hex
を実行する(root 権限が必要)。

> Please plug in the device ...
> Press CTRL+C to terminate the program.

と表示されるので、この状態で USB 端子に Digispark を挿入するとファームウェアが書き込まれ、続けて実行される(書き込みが始まらない場合は、一度 CTRL+Cを押して停止し、再度実行する)。書き込みが完了すると、進行度の表示が 100% になる。Digispark 上の LED が点滅すれば、ファームウェアが正常に動作している。


2016年12月2日金曜日

USBメモリの寿命と中身

以前 USB メモリ(Buffalo RUF2-PS16GS)に FreeBSD をインストールして使っていたことがある(この記事)。問題なく使えていたのだが、運用開始から約 2年5ヶ月でその USB メモリは読み書きできなくなり、OS が起動しなくなってしまった。ちなみにその後の FreeBSD は素直にメインストレージを SSD に変更したところ、3年以上問題なく動作し続けている。

読み書きできなくなった USB メモリは別の Windows PC に挿しても全く反応しない状態になってしまっていた。最近この USB メモリの中身が マイクロ SD カードだという情報を聞いて分解してみたところ、本当にマイクロ SD カードが出てきたので写真を残しておく。





2016年5月21日土曜日

DigiSpark と FreeBSD の間の通信

 

 

 はじめに

Arduino の小型ボード DigiSpark は、ソフトウェア USB を実装していて HID デバイスとして認識されるので、しかるべきファームウェアを書き込めば、ホスト PC と通信ができる。

ホスト PC 側で実行するサンプルコードとして、DigiSpark に文字列を送信する "send" というソフトがあったので、これを FreeBSD でコンパイルして、FreeBSD に接続した DigiSpark の LED を制御する実験を行った。デフォルトでは FreeBSD で作成したプログラムから DigiSpark が認識されなかったため、FreeBSD のカーネルの再コンパイルが必要だった(最近の OS ではカーネルの再コンパイルは不要。追記参照)。

DigiSpark のスケッチの修正と書き込み

すでに HEX ファイルがある場合は FreeBSDでファームウェアを書き込めるが(「DigiSpark に FreeBSD から書き込む」参照)、今回ベースにしたコードは Arduino のスケッチなので、今のところ統合開発環境(IDE)が対応している OS で開発するしかない。今回は Windows 上でサンプルのスケッチを修正して、DigiSpark への書き込みまでを行った。

公式ページ https://www.arduino.cc/en/Main/Software/ から DigiSpark に対応した Arduino の統合開発環境(IDE) DigisparkArduino-Win32-1.0.4-May19.zip をダウンロードし、Windows にインストールする(ここで Windows 用のデバイスドライバも入れる必要がある)。

IDE(arduino.exe)を起動し、 メニューから [ファイル]→[スケッチの例]→[DigisparkUSB]→[Echo] を開く。このサンプルファームウェアは PC から USB 経由で送られてきた文字列をそのまま返すものだが、これに少しだけ追加して、大文字の "B" を受信したときは LED を ON, 小文字の "b" を受信したときは OFF にするようにした(文字 "B" を使ったのは、後から LED を増やして上の動画のように ABCD の4つの LED を制御するため)。

DigiUSB.write(lastRead);

の行の直後に次の行を追加する。
// [サンプル Echo の修正点]
// PB1 を出力ピンにする
DDRB = _BV(PB1);
// "B" 受信時に LED 点灯
if (lastRead == 'B') { PORTB |= _BV(PB1); }
// "b" 受信時に LED 消灯
if (lastRead == 'b') { PORTB &= ~_BV(PB1); }
修正はこれで OK。完成したスケッチを Windows の IDE で DigiSpark に書き込んでおく。

FreeBSD での文字列送信ソフトのコンパイル

FreeBSD 6.4 で DigiSpark に文字列を送るソフト send をコンパイルした。

Github https://github.com/digistump/DigisparkExamplePrograms/ から、通信用ソフトのサンプル DigisparkExamplePrograms-master.zip をダウンロードして FreeBSD 上に展開する。DigisparkExamplePrograms-master/C++/send/ に send のソースファイルがある。

libusb-0.1 を ports からインストールする(あれば。新しい OS では不要)。
コンパイル時には Makefile の修正が必要。OS 判定部分の
ifeq ($(shell uname), Linux)

ifeq ($(shell uname), FreeBSD)
に修正して make を実行すると、実行プログラム send が生成する。

(追加: FreeBSD 9.2 の場合)
libusb のインストールは不要(というかない?)。
send.cpp の先頭に
#include <libusb.h>
を追加する。また Makefile に
USBLIBS = `libusb-config --libs` -lusb
を追加する(USBLIBS は OS 毎に複数あるので FreeBSD 用の行を修正する)。

FreeBSD カーネルの再コンパイル


(注) 新しい OS では状況が違い、再コンパイルは不要。下の追記参照。

ここで FreeBSD に DigiSpark を接続して send コマンドを実行しても "No Digispark Found" と表示されて動作しない。http://nonakap.hatenablog.com/entry/2015/12/24/000700 を見ると、 カーネルが uhid や ukbd として認識した USB デバイスは libusb を使ったユーザプログラムから発見できないのが原因のようだ(上記 URL のサイトを参考にさせていただきました。どうもありがとうございます)。

# usbdevs -v
(または # usbconfig dump_device_desc)

コマンドで調べると
port 1 addr 4: low speed, power 100 mA, config 1, DigiUSB(0x05df), digistump.com(0x16c0), rev 1.00
と表示されるので、動作中の DigiSpark のベンダ ID は 0x16c0, プロダクトID は 0x05df とわかる。このデバイスを無視するように FreeBSD(FreeBSD 6.4) のソースを修正する。/usr/src/sys/dev/usb/usb_quirks.c
を修正して赤字部分を追加し、カーネルを再コンパイルする。
/* Devices which should be ignored by both ukbd and uhid */
{ USB_VENDOR_CYPRESS, USB_PRODUCT_CYPRESS_WISPY,
ANY, { UQ_KBD_IGNORE }},
{ 0x16c0, 0x05df,
ANY, { UQ_KBD_IGNORE }},
{ 0, 0, 0, { 0 } }
};
FreeBSD を再起動すると send が動作するようになる。

実行

send の実行には root 権限が必要。一般ユーザでは "No Digispark Found" と表示される。
FreeBSD 機の USB 端子に DigiSpark を接続する。正しく認識されていると dmesg コマンドで
ugen0: digistump.com DigiUSB, rev 1.10/1.00, addr 4
等と表示される。ここで
# send "B"
で DigiSpark  の LED が点灯し
# send "b"
で消灯する。



【追記】
新しい OS では USB の挙動が異なる。FreeBSD9.2 ではカーネルの再構築は不要で、dmesg コマンドで
uhid1: <digistump.com DigiUSB, class 0/0, rev 1.10/1.00, addr 2> on usbus0
と表示されている状態で、カーネルには手を加えなくても send が動作した。
/sys/dev/usb/quirk/usb_quirk.c の修正が必要ではないか。man usb_quirk によると、無視するデバイスをusbconfig(5) コマンドで動的に追加できるかもしれない

補足1(Windows PC から制御する場合)


Windows ではドライバ
DigisparkArduino-Win32\DigiUSB Programs\DigiUSB Windows Driver
を手動インストールすると
DigisparkExamplePrograms-master\Python\DigiUSB\windows\send.exe
が動作するようになる。ドライバのインストール前は send.exe 実行時に"No DigiUSB Device Found" と表示されて動作しない。

ただしサンプルの send.exe でなく、 libusb1.0 を使って作成した自作した Windows 用アプリケーションでは Windows7/2000 にドライバを入れなくても動作したので、必須ではないと思われる。

補足2(Arduino を使わず V-USB を直接使う)


Arduino を使わず V-USB を直接 AVR-GCC でコンパイルしてテストしたので覚え書き。Tiny85 に HID で通信するファームウェアを書き込んで動かすところまではできた。
最初はサンプルのファームウェア (vusb-20121206\examples\hid-custom-rq\firmware\main.c) 中の下記のコードの赤文字の部分が 0x01 になっていたが、その状態だと Linux, FreeBSD との通信はできたが、Windows (+libusb1.0 +MinGW) の libusb_control_transfer() という関数で Tiny85→Windows の通信時、データを受信していないのに 1バイト受信したという間違いの値が返ってきた。
USB の原理やディスクリプタについて分かっていないのが問題。要調査。

/* ------------------------------------------------------------------------- */
/* ----------------------------- USB interface ----------------------------- */
/* ------------------------------------------------------------------------- */

PROGMEM const char usbHidReportDescriptor[22] = {   /* USB report descriptor */
    0x06, 0x00, 0xff,              // USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 0x08,                    //   REPORT_COUNT (1)
    0x09, 0x00,                    //   USAGE (Undefined)
    0xb2, 0x02, 0x01,              //   FEATURE (Data,Var,Abs,Buf)
    0xc0                           // END_COLLECTION
};
/* The descriptor above is a dummy only, it silences the drivers. The report
 * it describes consists of one byte of undefined data.
 * We don't transfer our data through HID reports, we use custom requests
 * instead.
 */

DigiSpark に FreeBSD から書き込む


はじめに


(2017/08/31 追記) 手順がわかりにくいので新記事として書き直しました。こちらのページをご覧ください。


DigiSpark は ATTiny85 を使った Arduino 互換の小型ボード。下記のようにいろいろとすごい。
  • 小さくて安い。USB コネクタに直挿しできる。正規品でも 1000円以下。クローン品だと200円以下。
  • AVR マイコンのソフトウェア(V-USB) だけで USB を実現していて、HID デバイスとして認識され、ツール(micronucleus) を使ってファームウェアを書き込める。
  • USB 通信機能は書き込み用だけでなくファームウェアからも呼び出せて、しかるべき通信をするファームウェアを書き込めば、USB 経由で PC と通信できる。
本来は Arduino 互換ボードとして使うものだが、0番地から始まる通常の ATTiny85 のファームウェアを書き込めば(ファームのサイズが 6kB 以下なら) 動作するので、C言語(avr-gcc) で開発したファームウェアを書き込んで ATTiny85 ボードとして使える。今回はこのような使い方が前提。

専用の書き込みツールはもともと Windows, Mac, Linux,OpenBSD に対応しているようだが、試してみたところ FreeBSD からも書き込みができたので報告する。通信用のファームウェアを書き込んで FreeBSD と USB で通信する方法も別稿で紹介する。

コマンドライン版書き込みツール micronucleus (Windows 版)

DigiSpark に対応した Arduino の統合開発環境 

https://sourceforge.net/projects/digistump/files/DigisparkArduino-Win32-1.0.4-May19.zip/download
(または  https://www.arduino.cc/en/Main/Software/ )


をダウンロードする。Windows 用の書き込みツール Digispark-Arduino-1.0.4\hardware\tools\avr\bin\micronucleus.exe がこの統合開発環境に含まれている(Windows では書き込み前にドライバのインストールが必要。ドライバが古いと書き込みソフトが応答しなくなる。 こちらの手順を参照して Digistump.Drivers.zip をインストールする)。

Windows での使い方は
C:\> micronucleus.exe --run FILENAME.hex
実行後、一度 DigiSpark を USB コネクタから抜いて挿しなおすと FILENAME.hex が DigiSpark に書き込まれる。

FreeBSD 用 micronucleus のコンパイル

この書き込みツール micronucleus の FreeBSD 版をコンパイルする。開発元のページ http://digistump.com/wiki/digispark/tutorials/connecting/ から、書き込みツール micronucleus の github にリンクがある。ここから micronucleus-master.zip をダウンロードして展開する。書き込みツールのソースファイル micronucleus-master/commandline/ を FreeBSD に置く。

コンパイル時に Makefile の修正が必要。OS 判定部分の
else ifeq ($(shell uname), OpenBSD)

else ifeq ($(shell uname), FreeBSD)
に修正する。make を実行すると micronucleus が生成する(FreeBSD6.4 で確認。libusb-0.1.12_2 を ports でインストールしてある)。

micronucleus の実行には root 権限が必要。
# micronucleus --run FILENAME.hex
で FILENAME.hex が書き込める。

備考

ATTiny85 には他の高機能のマイコンと異なり、専用のブートローダ領域がなく、割り込みベクタも移動できない。どうやってブートローダを実現しているのかと思ったら、下記のような工夫をしているそうだ(フラッシュメモリの先頭にある割り込みベクタをブートローダから書き換える。ブートローダはフラッシュの末尾に置く)。
http://digistump.com/board/index.php?topic=168.5;wap2

C言語(avr-gcc) で開発したファームウェアを書き込んだところちゃんと動いたが、ウォッチドッグリセットがうまくいかない(ウォッチドッグリセットの発生後フリーズしてしまう。ファームウェアの書き込みモードに移行せず、書き込んだファームウェアも再始動しない)。リセットせずウォッチドッグ割り込みを発生するだけなら正常に動作する。

参考

DigiSpark 国内販売店
http://www.elefine.jp/SHOP/Digispark.html/

DigiSpark 回路図
https://s3.amazonaws.com/digispark/DigisparkSchematicFinal.pdf

ArduinoのHEXファイルを残す方法
http://d.hatena.ne.jp/licheng/20130826/p1
参考にさせていただきました。どうもありがとうございます。

Digispark のヒューズ
hfuse=5FH, lfuse=F1H, efuse=FEH (RESET 無効, 低電圧プログラミング不可, BOD なし)


2016年3月1日火曜日

One output pin drives 2 LEDs





Two LEDs (red and green) are controlled by only one output pin. The LED1 will be ON by 62.5kHz, 1/16 duty cycle pulse. The LED2 will be ON by  H-level output.
(The duty cycle must be 1/16. Note: 1/2 duty cycle pulse can't turn on LED1 at 3[V] Vcc.)


Vcc=3-6[V]




Output(pin 6)RED(LED1)GREEN(LED2)
LOFFOFF
62.5kHz, 1/16 duty cycle pulseONOFF
HOFFON
The pulse and H every 0.5[ms]ON(visually)ON(visually)



62.5kHz, 1/16 duty cycle pulse output


H level output

2016年2月23日火曜日

一本の出力ピンだけで2色のLEDを駆動する





8pin のマイコンを使っていると、入出力のピン数が足りなくなることがよくある。AVR の Tiny85 で赤外線リモコンを作っていて、表示用に赤と緑の LED を付けようとしたら、残っている出力ピンが1本しかなかった。

1本の出力ピンは H, L, Hi-Z(AVR では入力ピンにして内蔵プルアップを無効にした状態)の3種類の状態にできるのだから、簡単に赤LED、緑LED、消灯の3種類を制御できそうなものだが、回路を作ろうとすると結構難しい。Charlieplexing という少ないピン数で多数の LED を駆動するテクニックがあるが、さすがにピンが1本だけではその手は使えない。

検索してみると、世界は広いもので、同じ問題で困っている人がいるらしく、下記の英文記事があった。
いろいろと工夫してあっておもしろい(上の記事の Figure1 などは最初発振回路かと思った)。ただ、これらの方法には
  • 消灯時(赤LED、緑LED の両方が消えているとき)にも常に電力を消費してしまう
とか
  • 結構な外付け部品が必要

という問題があって、電池駆動のちょっとした回路では使いにくい。


そこで、出力ピンの先に抵抗とコンデンサだけで作った HPF(ハイパスフィルタ)とLPF(ローパスフィルタ)を付け、それぞれの先にLEDを付けて、高周波のパルスを出力した時には LED1 が点灯, H を出力した時には LED2 が点灯, Hi-Z Lにしたときは両方消灯するという回路を考えた。


一本の出力ピンだけで2色のLEDを駆動する, Vcc=3[V]


PB1(pin6) が出力ピン。PB1 に 62.5kHz のパルスを出力すると LED1(赤) が、H を出力すると LED2(緑) が点灯する。L にすると両方消灯する。

LED1 を点灯: A-B 間の電位(62.5kHz、デューティー比 1/16, 電源 CR2032)

LED2 を点灯: A-B 間の電位(H 出力, 電源 CR2032)

外付け部品は抵抗2個とコンデンサ2個のみ。どちらの LED も光っていないときは電力を消費しない。これで完璧、と思ったが、やってみると意外と条件が厳しい。この RC回路のフィルタではパルスと直流をきれいに分離できず、片方だけを点灯させるつもりでも、消灯している方がわずかに光ってしまうことがある。また通常の矩形波で駆動すると、電源電圧によっては LED1(高周波で駆動する方) にかかる電圧が低くて光らない。

試行錯誤の結果、パルスは 62.5KHz, デューティー比 1/16~1/32 で、上の方に載せた回路図にある定数の部品を使うとよいことが分かった。下記はこのパルスを出力した時の B-C 間の電圧(= LED1 にかかる電圧)。


62.5KHz, デューティー比 1/16 出力時に LED1 にかかる電圧(B-C 間)


このように、デューティー比を 1/2 でなく細いパルスにすると、LED1(高周波で駆動する方) にかかる電圧が上がってなんとか光るようになった。ただ目で見てもわからないが、間欠的にしか光っていないので、どうしても LED2(直流で駆動している方) よりも暗くなってしまう。対策としては LED2 の電流制限抵抗を大きくして暗くするなど。 LED1 に高輝度 LED を使う方法もあるが、そうすると LED2 の点灯開始時に LED1 が一瞬光るのが見えてしまうかもしれない。その場合はC2を2200pF に変更するとよい。

なんとかして 3個の LED の制御もできないだろうか。やはり難しいか。

【追記】
上記の 62.5kH のパルスとH連続の直流を 0.5[ms]周期(2kHz)で繰り返すと、2つの LED は同時に点灯しているように見える。 一番上の動画参照。


2015年2月28日土曜日

AVRと赤外線センサを使った回路の省電力化

赤外線受信モジュールを使った AVR の回路を、コイン電池 CR2032 でどれくらいの時間駆動できるかを測定した。

赤外線センサー GP1UXC41QS は 0.3~0.6[mA] の電流を消費する。コイン電池 CR2032 の容量が 225[mAh] とすると、センサーの消費電力だけで 375時間=15日 くらいで電池がなくなってしまう。これに AVR マイコンの消費も加わるので、実際はもっと短くなる。

そこで、省電力化のために、通常は AVR マイコンをスリープ状態にして、ウォッチドッグタイマ割り込みで定期的に起動し、赤外線センサーの入力を見て、赤外線を受信していなければまたスリープすることにした。 赤外線センサーの電源は、AVR マイコンの出力ピンから取り、スリープ中は OFF にする。

この方法はウォッチドッグタイマ割り込みのある AVR でしかできない。今回は Tiny10 でテストした。メインクロックは内蔵 CR で 1[MHz] とする。

スリープ後 125[ms] にウォッチドッグ割り込みが発生するように指定して、割り込みが発生したら IR センサの電源を入れて 1[ms] 待つ (センサの出力の安定を待つため)。その後 IR センサの出力を読んで、L (赤外線信号あり) ならコードを実行、H (赤外線信号なし) ならスリープする。これの繰り返しを行う。

10Ωの抵抗は電流測定用。この両端の電圧をオシロで測定して、消費電流を測定した。

結果は、1秒間に約7回、1.06[ms] の間、最大約 6[mA] のパルス状の電源電流が流れた(オシロのピーク値測定モードで測定したので、実際の消費電流はこれより小さいと思う)。それ以外 (スリープ中) の消費電流はノイズに埋もれて測定できなかったが、アナログテスターでの測定とデータシートによると、約 4[μA] くらいと思われる。

平均すると、常時 50[μA] 程度の電流が流れているのと等価になる。容量 225[mAh] の CR2032 なら 4500時間=187日間動作するはず。あくまで理想上の計算なので、実験してみないとわからないが。

割り込み周期を変更したり、待ち時間を短縮すれば、さらに消費電力化も可能と思われる。

ちなみに家電用の赤外線リモコンの場合、1ビットを表すパルス1個は 1~4[ms] 程度の長さ。信号全体でも 100~200 [ms] 程度のことが多いので、今回の方法の速度では、信号全体を取り込むことはできない (割り込みで赤外線信号に気付いた時は、すでに信号の前半を取りこぼしてしまっている)。信号が繰り返し送られることを前提として受信するか、リーダーが長い特殊な赤外線フォーマットで通信する必要がある。

ウォッチドッグタイマ割り込みで定期的にIRセンサを起動 (Tiny10, 1MHz)。
PB0 から IR センサの電源を出力する。10Ωの抵抗は電流測定用。




// 消費電力測定: Tiny10 用
#include <avr/io.h>
#define F_CPU 1000000UL
#include <util/delay.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/wdt.h>

// 起動前にウォッチドッグを禁止
uint8_t mcusr_mirror __attribute__ ((section (".noinit")));
void get_mcusr(void) __attribute__((naked)) __attribute__((section(".init3")));
void get_mcusr(void) {
  mcusr_mirror = RSTFLR;
  RSTFLR = 0;
  wdt_disable();
}

// ウォッチドッグ割り込みハンドラ
EMPTY_INTERRUPT(WDT_vect)

int main(void) {
  DDRB = 0b11111011; // PORTB 0,1,3 を出力に
  PUEB = 0b00000000; // プルアップしない

  // ウォッチドッグタイマ割り込みを有効にして(WDIE=1)
  // ウォッチドッグリセットは発生しないようにする(WDE=0)(125ms)
  RSTFLR = 0;
  WDTCSR = _BV(WDIE) | _BV(WDP1) | _BV(WDP0) ;
  
  sei();
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);

  while(1) {
    PORTB |= _BV(PB0); // IR 電源 ON
    _delay_ms(1);      // 1[ms] 待つ
    if ((PINB & _BV(PB2)) == 0) {
      PORTB &= ~_BV(PB0);  // IR 電源 OFF
      // ここに赤外線を受信したときに実行したい処理を書く
    }
    PORTB &= ~_BV(PB0);  // IR 電源 OFF
    wdt_reset();
    WDTCSR |= _BV(WDIF);
    sleep_mode();
  }
}


パルス状に見えるのは1秒間に約7回(144[ms]間隔)の電源 ON。
それ以外の期間はスリープ。スリープ中の測定ノイズが激しい。
オシロはピーク検出モードにしている。

1回のパルスを拡大すると、長さは1.06[ms]。
この期間、最大約 6[mA] の電流が流れている。

2015年1月30日金曜日

Perl でキーボードからリアルタイムに文字入力

Perl で入力した文字をリアルタイムに読みたいことがあるが、普通に

$char = <STDIN>;

と書くと
  • ENTER キーを押すまでは入力されない
  • 入力した文字がそのまま画面に表示される(エコーバック)
という問題がある。

◇ (方法1) Term::ReadKey を使う (Windows, UNIX)


Windows で使える方法はないかと思って調べたら、実は PerlFAQ に書いてあった方法。知らなかったが通常はこちらで OK だろう。

use Term::ReadKey;
ReadMode('cbreak');
while(1) {
  if (defined ($char = ReadKey(-1)) ) {
    # 文字が入力された場合
      print STDERR $char, "\n";
  } else {
    # 文字が入力されていない場合
  }
}
# ReadMode('normal');


こう書けば押したキーの文字がリアルタイムで $char に代入される。
何も押さなかった場合は「# 文字が入力されていない場合」のほうが実行される。

"Term::ReadKey" モジュールのインストールが必要。
便利なことに Windows のプロンプトでも利用できる。ActivePerl5.20.1 ではTerm::ReadKey モジュールは最初からインストールされていた。

◇ (方法2) stty コマンドを使う (UNIX)

$|=0;

system("stty cbreak");
system("stty -echo");

while(1) {
  sysread(STDIN,$char,1);
  print STDERR $char, "\n"

}

UNIX 限定だが、system() 関数で stty コマンドを実行することでも似たことが実現できる。
"Term::ReadKey" モジュールがインストールできない場合はこちらで。 ただ、この書き方では何か文字が入力されるまで sysread(STDIN,$char,1); のところで動作が停止(ブロック) してしまうので、リアルタイムな処理はできない。リアルタイムに処理するためには工夫が必要になる。

2014年6月22日日曜日

AVR-GCC でタイマ割り込みに間に合わない場合(部分的に最適化レベルを変える)

タイマ割り込み中に行う処理は、当然だが割り込み間隔以下の時間で実行が終わるステップ数で書かないといけない。

AVR-GCC でコンパイルするときには、できるだけコードサイズを節約するために、最適化オプションをコードサイズ優先 (-Os) にすることが多い。しかし、タイマ割り込み中に実行する命令が多すぎて、割り込み間隔に間に合わないときには、割り込みハンドラの部分だけを速度優先 (-O3) で最適化してほしい。

これは下記のように 割り込みハンドラの前に #pragma を置いて最適化レベルを速度優先 (-O3) と指定すれば実現できる。割り込みハンドラの後ろにも #pragma を置いて、最適化レベルをコードサイズ優先 (-Os)に戻す

#pragma GCC optimize ("O3")
ISR(SIG_OUTPUT_COMPARE0A) {
  タイマ割り込み中に行う処理
} 
#pragma GCC optimize ("Os")

コンパイルするときに下記のように -Os オプションを付ければ、割り込みハンドラ以外はコードサイズ優先 (-Os) で、割り込みハンドラ部分だけ速度優先 (-O3) で最適化される。

% avr-gcc -g -Os -mmcu=atmega8 -o out.o main.c

2014年6月19日木曜日

AVR-GCC 用内蔵 EEPROM 簡易ファイルシステム

ダウンロード

MEGA8 と TINY85 で動作確認したが、おそらく他の AVR でも使える。avr-gcc, avr-libc でプログラミングする人向け。

解説

多くの AVR マイコンには EEPROM (不揮発性メモリ) が内蔵されている。容量は少ない機種で 64Byte、多くても数キロバイト程度だが、電源を切っても消えないので、設定やデータをちょっと保存するのに便利だ。

AVR 用のライブラリ avr-libc には EEPROM を読み書きする関数があるので、バイト単位やワード単位でアドレスを指定して読み書きできる。しかし、EEPROM に複数の可変長のデータを保存したい時など、もう少しだけ扱いやすい読み書きの手段が欲しい。自分は赤外線リモコンを作った時に学習データ(可変長。複数。学習のたびに書き換えが必要)を保存するために必要だった。

そこで、AVR 内蔵 EEPROM 用に簡易ファイルシステムを作ってみた。C 言語でファイルアクセスする時の open(), close() といったスタイルをまねてはいるが、必要最小限の機能のみに絞った。原理はおそらく誰もが思いつく一番単純な方式。

コードサイズは1kB弱。使わない関数をコメントアウトすればもう少しだけ小さくできる。

使用例


例として、MEGA8 を PC のシリアルポートに接続して、MEGA8 から出力した文字列を PC に表示させる回路を作った。



MEGA8 に書き込んだデモのコードは次の通り。このプログラムは、文字列を直接シリアルポートに出力するのではなく、一旦ファイルに保存して、ファイルから読みだした文字列をシリアルポートに出力するというもの。

// EEPROM 用簡易ファイルシステムのデモ (ATMEGA8)
#include <avr/io.h>
#include <avr/eeprom.h>
#include "fs.h"

// シリアルポートの初期化(1200bps, MCU クロック=1MHz)
void uart_init(void) {
  UCSRB  = _BV(TXEN);
  UBRRH=0;  UBRRL= 51;
}

//
// シリアルポートに1文字出力
//
void  uart_putchar(char c) {
  loop_until_bit_is_set(UCSRA, UDRE);
  UDR = c;
  return;
}

// メイン
//
int main(void) {

  uint8_t fileName;
  char buf[100];
  char *pt;

  uart_init();
  fs_init();

  fileName = 0;
  if (fs_ropen(fileName) == FS_OK) {
    // ファイル0 がオープンできたら、ファイルからシリアルポートに出力
    // 1行目表示
    while (!fs_EOF()) uart_putchar(fs_read_b());
    // 2行目表示
    fileName = 1;
    fs_str_r(buf, fileName, 100);
    pt=buf;
    while (*pt) { uart_putchar(*pt); pt++; }
  } else {
    // ファイル0 がオープンできなかったら、文章をファイルに書き込む
    fileName = 0;
    if (fs_wopen(fileName) == FS_OK) {
      char* pt = "恥の多い生涯を送って来ました。"
        "自分には、人間の生活というものが、見当つかないのです。\r\n";
      while (*pt!='\0') { fs_write_b(*pt); pt++; }
      fs_wclose();
      fileName = 1;
      fs_str_w("自分は東北の田舎に生れましたので、汽車をはじめて見たのは、"
               "よほど大きくなってからでした。\r\n", fileName);
    }
  }
  while (1); // どちらかの処理をした後は無限ループ
  return 0;
}


このソースファイル(main.c) と同じ場所に "fs.c" と "fs.h" を置き、コンパイルするときに fs.c にある関数がリンクされるようにする。コンパイルの手順はこんな感じ。

# コンパイルとリンク,Intel HEX 形式の出力
% avr-gcc -g -Os -mmcu=atmega8 -o out.o main.c fs.c
% avr-objcopy -j .text -j .data -O ihex out.o out.hex

ヒューズは MEGA8 のデフォルト(hfuse=0xd9, lfuse=0xe1)にする。

生成した out.hex を MEGA8 に書きこんだら、上記の回路図に従って MEGA8  を PC のシリアルポートに接続して、PC 上で TeraTerm を起動する。TeraTerm でシリアルポートを 1200bps に設定する。

1回目の電源ONではファイルが存在しないため、fs_ropen(fileName) はエラーを返すので、ファイルに文字列が書き込まれるだけで、シリアルポートには何も出力されない。2回目以降の電源 ON ではファイルが存在するので fs_ropen(fileName) に成功して分岐し、ファイルから文字列が読み出されシリアルポートに出力される。Teraterm で見ると、2回目以降の実行ではちゃんと文字列が出力されている。



ファイルシステム fs.c の使い方

 使い方は次の通り。ファイル名(というかファイル番号) は、0~255 の値1バイトのみ。

ファイルから読み込む場合は、まず fs_ropen(fileName) でファイルを読み込みモードでオープンする。続けて fs_read_b() (8bitの場合)または fs_read_w() (16bitの場合) を繰り返して読み込む。読むと自動でポインタが進む。最後まで読むと fs_EOF() が真になる。読み込みモードでオープンした場合はクローズはしなくてもよい。

ファイルに書き込む場合は、まず fs_wopen(fileName) でファイルを書き込みモードでオープンする。fs_write_b(8bit値) または fs_write_w(16bit値) を繰り返して書き込む。書き込むと自動でポインタが進む。書き終わったら fs_wclose() でクローズする必要がある。

オープンできるファイルは、読み込みと書き込み合わせて1つだけ。同時オープンはできない(EEPROM の書きこみ/読み込み位置を保持している変数が1つしかないため)。

オープン、クローズの手続きが不要で、文字列を一気に読み込む fs_str_r(buf, fileName, maxlen) という関数と、 同じく文字列を一気に書き込む fs_str_w(buf、fileName) という関数もある。文字列を扱うならそちらの方が便利。

構造

全てのファイルの先頭に3バイトのヘッダを付けて EEPROM に格納する。ヘッダの1バイト目はファイル名、2~3バイト目はファイルサイズ。ファイルは詰めて記録し、最後のファイルの後に終了マークとして 0xFF を3バイト書く。構造はこれだけ。

ファイルが何もない状態から、ファイル名=2 のファイル(サイズ=10バイト)を書き込み、次にファイル名=0のファイル(サイズ=15バイト)を書き込むと、EEPROM は下の図のような状態になる。

もし下記のような状態でファイル2が削除されたときは、その下にあるファイル0を上方向(低アドレス側)に詰める。つまりファイル0のヘッダとファイル0の実体、終了マーク全体を13バイト上にずらす EEPROM のコピーの動作が自動的に行われる。非効率的で遅いが、EEPROM のサイズは小さいので、このような力技でもなんとかなる。もっとも頻繁にファイルの削除や更新が発生する用途やタイムクリティカルな用途には向かない。

fs.h で指定された EEPROM のアドレス範囲外には書き込みが行われないようにアドレス範囲をチェックしていて、もしアドレス範囲をオーバーしている時は実際の書き込みは行わず、戻り値でエラー(FS_DISKFULL) を返す。ファイルの書き込み後にはファイルクローズが必要だが、クローズ時にアドレス範囲のオーバーが発生していないことを再度確認している。なので、書き込み後にはファイルをクローズして fs_wclose() の戻り値が FS_OK になっていることを確認する必要がある。

EEPROM のデータ構造 (ファイル2とファイル0の2個のファイルが保存されている状態)


定義

注: ソースファイルの方の変数の型の書き方を変更しました (例: unsigned char → uint8_t など)。プログラムの動作は変わりませんが、下記の説明の型とは食い違っています。

定数
#define EEPROM_FS ((uint8_t *)0)
ファイルシステムで使用したい EEPROM の先頭アドレスを fs.h 内のこの #define 文で指定すること。普通はこの例のようにアドレスに 0 を指定すればよい。 EEPROM の先頭アドレスから使用される。


定数
#define FS_SIZE (E2END-(unsigned int)EEPROM_FS+1-4)
ファイルシステムで使用したい EEPROM のサイズを fs.h 内のこの #define 文で指定すること。E2END が EEPROM の最終アドレス(avr-libc で定義済み)。EEPROM の全領域を使用するなら E2END-EEPROM_FS+1 を指定する。ただし EEPROM の末尾 4バイトはフラッシュメモリの書き込み回数の記録に使われることが多いので、普通はこの例のように -4 して末尾 4 バイトを空けたほうが良い。

マクロ
char fs_EOF()
fs_ropen() による読み込みオープン中のみ有効。オープン中のファイルに、まだ読めるデータが残っていれば偽、すでに最後まで読み終わっていれば真になる。実体はマクロ。

引数
  なし
戻り値
  ファイルを最後まで読み終わっていれば真

マクロ
void fs_format()
ファイルをすべて削除する。ファイルシステムに割り当てられた EEPROM の全領域に 0xff を書き込む(実際は先頭3バイトと末尾3バイトのみに 0xff を書きこめば事足りるが、コードの簡略化のため全領域に書いている)。出荷時の AVR の EEPROM は全領域が 0xff なので、フォーマット済みとして扱える。実体はマクロ。

引数
  なし
戻り値
  なし

関数
void fs_init(void);
ファイルシステム関係の変数を初期化する。コードの先頭で1回実行する。

引数
  なし
戻り値
  なし

関数
unsigned char fs_delete(unsigned char fn); 
指定したファイルを削除する。

引数
  fn: ファイル名
戻り値
  FS_OK: 削除に成功した
  FS_NOTFOUND: 指定されたファイルは存在しない

関数
unsigned char fs_ropen(unsigned char fn);
引数で指定したファイルを読み込みモードでオープンする。読み込みでオープンした場合はクローズはしなくてもよい。ファイルがオープンされている間は次の変数が使える(下記の変数は読み込みのみ。変更してはいけない)。

unsigned int fs_size: オープン中のファイルのサイズ
uint8_t *fs_pt: 現在の読み込み位置(EEPROM のアドレス)

引数
  fn: ファイル名
戻り値
  FS_OK: オープンに成功した
  FS_NOTFOUND: 指定されたファイルは存在しない

関数
unsigned char fs_read_b(void);
読み込みモードでオープン中のファイルから 8bit 値を読み込む。

引数
  なし
戻り値
  8bit値

関数
unsigned int fs_read_w(void);
読み込みモードでオープン中のファイルから 16bit 値を読み込む。EEPROM での 16bit 値のバイトオーダーは下位-上位の順(リトルエンディアン)。

引数
  なし
戻り値
  16bit値

関数
unsigned char fs_wopen(unsigned char fn);
指定したファイルを書き込みモードでオープンする。引数で指定したファイルがすでに存在していた場合は、書き込みモードでオープンした時点で削除される。ファイルがオープンされている間は次の変数が使える(下記の変数は読み込みのみ。変更してはいけない)。

unsigned int fs_size: 書き込んだサイズ
uint8_t *fs_pt: 現在の書き込み位置(EEPROM のアドレス)

引数
  fn: ファイル名
戻り値
  FS_OK: オープンに成功した

関数 
unsigned char fs_write_b(unsigned char val);
書き込みモードでオープン中のファイルに 8bit 値を書き込む。

引数
  val: 8bit値
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
unsigned char fs_write_w(unsigned int val);
書き込みモードでオープン中のファイルに 16bit 値を書き込む。EEPROM での 16bit 値のバイトオーダーは下位-上位の順(リトルエンディアン)。

引数
  val: 16bit値
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
unsigned char fs_wclose(void);
書き込みモードでオープン中のファイルをクローズする。この関数が FS_OK を返さなかった場合はファイルは書き込まれていない。書き込み後にこの関数を呼ばなかった場合も、ファイルには書きこまれない。

引数
  なし
戻り値
  FS_OK: 書き込みに成功した
  FS_NOTFOUND: 書き込みモードでオープンされていない
  FS_DISKFULL: 容量の限界のため書き込めなかった

関数
void fs_str_w(char* str, unsigned int fn);
fn で指定したファイルに、文字列 str を書き込む。 str は '\0' で終端されている必要がある。処理はこの関数だけで完結するので、別途ファイルをオープン、クローズする必要はない。

引数
  str: 書き込む文字列へのポインタ
  fn: ファイル名
戻り値
  なし

関数
char* fs_str_r(char* str, unsigned char fn, unsigned int maxlen); 
fn で指定したファイルから、文字列 str に読み込む。maxlen には str に読み込める最大の長さ(終端のための '\0' を含む)を指定する。読み終わった場合も、maxlen の制限で読み込みが中断した場合も str は '\0' で終端される。処理はこの関数だけで完結するので、別途ファイルをオープン、クローズする必要はない。

引数
  str: 書き込む文字列へのポインタ
  fn: ファイル名
  maxlen: str に読み込める最大の長さ(終端のための '\0' を含む)
戻り値
  str: str のポインタを返す

関数
unsigned int fs_free(void);
空き容量を返す。

引数
  なし
戻り値
  空き容量を返す
 

公開される変数
extern unsigned char fs_filename;
  オープン中のファイル名

公開される変数
extern unsigned int fs_size;
オープン中のファイルのサイズ。特定のファイルのファイルサイズを知りたいときは、試しにfs_ropen(unsigned char fn) してから fs_size の値を読む。


公開される変数
extern uint8_t *fs_pt;
  現在読み書き中のポインタ(EEPROM のアドレス)


2014年6月3日火曜日

AVR がウォッチドッグタイマ発動後に再起動を繰り返す問題

AVR マイコンにはウォッチドッグタイマが搭載されている。 ウォッチドッグタイマとは、メインプログラムの実行とは独立に勝手にカウントアップされていくカウンタで、そのカウンタがオーバーフローすると強制的にマイコンにリセットがかかるというものである。よってウォッチドッグタイマを有効にしたときには、メインプログラム側で、定期的にウォッチドッグタイマの値を0にクリアする命令を実行しないといけない。

これが何の役に立つかというと、例えば異常が発生してメインプログラムが無限ループにはまった時には、ウォッチドッグタイマがオーバーフローして自動的にリセットしてくれる。よって、完全に固まるという最悪の状況だけは避けることができる。

※ もっとも、運悪く無限ループの中にウォッチドッグタイマの値を0にクリアする命令が含まれていた場合は、ウォッチドッグタイマを有効にしていたとしても固まってしまうが。

他の使い方もある。例えば AVR にはリセット命令がないので、ソフトウェアからリセットするには、ウォッチドッグタイマを有効にした上で無限ループを実行するのが正規のやり方だそうである。

今回、赤外線リモコンの制作で、AVR が暴走した時のために、ATTiny85 のウォッチドッグタイマを 有効にした。AVR-libc のマクロ (avr/wdt.h) を使えば簡単に設定できる。実際に無限ループに陥った時には、ウォッチドッグタイマはきちんと動作して、リセットがかかってくれた。ところがその後がよくない。リセットがかかった直後にまたウォッチドッグタイマによるリセットが再発生し、再起動を繰り返してしまう。

これは一部の新しい AVR のみに発生する現象のようで、AVR-libc のマニュアルに回避策が書いてあった。ただ意味がちょっとわかりにくかったので、その部分をチョー訳してみた。

ソースファイルのどこかに下記の緑色の部分のコードを書いておけば、ウォッチドッグタイマによるリセットの後に、自動的にウォッチドッグタイマが無効になる。main() の途中で改めて wdt_enable() を使ってウォッチドッグタイマを有効にすればよい。

AVR-libc のドキュメントWatchdog timer (日本語)より


(原文)
Detailed Description
#include <avr/wdt.h>
This header file declares the interface to some inline macros handling the watchdog timer present in many AVR devices. In order to prevent the watchdog timer configuration from being accidentally altered by a crashing application, a special timed sequence is required in order to change it. The macros within this header file handle the required sequence automatically before changing any value. Interrupts will be disabled during the manipulation.

Note:
Depending on the fuse configuration of the particular device, further restrictions might apply, in particular it might be disallowed to turn off the watchdog timer.

Note that for newer devices (ATmega88 and newer, effectively any AVR that has the option to also generate interrupts), the watchdog timer remains active even after a system reset (except a power-on condition), using the fastest prescaler value (approximately 15 ms). It is therefore required to turn off the watchdog early during program startup, the datasheet recommends a sequence like the following:

    
#include <stdint.h>
#include <avr/wdt.h>
uint8_t mcusr_mirror __attribute__ ((section (".noinit")));
void get_mcusr(void) __attribute__((naked)) __attribute__((section(".init3")));
void get_mcusr(void)
{
  mcusr_mirror = MCUSR;
  MCUSR = 0;
  wdt_disable();
}

(日本語訳)
詳細説明
#include <avr/wdt.h>
このヘッダファイルには、多くのAVRデバイスでサポートされているウォッチドッグタイマを扱うためのインラインマクロへのインタフェースが書かれている。アプリケーションがクラッシュして誤った警報を出さないように、ウォッチドッグタイマの設定を変更する時は特殊なタイミングの手続きが必要である。このヘッダファイルにあるマクロを使って変更すれば、変更前にその手続きが自動的に行われる。 変更中は割り込みは禁止される。

注意:
デバイスのヒューズの設定によっては、とくにウォッチドッグタイマの無効化が禁止されている場合には、さらなる制約がかかることがある。

新しいデバイス(ATmega88 以降。割り込みを生成するオプションがあるデバイス)では、(電源 ON の直後を除く)システムリセットの後でもなお、ウォッチドッグタイマが有効のままになり、最速のプリスケーラ値(約15ms)が設定されることに注意が必要である。そのため、データシートでは、プログラムの起動時の最初の方で、次のようなシーケンスでウォッチドッグを無効にするように要求している。
    
#include <stdint.h>
#include <avr/wdt.h>
uint8_t mcusr_mirror __attribute__ ((section (".noinit")));
void get_mcusr(void) __attribute__((naked)) __attribute__((section(".init3")));
void get_mcusr(void)
{
  mcusr_mirror = MCUSR;
  MCUSR = 0;
  wdt_disable();
}
 



さて、この問題が起こる「新しいデバイス」とはどのデバイスのことなのか。マニュアルの記載からはわかりにくいが、ウォッチドッグタイマのオーバーフロー割り込みを持つ AVR マイコンという事だと思う。

同じく AVR-libc のマニュアルの「interrupt」の所に、各割り込みを持つデバイスはどれかの表がある。表によれば、下記の (1)~(3) に名前があるデバイスには上記の対応が必要のようだ。

(1) WATCHDOG_vect (SIG_WATCHDOG_TIMEOUT)
ATtiny24, ATtiny44, ATtiny84

(2) WDT_OVERFLOW_vect (SIG_WATCHDOG_TIMEOUT, SIG_WDT_OVERFLOW)
ATtiny2313

(3) WDT_vect (SIG_WDT, SIG_WATCHDOG_TIMEOUT)
AT90PWM3, AT90PWM2, AT90PWM1, ATmega1284P, ATmega168P, ATmega328P, ATmega32HVB, ATmega406, ATmega48P, ATmega88P, ATmega168, ATmega48, ATmega88, ATmega640, ATmega1280, ATmega1281, ATmega2560, ATmega2561, ATmega324P, ATmega164P, ATmega644P, ATmega644, ATmega16HVA, ATtiny13, ATtiny43U, ATtiny48, ATtiny45, ATtiny25, ATtiny85, ATtiny261, ATtiny461, ATtiny861, AT90USB162, AT90USB82, AT90USB1287, AT90USB1286, AT90USB647, AT90USB646

たとえば、自分の使っているデバイスでは、ATmega8, ATmega64 はこのリストにないが、ATmega88, ATtiny85 は含まれている。

2014年6月2日月曜日

どんな信号でも学習できる赤外線リモコンを作りたい

(2015/2/28 補足) 本文中に部品選択に関する記述がありますが、いろいろな製品で試した結果、現在推奨している赤外線受信ユニットは GP1UXC41QS、赤外線 LED は L-53F3BT です。

(2016/7/1 補足) Digispark 赤外線受信シールドに使われている赤外線受信ユニット TSOP38238 も適していますが、国内での入手が難しいかもしれません。

 まえがき

AVR マイコンATTiny85 で学習型万能赤外線リモコンを作った。

※ エアコンのリモコンも学習できるようになった
※ 実際に作ってみたい方は Wiki (←リンク) を参照 。
※ この技術を応用して USB から制御できる赤外線リモコン(リンク)を作成した。

プリント基板版

ブレッドボード版


実際に作ってみるまで分からなかったことが結構あって、今後赤外線リモコンを一から自作する人の参考になるかもしれないので、こちらのブログに、感想とか工夫した点とかノウハウっぽいことを書いておく。

赤外線リモコンの制作は電子工作ワールドでは人気テーマで、検索すると制作例が山のように出てくるので、新しく開発するからには何か特徴がないとおもしろくない。そういうわけで、今回の設計では「リモコンの種類(通信フォーマット)によらずなんでも学習できる」赤外線リモコンを作ることを目標にした。

また、ピン数の足りない安い8pinのマイコンで作れるように、学習と送信とその他の操作を全て1ボタンで操作できるようにした。

「なんでも学習できる」リモコンをわずかなメモリしかないマイコンで作るのは大変だ。受信した赤外線データを生のまま不揮発メモリ(内蔵EEPROM)に記憶して、それを送信する仕組みにすれば「なんでも学習できる」は実現できるが、そのためには生データを記憶しておくための不揮発メモリが大量に必要になるためだ。

これを解決するにはデータを圧縮するしかない。しかし、どんなデータでもきっちり圧縮できる (ZIPみたいな) 本物の圧縮アルゴリズムを作るのは難しいし、特にコード用のメモリも RAM も少ないマイコンで実装するのは簡単ではなさそうだ。そこで「赤外線リモコンに特化した、簡単で万能で効率の良い圧縮アルゴリズムを考える」という作戦にした。

工夫の結果、自分の身の回りのリモコンについてはほとんど学習して不揮発メモリに保存できるようになった。ただ圧縮がリアルタイムではできない(赤外線信号をサンプリングする速度に追いつかない)ため、圧縮前の生データを一時的にマイコンのメイン RAM に記録する必要があり、そこがネックになっている。メイン RAM のサイズが 512バイトのマイコンでは、1つの信号が RAM サイズを超えるために学習できなかったエアコンのリモコンがある。可能ならいずれ改良したい。 → この問題は、赤外線の受信中にリアルタイムにランレングス圧縮を行う機能を実装して解決した。

赤外線リモコンのフォーマット


赤外線リモコンのフォーマットについては、ELM さんの「赤外線リモコンの通信フォーマット」がとても詳しい。これによると、日本では主に NECフォーマット、家製協(AEHA)フォーマット、SONYフォーマットが使われているので、この三種類の規格に対応できればよさそうだ。

と思っていたがこれはだいぶ甘くて、実際に身の回りのリモコンの信号をサンプリングして見てみると、微妙に規格に沿っていないものがかなり多かった。

まずは手持ちのリモコンの信号を片っ端からサンプリングして見てみることにする。AVR マイコンでサンプリングした信号をシリアルで出力して、可視化するプログラムを VisualBasic 2008 で作ってみた。やはり可視化は重要で、生の数字を見るよりはるかに分かりやすい(赤い線と緑の線があるが、圧縮する際の参考用に色を変えているだけなのでここでは無視してよい)。

サンプリングした信号を可視化するプログラム(Visual Basic 2008)
Hが点灯、Lが消灯状態。赤外線受信モジュールの出力は負論理でこの図とは逆なので注意。
搬送波は赤外線受信モジュールで除去済み。

テレビのリモコンのフォーマットはばらばら

ビデオプレイヤーなどの AV 機器のリモコンにはたいてい他社のテレビを操作する機能がある。試しに Panasonic のブルーレイレコーダ DMR-BR670V のリモコンで、各社のテレビのリモコンの「電源」ボタンの信号をサンプリングしてみた。メーカー名の後のカッコの数字はこのブルーレイレコーダ独自のメーカーコード。同じメーカーに複数のメーカーコードが割り当てられているのは、同じメーカーでもテレビの機種によって送信するデータが異なる場合があるということ。


上の図を元の大きさで表示する
 ※ 図の1ドット=50[μs]。Hが点灯、Lが消灯状態。赤外線受信モジュールの出力は負論理でこの図とは逆なので注意。
各社のテレビのリモコンの赤外線フォーマット(各社対応リモコンからサンプリング)
サンプリング結果を観察してまとめてみたのが上の表。フォーマットはかなりばらばらだ。特に特殊なのはシャープ、ビクター、三菱のテレビだが、それ以外にも独自フォーマットの機種がある(上の図と対応させるために、全く同じデータの場合も表には重複して載せている)。

独自フォーマットのシャープ(02)とシャープ(11)にはリーダーがない。 また 30~50[ms]の間隔をあけて2つの異なるパターンが連続で送られてくるので、両方記録しないといけない(※図にはないが、シャープのVHSビデオデッキ VC-VS1 もこれと同じフォーマットだった)。シャープ(21)は素直な家製協フォーマット。シャープは昔は独自フォーマットだったが、最近の製品は家製協フォーマットに準拠したということだろうか。

ビクター(14)はリーダに続いてまず16bitのデータを送信する。その後同じデータが反復されて送られてくるが、反復の方にはリーダがなく、いきなり16bitデータから始まっている。手元にあったビクター製VHS/DVDデッキ用リモコン(RM-SHR013J)も同じフォーマットなので、これがビクター独自のフォーマットなのだろう。

三菱(08)と三菱(12)はリーダはなく、データも6bitのみ。必要最小限といった感じ。

受信側の機器の挙動にも注意が必要。Panasonic のアナログテレビ TH-15FA5 は、電源を OFF にするときは、素直な家製協フォーマットの信号に反応する。その直後は同じ信号で電源 ON にできる。しかし、電源を OFF にしたあと数分たつと、テレビが内部的にパワーセーブモード(?)になるらしい。パワーセーブモードからは、電源 ON のパターン1つを受信しただけでは ON にならず、最低1回は同じデータを反復受信しないと電源が入らない。さらにその場合は、最初のデータと反復データの間が40ms以上開いていないと電源が入らなかった。誤動作しないように、電源 ON のボタンだけは厳密に判定しているようだ(※シャープの AQUOS テレビ LC-20DZ3(家製協フォーマット) も同じような挙動)。

テレビ以外のリモコンのフォーマット

テレビ以外の機器のリモコンもサンプリングしてみた。

上の図を元の大きさで表示する
※ 図の1ドット=50[μs]。Hが点灯、Lが消灯状態。赤外線受信モジュールの出力は負論理でこの図とは逆なので注意。
テレビ以外のリモコンの赤外線フォーマット


三洋のプロジェクタ LP-XP100L は天吊りの大型のもの。ビクターの DVD プレイヤー RM-SHR013Jはビクターの独自フォーマットだった。シャープのエアコン(AY-Z22VX)は家製協フォーマットのようだがやたらに長い。蛍光灯のリモコンの信号もやたら長い。

TV会議システム VSX7000 は、米国 POLYCOM 社の製品。
これはちょっとクセのある挙動をする。1つのボタンに2つのパターンが割り当てられていることがあって(数字ボタンなど)、そのボタンを押すたびに2つのパターンが交互に出力される。また、リモコン本体を床から持ち上げるとスイッチが入って、自動でいくつかの信号を送信する。

WOLFVISION の卓上カメラはこういう製品。リモコンのフォーマットはソニーフォーマットに似ていた。(後に自作リモコンが完成した後に試したところ、電源 OFF は確実にできたが、電源 ON に失敗することが多い。原因は不明) → (2015/2/28 補足: 若干長めに学習してしまうバグがあったため。修正したら ON/OFF ともできるようになった。ただし赤外線の到達距離が短く、近距離からのみ)。





赤外線信号の保存形式(EEPROM 保存時の圧縮)


以上の観察結果をふまえて、赤外線信号の保存形式を設計することにする。

万能のリモコンにするためには、どんなフォーマットだったとしても正しく保存・再生ができる必要がある。また、容量の節約のためにできる限り短いデータ形式で保存したい。そこで、受信した赤外線信号を次の形式で EEPROM に保存する。

まず、基本方針として、一連の赤外線パルスの発光時間を、そのまま生で保存することにする。ただし、信号の途中にビット列とみなして短縮(圧縮)形式で保存できる部分が見つかった場合は、その部分は可能な限り短縮形式で保存する。

まずは圧縮せずに、発光時間を生のまま保存する方法について。信号がうまく圧縮できない場合はこの方式で記録することになる (ただし後述の方法で、部分的にでも圧縮できるところは圧縮する)。

どんな赤外線信号でも、最初は赤外線 ON から始まって一定時間継続し、その後にOFF が一定時間継続する。これの繰り返しである。そこで、ONの継続時間と OFF の継続時間を測定して、15bit の値としてそのまま保存する(時間は2バイト値で保存するが、後述する短縮形式との区別のため、最上位ビット(MSB)は常に0にしておく。そのため実際のデータが保存できるのは 15bit)。この形式では必ず ON と OFF のペアで記録することにし、一つの ON+OFF のペアで 4バイトのメモリを消費する。時間の計測は 72kHz の割り込みで行っているので、1[秒]÷72[kHz] = 13[μs] が1単位になる。15bit 値では 32767 まで表現できるので、ON も OFF も最大 32767 * 13[μs] = 425971[μs] = 425.971[ms] までの時間が記録できる。



赤外線信号の記録形式 (EEPROM上のフォーマット)

次に圧縮の方法を考える。圧縮のための短縮形式で記録する方法について。

赤外線信号にはさまざまなフォーマットがあるが、大抵どれも下の図に示した 「短いON」「長いOFF」「長いON」「短いOFF」 の4種類の時間の組み合わせで表現できる(リーダーや区切りなど特殊な部分以外は)。そこで、取り込んだ信号全体を一度プレスキャンして、4種類の時間がそれぞれ何μs になっているかを、パルス長の平均をとるなどしてがんばって求める。そしてこの4種類の長さを EEPROM の別の領域に保存しておく。
赤外線信号を構成する4種類の長さ

実はここに工夫できる余地がある。一種類のリモコンには、普通はこの4種類の時間のうち3種類しか存在しない。なぜかというと、一般的なフォーマット(NEC、家製協、ソニー、ビクターなど) はどれも、ON か OFF かどちらかの長さは固定で、もう片方の長さで1か0かを表現しているから。そのため、普通は、プレスキャンした結果を見ると「(A) 短いONと長いONは同じ長さだった」 または「(B) 長いOFFと短いOFFは同じ長さだった」のどちらかの結果になる。つまり4つの値のうち、(A)または(B)は同じ値になる。

同じ値があっても気にせずに、取り込んだ一連の信号と上の4つの値を順次比較し、取り込んだ信号の中に「長いON+長いOFF」 が見つかったら 1 「短いON+短いOFF」が見つかったら 0、としてエンコードする。この2種類以外のパターンは考慮しなくてよい(比較する時には測定誤差を考慮して若干のマージンを持たせる)。

以上の方法で判定すると、結局、下の表の上の段の4つのパターンは1として、下の段の2つのパターンが0として判定される。 これだけで、一般的なフォーマット(NEC、家製協、ソニー、ビクターなど) なら、うまい具合に 0 と 1 のビット列に変換でき、後から同じ波形を再生できる。再生する時は、同じ4つの値を使って、ビットが1なら「長いON+長いOFF」 を、0なら「短いON+短いOFF」を出力すればよい。

※ 分かりにくいので補足: 要するに長いONまたは長いOFFのどちらかを含むパターンは1、どちらも含まないパターンは0とエンコードするということ。一般的なフォーマット(NEC、家製協、ソニー、ビクターなど)ならこの方法で再現できる。


※ ただし1と0のパルス長が同じ長さのフォーマット(変調方式が PPM でない)もあり、その場合はこの方式でビット列化できない 。YUASA の扇風機のリモコンはそうだった。その場合は圧縮できないので生で記録するしかない。

 0 と 1 の列に変換したら、ヘッダを付けて 0/1 のビット列を記録する。ヘッダは目印として MSB(最上位ビット) を 1 にする。ヘッダの MSB 以外の 7bit には、その後に続くデータのビット数を格納する。続くデータの長さが中途半端な場合は 8bit 単位でパディングする。

もしこの方法でエンコードしている最中に、0とも1とも判定されない長さのパルスが出てきたら(信号の途中にリーダーや区切りが入っているとそうなる)、一旦パディングしてビット列の短縮記録を止め、その特殊な長さのパルスだけは生のまま4バイトを使って記録す る。その後にまたヘッダを付けて、ビット列の記録を再開する。

受信した信号を 0/1 の列に変換する (EEPROM上のフォーマット)

一旦ビット列に直せれば、その後はいろいろな圧縮アルゴリズムが適用できるが、今回はこれ以上圧縮せずにそのまま記録している。

最悪のケースでは、全くビットに直せない信号の場合には全て生のまま記録することになるが、ともかくこの方法なら、どんな信号がやってきても記録することができる。

追加: リアルタイムランレングス圧縮 (エアコンのリモコン対策)


上に 「赤外線信号の保存形式(EEPROM 保存時の圧縮)」 として、EEPROM に保存する時に容量を節約する方法について書いた。これによって、保存先の容量は少なくてもよくなった。大抵のリモコンの信号が96バイト以内に収まるようになるので EEPROM が512バイトのマイコンでもある程度の信号が保存可能である。

保存するときはそれでよいが、問題はリアルタイムに学習している最中のメモリ不足。学習中は受信した赤外線を一旦 RAM に記録し、これをあとで上記の方法で圧縮することになる。ところが TINY84 は EEPROM だけでなく、RAM も 512 バイトしかない。しかも、信号の保存専用に使える EEPROM と違って、RAM は変数やスタック (割り込みを使うと、割り込みの前後で多数のレジスタをスタックに PUSH/POP しないといけないため、結構なスタックが必要になる) のために消費されるので、赤外線信号の記録に使える RAM は、実質的には 330バイトくらいしかない。TV などの通常のリモコンなら収まるが、エアコンなどの長い信号は 330 バイトに収まりきらなくなる。そうなると学習できないという事になる。

そこで、学習時には RAM に記録する前にリアルタイムにランレングス圧縮を行い、RAM から読みだす時はリアルタイムに展開するようにした。この方法で、従来の方法では RAM 不足で学習できなかったダイキン、日立製エアコンの学習が可能になった

今回とった方法は以下の通り。まず RAM 上で数値を記録する時の表記方法の規定。下記の3種類のいずれかの形式で表現することにする。
RAM 上のフォーマット
0~63 までの数値は形式1、64~16383 までの数値は形式2で保存する (16384 以上の数値は表現できない)。形式2では、上位-下位の順に記録する。形式3はランレングス圧縮された値を示し、保存してある「直前の値」 を 「繰り返し長」の回数繰り返す。

「直前の値」について、今回の工夫として、RAM の奇数バイト目と偶数バイト目を分けて扱うことで圧縮効率を上げている。RAM に記録すべき生のリモコンの信号は 、ON の時間長と OFF の時間長を交互に並べたデータである。よって、常に偶数バイト目は ON の時間長、奇数バイト目は OFF の時間長を表している。ON 同士、OFF 同士は同じ値になることが多い。そこで、今回は「直前の値」として、着目しているのが奇数バイト目なら直前の奇数バイト目の値、偶数バイト目なら直前の偶数バイト目の値を使うことにした。

上記の表の表現形式でランレングス圧縮を行えば、データは短くなることはあっても長くなることはない。(奇偶ごとに1個おきに)同じ値が頻出する赤外線信号では、これだけで必要な RAM の量はかなり減る。

問題はマイコンでのランレングス圧縮の処理時間だった。赤外線信号の送受信はタイミングが重要なので、72kHz のタイマー割り込みのイベントハンドラ内で行っている。そのため、期間内(13.9μs)に送受信に関する全ての処理を終わらせないといけない。クロック数で言うと 222 クロックしかないわけで、今回そこにランレングス圧縮の処理が加わることになる。これは難しいかと思ったが、AVR-GCC の最適化オプションを速度優先 (-O3) にすることで何とか間に合った。

学習結果の EEPROM への保存

学習結果は AVR 内蔵の EEPROM (容量 512バイト) に保存する。その際、独自に開発した AVR-GCC 用内蔵 EEPROM 簡易ファイルシステム を使っている。ファイルシステムについては別途記事を書いたので、そちら (←リンク)を参照。



部品の選択

重要な部品は AVR マイコン、赤外線LED、赤外線受光ユニット、ピエゾブザーである。


AVR マイコン

ATTINY85-20PU-ND

回路の心臓部のマイコンには ATTiny85 を採用した。DIP 8pin(表面実装でないタイプ)の製品。価格は Digikeyで1個買うと199円。25個まとめ買いすると1個当たり161.16円。Aliexpress では 20個まとめ買いで1個当たり US$1.25 くらいの店があるようだ(Aliexpress は中国の通販サイト。送料無料の店もあるが、その場合注文してから届くまで遅いときには1か月くらいかかる)。マルツだと215円、秋月では扱っていない(2014年6月)。Futurlec は $5 の送料がかかるが(またこちらも届くまでちょっと時間がかかるが) 1個 $1.15 と安い。たくさん買うなら Futurlecで。

スペックは、フラッシュメモリ8KB, RAM512B, EEPROM512B。電源電圧は4.5~5.5V。以前 LAN から操作できる赤外線リモコンをATMega64(フラッシュメモリ64kB, RAM 4kB, EEPROM 2kB) で作成したことがあるが、今回の ATTiny85はそれと比べるとかなり非力。

ATTiny85 は CPU クロックに、内蔵発振回路(外付け部品なし)の 16MHzが使える(High Frequency PLL Clock)。これはとても重要。クロックを遅くすれば消費電力が減るので省電力的には有利なのだが、今回は RAM の少なさをカバーするため、学習中にランレングス圧縮を行うなど結構なことをしていて、72kHz の割り込み時間内にそれらの処理を完了させる必要がある。そのためクロックが 16MHz でないと間に合わないところがあった。

ATTiny85 にはシリアルインタフェースがない。学習したデータを出力して PCで確認する必要があったので、そのために後日ソフトウェア UART (送信部分のみ)を自作した。(追記: 後日受信部分も作成して、PC から本機をコントロールできるようにした)。

ケースの都合上、回路をボタン電池 LR44*3個の 4.5V で駆動する必要があり、 省電力が重要になった。一定時間操作がないときは AVR をパワーダウン状態にしている。パワーダウン中の消費電流は 2μA 以下。消費電力を削減するために、ADC やコンパレータをソフトウェア的に OFF にしている。また、内蔵プルアップ抵抗を使うとパワーダウン中の消費電力が激増するので、内蔵プルアップは使わず、外付けのプルアップ抵抗(10kΩ)にした。

問題はパワーダウン状態にするときに BOD(Brown-out Detector) を無効にしないと、パワーダウン中の消費電流が 20μA 以上に跳ね上がること。ATTiny85 のデータシートには「ソフトウェアで BOD  を無効にするためにはリビジョンが C 以降(ATtiny85, revision C, and newer) でないといけない」と書いてあるが、リビジョンの調べ方がわからない。リビジョンについて、まず米国 Digikey に問い合わせてみたが、明確な回答は得られず。次にアトメルジャパンに問い合わせたところ、「リビジョン C 以降にあたる製品は車載用の型番のみで、通常品では BOD をソフトウェアで ON/OFF することはできない」とのこと。やむを得ず、ヒューズビットの設定で BOD を常時無効にした。BOD が常時無効だと、ショックで EEPROM が消えることがあるがしかたがない。

ソフトウェアの開発には GCC (avr-gcc-4.5.1_1) を使った。 開発は FreeBSD9.2 で行った。avr-libc-1.8.0,1 を利用し、ソフトウェアは本当に一から全部作った。AVR への書き込みには FreeBSD でavrdude-5.11 を使い、FreeBSD マシンのパラレルポートと ATTiny85 を直結して書き込んだ。これらの必要なソフト (avr-gcc-4.5.1_1 avr-libc-1.8.0,1 avr-binutils-2.20.1_1 avrdude-5.11) は FreeBSD 9.2 の ports に全部あった。

#追記(2014/6/8)
マイコンに ATTiny85 を選択したのはなかなか絶妙だったかも。DIP 品があり、外付け部品なしでクロック16MHz が実現できる AVR 製品がほかにない。ATTiny85の難点は RAM が 512Byte な点。本当は RAM が 1kByte の品(ATMega8など)を使いたいのだが、ATMega8 を 16MHz で動かすには外付け部品が必要。ATMega8U2 なら、外付け部品なしでも 16MHz で動かせそうだが、DIP 品がない(おまけに高い)。

赤外線LED


OSIR5113A
(現在は L-53F3BT を推奨します)

ボタン電池 LR44*3個の 4.5V 電源で数mの距離から使えるリモコンを作るには、実質この赤外線 LED を選ぶしかなかった(2014/6/25: 電源をリチウムコイン電池 2個直列の 6V にする場合は、東芝の赤外線 LED,  TLN105B の方が照射角度が広くてよい。到達距離は結構短くなるが)。

2014/8/20 追記: TLN105B は入手しにくい(ディスコン?)。広角に照射したい場合は共立エレショップで取り扱いのある L-53F3BT を使う。

赤外線 LEDの使い方は通常の LED と同じだが、光っても肉眼で見えないので選ぶのが難しい。最終的には実際にリモコンとして使ってみて動作を見るしかなかったりする。主なパラメータは順方向電圧(VF)、最大電流、放射強度、半値全角など。もちろん小さい電流で放射強度が大きく、半値全角が広い方がよいが、トレードオフになっていて、どれかを犠牲にして選ばなくてはいけない。 TLN105B, TLN103, OSIR5113A などを試して、最終的にOSIR5113A を使うことにした(高輝度版のOSI5FU5113Aというのもあったが試していない→2014/6/21 高輝度版のOSI5FU5113A で試してみたが、大きな違いは見られなかった)。 OSIR5113A は VF=1.25V, 最大電流が 100mA, 半値全角が15度。指向性が強いので受信部を狙わないといけない。秋月で100個まとめ買いすると1個7円。安い。

電源がひ弱なアルカリボタン電池(4.5V)なので、赤外線 LED に直列に入れる電流制限抵抗の選び方は難しい。抵抗値が大きいと赤外線 LED の発光が弱すぎて本当に近くからしか使えなくなる。かといって抵抗値が小さいとボタン電池が電源の場合は、発光時に電源電圧が瞬間的に下がって誤動作するし、スイッチング電源などを電源にすると AVR に過電流が流れてリセットがかかる。ぎりぎりの選択で 51Ωにした。

2014/8/20 追記: 電源が 6V の場合は 100Ω にする。

赤外線受信モジュール


RPM7138-R
(現在は GP1UXC41QS を推奨します)

電源、GND、信号出力(VOUT)の三本足。電源を入れると VOUT が H になり、赤外線を受信している間だけ L になる。これもいろいろな製品が出ている。これまではずっと秋月で 1個50円くらいの製品(OSRB38C9AA等) を買って使っていたが、動作がデリケートで、電気的ノイズ、周囲の明るさや蛍光灯の影響で、赤外線信号が入っていないのに誤検出することがよくあって困っていた。

一度近所のマルツ仙台店で RPM7138-R を買ってみたら、使い勝手がすこぶるよかったので、以後少々高いけれどもずっとそれを使っている。まずこの製品は電源電圧がかなり下がってしまってもなんとか動作してくれる。また、オシロスコープを使って確認してみると、赤外線信号をかなり忠実にサンプリングできている。日本のロームという会社の製品で、ロームの赤外線モジュールのカタログを読んでみると、正確に受信するためにかなりいろいろな工夫をしていることがわかった。プラスチックパッケージで、電源電圧は 4.5-5.5V。消費電流は 1.5mA。マルツだと1個158円。Digikeyだと1個200円。10個まとめ買いすると1個当たり157.7円。

消費電流の 1.5mA は結構大きいので、赤外線受信モジュールの電源 を AVR の出力ピンからとって、不要なときは電源を切るようにしている。このような使い方をする時の注意点として、赤外線受信モジュールの電源を ON にした直後にすぐに受信を開始してはいけない。誤動作する。動作が安定するまで 100ms くらい待ってから受信をスタートするようにした。

低電圧(2.7V)で動作する版(RPM7238-R)も存在し、本当はそちらを使いたいのだがまだ手に入らない。低電圧版が手に入れば、システム全体を 3V のリチウムコイン電池で駆動できそうなのだが。

2014/7/7 追記:  低電圧で動作するロームの赤外線受信モジュール RPM7238-H8R(電源電圧: 推奨2.7~3.6V, 最大6.3V) を入手して試したところ、システム全体を 3V のリチウムコイン電池で駆動できた。赤外線信号の学習も特に問題なくできた。ただし低電圧版モジュールにはプラスチックパッケージ版がなく、全て金属シールド付きなので、回路はそのままでよいが、部品の配置を変更する必要がある。

2014/7/7 追記: RPM7238-H8R の金属シールドを外せば形状は 5V品の RPM7138-R と同じ。シールドを外してみても一応動作はしたが、ノイズ耐性などは不明。

2014/8/20 追記: 秋月の GP1UXC41QS が 2.7V~5.5V で動作し、ノイズにも強くてよいことが分かった。金属シールドはなく、外装はプラスチック。ピン配列はロームの赤外線モジュールと同じ。


ピエゾブザー


PKM13EPYH4000-A0

学習開始やエラーなどをビープ音で知らせるために、ムラタのピエゾブザー PKM13EPYH4000-A0 を使っている。秋月で1個30円。Digikey で1個68円。なぜかこの製品だけ安い。サイズが小さくてよいが音が少し小さい(今回は操作確認音に使うので音は小さくてもよしとする)。ブザー単品(円盤状の金属)だけでも売られていることがあるが、その状態だと本当にかすかな蚊の鳴くような音しか出ない。黒い樹脂ケース部分の設計が重要のようだ。ピエゾブザーは電圧をかけただけでは鳴らず、AVR で鳴らしたい音程のパルスを生成して送る必要がある。今回はタイマ割り込みで 440Hz と 880Hz の矩形波を作って鳴らしている。