RTC(RX8900)を使った光の目覚まし時計 ~I2Cバスの勉強も兼ねて~
昨今のコロナにより在宅勤務になった人も多いのではないでしょうか。在宅により睡眠障害になる人も増えているみたいです。光は自然な起床を促し、音による起床よりもストレスが少ないとされます。
そこで今回はI2Cの勉強も兼ねて朝の目覚めを助ける目覚まし時計を製作し、noteにアウトプットすることとしました。
0.導入
シリアル通信(バス)とは同じ制御線に連続してデータを送る方法で、パラレル通信より配線が少なくなるのがメリット。その分プログラムが複雑となるのがデメリット。シリアル通信も様々ありますが、マイコン関連でよく使われるのが、3つ。
・1-Wire (電線2本(電力供給と兼)でOK)
・I2C (通信線2本(スレーブが増加しても線は増えない)、電力は別)
・SPI (通信線4本(スレーブが増加するごとに線が増える)、電力は別)
I2Cはスレーブの台数によらず線が増えないのがメリットですが、スレーブのアドレスはハードウェアで固定となっているため、違う部品で同じアドレスの場合、基本的には同じ回路に共存させることができないのがデメリットです。今回I2Cを選定します。
1.I2Cの基本
マスタ(CPU)とスレーブ(通信相手)がいる。
スレーブには製造時に決められた固有のスレーブアドレスがある。
マスタがクロックを生成して通信を制御する。
クロック周波数は 100 kHz か 400 kHz 。
I2C通信には 7-bitと 10-bitがある。一般的に使われるのは 7-bit。
書込の時
マスタがスタートコンディションを送信→7 bits のスレーブアドレス+ 1 bit の Write(0) の 8 bits を送信し、スレーブから ACK(Acknowledge) を受信する。その後は 8 bits のデータを送信したら スレーブからACK を受信する事を繰り返します。データの送受信が完了したらマスタがストップコンディションを送信して通信を終了します。
読込の時 (アドレス指定)
マスタがスタートコンディションを送信→7 bits のスレーブアドレス+ 1 bit の Write(0) の 8 bits を送信し、正常に完了したらスレーブから ACK (Acknowledge) を受信する。その後は読み込むアドレスの 8 bits のデータを送信したら スレーブからACK を受信する。マスタがリピートスタートコンディションを送信→マスタがスタートコンディションを送信→7 bits のスレーブアドレス+ 1 bit の READ(1) の 8 bits を送信したら スレーブからACK を受信する。ようやくデータをREADすることができます。受信のときは最終データを受信したら マスタからNACK (Not Acknowledge) を送信し、最終データであることをスレーブに伝える必要があります。データの送受信が完了したらマスターがストップコンディションを送信して通信を終了します。
送るデータや方法は製品データシートにより確認します。
2.仕様決定
目覚まし時計の仕様を決めます。RTC(リアルタイムクロック)と呼ばれる精度が良い時計専用のモジュールがあります。これを使用すれば正確な時刻が簡単に得られます。また、マイコンでの時間カウントもする必要がなくなり処理も楽になります。インターフェイスはI2Cを使用し配線を少なくしたいと思います。(I2C使ったことないけど) I2Cを勉強しようと思ったきっかけがこれ。
・マイコン:PIC16F877A (I2Cが使えればなんでもOK。家に余ってたのと将来の拡張性を考慮して選択。)
・RTC:RX8900 DIP化モジュール (今回のメイン)
・LCD: AQM1602 変換基板付き
・クロック: セラロック 20MHz
マイコンのみで時計を作成する場合、クロックの精度が時刻の精度に直結しますが、今回RTCを使用するのでセラロックの精度はそんなに問題になりません。
(ちなみにRTCからも32KHzが出力できますが、LCDの表示処理が遅すぎて実用的ではなかった為、20MHzを使用しています。)
・タクトスイッチ5個:時刻調整、アラーム時間調整、UP、DOWN、SET
・電源:主電源 5V 500mA (USB駆動)
バックアップ RTCの時刻保存として乾電池も装着(単3x2本)
・LED:スタンド型LEDを自作。(この辺は好みです。)
・その他:今後のソフトウェア更新も考えICSP用の端子も設置
すべて秋月電子で入手できます。
3.ハードウェア製作
回路図は氷魚堂さんのBSch3Vを利用しています。
・マイコン:RB0~RB7までは内蔵プルアップを利用。
・RTC:RTCは変換基板利用後のピンアサインで記載。I2Cバスラインのプルアップは変換基板をジャンパして内蔵プルアップを利用。
・LCD:LCDは変換基板を利用。I2Cバスラインのプルアップは変換基板をジャンパして内蔵プルアップを利用。
・ICSP部はPIC KITでは動作未確認です。(ピン数違うからそもそも論)
筆者は今更ながら秋月電子のPICライターを利用していますが、ちょっと改造するとPIC KITのICSPのように書き込むことができます。
ICSP周りの回路はこちらのサイトを参考にさせて頂きました。
サイト上は39KΩとなっていますが、ちょっと物臭だったので10KΩx4の40Ωにて作成。ただし、秋月電子としてはこの使い方は非公式ですので、利用される方は自己責任にてお願いします。最悪の場合、PICライターが壊れます。
その後ユニバーサル基板にてUEWにて配線。試作につき見た目はご容赦。(RB0に実験で10KΩのプルアップ抵抗つけてますが、特に意味はないです。)
4.ソフトウェア製作
4-1.I2Cライブラリ
I2C関連はネットにたくさん落ちていますので、スムーズに作成できました。16F877A用なのでこのまま利用します。以下のサイトが特に見やすく参考にさせて頂きました。(16F877AはSSPCON1ではなく、SSPCONなのでほかのPIC利用時には注意です。)
void I2C_Master_Init(const unsigned long c){
SSPCON = 0b00101000; //SSP Module as Master
SSPCON2 = 0;
SSPADD = (_XTAL_FREQ/(4*c))-1; //Setting Clock Speed
SSPSTAT = 0;
TRISC3 = 1; //Setting as input as given in datasheet
TRISC4 = 1; //Setting as input as given in datasheet
}
void I2C_Master_Wait(){
while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F)); //Transmit is in progress
}
void I2C_Master_Start(){
I2C_Master_Wait();
SEN = 1; //Initiate start condition
}
void I2C_Master_RepeatedStart(){
I2C_Master_Wait();
RSEN = 1; //Initiate repeated start condition
}
void I2C_Master_Stop(){
I2C_Master_Wait();
PEN = 1; //Initiate stop condition
}
void I2C_Master_Write(unsigned d){
I2C_Master_Wait();
SSPBUF = d; //Write data to SSPBUF
}
unsigned short I2C_Master_Read(unsigned short a){
unsigned short temp;
I2C_Master_Wait();
RCEN = 1;
I2C_Master_Wait();
temp = SSPBUF; //Read data from SSPBUF
I2C_Master_Wait();
ACKDT = (a)?0:1; //Acknowledge bit
ACKEN = 1; //Acknowledge sequence
return temp;
}
関数説明
void I2C_Master_Init(const unsigned long c)
I2C通信を初期化するときに呼び出す。cには通信スピードを引数として渡す(単位はHz)。100KHzと400KHzがありますが、100KHzでだいたい大丈夫です。400KHzに対応していないモジュールや400KHzにしても250KHzくらいしかでないモジュールとか様々なので製品によるみたいです。
void I2C_Master_Wait()
I2C通信が確立するまでの待機です。
他の関数から呼び出されます。以下の2つがORで満たされる場合は待機(while無限ループ)します。
・SSPSTATレジスタのR/W ビットに1
マスター設定時 1:送信中 0:レディ
・SSPCON2レジスタの 以下のビットがすべて1
ACKEN 1:ACKDTビットを返信(返信後自動クリア)
RCEN 1:受信許可 0:禁止
PEN 1:ストップコンディション送信(送信後自動クリア)
RSEN 1:リピートスタートコンディション送信(送信後自動クリア)
SEN 1:スタートコンディション送信(送信後自動クリア)
void I2C_Master_Start()
I2C通信を開始します。スタートコンディションを発行します。
SSPCON2レジスタのSENビットを1にします。
void I2C_Master_RepeatedStart()
I2C通信を開始します。リピートスタートコンディションを発行します。
SSPCON2レジスタのRSENビットを1にします。 void I2C_Master_Start()との違いは通信を切らずに通信の方向(READ/WRITE)を変える時に使用します。(詳細はRTCライブラリにて後述)
void I2C_Master_Stop()
I2C通信を終了します。ストップコンディションを発行します。
SSPCON2レジスタのPENビットを1にします。
void I2C_Master_Write(unsigned d)
I2C通信でデータを送信します。dには送信データを引数として渡します。
SSPBUFは送受信のデータを格納するバッファです。
unsigned short I2C_Master_Read(unsigned short a)
I2C通信でデータを受信します。
SSPCON2のRCENビットを1にします。aにはACK or NACKの設定(0 or 1)を引数として渡します。ちょっとややこしいのが「ACKDT = (a)?0:1; 」という条件演算子(3項演算子)です。この場合、(a)が真(1)であれば0を代入し、(a)が偽(0)であれば1をACKDTに代入するという式になります。(引数とは逆の数字がACKDTに代入されます。)ACKDTには返信するACK設定 0:ACK 1:NACKのどちらかを代入し、SSPCON2のACKENビットを1にすればマイコンが自動でACKDTの内容でスレーブに返信します。READ途中はACKで、READ終了時に必ずNACKを送信しないといけません。
逆にしてしまうと、1度目で送信が終わってしまいますのでご注意を。aは引数として1 or 0があるので直接代入すればいいような気がしますが、参考にしたサイトの方も苦戦した模様なのでそのまま行きます。返り値としてtemp(SSPBUFの受信データ)を返します。
とりあえずこれで意味が分からなくてもコピペでRTCもLCDもI2Cでアクセスできます。
4-2.LCDライブラリ
説明書に書いてある通りですが、一部変更です。
LCDプログラムは以下のサイトを参考にさせて頂きました。
https://wak-tech.com/archives/136
void LCD_Init()
{
I2C_Master_Init(100000);
__delay_ms(400);
LCD_writeCommand(0x38);
__delay_ms(20);
LCD_writeCommand(0x39);
__delay_ms(20);
LCD_writeCommand(0x14);
__delay_ms(20);
LCD_writeCommand(0x73);
__delay_ms(20);
LCD_writeCommand(0x52);
__delay_ms(20);
LCD_writeCommand(0x6C);
__delay_ms(250);
LCD_writeCommand(0x38);
__delay_ms(20);
LCD_writeCommand(0x0C); //ここを逆にしています。
__delay_ms(20);
LCD_writeCommand(0x01);
__delay_ms(20);
}
void LCD_writeData(char t_data)
{
I2C_Master_Start();
I2C_Master_Write(LCD_ADD);
I2C_Master_Write(0x40);
I2C_Master_Write(t_data);
I2C_Master_Stop();
__delay_ms(10);
}
void LCD_writeCommand(char t_command)
{
I2C_Master_Start();
I2C_Master_Write(LCD_ADD);
I2C_Master_Write(0x00);
I2C_Master_Write(t_command);
I2C_Master_Stop();
__delay_ms(10);
}
void LCD_str(char *c)
{
unsigned char i,wk;
for (i=0 ; ; i++) {
wk = c[i];
if (wk == 0x00) {break;}
LCD_writeData(wk);
}
}
関数の説明
LCD_ADDは事前に#deifine LCD_ADD 0x3eと宣言する必要があります。
LCDは書き込みしかないのでシンプルです。
スレーブアドレスのR/Wビットは強制的にWriteのみとなります。
void LCD_Init()
LCDの初期化です。ほぼ説明書通りですが、LCD_writeCommand(0x01);→LCD_writeCommand(0x0C); と説明書通り記載するとなぜか動かないので逆にしています。
void LCD_writeData(char t_data)
LCDにデータを書き込みます。t_dataに引数として書き込むデータを渡します。
スタートコンディション発行→スレーブアドレス送信→データを送る場合は最初に0x40を送る→t_dataの中身を送る→ストップコンディション発行の流れです。
void LCD_writeCommand(char t_command)
LCDにコマンドを書き込みます。t_commandに引数として書き込むコマンドを渡します。
スタートコンディション発行→スレーブアドレス送信→コマンドを送る場合は最初に0x00を送る→t_commandの中身を送る→ストップコンディション発行の流れです。
void LCD_str(char *c)
LCDに書き込む文字列の配列展開用です。*cには事前に宣言している文字列を引数として渡します。void LCD_writeData(char t_data)と合わせて使用します。配列に0x00が代入されているところまでループします。
ちなみにこの*ですがポインタです。概念が難しいので今回は理解を割愛しました(動けばヨシ)
サンプルプログラム
#define LCD_ADD 0x3e;
char moji[]='TEST';
LCD_Init();
LCD_writeCommand(0x02); //ホームへカーソル移動
LCD_str(moji); //moji の中身を表示
LCD_writeCommand(0x40+0x80); //2列目へ移動
LCD_str(moji);
LCD_writeCommand(0x02); //ホームへカーソル移動
LCD_writeCommand(0x01); //画面を クリア
LCD_Init()にて初期化
LCD_writeCommand()にてコマンドを送信。()にコマンドを指定します。
コマンドも説明書にある通りです。
0x01 画面クリア
0x02 ホームへカーソル移動
0x80 DDRAMアドレス指定
LCD_str(moji);にて配列の文字を展開します。()に変数を指定します。LCD_writeCommand(0x40+0x80); //2列目へ移動
0x40(移動先DDRAMアドレス指定)+0x80(DDRAMアドレス指定モード)の意味となります。
表示の処理が追い付かない場合は、適宜wait等を入れて調整します。
4-3.RTCライブラリ
機能もたくさんあるので複雑です。RTC-8564の製作記事はたくさん見つけましたがRX8900の製作記事はまだ少なく難航。
RTC-8564の以下の記事を参考にさせて頂き、RX8900用を作成しました。
基本的にこれも説明書通りです。RTCはREAD/WRITE両方あるので#defineで宣言しておきます。(2進数でも16進数でも可)
#define RTC_WRITE 0b01100100
#define RTC_READ 0b01100101
起動時にRTCの初期化します。メインループからこの関数を呼び出し、引数として、割込設定,年,月,日,曜,時,分,秒を渡します。この日時は仮で問題ありません。
曜日はソフトウェアで区別します。
ここで注意なのが、BCDにてデータを引数として渡す必要があるため、BCD変換関数をあらかじめ準備しておきます。(曜日はBCDでないがBCD変換しても同じなので問題ありません。)
/////////////////////////////////////////////////////////////////////////////////
// BCDをバイナリ(10進数)に変換する処理
/////////////////////////////////////////////////////////////////////////////////
unsigned int bcd2bin(char dt)
{
return ((dt >> 4) * 10) + (dt & 0xf) ;
}
/////////////////////////////////////////////////////////////////////////////////
// バイナリ(10進数)をBCDに変換する処理
/////////////////////////////////////////////////////////////////////////////////
unsigned int bin2bcd(unsigned int num)
{
return ((num/100) << 8) | ( ((num%100) / 10) << 4 ) | (num%10) ;
}
RTC初期化
/*******************************************************************************
* ans = RTC_Init(Inter,Year,Mon,mDay,wDay,Hour,Min,Sec) *
* RTCの初期化を行う処理 *
* アラーム機能無効(OFF)で初期化しています。 *
* タイマー機能は無効(OFF)、/INT端子出力は有・割込みは繰返すで初期化 *
* クロック出力(CLKOUT)は1Hz(1秒間に1回ON)で初期化しています。 *
* *
* Inter: 外部割込みを行うのかを指定する(CLKOUTは常に1Hzで出力している) *
* (0:割込みは無し 1[RB0]-8[RB7]の該当するピンで割込みを行う) *
* Year : 日付の年(0-99)を指定する(西暦の下2ケタ 2000年-2099年) *
* Mon : 日付の月(1-12)を指定する *
* mDay : 日付の日(1-31)を指定する(在りえない日を指定したら動作が不定らしい) *
* wDay : 曜日の指定(日[0] 月[1] 火[2] 水[3] 木[4] 金[5] 土[6]) *
* Hour : 時刻の時(0-23)を指定する *
* Min : 時刻の分(0-59)を指定する *
* Sec : 時刻の秒(0-59)を指定する *
* ans : 0:正常 1:異常(RTCがACKを返さない) *
*******************************************************************************/
int RTC_Init(char Inter,char Year,char Mon,char mDay,char wDay,char Hour,char Min,char Sec)
{
char reg1=0 , reg2 ;
int ans ;
// 1秒後に開始(RTC水晶振動子の発振を待つ)
__delay_ms(1000);
// 初期化状態のチェックを行う
I2C_Master_Start(); // スタートコンディションを発行する
ans=I2C_Master_Write(RTC_WRITE) ; //WRITEにする。
I2C_Master_Write(0x0D) ; // レジスターアドレスは0Dhを指定する
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_READ) ; // READにする。
reg1 = I2C_Master_Read(1) ; // Reg 0Eh を受信する 1を渡してACK
reg2 = I2C_Master_Read(0) ; // Reg 0Fh を受信する 0を渡してNACK(受信終了)
if (reg1 == 0x02) { // VLFビットがONなら初期化する
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ; //WRITEにする。
I2C_Master_Write(0x0D) ; // reg addレジスターアドレスは0Dhを指定する
I2C_Master_Write(0x01) ; // data 0x01 指定する(0Dh)
I2C_Master_Write(0x00) ; // data レジスターアドレスは0Eh書き込み(0Eh)
I2C_Master_Write(0x01) ; // data Control(Reg0F)の設定(RESET=0,RESET=1)
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ; //WRITEにする。
I2C_Master_Write(0x00) ; // レジスターアドレスは00hを指定する(sec)
I2C_Master_Write((char)bin2bcd(Sec)) ;// Seconds(Reg00)の設定(時刻の秒0-59,VL=0)
I2C_Master_Write((char)bin2bcd(Min)) ;// Minutes(Reg01)の設定(時刻の分0-59)
I2C_Master_Write((char)bin2bcd(Hour));// Hours(Reg02)の設定(時刻の時0-23)
I2C_Master_Write((char)bin2bcd(wDay));// WeekDays(Reg03)の設定(カレンダの曜日0-6
I2C_Master_Write((char)bin2bcd(mDay));// Days(Reg04)の設定(カレンダの日1-31)
I2C_Master_Write((char)bin2bcd(Mon)) ;// Months(Reg05)の設定(カレンダの月1-12)
I2C_Master_Write((char)bin2bcd(Year));// Years(Reg06)の設定(カレンダの年00-99)
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ; //WRITEにする。
I2C_Master_Write(0x08) ; // レジスターアドレスは08hを指定する()
I2C_Master_Write(0x80) ; // MinuteAlarm(Reg08)の設定(アラームの分無効)
I2C_Master_Write(0x80) ; // HourAlarm(Reg09)の設定(アラームの時無効)
I2C_Master_Write(0x80) ; // HourAlarm(Reg0A)の設定(アラームの日無効)
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ; //WRITEにする
I2C_Master_Write(0x0D) ; // レジスターアドレスは0Dhを指定する()
//I2C_Master_Write(0x80) ; // WeekDayAlarm(Reg0C)の設定(アラームの曜日無効)
I2C_Master_Write(0x03) ; // CLKOUT(Reg0D)の設定(1Hzで出力する)
I2C_Master_Write(0x00) ; // TimerControl(Reg0E)の設定(タイマ機能は無効)
I2C_Master_Write(0x00) ; // Timer(Reg0F)の設定(タイマ初期値は0)
// 時刻のカウント開始を指示
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ; //WRITEにする
I2C_Master_Write(0x0F) ; // レジスターアドレスは0fhを指定する
I2C_Master_Write(0x01) ; // data Control(Reg0F)の設定(RESET=0,RESET=1)
I2C_Master_Stop() ; // ストップコンディションを発行する
__delay_ms(100); ; // 時刻のカウント開始を待つ
}else {
}
return ans ;
ans = RTC_Init(0,21,01,03,0,22,39,00) ;
水晶が安定するまで待ち、0DhのVLFビット(低電圧検知)が1の場合、初期化します。
アドレスオートインクリメント機能があるので最初のアドレス指定をすれば自動的にREAD/WRITEするたびにアドレスが増加します。
READ/WRITEを切り替える場合や、インクリメントでは到達しないアドレスにアクセスしたい場合はリピートスタートコンディションを発行し、スレーブアドレス+READ/WRITE→アドレス指定をします。
RTC時刻更新
/*******************************************************************************
* ans = RTC_sTime(Year,Mon,mDay,wDay,Hour,Min,Sec) *
* RTCに日付と時刻を書き込む処理 *
* *
* Year : 日付の年(0-99)を指定する(西暦の下2ケタ 2000年-2099年) *
* Mon : 日付の月(1-12)を指定する *
* mDay : 日付の日(1-31)を指定する(在りえない日を指定したら動作が不定らしい) *
* wDay : 曜日の指定(日[0] 月[1] 火[2] 水[3] 木[4] 金[5] 土[6]) *
* Hour : 時刻の時(0-23)を指定する *
* Min : 時刻の分(0-59)を指定する *
* Sec : 時刻の秒(0-59)を指定する *
* ans : 0:正常 1:異常(RTCがACKを返さない) *
*******************************************************************************/
int RTC_sTime(char Year,char Mon,char mDay,char wDay,char Hour,char Min,char Sec)
{
int ans ;
// 時刻のカウント停止を指示
I2C_Master_Start(); // スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE);
I2C_Master_Write(0x0F) ; // レジスターアドレスは0Fhを指定する
I2C_Master_Write(0x01) ; // Control(Reg0F)の設定(RESET=0,RESET=1)
// 日付と時刻をを書き込む
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
ans=I2C_Master_Write(RTC_WRITE) ; //ansにACK受信状態を入れる。
I2C_Master_Write(0x00) ; //レジスターアドレスは00hを指定する
I2C_Master_Write((char)bin2bcd(Sec)) ; // Seconds(Reg01)の設定(時刻の秒0-59,VL=0)
I2C_Master_Write((char)bin2bcd(Min)) ; // Minutes(Reg02)の設定(時刻の分0-59)
I2C_Master_Write((char)bin2bcd(Hour)); // Hours(Reg03)の設定(時刻の時0-23)
I2C_Master_Write((char)bin2bcd(wDay)); // WeekDays(Reg04)の設定(カレンダの曜日0-6)
I2C_Master_Write((char)bin2bcd(mDay)); // Days(Reg05)の設定(カレンダの日1-31)
I2C_Master_Write((char)bin2bcd(Mon)) ; // Months(Reg06)の設定(カレンダの月1-12)
I2C_Master_Write((char)bin2bcd(Year)); // Years(Reg07)の設定(カレンダの年00-99)
// 時刻のカウント開始
I2C_Master_Start(); // スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE);
I2C_Master_Write(0x0F) ; // レジスターアドレスは0Fhを指定する
I2C_Master_Write(0x01) ; // Control(Reg0F)の設定(RESET=0,RESET=1)
__delay_ms(100); ; // 時刻のカウント開始を待つ
return ans;
}
初期化の時とほぼ同じですが、年,月,日,曜日,時,分,秒が変数になっています。
タクトスイッチ等のUP,DOWNを10進数でカウントし、そのままこの関数に渡せばOK。
RTC読み出し
/*******************************************************************************
* ans = RTC_rTime(*tm) *
* RTCから現在の日付と時刻を読み取る処理 *
* *
* *tm : 読み取ったデータを保存する配列変数を指定する(配列は7バイト必要) *
* 配列にはBCDデータで秒・分・時・日・曜日・月・年の順で保存する *
* ans : 0:正常 1:異常(RTCがACKを返さない) *
*******************************************************************************/
int RTC_rTime(char *tm)
{
int ans;
I2C_Master_Start() ; // スタートコンディションを発行する
I2C_Master_Write(RTC_WRITE) ;
I2C_Master_Write(0x00) ; // レジスターアドレスは00hを指定する
I2C_Master_RepeatedStart() ; // リピート・スタートコンディションを発行する
ans=I2C_Master_Write(RTC_READ) ;
*tm = I2C_Master_Read(1) &0x7f ; // 秒 //
tm++ ;
*tm = I2C_Master_Read(1) &0x7f ; // 分
tm++ ;
*tm = I2C_Master_Read(1) &0x3f ; // 時
tm++ ;
*tm = I2C_Master_Read(1) &0x7f; // 曜日
tm++ ;
*tm = I2C_Master_Read(1) &0x3f ; // 日
tm++ ;
*tm = I2C_Master_Read(1) &0x1f ; // 月
tm++ ;
*tm = I2C_Master_Read(0); // 年
I2C_Master_Stop() ; // ストップコンディションを発行する
return ans;
}
READも今まで通りの繰り返しです。読み出すデータはBCDです。
変数tm[]はメインループの方であらかじめ宣言してあります。
読み出す毎にtmをインクリメントします。
*tm = I2C_Master_Read(1) &0x7f ; 後ろに謎の数字がありますが、
もらってきたデータのマスク処理(誤作動防止)です。下の方にレジスタテーブルがありますが、〇と記載されている場所には何が返ってくるかはわからないので0をかける事によって強制的に0にしてしまいます。〇以外の場所は1でOKです。それをただ16進数で記載しています。
アラーム時刻設定もアドレスが違うだけで同様に行えます。RTC8564ではDAY ALARMとWEEK ALARMは別レジスタで用意されていますが、RX8900ではWADAビットによって切り替える(0:WEEK 1:DAY)ようになっています。毎日アラーム設定する場合は気にしなくてもいいですが、曜日指定する場合は気を付けてください。
AE,AF,AIEの意味合いはどちらも同じです。AEを1にするとアラームが有効になりますが、08hは時だけ、09hは分だけ、0Ahは毎日となります。
AFはアラームがONになった場合、1が保存されます。(ソフトウェアでクリアが必要)
AIEはアラームがONになった場合、INT端子からLowレベルが出力するかどうか選べます。
4-4 メイン処理
メイン処理では今まで作ってきたライブラリを呼び出します。
PIC、LCD、RTC、I2Cの初期化
RTCの現在時刻の表示で待機
ボタンにより、アラーム時刻調整、時刻調整モードに入る
モードに入ったらUP,DOWNで時刻調整、SETで抜ける
RTCからのINT端子 LOWを検知したら割り込み処理→LED点灯
アラームOFF待ち
といった処理を組み込みます。
この辺は簡単なのでRTC,LCDの連接部分のみ記載します。
曜日の配列をグローバル変数にて用意します。
const char WeekData[7][4] = { "SUN","MON","TUE","WED","THU","FRI","SAT" } ;
RTCの日付と時刻を文字列に変換する処理
/*******************************************************************************
* RTC_cTime(*tm,*c) *
* RTCの日付と時刻を文字列に変換する処理 *
* *
* *tm : RTC_rTimeから読み込んだ配列変数を指定する *
* *c : 文字列に変換したデータ(24バイト)を格納する配列変数を指定する *
* "yyyy/mm/dd www hh:mm:ss"で返す 例)"2010/01/15 TUE 15:30:00" *
*******************************************************************************/
// 日付と時刻を文字列に変換する処理のサブ関数
void set_ctime(char tm,char s,char *a)
{
*a = s ;
a++ ;
*a = (tm >> 4) + 0x30 ;
a++ ;
*a = (tm & 0x0f) + 0x30 ;
}
void RTC_cTime(char *tm, char *c)
{
int ans;
char buf[24] ;
buf[23] = 0x00 ;
set_ctime(*tm,':',&buf[20]) ; // 秒
tm++ ;
set_ctime(*tm,':',&buf[17]) ; // 分
tm++ ;
set_ctime(*tm,' ',&buf[14]) ; // 時
tm++ ;
memcpy(&buf[11],&WeekData[*tm&0x7f][0],3) ; // 曜日
tm++ ;
buf[10] = ' ' ;
set_ctime(*tm,'/',&buf[7]) ; // 日
tm++ ;
set_ctime(*tm,'/',&buf[4]) ; // 月
tm++ ;
set_ctime(*tm,'0',&buf[1]) ; // 年
buf[0] = '2' ;
memcpy(c,buf,24) ;
}
数字は数字としてLCD書き込めないので文字列に変換する必要があります。
このプログラムではtmに格納されたBCDから下記のキャラクタパターンに変換したのち、bufに格納しています。
例えば29秒だった場合、BCDでは00101001です。
void set_ctime(char tm,char s,char *a)
{
*a = s ; //*a(buf[20]) にs(:)を代入する
a++ ; //aをインクリメントする。buf[21]指定
*a = (tm >> 4) + 0x30 ; //00110010に変換され、buf[21]に代入する
a++ ; //aをインクリメントする。buf[22]指定
*a = (tm & 0x0f) + 0x30 ; //00111001に変換され、buf[22]に代入する
}
これを繰り返しますが、曜日は0~6の値に応じてもともと宣言している配列weekdataからbuf[11]に3文字コピーします。最後にbuf24個分をcにコピーしメインループに戻ります。
メイン(抜粋)
char tm[7] ;
char buf[24] ;
ans=RTC_rTime(tm);
RTC_cTime(tm,buf) ; // 日付と時刻を文字列に変換する
buf[14] = 0x00 ;
__delay_ms(20);
LCD_writeCommand(0x02); //ホームへカーソル移動
LCD_str(&buf[0]);
LCD_writeCommand(0x40+0x80); //2列目へ移動
LCD_str(&buf[15]);
LCD_writeCommand(0x02); //ホームへカーソル移動
tm[7]とbuf[24]を用意します。
ans=RTC_rTime(tm); RTCの読込データをtm[]に格納します。
RTC_cTime(tm,buf) ; tm[]のデータを文字列に変換しbuf[]に格納します。
buf[14]=0x00; 1列目終了。LCD_str()のループがブレイク。
LCD_str(&buf[0]); buf[0]から表示します。
LCD_writeCommand(0x40+0x80); 2列目へ移動
LCD_str(&buf[15]); buf[15]から表示します。
4-5補足
I2Cレジスタアドレスが8564と8900では違うのは想像していましたが、
DAYとWEEKDAYの順番が逆になっているの気づくのに遅れ悩む時間が増加。どちらもオートインクリメント機能があるので最初のレジスタアドレスしか意識しないのです。またRX8900にはRTC8564でいうところのSTOPはないので、RESETにて対応します。あとはアドレスが違ったり、名前が違ったりしますが大体一緒です
RX8900レジスタテーブル
RTC8564 レジスタテーブル
ちゃんとアドレス見に行かないと文字化けします。
5.使用感
タクトスイッチむき出しが使いにくい以外は問題ありません。
バックアップ電池を入れているので、電源が落ちても時刻調整の手間が
ないのが楽です。
写真ではAM ONになってますが、
ALM ON :アラームON
ALM OFF :アラームOFF
ALM ADJ :アラーム時刻調整モード
TIME ADJ :時刻調整モード
により設定をわかりやすく表示するようにしました。
6.実験
朝6時にセットして
1日目 6時30分
2日目 6時10分
3日目 6時20分
4日目 6時30分
起床といった具合です。
夜寝る時間もバラバラなのと、LEDの適切な明るさが分からないのが難点。
自然に起きるのが目的なので起きる時間がバラバラなのは仕方ないのかもしれません。
7.今後
RTCはWIFIやGPSから時間をもらえれば時刻調整用の回路を組み込む必要もなくなり簡単です。一度時刻を設定できれば基本的に再設定不要であり、精度よく時刻を刻めるので自作IOTでのタイマー機能など様々な活用に期待できます。