Raspberry Pi Pico 互換ボードで自作GPSロガーを作ってみた【RP2040】

Raspberry Pi Zero2 でGPSロガーを作ってきたgoldear@goldear820です。

Raspberry Pi Zero2では小型化に限界がある、OSが不安定になることがあるため、小型かつOSレスのWaveShare製 Raspberry Pi Pico互換ボード RP2040-Tinyを使って、GPSロガーを作ってみました。

試作機を4台ほど作って、ついに5台目でそこそこのものができたのでまとめておきます。

Raspberry Pi Zero2 W でGPSロガーを作ってみる - 第02回 実装・動作確認編
重い腰を上げて自作GPSロガー作成にやっとこ着手したgoldear@goldear820です。自宅環境で上手くGPSが受信できない、Raspberry Pi Zero WH が非力過ぎて無線でSSH接続すると反応が悪いなど、言い訳でしかあり...
スポンサーリンク

Raspberry Pi RP2040 について

Just a moment...

RaspberryPi財団が開発したマイコンチップです。Raspberry Piの名を掲げているだけあって、Pythonを使って(正確にはMicroPython)ソフトを実装することが可能です。

もうPICマイコン使ってGPSロガー作っちゃえばいいじゃんとか思いましたが、ここまでPythonで実装してきたので最後までPythonで実装していきます。

Zero2ではなくRP2040をGPSロガーに使用するメリットとしては
 1. OSレスなので電源投入後からGPS信号を受信処理できる
 2. 低消費電力なので、長時間駆動が可能
 3. (互換ボードだけだが、) GPIOポート数を減らして小型化されている
 4. 安くて入手性が良い
が挙げられます。

逆にRP2040のデメリットとしては
 1. MicroPythonはPython(いわゆるCPython)と同じコード記述はできない
 2. フラッシュメモリ容量を考慮したコード記述が必要
が挙げられます。

私がソフトウェア実装に不慣れということもありますが、結構苦しめられました。

回路図

以前まではパワーポイントを使って描いていましたが、今回はKiCadの回路エディタを使ってみました。

KiCad
A Cross Platform and Open Source PCB Design Suite

モジュールの組み合わせなので、特に面白みのない回路になっています。

補足として、USB電源から電流の逆流を防ぐため、DCDCコンバータ(U2)の後段にショットキーバリアダイオード(D2)を追加しています。そのためRP2040-Tiny(U1)の5Vポートに入力されるのは約4.7Vになります。

RP2040-Tiny(U1)内部のLDO(RT9013-33)で3.3Vを出力するためには最低でも3.8Vの入力が必要なので、今回は問題にはなりません。また周辺モジュールも3.3V駆動のため、5Vは必要としません。

ダイオードがない状態で、USB接続した時に電流が逆流してケーブルが溶けてしまったことがあります。。。保護回路って大事だなって感じました。。。

部品表

図番 メーカー 品名 購入先
U1 WaveShare RP2040-Tiny SwitchScience
U2 Strawberry Linux TPS61230A 昇圧型DC-DCコンバータモジュール(5V 2.5A) Strawberry Linux
U3 ACEIRMC Micro SD Card Adapter (SPI I/F) Amazon
U4 YIC GPS module GT-502MGG-N 秋月電子通商
U5 VKLSVAN 0.96inch OLED SSD1306 Amazon
SW1 ミヤマ電気 トグルスイッチ MS-600K-B (3A) 秋月電子通商
SW2,SW3 XiaMen JinBeili スライドスイッチ (0.1A) 秋月電子通商
C1 村田製作所 積層セラミックコンデンサ 470pF 50V 秋月電子通商
D1 OptoSupply 抵抗内蔵 3mm LED 秋月電子通商
D2 PANJIT INTERNATIONAL ショットキーバリアダイオード 45V2A 秋月電子通商
BT1 NLAセレクト 18650型 リチウム電池 直販サイト
無 BENECREAT 収納ケース Amazon

