2012年9月13日木曜日

電子サイコロの製作(AVRマイコン版)

AVRマイコンのネイティブ開発環境が最低限整ったため、簡単な製作例を紹介致します。

開発環境の整備については、別途公開します。


1、概要

・LED×7・ブザー×1・操作スイッチ×1を持つ、ごく一般的な電子サイコロの構成とする。

・操作スイッチを押下する毎に、LEDシャッフル ⇒ カウントダウン ⇒ 出目表示 を繰り返す。

・同時に、出目に応じた音階でブザーを鳴らして、目だけでなく耳でも楽しむことができる。

・電源スイッチは持たず、一定時間放置するとスリープする。


2、仕様
2.1 内部ステータス

内部ステータスを以下の通り定義します。

・パワーオン
電源を最初に投入したとき、動作チェックを兼ねて全LEDを点滅してブザーを鳴らす。
以後、電池を交換しない限り、このステータスにはならない。

・シャッフル
サイコロを投げて、勢い良く転がっている状態。
1~6の各目を高速&ランダムに表示する。

・カウントダウン
サイコロが序々に止まろうとする状態。
各目をランダムに表示しながら、表示間隔が序々に遅くなる。

・出目決定
サイコロが止まった状態。
決定した目を連続して点滅する。

・スリープ
一定時間経過すると低消費電力状態となる。復帰のためのスイッチ入力のみ有効となる。


2.2 ステータス遷移

上記で決めたステータスの遷移を以下のように整理します。よく紹介されている作例とほぼ同じです。












2.3 詳細個別機能

サイコロとしての動作を実現するための個別機能について、以下の通り検討します。
※ この項は、プログラムの内部構造を理解されたい方以外は、スルーして頂いて構いません。

2.3.1  タイマ

時間管理用に、以下のタイマを用意する。

ウェイトタイマ(LED点滅時間待ち 100ms~1000ms程度)
スリープタイマ(放置時間監視 最大30sec程度)

何れも、AVRのハードウエアだけでは実現できないので、夫々タイマ変数を用意する。
今回は、Atmega168のTimer0(CTCモード)で10ms毎に発生する割込み(2.3.7項参照)でインクリメントする。

ウェイトタイマは、ウェイト開始時にプログラム本体からリセットし、タイマオーバーでウェイト終了する。
スリープタイマは、ステータス遷移直後にプログラム本体からリセットし、タイマオーバーでリクエストを発行する。

2.3.2 ブザー制御

LED表示に同期した時間&音階て鳴らす。
時間と音階(周波数)は、Atmega168のTimer1(CTCモード)の機能を使用する。

音階は比率で決まるので、一般に基準とされる「A(ラ)」中心に比率で求めた配列を用意し、使用するブザーが明瞭に鳴るようにTimer1の分周比を適当に決める方法で十分である。

2.3.3 スイッチ入力

以下の機能が求められる。

・ステータス遷移用
・スリープ復帰のためのハードウエア割込み(2.3.7項参照)

スイッチ入力は、チャタリング(接点あばれ)が発生するので、タイマ割込み間隔(2.3.7項参照)でサンプリングする。
一つ前のサンプリング結果と比較して、OFF⇒ONを検出し、リクエストを発行する。

2.3.4 リクエストのポーリング

スイッチ入力とスリープタイマオーバーで発生するイベントリクエストを監視する。
今回は、イベント数も少ないので、オーバーヘッドが生じる表示点滅のウェイト処理の中で監視し、イベントリクエストがあったらすぐにウェイトを抜けて、動作にタイムラグが無いようにする。

※ 全て割込みで処理する方法・static変数などを多用してオーバーヘッドを無しににする方法も検討しましたが、プログラムが非常に見難くなったので採用しませんでした。

2.3.5 スリープ

