PR

ArduinoとGPS/RTCを組み合わせてGPS時計を作る

組み込みエンジニア
本記事はプロモーションが含まれています。

こんにちは、ENGかぴです。

ArduinoのWireライブラリとシリアル通信を使ってGPSモジュールとRTCモジュールの双方から時刻データを取得することができます。双方のモジュールの利点と欠点を補完しあうように組み合わせることでGPS時計を構成することができます。

秋月電子の「GPS受信機キット:AE-GYSFDMAXB」と「RTCモジュールDIP化キット:AE-RX8900」組み合わせて日付と時刻情報を取得してLCDに表示する方法を説明しています。LCDはQAPASS1602(スターターキットに付属)を使用しています。

本記事の内容は以下のリンクの応用例になります。GPSモジュールとRTCモジュールの使い方については下記記事にまとめています。

ArduinoとGPSモジュールで時刻を取得してLCDに表示する

ArduinoでRTCモジュールの情報をLCDを使って更新する

Arduino UNOを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

RTCモジュールとGPSモジュールの関係

RTCモジュールとGPSモジュールの関係
RTCモジュールとGPSモジュールの関係

GPSモジュールとRTCモジュールの双方のメリットを組み合わせることで時刻の管理を行うシステム構成です。それぞれのモジュールの特徴は以下の通りです。

GPS受信機キット:AE-GYSFDMAXBにおいてUTC時刻を東京の標準時に合わせるパケットが見つけ切れていない(330 PMTK_API_SET_DATAUMを発行してもオフセットしなかった)ため世界標準時を基準としています。

GPSモジュールの特徴:

  1. 電波を受信している限り時刻が正確に得られる。一度測位できていると途中で受信できなくなっても内部で時刻管理ができるが1PPS出力が停止する。
  2. 簡易的なモジュールの場合、世界標準時が基準なので日本時間に換算(+9時間)が必要となる。

RTCモジュールの特徴:

  1. 時刻を書き込むとうるう年を含めてモジュールで管理できる。
  2. 時刻設定を手動で行う必要があるため秒まで正確に管理しにくい

2つのモジュールの特徴を補いながら時間管理を行う手順は以下の通りです。

  1. GPSモジュールが測位開始し取得するまではRTCによる時間管理で動作する。受信できない場合はRTCの時間をボタンを使って設定する
  2. GPSモジュールがGPZDAパケットを出力と同時に1PPSパルスを出力するようになれば測位完了としてGPZDAパケットを受け入れる(①と②の処理)
  3. GPZDAパケットをRTCモジュール用のデータに変換し、1分ごとにRTCモジュールに時刻を書き込む。(③の処理)
  4. RTCモジュールの時刻を読み出して、LCD表示する。(④と⑤の処理)

GPSモジュールの受信が途中で途絶えてもRTCモジュールに書き込んだデータが時間管理するので秒単位での管理が可能になります。

広告

GPSモジュールのデータを変換する

GPSモジュールは衛星から時刻データを受信すると1PPSが出力されるようになるため1PPSのパルスを確認した後、受け入れたGPZDAパケットをもとにRTCモジュールに書き込むためのデータを生成します。

/*ControlSet()内のGPZDAパケットを受け入れた後の処理を一部抜粋*/
  
    if( gpsok ){ //GPS測位できているかの判定がOKなら以下の処理
        hour_now = charToDecimal(RxData[7])*10 + charToDecimal(RxData[8]);

        if( hour_now < 15 ){
            setdata.year = charToDecimal(RxData[26])*10 + charToDecimal(RxData[27]);
            setdata.month = charToDecimal(RxData[21])*10 + charToDecimal(RxData[22]);
            setdata.day = charToDecimal(RxData[18])*10 + charToDecimal(RxData[19]);
            setdata.hour = charToDecimal(RxData[7])*10 + charToDecimal(RxData[8])+9;
            setdata.minute = charToDecimal(RxData[9])*10 + charToDecimal(RxData[10]);
            setdata.second = charToDecimal(RxData[11])*10 + charToDecimal(RxData[12]);
            setdata.week = ( 1 << WeekSet(setdata.year));
            smin = setdata.minute;
      
            if(smin ^ oldsmin ){ //分に変更があったか?
                RX8900.setDateTime(&setdata);
            }
            oldsmin = smin;
        }
    }

