本文へスキップ

アトメル社のAVRマイコンをつかった電子工作および無線を使った電子工作を紹介します。





ホーム > 工作室トップ > デジタル時計

デジタル時計

キャラクタLCDには秋月電子さんで販売しているSC2004CSを使用します。キャラクタLCDの制御にはChaNさんのEZ-LCDを使用します。これを利用することによりキャラクタLCDの制御部分のプログラムを作る必要なく楽にキャラクタLCDを制御可能となります。EZ-LCDの詳細についてはChaNさんのホームページを参照してください。
時計はマイコンに内蔵されていますタイマを使用してつくります。内部クロックを使用しますので若干精度が悪くなります。

1.仕様

項目 内容
機能 1行目:年月日を表示します。
2行目:時分秒を 表示します。
時計変更モードを持つ。[SET]、[UP]、[DOWN]の3つのスイッチを持ちます。。
時計変更モードには[SET]と[UP]を同時に押します。
[時計変更モード]
  SET:1回押す都度に年→月→日→時→分→秒の順に切り替わります。
  UP:押す毎に選択された項目が1ずつ加算します。上限まで行くと下限値から加算をします。
  DOWN:押す毎に選択された項目が1ずつ減算します。下限まで行くと上限値から減算をします。
  時計変更モードから通常モードへの復帰は時計設定モードで順に押していき「年→月→日→時→分」の次は通常モードとなります。
タイマ周期測定用出力を持ちます。
電源 単3ニッケル水素電池4本(4.8V) 
LCD 形名:SC2004CS
表示:20文字X4行
データ:4ビットモードで使用します。
マイコン ATmega328
LCD制御 CHANさんのEZ-LCDを使用します。

2.回路

回路図

時計計測はタイマ割込を使って加算します。タイマ割込毎にスイッチの状態を取り込みます。タイマはタイマ1を使用し、CTCモードで動作させます。タイマの割込間隔を測定するためにPB1をOCR1Aのコンペアマッチでトグル出力として周波数を測定できるようにします。
スイッチはPC3~PC5を使用します。
キャラクタLCDは4ビットモードを使用します。キャラクタLCDには書き込みのみを行いリードは行いまいませんのでキャラクタLCDのR/Wは’L’に固定します。
データはPB4~PB7を使用します。
レジスタ選択信号RSはPB0を使用します。
イネーブル信号EはPB2を使用します。
タイマ割込毎にPB1の出力を反転させます。
部品表
名称 形名 メーカ 購入先 数量
LCD SC2004CS  秋月電子
マイコン ATmega328 アトメル 秋月電子
抵抗 10kΩ 1/4W 秋月電子
半固定抵抗 10kΩ 秋月電子
押しボタンスイッチ 赤色タクトスイッチ 秋月電子
単3電池ホルダ4本用 秋月電子
単3ニッケル水素電池

組立図

3.フローチャート

LCDに表示するフローチャート

メインフロー

タイマ割込が入るまではスリープ状態とします。
タイマ割込によりプログラムの動作が再開されます。
再開されると時計の計測を行い、その後にスイッチの状態を確認します。
タイマ割込は250ms毎に行われ4回割込が入ると1秒をカウントします。
スイッチの状態監視はタイマ割込が発生する毎に行いますので割込間隔が長いとスイッチの反応が悪いように感じられるため250msとしました。
スイッチの動作は押してから離したときに動作するようにしています。
LCDの表示はEZ-LCDを使用します。
時計計測フローチャート

時計計測フロー

日のがかわるときのみ月および年(大小の月、閏年)にきおつけます。
時計計測値表
0~59
0~59
;時 0~23
1~末日
1~12
00~99

4.プログラムの実行

時計実行LCD表示画面

実行画面

プログラムを実行するとLCDは「12/06/01 00:00:00」から動作を開始します。
時計表示変更モード画面

時計変更モード(変更後の写真を示す)

