Вступление
Я являюсь обладателем замечательного устройства — GPS логгера Holux M-241. Штука весьма удобная и полезная в путешествиях. С помощью логгера я пишу GPS трек поездки, по которому потом можно посмотреть свой путь в деталях, а также привязать снятые фотографии к GPS координатам. А еще у него есть небольшой экран который показывает дополнительную информацию — часы, текущую скорость, высоту и направление, одометр и многое другое. Вот тут я когда то написал небольшой обзор.
При всех достоинствах железки я стал из нее вырастать. Мне не хватает нескольких небольших, но полезных плюшек: несколько одометров, показ вертикальной скорости, замер параметров участка пути. Вроде мелочи, но фирма Holux посчитала это недостаточно полезным для реализации в прошивке. Так же мне не нравятся кое какие параметры железяки, а некоторые вещи за 10 лет уже морально устарели…
В какой то момент я осознал, что могу сам сделать логгер с такими фичами как мне нужно. Благо все необходимые компоненты достаточно дешевы и доступны. Свою реализацию я начал делать на основе Arduino. Под катом дневник постройки, где я постарался расписать свои технические решения.
Определяемся с фичами
Многие спросят, зачем мне строить свой логгер, если наверняка есть что нибудь готовое у именитых производителей. Возможно. Если честно, особо не искал. Но наверняка там будет чего нибудь нехватать. В любом случае этот проект — фан для меня. Почему мы и не заняться постройкой устройства своей мечты?
Итак, за что же я ценю свой Holux M-241.
- Экран делает из “черного ящика”, результаты работы которого доступны только после поездки, весьма удобный инструмент, показания которого доступны здесь и сейчас. Наличие экрана делает возможным практически все фичи в этом списке
- Часы — штука сама по себе полезная. В поездках GPS логгер болтающийся на веревочке на шее часто оказывается ближе, чем мобилка в кармане или в рюкзаке. Часы поддерживают все таймзоны (хотя и с ручным переключением)
- Кнопка POI позволяет отметить на треке текущую координату. Например, отметить прошмыгнувшую за окном автобуса достопримечательность, про которую хочется погуглить позже.
- С помощью одометра можно измерять расстояние пройденное от какой то точки. Например расстояние пройденное за день, или длину некоторого трека.
- Текущие скорость, высота и направление помогают найти себя в пространстве
- Живучесть 12-14ч от одной батарейки АА в большинстве случаев позволяет не думать о вопросах электропитания. Т.е. почти всегда заряда хватает на полный день путешествия.
- Компактность и простота использования — штуки в современном мире весьма приятные
Однако некоторые вещи можно было бы сделать несколько лучше:
- Подсистему питания на АА батареях многие записывают в однозначный плюс — одной батарейки хватает надолго, а пополнить запас можно в любой глуши. Можно хоть на месяц автономного похода затариться.
Но для меня работа от батареек это сущий геморрой. Приходится носить горсть батареек и кто его знает насколько они качественные (вдруг они лежали 5 лет на полке и уже саморазрядились). С аккумами гемор еще больше. У меня зарядник умеет только парами заряжать. Приходится разряжать аккумы, чтобы они были одной степени разряжености. В итоге никогда не помнишь где уже разряженные, а где еще нет.
За 6 лет использования логгера я всего пару раз оказывался в глуши без электричества. Как правило у меня хотя бы раз в сутки появляется доступ к розетке. В таком случае встроенный литиевый аккумулятор был бы гораздо удобнее. Ну а на крайний случай у меня павербанк есть
- Индикация степени разряда сделана весьма бестолково — индикатор начинает мигать когда батарея вот вот разрядится. При чем может через 5 минут уже сдохнет, а может еще час проработать. Очень легко проморгать этот момент и потерять часть лога.
- Как человек интересующийся авиацией мне бы очень интересно было наблюдать текущую вертикальную скорость
- Несколько одометров — часто интересно измерять более чем одно расстояние. Например расстояние пройденное за день и за всю поездку
- Одометр сбрасывается при выключении устройства или при замене батареи. Это жутко неудобно. Если остановился в кафе покушать, то GPS логгер выключить нельзя ибо значение сбросится. Приходится оставлять его включеным и он продолжает мотать километры и жрать батарею. Было бы куда удобнее иметь возможность поставить одометр на паузу и сохранять значения между включениями
- Замер параметров участка. Катаясь на лыжах мне, например, интересна длина спуска, перепад высот, средняя и максимальная скорость на участке, затраченное время. При чем узнать хочется это прямо сразу, а не дома, когда скачаешь трек.
- Точность оставляет желать лучшего. Когда быстро двигаешься — еще ничего. Но когда скорость маленькая на треке отчетливо видны “шумы” +- 50м. А за час стояния можно “настоять” почти километр. Благо технологии за 10 лет ушли далеко вперед и современные приемники дают гораздо большую точность.
- Скорость сливания треков всего 38400. Не, ну это несерьезно в 2017 использовать COM порт для передачи больших объемов данных. Сливание 2 мегабайт внутреннего флеша занимает более 20 минут.
К тому же не каждая программа умеет слопать формат слитых треков. Родная утилита очень убога. Благо есть BT747, которая может адекватно слить трек и сконвертировать в какой нибудь удобоваримый формат.
- Размер флешки всего 2Мб. С одной стороны этого достаточно для двухнедельной поездки с сохранением точек раз в 5с. Но во-первых внутренний упакованный формат
требует переконвертации, а во-вторых не позволяет увеличить объем - Mass storage device почему то сейчас не в моде. Современные интерфейсы факт наличия файлов стараются скрыть. Я с компами уже 25 лет, и мне работа с файлами напрямую гораздо удобнее нежели каким либо другим способом.
Тут нет ничего такого, чтобы нельзя было бы реализовать без существенных усилий.
Всякое разное. Сам не использую, но вдруг кому полезно:
- Показывает текущие координаты (широта, долгота)
- В левой части экрана рисуются разные иконки суть которых без мануала я и не вспомню.
- Есть переключение метры/км — футы/мили
- Блютус — логгер можно подключать к мобилкам без GPS
- Абсолютное расстояние до точки
- Логирование по времени (каждые N секунд) или по расстоянию (каждые X метров)
- Поддержка разных языков
Выбираем железо
С требованиями более менее определились. Пора понять на чем это все можно реализовать. Главные компоненты у меня будут такие
- Микроконтроллер — у меня не планируется каких либо навороченых вычислительных алгоритмов, поэтому вычислительная мощность ядра не особо важна. Особых требований по начинке у меня тоже нет — набор стандартной периферии подойдет.
Под рукой как раз валялась россыпь разнокалиберных ардуинок, а также парочка stm32f103c8t6. Решил начать с AVR, которые я хорошо знаю на уровне контроллера/регистров/периферии. Если упрусь в ограничения — будет повод пощупать STM32.
- GPS приемник выбирал из модулей NEO6MV2, Beitan BN-800 и Beitan BN-880. Некоторое время гуглил по форумам. Опытные люди сказали, что первый приемник — это уже прошлый век. Два других отличаются друг от друга лишь расположением антенны — у BN-800 она болтается на проводе, а у BN-880 приклеена бутербродом на основной модуль. Взял BN-880
- Экран — в оригинале используется ЖК дисплей 128х32 с подсветкой. Точно такого же не нашел. Я купил OLED 0.91” на контроллере SSD1306 и 1.2” ЖК экран на контроллере ST7565R. Решил начать с первого, т.к. его легче подключить стандартной гребенкой по I2C или SPI. Но он слегка меньше по сравнению с оригиналом, а также на нем не получится постоянно показывать изображение из соображений топливной эффективности. Второй дисплей должен быть менее прожорливый, но под него нужно распаивать хитрый разъем и придумать как запитать подсветку.
Из мелочей:
- Кнопок когда то купил целый мешок
- Шилд с для SD карты — тоже валялся под рукой
- Контроллеров заряда литиевых батарей купил пару штук разных, но еще не разбирался.
Плату решил проектировать в самом конце, когда будет готова прошивка. К этому времени окончательно определюсь с основными компонентами и схемой их включения. На первом этапе отладку решил делать на макетке соединяя компоненты с помощью патчкордов.
Но для начала нужно определится с очень важным вопросом — питание компонентов. Мне показалось разумным запитать все от 3.3В: GPS и экран только на нем и умеют работать. Это так же родное напряжение для USB и SD. К тому же схему можно запитать от одной литиевой банки
Выбор пал на Arduino Pro Mini, которую можно найти в версии 8МГц/3.3В. Вот только USB у нее на борту не оказалось — пришлось использовать USB-UART переходник.
Первые шаги
Вначале проект создал в Arduino IDE. Но если честно, у меня язык не поворачивается называть это IDE — так, текстовый редактор с компилятором. Во всяком случае после Visual Studio, в которой я работаю последние 13 лет делать что либо серьезное в Arduino IDE без слез и матюков не получается.
Благо есть бесплатная Atmel Studio, в которой даже Visual Assist из коробки встроен!!! Программа умеет все что нужно, все привычно и на своих местах. Ну почти все (не нашел только как скомпилировать только один файл, например, чтобы синтаксис проверить)
Начал с экрана — это нужно чтобы отладить скелет прошивки, а потом наполнять ее функциональностью. Остановился на первой попавшейся библиотеке для SSD1306 от Adafruit. Она умеет все что нужно и предоставляет очень простой интерфейс.
Поиграл шрифтами. Оказалось один шрифт может занимать до 8кб (размер букв 24пт) — особо не разгуляешься в 32кб контроллере. Большие шрифты нужны, например, для вывода времени.
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <gfxfont.h>
#include <fonts/FreeMono12pt7b.h>
#include <fonts/FreeMono18pt7b.h>
...
#include <fonts/FreeSerifItalic24pt7b.h>
#include <fonts/FreeSerifItalic9pt7b.h>
#include <fonts/TomThumb.h>
struct font_and_name
{
const char * PROGMEM name;
GFXfont * font;
};
#define FONT(name) {#name, &name}
const font_and_name fonts[] = {
// FONT(FreeMono12pt7b),
FONT(FreeMono18pt7b),
/*
FONT(FreeMono24pt7b),
FONT(FreeMono9pt7b),
FONT(FreeMonoBold12pt7b),
...
FONT(FreeSerifItalic9pt7b),
FONT(TomThumb)*/
};
const unsigned int fonts_count = sizeof(fonts) / sizeof(font_and_name);
unsigned int current_font = 0;
extern Adafruit_SSD1306 display;
void RunFontTest()
{
display.clearDisplay();
display.setCursor(0,30);
display.setFont(fonts[current_font].font);
display.print("12:34:56");
display.setCursor(0,6);
display.setFont(&TomThumb);
display.print(fonts[current_font].name);
display.display();
}
void SwitchToNextFont()
{
current_font = ++current_font % fonts_count;
}
Шрифты в комплекте с библиотекой весьма корявые. Моноширинный шрифт оказался очень широким — строка “12:34:56” не влазит, Serif — все цифры разной жирности. Разве что стандартный шрифт 5x7 в библиотеке выглядит съедобно.
Оказалось, что эти шрифты были сконверчены из каких то опенсорсных ttf шрифтов, которые просто не оптимизированы под мелкие разрешения.
Пришлось рисовать свои шрифты. Точнее сначала выколупывать из готовых отдельные символы. Символ ‘:’ в таблице ASCII очень кстати находится сразу после цифр и можно выколупать одним блоком. Так же удобно, что можно делать шрифт не на все символы, а только на диапазон, например от 0x30 (‘0’) до 0x3a (‘:’). Т.о. из FreeSans18pt7b получилось сделать весьма компактный шрифт только на нужные символы. Пришлось правда чуток подхачить ширину, чтобы текст влезал на ширину экрана.
// This font consists only of digits and ':' to display current time.
// The font is very based on FreeSans18pt7b.h
//TODO: 25 pixel height is too much for displaying time. Create another 22px font
const uint8_t TimeFontBitmaps[] PROGMEM = {
/*
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE9, 0x20, 0x3F, 0xFC, 0xE3, 0xF1,
0xF8, 0xFC, 0x7E, 0x3F, 0x1F, 0x8E, 0x82, 0x41, 0x00, 0x01, 0xC3, 0x80,
...
0x03, 0x00, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x00, 0xC0, 0x30, 0x18, 0x06,
0x01, 0x80, 0xC0, 0x30, 0x00, */0x07, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C,
0x3C, 0x78, 0x1E, 0x70, 0x0E, 0x70, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0,
0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0,
0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x3C,
0x3C, 0x1F, 0xF8, 0x1F, 0xF0, 0x07, 0xE0, 0x03, 0x03, 0x07, 0x0F, 0x3F,
0xFF, 0xFF, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0xE0, 0x1F, 0xF8,
0x3F, 0xFC, 0x7C, 0x3E, 0x70, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07,
0x00, 0x07, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0xF8,
0x03, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x38, 0x00, 0x70, 0x00,
0x60, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xF0,
0x07, 0xFE, 0x07, 0xFF, 0x87, 0x83, 0xC3, 0x80, 0xF3, 0x80, 0x39, 0xC0,
0x1C, 0xE0, 0x0E, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x7F, 0x00, 0x3F, 0x00,
0x1F, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, 0xF0, 0x01,
0xF8, 0x00, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F,
0xF8, 0x07, 0xF0, 0x00, 0x00, 0x38, 0x00, 0x38, 0x00, 0x78, 0x00, 0xF8,
0x00, 0xF8, 0x01, 0xF8, 0x03, 0xB8, 0x03, 0x38, 0x07, 0x38, 0x0E, 0x38,
0x1C, 0x38, 0x18, 0x38, 0x38, 0x38, 0x70, 0x38, 0x60, 0x38, 0xE0, 0x38,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38,
0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x1F, 0xFF, 0x0F, 0xFF, 0x8F, 0xFF,
0xC7, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x39,
0xF0, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0x83, 0xE7, 0x00, 0xF0, 0x00, 0x3C,
0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xFC, 0x00,
0xEF, 0x00, 0x73, 0xC0, 0xF0, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xE0, 0x00,
0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x38, 0x0E, 0x70, 0x0E,
0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xE3, 0xE0, 0xEF, 0xF8, 0xFF, 0xFC,
0xFC, 0x3E, 0xF0, 0x0E, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07,
0x60, 0x07, 0x70, 0x0F, 0x70, 0x0E, 0x3C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8,
0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0E,
0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xE0,
0x00, 0xC0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00,
0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x00,
0x1C, 0x00, 0x1C, 0x00, 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x83,
0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1E,
0x0F, 0x07, 0xFF, 0x01, 0xFF, 0x03, 0xFF, 0xE3, 0xE0, 0xF9, 0xC0, 0x1D,
0xC0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0xF7, 0x00,
0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xF0, 0x00, 0x07, 0xE0,
0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3C, 0x70, 0x0E, 0xF0, 0x0E, 0xE0, 0x06,
0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0F, 0x78, 0x3F,
0x3F, 0xFF, 0x1F, 0xF7, 0x07, 0xC7, 0x00, 0x07, 0x00, 0x06, 0x00, 0x0E,
0x70, 0x0E, 0x70, 0x1C, 0x78, 0x3C, 0x3F, 0xF8, 0x1F, 0xF0, 0x07, 0xC0,
0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80 /*, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x07, 0xFF, 0xB6, 0xD6, 0x00, 0x00, 0x80, 0x03, 0xC0, 0x07, 0xE0,
0x0F, 0xC0, 0x3F, 0x80, 0x7E, 0x00, 0xFC, 0x01, 0xF0, 0x00, 0xE0, 0x00,
...
0x38, 0x38, 0xF8, 0xF0, 0xE0, 0x38, 0x00, 0xFC, 0x03, 0xFC, 0x1F, 0x3E,
0x3C, 0x1F, 0xE0, 0x1F, 0x80, 0x1E, 0x00
*/
};
//TODO Recalc offset numbers
const GFXglyph TimeFontGlyphs[] PROGMEM =
{
{ 449-449, 16, 25, 19, 2, -24 }, // 0x30 '0'
{ 499-449, 8, 25, 19, 4, -24 }, // 0x31 '1'
{ 524-449, 16, 25, 19, 2, -24 }, // 0x32 '2'
{ 574-449, 17, 25, 19, 1, -24 }, // 0x33 '3'
{ 628-449, 16, 25, 19, 1, -24 }, // 0x34 '4'
{ 678-449, 17, 25, 19, 1, -24 }, // 0x35 '5'
{ 732-449, 16, 25, 19, 2, -24 }, // 0x36 '6'
{ 782-449, 16, 25, 19, 2, -24 }, // 0x37 '7'
{ 832-449, 17, 25, 19, 1, -24 }, // 0x38 '8'
{ 886-449, 16, 25, 19, 1, -24 }, // 0x39 '9'
{ 936-449, 3, 19, 7, 2, -20 }, // 0x3A ':'
};
const GFXfont TimeFont PROGMEM = {
(uint8_t *)TimeFontBitmaps,
(GFXglyph *)TimeFontGlyphs,
0x30, 0x3A, 20 };
Оказалось, что шрифт 18пт на самом деле высотой 25 пикселей. Из-за этого он слегка налазит на другую надпись
Инвертированный дисплей, кстати, помогает понять где на самом деле находятся границы области рисования и как относительно этой границы лежит строка — дисплей имеет весьма большие рамки.
Долго гуглил готовые шрифты, но они не подходили или по размеру, или по форме, или по содержанию. К примеру в интернете валом шрифтов 8х12 (дампы знакогенераторов VGA карт). Но по факту эти шрифты являются 6х8, т.е. гуляет куча места — в случае такого маленького разрешения и размера как у меня это критично.
Пришлось таки рисовать свои шрифты, благо формат шрифтов у Adafruit библиотеки очень простой. Картинку готовил в Paint.net — просто рисовал буквы нужным шрифтом, потом чуток корректировал карандашом. Картинку сохранял как png, а затем отправлял в побыстряку написанный на коленке питоновский скрипт. Этот скрипт генерировал полуфабрикат кода, который уже точечно правил в IDE прямо в хекс кодах.
Например так выглядит процесс создания моноширинного шрифта 8х12 с маленькими межбуквенными и межстрочными интервалами. Каждый символ в итоге получился примерно 7х10, и по умолчанию занимал 10 байт. Можно было бы упаковать каждый символ в 8-9 байт (библиотека это позволяет), но я не стал заморачиваться. К тому же в таком виде можно редактировать отдельные пиксели прямо в коде
// A simple 8x12 font (slightly modifier Courier New)
const uint8_t Monospace8x12Bitmaps[] PROGMEM = {
0x1e, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x1e, //0
0x18, 0x68, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x7f, //1
0x3e, 0x41, 0x41, 0x01, 0x02, 0x0c, 0x10, 0x20, 0x41, 0x7f, //2
0x3e, 0x41, 0x01, 0x01, 0x0e, 0x02, 0x01, 0x01, 0x41, 0x3e, //3
0x02, 0x06, 0x0a, 0x12, 0x12, 0x22, 0x3f, 0x02, 0x02, 0x0f, //4
0x7f, 0x41, 0x40, 0x40, 0x7e, 0x01, 0x01, 0x01, 0x41, 0x3e, //5
0x1e, 0x21, 0x40, 0x40, 0x5e, 0x61, 0x41, 0x41, 0x41, 0x3e, //6
0x7f, 0x41, 0x01, 0x02, 0x02, 0x04, 0x04, 0x04, 0x08, 0x08, //7
0x1e, 0x21, 0x21, 0x21, 0x1e, 0x21, 0x21, 0x21, 0x21, 0x1e, //8
0x1e, 0x21, 0x21, 0x21, 0x23, 0x1d, 0x01, 0x01, 0x22, 0x1c, //9
0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, //:
};
const GFXglyph Monospace8x12Glyphs[] PROGMEM =
{
{ 0, 8, 10, 8, 0, -11 }, // 0x30 '0'
{ 10, 8, 10, 8, 0, -11 }, // 0x31 '1'
{ 20, 8, 10, 8, 0, -11 }, // 0x32 '2'
{ 30, 8, 10, 8, 0, -11 }, // 0x33 '3'
{ 40, 8, 10, 8, 0, -11 }, // 0x34 '4'
{ 50, 8, 10, 8, 0, -11 }, // 0x35 '5'
{ 60, 8, 10, 8, 0, -11 }, // 0x36 '6'
{ 70, 8, 10, 8, 0, -11 }, // 0x37 '7'
{ 80, 8, 10, 8, 0, -11 }, // 0x38 '8'
{ 90, 8, 10, 8, 0, -11 }, // 0x39 '9'
{ 100, 8, 10, 8, 0, -11 }, // 0x3A ':'
};
const GFXfont Monospace8x12Font PROGMEM = {
(uint8_t *)Monospace8x12Bitmaps,
(GFXglyph *)Monospace8x12Glyphs,
0x30, 0x3A, 12 };
Каркас
Оригинальное устройство предоставляет весьма простой и удобный интерфейс. Информация группируется по категориям, которые показываются от отдельных страничках (экранах). С помощью кнопки можно циклически переключаться между страничками, а второй кнопкой выбрать текущий пункт или выполнить действие которое указано в подписи под кнопкой. Такой подход мне кажется весьма удобным и не нужно ничего менять.
Мне нравится красота ООП, потому я сразу слепил небольшой интерфейсик, каждая страничка реализует интерфейс как ей требуется. Страничка знает как себя нарисовать и реализует реакцию на кнопки.
class Screen
{
Screen * nextScreen;
public:
Screen();
virtual ~Screen() {}
virtual void drawScreen() = 0;
virtual void drawHeader();
virtual void onSelButton();
virtual void onOkButton();
virtual PROGMEM const char * getSelButtonText();
virtual PROGMEM const char * getOkButtonText();
Screen * addScreen(Screen * screen);
};
В зависимости от текущего экрана кнопки могут выполнять различные действия. Поэтому верхнюю часть экрана высотой в 8 пикселей я отвел на подписи для кнопок. Текст для подписей зависит от текущего экрана и возвращается виртуальными функциями getSelButtonText() и getOkButtonText(). Также в шапке будут еще отображаться служебные штуки типа уровня сигнала GPS и заряда батареи. Оставшиеся ¾ экрана доступны для полезной информации.
Как я уже сказал экранчики могут перелистываться, а значит где то должен быть список объектов для разных страниц. При чем не один — экраны могут быть вложенными, как подменю. Я даже завел класс ScreenManager, который должен был управлять этими списками, но потом я нашел решение проще.
Так каждый экран просто имеет указатель на следующий. Если скрин позволяет войти в подменю, то у него добавляется еще один указатель на скрин этого подменю
class Screen
{
Screen * nextScreen;
…
};
class ParentScreen : public Screen
{
Screen * childScreen;
…
};
По умолчанию обработчик кнопки просто вызывает функцию смены экрана, передавая ей нужный указатель. Функция получилась тривиальной — она просто переключала указатель на текущий экран. Чтобы обеспечить вложенность экранов я сделал небольшой стек. Так что весь менеджер экранов у меня поместился в 25 строк и 4 маленькие функции
Screen * screenStack[3];
int screenIdx = 0;
void setCurrentScreen(Screen * screen)
{
screenStack[screenIdx] = screen;
}
Screen * getCurrentScreen()
{
return screenStack[screenIdx];
}
void enterChildScreen(Screen * screen)
{
screenIdx++; //TODO limit this
screenStack[screenIdx] = screen;
}
void backToParentScreen()
{
if(screenIdx)
screenIdx--;
}
Правда код наполнения этих структур выглядит не очень красиво, но пока лучше не придумал
Screen * createCurrentTimeScreen()
{
TimeZoneScreen * tzScreen = new TimeZoneScreen(1, 30);
tzScreen = tzScreen->addScreen(new TimeZoneScreen(2, 45));
tzScreen = tzScreen->addScreen(new TimeZoneScreen(-3, 30));
// TODO Add real timezones here
CurrentTimeScreen * screen = new CurrentTimeScreen();
screen->addChildScreen(tzScreen);
return screen;
}
Идем дальше. В своей реализации интерфейса мне захотелось сделать что то наподобие message box’а — короткого сообщения, которое бы показывалось на секунду-другую, а потом исчезало. Например, если на экране с текущими координатами нажать кнопку POI (Point Of Interest), то помимо записи точки в трек было бы неплохо показать пользователю сообщение “Waypoint Saved” (в оригинальном устройстве просто на секунду показывается дополнительная иконка). Или при разряде батареи “взбодрить” пользователя соответствующим сообщением.
Поскольку данные с GPS будут приходить постоянно, то ни о каких блокирующих функциях речи быть не может. Поэтому пришлось изобрести простенькую стейт машину (конечный автомат), которая в функции loop() выбирала бы что делать — показывать текущий экран или мессадж бокс.
enum State
{
IDLE_DISPLAY_OFF,
IDLE,
MESSAGE_BOX,
BUTTON_PRESSED,
};
Также с помощью машины состояний удобно обрабатывать нажатия кнопок. Возможно, через прерывания было бы правильно, но так тоже неплохо получилось. Работает это так: если в состоянии IDLE была нажата кнопка — запомним время нажатия и переходим в состояние BUTTON_PRESSED. В этом состоянии ждем пока пользователь отпустит кнопку. Тут мы можем подсчитать длительность когда кнопка была нажата. Короткие срабатывания (<30мс) просто игнорируем — скорее всего это дребезг контактов. Длинные срабатывания уже можно интерпретировать как нажатие кнопки.
Я планирую использовать как короткие нажатия на кнопки для обычных действий, так и длинные (>1c) для специальных функций. Например, короткое нажатие запускает/приостанавливает одометр, длинное нажатие сбрасывает значение счетчика в 0.
Возможно и другие состояния добавятся. Так, например, в оригинальном логгере после переключения на очередную страничку значения на экране меняются часто, а через пару секунд реже — раз в секунду. Это можно сделать добавлением еще одного состояния.
Когда каркас был готов, я уже, было, начал подключать GPS. Но тут возникли нюансы, которые заставили меня отложить эту задачу.
Оптимизация прошивки
Прежде чем идти дальше мне нужно отвлечься на кое какие технические детали. Дело в том, что примерно в этом месте я начал бодаться с растущим потреблением памяти. Оказалось, что строка опрометчиво объявленная без модификатора PROGMEM на старте прошивки копируется в ОЗУ и занимает там место в течении всего времени выполнения.
В микроконтроллерах, как правило, используется Гарвардская архитектура, где код и данные разделены. Т.о. приходится использовать различные функции для чтения памяти и флеша. С точки зрения языка C/C++ указатели выглядят одинаково, но при написании программы мы должны точно знать куда на какую именно память указывает наш указатель и вызывать соответствующие функции.
Благо разработчики библиотек уже, отчасти, позаботились об этом. Основной класс библиотеки дисплея — Adafruit_SSD1306 наследуется от класса Print из ардуиновской стандартной библиотеки. Это предоставляет нам целую серию разных модификаций метода print — для печати строк, отдельных символов, чисел и чего то там еще. Так вот в нем есть 2 отдельные функции для печати строк
size_t print(const __FlashStringHelper *);
size_t print(const char[]);
Первая знает, что нужно печатать строку из флешки и посимвольно ее загружает. Вторая печатает символы из ОЗУ. По факту обе эти функции принимают указатель на строку, только из разных адресных пространств.
Я долго искал в коде ардуино этот самый __FlashStringHelper чтобы научиться вызывать нужную функцию print(). Оказалось дядьки поступили хитро: они просто объявили такой тип с помощью forward declaration (без объявления самого типа) и написали макрос, который кастил указатели на строки во флеше к типу __FlashStringHelper. Просто чтобы компилятор сам выбирал нужную перегруженную функцию
class __FlashStringHelper;
#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))
Это позволяет писать так:
display.print(F(“String in flash memory”));
Но не позволяет писать так
const char text[] PROGMEM = "String in flash memory";
display.print(F(text));
И, судя по всему, библиотека не предоставляет ничего, что бы можно было так делать. Я знаю, что нехорошо в своем коде использовать приватные штуки библиотек, но что мне было делать? Я нарисовал свой макрос, который делал то, что мне нужно.
#define USE_PGM_STRING(x) reinterpret_cast<const __FlashStringHelper *>(x)
Так функция рисования шапки стала выглядеть так
void Screen::drawHeader()
{
display.setFont(NULL);
display.setCursor(20, 0);
display.print('x1e');
display.print(USE_PGM_STRING(getSelButtonText()));
display.setCursor(80, 0);
display.print('x1e');
display.print(USE_PGM_STRING(getOkButtonText()));
}
Ну а раз я уж влез в низкоуровневые штуки прошивки, то решил глубже изучить как же там оно все внутри устроено.
Вообще, ребятам который придумали Ардуино нужно поставить памятник. Они сделали простую и удобную платформу для прототипирования и поделок. Огромное количество народу с минимальными знаниями электроники и программирования смогли войти в мир Ардуино. Но все это гладко и красиво пока делаешь фигню типа моргалки светодиодами или считывания показаний термометра. Как только замахиваешься на что нибудь серьезное сразу приходится разбираться глубже чем хотелось с самого начала.
Так, после каждой добавленной библиотеки или даже класса я отмечал как быстро растет потребление памяти. К этому моменту у меня было занято более 14 кб из 32 кб флеша и 1300 байт ОЗУ (из 2к). Каждое неосторожное движение добавляло еще процентов 10 к уже используемому. А ведь я еще толком не подключил GPS и SD/FAT32 библиотеки, а самого функционала пока кот наплакал. Пришлось брать в руки шашку дизассемблер и изучать что же там компилятор такого наколбасил.
Я в тайне надеялся, что линкер выкидывает неиспользуемые функции. Но оказалось, что некоторые из них линковщик вставляет практически целиком. В прошивке я обнаружил функции рисования линий и некоторые другие из библиотеки работы с экраном, хотя в коде я их явно на тот момент не вызывал. Неявно они тоже вызываться не должны — зачем нужна функция рисования линии, если я только буквы из батмапок рисую? Более 5.2кб на ровном месте (и это не считая шрифтов).
Помимо библиотеки управления дисплеем я еще обнаружил:
- 2.6 кб — на SoftwareSerial (я ее затянул в проект в какой то момент)
- 1.6 кб — I2C
- 1.3 кб — HardwareSerial
- 2 кб — TinyGPS
- 2.5 кб на собственно ардуино (инициализация, пины, всевозможные таблицы, основной таймер для функций millis() и delay()),
Цифры весьма ориентировочные, т.к. оптимизатор серьезно перемешивает код. В одном месте может начаться какая нибудь функция, а потом сразу за ней может идти другая из другой библиотеки, которая вызывается из первой. При чем отдельные ветки этих функций могут располагаться в другом конце флеша.
Так же в коде я обнаружил
- Управление экраном по SPI (хотя он у меня подключен по I2C)
- Методы базовых классов, которые сами не вызываются, т.к. переопределены в наследниках
- Деструкторы, которые по дизайну никогда не вызываются
- Функции рисования (причем не все — часть функций линкер все таки повыкидывал)
- malloc/free в то время как в моем коде все объекты, по сути, статические
Но семимильными шагами растет не только потребление флеш памяти, но и SRAM
- 130 байт — I2C
- 100 байт — SoftwareSerial
- 157 байт — Serial
- 558 байт — Display (из них 512 это буфер кадра)
Не менее занимательной оказалась секция .data. Там около 700 байт и эта штука грузится из флеша в ОЗУ на старте. Оказалось, что там зарезервированы места под переменные в памяти, причем вместе с значениями инициализации. Тут живут те переменные и константы которые забыли объявить как const PROGMEM.
Среди этого нашелся здоровенный массив со “сплешскрином” экрана — начальные значения буфера кадра. Теоретически если сделать экрану display() сразу после старта, то можно увидеть цветок и надпись Adafruit, но в моем случае тратить на это флеш память бессмысленно.
В секции .data так же находятся vtable’ы. Они копируются в память из флешки, видимо из соображений эффективности в рантайме. Но приходится жертвовать довольно большим куском оперативной памяти — на десяток классов более 150 байт. Причем, похоже, нет ключа компилятора, который жертвуя производительностью оставит виртуальные таблицы во флеш памяти.
Что с этим делать? Пока не знаю. Будет зависеть от того как будет расти потребление дальше. По хорошему найденные косяки нужно нещадно чинить. По всей видимости мне придется втянуть к себе в проект все библиотеки явно а потом почекрыжишь их хорошенько. А еще возможно придется по другому переписать некоторые куски с целью оптимизировать память. Или перейти на более мощное железо. В любом случае теперь я знаю о проблеме и есть стратегия как его чинить.
Послесловие
Изначально я хотел написать одну статью в конце работы над проектом. Но поскольку заметки по ходу работы накапливаются с большой скоростью, то статья грозится быть очень большой. Так что я решил разбить ее на несколько частей. В этой части я рассказал о подготовительных этапах: понимание чего же я вообще хочу, выбор платформы, реализация каркаса приложения.
В следующей части я планирую перейти уже к реализации основной функциональности — работу с GPS. Я уже столкнулся с парочкой интересных граблей, про которые хотел бы рассказать.
Я более 10 лет серьезно не программировал под микроконтроллеры. Оказалось, что я несколько избалован обилием ресурсов больших компов и мне тесновато в реалиях ATMega32. Поэтому пришлось продумать разные бекапные варианты, как то урезание функционала библиотек или редизайн приложения во имя эффективного использования памяти. Так же я не исключаю переход на более мощные контроллеры — ATMega64 или что нибудь из линейки STM32.
По стилистике статья получается что-то вроде журнала постройки. И я буду рад конструктивным комментариям — еще не поздно что либо поменять. Желающие могут присоединиться к моему проекту на гитхабе: github.com/grafalex82/GPSLogger
Конец первой части
Автор: grafalex