フタなしカンヅメ

徒然なるままに @happytar0

アセンブリでLEDを点灯させる【後編】

後編ということで、前回の続きをだらだらと書きたいと思う。
2つのLEDを交互に点灯させたい、ということなので一定間隔で点灯させるために、ディレイ処理を行う必要がある。
C言語であれば_delay_ms関数が使えるのでかなり簡単に実装できるが、アセンブリとなるとクロック周期をカウントしていく必要があるようだ。

指定秒数待機するルーチンを実装する

いろいろと参考サイトを見ていくと、下記のような実装が一番シンプルなようだ。
AVRŽŽ—p‹L-assembly

delay1s:
    ldi  r16, 100
    mov  r2,  r16
dly2:
    ldi  r16, 100
    mov  r1,  r16
dly1:
    ldi  r16, 200
    mov  r0,  r16
dly0:
    nop
    dec  r0
    brne dly0
    dec  r1
    brne dly1
    dec  r2
    brne dly2
    ret

最初にこれを見せられたらちんぷんかんぷんである。
まず上から順に実行されていくので、r2に100、r1に100、r0に200とレジスタにそれぞれの値が入っていく模様。

NOP命令「無操作」と書いてある。何もせずに1クロック消費するということだ。

DEC命令「汎用レジスタを減少」と書いてある。要はデクリメント(-1減算)ということみたいだ。

BRNE命令「不一致で分岐」と書いてある。0の時に実行されるラベルを指定すると、そこにジャンプするようだ。

RET命令「サブルーチンからの復帰」と書いてある。サブルーチンとして呼び出された場合に、呼び出し元にジャンプするようだ。

これでだいたい分かると思うが、r2で100ループ、r1で100ループ、r0で200ループと、子ルーチンが呼び出されていく。
nop、dec r0、で2クロック、brne dly0の呼び出しは、条件成立時は2、不成立時は1と変化するようだ。ほとんど成立して動作するので、2クロックとカウントしていいと思う。

4 * 100 * 100 * 200 = 8000000 = 8Mhz となる。

解説しているサイトによると、内側のループ処理に5クロックかかるので、

5 * 100 * 100 = 50000 = 50Khz となるようだ。1%未満の誤差ようなのでおよそ1秒となる。

また、r0〜r15までは、使える命令が限定されている。r16~r31については何でも使えるようで使い分けが重要みたいだ。
例えば、LDI命令はr16以上でしか使えないので、一度r16を経由させたあとにMOV命令でr0に値をコピーしている。

待機処理を入れてLEDを交互に点灯させる

先ほどの待機ルーチンを使ってみて以下のように書いてみた。

.include "tn2313def.inc"

main:
    ldi   r16, 0b00011000
    out   DDRD, r16
    ldi   r16, 0b00010000
    out   PORTD, r16
    rcall delay1s
    ldi   r16, 0b00001000
    out   PORTD, r16
    rcall delay1s
    rjmp  main

delay1s:
    ldi  r16, 100
    mov  r2,  r16
dly2:
    ldi  r16, 100
    mov  r1,  r16
dly1:
    ldi  r16, 200
    mov  r0,  r16
dly0:
    nop
    dec  r0
    brne dly0
    dec  r1
    brne dly1
    dec  r2
    brne dly2
    ret

RCALL命令「PC相対サブルーチン呼び出し」と書いてある。そのままの意味でサブルーチンの呼び出しである。
さっそくマイコンに転送してみたところ、非常にゆっくり点灯しているようだ…。
なぜ…という感じだが、クロック周波数がおかしいのかもしれない。

マイコンに設定されいてるクロック周波数を確認してみることにした。

マイコンのクロック周波数を確認する

「CKDIV8」というワードが重要なようだ。
調べてみると、CPUクロックの分周比を設定します、と書いてある。

マイコンに設定されているヒューズ情報を確認する必要があるので、以下のコマンドを実行してみた。

# 対話モードに入る
$ avrdude -c avrispmkii -P usb -p attiny2313 -t
$ read lfuse
0000   64
$ read hfuse
0000   df

これだけでは意味がさっぱりである。
ヒューズビットの意味を調べる必要があるので、以下のデータシートを確認してみた。
http://www.avr.jp/user/DS/PDF/tiny2313.pdf

lfuseは、ヒューズの下位ビット。hfuseは、上位ビットとなる。
「CKDIV8」が設定されているのは、下位ビットのほうになる。

ヒューズビットについては以下のサイトが参考になるようだ。
◆ヒューズビット

要は、7ビット目が「0」になっていると、クロックが1/8になってしまうらしい…なんてこったい。
初期設定では全てこのようになっているとのことだ。
設定されている値をビット値に変えてみると…