GPSが測位できているとgpsokのフラグがセットされ内部の処理を行うようにしています。GPSからの時刻データは文字コードになっているので10進数に変換してRTCモジュールへの書き込みデータを生成する必要があります。

/* 文字コードを10進数に変換 */
int8_t charToDecimal(uint8_t chr){
    int8_t ret;

    ret = chr - 0x30;
    if( ret < 0){
        ret = 0;
    }
    return ret;
}

10進数の0~9は文字コード(アスキーコード)に変換すると0x30~0x39になります。この逆をすると文字コードから10進数に変換することができます。基本的にGPSから受信したGPZDAパケットの時刻情報を使用しているため単純に0x30を引くだけの変換にしていますが、汎用的に変換したい場合は少しバグ対策が必要です。

GPSから取得する時刻は世界標準時が基準なので先に時を10進数に変換し、世界標準時で15時未満であるかによってデータを生成するかの判断を行っています。日本時間に換算したとき日付を超える場合の管理が煩雑になるための対策です。

スポンサーリンク

GPSデータからRTC用のWEEKデータを計算する

GPSモジュールによってはGPS週番号やGPS週秒の情報が取れるものもありますが、今回使用しているGPSモジュールには曜日のデータは付属されません。(あるかもしれませんが、気づいていないだけかもしれません・・)曜日を表示したい場合はGPZDAの年と日付のデータから曜日を算出する計算を行う必要があります。

#define YEAR4CNT 1461 //366+365+365+365 //4年経過後の日数
const uint16_t yearplus[4] = {366,365,365,0}; //年経過のテーブル
const uint8_t monthtype[2][12] = {
                                  {0,31,28,31,30,31,30,31,31,30,31,30}, //通常年のテーブル
                                  {0,31,29,31,30,31,30,31,31,30,31,30} //うるう年のテーブル
                                 };

uint8_t WeekSet(uint8_t y ){
    uint8_t week;
    uint8_t y_1;
    uint8_t bb;
    uint8_t cc;
    uint16_t summonth = 0;
    uint16_t sumyear = 0;
    uint8_t uru = 0;

    y_1 = y - 20; //基準年(20:2020)からの経過年
    bb = y_1 / 4; //4年経過したら商として表示
    cc = y_1 % 4; //0-3年までの経過年数

    if( cc == 0 ){ //うるう年の場合は0
        uru = 1;
    }
  
    for(uint8_t i=0; i< setdata.month; i++ ){ //該当年の1月からの経過日数(月ベース)
        summonth += monthtype[uru][i];  
    }

    for(uint8_t i=0; i< cc; i++ ){ //0-3年経過まで経過日数(年ベース)
        sumyear += yearplus[i];  
    }
  
    sumday = YEAR4CNT * bb + sumyear + summonth + setdata.day -1; //基準日からの経過日数
    week = sumday % 7; //基準曜日(WED)からの差
    week +=3; //RTCモジュールではWEDは3回シフトなので+3する
    if( week > 6 ){ //+3して6より大きい場合は週をまたぐので1週間分(-7)を引く
        week =- 7;
    }
    return week;
}
//RTCモジュールへの曜日データをセット
setdata.week = ( 1 << WeekSet(setdata.year));

うるう年は4年に一度来るので4年間のうち1年間の年日数が366日なります。このため4年間を一区切りとして基準日(2020年1月1日水曜日)からの日数を計算して曜日の計算を行っています。

うるう年の定義から2100年は100で割り切れるため平年扱いになりますが、RTCモジュールが2099までしか対応していないため単純に4で割り切れる年がうるう年として問題ありません。

例)2025年2月11日までの経過日数と曜日
引数が25なのでy_1が5になり5年経過したことになります。bbを計算すると1になり4年経過後の日数の単位を1回経過したことになります。次にccを計算すると1になるので2024年から1年経過したことになります。2024年はうるう年なので366日経過するので2024年経過時の1461日に366日を加えた1827日が2025年1月1日を経過した日数になります。

2025年1月1日から2月11日までの経過日数は月ベースのテーブルから31日と11日となり当日を含むため-1すると41日になります。合計すると1827+41=1868日になります。これを1週間(7日)で割ったときの余りによって基準曜日から進んだ曜日が分かります。1868を7で割ると余りは6になるので基準曜日の水曜日から6日分進んでいることになるので火曜日になります。