時計変更モードにすると時計を変更できます。
タイマ周期確認チェックピン出力

タイマ周期確認チェックピン出力

コンペアレジスタA(OCR1A)=31249
250ms毎に割込を行う場合OCR1A=1MHz/(8 X 1/250ms)-1=31249
が設計値になります。
今回の工作では設計値だと少し早いです。
タイマ1コンペアレジスタA調整後

タイマ1コンペアレジスタA調整後

コンペアレジスタA(OCR1A)=31686
今回の実験では少し早いです。設計値の値(31249)ですと、5時間動作させたところストップウォッチに対して14分ほど早くカウントしていました。
OCR1A=31686でほぼ正しくなりました。
正確な時を刻むにはクリスタルが必要な用です。

5.プログラム

5.1.EZ-LCDを移植

EZ-LCDを今回のプログラムで使えるようにします。
LCDは4行20文字表示です。
hd44780.hとhd44780.cをプロジェクトに追加します。追加方法は「ツールバー」の「Add Existing Item」で追加します。

(1)hd44780.hの変更
#define  _LCD_ROWS  
4       /* Number of Rows (1,2 or 4) */
#define  _LCD_COLS  
 20      /* Number of Columns (8..40) */

(2)hd44780.cの変更
DELAY_US(n)はマイコンのライブラリの_delay_usを使いますのでdelay.hをインクルードしておきます。クロックはATmega328デフォルトの1MHzで使用します。
赤文字部分が変更箇所です。

/* Bus controls */
#include <avr/io.h>             /* Hardware specific include file */
#include <util/delay.h>
#define IF_BUS     4     /* Bus width (4 or 8) */
#define IF_INIT()        /* Initialize control port */
#define IF_DLY60()      /* Delay >=60ns (can be blanked for most uC) */
#define IF_DLY450()  
{PINB;}   /* Delay >=450ns@3V, >=250ns@5V */
#define E1_HIGH()    
 PORTB |= 1 << PORTB2      /* Set E/E1 high */
#define E1_LOW()      
PORTB &= ~(1 << PORTB2) /* Set E/E1 low */
#define E2_HIGH()      
PORTB |= 0x01           /* Set E2 high (dual controller only) */
#define E2_LOW()      
 PORTB &= 0xFE          /* Set E2 low (dual controller only) */
#define RS_HIGH()    
PORTB |= 1 << PORTB0          /* Set RS high */
#define RS_LOW()      
PORTB &= ~(1 << PORTB0)     /* Set RS low */
#define OUT_DATA(d)
 PORTB = ( d & 0xf0) | (PORTB & 0x0f)    /* Output a byte d on the bus (higher 4 bits of d in 4-bit mode) */
#define DELAY_US(n) 
_delay_us(n)                /* Delay d microseconds */

//static
//void delay_us (unsigned int n) /* An example of delay n X 10 microsecond routine (for Atmel AVR/8MHz) */
//{
//          do {             /* 10 clocks per loop (Atmel AVR) */
//
         } while (--n);
}

5.2.レジスタ設定

5.2.1.タイマ1設定値
タイマモード[ WGM13:WGM10]   CTCモード
分周比[CS12:CS10] 8分周
出力[COM1A1:COM1A0] OC1A(PB1)にコンペアマッチでトグル  割込コンペアマッチAで割込

TCCR1A
COM1A1 COM1A0 COM1B1 COM1B0 WGM11 WGM10

TCCR1B
ICNC1 ICES1 WGM13 WFM12 CS12 CS11 CS10

TIMSK
ICIE1 OCIE1B OCIE1A TOIE1

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include "hd44780.h" // EZ-LCDのヘッダファイルをインクルードしておきます。

#define RED 0x20 // 赤スイッチ
#define YELLOW 0x10 // 黄スイッチ
#define GREEN 0x08 // 緑スイッチ
#define MODE (RED | YELLOW) // 時計変更モード
#define SWON (RED | YELLOW | GREEN) // 全てのスイッチがON