64 = 0110 0100

7ビット目が0である…そういうことか。こいつを1に書き換えてやる必要がある。

1110 0100 = e4

16進数でe4という値に書き換えることで、1/8動作を変更できる。
以下のコマンドを実行して書き換えてみたところ無事に正常な動作となった。
なお、ヒューズの書き換えは失敗すると、動作しなくなってしまう場合もあるようなので注意して欲しい。

$ avrdude -c avrispmkII -P usb -p t2313 -U lfuse:w:0xe4:m

あと、こんなサイトも見つけた。
マイコンの種類を選択するとWeb上で適切な値を表示してくれるようだ。便利そう。
Engbedded AVR Fuse Calculator

今回利用したコードもGitHubにアップしてみた。
pontago/avr-LedTest-asm · GitHub

アセンブリでLEDを点灯させる【前編】

前回の記事で無事にLEDを点灯させることができた。
コードは全てC言語で書いたが、もう一つの方法としてアセンブリで書いてみることにした。
アセンブリなんてコードを見ただけでウンザリしてしまう。

でもやってみる前から諦めてもしょうがない。
この機会に少し調べてみることにした。

開発環境を整える

アセンブリコードをコンパイルするためのコマンドは、最初に導入したCrossPackAVRというものに含まれているavr-asコマンドを使うことができる。
しかし、このコマンドでコンパイルできるアセンブリはGasと言われるもので、
いわゆるよく知られている、アセンブリコードのそれとは違うらしい。

そこで今回は、avraというアセンブラを導入してみることにした。
AVRA Home Page

下記サイトを参考にしながらインストール。
のぅわんべたぁ|AVRアセンブリ

まず、最新版をダウンロード後に展開し、srcディレクトリの中で以下のコマンドを実行してみた。
参考通りに上手くいかず、automakeの部分でコケてしまったので、touchコマンドでエラーが出るファイルを作ってみたところ成功。

$ touch NEWS README AUTHORS ChangeLog
$ aclocal
$ autoconf
$ automake -a
$ ./configure --prefix=/opt/local
$ sudo make install

これで開発するための環境は用意できた。
また、includesディレクトリに.inc拡張子のついたファイル群が入っているようで、マイコンの種類に応じた定義ファイルが格納されているようだ。
レジスタやポートに応じたアドレス、値がマッピングされているようで、初めにインクルードして使う感じだと思う。

LEDを点滅させる

なんと言っても最初はLEDを点滅させるところからだろう。
マイコンにはLEDを2つ接続しているが、手始めに一つ点灯させてみる。

.include "tn2313def.inc"

main:
    ldi   r16, 0b00011000
    out   DDRD, r16
    ldi   r16, 0b00001000
    out   PORTD, r16
    rjmp  main

なんとこれだけである…実に簡単だ。

まず、.includeで定義ファイルをインクルード。コロンが付く行はラベルなので解説は不要だと思う。
ちなみにドットから始まるものは擬似命令と呼ばれるもので機械語に変換されることはないらしい。
続いて、各命令について調べてみた。

使える命令セットは以下のURLからダウンロード出来る模様。
http://www.avr.jp/user/DS/PDF/AVRinst.pdf

LDI命令は「即値バイト定数を汎用レジスタに取得」と書いてある。
レジスタr16に右オペランドの値を入れると考えればいいと思う。

OUT命令は「汎用レジスタからI/Oレジスタに設定」と書いてあった。
そのままで、I/Oレジスタに汎用レジスタの値を設定するときに使う命令のようだ。

RJMP命令は「PC相対無条件分岐」と書いてある。
要は無条件でこの場所に飛ばすってことみたいだ。今回の場合はmainラベルに飛ばすので、ループさせるということになる。
いわゆるGOTOみたいな感じだろう。

DDRDや「0b00011000」などの値は、前回の記事で説明していたと思うので今回は省く。
上のコードをavraコマンドでコンパイルしてみた。

$ avra main.asm
AVRA: advanced AVR macro assembler Version 1.3.0 Build 1 (8 May 2010)
Copyright (C) 1998-2010. Check out README file for more info

   AVRA is an open source assembler for Atmel AVR microcontroller family
   It can be used as a replacement of 'AVRASM32.EXE' the original assembler
   shipped with AVR Studio. We do not guarantee full compatibility for avra.

   AVRA comes with NO WARRANTY, to the extent permitted by law.
   You may redistribute copies of avra under the terms
   of the GNU General Public License.
   For more information about these matters, see the files named COPYING.

Pass 1...
Pass 2...
done