最も消費電力の少ない「パワーダウンモード」でスリープする。
このとき、LEDとブザーに使用しているポートはHiのままだと電流が流れ続けるので、忘れずに全てLowにする。
また、パワーダウンモードにおいて、復帰できる外部割込みはレベル検出のみとなるが、復帰直後に無効化している(2.3.7項参照)ため、スリープ直前に再び有効にしておく。

2.3.6 乱数

出目をランダムに表示するために乱数を用いる。
乱数の生成は、AVRToolChain(C言語開発環境)の rand() 関数が利用できる。
ただし、単なる乱数だと、サイコロとして類似の表示が続く場合があるので、最低限表示が類似しない処置を行う。
※ 今回は、「直近と同目」と、「偶数同士/奇数同士」の表示が続かないようにしました。

2.3.7 割込み

以下の割込み処理が必要となる。

・タイマ割込み
・外部(INT0)割込み

タイマ割込みは、間隔を短くしてもメリットは余り無いので、同時に行っているスイッチのサンプリング(2.3.3項参照)が遅過ぎない程度で、10ms間隔とする。
この間隔で、タイマ変数のカウントアップを行い、スイッチ及びスリープタイマのリクエスト生成(2.3.3項参照)を行う。

INT0割込みは、パワーダウンモードから復帰するため、レベル割込みとなっている(2.3.5項参照)が、この設定のままだと、スイッチをずっと押下し続けたときに割込みが連発するので、割り込み処理内でレベル割込み設定を無効にしておく。


3、回路

作画には、フリーソフト BSch3V を使用しています。















電子サイコロには勿体無いですが、手持ちにあったAtmega168を使用しました。
これによりポート余裕があるため、単にLED1個にポート一本を割り当てています。
機能上内部クロックで十分なので、発振子は省略しています。
スリープ復帰のため、スイッチは必ずAtmega168の4番ピンに接続します。
今回は、上記ピンの内蔵プルアップ抵抗を使用しました。



4、プログラム

以下にソースリストを示します。
ソースリストここから ↓

//////////////////////////////////////////////////////////////////////////////
//
// 電子サイコロの製作 Programmed by (c)ota957
//
// MPU = ATmega168P
// lfuse = 0x62
// hfuse = 0xdf
// efuse = 0x01
//
//////////////////////////////////////////////////////////////////////////////

#include <avr/io.h>
#include <stdlib.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <math.h>


//////////////////////////////////////////////////////////////////////////////
// グローバル変数及び各定義
//////////////////////////////////////////////////////////////////////////////

volatile uint16_t uWAITTIMER10MS;
volatile uint16_t uSLEEPTIMER10MS;

volatile uint8_t uREQUEST;
#define _CBI(p, q) ((p) &= ~_BV(q))
#define _SBI(p, q) ((p) |= _BV(q))

#define SW_ON 1
#define SW_OFF 0

#define REQUEST_IDLE 0
#define REQUEST_SWON 1
#define REQUEST_SWSTILLON 2
#define REQUEST_SWOFF 3
#define REQUEST_SLEEP 9

#define STATUS_POWERON 0
#define STATUS_SHUFFLE 1
#define STATUS_COUNTDOWN 2
#define STATUS_DECIDE 3
#define STATUS_SLEEP 9


//////////////////////////////////////////////////////////////////////////////
// 各割込み処理
//////////////////////////////////////////////////////////////////////////////
// ---------------------------------------------------------------------------
// 外部(INT0)入力
// ---------------------------------------------------------------------------

ISR(INT0_vect)
{
    // 次にスリープするまで、INT0割込みを無効にする
    _CBI(EIMSK, INT0);
}

// ---------------------------------------------------------------------------
// カウンタ/タイマ0比較一致
// ---------------------------------------------------------------------------

