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

組み込みエンジニア

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

GPSモジュールは現在地の情報や時刻情報などを衛星からの電波を受信してユーザーに通知するモジュールです。GPSモジュールのデータはシリアル通信を使って得られますが、1ppsと組み合わせることで測位後のデータと判断してデータを得られます。

秋月電子の「GPS受信機キット:AE-GYSFDMAXB」を使ってGPSモジュールから日付と時刻情報を取得してLCDに表示する方法を説明しています。

Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリーを使用して動作確認したことをまとめています。

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

GPSモジュールの使い方

GPSモジュールはArduinoのシリアル通信とGPSモジュールを配線するだけでデータを取得することができます。デフォルト設定では不要なデータも含まれてしまうため必要なデータだけが受信できるようにGPSモジュールの設定を行います。

GPSモジュールは秋月電子の「GPS受信機キット:AE-GYSFDMAXB」を使っているため下記リンクのデータシート(技術資料)を一部抜粋して説明します。

秋月電子ーGPS受信機キット:AE-GYSFDMAXB

GPSモジュールの電文(NMEAパケット)

GPSモジュールはメーカー問わず基本的にNMEAパケットフォーマットに従ってデータを送信するようになっています。NEMAパケットフォーマットは以下の通りです。

NMEAパケットフォーマット:GYSFDMAXB仕様書より抜粋
NMEAパケットフォーマット:GYSFDMAXB仕様書より抜粋

NMEAパケットは最後のCR・LFを除いて文字コード(アスキーコード)となります。各パラメータをまとめます。

パケット内容
Preamble1バイトの文字’$’
Talker ID4バイトの文字”PMTK”(メーカーによって異なる)
Packet Type3バイトの文字”000″から”999″(電文の番号)
Data Field任意の文字列で、Packet Typeによってデータ長が異なる。
設定項目が複数ある場合は”,”で区切る。
*1バイトの文字でData Fieldの終わり判定に使用する。
CHK1、CHK2Preambleと*までの間のチェックサム(EOR)値を2バイト文字で表現する。
CR、LF2バイトのバイナリデータCR:0x0D、LF:0x0Aとなる。
NMEAパケットの説明

GYSFDMAXBの仕様書ではCHK1、CHK2の説明にPreambleと*までのチェックサム値と記載されているため各バイトを加算して1バイトデータを文字列としてチェックサムとして計算してしまいそうですが、EORで計算する必要があります。

チェックサムが一致しない場合はGPSモジュール側で無効なパケットとして処理されてしまいます。

出力制御のフォーマット

GPSモジュールには各種データを出力する項目を選択する314パケットがあります。今回使用しているAE-GYSFDMAXBにおいてはDate Fieldは全部で19の設定項目がありますが、実装されている項目は以下の通りです。

種別内容設定番号
GPGLL位置情報(緯度、経度)を出力する。0
GPRMC位置情報と時刻(UTC時刻)、速度と方位を同時に出力する。1
GPVTG方位と速度を出力する。2
GPGGA位置情報(緯度、経度)、GPS測位状態、測位衛星数を出力する。3
GPGSAGPS衛星の使用衛星番号、各種DOP、動作モードを出力する。4
GPGSVGPS衛星の生成情報を出力する。5
GPZDA時刻(UTC時刻)と日付を出力する。17
GPSモジュール(GYSFDMAXB)に実装されている出力項目

GPSモジュールから日付と時刻を含む情報を取得したいので「314 PMTK_API_SET_NMEA_OUTPUT」の設定からGPZDA interval以外を出力しないように設定します。314フォーマットにおいて対象の番号の周期設定を変更することでインターバルのタイミングを任意に設定できます。

char buf314[]= "PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0";
//正式なフォーマットは$,*checksum,CR,LFを追加する

例のように設定番号の17番目以外のインターバルを0にすることでGPSモジュールからの情報を制限することができます。GPZDAにおいては1にしているため1秒周期でGPZDAが出力されます。他の項目についても出力したい場合は0~5までの任意のタイミングに設定します。

Arduinoをリセット状態にしてから314パケットを送出しても応答しないケースがあったためArduinoのSetup()の最初にGPSモジュールをリセットする処理を入れています。(リセットはリセットパケットの整合性が取れていれば必ず受け付けるようです。)

初期化処理で314パケットが受け付けられなかった場合は受信データにGPZDA以外のデータが来た場合に再度314パケットを送出するようにして受け付けるまで繰り返すようにしています。

