610tのアイコン画像
610t 2024年12月31日作成 (2025年01月01日更新) © MIT
製作品 製作品 閲覧数 200
610t 2024年12月31日作成 (2025年01月01日更新) © MIT 製作品 製作品 閲覧数 200

SpreM5GPSense: SPRESENSEとM5Stackで作るGPSシステム

SpreM5GPSense: SPRESENSEとM5Stackで作るGPSシステム

本プロジェクトは、2024年 SPRESENSE™ 活用コンテストのために作成されました。

はじめに

SpreM5GPSense(すーぱーえむふぁいぶじーぴーせんす)は、SPRESENSEのGPSデータをM5Stackのディスプレイ上に表示するGPS viewerおよびSDカードにGPS情報を記録する loggerです。

登場人物

今回、利用したのは、以下のような技術/デバイスです。

SPRESENSE

SPRESENSEは、Sonyの開発した高性能のマイコンボードです。
メインボード単体で、GPS情報が取得できたり、高性能の音の入出力、AI機能など色々なことができるようになっています。
マルチコアも利用可能で、全部で6つのコアを利用して、高い性能が求められる構成を取ることが可能です。

今回は、GPSによる情報をSPRESENSEから取得し、シリアル2経由でM5Stackにデータを送ります。

M5Stack

M5Stackファミリー

M5Stackは、オールインワンの使いやすいマイコンです。
今回利用したのはM5Stack Core2で、GPS情報を画面上に表示するために利用しました。

今回は、SPRESENSEと接続するために、PlusモジュールやGrove-pinケーブルなどを利用していますが、M5StackのMBUSからシリアル2の信号が出ているため、これらは無くても動作させることが可能です。
MBUSのシリアル2とSPRESENSEのシリアル2を繋いで下さい。

システム構成

ここでは、システムについて説明していきます。

今回のシステムは、できるだけシンプルにGPS viewer/loggerを構成することを目標にしています。

部品

システムで利用した部品は、以下の通りです。

名称 SKU 用途
SPRESENSEメインボード CXD5602PWBMAIN1 GPSデータ取得
M5Stack Core2 M5STACK-K010-V11 SPRESENSEからのデータを表示
M5Stack Module Plus M5STACK-PLUSEM SPRESENSEとCore2の接続
GROVE - 4ピン - ジャンパオスケーブル SEEED-110990210 SPRESENSEとM5Stackの接続
micro SDカード - ロガー動作させる時のデータ記録用
モバイルバッテリー - 電源供給用:低電流対応版が必要

設計図

システム構成図

システム構成イメージ

シリアル2はM5Stackのシリアル2と接続されており、データのやり取りを行います。

ソースコード

ソースコードは全て、github https://github.com/610t/SpreM5GPSense/ で公開しています。

SPRESENSソースコード

SPRESENSEでは、GPSデータを取得し、その値をシリアル2経由で出力するようになっています。
GPSデータの取得に関しては、スケッチ例のgnss.inoを参考にして作成しました。

SPRESENSE.ino