ISR(TIMER0_COMPA_vect)
{
    static uint8_t uPrev = SW_OFF;
    static uint16_t uOnTimerCount = 0;

    uWAITTIMER10MS++;
    // チャタリング対策のため、スイッチ入力をサンプリングする
    uint8_t uSwitch = bit_is_clear(PIND, PIND2) ? SW_ON : SW_OFF;
   
    // スリープタイマーがオーバーフローしたら、要求フラグを決定する
    if(uSLEEPTIMER10MS++ > 20000 / 10){

        uREQUEST = REQUEST_SLEEP;
        uSLEEPTIMER10MS = 0;

    // スイッチの直近の状態と比較して、要求フラグを決定する
    }else if(uPrev == SW_OFF && uSwitch == SW_ON){

        uREQUEST = REQUEST_SWON;
        uOnTimerCount = 0;

    }else if(uPrev == SW_ON && uSwitch == SW_ON){
        uREQUEST = ++uOnTimerCount > 1200 / 10 ? REQUEST_SWSTILLON : uREQUEST;
    }else if(uPrev == SW_ON && uSwitch == SW_OFF){
        uREQUEST = REQUEST_SWOFF;
        uOnTimerCount = 0;

    }
    uPrev = uSwitch;
}


//////////////////////////////////////////////////////////////////////////////
// 各サブルーチン
//////////////////////////////////////////////////////////////////////////////
// ---------------------------------------------------------------------------
// 要求フラグ操作マクロ
// ---------------------------------------------------------------------------

#define GetRequest() uREQUEST
#define RequestSWOn() (uREQUEST == REQUEST_SWON)
#define RequestSleep() (uREQUEST == REQUEST_SLEEP)
#define AcceptRequest() (uREQUEST = REQUEST_IDLE)
#define IgnoreRequest() (uREQUEST = REQUEST_IDLE)

// ---------------------------------------------------------------------------
// タイマ変数操作マクロ
// ---------------------------------------------------------------------------

#define GetWaitTimer() uWAITTIMER10MS
#define ResetWaitTimer() uWAITTIMER10MS = 0
#define WaitTimerOver(ms) (uWAITTIMER10MS >= ms / 10)

#define GetSleepTimer() uSLEEPTIMER10MS
#define ResetSleepTimer() uSLEEPTIMER10MS = 0
#define SleepTimerOver() (uSLEEPTIMER10MS >= 2000)

// COR1A設定時、比較一致が起こらない条件(OCR1A < TCNT1)を回避する
#define BeepScale(s) OCR1A = (uint8_t)(s); if(OCR1A < TCNT1) TCNT1 = OCR1A - 1
#define BeepOn() TCCR1A |= (1<<COM1A0)
#define BeepOff() TCCR1A &= ~(1<<COM1A0)

// ---------------------------------------------------------------------------
// 各LEDを表示するマクロ
// ---------------------------------------------------------------------------

#define LED1ON() PORTC |= (1<<PC5)
#define LED1OFF() PORTC &= ~(1<<PC5)
#define LED2ON() PORTC |= (1<<PC4)
#define LED2OFF() PORTC &= ~(1<<PC4)
#define LED3ON() PORTC |= (1<<PC3)
#define LED3OFF() PORTC &= ~(1<<PC3)
#define LED4ON() PORTC |= (1<<PC2)
#define LED4OFF() PORTC &= ~(1<<PC2)
#define LED5ON() PORTC |= (1<<PC1)
#define LED5OFF() PORTC &= ~(1<<PC1)
#define LED6ON() PORTC |= (1<<PC0)
#define LED6OFF() PORTC &= ~(1<<PC0)
#define LED7ON() PORTD |= (1<<PD4)
#define LED7OFF() PORTD &= ~(1<<PD4)

// ---------------------------------------------------------------------------
// サイコロの目を表示する
// LEDの配置は以下の通り
//
//  LED1      LED4
//
//  LED2 LED7 LED5
//
//  LED3      LED6
// ---------------------------------------------------------------------------

