- PVSM.RU - https://www.pvsm.ru -
Я являюсь обладателем замечательного устройства — GPS логгера Holux M-241. Штука весьма удобная и полезная в путешествиях. С помощью логгера я пишу GPS трек поездки, по которому потом можно посмотреть свой путь в деталях, а также привязать снятые фотографии к GPS координатам. А еще у него есть небольшой экран который показывает дополнительную информацию — часы, текущую скорость, высоту и направление, одометр и многое другое. Вот тут [1] я когда то написал небольшой обзор.
При всех достоинствах железки я стал из нее вырастать. Мне не хватает нескольких небольших, но полезных плюшек: несколько одометров, показ вертикальной скорости, замер параметров участка пути. Вроде мелочи, но фирма Holux посчитала это недостаточно полезным для реализации в прошивке. Так же мне не нравятся кое какие параметры железяки, а некоторые вещи за 10 лет уже морально устарели…
В какой то момент я осознал, что могу сам сделать логгер с такими фичами как мне нужно. Благо все необходимые компоненты достаточно дешевы и доступны. Свою реализацию я начал делать на основе Arduino. Под катом дневник постройки, где я постарался расписать свои технические решения.
Многие спросят, зачем мне строить свой логгер, если наверняка есть что нибудь готовое у именитых производителей. Возможно. Если честно, особо не искал. Но наверняка там будет чего нибудь нехватать. В любом случае этот проект — фан для меня. Почему мы и не заняться постройкой устройства своей мечты?
Итак, за что же я ценю свой Holux M-241.
Однако некоторые вещи можно было бы сделать несколько лучше:
Но для меня работа от батареек это сущий геморрой. Приходится носить горсть батареек и кто его знает насколько они качественные (вдруг они лежали 5 лет на полке и уже саморазрядились). С аккумами гемор еще больше. У меня зарядник умеет только парами заряжать. Приходится разряжать аккумы, чтобы они были одной степени разряжености. В итоге никогда не помнишь где уже разряженные, а где еще нет.
За 6 лет использования логгера я всего пару раз оказывался в глуши без электричества. Как правило у меня хотя бы раз в сутки появляется доступ к розетке. В таком случае встроенный литиевый аккумулятор был бы гораздо удобнее. Ну а на крайний случай у меня павербанк есть
К тому же не каждая программа умеет слопать формат слитых треков. Родная утилита очень убога. Благо есть BT747, которая может адекватно слить трек и сконвертировать в какой нибудь удобоваримый формат.
Тут нет ничего такого, чтобы нельзя было бы реализовать без существенных усилий.
Всякое разное. Сам не использую, но вдруг кому полезно:
С требованиями более менее определились. Пора понять на чем это все можно реализовать. Главные компоненты у меня будут такие
Под рукой как раз валялась россыпь разнокалиберных ардуинок, а также парочка stm32f103c8t6. Решил начать с AVR, которые я хорошо знаю на уровне контроллера/регистров/периферии. Если упрусь в ограничения — будет повод пощупать STM32.
Из мелочей:
Плату решил проектировать в самом конце, когда будет готова прошивка. К этому времени окончательно определюсь с основными компонентами и схемой их включения. На первом этапе отладку решил делать на макетке соединяя компоненты с помощью патчкордов.
Но для начала нужно определится с очень важным вопросом — питание компонентов. Мне показалось разумным запитать все от 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 [5]. Она умеет все что нужно и предоставляет очень простой интерфейс.
Поиграл шрифтами. Оказалось один шрифт может занимать до 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 на старте прошивки копируется в ОЗУ и занимает там место в течении всего времени выполнения.
В микроконтроллерах, как правило, используется Гарвардская архитектура [7], где код и данные разделены. Т.о. приходится использовать различные функции для чтения памяти и флеша. С точки зрения языка 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кб на ровном месте (и это не считая шрифтов).
Помимо библиотеки управления дисплеем я еще обнаружил:
Цифры весьма ориентировочные, т.к. оптимизатор серьезно перемешивает код. В одном месте может начаться какая нибудь функция, а потом сразу за ней может идти другая из другой библиотеки, которая вызывается из первой. При чем отдельные ветки этих функций могут располагаться в другом конце флеша.
Так же в коде я обнаружил
Но семимильными шагами растет не только потребление флеш памяти, но и SRAM
Не менее занимательной оказалась секция .data. Там около 700 байт и эта штука грузится из флеша в ОЗУ на старте. Оказалось, что там зарезервированы места под переменные в памяти, причем вместе с значениями инициализации. Тут живут те переменные и константы которые забыли объявить как const PROGMEM.
Среди этого нашелся здоровенный массив со “сплешскрином” экрана — начальные значения буфера кадра. Теоретически если сделать экрану display() сразу после старта, то можно увидеть цветок и надпись Adafruit, но в моем случае тратить на это флеш память бессмысленно.
В секции .data так же находятся vtable’ы. Они копируются в память из флешки, видимо из соображений эффективности в рантайме. Но приходится жертвовать довольно большим куском оперативной памяти — на десяток классов более 150 байт. Причем, похоже, нет ключа компилятора, который жертвуя производительностью оставит виртуальные таблицы во флеш памяти.
Что с этим делать? Пока не знаю. Будет зависеть от того как будет расти потребление дальше. По хорошему найденные косяки нужно нещадно чинить. По всей видимости мне придется втянуть к себе в проект все библиотеки явно а потом почекрыжишь их хорошенько. А еще возможно придется по другому переписать некоторые куски с целью оптимизировать память. Или перейти на более мощное железо. В любом случае теперь я знаю о проблеме и есть стратегия как его чинить.
Изначально я хотел написать одну статью в конце работы над проектом. Но поскольку заметки по ходу работы накапливаются с большой скоростью, то статья грозится быть очень большой. Так что я решил разбить ее на несколько частей. В этой части я рассказал о подготовительных этапах: понимание чего же я вообще хочу, выбор платформы, реализация каркаса приложения.
В следующей части я планирую перейти уже к реализации основной функциональности — работу с GPS. Я уже столкнулся с парочкой интересных граблей, про которые хотел бы рассказать.
Я более 10 лет серьезно не программировал под микроконтроллеры. Оказалось, что я несколько избалован обилием ресурсов больших компов и мне тесновато в реалиях ATMega32. Поэтому пришлось продумать разные бекапные варианты, как то урезание функционала библиотек или редизайн приложения во имя эффективного использования памяти. Так же я не исключаю переход на более мощные контроллеры — ATMega64 или что нибудь из линейки STM32.
По стилистике статья получается что-то вроде журнала постройки. И я буду рад конструктивным комментариям — еще не поздно что либо поменять. Желающие могут присоединиться к моему проекту на гитхабе: github.com/grafalex82/GPSLogger [8]
Конец первой части
Автор: grafalex
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/gadzhety/246496
Ссылки в тексте:
[1] Вот тут: http://grafalexphoto.livejournal.com/10901.html
[2] BN-880: http://www.banggood.com/ru/UBLOX-NEO-M8N-BN-880-Flight-Control-GPS-Module-Dual-Module-Compass-p-971082.html
[3] OLED 0.91” на контроллере SSD1306: https://www.aliexpress.com/item/1pcs-0-91-inch-OLED-module-0-91-blue-OLED-128X32-OLED-LCD-LED-Display-Module/32777216785.html
[4] 1.2” ЖК экран на контроллере ST7565R: http://www.ebay.com/itm/291035862427
[5] первой попавшейся библиотеке для SSD1306 от Adafruit: https://github.com/adafruit/Adafruit_SSD1306
[6] Фон Неймановская архитектура: https://ru.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%84%D0%BE%D0%BD_%D0%9D%D0%B5%D0%B9%D0%BC%D0%B0%D0%BD%D0%B0
[7] Гарвардская архитектура: https://ru.wikipedia.org/wiki/%D0%93%D0%B0%D1%80%D0%B2%D0%B0%D1%80%D0%B4%D1%81%D0%BA%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0
[8] github.com/grafalex82/GPSLogger: https://github.com/grafalex82/GPSLogger
[9] Источник: https://geektimes.ru/post/286348/
Нажмите здесь для печати.