RTCモジュールのデータに合わせるために+3して6より大きいと1週間経過したことになるので-7しています。これを戻り値として返すことでRTCモジュールのWEEKデータとしてシフトする回数が指定できます。

動作確認

GPSモジュールとRTCモジュールの組み合わせ(GPS時計)の回路図
GPSモジュールとRTCモジュールの組み合わせ(GPS時計)の回路図

GPSモジュールとRTCモジュールとLCDを使っておりArduinoのDIDOピンがいっぱいになってしまったためRTCモジュールの/INT(1秒カウント通知)を接続していません。そのためGPSモジュールの1PPSによってRTCモジュールデータの読み込みタイミングを同期しています。

SW1~SW3はGPSが受信できない時に時刻を設定する際に使用するものですが、任意の時間をセットしてもGPSからの測位情報で時刻を更新するためほとんど使用することはありません。

GPSモジュールとRTCモジュールの組み合わせ(GPS時計)の動作確認
GPSモジュールとRTCモジュールの組み合わせ(GPS時計)の動作確認

電源を入れると初期画面としてバージョンを表示しています。2秒ほどしてからメインの時刻表示しますが、最初はRTCの初期時間から時刻のカウントをスタートします。GPSは環境によりますが、窓際で30秒~40秒で測位が完了しGPSからの受信時間が表示されます。

LCDに表示している時間はRTCモジュールから読み出した時間ですが、GPSモジュールからの測位データを書き込んでいるので間接的にGPSの時刻を表示していることになります。

世界標準時で15時になる前まで(日本時間で23:59まで)にGPSから受信できれば正確な時間を刻むことができます。スマホの時計や部屋に置いているGPS時計と同じように時を刻んでいるのを確認しました。

スポンサーリンク

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

下記ソースコードを使用する際はRTCモジュールのAPIのファイルを追加する必要があります。以下のリンクのソースコード全体にあるものと同じですので、ご使用の際は下記リンクを参照ください。

ArduinoでRTCを使用してファイル管理とAPIの実装する

Arduinoのスケッチ:

#include <Wire.h>
#include <MsTimer2.h>
#include <LiquidCrystal.h>
#include <SoftwareSerial.h>
#include "RX8900.h" //自作のAPIファイル詳細は下記リンクを参照

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define MODE_CNG 200 //2sでモード切替
#define RTC_MAX 100 //1s

#define PIN_DI1 11
#define PIN_DI2 12
#define PIN_DI3 13
#define PIN_DI4 10 //GPS 1PPS
#define DI_FILT_MAX 4
#define FILT_MIN 1

#define LCD_RS 2
#define LCD_EN 3
#define LCD_D4 4
#define LCD_D5 5
#define LCD_D6 6
#define LCD_D7 7
#define COL_SZ 16

#define RING_SZ 256 //GPS
#define GPS_WAIT 500
#define YEAR4CNT 1461 //366+365+365+365 4年経過後の日数

enum MODE_BTN{
    MD_DATE = 0,
    MD_SET,
    MD_MAX
};

enum SET_MODE{
    SET_YEAR = 0,
    SET_MONTH,
    SET_DAY,
    SET_WEEK,
    SET_HOUR,
    SET_MINUTE,
    SET_SECOND,
    SET_MAX  
};

enum DI_NO{
    DI1 = 0,
    DI2,
    DI3,
    DI_MAX
};

struct DIFILT_TYP{
    uint8_t wp;
    uint8_t buf[DI_MAX][DI_FILT_MAX];
    uint8_t di1;
    uint8_t di2;
    uint8_t di3;
};

const char disptbl[2][COL_SZ]={
    "SET DATE        ",
    "SET TIME        "
};

const uint8_t daymax[4]={28,29,30,31};
const char buf101[]= "$PMTK101*32"; //GPS
const char buf314[]= "PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0"; //GPS
const uint16_t yearplus[4] = {366,365,365,0}; //年経過のテーブル
const uint8_t monthtype[2][12] = {
                                  {0,31,28,31,30,31,30,31,31,30,31,30}, //通常年のテーブル
                                  {0,31,29,31,30,31,30,31,31,30,31,30}  //うるう年のテーブル
                                 };

struct RING_MNG{
    uint16_t wp;
    uint16_t rp;
    uint8_t buf[RING_SZ];
};