#include <GNSS.h> #define STRING_BUFFER_SIZE 128 #define RESTART_CYCLE (60 * 5) // Every 5min static SpGnss Gnss; void setup() { int result; Serial.begin(115200); Serial2.begin(115200); // Connect to M5Stack via Serial2. ledOn(PIN_LED0); // Turn on LED0 to indicate initialize Gnss.setDebugMode(PrintInfo); // Set Debug mode to Info if (Gnss.begin() != 0) { Serial.println("Gnss begin error!!"); ledOn(PIN_LED1); exit(0); } // Select all GPS mode. Gnss.select(GPS); Gnss.select(GLONASS); Gnss.select(QZ_L1CA); // Start positioning if (Gnss.start(COLD_START) != 0) { Serial.println("Gnss start error!!"); ledOn(PIN_LED2); exit(0); } ledOff(PIN_LED0); /// Turn off LED0 :Setup done. } // Print received data Serial for debug and Serial2 for M5Stack static void print_with_debug(char *strbuf) { Serial.print(strbuf); Serial2.print(strbuf); } static void print_pos(SpNavData *pNavData) { char StringBuffer[STRING_BUFFER_SIZE]; // print date & time snprintf(StringBuffer, STRING_BUFFER_SIZE, "Date:%04d/%02d/%02d\n", pNavData-> time.year, pNavData->time.month, pNavData->time.day); print_with_debug(StringBuffer); snprintf(StringBuffer, STRING_BUFFER_SIZE, "Time:%02d%02d%02d.%02d\n", pNavDat a->time.hour, pNavData->time.minute, pNavData->time.sec, int(pNavData->time.usec / 10000)); print_with_debug(StringBuffer); // print satellites count snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSat:%2d\n", pNavData->numSatellites); print_with_debug(StringBuffer); snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSatCalc:%2d\n", pNavData->numSatellitesCalcPos); print_with_debug(StringBuffer); // HDOP snprintf(StringBuffer, STRING_BUFFER_SIZE, "HDOP:%.1f\n", pNavData->hdop); print_with_debug(StringBuffer); // altitude snprintf(StringBuffer, STRING_BUFFER_SIZE, "alt:%.1f\n", pNavData->altitude); print_with_debug(StringBuffer); // print position data if (pNavData->posFixMode == FixInvalid) { print_with_debug("Fix:No-Fix\n"); } else { print_with_debug("Fix:Fix\n"); } if (pNavData->posDataExist == 0) { Serial.print("Post:No Position\n"); } else { snprintf(StringBuffer, STRING_BUFFER_SIZE, "Lat:%f\nLon:%f\n", pNavData->latitude, pNavData->longitude); print_with_debug(StringBuffer); } } static void print_condition(SpNavData *pNavData) { char StringBuffer[STRING_BUFFER_SIZE]; unsigned long cnt; // Print satellite count. snprintf(StringBuffer, STRING_BUFFER_SIZE, "numSat:%2d\n", pNavData->numSatellites); print_with_debug(StringBuffer); for (cnt = 0; cnt < pNavData->numSatellites; cnt++) { const char *pType = "GPS"; SpSatelliteType sattype = pNavData->getSatelliteType(cnt); // Get print conditions. unsigned long Id = pNavData->getSatelliteId(cnt); unsigned long Elv = pNavData->getSatelliteElevation(cnt); unsigned long Azm = pNavData->getSatelliteAzimuth(cnt); float sigLevel = pNavData->getSatelliteSignalLevel(cnt); // Print satellite condition. snprintf(StringBuffer, STRING_BUFFER_SIZE, "[%2ld] Type:%s, Id:%2ld, Elv:%2ld, Azm:%3ld, CN0:", cnt, pType, Id, Elv, Azm); Serial.print(StringBuffer); Serial.println(sigLevel, 6); } } void loop() { static int LoopCount = 0; static int LastPrintMin = 0; // Check update. if (Gnss.waitUpdate(-1)) { // Get NaviData. SpNavData NavData; Gnss.getNavData(&NavData); // Print satellite information to Serial every minute. if (NavData.time.minute != LastPrintMin) { print_condition(&NavData); LastPrintMin = NavData.time.minute; } // Send position information via Serial2 print_pos(&NavData); } else { // Not update. Serial.println("data not update"); } // Check loop count. LoopCount++; if (LoopCount >= RESTART_CYCLE) { int error_flag = 0; // Restart GNSS. if (Gnss.stop() != 0) { Serial.println("Gnss stop error!!"); error_flag = 1; } else if (Gnss.end() != 0) { Serial.println("Gnss end error!!"); error_flag = 1; } else { Serial.println("Gnss stop OK."); } if (Gnss.begin() != 0) { Serial.println("Gnss begin error!!"); error_flag = 1; } else if (Gnss.start(HOT_START) != 0) { Serial.println("Gnss start error!!"); error_flag = 1; } else { Serial.println("Gnss restart OK."); } // If error on LED3 and halt. if (error_flag == 1) { ledOn(PIN_LED3); exit(0); } LoopCount = 0; } }

M5Stackソースコード

M5Stackのソースコードは、以下の通りです。

viewerのみのコード

viewerだけのコードは、以下のようにとてもシンプルになります。