補足として、18650型リチウム電池(BT1)は過電流・過充電・過放電などから保護する回路を内蔵している電池、PSEを取得している電池、を使用してください。保護回路の内蔵されていない、いわゆる「生セル」は保護回路を外付けして使用するもので、今回の回路には適しません。

安いからと言ってAmazonで使えそうなものを購入したが、MicroSDカードを抜き差ししていたらコネクタがもげました・・・(袋のパッケージから取り出したら、すでにもげたものもあった)

抜き挿しのしやすさや、部品の頑丈さを考えたら秋月電子で販売しているヒロセのMicroSDカードDIP化基板をオススメします。なかなか入荷しないんですよねこれ。

【電子工作】左手デバイス用に自作キーボード Cannonball を組み立ててみた【キット組立】
映像編集や普段使い用に左手デバイスに興味を持ったgoldear@goldear820です。左手デバイスはニッチな世界のようで市販品の価格は少々割高に設定されています。左手デバイスはハードウェアの質はもちろんなのですが、キーを割り当てるソフト...

上記の安いmicroSDカードスロットを使用する場合は、以前紹介したエポキシ樹脂で固定するモゲマイクロ対策を施すのが有効です。

実装

Thonny, Python IDE for beginners

開発環境はthonnyを使って、MicroPythonで実装します。事前にthonnyの「パッケージ管理」から os, datetime, sdcard, ssd1306 をインストールしておいてください。

また micropyGPS という便利なライブラリもMicroPython対応しているので使用可能ですが、私自身が動作を理解しきれていないため今回は使用しません。使用したら記述量が一気に少なくなります。

GitHub - inmcm/micropyGPS: A Full Featured GPS NMEA-0183 sentence parser for use with Micropython and the PyBoard embedded platform
A Full Featured GPS NMEA-0183 sentence parser for use with Micropython and the PyBoard embedded platform - inmcm/micropy...

RP2040互換ボードであれば、# Config 箇所を修正することで他のボードや、他のポートを使用できます。

注意点として、SPIフラッシュにRaspberry Pi Pico用のUF2ファイルを書き込んでいるため、UARTのポート設定をする箇所はRaspberry Pi Pico のピンアサインで設定します。

###########################
# MicroPython
# for WaveShare RP2040-Tiny
# GPS: GT-502MGG-N
# UF2: RPI_PICO-20240105-v1.22.1.uf2
# Author:	goldear.net
# Date:		2024/02/24
# Rev:		02
############################

####################################################
# import Library
## common
import os
import time
import datetime
import gc
from machine import Pin
from machine import UART, I2C, SPI
## GPS
## OLED SSD1306
from ssd1306 import SSD1306_I2C
## SDCard
import sdcard


####################################################
# Config
## GPS
UART_ID    = 1
UART_RX    = 7 	# RPi Pico Pin Number 7 <-> GP5
UART_TX    = 6  # RPi Pico Pin Number 6 <-> GP4
BAUDRATE   = 9600
TIME_ZONE  = 9  # (UTC+TIME_ZONE->JST)
## OLED SSD1306
I2C_ID	 = 0
I2C_SDA  = 0		# GP0
I2C_SCL  = 1		# GP1
I2C_FREQ = 400000
DISP_W   = 128
DISP_H   = 64
OUTPUT_PATH = "/sd/"
## Panel Switch
SW0_GPIO = 2	# GP2
SW1_GPIO = 3	# GP3
## SDCard
SPI_ID  = 1
SPI_RX  = 12 # GP12
SPI_CS  = 13 # GP13
SPI_SCK = 14 # GP14
SPI_TX  = 15 # GP15


