編集履歴一覧に戻る
610tのアイコン画像

610t が 2025年01月01日14時56分00秒 に編集

コメント無し

本文の変更

本プロジェクトは、[2024年 SPRESENSE™ 活用コンテスト](https://elchika.com/promotion/spresense2024/)のために作成されました。 # はじめに SpreM5GPSense(すーぱーえむふぁいぶじーぴーせんす)は、SPRESENSEのGPSデータをM5Stackのディスプレイ上に表示するGPS viewerおよびSDカードにGPS情報を記録する loggerです。 # 登場人物 今回、利用したのは、以下のような技術/デバイスです。 ## SPRESENSE [SPRESENSE](https://developer.sony.com/ja/spresense)は、Sonyの開発した高性能のマイコンボードです。 メインボード単体で、GPS情報が取得できたり、高性能の音の入出力、AI機能など色々なことができるようになっています。 マルチコアも利用可能で、全部で6つのコアを利用して、高い性能が求められる構成を取ることが可能です。 今回は、GPSによる情報をSPRESENSEから取得し、シリアル2経由でM5Stackにデータを送ります。 ## M5Stack ![M5Stackファミリー](https://camo.elchika.com/244251a02a782ba8383ec6c0a3a8dbea66713531/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32633361343463322d626630612d343637382d626265332d3736393635396530393637322f37313739343530302d363132302d343562622d386130622d326634323763323762643734/) [M5Stack](https://m5stack.com/)は、オールインワンの使いやすいマイコンです。 今回利用したのは[M5Stack Core2](https://www.switch-science.com/products/9349)で、GPS情報を画面上に表示するために利用しました。 今回は、SPRESENSEと接続するために、PlusモジュールやGrove-pinケーブルなどを利用していますが、M5StackのMBUSからシリアル2の信号が出ているため、これらは無くても動作させることが可能です。 MBUSのシリアル2とSPRESENSEのシリアル2を繋いで下さい。 # システム構成 ここでは、システムについて説明していきます。 今回のシステムは、できるだけシンプルにGPS viewer/loggerを構成することを目標にしています。 ## 部品 システムで利用した部品は、以下の通りです。 |名称|SKU|用途| |--|--|--| |[SPRESENSEメインボード](https://developer.sony.com/ja/spresense/products/spresense-main-board)|CXD5602PWBMAIN1|GPSデータ取得| |[M5Stack Core2](https://www.switch-science.com/products/9349)|M5STACK-K010-V11|SPRESENSEからのデータを表示| |[M5Stack Module Plus](https://www.switch-science.com/products/5206)|M5STACK-PLUSEM|SPRESENSEとCore2の接続| |[GROVE - 4ピン - ジャンパオスケーブル](https://www.switch-science.com/products/6245)|SEEED-110990210|SPRESENSEとM5Stackの接続| |micro SDカード|-|ロガー動作させる時のデータ記録用| |モバイルバッテリー|-|電源供給用:低電流対応版が必要| ## 設計図 ![システム構成図](https://camo.elchika.com/3d203b1cc89f14657458f1f433916b6119850bc3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32633361343463322d626630612d343637382d626265332d3736393635396530393637322f38363932643363652d363934352d346536612d613334362d376564366339653135366538/) ![システム構成イメージ](https://camo.elchika.com/04dd715ebf4b05eab02346bd05be8580ddbf05a2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32633361343463322d626630612d343637382d626265332d3736393635396530393637322f30316166663264302d646564362d346461642d393035382d383264323930613064363038/) シリアル2はM5Stackのシリアル2と接続されており、データのやり取りを行います。 ## ソースコード ソースコードは全て、github https://github.com/610t/SpreM5GPSense/ で公開しています。 ### SPRESENSソースコード SPRESENSEでは、GPSデータを取得し、その値をシリアル2経由で出力するようになっています。 GPSデータの取得に関しては、スケッチ例の`gnss.ino`を参考にして作成しました。 ```c++: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](https://github.com/m5stack/M5Unified/)を利用することで、M5Stackファミリーの様々な機種に対応できるだけではなく、シリアル2で受け取ったデータをディスプレイに簡単に表示できるようになっています。 ```c++: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形式にしているつもりなのですが、現状ではうまく変換アプリケーションで変換できないものになっています。 原因がわかり次第直したいと思っています。 ```c++: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(); } } } } ```

+

# 動作の様子 実際の動作の様子を以下の動画に示します。 @[youtube](https://youtu.be/dG8vt3PUlZg)

# おわりに SPRESENSEとM5Stackを使って、GPSのviewerとloggerを作ってみました。

-

もちろん、SPRESENSEに拡張ボードを追加してSDを利用し、ディスプレイとしてILI9341などを追加することでGPS loggerは作れます。

+

もちろん、SPRESENSEに拡張ボードを追加してSDを利用し、ディスプレイとしてILI9341などを追加することでGPS viewer/loggerは作れます。

しかし、M5StackのSDとディスプレイを流用することで、同じようなシステムを簡単に作ることができました。 このようなシステム構成にすると、他にもM5StackのWiFi/BLEなどの機能も利用可能になるため、作品の可能性が広がると思います。 **Enjoy your SPRESENSE life with M5Stack!!** # このプロジェクトについて このプロジェクトの開発の一部は、[大晦日ハッカソン2024](https://omisoka-hackathon.connpass.com/event/340543/)で行いました。