void DispDice(uint8_t uPip)
{
    if(uPip == 0){
        LED1OFF(); LED2OFF(); LED3OFF(); LED4OFF(); LED5OFF(); LED6OFF(); LED7OFF();
    }else if(uPip == 1){
        LED1OFF(); LED2OFF(); LED3OFF(); LED4OFF(); LED5OFF(); LED6OFF(); LED7ON(); 
    }else if(uPip == 2){
        LED1OFF(); LED2ON(); LED3OFF(); LED4OFF(); LED5ON(); LED6OFF(); LED7OFF();
    }else if(uPip == 3){
        LED1OFF(); LED2OFF(); LED3ON(); LED4ON(); LED5OFF(); LED6OFF(); LED7ON();
    }else if(uPip == 4){
        LED1ON(); LED2OFF(); LED3ON(); LED4ON(); LED5OFF(); LED6ON(); LED7OFF();
    }else if(uPip == 5){
        LED1ON(); LED2OFF(); LED3ON(); LED4ON(); LED5OFF(); LED6ON(); LED7ON();
    }else if(uPip == 6){
        LED1ON(); LED2ON(); LED3ON(); LED4ON(); LED5ON(); LED6ON(); LED7OFF();
    }else{
        LED1ON(); LED2ON(); LED3ON(); LED4ON(); LED5ON(); LED6ON(); LED7ON();
    }
}

// ---------------------------------------------------------------------------
// 1 ~ 6 の乱数を得る
// ---------------------------------------------------------------------------

uint8_t GetDiceRandom(void)
{
    static uint8_t uPrev;

    while(1){
        uint8_t uRand = (rand() % 6) + 1;
        // 前回と同数&表示が類似する数は除外する
        //if(uRand != uPrev){
        if((uRand + uPrev) % 2 != 0){
            uPrev = uRand;
            return uRand;
        }
    }
}

// ---------------------------------------------------------------------------
// 単に10msec単位でウェイトする
// ---------------------------------------------------------------------------

void WaitTimer(uint16_t uWaitMSec)
{
    ResetWaitTimer();

    while(!WaitTimerOver(uWaitMSec));
}

// ---------------------------------------------------------------------------
// 10msec単位でウェイトしながら、イベントリクエストを待つ
// ---------------------------------------------------------------------------

uint8_t WaitRequest(uint16_t uWaitMSec)
{
    ResetWaitTimer();

    while(!WaitTimerOver(uWaitMSec)){
        if(RequestSWOn() || RequestSleep())
            return GetRequest();
    }

    return GetRequest();
}

// ---------------------------------------------------------------------------
// ブザーを指定音階で鳴らす 1:C ~ 7:B
// ---------------------------------------------------------------------------

void Beep(uint8_t uScale)
{
    uint8_t uFreq[] = {131, 147, 165, 175, 196, 220, 247};

    BeepScale(F_CPU / (110 * uFreq[uScale-1]));
    BeepOn();
}

// ---------------------------------------------------------------------------
// 起動時表示
// ---------------------------------------------------------------------------

void PoweronDisp(void)
{
    uint8_t i = 0;

    while(!RequestSWOn() && !RequestSleep()){
        DispDice(7);
        Beep(i++ % 6 + 1);
        WaitRequest(500);
        DispDice(0);
        BeepOff();
        WaitRequest(500);

    }
}

// ---------------------------------------------------------------------------
// シャッフル実行
// ---------------------------------------------------------------------------

void Shuffle(void)
{
    while(!RequestSWOn() && !RequestSleep()){

        uint8_t uPip = GetDiceRandom();
        DispDice(uPip);
        Beep(uPip);
        WaitRequest(50);
        DispDice(0);
        BeepOff();
        WaitRequest(50);

    }
}

// ---------------------------------------------------------------------------
// カウントダウン実行
// ---------------------------------------------------------------------------

void CountDown(void)
{
    for(uint8_t i = 0; i <= 10; i++){

        uint8_t uPip = GetDiceRandom();
        DispDice(uPip);
        Beep(uPip);
        WaitTimer(50);
        BeepOff();
        WaitTimer(75 * i);
        DispDice(0);
        WaitTimer(50 + 10 * i);

    }
}

