
Raspberry Pi Zero2 でGPSロガーを作ってきたgoldear@goldear820です。
Raspberry Pi Zero2では小型化に限界がある、OSが不安定になることがあるため、小型かつOSレスのWaveShare製 Raspberry Pi Pico互換ボード RP2040-Tinyを使って、GPSロガーを作ってみました。
試作機を4台ほど作って、ついに5台目でそこそこのものができたのでまとめておきます。

目次
Raspberry Pi RP2040 について
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の回路エディタを使ってみました。
モジュールの組み合わせなので、特に面白みのない回路になっています。
補足として、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化基板をオススメします。なかなか入荷しないんですよねこれ。

上記の安いmicroSDカードスロットを使用する場合は、以前紹介したエポキシ樹脂で固定するモゲマイクロ対策を施すのが有効です。
実装
開発環境はthonnyを使って、MicroPythonで実装します。事前にthonnyの「パッケージ管理」から os, datetime, sdcard, ssd1306 をインストールしておいてください。
また micropyGPS という便利なライブラリもMicroPython対応しているので使用可能ですが、私自身が動作を理解しきれていないため今回は使用しません。使用したら記述量が一気に少なくなります。
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 変換

今回は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. 電気的な保護回路を組み込む など挙げていけば切りがありませんが、今後も着々と作っていこうと思います。


コメント