Background
For these 3 years after developing Adafruit Feather based GPS logger, I frequently carry the logger and very satisfied with it.
But last year, a new GPS receiver kit was released. That is M5 ATOM GPS and can be purchased from M5's official site.
Its big advantage compared to Adafruit Feather for me is that its circuit board is covered. That fact will make carrying the logger onto airplane easier. See the right how it looks with battery.
This article is about how to develop GPS logger with it. It was tougher than expected.
Note that I will not publish the completed software, which is just a variation of my previous software. I believe teaching how to fish is more valuable than giving fish.
Also, M5 ATOM GPS is a kit, not a consumer product with warranty. To maintain that status, leaving some task to user is important.
Choice of power supply
In terms of hardware, only thing to do is connecting some power supply. And generic portable chargers are widely available these days.
However, there is one pitfall; many models of portable chargers cut power supply when connected to small load like this GPS kit.
So, you need "IoT ready" portable charger. Though availability of lithium battery varies by region due to tight flight restrictions, I used Cheero branded one available in Japan. Though they wouldn't ship overseas, here's the product page.
Software building
In terms of software, I used Arduino IDE as in my previous effort.
But this time, development required some struggle. Obstacles were like:
- ESP32 has some differently implemented library under the same name such as
SD.h
. - ESP32's library code have thicker layers. e.g.
class SDFS < FS < Stream
. - ESP32 runs on FreeRTOS, and reports unfamiliar "Guru Meditation Error".
M5Atom.h
works, but looking into the code gives me some uneasiness.
Building and running the manufacturer's sample was easy, as long as you carefully follow M5's instructions.
But then, porting my own code took about a week mainly struggling with "Guru Meditation" Error.
It turned out that my own code, which runs flawlessly for 3 years on Adafruit Feather, has one char array without null termination. (shame on me!) But before reaching to this conclusion, I looked behind the scenes of Arduino IDE, dug ESP32's core code, try-and-errored various silly modifications like changing declaration order of variables, etc.
My takeaways are:
- Enable verbose output while compiling in Arduino IDE.
- Analyze the verbose output of compiling and find which files are actually used.
- Read core / library codes under use.
- Use FastLED directly without
M5Atom.h
. - Use
xtensa-esp32-elf-addr2line
to decode Guru Meditation error.
Followings are details on them.
Enable verbose output while compiling
Follow the steps below.
- Start Arduino IDE.
- File > Preferences
- Under "Settings" tab, find "Show verbose output during:" label.
- Put check mark for "compilation."
Then, you'll see very verbose output while compiling sketch on Arduino IDE.
Analyze the verbose output
Arduino IDE uses GCC (GNU compiler collection) under the hood for compiling. The verbose output shows actual invocations of GCC. Analyzing those invocations may seem daunting at first glance, but is not that hard. You can copy the output on Arduino IDE by CTRL-C and paste into other text editor.
It looks very busy because everything is full-path. Essentially, typical lines are
invoking gcc
or g++
as:
gcc -D$(variable name) -I$(include directory) $(some file).c -o $(some file).o
Both -D
and -I
options can appear quite many times.
-I
tells the compiler where #include
has to look and $(some file).c
is actually
compiled file.
Browsing the output teaches locations like:
- ESP32 core:
C:\\Users\\$(user name)\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\1.0.5\\cores\\esp32\\
SD
library:C:\\Users\\$(user name)\\AppData\\Local\\Arduino15\\packages\\esp32\\hardware\\esp32\\1.0.5\\libraries\\SD\\src
Read core / library codes
Once you find core / library codes, search inside. Some questions I had before searching were like:
- How is Arduino
loop()
is implemented for ESP32 with FreeRTOS? - Do
millis()
anddelay()
work?
Answers are accordingly:
- It's in
main.cpp
. Invoked inloopTask()
started byxTaskCreateUniversal()
. - Both are defined in
esp32-hal-misc.c
and seem to work correctly.
My conlusion is I wouldn't touch FreeRTOS and just stick to Arduino setup()
and loop()
structure and timing with millis()
and delay()
just like my previous development.
Use FastLED directly without M5Atom.h
This section highly relied on the following article in Japanese. Though you don't have to read it, I put URL here to show my respect.
https://tomoto335.hatenablog.com/entry/m5atom-dont-use-library
The manufacturer's sample uses M5Atom.h: the code has #include "M5Atom.h"
line. M5Atom.h
has some NeoPixel LED support, but I felt uneasy to use it for the
reasons below.
- LED control is implemented unnecessarily as seperate FreeRTOS task from Arduino main loop.
- LED control unnecessarily allocates 25 buffers for LED.
- The code has so many hard coded numbers.
You have to write equivalent for M5.begin()
by yourself, but it's actually simple.
Then, you have to manipulate FastLED library by yourself, but the library seems widely used and thus more trustable.
The only thing I would add is that M5 ATOM's NeoPixel LED is GRB ordered and the LED controller is SK6812. So initialization code looks like:
// definitions
const uint8_t NEOPIXEL_PIN = 27;
const uint8_t NUM_NEOPIXEL = 1;
const uint8_t NEOPIXEL_BRIGHTNESS = 2;
// initialize LED (NeoPixel)
CRGB leds[NUM_NEOPIXEL];
FastLED.addLeds<SK6812, NEOPIXEL_PIN, GRB>(leds, NUM_NEOPIXEL);
FastLED.setBrightness(NEOPIXEL_BRIGHTNESS);
leds[0] = CRGB::Blue;
FastLED.show();
FastLED version I used is 3.003.003.
decoding Guru Meditation Error
Guru Meditation Error is equivalent to blue screen on Windows or to kernel panic on UNIX variants.
Thanks to the following blog article, error location can be decoded to line number of a file. That article is in Japanese, but most contents are commands and here's rough summary.
https://wamisnet.github.io/blog/2020-07-14-esp32-backtrace/
- Guru Meditation Error's backtrace address can be decoded.
- Salvage elf file from build directory.
- Run
xtensa-esp32-elf-addr2line
command with-pfiaC
option.
Finding those file locations is easy by googling.
Some other considerations
on LED
This kit (M5 ATOM GPS) has only one NeoPixel LED visible. GPS submodule has other 2 LEDs, which are hidden under the cover.
For one NeoPixel, I have two things to monitor continuously: GPS status and SD flush. I assigned LED's color and blink timing to them accordingly.
What does "hidden" mean? See the following Tweet by @n602_NA for images.
Atomic GPS のユニットは
— n602 (@n602_NA) August 29, 2020
動作時に内部の青いLEDが点滅していて
測位できた感じになると青/赤点滅になるヤツだと思う
しかし、そのLEDがわりとケースで隠れているので結局よくわからないのだ^^; pic.twitter.com/9clGmF1TIH
on GPS module configuration
You can't send commands to GPS submodule, because GPS's RX pin is not connected.
See the following Tweet by @norifumi5001 for image (find a red boxed arrow.)
とすると、「ATOM TF-Card Kit」はmicroSDカード付素体としてかなり応用性ありそう(だからコイツだけ「Kit」なのか!)
— norifumi (@norifumi5001) June 13, 2020
惜しむらくはUARTの線が片方しか配線されてないくさいこと(GPSなので双方向通信不要という判断だったんだろな) pic.twitter.com/4LXgbJrWwx
I noticed this after spending some time to interpret protocol specification written in Chinese available at product page. Chinese I barely understand. Save your time before deciphering that document.
on NMEA 0183 parsing
The manufacturer's sample has NMEA 0183 parsing function. But it uses Arduino's String
class, which dynamically allocates memory.
Since I sometimes keep the software running for a long time like over 10 hours, static memory allocation is desired. So, I wrote NMEA 0183 parsing with very limited function (just to see position is fixed or not from RMC sentence.)
Here's the code fraction, just to show how limited.
if (c == '\n' || linebuf_curpos - linebuf > LINEBUF_LEN - 1) {
if (strstr(linebuf, "RMC")) {
// find 2nd comma in linebuf
int count = 0;
char* linebuf_ptr = linebuf;
while (count < 2 && linebuf_ptr - linebuf < LINEBUF_LEN - 1) {
if (*linebuf_ptr == ',') {
count++;
}
linebuf_ptr++;
}
// See whether letter after 2nd comma is A or not
// Note: 2nd element of RMC sentence is A (position fixed) or V (not)
if (*linebuf_ptr == 'A') {
global_status.position_fixed = true;
} else {
global_status.position_fixed = false;
}
}
on battery saving
One day, months after finishing the development, I left the logger on until battery goes empty.
By simply comparing the first and the last RMC sentence in the log file (the second column is hhmmss.ss), I can see how long the battery lasted.
$GNRMC,075726.41,V,,,,,,,,,,N*67
$GNRMC,021010.00,V,,,,,,,160321,,,N*66
It was 18 hours and 13 minutes.
Since my battery capacity is 3200mAh according to its specification, average current is 175mA (3200 / (18 + 13/60)). Though this calculation is rough (not 100% of battery capacity can be used), over 100mA at 5V is just too much.
First I thought that Wifi and/or Bluetooth is kept on unintentionally, but digging Arduino ESP32 core code and simple device search from external PC gives the feeling that it isn't so.
And, according to the following great guide (English, this time), ESP32 automatically goes to modem sleep mode.
https://diyi0t.com/reduce-the-esp32-power-consumption/
Since this module has to continuously read NMEA sentence from GPS submodule, further sleep modes cannot be used. Only option left is reducing CPU clock.
Again, digging Arduino ESP32 core code taught me that there's
setCpuFrequencyMhz()
function available. I added its call at the
beginning of my setup()
like below.
Default clock was 240MHz according to boards.txt (m5stick-c.build.f_cpu=240000000L
)
void setup() {
setCpuFrequencyMhz(80);
Serial.begin(115200);
Serial.flush();
By repeating the same experiment, battery life is now 24 hours and 47 minutes. Though the improvement is too good to instantly trust, the heat of the module felt by hand bacame lower and thus I can have some confidence in the result.
I also updated Arduino ESP32 library from v1.0.4 to v1.0.5, which may contribute to power saving (something in v1.0.4 core might have prevented ESP32 from going modem sleep automatically.)
More analysis, like whether CPU clock actually goes down, measuring current with ampere meter, etc. may be needed for professional development, but I stop here because I'm enough satisfied.
Appendix on CPU frequency (2021-03-25)
This section is for who wonders why I pick 80MHz.
I picked 80MHz for my first attempt of emptying battery test, which turned out enough satisfying, for the following reasons.
- 80MHz is the lowest among listed in the ESP32 series datasheet (as in the right image.)
- CPU clock lower than 80MHz seems to change peripheral frequency by brief googling and may have side effects on my use. See pages like this (though in Japanese, the first table will tell that.)
After publishing this article, I tried 40, 20, 10 MHz.
At all frequency, NeoPixel LED doesn't work (stays white at full brightness) and SD.begin()
fails.
I have spent some time to look for reasons but couldn't find conclusive answers.
Further analysis needs oscilloscope, which I don't have, to inspect FastLED's signal wave on GPIO27 or SPI signal wave.
Again, I'm enough satisfied with 24 hours of battery life and stop here.