// ---------------------------------------------------------------------------
// 出目決定
// ---------------------------------------------------------------------------

void Decide(void)
{
    WaitTimer(800);

    uint8_t uPip = GetDiceRandom();
    //while(!RequestSWOn() && !RequestSleep()){
    for(uint8_t uCount = 1; !RequestSWOn() && !RequestSleep(); uCount++){
        DispDice(uPip);
        if(uCount <= 5) Beep(uPip);
        WaitRequest(750);
        DispDice(0);
        if(uCount <= 5) BeepOff();
        WaitRequest(250);

    }
}


//////////////////////////////////////////////////////////////////////////////
// メインループ
//////////////////////////////////////////////////////////////////////////////

int main(void)
{
    // I/O Setting
    DDRC |= _BV(DDC0) | _BV(DDC1) | _BV(DDC2) | _BV(DDC3) | _BV(DDC4) | _BV(DDC5);
    DDRD |= _BV(DDD4);

    _SBI(DDRB, DDB1);
    // Timer0 Setting
    _SBI(TCCR0A, WGM01);    // TIMER0 CTCMode
    _SBI(TCCR0B, CS02);     // TIMER0 Prescaler
    OCR0A = 39;             // TIMER0 CTC Top

    _SBI(TIMSK0, OCIE0A);   // TIMER0 CTC Interrut Enable
    // Timer1 Setting
    _SBI(TCCR1B, WGM12);    // TIMER1 CTCMode
    _SBI(TCCR1B, CS11);     // TIMER1 Prescaler
    _CBI(TCCR1B, CS10);
    OCR1A = 40;             // TIMER1 CTC Top

    // INT0 Pin Setting
    _SBI(EIMSK, INT0);      // INT0 Interrut Enable
    _SBI(PORTD, PD2);       // PORTD2 PullUp

    sei();

    uint8_t uStatus = STATUS_POWERON;
    while(1){
        switch(uStatus){
            case STATUS_POWERON:
                ResetSleepTimer();
                PoweronDisp();
                if(RequestSWOn())
                    uStatus = STATUS_SHUFFLE;
                else if(RequestSleep())
                    uStatus = STATUS_SLEEP;

                AcceptRequest();
                break;
            case STATUS_SHUFFLE:
                ResetSleepTimer();
                Shuffle();
                if(RequestSWOn())
                    uStatus = STATUS_COUNTDOWN;
                else if(RequestSleep())
                    uStatus = STATUS_SLEEP;

                AcceptRequest();
                break;
            case STATUS_COUNTDOWN:
                CountDown();
                uStatus = STATUS_DECIDE;
                IgnoreRequest();
                break;
            case STATUS_DECIDE:
                ResetSleepTimer();
                Decide();
                if(RequestSWOn())
                    uStatus = STATUS_SHUFFLE;
                else if(RequestSleep())
                    uStatus = STATUS_SLEEP;

                AcceptRequest();
                break;
            case STATUS_SLEEP:
                DispDice(0);
                BeepOff();

                _SBI(EIMSK, INT0);
                set_sleep_mode(SLEEP_MODE_PWR_DOWN);
                sleep_mode();

                while(!RequestSWOn());
                uStatus = STATUS_SHUFFLE;

                AcceptRequest();
                break;
        }
    }
}


ソースリストここまで ↑
Atmega168のヒューズビットは、デフォルトのままで全く変更していません。


5、組み立て

5.1 基板パターン

作画は、フリーソフト PCBE を使用しています。

























※ 実装が容易になるように、LED ⇔ 抵抗 を入れ替えてある箇所があります。


5.2 実装

メイン基板は、ユニバーサル基板に以下の通り実装します。
















電池ボックスは、もう一枚のユニバーサル基板にネジ止めして、互いに背中合わせになるように取り付けます。

















6、完成

















少し美観に課題が残りますが、ひとまず完成としました。