void InitHard(void); // ハードウェア初期化
void InitTime(void); // 時間初期化
void CalcDate(uint8_t, uint8_t); // 時計演算 0:加算、1:減算
void BlinkDisp(uint8_t, uint8_t); // 指定位置ブリンクする。、0:通常、1:前桁の文字を表示
void DateDisp(uint8_t, uint8_t, uint8_t); // 指定場所の文字を更新する。
void UpdateTime(void); // 時計更新

uint8_t g_u8BlinkFlg = 0; // 0:点灯、1:消灯
volatile uint8_t g_u8IntTimeFlg; // 時計割り込みフラグ

uint8_t g_u8CalenderBuf[6]; // 時計データ保存、データはBCDで保存。上位4bit:10の位、下位4bit:1の位

union switch_states // スイッチ状態フラグ
{
uint8_t BYTE;
struct
{
uint8_t mode :1; // 時計変更モード
uint8_t mode_keep :1; // 時計変更モード確定
uint8_t red :1; // 赤スイッチON
uint8_t red_keep :1; // 赤スイッチON->OFF
uint8_t yellow :1; // 黄スイッチON
uint8_t yellow_keep :1; // 黄スイッチON->OFF
uint8_t green :1; // 緑スイッチON
uint8_t green_keep :1; // 緑スイッチON->OFF
}BIT;
};
union switch_states g_uSwitchState;

