#norelated #contents * RTCサンプル [#o86fce4c] RTCモジュールとI2Cで通信して時計を作る。~ I2Cは前回よくわかってないって書いたけど、前よりはちょっと理解した。~ USARTは、2本の線を使って、1本が送信、1本が受信の役割をするけど、I2Cは同じ二本でも、1本がクロック、1本がデータになっていて、複数の機器を制御できるバス通信らしい。~ 複数の機器は、それぞれがマスターにもスレーブにもなれて、それぞれの状態で送信受信ができるらしい。~ AVRと[[秋月のRTCモジュール:http://akizukidenshi.com/catalog/g/gI-00233/]]で、このI2Cという方式でデータのやり取りをすることにより、時刻の設定、読み出し等が可能になると。~ ** 回路図 [#s17638b1] 前回書いたように、UART(USART)はRXD(PD0)とTXD(PD1)をクロスに繋いだけど、I2CはSCL(PC5)とSDA(PC4)は、それぞれ同じもの同士を繋ぐ。 SCLとSDAは、RTCモジュールのジャンパをショートすれば、内部でプルアップ抵抗が有効になるらしいけど、なんとなくそのままにしておきたかったので、自前で内部に持っているのと同じ2.2kΩの抵抗をつけてあります。~ プルアップ抵抗値の決め方とかも、理由があるんだろうけど、まだよくわかってません。~ #ref(RtcTest.png,left,nowrap,RTCサンプル) ** ソースコード [#lfaf1c0f] 今回、RTC-8564NBモジュールを使った時計表示を行うために、LCDライブラリとUSARTライブラリに、多少の手を加えてあります。~ LCDライブラリは、カーソル表示とカーソルブリンクのON/OFFを、初期化時に設定できるように。~ USARTライブラリは、関数ポインタを登録することで、受信完了時、送信完了時の割り込み処理ができるように。~ &ref(LcdLib.h,left,nowrap,LCDライブラリヘッダ);&ref(LcdLib.c,left,nowrap,LCDライブラリソース);&ref(UsartLib.h,left,nowrap,USARTライブラリヘッダ);&ref(UsartLib.c,left,nowrap,USARTライブラリソース);&ref(util.h,left,nowrap,ユーティリティヘッダ);&ref(util.c,left,nowrap,ユーティリティソース);~ 今回は長くなるので、分割しながら載せていきます。~ プログラムサイズは、sprintf使用で4.8kbくらいです。~ *** 定義部分ソースコード [#b78da7d4] #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の[[アプリケーションマニュアル:http://www.epsontoyocom.co.jp/product/RTC/set03/rtc8564je_nb/index.html]]に載っているので、それを参考に。~ 次は、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)部分ソースコード [#e1945366] 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部分ソースコード [#u40a33a4] 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; } 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; } void Rtc_allread( uint8_t *data ) { Rtc_getdata( 0, data, 16 ); } void Rtc_allwrite( uint8_t *data ) { Rtc_setdata( 0, data, 16 ); } 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 ); // 全データ書き込み } 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; } *** main動作部分ソースコード [#m38fad0d] 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; } } 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 ); } 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 ); } 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; } ** 基本動作 [#k26822dd] まず、電源を入れてPCとUSB通信ができるようにします。~ この辺は、前回の[[USBシリアル通信サンプル]]を参照のこと。~ 繋がったら、PCから設定したい日時を、以下のように打ち込んで、Enterを押すと、日時がRTCに設定されます。~ 20100418123456 この例では、2010/04/18の、12:34:56に設定されるわけです。~ 1回キーを押す度に、その時点のバッファの内容がエコーバックされるようになっているので、入力内容を確認することができます。~ 間違ったキーを入力してしまった場合、「.」(ピリオド)の入力で、バッファの内容がリセットされるので、再度最初から打ち込んでください。~ 設定した時刻は、LCDに二段に分かれて表示されます。~ 上段は、日付と曜日、下段は時刻。~ 曜日は、PCからの入力時に、[[ツェラーの公式:http://www.google.com/search?hl=ja&lr=lang_ja&ie=UTF-8&oe=UTF-8&q=%E3%83%84%E3%82%A7%E3%83%A9%E3%83%BC%E3%81%AE%E5%85%AC%E5%BC%8F]]で算出しています。 PCから入力した西暦の1000の位と100の位は、この曜日計算にしか使用していません。~ RTCモジュールは、西暦の下2桁しか気にしないので、RTC内にも保存できないし、今回のプログラム中でも保存していません。~ なので、表示時にも下2桁しか表示出来ない仕様となります。~