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、完成
少し美観に課題が残りますが、ひとまず完成としました。
0 件のコメント:
コメントを投稿