リスタートには大きくホットスタートとコールドスタートがありますが、今回使用しているGPSモジュールにおいては4つの項目に分類されています。

種別説明
HOT START利用可能なデータを保持したままのリスタートする。
WARM STARTリスタート後エフェメリス(衛星の測位情報)が使用できない。
COLD STARTリスタート後、時間・位置・日付・エフェメリスのデータが使用できない。
FULL COLD STARTCOLD STARTよりもさらにシステム/ユーザーのコンフィグレーションデータをクリアする。
リスタートの種類

電源ON以降(モジュールは電源ON後で最大1500ms準備時間が必要)にGPSモジュールが取得したデータを無駄にせずにリスタートができるホットスタートを使用することで初期化を行い、314フォーマットによる出力制御設定を行います。

外部機器をマイコンから制御する場合、マイコン起動時に外部機器に対して初期化を行ってから動作させることが経験上多いです。

時間データの受け取りタイミング

GPSモジュールは衛星を測位する前からシリアル通信でデータを通知します。データが確定していない場合のデータで時刻や測位情報として採用してしまうと不確定な情報が表示されることになります。

GPSモジュールは測位が完了し正確なデータとして確定できた時1ppsピンからパルスを出力して通知する機能を持っています。1ppsピンの状態を監視してパルスが生成されていれば正常な情報であると判断してGPSモジュールの情報として処理するようにします。

void loop(){
  GpsMain();

  if( digitalRead(PIN_DI1) == 0 ){ //LOWアクティブなので0で判定
      gpsok = true; //GPSのデータが確定
      timgpswait = GPS_WAIT;
  }

  if( timgpswait == TIME_UP ){ //1ppsが規定時間途絶えた
      timgpswait = TIME_OFF;
      gpsok = false; //GPSのデータが未確定とする
  }
}
/*** RTCメイン処理  ***/
void GpsMain(){

  if( gpsok ){ //GPSのデータが確定していれば以下の処理
     //LCDに時刻を表示する
  }
}

GPSモジュールからの受信データの受け入れ

GPSモジュールから受信するデータもNMEAパケットに従っているためNMEAパケットの’$’から’*’までのデータを確認することでパケットタイプによるデータの管理ができます。

/* RX function add */
void RxDataChk(){

    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; //チェックサムと制御文字を含めた全体を受信するため+4
                }
               if(++rp >= RING_SZ ){
                   rp = 0;
              }
            }
            if( rxsz >= allsz && flg){
                //受信データを一時的に保管
                sum = sumbcc(&RxData[1], sz-2); //チェックサムの計算
          
                if( RxData[sz] == sum_h && RxData[sz+1] == sum_l ){
                    ControlSet(); //チェックサムで受け入れた後の処理
                }
        }else{
            if(++rxgps.rp >= RING_SZ ){
                rxgps.rp = 0;
            } 
        }
    }
}

データ長はGPSモジュールから出力される種別に応じて異なるため、データ長の確認を行ってから必要なデータ数のみ一時的に受け入れチェックサム(EOR)の計算を行い、異常がなければデータを受け入れる処理を行います。