struct TYP314{
    uint8_t header;
    uint8_t dat[sizeof(buf314)];
    uint8_t endchar;
    uint8_t sum_h;
    uint8_t sum_l;
    uint8_t cr;
    uint8_t lf;
};

// application use
SoftwareSerial mySerial(8, 9); // RX, TX GPS
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4,LCD_D5, LCD_D6, LCD_D7);
//const byte INT_PIN = 7;
int16_t cnt10ms;
int16_t timdifilt = TIME_OFF;
int16_t timbtn1 = TIME_OFF;
int16_t timrtc = TIME_OFF;
DIFILT_TYP difilt;
dateTime tim;
uint8_t temp;
dateTime initdate =  {0, 0, 0, WED, 1, 1, 20}; //初期値-秒-分-時-曜日-日-月-年
dateTime nowdata;
dateTime setdata;
MODE_BTN mainmode;
SET_MODE submode;
int16_t wcnt; //week計算用
bool uruflg;
bool btn1hold;
bool btn2hold;
bool btn3hold;

/*GPS-USE */
uint8_t sum;
uint8_t sum_h;
uint8_t sum_l;
uint8_t RxData[RING_SZ/2];
uint8_t date[18];
RING_MNG rxgps;
TYP314 Tx314;
int16_t timlcd = TIME_OFF;
int16_t timgpswait = TIME_OFF;
bool gpsok;

uint8_t smin;
uint8_t oldsmin;
uint32_t sumday;

/*** Local function prototypes */
void TimerCnt();
void mainTimer();
void DiFilter();
void RtcMain();
void SetSubMode();
uint16_t SetDate( SET_MODE date);

void GpsInit(); //GPS
void GpsMain(); //GPS
uint8_t dectoask(uint8_t dec);
void RxDataChk();
uint8_t sumbcc( uint8_t *adrs, uint8_t sz );
void ControlSet();
int8_t charToDecimal(uint8_t chr);
uint8_t WeekSet(uint8_t y );

void setup() {
  
    pinMode( PIN_DI1, INPUT_PULLUP );
    pinMode( PIN_DI2, INPUT_PULLUP );
    pinMode( PIN_DI3, INPUT_PULLUP );

    Serial.begin(9600);

    lcd.begin(16,2); //16×2を表示領域
    lcd.print("RTC-GPS-TEST");
    lcd.setCursor(0, 1); //2段目の左端にカーソル
    lcd.print("Ver1.00         ");
    delay(1500); //初期表示を1.5秒間行う(GPSモジュールの準備期間を兼ねる) 
    
    GpsInit();
    RX8900.begin(&initdate); //初期値を指定する場合

    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移
    MsTimer2::start();
    timdifilt = FILT_MIN;
    
    for( uint8_t i=0; i < 10; i++ ){
        mainTimer();
        DiFilter();
        delay(10);
    }
   
    nowdata = initdate;
    timrtc = RTC_MAX;
}

