RTCサンプル

RTCモジュールとI2Cで通信して時計を作る。
I2Cは前回よくわかってないって書いたけど、前よりはちょっと理解した。

USARTは、2本の線を使って、1本が送信、1本が受信の役割をするけど、I2Cは同じ二本でも、1本がクロック、1本がデータになっていて、複数の機器を制御できるバス通信らしい。
複数の機器は、それぞれがマスターにもスレーブにもなれて、それぞれの状態で送信受信ができるらしい。

AVRと秋月のRTCモジュールで、このI2Cという方式でデータのやり取りをすることにより、時刻の設定、読み出し等が可能になると。

回路図

前回書いたように、UART(USART)はRXD(PD0)とTXD(PD1)をクロスに繋いだけど、I2CはSCL(PC5)とSDA(PC4)は、それぞれ同じもの同士を繋ぐ。

SCLとSDAは、RTCモジュールのジャンパをショートすれば、内部でプルアップ抵抗が有効になるらしいけど、なんとなくそのままにしておきたかったので、自前で内部に持っているのと同じ2.2kΩの抵抗をつけてあります。
プルアップ抵抗値の決め方とかも、理由があるんだろうけど、まだよくわかってません。

RTCサンプル

ソースコード

今回、RTC-8564NBモジュールを使った時計表示を行うために、LCDライブラリとUSARTライブラリに、多少の手を加えてあります。
LCDライブラリは、カーソル表示とカーソルブリンクのON/OFFを、初期化時に設定できるように。
USARTライブラリは、関数ポインタを登録することで、受信完了時、送信完了時の割り込み処理ができるように。

fileLCDライブラリヘッダfileLCDライブラリソースfileUSARTライブラリヘッダfileUSARTライブラリソースfileユーティリティヘッダfileユーティリティソース

今回は長くなるので、分割しながら載せていきます。
プログラムサイズは、sprintf使用で4.8kbくらいです。

定義部分ソースコード

#include <stdio.h>
#include <avr/interrupt.h>
#include "LcdLib.h"
#include "UsartLib.h"
#include "util.h"

#define RTC_ADDR_WRITE 0xA2	// RTC書き込みアドレス
#define RTC_ADDR_READ  0xA3	// RTC読み出しアドレス

#define I2C_BPS	2	// ボーレート設定値

// TWIモード列挙型定義
typedef enum _tagTWIMode{
	TWI_MODE_SEND,		// 送信モード
	TWI_MODE_RECEIVE,	// 受信モード
} TWIMode;
// TWIアクノリッジモード列挙型定義
typedef enum _tagTWIACKMode{
	TWI_ACKMODE_ACK,	// ACK送信モード
	TWI_ACKMODE_NACK,	// NACK送信モード
} TWIACKMode;

// USART受信バッファ
#define USART_BUFF_SIZE 16
char	usart_buff[USART_BUFF_SIZE];
uint8_t	usart_buff_cnt;

まずは、RTCの書き込みと読み出しをするためのI2Cスレーブアドレスを定義。
これは、RTCのアプリケーションマニュアルに載っているので、それを参考に。

次は、RTCとI2C通信をするための、ビットレートの設定。
RTC-8564NBモジュールは、最大で400kHzでの通信に対応しているらしい。
しかし、AVR側のTWI設定は、最大でもCPUクロックの1/16でしか駆動できないらしい。
現在AVRは1MHz駆動で動かしているので、最大で62.5kHzでの通信しか出来ないことになる。
なので、ここはきりのいい50kHzで通信することにし、AVRのTWIクロックのプリスケーラも等倍とし、以下の式でTWBRレジスタに設定する値を求める。

TWBRレジスタ値 = ( 1000000(CPU周波数) / 50000(SCLクロック) - 16 ) / ( 2 * 1(プリスケーラ値))

というわけで、設定する値は2。

次はTWIモード定義。
TWIには、マスター動作の送信/受信と、スレーブ動作の送信/受信がある。
今回はマスター動作しかしないので、とりあえず送信/受信を切り分けられるようにして、スレーブアドレス設定時の内部動作を切り分けられるようにした。

TWIアクノリッジモードは、データ受信時に、相手に返す応答の種類となる。
応答でACKを返すと、「正しく受け取ったので次を送ってください」みたいな意味で、NACKを返すと、「これ以上データはいらないので送らないでください」みたいな意味になるらしい。
まだこのRTCモジュールしか知らないので、他のI2C通信機器も同じような設計になっているのかは謎。

USART受信バッファは、PCから入力されたデータを一旦保存しておくバッファ。
ここにデータをためておいて、Enter入力時にバッファ内のデータをRTCに設定するような動作にする。

I2C(TWI)部分ソースコード

void I2c_init( void )
{
	TWSR &= ~0x03;		// BPSの前置分周は等倍にする
	TWBR = I2C_BPS;		// CPUクロックが1MHzなのに対して、50KHzで通信する
	TWCR = 0b00000100;	// ピンをTWI(SCL/SDA)として使用する
}

初期化部分。
プリスケーラの等倍設定と、ビットレート指定と、PC4とPC5をSDAとSCLとして使いますよという設定。

int8_t I2c_start( void )
{
	TWCR = 0b10100100;	// スタート設定
	while( !( TWCR & 0b10000000 )){
		;	// 送信開始OKになるまで待つ
	}

	if( 0x08 == ( TWSR & 0xF8 )){
		return 0;
	} else if( 0x10 == ( TWSR & 0xF8 )){
		return 1;
	}
	return -1;
}

マスターとしての、通信開始部分です。
スレーブモードだとスタートはいらないのかな?
とりあえずはまだマスター動作しかよくわかっていないので、スレーブも使えるようにするには改造が必要なのかも。
基本的に、今のAVRとRTCが1対1の作りで、通信に失敗するような事はないですが、処理後のステータスも一応考慮して作っています。
失敗したことがないので、この作りが正しいのかどうかはよくわかりません(ぇー

int8_t I2c_setaddr( uint8_t addr, TWIMode mode )
{
	TWDR = addr;
	TWCR = 0b10000100;	// 送信設定
	while( !( TWCR & 0b10000000 )){
		;	// 相手の応答まで待つ
	}
	if( TWI_MODE_SEND == mode ){
		if( 0x18 == ( TWSR & 0xF8 )){
			return 0;
		} else if( 0x20 == ( TWSR & 0xF8 )){
			return 1;
		}
	} else {
		if( 0x40 == ( TWSR & 0xF8 )){
			return 0;
		} else if( 0x48 == ( TWSR & 0xF8 )){
			return 1;
		}
	}
	return -1;
}

通信相手を指定するための、スレーブアドレス送信処理です。
この処理については、送信モードと受信モードで処理後のステータスが変わるらしいので、引数でモードを指定するようにしています。

int8_t I2c_snddata( uint8_t data )
{
	TWDR = data;
	TWCR = 0b10000100;	// 送信設定
	while( !( TWCR & 0b10000000 )){
		;	// 相手の応答まで待つ
	}
	if( 0x28 == ( TWSR & 0xF8 )){
		return 0;
	} else if( 0x30 == ( TWSR & 0xF8 )){
		return 1;
	}
	return -1;
}

データの送信処理です。

int8_t I2c_rcvdata( uint8_t *data, TWIACKMode mode )
{
	if( TWI_ACKMODE_ACK == mode ){
		TWCR = 0b11000100;
	} else {
		TWCR = 0b10000100;
	}
	while( !( TWCR & 0b10000000 )){
		;	// データを受信するまで待つ
	}
	if( 0x50 == ( TWSR & 0xF8 )){
		*data = TWDR;
		return 0;
	} else if( 0x58 == ( TWSR & 0xF8 )){
		*data = TWDR;
		return 1;
	}
	return -1;
}

データの受信処理です。
受信後に相手に返すアクノリッジモードを引数で指定するようにしています。

void I2c_stop( void )
{
	TWCR = 0b10010100;	// ストップ設定
}

I2C通信の停止処理です。
値の設定だけ。

RTC部分ソースコード

uint8_t Rtc_setdata( uint8_t start, uint8_t *data, uint8_t num )
{
	uint8_t	i	= 0;	// ループ変数

	if( 16 < start + num || NULL == data ) return -1;

	while( 1 ){
		// 書き込みアドレス設定
		if( 0 > I2c_start() ){
			continue;
		}
		if( 0 != I2c_setaddr( RTC_ADDR_WRITE, TWI_MODE_SEND )){
			continue;
		}
		if( 0 != I2c_snddata( start )){
			continue;
		}
		// データ書き込み
		for( i = 0; i < num; i++ ){
			if( 0 > I2c_snddata( data[i] )) break;
		}
		I2c_stop();
		break;
	}
	return 0;
}

RTCにデータを送信する部分です。
RTCにデータを送ると、RTCでは内部のアドレスを自動的にインクリメントするので、最初にアドレスを指定した後は、連続でデータを送信することが出来ます。
なので、引数には、スタートアドレスとデータの先頭ポインタと設定したい数を渡すようにしています。
このあたりの、書き込み読み出しの手順もアプリケーションマニュアルに載っているので、参考に。

uint8_t Rtc_getdata( uint8_t start, uint8_t *data, uint8_t num )
{
	uint8_t	i	= 0;	// ループ変数

	if( 16 < start + num || NULL == data ) return -1;

	while( 1 ){
		// 読み出しアドレス設定
		if( 0 > I2c_start() ){
			continue;
		}
		if( 0 != I2c_setaddr( RTC_ADDR_WRITE, TWI_MODE_SEND )){
			continue;
		}
		if( 0 != I2c_snddata( start )){
			continue;
		}
		// データ読み出し
		if( 0 > I2c_start() ){
			continue;
		}
		if( 0 != I2c_setaddr( RTC_ADDR_READ, TWI_MODE_RECEIVE )){
			continue;
		}

		for( i = 0; i < num - 1; i++ ){
			if( 0 > I2c_rcvdata( &data[i], TWI_ACKMODE_ACK )) break;
		}
		if( num - 1 != i ){
			continue;
		}
		if( 0 > I2c_rcvdata( &data[i], TWI_ACKMODE_NACK )){
			continue;
		}
		I2c_stop();
		break;
	}
	return 0;
}

RTCからデータを受信する部分です。
送信と同じく、RTCでは内部のアドレスを自動的にインクリメントするので、最初にアドレスを指定した後は、連続でデータを受信することが出来ます。
スタートしてから、書き込みモードでアドレス指定して、読み出しアドレスを書き込んで、再度スタート後に、読み出しモードでアドレス指定して、データを受信するっていう手順が、最初はわかりにくかったというか、とっつきにくかったというか。
最後のデータを受信する時には、もうこれ以上データを送らなくていいよという意味のアクノリッジモードを指定しています。

void Rtc_allread( uint8_t *data )
{
	Rtc_getdata( 0, data, 16 );
}

全データ(16個のRTCのレジスタ)を読み出すためのラッパー関数です。

void Rtc_allwrite( uint8_t *data )
{
	Rtc_setdata( 0, data, 16 );
}

全データ(16個のRTCのレジスタ)を書き込むためのラッパー関数です。

void Rtc_init( uint8_t *data )
{
	Rtc_allread( data );	// 全データ読み込み

	// 初期データ設定
	data[0]		= 0;
	data[1]		= 0;
	data[2]		= data[2] & ~0x80;
	data[3]		= data[3] & ~0x80;
	data[4]		= data[4] & ~0xC0;
	data[5]		= data[5] & ~0xC0;
	data[6]		= data[6] & ~0xF8;
	data[7]		= data[7] & ~0xE0;
//	data[8]		= data[8];
	data[9]		= data[9] & 0x80;
	data[10]	= ( data[10] & ~0xC0 ) | 0x80;
	data[11]	= ( data[11] & ~0xC0 ) | 0x80;
	data[12]	= ( data[12] & ~0xF8 ) | 0x80;
	data[13]	= 0;
	data[14]	= 0;
	data[15]	= 0;

	Rtc_allwrite( data );	// 全データ書き込み
}

RTCの初期化関数です。
全データをバッファに読み込み、不要なビットを落として、設定し直しています。
ここでいう初期化と言うのは、有効な日時を設定するという意味の初期化ではなく、計時機能を正しく動かして、他の余計な機能(タイマーとかアラームとか)は止めておくという設定を行うという意味の初期化です。

uint8_t Rtc_settime( char* time )
{
	uint8_t		i		= 0;	// ループ変数
	uint8_t		data[7];		// 設定データ
	uint16_t	year	= 0;	// 年(数字格納用)
	uint16_t	month	= 0;	// 月(数字格納用)
	uint16_t	day		= 0;	// 日(数字格納用)
	uint8_t		uitmp	= 0;	// 数字格納用

	// 入力チェック
	for( i = 0; i < 14; i++ ){
		if( '0' > time[i] || '9' < time[i] ){
			return -1;	// 数字以外のデータが混ざっているので失敗
		}
	}

	// 現在データ取得
	if( 0 > Rtc_getdata( 2, data, 7 )){
		return -1;
	}

	// 年のチェック
	year = ( time[0] - '0' ) * 1000 + ( time[1] - '0' ) * 100 + ( time[2] - '0' ) * 10 + ( time[3] - '0' );
	data[6]	= (( time[2] - '0' ) << 4 ) | ( time[3] - '0' );

	// 月のチェック
	month = ( time[4] - '0' ) * 10 + ( time[5] - '0' );
	if( 12 < month ){
		return -1;
	}
	data[5]	&= ~0x7f;
	data[5]	|= (( time[4] - '0' ) << 4 ) | ( time[5] - '0' );

	// 日のチェック
	day = ( time[6] - '0' ) * 10 + ( time[7] - '0' );
	if( 31 < day ){
		return -1;
	}
	data[3]	= (( time[6] - '0' ) << 4 ) | ( time[7] - '0' );

	// 曜日の計算(ツェラーの公式)
	data[4]	= ( year + ( year >> 2 ) - year / 100 + year / 400 + ( 13 * month + 8 ) / 5 + day ) % 7;

	// 時のチェック
	uitmp = ( time[8] - '0' ) * 10 + ( time[9] - '0' );
	if( 24 < uitmp ){
		return -1;
	}
	data[2]	= (( time[8] - '0' ) << 4 ) | ( time[9] - '0' );

	// 分のチェック
	uitmp = ( time[10] - '0' ) * 10 + ( time[11] - '0' );
	if( 59 < uitmp ){
		return -1;
	}
	data[1]	= (( time[10] - '0' ) << 4 ) | ( time[11] - '0' );

	// 秒のチェック
	uitmp = ( time[12] - '0' ) * 10 + ( time[13] - '0' );
	if( 59 < uitmp ){
		return -1;
	}
	data[0]	&= ~0x7f;
	data[0]	|= (( time[12] - '0' ) << 4 ) | ( time[13] - '0' );

	// 現在時刻設定
	if( 0 > Rtc_setdata( 2, data, 7 )){
		return -1;
	}

	return 0;
}

RTCに入力されたデータを設定する関数です。
一度読み出しを行っているのは、設定する箇所以外のビットはそのままにしておきたいから。
(秒レジスタのVLビットとか、月レジスタの世紀ビットとか、ってかその2つか)

ふと気づいたけど、本当は最後のRTCにデータを設定する前に、RTCのコントロールレジスタのSTOPビットを立てて、計時機能を停止してからデータを更新して、再度STOPビットを落としてコントロールレジスタを設定して計時機能を復活させるべきなのかも。

main動作部分ソースコード

void usart_buff_init( void )
{
	uint8_t	i	= 0;	// ループ変数

	usart_buff_cnt = 0;
	for( i = 0; i < USART_BUFF_SIZE; i++ ){
		usart_buff[i] = 0;
	}
}

PCからの入力を保持しておくグローバルバッファを初期化します。

void usart_rcv_callback( char data )
{
	if( 0x2e == data ){
		// 入力を間違った時のリセット「.」
		usart_buff_init();
		Usart_sndstr( usart_buff );
		return;
	}
	if( 0x0d == data ){
		// ターミネータの処理
		Rtc_settime( usart_buff );
		usart_buff_init();
		Usart_sndstr( usart_buff );
		return;
	}

	usart_buff[usart_buff_cnt] = data;
	usart_buff_cnt++;
	if( USART_BUFF_SIZE <= usart_buff_cnt ){
		// オーバーフロー時はターミネータが来るまで最終バッファを書き換えるだけにする
		usart_buff_cnt = USART_BUFF_SIZE - 1;
	}
	Usart_sndstr( usart_buff );
}

PCからの入力があった時、割り込みで呼ばれる関数です。
USARTの受信完了割り込みで呼ばれます。
Usart_init関数の引数にこの関数のポインタを指定することで、割り込み時にコールされるようにしています。
PCから「.」(0x2e)が入力された時には、グローバルバッファを初期化し、Enter(0x0d)が押された時にはバッファの内容をRTCに設定してからグローバルバッファを初期化、それ以外の入力時は、グローバルバッファにデータを貯めます。

void print_time( uint8_t *data )
{
	uint8_t sec = data[2];
	uint8_t min = data[3];
	uint8_t hour = data[4];
	uint8_t day = data[5];
	uint8_t week = data[6];
	uint8_t month = data[7];
	uint8_t year = data[8];
	char date[17];
	char weekstr[7][4] = {
		"Sun",
		"Mon",
		"Tue",
		"Wed",
		"Thr",
		"Fri",
		"Sat",
	};
	sec = (( sec >> 4 ) & 0x07 ) * 10 + ( sec & 0x0F );
	min = (( min >> 4 ) & 0x07 ) * 10 + ( min & 0x0F );
	hour = (( hour >> 4 ) & 0x03 ) * 10 + ( hour & 0x0F );
	day = (( day >> 4 ) & 0x03 ) * 10 + ( day & 0x0F );
	week = week & 0x07;
	month = (( month >> 4 ) & 0x01 ) * 10 + ( month & 0x0F );
	year = (( year >> 4 ) & 0x0F ) * 10 + ( year & 0x0F );
	sprintf( date, "%02d/%02d/%02d %s", year, month, day, weekstr[week] );
	Lcd_setpos( 0, 0 );
	Lcd_setstr( date );
	sprintf( date, "%02d:%02d:%02d", hour, min, sec );
	Lcd_setpos( 1, 0 );
	Lcd_setstr( date );
}

BCD形式の入力を、10進数に変換して、sprintfで書式化してからLCDに表示しています。
上段に年月日と曜日、下段に時分秒の表示です。

int main( void )
{
	uint8_t data[16];	// 受信データ格納用
//	uint8_t	i	= 0;	// ループ変数
//	char str[5] = "  ";	// 16進コード格納用

	usart_buff_init();	// 受信バッファ初期化

	Lcd_init( 0 );	// LCD初期化

	Usart_init( usart_rcv_callback, NULL );	// シリアル通信初期化

	I2c_init();	// I2C通信初期化

	Rtc_init( data );	// RTCモジュール初期化

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

	while( 1 ){
		Rtc_allread( data );	// 全データ読み込み

		print_time( data );
/*		Lcd_setpos( 0, 0 );
		for( i = 0; i < 8; i++ ){
			sprintf( str, "%02X", data[i] );
			Lcd_setstr( str );
		}
		Lcd_setpos( 1, 0 );
		for( i = 8; i < 16; i++ ){
			sprintf( str, "%02X", data[i] );
			Lcd_setstr( str );
		}*/
		delay_ms( 100 );
	}

	return 0;
}

main関数ですが、タイマー割り込み等を使用しているわけではなく、100ms間隔でRTCと通信して、表示を更新しています。
せっかくだから、RTCからクロック出力をもらって、その割り込みで更新とかをするといいのかも。
バッテリーバックアップ等も含めて、更新契機は今後の課題ですかねぇ。

基本動作

まず、電源を入れてPCとUSB通信ができるようにします。
この辺は、前回のUSBシリアル通信サンプルを参照のこと。

繋がったら、PCから設定したい日時を、以下のように打ち込んで、Enterを押すと、日時がRTCに設定されます。

20100418123456

この例では、2010/04/18の、12:34:56に設定されるわけです。
1回キーを押す度に、その時点のバッファの内容がエコーバックされるようになっているので、入力内容を確認することができます。
間違ったキーを入力してしまった場合、「.」(ピリオド)の入力で、バッファの内容がリセットされるので、再度最初から打ち込んでください。

設定した時刻は、LCDに二段に分かれて表示されます。
上段は、日付と曜日、下段は時刻。
曜日は、PCからの入力時に、ツェラーの公式で算出しています。

PCから入力した西暦の1000の位と100の位は、この曜日計算にしか使用していません。
RTCモジュールは、西暦の下2桁しか気にしないので、RTC内にも保存できないし、今回のプログラム中でも保存していません。
なので、表示時にも下2桁しか表示出来ない仕様となります。


添付ファイル: fileUsartLib.h 806件 [詳細] fileUsartLib.c 948件 [詳細] fileLcdLib.h 829件 [詳細] fileLcdLib.c 1334件 [詳細] fileutil.h 768件 [詳細] fileutil.c 793件 [詳細] fileRtcTest.png 1350件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2010-05-01 (土) 22:45:38 (3276d)