####################################################
# Create Instance
## GPS
uart = UART(UART_ID, BAUDRATE, UART_RX, UART_TX)
## OLED SSD1306
i2c  = I2C(I2C_ID, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=I2C_FREQ )
oled = SSD1306_I2C(DISP_W, DISP_H, i2c)
## Panel Switch
sw0  = Pin(SW0_GPIO, Pin.IN, Pin.PULL_UP)
sw1  = Pin(SW1_GPIO, Pin.IN, Pin.PULL_UP)
## SDCard
spi  = SPI(SPI_ID, sck=Pin(SPI_SCK), mosi=Pin(SPI_TX), miso=Pin(SPI_RX))
sd   = sdcard.SDCard(spi, Pin(SPI_CS))
### Mouns SDCard
os.mount(sd, "/sd")


####################################################
# SUB FUNCTION
## Init OLED
def clear_disp(disp):
    disp.fill(0)
    return        

####################################################
# MAIN FUNCTION
def main():

    # Init
    valid    = 0
    sentence = ""
    csv_buf  = []
    
    while True:
        buf = uart.readline()

        if buf != None:
            sentence += buf.decode("utf-8")
            
            if sentence[len(sentence)-1] == "\n":
                # Split sentence
                segments = sentence.split(",")
                ## Clear
                sentence = ""
        
                ## Check Valid in $GPRMC or GNRMC
                if segments[0] == "$GPRMC" or segments[0] == "$GNRMC":
                    val = segments[2]
                    
                    if val == "V":
                        valid = 0
                        gprmc_hour = str((int(segments[1][0:2]) + TIME_ZONE) % 24)
                        gprmc_min  = (segments[1][2:4])
                        gprmc_sec  = (segments[1][4:6])
                        
                        if sw0.value() == 0:
                            oled.poweron()
                            clear_disp(oled)
                            oled.text(f"Not Ready...", 7, 15)
                            oled.text(f"{gprmc_hour}:{gprmc_min}:{gprmc_sec}", 14, 30)
                            oled.show()
                        else:
                            oled.poweroff()
                            pass
                        
                        continue
                    else:
                        valid    = 1
                        dd   	 = int(segments[9][0:2])
                        MM   	 = int(segments[9][2:4])
                        yy   	 = int(f"20{segments[9][4:6]}")
                        
                ## Speed in $GPVTG or $GNVTG
                if (segments[0] == "$GPVTG" or segments[0] == "$GNVTG") and valid == 1:
                    speed = segments[7]
                    
                ## Data in $GPGGA or $GNGGA
                if (segments[0] == "$GPGGA" or segments[0] == "$GNGGA") and valid == 1:
                    hh = int(segments[1][0:2])
                    mm = int(segments[1][2:4])
                    ss = int(segments[1][4:6])
                    utc_datetime = datetime.datetime(yy, MM, dd, hh, mm, ss, tzinfo=datetime.timezone.utc)
                    ### Calc JST
                    jst_datetime = str(utc_datetime.astimezone(datetime.timezone(datetime.timedelta(hours=+9))))
                    csv_buf.append(jst_datetime[0:10] + "T" + jst_datetime[11:])
                    
                    ### Add latitude
                    lat = (segments[2][0:2] + "." + segments[2][2:4] \
                                                  + segments[2][5:])
                    csv_buf.append(segments[3])
                    csv_buf.append(lat) 
                    
                    ### Add longitude
                    lon = (segments[4][0:3] + "." + segments[4][3:5] \
                                                  + segments[4][6:])
                    csv_buf.append(segments[5])
                    csv_buf.append(lon)
                        
                    # Add elevation
                    ele = segments[9]
                    csv_buf.append(ele)

                    ### Add speed
                    csv_buf.append(speed)
                    # Add UTC
                    csv_buf.append(str(utc_datetime)[0:10] + "T" + str(utc_datetime)[11:])
                    
                    ### Store log
                    file_name = f"{OUTPUT_PATH}{jst_datetime[0:10]}.csv"
                    if sw1.value() == 0:
                        with open(file_name, "a", encoding="utf-8") as f:
                            s_csv_buf = ','.join([str(x) for x in csv_buf])
                            f.write(s_csv_buf + "\n")
                    else:
                        pass
                    
                    ### Show OLED
                    if sw0.value() == 0:
                        oled.poweron()
                        clear_disp(oled)
                        oled.text(f"{jst_datetime[0:10]}", 0, 0)
                        if sw1.value() == 0:
                            oled.text(f"{jst_datetime[11:19]} GET", 0, 10)
                        else:
                            oled.text(f"{jst_datetime[11:19]} WAIT", 0, 10)
                            oled.text(f"lat: {csv_buf[1]}{csv_buf[2]}", 0, 20)
                            oled.text(f"lon: {csv_buf[3]}{csv_buf[4]}", 0, 30)
                            oled.text(f"spd[km/h]: {csv_buf[6]}", 0, 40)
                            oled.text(f"ele[m]: {csv_buf[5]}", 0, 50)                          
                            oled.show()
                    else:
                        oled.poweroff()
                        pass
                    
                    ## Clear
                    csv_buf = []

        # Clear
        gc.collect()
    
                        