void ControlSet(){

  if( RxData[0] =='$' && RxData[1] =='G'
   && RxData[2] =='P' && RxData[3] =='Z'
   && RxData[4] =='D' && RxData[5] =='A' ){
      //LCD表示用のデータを生成するなど必要な処理を追加
}

受け入れたデータのパケットタイプから必要なパケットのデータであるかの確認を行い対象の処理を行います。上記の例では「$GPZDA」のパケットであることを判断してLCD用のデータを生成しています。

位置情報を示すパケットに対してデータを作成する場合は「$GPGLL」のパケットであることを確認して処理を追加します。

動作確認

GPSモジュール動作確認の回路図
GPSモジュール動作確認の回路図

GPSモジュールからの受信データをLCDに表示するための回路図です。初期画面はGPS-TESTとVER1.00を表示しておき、GPSモジュールの時刻データが確定した後は時刻を表示するようにしています。すでに確定している場合は最初で2秒の表示としています。

GPSモジュールの時刻情報を表示
GPSモジュールの時刻情報を表示

LCDに時刻が表示されますが、GPSモジュールの時刻と日時は世界標準時が基準であるため日本での時間は世界標準時+9時間となります。

GPS受信機キット:AE-GYSFDMAXBのデータシートを確認すると330 PMTK_API_SET_DATUMを発行してTOKYO-Mに合わせると時刻調整できると考えていましたが、発行してもうまく時刻が補正されませんでした。他のAPIで補正することができるかもしれません。

以下は世界標準時から時刻のオフセットができていないことを前提とします。

世界標準時で15:00を超えたとき日本時間に換算すると24時を超えることになり日付も更新する必要があります。うるう年が絡んだ時など日本時間に換算しようとしたとき少し面倒なのが欠点だと言えます。

上記欠点を補うシステム構成としては世界標準時で00:00~14:59までの間でGPSモジュールから受信したデータをRTCモジュールに書き込んで置き日本時間に換算したときに日付を超える時間帯においてはRTCモジュールで管理する仕組みを作ると日本時間での管理が楽にできます。

GPSモジュールも簡易的なものでなく機能が充実しているモジュール(古野電気製:GT-87シリーズなど)があり日本時間に換算したデータが取得できるようになっている製品もあります。

GPSモジュールは正確な時刻と位置情報を取得できることであると思うので位置情報を取得してみると面白いと思います。今回は公開していませんが、GPGLLフォーマットで位置情報を取得して座標をMAP上で入力すると少し誤差がありました(約100m)が位置情報が取れていました。

ソースコード全体

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

#include <SoftwareSerial.h>
#include <LiquidCrystal.h>
#include <MsTimer2.h>

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define LCD_MAX 50
#define GPS_WAIT 500

#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 PIN_DI1 13
#define RING_SZ 256

const char buf101[]= "$PMTK101*32";
const char buf314[]= "PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0";

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(10, 11); // RX, TX
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4,LCD_D5, LCD_D6, LCD_D7);
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 cnt10ms;
int16_t timlcd = TIME_OFF;
int16_t timgpswait = TIME_OFF;
bool gpsok;

/*** Local function prototypes */
void TimerCnt();
void mainTimer();
uint8_t dectoask(uint8_t dec);
void RxDataChk();
uint8_t sumbcc( uint8_t *adrs, uint8_t sz );
void GpsMain();
void ControlSet();

void setup(){
    uint8_t i;
    uint8_t *adrs;

    pinMode( PIN_DI1, INPUT_PULLUP ); //1pps用のDI
    Serial.begin(9600);
    mySerial.begin(9600);

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

    //ホットスタートによるリスタートを実施(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フォーマット送出
  //Serial.write( &Tx314.header, sizeof(Tx314));

    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移
    MsTimer2::start();
    timlcd = LCD_MAX;
}

void loop(){

    mainTimer();
    GpsMain();

    if( digitalRead(PIN_DI1) == 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();
}

/* callback function add */
void TimerCnt(){
    ++cnt10ms;
}
/* Timer Management function add */
void mainTimer(){

    if( cnt10ms >= BASE_CNT ){ //10msごとにここに遷移する
        cnt10ms -=BASE_CNT;
    
        if( timlcd > TIME_UP ){
            timlcd--;
        }
    }
}
/*文字コード変換 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;
}
/* 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(){

    if( RxData[0] =='$' && RxData[1] =='G'
     && RxData[2] =='P' && RxData[3] =='Z'
     && RxData[4] =='D' && RxData[5] =='A' ){
        date[0] = RxData[24];
        date[1] = RxData[25];
        date[2] = RxData[26];
        date[3] = RxData[27];
        date[4] = '/';
        date[5] = RxData[21];
        date[6] = RxData[22];
        date[7] = '/';
        date[8] = RxData[18];
        date[9] = RxData[19];
        date[10]= RxData[7];
        date[11]= RxData[8];
        date[12]= ':';
        date[13]= RxData[9];
        date[14]= RxData[10];
        date[15]= ':';  
        date[16]= RxData[11];
        date[17]= RxData[12];
        Serial.write(&date[0], sizeof(date));
    }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フォーマット送出
        }
    }
}
/* 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;
}
/*** RTCメイン処理  ***/
void GpsMain(){

    if( gpsok ){
        if( timlcd == TIME_UP ){
            timlcd = LCD_MAX;
            lcd.setCursor(0, 0); //1段目の左端にカーソル
            lcd.write(&date[0],10);
            lcd.setCursor(0, 1); //2段目の左端にカーソル
            lcd.write(&date[10],8);
        }
    }
}

関連リンク

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

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

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

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

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

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

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