uint8_t g_u8CalenderArray[2][12] = {{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // カレンダ通常年
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; // カレンダ閏年

int main(void)
{
uint8_t u8PortcState;
uint8_t u8LcdLocate; // 変更位置記憶 0:yy, 1:mm, 2:dd, 3:hhh, 4:mmm, 5:sss
uint8_t u8LocateChange = 0; // 表示位置変更フラグ 0:変更位置移動なし、1:移動あり

InitHard(); // ポート、LCD初期化
g_u8IntTimeFlg = 0; // 250ms毎にカウント
InitTime(); // 時計初期化および初期値表示

sei(); // 割り込み許可

while(1)
{
sleep_enable(); //sleep許可
sleep_cpu(); //sleep中
sleep_disable(); // sleep禁止

// 時計更新
if (g_u8IntTimeFlg > 3)
{
UpdateTime(); // 時計更新
g_u8IntTimeFlg = 0;
}

// 時計調整
u8PortcState = ~PINC & SWON;

if (g_uSwitchState.BIT.mode_keep == 0) // 時計変更モード確認
{
if ( (u8PortcState == MODE)) // 赤と黄色のスイッチを同時on。
{
g_uSwitchState.BIT.mode = 1;
}
else if ( (u8PortcState == 0) && (g_uSwitchState.BIT.mode == 1)) // 赤と黄スイッチがoff->on->offで時計変更モードに移行
{
g_uSwitchState.BIT.mode_keep = 1;
u8LcdLocate = 0;
BlinkDisp(u8LcdLocate, 0); // 年の位置ブリンク表示
}
}
else if (g_uSwitchState.BIT.mode_keep) // 時計変更モード、スイッチは赤->黄->緑の順で優先順位が低くなる。
{ // 赤と緑が押されだ場合は赤が優先される。
u8LocateChange = 0; //表示位置変更フラグクリア
if (u8PortcState == RED) // 赤がon
{
g_uSwitchState.BYTE = 0;
g_uSwitchState.BIT.mode_keep = 1;
g_uSwitchState.BIT.red = 1;
}
else if (u8PortcState == YELLOW) // 黄がon
{
g_uSwitchState.BYTE = 0;
g_uSwitchState.BIT.mode_keep = 1;
g_uSwitchState.BIT.yellow = 1;
}
else if (u8PortcState == GREEN) // 緑がon
{
g_uSwitchState.BYTE = 0;
g_uSwitchState.BIT.mode_keep = 1;
g_uSwitchState.BIT.green = 1;
}
else if ( u8PortcState == 0) // スイッチが全部off、スイッチがoff->on->offで各動作をする。
{
if (g_uSwitchState.BIT.red == 1) // 赤がonだった
{
g_uSwitchState.BIT.red = 0;
u8LcdLocate++;
u8LocateChange = 1;
}
if (g_uSwitchState.BIT.yellow == 1) // 黄がonだった
{
g_uSwitchState.BIT.yellow = 0;
CalcDate( u8LcdLocate, 0);
}
if (g_uSwitchState.BIT.green == 1) // 緑がonだった
{
g_uSwitchState.BIT.green = 0;
CalcDate(u8LcdLocate, 1);
}

}
BlinkDisp(u8LcdLocate, u8LocateChange); // ブリンク、カレンダ更新
}
} // while loop
}

void InitHard(void)
{
// port初期化
DDRB = 0xff; // portb7:0=出力
PORTB = 0;
DDRC = 0x07; // portc2:0=出力
PORTC = 0;
DDRD = 0xff; // portd7:0=出力
PORTD = 0;
// LCD初期化
lcd_init();
// 16bitタイマ設定
// CTC動作、1/8分周
OCR1A = 31686; // 31249 設定値
TCCR1A = (1 << COM1A0);
TCCR1B = 0x0a; // スタート
TIMSK1 = 1 << OCIE1A; // ctc割り込み許可
// スリープモードはアイドルに設定
set_sleep_mode(SLEEP_MODE_IDLE);
}

void InitTime(void)
{
g_u8CalenderBuf[0] = 0x12;
g_u8CalenderBuf[1] = 6;
g_u8CalenderBuf[2] = 1;
g_u8CalenderBuf[3] = 0;
g_u8CalenderBuf[4] = 0;
g_u8CalenderBuf[5] = 0;

lcd_locate( 0, 0); // 1行目:年月日表示
lcd_putc((g_u8CalenderBuf[0] >> 4) + 0x30); // 年
lcd_putc((g_u8CalenderBuf[0] & 0x0f) + 0x30);
lcd_putc('/');
lcd_putc((g_u8CalenderBuf[1] >> 4) + 0x30); // 月
lcd_putc((g_u8CalenderBuf[1] & 0x0f) + 0x30);
lcd_putc('/');
lcd_putc((g_u8CalenderBuf[2] >> 4) + 0x30); // 分
lcd_putc((g_u8CalenderBuf[2] & 0x0f) + 0x30);

lcd_locate( 1, 0); // 2行目:時分秒表示
lcd_putc((g_u8CalenderBuf[3] >> 4) + 0x30); // 時
lcd_putc((g_u8CalenderBuf[3] & 0x0f) + 0x30);
lcd_putc(':');
lcd_putc((g_u8CalenderBuf[4] >> 4) + 0x30); // 分
lcd_putc((g_u8CalenderBuf[4] & 0x0f) + 0x30);
lcd_putc(':');
lcd_putc((g_u8CalenderBuf[5] >> 4) + 0x30); // 秒
lcd_putc((g_u8CalenderBuf[5] & 0x0f) + 0x30);
}

void BlinkDisp(uint8_t dat, uint8_t flg) //指定位置から2文字分ブリンクする。
{
uint8_t n, nn, raw, col;

switch( dat)
{
case 0: // 年
lcd_locate(0, 0);
break;
case 1: // 月
lcd_locate(0, 3);
raw = 0;
col = 0;
break;
case 2: // 日
lcd_locate(0, 6);
raw = 0;
col = 3;
break;
case 3: // 時
lcd_locate(1, 0);
raw = 0;
col = 6;
break;
case 4: // 分
lcd_locate(1, 3);
raw = 1;
col = 0;
break;
case 5: // 秒
lcd_locate(1, 6);
raw = 1;
col = 3;
break;
default: // 時計修正モード終了
g_uSwitchState.BYTE = 0;
lcd_locate(1, 6);
lcd_putc((g_u8CalenderBuf[5] >> 4) + 0x30);
lcd_putc((g_u8CalenderBuf[5] & 0x0f) + 0x30);
return;
break;
}

if ( g_u8BlinkFlg == 0)
{
// 消灯
g_u8BlinkFlg = 1;

lcd_putc(' ');
lcd_putc(' ');
}
else
{
// 点灯
g_u8BlinkFlg = 0;
lcd_putc((g_u8CalenderBuf[dat] >> 4) + 0x30);
lcd_putc((g_u8CalenderBuf[dat] & 0x0f) + 0x30);
}

if (flg == 1)
{
lcd_locate(raw, col);
lcd_putc((g_u8CalenderBuf[dat - 1] >> 4) + 0x30);
lcd_putc((g_u8CalenderBuf[dat - 1] & 0x0f) + 0x30);
}
}


void CalcDate(uint8_t dat, uint8_t flg)
{
int sum, day;

switch ( dat)
{
case 0: // 年
sum = (g_u8CalenderBuf[dat] >> 4) * 10 + (g_u8CalenderBuf[dat] & 0x0f);
switch(flg)
{
case 0: // 加算
if ( ++sum > 99)
{
sum = 0;
}
break;
default: // 減算
if ( --sum < 0 )
{
sum = 99;
}
break;
}
g_u8CalenderBuf[dat] =((sum / 10) << 4) | (sum % 10);
break;
case 1: // 月
sum = (g_u8CalenderBuf[dat] >> 4) * 10 + (g_u8CalenderBuf[dat] & 0x0f);
switch(flg)
{
case 0: // 加算
if ( ++sum > 12)
{
sum = 1;
}
break;
default: // 減算
if ( --sum < 1 )
{
sum = 12;
}
break;
}
g_u8CalenderBuf[dat] =((sum / 10) << 4) | (sum % 10);
break;
case 2: // 日
sum = (g_u8CalenderBuf[1] >> 4) * 10 + (g_u8CalenderBuf[1] & 0x0f); // 月を抽出
if ((((g_u8CalenderBuf[0] >> 4) * 10 + (g_u8CalenderBuf[0] & 0x0f)) % 4) == 0) // うるう年なら
{
day = g_u8CalenderArray[1][sum - 1];
}
else
{
day = g_u8CalenderArray[0][sum -1];
}
sum = (g_u8CalenderBuf[dat] >> 4) * 10 + (g_u8CalenderBuf[dat] & 0x0f);
switch(flg)
{
case 0: // 加算
if ( ++sum > day)
{
sum = 1;
}
break;
default: // 減算
if ( --sum < 1 )
{
sum = day;
}
break;
}
g_u8CalenderBuf[dat] =((sum / 10) << 4) | (sum % 10);
break;
case 3: // 時
sum = (g_u8CalenderBuf[dat] >> 4) * 10 + (g_u8CalenderBuf[dat] & 0x0f);
switch(flg)
{
case 0: // 加算
if ( ++sum > 23)
{
sum = 1;
}
break;
default: // 減算
if ( --sum < 1 )
{
sum = 23;
}
break;
}
g_u8CalenderBuf[dat] =((sum / 10) << 4) | (sum % 10);
break;
default: // 分、秒
sum = (g_u8CalenderBuf[dat] >> 4) * 10 + (g_u8CalenderBuf[dat] & 0x0f);
switch(flg)
{
case 0: // 加算
if ( ++sum >59)
{
sum = 0;
}
break;
default: // 減算
if ( --sum < 1 )
{
sum = 59;
}
break;
}
g_u8CalenderBuf[dat] =((sum / 10) << 4) | (sum % 10);
break;
}
}

void UpdateTime(void) // 時計
{
uint8_t sum, day, flg = 0;

// 秒
sum = (g_u8CalenderBuf[5] >> 4) * 10 + (g_u8CalenderBuf[5] & 0x0f);
if ( ++sum > 59)
{
g_u8CalenderBuf[5] = 0;
flg = 1;
DateDisp(1, 6, g_u8CalenderBuf[5]);
}
else
{
g_u8CalenderBuf[5] =((sum / 10) << 4) | (sum % 10);
DateDisp(1, 6, g_u8CalenderBuf[5]);
return;
}
// 分
if ( flg == 1)
{
flg = 0;
sum = (g_u8CalenderBuf[4] >> 4) * 10 + (g_u8CalenderBuf[4] & 0x0f);
if( ++sum > 59)
{
g_u8CalenderBuf[4] = 0;
flg = 1;
DateDisp(1, 3, g_u8CalenderBuf[4]);
}
else
{
g_u8CalenderBuf[4] =((sum / 10) << 4) | (sum % 10);
DateDisp(1, 3, g_u8CalenderBuf[4]);
}
}

// 時
if ( flg == 1)
{
flg = 0;
sum = (g_u8CalenderBuf[3] >> 4) * 10 + (g_u8CalenderBuf[3] & 0x0f);
if( ++sum > 23)
{
g_u8CalenderBuf[3] = 0;
flg = 1;
DateDisp(1, 0, g_u8CalenderBuf[3]);
}
else
{
g_u8CalenderBuf[3] =((sum / 10) << 4) | (sum % 10);
DateDisp(1, 0, g_u8CalenderBuf[3]);
}
}
// 日
if ( flg == 1)
{
flg = 0;

sum = (g_u8CalenderBuf[1] >> 4) * 10 + (g_u8CalenderBuf[1] & 0x0f); // 月を抽出
if ((((g_u8CalenderBuf[0] >> 4) * 10 + (g_u8CalenderBuf[0] & 0x0f)) % 4) == 0) // うるう年なら
{
day = g_u8CalenderArray[1][sum - 1];
}
else
{
day = g_u8CalenderArray[0][sum -1];
}

sum = (g_u8CalenderBuf[2] >> 4) * 10 + (g_u8CalenderBuf[2] & 0x0f);
if( ++sum > day)
{
g_u8CalenderBuf[2] = 1;
DateDisp(0, 6, g_u8CalenderBuf[2]);
flg = 1;
}

else
{
g_u8CalenderBuf[2] =((sum / 10) << 4) | (sum % 10);
DateDisp(0, 6, g_u8CalenderBuf[2]);
}
}
// 月
if ( flg == 1)
{
flg = 0;
sum = (g_u8CalenderBuf[1] >> 4) * 10 + (g_u8CalenderBuf[1] & 0x0f);
if( ++sum > 12)
{
g_u8CalenderBuf[1] = 1;
DateDisp(0, 3, g_u8CalenderBuf[1]);
flg = 1;
}
else
{
g_u8CalenderBuf[1] =((sum / 10) << 4) | (sum % 10);
DateDisp(0, 3, g_u8CalenderBuf[1]);
}
}
// 年
if ( flg == 1)
{
flg = 0;
sum = (g_u8CalenderBuf[0] >> 4) * 10 + (g_u8CalenderBuf[0] & 0x0f);
if( ++sum > 99)
{
g_u8CalenderBuf[0] = 0;
DateDisp(0, 0, g_u8CalenderBuf[0]);
flg = 1;
}
else
{
g_u8CalenderBuf[0] =((sum / 10) << 4) | (sum % 10);
DateDisp(0, 0, g_u8CalenderBuf[0]);
}
}
}

void DateDisp(uint8_t raw, uint8_t col, uint8_t dat) // 指定場所の文字を更新する。
{
lcd_locate(raw, col);
lcd_putc((dat >> 4) + 0x30);
lcd_putc((dat & 0x0f) + 0x30);
}

SIGNAL(TIMER1_COMPA_vect) // 0.25秒ごとに割込発生
{
g_u8IntTimeFlg++;
}