Used memory blocks:
   Code      :  Start = 0x0000, End = 0x0004, Length = 0x0005

Assembly complete with no errors.
Segment usage:
   Code      :         5 words (10 bytes)
   Data      :         0 bytes
   EEPROM    :         0 bytes

こんなメッセージが表示されて成功した。
最初にエラーが出たのだけど、tn2313def.incファイル内にある「#」から始まる行の先頭に「;」を付けたところエラーは消えた。

# vimでこんな感じに処理した
:%s/^#/;#/g

出来上がったmain.hexというファイルを開いてみたところ、かなり小さくてびっくりした。わずか63バイトである…。

:020000020000FC
:0A00000008E101BB08E002BBFBCFE2
:00000001FF

バイナリの転送はavrdudeを使って以下のようなコマンドを実行した。

$ avrdude -c avrispmkII -P usb -p t2313 -U flash:w:main.hex:i

簡単なものだけど一発で成功したので抵抗感も薄らいだ。

アセンブリと聞くだけで拒否反応を示してしまいたくなるが、命令セットを覚えていくことで少しずつ理解が深まっていき、単純なコードながら深さと楽しさがあるように思える。

長くなってしまいそうなので、次の記事で2つのLEDを一定間隔で切り替えて点灯させる、ということをやってみたいと思う。

今回書いたコードもGitHubに上げてみた。
pontago/avr-LedTest-asm · GitHub


参考サイト
アセンブラなんて簡単じゃないか(1/3) − @IT MONOist
解説 AVRアセンブラ講座 (1)|freeml byGMO

AVRで温度計を作るために考える

作りたいと思ってから1ヶ月ほどが経過した。
時間を空けすぎるのはよくないので、作るのに何が必要か、どうやって作るのかを調べることにした。

温度を測るには温度計センサー(LM60BIZ)なるものがあるようで、
こいつを使えば温度によって電圧を返してくれるらしい。
比較的難易度は低そうだ。

実は湿度計も同時に作ろうと思ったのだけど、
安く入手できるHS-15Pを使うとなると、
湿度によって変動するインピーダンスを調べて対数演算が必要らしい。
あほの俺には無理だ。

TDKが出しているCHK-GSSというのを使うと、
電圧の変化で処理できる上に、温度も分かるらしい。俺でも出来そう。
しかし2000円くらいするので尻込みしてしまった。
今回は温度計のみで絞ることにした。

ACアダプタを使う

今まで乾電池から電源を取っていたけど、ACアダプタを使うことにした。
スイッチングACアダプター12V1A(NP12-1S120)を使う予定だ。
12Vの電圧を5Vまで降圧させる必要があるので、3端子レギュレータ(LM7805CV)を使う。

ノイズや発振を防ぐために、前後にコンデンサをかませる必要があるようだ。
0.33μF50Vと0.1μF50Vを使うことにした。

ブレッドボードでも簡単に使えるようになる、
ブレッドボード用DCジャックDIP化キットというものを使う。

7セグLEDを使う

計測した温度を表示するために7セグLEDを利用する。
3桁表示できるC-533Sを使うことにした。

各セグメントに対してマイコンの各ポート接続してしまうと、
ポート数が足りなくなってしまうので、トランジスタで電流を増幅させダイナミック点灯させる。

トランジスタは、2SC1815GRで60V150mAまで増幅出来るようだ。

各セグメント(8本)に対して、10mA程度流せば十分なようなので、
10mA * 8本 = 80mAが必要な計算だ。余裕をもって100mAとした。

トランジスタとマイコンの間には必要以上に電流が流れないように、電流制限抵抗というものが必要らしい。

このトランジスタの電流増幅率(hFE)は100で、
トランジスタを通すことによって0.7Vほど電圧降下がすることを考慮して、以下ように計算した。
(5V - 0.7V) * 100hFE / 100mA = 4.3kΩ
4.3kΩに近い4.7kΩの抵抗を使うこととした。

トランジスタのコレクタとベース間には安定化させるために抵抗が必要らしいので、
10kΩの抵抗を使うことにした。
あとはエミッタと7セグLEDのコモンに接続すればいいはずだ。

続いて、各セグメントとマイコンのポートに接続する。
この時に抵抗も挟む必要があるので、7セグLEDの順方向電圧の1.8Vを差し引いて、抵抗値を以下のように計算した。
(5V - 0.7V - 1.8V) / 0.01A = 250Ω
近い240Ωの抵抗を使うこととした。

必要なもの

これでだいたいの流れを調べたので、必要なものをピックアップする。
マイコンはATTinyしか持っていなかったので、ATMegaも買うことにした。
あとこれから必要になるであろうテスターと、いくつか抵抗も追加で買う。