if __name__ == "__main__":
    main()

    """
    csv format
    [0]: JST "20yy-mm-ddTHHMMSS+09:00"
    [1]: "N"orthern or "S"outhern
    [2]: Latitude deg
    [3]: "E"ast or "W"est
    [4]: Longitude deg
    [5]: Elevation [m]
    [6]: Speed [km/h]
    [7]: UTC "20yy-mm-dd HHMMSS+00:00"

    """

動作確認

前回作成したZero2と遜色ない動作をすることを確認しました。試作機では006P(9V, 800mAh)を使っていたのですが、約6~7時間程度しか連続稼働しませんでした。

今回は18650型リチウム電池(3.7V, 3400mAh)を使用して、約12時間の連続稼働を確認しました。連続稼働後の電圧はまだまだ余力があり、おそらく24時間程度は連続稼働するのではないかと思います。(未検証)

CSV -> GPX 変換

Raspberry Pi Zero2 W でGPSロガーを作ってみる – 第03回 CSV to GPX 変換編
最近RaspberryPi沼にハマりかけているgoldear@goldear820です。前回、ようやくGPSロガーとして形になりましたが、記録したCSVファイルではGoogleMapにインポートすることができません。GPS情報をわざわざ記録...

今回はmicroSDカードにGPSログを保存しているので、同じディレクトリにCSV -> GPX変換のpythonファイルと、バッチファイルを置いておきます。(あらかじめWindowsでPython3が使用できることが前提ですが)

前回のコードとほとんど同じですが、複数ファイルを一度に変換できるように修正しています。

#!/usr/bin/env python3
# encoding: utf-8
# for RPi Pico W 
# GPS: GT-502MGG-N

## common
import os
import sys
from datetime import datetime as dt
import xml.etree.ElementTree as ET
import xml.dom.minidom as md
import re

args       = sys.argv
INPUT_CSV  = f"{args[1]}.csv"
OUTPUT_GPX = f"{args[1]}.gpx"
VAL_NONE   = "."

GPX_VER = "1.1"
CREATOR = "goldear - https://goldear.net"
XMLNS_XSD = "https://www.w3.org/2001/XMLSchema"
XMLNS_XSI = "https://www.w3.org/2001/XMLSchema-instance"
XSI_SL    = "http://www.topografix.com/GPX/1/1/gpx.xsd"

####################################
# SUB FUNCTION
def get_date(file):
    timestamp = os.path.getmtime(file)
    date = dt.fromtimestamp(timestamp)
    return date

def dmm2deg(dmm_lat, dmm_lon):
 
    # Convert DMM(Degree Minute) -> DEG(Degree)
    ## Latitude: decimal -90.0 <= value <=90.0
    d_lat       = dmm_lat[0:2]
    int_m_lat   = str(dmm_lat[3:5])
    sub_m_lat   = "." + str(dmm_lat[5:])
    float_m_lat = float(int_m_lat + sub_m_lat)
    m2d_lat     = float_m_lat/60
    lat         = float(d_lat) + m2d_lat
 
    deg_lat = str(lat)
 
    ## Longitude: decimal -180.0 <= value <=180.0
    d_lon       = dmm_lon[0:3]
    int_m_lon   = str(dmm_lon[4:6])
    sub_m_lon   = "." + str(dmm_lon[6:])
    float_m_lon = float(int_m_lon + sub_m_lon)
    m2d_lon     = float_m_lon/60
    lon         = float(d_lon) + m2d_lon
 
    deg_lon = str(lon)
 
    return deg_lat, deg_lon