void loop() {
  
    mainTimer();
    DiFilter();
    RtcMain();
    ModeCng();
    GpsMain();
}
/*** RTCメイン処理  ***/
void RtcMain(){

    if( mainmode == MD_DATE ){
        if (timrtc == TIME_UP ){
            if(rtcmode == RTC_MODE_TYP::RTC_RX_END ){
              timrtc = RTC_MAX;
                rtcmode = RTC_MODE_TYP::RTC_IDLE;
            }
        }else{
            if(rtcmode == RTC_MODE_TYP::RTC_IDLE){
                rtcmode = RTC_MODE_TYP::RTC_RX;
            }
        }

        switch(rtcmode ){
        case RTC_MODE_TYP::RTC_IDLE:
          //処理なし
          break;
        case RTC_MODE_TYP::RTC_RX:
            RX8900.getDateTime(&tim);
            nowdata = tim;
            lcd.setCursor(0, 0); //1段目の左端にカーソル
            lcd.print("20");
            if(tim.year < 10){
                lcd.print("0");
            }
            lcd.print(tim.year);
            lcd.print("/");
    
            if(tim.month < 10){
              lcd.print("0");
            }
            lcd.print(tim.month);
            lcd.print("/");
            
            if(tim.day < 10){
              lcd.print("0");
            }
            lcd.print(tim.day);
            
            switch(tim.week){
            case SUN:
                lcd.print(" SUN");
                break;
            case MON:
                lcd.print(" MON");
                break;
            case TUE:
                lcd.print(" TUE");
                break;
            case WED:
                lcd.print(" WED");
                break;
            case THU:
                lcd.print(" THU");
                break;
            case FRI:
                lcd.print(" FRI");
                break;
            case SAT:
                lcd.print(" SAT");
                break;
            }
            lcd.setCursor(0, 1); //2段目の左端にカーソル
            if(tim.hour < 10){
                lcd.print("0");
            }
            lcd.print(tim.hour);
            lcd.print(":");
    
            if(tim.minute < 10){
                lcd.print("0");
            }       
            lcd.print(tim.minute);
            lcd.print(":");
    
            if(tim.second < 10){
                lcd.print("0");
            } 
            lcd.print(tim.second);
            lcd.print("  "); 
            RX8900.getTemp(&temp);
            lcd.print(((float)temp * 2 - 187.19)/ 3.218);
            rtcmode = RTC_MODE_TYP::RTC_RX_END;
            break;
        case RTC_MODE_TYP::RTC_RX_END:
            //タイムアップで異常を検出する場合の処理
            break;
        }
    }else if( mainmode == MD_SET){
        SetSubMode();
    }
}
/*** モード切替処理  ***/
void ModeCng(){

    if( difilt.di1 == 0 ){
        if( timbtn1 == TIME_OFF ){
            timbtn1 = MODE_CNG;
        }
    }else{
        timbtn1 = TIME_OFF;
        btn1hold = false; 
    }

    if( timbtn1 == TIME_UP ){ //2秒経過したか
        timbtn1 = TIME_OFF;
        if( mainmode == MD_DATE ){
            mainmode = MD_SET;
            submode = SET_YEAR;
            setdata = nowdata;
            lcd.clear();
            lcd.setCursor(0, 0); //1段目の左端にカーソル
            lcd.print(disptbl[0]); //1段目の文字列
            btn1hold = true;
        }else if(mainmode == MD_SET){
            mainmode = MD_DATE;
            RX8900.setDateTime(&setdata);
        }
    }
}
/*** サブモード(時刻設定)処理  ***/
void SetSubMode(){
    char strbuf[2];

    if( btn1hold == false ){
        switch(submode){
        case SET_YEAR:
            lcd.setCursor(0, 0); //1段目の左端にカーソル
            lcd.print(disptbl[0]); //1段目の文字列
            lcd.setCursor(0, 1); //2段目の左端にカーソル
            lcd.print("20");
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            lcd.print("/");
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_MONTH;
            }
            break;
        case SET_MONTH:
            lcd.setCursor(5, 1); //2段目の左端にカーソル
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            lcd.print("/");
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_DAY;
            }  
            break;
        case SET_DAY:
            lcd.setCursor(8, 1); //2段目の左端にカーソル
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            lcd.print(" ");
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_WEEK;
            }    
            break;
        case SET_WEEK:
            lcd.setCursor(11, 1); //2段目の左端にカーソル
       
            switch(SetDate(submode)){
            case SUN:
                lcd.print(" SUN");
                break;
            case MON:
                lcd.print(" MON");
                break;
            case TUE:
                lcd.print(" TUE");
                break;
            case WED:
                lcd.print(" WED");
                break;
            case THU:
                lcd.print(" THU");
                break;
            case FRI:
                lcd.print(" FRI");
                break;
            case SAT:
                lcd.print(" SAT");
                break;
            }
        
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_HOUR;
                lcd.clear();
            }  
            break;
        case SET_HOUR:
            lcd.setCursor(0, 0); //1段目の左端にカーソル
            lcd.print(disptbl[1]); //1段目の文字列
            lcd.setCursor(0, 1); //2段目の左端にカーソル
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            lcd.print(":");
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_MINUTE;
            }  
            break;
        case SET_MINUTE:
            lcd.setCursor(3, 1); //2段目の左端にカーソル
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            lcd.print(":");
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_SECOND;
            }   
            break;
        case SET_SECOND:
            lcd.setCursor(6, 1); //2段目の左端にカーソル
            sprintf(strbuf,"%02d",SetDate(submode)); //文字を変換2桁の10進数
            lcd.print(strbuf);
            if( difilt.di1 == 0 ){
                btn1hold = true;
                submode = SET_YEAR;
                lcd.clear();
            }   
            break;
        }
    }
}
/*** 時刻設定カウント処理  ***/
uint16_t SetDate(SET_MODE date){
    uint16_t ret = 0;
    uint16_t ycnt;
    uint8_t dcnt;

    switch(date){
    case SET_YEAR:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.year > 99 ){
                    setdata.year = 0;
                }     
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( setdata.year == 0 ){
                    setdata.year = 99;
                }else{
                    --setdata.year;
                }    
            }
        }else{
            btn3hold = true;
        }
        ycnt = 2000 + setdata.year;
        if( (ycnt % 4) == 0 ){ //4で割り切れる
            if( (ycnt % 400)== 0){ //400で割り切れる
                uruflg = true;
            }else if( ( ycnt % 100 )== 0 ){ //100で割り切れる(平年)
                uruflg = false;
            }else{
                uruflg = true;
            }
        }else{
            uruflg = false;
        }
        ret = setdata.year;
        break;
    case SET_MONTH:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.month > 12 ){
                    setdata.month = 1;
                }   
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( --setdata.month == 0 ){
                    setdata.month = 12;
                }    
            }
        }else{
            btn3hold = true;
        }
        ret = setdata.month;
        break;
    case SET_DAY:
        switch(setdata.month){
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            dcnt = daymax[3];
            break;
        case 4:
        case 6:
        case 9:
        case 11:
            dcnt = daymax[2];
            break;
        case 2:
            if(uruflg ){
                dcnt = daymax[1];
            }else{
                dcnt = daymax[0];
            }
            break;
        }

        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.day > dcnt ){
                    setdata.day = 1;
                }  
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( --setdata.day == 0 ){
                    setdata.day = dcnt;
                }   
            }
        }else{
            btn3hold = true;
        }
        ret = setdata.day;
        break;
    case SET_WEEK:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++wcnt > 6 ){
                    wcnt = 0;
                }  
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( --wcnt < 0 ){
                    wcnt = 6;
                }   
            }
        }else{
            btn3hold = true;
        }
     
        setdata.week = ( 1 << wcnt);
        ret = setdata.week;
        break;
    case SET_HOUR:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.hour > 23 ){
                    setdata.hour = 0;
                }  
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( setdata.hour == 0 ){
                    setdata.hour = 23;
                }else{
                    --setdata.hour;
                }  
            }
        }else{
            btn3hold = true;
        }
        ret = setdata.hour;
        break;
    case SET_MINUTE:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.minute > 59 ){
                    setdata.minute = 0;
                }  
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( setdata.minute == 0 ){
                    setdata.minute = 59;
                }else{
                    --setdata.minute;
                } 
            }
        }else{
            btn3hold = true;
        }  
        ret = setdata.minute;
        break;
    case SET_SECOND:
        if( difilt.di2 == 0){//カウント+
            if( btn2hold ){
                btn2hold = false;
                if( ++setdata.second > 59 ){
                    setdata.second = 0;
                } 
            }
        }else{
            btn2hold = true;
        }
     
        if( difilt.di3 == 0){//カウント-
            if( btn3hold ){
                btn3hold = false;
                if( setdata.second == 0 ){
                    setdata.second = 59;
                }else{
                    --setdata.second;
                }
            }
        }else{
            btn3hold = true;
        }
        ret = setdata.second;
        break;
    }
    return ret;
}
/* callback function add */
void TimerCnt(){
    ++cnt10ms;
}
/* Timer Management function add */
void mainTimer(){

    if( cnt10ms >= BASE_CNT ){ //10msごとにここに遷移する
        cnt10ms -=BASE_CNT;
    
        if( timdifilt > TIME_UP ){
            timdifilt--;
        }

        if( timbtn1 > TIME_UP ){
            timbtn1--;
        }

        if( timrtc > TIME_UP ){
            timrtc--;
        }
    }
}
/* DiFilter function add */
void DiFilter(){

    if( timdifilt == TIME_UP ){
        difilt.buf[DI1][difilt.wp] = digitalRead(PIN_DI1);
        difilt.buf[DI2][difilt.wp] = digitalRead(PIN_DI2);
        difilt.buf[DI3][difilt.wp] = digitalRead(PIN_DI3);
    
        if( difilt.buf[DI1][0] == difilt.buf[DI1][1] &&
            difilt.buf[DI1][1] == difilt.buf[DI1][2] &&
            difilt.buf[DI1][2] == difilt.buf[DI1][3] ){ //4回一致を確認
            difilt.di1 = difilt.buf[DI1][0];
        }
    
        if( difilt.buf[DI2][0] == difilt.buf[DI2][1] &&
            difilt.buf[DI2][1] == difilt.buf[DI2][2] &&
            difilt.buf[DI2][2] == difilt.buf[DI2][3] ){ //4回一致を確認
            difilt.di2 = difilt.buf[DI2][0];
        }
        
        if( difilt.buf[DI3][0] == difilt.buf[DI3][1] &&
            difilt.buf[DI3][1] == difilt.buf[DI3][2] &&
            difilt.buf[DI3][2] == difilt.buf[DI3][3] ){ //4回一致を確認
            difilt.di3 = difilt.buf[DI3][0];
        }

        if( ++difilt.wp >= DI_FILT_MAX ){
            difilt.wp = 0;
        }

        timdifilt = FILT_MIN;
    }
}
/*** GPSモジュール初期化  ***/
void GpsInit(){
    uint8_t i;
  
    pinMode( PIN_DI4, INPUT_PULLUP ); //1pps用のDI
    mySerial.begin(9600);

    //ホットスタートによるリスタートを実施(101)
    for( i=0; i < sizeof(buf101);i++ ){ 
        mySerial.write(buf101[i]);
    }  
    mySerial.write(0x0D);
    mySerial.write(0x0A);

    //GPSからの出力を変更(314)
    Tx314.header = '$';
    
    for( i=0; i < sizeof(buf314);i++ ){
        Tx314.dat[i] = buf314[i];
    }  

    Tx314.endchar = '*';
    sum = sumbcc(&Tx314.dat[0],sizeof(Tx314.dat)); //チェックサムの計算
    sum_h =( sum >> 4 );
    Tx314.sum_h = dectoask(sum_h);
    sum_l = sum & 0x0F;
    Tx314.sum_l = dectoask(sum_l);
    Tx314.cr = 0x0D;
    Tx314.lf = 0x0A;
    
    mySerial.write( &Tx314.header, sizeof(Tx314)); //314フォーマット送出
}
/* checksum(EOR) function add */
uint8_t sumbcc( uint8_t *adrs, uint8_t sz ){
    uint8_t bcc=0;
    uint8_t i;
  
    for( i=0; i < sz;i++ ){
        bcc ^= *adrs;
        ++adrs;
    }
    return bcc;
}
/*文字コード変換 10進数→16進数 function add */
uint8_t dectoask(uint8_t dec){
    uint8_t ret = dec;

    if( dec < 10 ){
        ret = dec + 0x30;
    }else if(dec >= 10 && dec < 16){
        ret = (dec % 10) + 0x41;
    }
    return ret;
}
/*** GPSメイン処理  ***/
void GpsMain(){
  
    if( digitalRead(PIN_DI4) == 0 ){
        gpsok = true;
        timgpswait = GPS_WAIT;
    }

    if( timgpswait == TIME_UP ){ //1ppsが規定時間途絶えた
        timgpswait = TIME_OFF;
        gpsok = false;
    }

    while( mySerial.available()){
        rxgps.buf[rxgps.wp] = mySerial.read();

        if( ++rxgps.wp >= RING_SZ ){ //次にデータを入れる場所を更新
            rxgps.wp = 0;
        }
    }

    RxDataChk();
}
/* RX function add */
void RxDataChk(){
    int rxsz;
    uint16_t sz;
    uint16_t allsz;
    uint8_t *adrs;
    uint16_t rp = rxgps.rp;
    uint8_t sum;
    bool flg;

    rxsz = rxgps.wp - rxgps.rp; //受信データ数の算出
    
    if( rxsz < 0 ){
        rxsz = rxsz + RING_SZ;
    }

    if( rxsz >= 1 ){
        if( rxgps.buf[rp] == '$' ){
            flg = false;
            for(uint16_t i=0; i< rxsz; i++ ){
                if( rxgps.buf[rp] == '*' ){
                    flg = true;
                    sz = i+1;
                    allsz = sz + 4;
                }
                if(++rp >= RING_SZ ){
                    rp = 0;
                }
            }
            if( rxsz >= allsz && flg){
                adrs = &RxData[0];
                for(uint16_t i=0; i< allsz; i++ ){
                    *adrs = rxgps.buf[rxgps.rp];
                    adrs++;
                    if(++rxgps.rp >= RING_SZ ){
                        rxgps.rp = 0;
                    }
                }
                sum = sumbcc(&RxData[1], sz-2);
                sum_h =( sum >> 4 );
                sum_h = dectoask(sum_h);
                sum_l = sum & 0x0F;
                sum_l = dectoask(sum_l);

                if( RxData[sz] == sum_h && RxData[sz+1] == sum_l ){
                    //チェックサムで受け入れた後の処理
                    ControlSet();
                    for(uint16_t i=0; i< allsz; i++ ){
                    Serial.write(RxData[i]);
                    }
                }else{
                    Serial.write("NG");
                }
            }
         }else{
              if(++rxgps.rp >= RING_SZ ){
                  rxgps.rp = 0;
              }    
         }
    }
}
/* GPZDA data-lcdset function add */
void ControlSet(){
    uint8_t hour_now;

    if( RxData[0] =='$' && RxData[1] =='G'
     && RxData[2] =='P' && RxData[3] =='Z'
     && RxData[4] =='D' && RxData[5] =='A' ){

        if( gpsok ){ //GPS測位できているかの判定がOKなら以下の処理
            hour_now = charToDecimal(RxData[7])*10 + charToDecimal(RxData[8]);

            if( hour_now < 15 ){
                setdata.year = charToDecimal(RxData[26])*10 + charToDecimal(RxData[27]);
                setdata.month = charToDecimal(RxData[21])*10 + charToDecimal(RxData[22]);
                setdata.day = charToDecimal(RxData[18])*10 + charToDecimal(RxData[19]);
                setdata.hour = charToDecimal(RxData[7])*10 + charToDecimal(RxData[8])+9;
                setdata.minute = charToDecimal(RxData[9])*10 + charToDecimal(RxData[10]);
                setdata.second = charToDecimal(RxData[11])*10 + charToDecimal(RxData[12]);
                setdata.week = ( 1 << WeekSet(setdata.year));
                smin = setdata.minute;
      
                if(smin ^ oldsmin ){
                    RX8900.setDateTime(&setdata);
                    timrtc = TIME_UP; //GPSの時間と同期をとる
                }
                oldsmin = smin;
            }
        }
    }else{ //ZPA以外であれば314電文が受け付けられていないので受け付けるまで繰り返す
        if( RxData[5] =='0' && RxData[6] =='0'
         && RxData[7] =='1' && RxData[9] =='3'
         && RxData[10] =='1' && RxData[11] == '4'
         && RxData[13] == '3'){
            Serial.println("314 ok");
        }else{
            Serial.println("314 retry");
            mySerial.write( &Tx314.header, sizeof(Tx314)); //314フォーマット送出
        }
    }
}
/* 文字コードを10進数に変換 */
int8_t charToDecimal(uint8_t chr){
    int8_t ret;

    ret = chr - 0x30;
    if( ret < 0){
        ret = 0;
    }
    return ret;
}