部品名 型番 値段
AVRマイコン ATMEGA328P-PU 250円
ポケット・デジタルマルチメータ(テスタ)[周波数+容量][オートレンジ] P-10 1000円
7セグメントLED表示器 超高輝度赤色3文字(3桁)(カソードコモン) C-533SR 200円
高精度IC温度センサ LM60BIZ 100円
ブレッドボード用DCジャックDIP化キット - 100円
トランジスタ 2SC1815GR 200円
3端子レギュレータ[5V1A] LM7805CV 100円
絶縁型ラジアルリードタイプ積層セラミックコンデンサー0.33μF50V - 100円
絶縁型ラジアルリードタイプ積層セラミックコンデンサー0.1μF50V - 100円
カーボン抵抗(炭素皮膜抵抗)1/4W 10kΩ - 100円
カーボン抵抗(炭素皮膜抵抗)1/4W 4.7kΩ - 100円
カーボン抵抗(炭素皮膜抵抗)1/4W 240Ω - 100円
カーボン抵抗(炭素皮膜抵抗)1/4W 1kΩ - 100円


マイコン先生に確認してみたところ、これで大丈夫らしい。
チェックが入らなかったのは嬉しい。
注文したらいよいよ組み立てることになるからワクワクだ。

AVRでLEDを点滅させるプログラム

前回の記事で無事にマイコンを経由してLEDを点灯させる事ができた。
その時に利用したプログラムはサンプルをコピペしただけだったので、2つ接続したLEDを交互にランダム点灯させるプログラムに変更したいと思う。

最初にサンプルコードで意味が分からない変数や関数があったので、それを調べることにしてみた。サンプルコードは以下のような感じだ。

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD = 1 << 4;           /* make the LED pin an output */
    for(;;){
        char i;
        for(i = 0; i < 10; i++){
            _delay_ms(30);  /* max is 262.14 ms / F_CPU in MHz */
        }
        PORTD ^= 1 << 4;    /* toggle the LED */
    }
    return 0;               /* never reached */
}

定義済みグローバル変数

DDRDとPORTDにビット演算の結果を代入しているようだけど、まずここが謎だった。

DDRD = 1 << 4;
PORTD ^= 1 << 4;

DDRD変数の中身が気になったので、宣言元である「avr/io.h」を覗いてみたところ、マイコンの種類判定後に「iotn2313.h」をインクルードしているようだ。

#elif defined (__AVR_ATtiny2313__)
#  include <avr/iotn2313.h>

「iotn2313.h」の中にDDRDの宣言があったけどマクロを呼んでいるようだ…。

#define DDRD    _SFR_IO8(0x11)

_SFR_IO8マクロの宣言元は「sfr_defs.h」にあった。どうやらアセンブラとCからの呼び出しでマクロの内容も変化するようだ。

// アセンブラ
#define _SFR_IO8(io_addr) ((io_addr) + __SFR_OFFSET)
// C
#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
#define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

uint8_t型でアクセスできるようで、__SFR_OFFSETは、0x20のアドレスになっているようだ。

AVRのSFRは基本的にメモリ空間にマッピングされているが、低位の64バイトはI/O空間にもマッピングされていて、どちらでもアクセスできる。 しかし、メモリ空間の最初の32バイトには汎用レジスタがマッピングされているのに対し、I/O空間にはそれがないのでアドレスが32バイトずれている。 つまり、メモリ空間で0x20~0x5Fのレジスタが、I/O空間で0x00~0x3Fに見える。 これらのレジスタはIN命令やOUT命令などでアクセスが可能だが、io.h ではレジスタ名がメモリ空間のアドレスで定義されいているので変換が必要になる。 このズレ(0x20)を示すマクロが__SFR_OFFSETで、メモリ空間からI/O空間に変換するためのマクロが_SFR_IO_ADDR。 _SFR_MEM_ADDRというのもあり、こちらは何も変換しない。

このサイトで詳しく解説されていた。
AVR libcを使ってみる you/junkbox

0x11をデータシートで確認してみると、DDRDレジスタ(0x31)となっていた。DDD0〜6とビット位置は同じになっているようだ。
そもそもDDRDレジスタってなんだろって事なんだけど、実は先生に概ね教わっていた。
DDR○は入力・出力を決定するレジスタらしい。この場合、ポートDを入力にする場合は「0」、出力にする場合は「1」に設定するようだ。
同じようにPORTDは、ポートDの出力をHで出す場合は「1」、Lで出す場合は「0」にするらしい。HはHigh、LはLowを表すようで、Hにした場合にプルアップになり、電流が流れるようだ。

これで変数の意味はだいたい分かった。「iotn2313.h」にはこの他にも定義されている変数があるようだ。

関数

続いて関数について調べてみた。今回使用されているのは一つで、_delay_msという関数だ。
調べなくともだいたい分かるが、指定ミリ秒遅延させるものだろう。
気になるのはコメントの「max is 262.14 ms / F_CPU in MHz」というもの。
今回クロック周波数は8MHzに設定していたので、262.14ms / 8Mhz = 32.7675ms になる。最大32msまで設定可能という事だと思う。
32ms以上遅延させたい場合は複数回呼び出す必要があるようで、forループなどで指定回数呼び出す。

for(i = 0; i < 10; i++){
  _delay_ms(30);  /* max is 262.14 ms / F_CPU in MHz */
}

30ms * 10 = 300ms(0.3秒遅延させている)

ランダム点滅させるプログラムを組む

お勉強はこのくらいで実際にプログラムを組んでみた。LEDは2つ接続していて、PD3とPD4に接続している。
DDRDのビット3と4を「1(出力)」に設定した。ランダム点滅は、rand関数を呼び出す事で遅延時間を調整する。
定義済み定数・マクロをフル活用するために、指定ビットを立てる_BVマクロ、PD3・PD4定数を利用した。

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h> 
#include <stdbool.h>

int main(void) {
  DDRD = 0b00011000;
  bool flag = false;

  for (;;) {
    char i;
    int r = rand() % 10 + 1;

    for(i = 0; i < r; i++) {
      _delay_ms(30);
    }

    PORTD = flag ? _BV(PD3) : _BV(PD4);
    flag = !flag;
  }

  return 0;
}

今回のコードとMakefileはGitHubにも上げてみた。
pontago/avr-LedTest · GitHub

アセンブラで書いてみるのもいいかな。。。
次に作るものは温度計・湿度計になると思うけど、一気にハードルも上るから苦労しそうだ。

AVRでLEDを点滅させてみた

前回LEDを点灯させる所まで何とかできた。と言っても、電池ボックスとLEDを接続しただけ…。
今回はいよいよマイコン(AVR)を使って、LEDをコントロールしてみようと思う。

まず、ライターであるAVRISPmkIIとマイコンを接続しなくてはいけない。パソコンとの接続はUSB経由で接続するだけなので簡単。
AVRISPmkIIのコネクタはISPコネクタ(6ピン)のため、このままだとブレッドボードに接続するのに大変なようだ。
そこで、共エレで販売されている「AVRWRT用ブレッドボードISPケーブル」のコネクタに付け替えた。

色々なサイトを参考にしつつ、マイコンに接続してみた。
続いて、マイコンに書き込むためのソフトをインストールするが、Mac環境だったため、正規のソフト(AVR Studio)が使えないようなので、CrossPack for AVR Developmentを使うことにした。
CrossPack - A Development Environment for Atmel’s AVR Microcontrollers

/usr/local配下にインストールされるようで、/usr/local/CrossPack-AVRというシンボリックリンクが作成される。
CrossPackインストール後に、avr-projectコマンドを実行することで、XCodeのプロジェクトファイルが生成されるようだ。

$ avr-project avr-LedTest

かなりシンプル…XCodeからいじるのも面倒なので、直接ファイルをいじることにした。
firmwareディレクトリにMakefile、main.cの2つのファイルが生成された。大事なのはこの2つのようだ。

Makefileでライターとマイコンの種類を書き換える必要があるようで、以下のように書き換えた。

DEVICE = attiny2313
PROGRAMMER = -c avrispmkII -P usb -p t2313

makeでプログラムのコンパイル、make flashでマイコンにプログラムを転送させるようだ。make fuseでヒューズの書き換えも可能とのこと。
とりあえず、マイコンにプログラムを転送してみようと思い、デフォルトのままmain.cをコンパイルし、make flashを実行してみたがエラーになってしまう。

// firmwareディレクトリで実行
$ make
avr-gcc -Wall -Os -DF_CPU=8000000 -mmcu=attiny2313 -c main.c -o main.o
avr-gcc -Wall -Os -DF_CPU=8000000 -mmcu=attiny2313 -o main.elf main.o
rm -f main.hex
avr-objcopy -j .text -j .data -O ihex main.elf main.hex
avr-size --format=avr --mcu=attiny2313 main.elf
AVR Memory Usage
----------------
Device: attiny2313

Program:      58 bytes (2.8% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

$ make flash
avrdude -c avrispmkII -P usb -p t2313  -U flash:w:main.hex:i

avrdude: stk500v2_command(): command failed
avrdude: stk500v2_program_enable(): bad AVRISPmkII connection status: Target not detected
avrdude: initialization failed, rc=-1
         Double check connections and try again, or use -F to override
         this check.


avrdude done.  Thank you.

make: *** [flash] Error 1

調べまくった結果、マイコンへの接続の仕方が間違っていたようで、一からコネクタを接続し直してみた。もともと付いていたコネクタと、付け替えたコネクタをよく見比べて繋いでいくと、やはり間違えていた…。
参考サイトを見ながらコネクタの色だけで判断して接続したのがいけなかったようで、しっかり仕様を把握して進めていかなくちゃいけないと反省。
トリパマ Toripama: AVRISPmkII ライタのピンの配列
サンプル・プログラムで学ぶAVRの実践【2/4回目】マイコン書き込み環境の構築 (PIC,78K,R8,HC(S)08/RS08,AVR,MSP430などのマイコン活用)

f:id:happytar0:20120714033630j:plain
こんな感じで接続すると上手くいった。

$ make flash
avrdude -c avrispmkII -P usb -p t2313  -U flash:w:main.hex:i

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.01s

avrdude: Device signature = 0x1e910a
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 "main.hex"
avrdude: writing flash (670 bytes):

Writing | ################################################## | 100% 0.28s

avrdude: 670 bytes of flash written
avrdude: verifying flash memory against main.hex:
avrdude: load data flash data from input file main.hex:
avrdude: input file main.hex contains 670 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.20s

avrdude: verifying ...
avrdude: 670 bytes of flash verified

avrdude: safemode: Fuses OK

avrdude done.  Thank you.

ひゃっほー、悩んだ末に解決した時の嬉しさはたまらない!

肝心のLEDを点灯させるプログラムを書いてみることにした。
マイコンを経由してLEDを接続するにはどうすればいいんだと少し考えたが、PD4(ポート8)にLEDと抵抗を接続することで解決。思ったより簡単だった。
サンプルコードはCrossPackのサイトに書いてあったので、それをコピペすればいいだけだ。

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRD = 1 << 4;           /* make the LED pin an output */
    for(;;){
        char i;
        for(i = 0; i < 10; i++){
            _delay_ms(30);  /* max is 262.14 ms / F_CPU in MHz */
        }
        PORTD ^= 1 << 4;    /* toggle the LED */
    }
    return 0;               /* never reached */
}

f:id:happytar0:20120714050150j:plain
LEDが点滅してる…肝心の点滅箇所はいともあっさり出来てしまった。
しかしながら、コピペして終わりというのはあまりにもつまらない。
実際はLEDを2つ接続してランダムに交互に点滅を繰り返す、というプログラムを組んだのだが、謎の変数やら関数が出現しているので、次回に詳しく掘り進めていきたいと思う。

電子工作はじめました

ついに電子工作に手を出してみた。やるやる詐欺をすること1年…はじめるまで長かったです。
三日坊主にならないようにコツコツやっていきたいと思う。

電子工作やらマイコンなんてまったく知らない人間なので、まずはLEDを点灯させてみようという事で以下の部品を購入。

  • プログラマ(ライター?) AVR ISP MKII
  • ブレッドボード EIC-108J
  • AVR ATTINY2313-20PU
  • 3mm赤色LED LT3U31P 250mcd
  • カーボン抵抗 1/4W 200Ω
  • カーボン抵抗 1/4W 150Ω
  • 電池ボックス 単3x3本
  • AVRWRT用ブレッドボードISPケーブル

f:id:happytar0:20120704201002j:plain

まずブレッドボードの使い方を覚えるためにLEDを電池ボックスと接続して点灯させてみた。
さすがにこれが出来なかったらやばいよな…と思いつつ、あっさり点灯して安心した。そしてブレッドボードも案外簡単な仕組みだった。

ブレッドボードに150Ωの抵抗とLED、そして電源を接続しただけ。
必要な抵抗は以下の式で求められるようだ。というかオームの法則ってやつだ。
中学の時に習ったけど勉強に熱心ではなかったのですっかり忘れてしまった…。

R(抵抗) = V(電圧) / A(電流)

LEDのスペックは以下のようだ。

Vf=1.85V〜2.5V If=20mA

そして電圧については、電池ボックスに単三乾電池3本ということなので以下になる。

1.5V x 3本 = 4.5V

これをもとに抵抗を計算してみると、

(4.5V - 1.85V) / 0.02A = 132.5Ω

どうやら132Ωくらいの抵抗でちょうどいいようだ。今回は適当なものがなかったので150Ωと念のために200Ωの抵抗を買ってみた。

f:id:happytar0:20120708151644j:plain
無事に点灯した!!全然むずかしい事してないのに嬉しい。
やっぱり形があるものを触って思い通りに動かせると、ソフトをいじるのと違った喜びを感じる。

20mAを越えた電流を流すと壊れると聞いたので、興味本位で抵抗を外してそのまま電源と接続してみた。
f:id:happytar0:20120708151803j:plain
あれ…壊れない。でも弱々しい光り方でなんかおかしい。

とにかく無事?に成功出来てよかった。困った事があれば詳しい先生が居るので心強い。
次はマイコンと接続してLEDの点灯を制御してみたいです。

OpenCVをいじってみた【後編】

前回の記事ではサンプル画像を集めるところまで書いた。
ポジティブサンプルをコツコツ処理するのは面倒なので、opencv_createsamplesコマンドを使って大量のサンプル画像を作ってみた。

opencv_createsamplesを使う

opencv_createsamplesコマンドの使い方は簡単だ。引数なしで実行すると以下のような引数の説明が表示される。

$ opencv_createsamples
Usage: opencv_createsamples
  [-info <collection_file_name>]
  [-img <image_file_name>]
  [-vec <vec_file_name>]
  [-bg <background_file_name>]
  [-num <number_of_samples = 1000>]
  [-bgcolor <background_color = 0>]
  [-inv] [-randinv] [-bgthresh <background_color_threshold = 80>]
  [-maxidev <max_intensity_deviation = 40>]
  [-maxxangle <max_x_rotation_angle = 1.100000>]
  [-maxyangle <max_y_rotation_angle = 1.100000>]
  [-maxzangle <max_z_rotation_angle = 0.500000>]
  [-show [<scale = 4.000000>]]
  [-w <sample_width = 24>]
  [-h <sample_height = 24>]

「-info」と「-img」はどちらか一方を指定するようで、ネガティブサンプルとポジティブサンプルを組み合わせる場合は「-img」を使用する。
サンプルデータは「-vec」引数で指定したファイルに出力されて、この出力されたサンプルダータをもとに学習処理をおこなう。
「-bg」で指定するネガティブサンプルは、画像ファイルのパスをリスト化したものを指定する。

findコマンドを利用することで簡単に画像ファイルのパスをリスト化することができる。

$ find negative_images/ -name '*.jpg' > negative_images.txt

以下のような引数を指定してサンプルデータを作成した。
「-num」が生成するサンプルデータ数になり、「-bgcolor」は背景とみなす色を指定するようだ。今回は255(白)を指定した。また、「-bgthresh」は背景色とみなす範囲?ということで5を指定してみた。

$ opencv_createsamples -vec positive.vec -img positive_images/1.jpg -bg negative_images.txt -num 7000 -bgcolor 255 -bgthresh 5 -w 24 -h 24

無事にポジティブサンプルの生成に成功したはいいが、これをもとに学習させてみてもいい結果を出すことは出来なかった。
たぶんこの方法でポジティブサンプルを用意する場合、文字など検出対象物として認識しやすいようなものではないとだめなのかもしれない。

地道にポジティブサンプルを集める

やっぱり地道に集めるしかないと思いつつ、なるべく楽に集められないか色々調べてみることにした。
あれこれ調べていると以下のサイトを見つけた。どうやらOpenCVでアニメの顔や目を認識させる、ということをやっているようだ。
anime.udp.jp

ここで公開されている便利な加工ツール「SC」を見つけたので使ってみることにした。ちなみにWindowsアプリのみのようだ。
加工元の画像はネガティブサンプルを集める時と同じように、Yahoo画像検索のAPIを利用して集めた。ひたすらカレーライスだけ切り出すのは正直しんどかった。
そして、色々なカレーライスの画像を見ていくうちに、これを認識させるのはかなり難しいのではないかと思い始めた…。

意外にカレーライスの画像は少なくて、どれも同じような画像が多いので結局800枚ほどしか用意することはできなかった。
これも同じようにopencv_createsamplesコマンドを利用してサンプルデータ化する。先ほどと違うのは、「-img」ではなく、「-info」引数を利用することだ。
「-info」には、「-bg」と同じくリスト化したファイルを指定する。この時に注意したいのは、単純に画像ファイルのパスを指定するだけではなく、「その画像に含まれる検出対象物の数」と「座標」、「大きさ」も書き出す必要があるようだ。

ImageMagickに含まれる「identify」コマンドを使うことで画像の大きさが分かるので、findコマンドと組み合わせて書き出してやればいいようだ。

$ find positive_images/ -name '*.png' -exec identify -format '%i 1 0 0 %w %h' \{\} \; > positive_images.txt
$ opencv_createsamples -vec positive.vec -info positive_images.txt -num 800 -bgcolor 255 -bgthresh 5 -w 24 -h 24