####################################
# MAIN FUNCTION
def csv2gpx():
    ## Get time 
    date = get_date(INPUT_CSV)
    s_date = date.strftime("%y-%m-%d")
    s_time = date.strftime("%H:%M:%S")

    ## Create ROOT
    root = ET.Element("gpx")
    root.set("version", GPX_VER)
    root.set("creator", CREATOR)
    root.set("xmlns:xsd", XMLNS_XSD)
    root.set("xmlns:xsi", XMLNS_XSI)
    root.set("xsi:schemaLocation", XSI_SL)

    ## Set Time
    l_time = ET.SubElement(root, "time")
    l_time.text = str("20" + s_date + "T" + s_time + "+09:00")

    ## Create truck
    l_trk = ET.SubElement(root, "trk")

    ## Create truck segment data
    l_trkseg = ET.SubElement(l_trk, "trkseg")

    ## Read Log 1-line
    with open(INPUT_CSV, mode="r", encoding="utf-8") as f_csv:

        while True:
            l_gpslog = f_csv.readline()

            if not l_gpslog:
                break

            else:
                ### split
                ls_gpslog = re.split("[,*]",l_gpslog)

                if ls_gpslog[2] == VAL_NONE or ls_gpslog[4] == VAL_NONE:
                    continue

                ## Create truck point
                l_trkpt= ET.SubElement(l_trkseg, "trkpt")

                ## Convert DMM -> DEG
                lat, lon = dmm2deg(ls_gpslog[2], ls_gpslog[4])

                ## Set Lat & Lon
                l_trkpt.set("lat", lat[0:10] )
                l_trkpt.set("lon", lon[0:11] )

                ## Create Elevation
                l_ele = ET.SubElement(l_trkpt, "ele")
                l_ele.text = ls_gpslog[5]

                ## Create time
                l_tr_time = ET.SubElement(l_trkpt, "time")
                l_tr_time.text = ls_gpslog[0]

                ## Create speed
                l_speed = ET.SubElement(l_trkpt, "speed")

                speed =float(ls_gpslog[6].rstrip())
                l_speed.text = f"{speed:.2f}"


    ## Organize XML
    XML = md.parseString(ET.tostring(root, "utf-8"))

    ## Write
    with open(OUTPUT_GPX, "w") as f:
        XML.writexml(f, encoding="utf-8", newl="\n", indent="", addindent="    ")


if __name__ == "__main__":
    csv2gpx()


PYTHON_EXE の”user_name”とPythonバージョンは適宜修正することで使用できます。

CSVファイルをバッチファイルにドラッグアンドドロップすることでGPXファイルを生成して、CSVファイルをconvertedディレクトリに移動します。

@echo off
REM Convert CSV to GPX

REM=============================================================================
set PYTHON_EXE="C:\Users\"user_name"\AppData\Local\Programs\Python\Python310\python.exe"
set PYTHON_PRG="conv_csv2gpx.py"

REM=============================================================================

for %%i in (%*) do (
    %PYTHON_EXE% %PYTHON_PRG% %%~ni
    echo on
    echo %%i
    echo off

)

move *.csv ./converted
pause
exit

まとめ

試作機5台目にして、まあまあ小型で実用に耐えられるGPSロガーを作成することができました。

今後の課題としては、1. GPSの精度をさらに上げる、 2. ケースの耐久性を上げる、3. 電気的な保護回路を組み込む など挙げていけば切りがありませんが、今後も着々と作っていこうと思います。

コメント