M5Unifiedを利用することで、M5Stackファミリーの様々な機種に対応できるだけではなく、シリアル2で受け取ったデータをディスプレイに簡単に表示できるようになっています。

M5Stack_viewer.ino

#include <M5Unified.h> void setup() { Serial.begin(115200); // for debug output Serial2.begin(115200); // get data from SPRESENSE M5.begin(); M5.setLogDisplayIndex(0); M5.Display.setTextScroll(true); M5.Lcd.setTextSize(2); } void loop() { char buf[1024] = { 0 }; int av = Serial2.available(); if (av > 0) { Serial2.readBytes(buf, av); M5.Log.printf("%s\n", buf); } }

loggerを含むコード

以下のコードは、SDへのログを取得できるようにしたものです。

ボタンAでログが開始され、ボタンBでログが停止されます。

ログのフォーマットは、NMEA形式にしているつもりなのですが、現状ではうまく変換アプリケーションで変換できないものになっています。
原因がわかり次第直したいと思っています。

M5Stack.ino

/* Function CoordinateToString() and the logic of caluclating GPGGA checksum is based on gnss_nmea.cpp. See license below. */ /* * gnss_nmea.cpp - NMEA's GGA sentence * Copyright 2017 Sony Semiconductor Solutions Corporation * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include <SD.h> #include <M5Unified.h> bool logger_mode = false; bool sd_exist = false; bool data_update = false; // Is data updated? const static char gps_log_filename[] = "/GPS/gps_log.nmea"; #define CORIDNATE_TYPE_LATITUDE 0 /**< Coordinate type latitude */ #define CORIDNATE_TYPE_LONGITUDE 1 /**< Coordinate type longitude */ static void CoordinateToString(char *pBuffer, int length, double Coordinate, unsigned int cordinate_type) { double tmp; int Degree; int Minute; int Minute2; char direction; unsigned char fixeddig; const static struct { unsigned char fixeddigit; char dir[2]; } CordInfo[] = { { .fixeddigit = 2, .dir = { 'N', 'S' } }, { .fixeddigit = 3, .dir = { 'E', 'W' } }, }; if (cordinate_type > CORIDNATE_TYPE_LONGITUDE) { snprintf(pBuffer, length, ",,"); return; } if (Coordinate >= 0.0) { tmp = Coordinate; direction = CordInfo[cordinate_type].dir[0]; } else { tmp = -Coordinate; direction = CordInfo[cordinate_type].dir[1]; } fixeddig = CordInfo[cordinate_type].fixeddigit; Degree = (int)tmp; tmp = (tmp - (double)Degree) * 60 + 0.00005; Minute = (int)tmp; tmp = (tmp - (double)Minute) * 10000; Minute2 = (int)tmp; snprintf(pBuffer, length, "%0*d%02d.%07d,%c", fixeddig, Degree, Minute, Minute2, direction); } void setup() { Serial.begin(115200); // for debug output Serial2.begin(115200); // get data from SPRESENSE M5.begin(); M5.setLogDisplayIndex(0); M5.Display.setTextScroll(true); M5.Lcd.setTextSize(2); M5.Log.printf("SpreM5GPSense start!!\n"); #define MAX_SD_WAIT 5 // Initialize SD // If SD can't find MAX_SD_WAIT, run viewer only mode. int i; for (i = 0; i < MAX_SD_WAIT; i++) { if (SD.begin(GPIO_NUM_4, SPI, 15000000)) { break; } M5.Log.printf("SD Wait...\n"); delay(500); } if (i != MAX_SD_WAIT) { M5.Log.printf("* Found SD\n"); sd_exist = true; if (!SD.exists("/GPS")) { SD.mkdir("/GPS"); } } else { M5.Log.printf("* Run viewer only mode\n"); Serial2.read(); // Discard all incoming data. sd_exist = false; } } String getStrValue(String data, String pattern) { data.replace(pattern.c_str(), ""); return (data); } float getFloatValue(String data, String pattern) { data.replace(pattern.c_str(), ""); return (data.toFloat()); } int getIntValue(String data, String pattern) { data.replace(pattern.c_str(), ""); return (data.toInt()); } void loop() { M5.update(); // logger mode on/off if (M5.BtnA.wasClicked() && logger_mode == false) { logger_mode = true; M5.Power.setLed(255); M5.Log.printf("* Logger mode on.\n"); } else if (M5.BtnB.wasClicked() && logger_mode == true) { logger_mode = false; M5.Power.setLed(0); M5.Log.printf("* Logger mode off.\n"); } // GPS data String date, time; int numSat, numSatCalc; float lat, lon; float hdop; float alt; bool fix_state; int av = Serial2.available(); if (av) { M5.Lcd.clear(); M5.Lcd.setCursor(0, 0); } while (av > 0) { String line = Serial2.readStringUntil('\n'); av = Serial2.available(); M5.Log.printf("%s\n", line.c_str()); // Convert to NMEA GGA format if (line.startsWith("Date:")) { date = getStrValue(line, "Date:"); } else if (line.startsWith("Time:")) { time = getStrValue(line, "Time:"); } else if (line.startsWith("numSat:")) { numSat = getIntValue(line, "numSat:"); } else if (line.startsWith("numSatCalc:")) { numSatCalc = getIntValue(line, "numSatCalc:"); } else if (line.startsWith("Lat:")) { lat = getFloatValue(line, "Lat:"); } else if (line.startsWith("Lon:")) { lon = getFloatValue(line, "Lon:"); } else if (line.startsWith("alt:")) { alt = getFloatValue(line, "alt:"); } else if (line.startsWith("HDOP:")) { hdop = getFloatValue(line, "HDOP:"); } else if (line.startsWith("Fix:")) { String fix = getStrValue(line, "Fix:"); if (fix.startsWith("Fix")) { fix_state = true; } else { fix_state = false; } } data_update = true; } // Log to SD if (data_update) { data_update = false; if (logger_mode && sd_exist) { // Write out NMEA GGA data to SD if (fix_state) { #define STRING_BUFFER_SIZE 1024 char StringBuffer[STRING_BUFFER_SIZE]; char latStr[STRING_BUFFER_SIZE]; char lonStr[STRING_BUFFER_SIZE]; CoordinateToString(latStr, STRING_BUFFER_SIZE, lat, CORIDNATE_TYPE_LATITUDE); CoordinateToString(lonStr, STRING_BUFFER_SIZE, lon, CORIDNATE_TYPE_LONGITUDE); snprintf(StringBuffer, STRING_BUFFER_SIZE, "$GPGGA,%s,%s,%s,1,%02d,%.1f,%.1f,M,34.05,M,1.0,512*", time.c_str(), latStr, lonStr, numSatCalc, hdop, alt); String gga = StringBuffer; // Calculate checksum: based on gnss_nmea.cpp. unsigned short CheckSum = 0; { int cnt; const char *pStrDest = gga.c_str(); /* Calculate checksum as xor of characters. */ for (cnt = 1; pStrDest[cnt] != 0x00; cnt++) { CheckSum = CheckSum ^ pStrDest[cnt]; } } snprintf(StringBuffer, STRING_BUFFER_SIZE, "%02X\r\n", CheckSum); gga += StringBuffer; M5.Log.printf(gga.c_str()); // Output to SD File GPSFile = SD.open(gps_log_filename, FILE_APPEND); GPSFile.printf(gga.c_str()); GPSFile.close(); } } } }

動作の様子

実際の動作の様子を以下の動画に示します。

ここに動画が表示されます

おわりに

SPRESENSEとM5Stackを使って、GPSのviewerとloggerを作ってみました。

もちろん、SPRESENSEに拡張ボードを追加してSDを利用し、ディスプレイとしてILI9341などを追加することでGPS viewer/loggerは作れます。
しかし、M5StackのSDとディスプレイを流用することで、同じようなシステムを簡単に作ることができました。

このようなシステム構成にすると、他にもM5StackのWiFi/BLEなどの機能も利用可能になるため、作品の可能性が広がると思います。

Enjoy your SPRESENSE life with M5Stack!!

このプロジェクトについて

このプロジェクトの開発の一部は、大晦日ハッカソン2024で行いました。

1
ログインしてコメントを投稿する