サンプルデータから学習させる

opencv_haartrainingコマンドか、opencv_traincascadeコマンドを利用してサンプルデータを学習させる。opencv_traincascadeコマンドの方が新しいようで、マルチコア対応やLBPにも対応しているのでこちらを利用するのがいいようだ。しかし、学習後に生成されるファイルも新しいフォーマットになるため、opencv_preformanceコマンドで利用することができないようなので注意。

opencv_traincascadeコマンドを引数なしで実行した結果は以下のようになる。

Usage: opencv_traincascade
  -data <cascade_dir_name>
  -vec <vec_file_name>
  -bg <background_file_name>
  [-numPos <number_of_positive_samples = 2000>]
  [-numNeg <number_of_negative_samples = 1000>]
  [-numStages <number_of_stages = 20>]
  [-precalcValBufSize <precalculated_vals_buffer_size_in_Mb = 256>]
  [-precalcIdxBufSize <precalculated_idxs_buffer_size_in_Mb = 256>]
  [-baseFormatSave]
--cascadeParams--
  [-stageType <BOOST(default)>]
  [-featureType <{HAAR(default), LBP, HOG}>]
  [-w <sampleWidth = 24>]
  [-h <sampleHeight = 24>]
--boostParams--
  [-bt <{DAB, RAB, LB, GAB(default)}>]
  [-minHitRate <min_hit_rate> = 0.995>]
  [-maxFalseAlarmRate <max_false_alarm_rate = 0.5>]
  [-weightTrimRate <weight_trim_rate = 0.95>]
  [-maxDepth <max_depth_of_weak_tree = 1>]
  [-maxWeakCount <max_weak_tree_count = 100>]
--haarFeatureParams--
  [-mode <BASIC(default) | CORE | ALL
--lbpFeatureParams--
--HOGFeatureParams--

「-data」で指定したパスに分類器が生成される。「-featureType」で学習方法を指定できるが、今回はLBPを利用することにした。

以下のような引数を指定して分類器を作成してみた。
「-numPos」でポジティブサンプルの数を指定するが、バグがあるようできっちり実際の数を指定すると途中で失敗してしまうようだ。実際の数より少なめに指定することで、失敗せずに実行できる。
OpenCV2.4.0でHAAR分類器を学習させるときの問題と対策 (ゆめ技:ゆめみスタッフブログ)

$ opencv_traincascade -data ./data -vec positive.vec -bg negative_images.txt -numPos 750 -numNeg 3000 -numStages 20 -mode ALL -w 24 -h 24 -precalcValBufSize 5000 -precalcIdxBufSize 1000 -featureType LBP

「-data」引数で指定したパスに「cascade.xml」が生成されるので、これが分類器となる。

作った分類器を試してみる

さっそく作った分類器を試してみた。結果からお伝えするとかなり認識率が低い…というかほぼ認識しない。
学習データが少ないのか、やり方が間違っているのか分からない。

OpenCVのサンプルプログラムを利用して作った分類器を試してみた。
引数に認識させたい画像ファイルを指定すると、認識された部分が緑の線で囲われて表示されるものだ。

#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"

#include <iostream>
#include <stdio.h>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
  Mat image = imread(argv[1]);

  String cascadeName = "cascade.xml";
  CascadeClassifier cascade;
  cascade.load(cascadeName);

  vector<Rect> komas;
  cascade.detectMultiScale(image, komas);
 
  for (vector<Rect>::iterator it=komas.begin(); it!=komas.end(); ++it) {
    rectangle(image, Rect( it->x, it->y, it->width, it->height), 
      Scalar(0,255,0));
  }

  cout << "count: " << komas.size() << endl;

  namedWindow("result", CV_WINDOW_AUTOSIZE | CV_WINDOW_FREERATIO);
  imshow("result", image);
  waitKey(0);

  return 0;
}

以下のコマンドでコンパイルして実行した。

$ g++ -I/opt/local/include -L/opt/local/lib -lm -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_objdetect -g -o sample sample.cpp
$ ./sample test.jpg // 実行

結果はというと…。
f:id:happytar0:20120708193503j:plain
一応成功したっぽいけど、もう少し認識率を上げていかないと使いものにならなそう。というか、何に使い道があるんだろ…。


参考サイト
Haar状特徴に基づくブースト分類器のカスケードを利用する高速物体検知 - penny
Tutorial: OpenCV haartraining (Rapid Object Detection With A Cascade of Boosted Classifiers Based on Haar-like Features) - Naotoshi Seo