uint8_t WeekSet(uint8_t y ){
    uint8_t week;
    uint8_t y_1;
    uint8_t bb;
    uint8_t cc;
    uint16_t summonth = 0;
    uint16_t sumyear = 0;
    uint8_t uru = 0;

    y_1 = y - 20; //基準年(20:2020)からの経過年
    bb = y_1 / 4; //4年経過したら商として表示
    cc = y_1 % 4; //0-3年までの経過年数

    if( cc == 0 ){ //うるう年の場合は0
        uru = 1;
    }
  
    for(uint8_t i=0; i< setdata.month; i++ ){ //該当年の1月からの経過日数(月ベース)
        summonth += monthtype[uru][i];  
    }

    for(uint8_t i=0; i< cc; i++ ){ //0-3年経過まで経過日数(年ベース)
        sumyear += yearplus[i];  
    }
  
    sumday = YEAR4CNT * bb + sumyear + summonth + setdata.day -1; //基準日からの経過日数
    week = sumday % 7; //基準曜日(WED)からの差
    week +=3; //RTCモジュールではWEDは3回シフトなので+3する
    if( week > 6 ){ //+3して6より大きい場合は週をまたぐので1週間分(-7)を引く
        week =- 7;
    }
    return week;
}

GPSモジュールから取得した時刻をtime.hのmktime()で換算しRTCモジュールに書き込む方法の方が簡単ですがうるう年の計算やWeekを算出する考え方は参考になる部分も大きいと思います。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

PR:テックキャンプエンジニア転職

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました