GPS logger
Hello everyone! Some time ago I got the idea to upgrade my faithful and beloved GPS logger Holux M241. It would be possible to look for something interesting on the market that could satisfy my needs. But it was more interesting for me to dig in the direction of microcontrollers, NMEA GPS protocol, USB and SD Card wisdoms, thereby constructing the device of your dreams.
What exactly do I build I described in detail in the first part. At that stage, I was shooting for technology – I felt Arduino in the context of a relatively large project. It turned out there are a lot of nuances that in ordinary tutorials do not particularly affect. In the comments I received a lot of interesting intuition, for which I am very grateful to the readers. I hope today you will find something interesting.
This is the second article in the series. Like the previous one, it is a kind of construction magazine. I try to describe the technical solutions that I take in the course of work on the project. Today we will connect the GPS. And also to switch to more mature technologies – FreeRTOS and the microcontroller STM32. Well, as always, we will disassemble the firmware and watch what’s written there.
GPS’им
By this time I already had the application framework. Everything was spinning on the Arduino Nano on the ATMega328 controller. It’s time to connect my Beitan BN-880 GPS receiver.
I have some prejudiced attitude towards UART as a low-speed protocol from the last century. Reason, of course, I understand – the interface is simple as 3 pennies, it works on everything that moves. What else is needed? I also have a prejudiced attitude to text protocols – messages need to be parsed as well. Why not send data in binary form? Yes, even packages? Anyway, their person does not read. And binary packages could greatly simplify the processing. Well then I am, I hum.
Seeing the feet of SDA and SCK sticking out of the module I wanted to hang on to them. I grabbed it and …. Realized that the data is not so easy to get. I do not even know how. If UART is used, the GPS receiver simply populates the messages, and the recipient parsit what it needs. I2C same transfer is initiated only from the host side. Those. You need to form a certain query in order to get an answer. But what?
Google’s Bug on BN-880 I2C did not give anything useful for a couple of hours. The people are just using UART, and most of the links led to quadro-cops forums and there were mostly quad-copter problems discussed there.
It was not so easy to get out on datasheets. Those. It was not entirely clear on which module to look for the datasheet. By indirect evidence, I found out that the UBlox NEO-M8N module is responsible for the GPS. It turned out that this thing knows how many features that the mother does not grieve (there’s even a built-in odometer and a logger there). But I had to read 350 pages a lot.
I went through the dasad and I realized that I can not take this module with a pounce. I had to step on my throat and connect to the already verified UART. And then immediately enter into another problem: on the Arduin UART only one, and he sticks out in the direction of the company (fill in the firmware). I had to look in the direction of the SoftwareSerial library.
I wrote the simplest “relinker” of messages from the GPS port in UART.
SoftwareSerial gpsSerial ( 10, 11); // RX, TX
Void setup ()
{
Serial.begin (9600);
GpsSerial.begin (9600);
}
Void loop ()
{
If (gpsSerial.available ()) {
Serial.write (gpsSerial.read ());
}
}
Messages were poured, but satellites could not catch. Although the time was right.
$ GNRMC, 203954.00, V ,,,,,,,,,, N * 6A
$ GNVTG ,,,,,,,,, N * 2E
$ GNGGA, 203954.00 ,,,,, 0,00,99.99 ,,,,,, * 71
$ GNGSA, A, 1 ,,,,,,,,,,,,, 99.99.99.99.99.99 * 2E
$ GNGSA, A, 1 ,,,,,,,,,,,,, 99.99.99.99.99.99 * 2E
$ GPGSV, 1,1,02,02 ,,, 21,08 ,,, 09 * 7B
$ GLGSV, 1,1,00 * 65
$ GNGLL ,,,,, 203954.00, V, N * 5D
GPS lay for more than an hour at the window of 21 floors before issuing sane coordinates. And most of the review is not closed by high-rise buildings. There is a suspicion that the windows have a certain spraying, which worsens the quality of the signal. In any case, near the open window, satellites seem to catch faster.
If there is a signal, then you can parse. On the Internet, TinyGPSPlus was the first to be found. Has connected not without hacks. In ArduinoIDE everything worked, but at Atmel Studio did not want. I had to manually write the way to the library.
But then the problem got out. On simple sketches from the examples of TinyGPS + everything worked. But when I connected it to my project with the display and buttons everything broke. The device tangibly missed, obviously skipping the screen’s rendering. In the port monitor, I began to notice the crumbled messages from the GPS.
The first assumption was that SoftwareSerial very seriously wasted processor resources. So SoftwareSerial needs to be sent to the furnace, because For reliable communication with GPS it is not suitable (at least in the form in which it is in the examples). I even wanted to turn the scheme inside out: GPS connect to the hardware UART’u arduiny, and the software series to use for debug (although if you have a screen to debug through the UART may not even be required). But with such a scheme, it will not be possible to download the firmware via UART. I had to get the USBAsp programmer.
But a little later I realized that the SoftwareSerial thing is gluttonous, but in this case the problem is not in it, but in the drawing function. Drawing the current screen takes 50-75ms (plus a bit more for overhead). SoftwareSerial works on receiving an interrupt on the leg controller and a lot, in general, it should not consume. But it has a receive buffer of only 64 bytes, which even at 9600 is filled in 60ms. It turns out that while the program is busy drawing the screen, some of the message from GPS is already passing by.
In the first half of the article, I get a lot of text. I’ll dilute them with pictures of them. This screen displays the current altitude and vertical speed
ARM
So. With the current approach, I rested at once in several restrictions:
- Flash and RAM. Not so much that was busy, but you had to constantly remember this
- There is only one UART. Additional SoftwareSerial consumes CPU resources significantly.
- It’s clearly impossible to do everything in one thread. We need to think about parallelizing tasks.
And I also had to design with the expectation of the future – I still have the connection of USB and SD cards.
After the release of the previous part, I received many comments that Arduino sucks and the future behind ARM and STM32 controllers. I did not really want to leave the Arduino platform. As I said, the framework is fairly simple and understandable, and I also know the ATMega controllers well.
At the same time, the transition to STM32 would most likely mean a change in the platform as a whole, the microcontroller, the compiler, the framework, the libraries, the IDE and who knows what else. Those. Almost all at once. For the project, this would mean completely stop, study the documentation for a long time, study different examples, and only then start copying everything from scratch.
I began to feel the exits from the situation, listening to the commentators of the first part. I wanted to find a solution that solved the restrictions, gave some reserve for the future, but it did not require huge resources to move everything at once. Here are a few (in general, independent) things I went through.
- I connected a Sparkfun Pro Micro clone to ATMega32u4 (3.3V, 8MHz). In it, I wanted to touch the hardware USB. It took me quite a lot of time to start this thing at all. A regular bootloader did not really like to start up like an arduino, and fuse bits were put up in some mysterious way. As a result, using USBAsp sewed a bootloader from Arduino Leonardo and everything started.
- A debug board for ATMega64 came. It has 2 times more memory (both flash and RAM) and 2 uart. In principle, it removes restrictions. Unfortunately, there is no circuit attached to the board and what kind of quartz is there too is not clear. While postponed.
- I tried to touch the port of FreeRTOS under AVR. But here Kaku has put Atmel Studio. It turned out that she has 2 kinds of projects. In one studio works in Arduino mode, but in this case, practically nothing can be changed in the project settings. Those. It’s trivial to even put FreeRTOS in a subdirectory and set the include path. It only knows how to add all the files in one heap, which would personally irritate me.The second option is the Generic C ++ Executable project type. It is understood that you need to write on bare C ++. Here you can already configure as your heart desires. But firstly it is necessary as that to fasten Arduinovsky frameworks, and secondly it is not clear how to fasten the filler of the firmware to the controller. Avrdude stubbornly did not want to overload the microcontroller in the bootloader (although the command line I peeked at ArduinoIDE using ProcessMonitor). I have USBasp, but if there is a USB port directly on the card, it’s like it’s not comme il faut.
- Finally, I combed the comb on the board with STM32F103C8T6 and, according to the instructions, installed STM32duino. To my surprise, the light-emitting diode on the LEDs immediately started working. To even more surprise, porting my project to a new controller took less than 10 minutes !!! In total, a couple of inkludov change and the number of pins to correct.
That was it. I was getting the power of STM32 (yes, the drawing function was now only 18ms long!) And I could still use the Arduino framework. This made it possible to continue working on the project, while, as necessary, smoothly dive into the new platform, reading in the metro on the microcontroller.
The increase in the flush is, in fact, a very illusory improvement. The project took half the flush on ATmega32, and it takes almost half on the new STM32 (okay, 26k from 64k). So you should not relax. Especially (as they write on the Internet), the compiled code is somewhat more sweeping and fills the flash faster than on the AVR. So just in case ordered a handkerchief with a 128k flush.
The truth here I was waiting for another surprise. The people on the Internet wrote that although the controller on datasheet has a 64K flush on board, in fact you can use 128k. Those. It seems that ST produces the same chip, only the part marks STM32F103C8T6, and another as STM32F103CBT6 (the same controller, but with a 128K flush).
By the way (follow up after the previous article). In the ARM architecture, both flash and RAM are in the same address space and are read in a single way. Therefore, dances with a tambourine and declaration of constants with the help of PROGMEM are no longer needed. Poole for the sake of cleanliness of the code. Tables of virtual functions, too, can not be copied anywhere; They are also in the same address space.
Another picture for diluting the text. From left to right: the direction of movement (now we are not going anywhere), the current speed, the current altitude. The screen is honestly licked with a similar one from Holux M241
FreeRTOS’im
The STM32duino also found a FreeRTOS port for my controller (and already two – 7.0.1 and 8.2.1). Examples with minimal corrections also earned. So you could switch to FreeRTOS without overwriting a significant part of the project.
After reading a couple of articles (one, two), I realized what power is now available to me – streams, mutexes, queues, semaphores and other synchronization. It’s like on big computers. The main thing is to design everything correctly.
Despite the fact that the main problem I had was GPS, I still decided to start with something simpler – the buttons. In a sense, FreeRTOS makes code much simpler – each thread can deal with some specific task, and, if necessary, notify other threads. So, the task of servicing the buttons perfectly lodged in this ideology – listen to yourself buttons and do not get distracted by anything. Step aside
static void selButtonPinHandler ()
{
Static uint32 lastInterruptTime = 0;
If (digitalRead (SEL_BUTTON_PIN)) // Falling edge
{
Uint32 cur = millis ();
Uint32 pressDuration = cur - lastInterruptTime;
Serial.print ("DePressed at");
Serial.println (lastInterruptTime);
If (pressDuration> LONG_PRESS_TIMEOUT)
Serial.println ("Sel Long Press");
Else
If (pressDuration> SHORT_CLICK_TIMEOUT)
Serial.println ("Sel Short Click");
Else
{
Serial.print ("Click was too short:");
Serial.println ((int) pressDuration);
}
}
LastInterruptTime = millis ();
If (! DigitalRead (SEL_BUTTON_PIN)) // Raising edge
{
Serial.print ("Pressed at");
Serial.println (lastInterruptTime);
}
}
Void initButtons ()
{
// Set up button pins
PinMode (SEL_BUTTON_PIN, INPUT_PULLUP); // TODO: using PullUps is an AVR legacy. Consider changing this to pull down
PinMode (OK_BUTTON_PIN, INPUT_PULLUP); // so pin state match human logic expectations
AttachInterrupt (SEL_BUTTON_PIN, selButtonPinHandler, CHANGE);
}
Instead of prints, there had to be a sending of messages about the button that was pressed.
But honestly, it turned out to be cumbersome (it’s processing only one button) and it’s also terribly buggy. There were some kind of false alarms, or vice versa non-triggering. It seems that the function millis () did something wrong and could return the same values for a fairly long time.
Let me remind you. In the main program cycle, I had a large state machine that controlled the display and listened to the buttons. Adding some kind of logic was accompanied by half of the code redrawing, and to understand how it works just looking at the code under the force was just the programming guru of the state machines. But since I have a PTM, everything worked out much easier.
// Pins assignment
Const uint8 SEL_BUTTON_PIN = PC14;
Const uint8 OK_BUTTON_PIN = PC15;
// Timing constants
Const uint32 DEBOUNCE_DURATION = 1 / portTICK_PERIOD_MS;
Const uint32 LONG_PRESS_DURATION = 500 / portTICK_PERIOD_MS;
Const uint32 VERY_LONG_PRESS_DURATION = 1000 / portTICK_PERIOD_MS;
Const uint32 POWER_OFF_POLL_PERIOD = 1000 / portTICK_PERIOD_MS; // Polling very rare when power is off
Const uint32 IDLE_POLL_PERIOD = 100 / portTICK_PERIOD_MS; // And little more frequent if we are on
Const uint32 ACTIVE_POLL_PERIOD = 10 / portTICK_PERIOD_MS; // And very often when the user is active.
QueueHandle_t buttonsQueue;
// Reading button state (perform debounce first)
Inline bool getButtonState (uint8 pin)
{
If (digitalRead (pin))
{
// dobouncing
VTaskDelay (DEBOUNCE_DURATION);
If (digitalRead (pin))
Return true;
}
Return false;
}
/// Return ID of the pressed button (perform debounce first)
ButtonID getPressedButtonID ()
{
If (getButtonState (SEL_BUTTON_PIN))
Return SEL_BUTTON;
If (getButtonState (OK_BUTTON_PIN))
Return OK_BUTTON;
Return NO_BUTTON;
}
// Initialize buttons related stuff
Void initButtons ()
{
// Set up button pins
PinMode (SEL_BUTTON_PIN, INPUT_PULLDOWN);
PinMode (OK_BUTTON_PIN, INPUT_PULLDOWN);
// Initialize buttons queue
ButtonsQueue = xQueueCreate (3, sizeof (ButtonMessage)); // 3 clicks more than enough
}
// Buttons polling thread function
Void vButtonsTask (void * pvParameters)
{
For (;;)
{
// Wait for a button
ButtonID btn = getPressedButtonID ();
If (btn! = NO_BUTTON)
{
// Button pressed. Waiting for release
TickType_t startTime = xTaskGetTickCount ();
While (getPressedButtonID ()! = NO_BUTTON)
VTaskDelay (ACTIVE_POLL_PERIOD);
// Prepare message to send
ButtonMessage msg;
Msg.button = btn;
// calc duration
TickType_t duration = xTaskGetTickCount () - startTime;
If (duration> VERY_LONG_PRESS_DURATION)
Msg.event = BUTTON_VERY_LONG_PRESS;
Else
If (duration> LONG_PRESS_DURATION)
Msg.event = BUTTON_LONG_PRESS;
Else
Msg.event = BUTTON_CLICK;
// Send the message
XQueueSend (buttonsQueue, & msg, 0);
}
// TODO: Use different polling periods depending on the global system state (off / idle / active)
VTaskDelay (ACTIVE_POLL_PERIOD);
}
}
It turned out very compact and understandable. The functions are all very linear. Just in the loop, we interrogate the buttons and, based on the duration of the click, send the corresponding message.
I decided that I will have 3 kinds of pressing times:
- Short for selecting the corresponding menu item
- Long for a special action (for example, resetting the selected parameter)
- A very long press to turn the device on and off
By the way, I decided to connect the buttons not to the plus, but to the minus. Naturally pull-up resistors replaced by pull-down. I am not strong in electronics and I can make mistakes here, but in general I was guided by the following considerations:
- In the released position of the button, the pin is pressed to zero, and so the current does not flow (even miserable)
- When reading the value from pin, the value is obtained non-inverted: 1 if the button is pressed, 0 – released
ScreenManager is also much simpler. There was no need for a global display state. The flow of rendering is exclusively rendered and is controlled by messages from the buttons. He just waited for messages in the queue and worked through the received commands. And the waiting cycle itself was also done through the queue with the help of timeout in the function xQueueReceive. Those. The function waits for the message, and if nothing happens for a long time – just draws the screen as it is
void vUserInteractionTask (void * pvParameters)
{
For (;;)
{
// Poll the buttons queue for an event. Process button if pressed, or show current screen as usual if no button pressed
ButtonMessage msg;
If (xQueueReceive (buttonsQueue, & msg, DISPLAY_CYCLE))
ProcessButton (msg);
// Do what we need for the current state
DrawDisplay ();
}
}
It turned out, in my opinion, very elegant. Later, I added a screen shutdown after some timeout (saving the battery), but the code did not become much more complicated.
Button handling is just trivial – just parsim message and call the required function
void processButton (const ButtonMessage & msg)
{
If (msg.button == SEL_BUTTON && msg.event == BUTTON_CLICK)
GetCurrentScreen () -> onSelButton ();
If (msg.button == OK_BUTTON && msg.event == BUTTON_CLICK)
GetCurrentScreen () -> onOkButton ();
// TODO: process long press here
}
The showMessageBox () function has also become much simpler and has now become completely linear
void showMessageBox (const char * text)
{
// Center text
Uint8_t x = 128/2 - strlen_P (text) * 6/2;
// Draw the message
Display.clearDisplay ();
Display.setFont (NULL);
Display.drawRect (2, 2, 126, 30, 1);
Display.setCursor (x, 12);
Display.print (text);
Display.display ();
// Wait required duration
VTaskDelay (MESSAGE_BOX_DURATION);
}
And finally. What is this device, if it does not have a blinking light bulb? It is necessary to correct. No matter how ridiculous it was, it’s easy to see whether the device is still working on the blinking diode.
void vLEDFlashTask (void * pvParameters )
{
For (;;)
{
VTaskDelay (2000);
DigitalWrite (PC13, LOW);
VTaskDelay (100);
DigitalWrite (PC13, HIGH);
}
}
Опять GPS’им
Наконец, настало время терзать GPS. Теперь уже нет проблемы одновременно слушать GPS и делать все остальное. Для начала я опять написал переливатор:
void initGPS()
{
// GPS is attached to Serial1
Serial1.begin(9600);
}
void vGPSTask(void *pvParameters)
{
for (;;)
{
while(Serial1.available())
{
int c = Serial1.read();
gps.encode(c);
Serial.write(c);
}
vTaskDelay(5);
}
}
Но тут возникла проблема. Сообщения формально парсились, только вот даже время выкусить оттуда не получилось. Курение исходников TinyGPS и документации на приемник показало небольшое несоответствие сообщений от GPS модуля и тем что умеет парсить библиотека.
Модуль UBlox реализует некое расширение протокола NMEA. Каждое сообщение начинается с пятибуквенного идентификатора сообщения.
$GNGGA,181220.00,,,,,0,00,99.99,,,,,,*70
Первые 2 буквы кодируют подсистему, которая приготовила данные: GP для GPS, GL для GLONASS, GA для GALILLEO. А вот если используется комбинация систем позиционирования то сообщения будут начинаться с GN.
Библиотека TinyGPS+ на такое рассчитана не была — она умела парсить только сообщения GP. Пришлось ее чуток подправить — поменял соответствующую строку в парсере и время на экране побежало. Только вот это все попахивало каким то хаком.
Товарищ подсказал альтернативу — библиотеку NeoGPS. Это намного более фичастая библиотека. Помимо того, что она умеет парсить сообщения с разными префиксами, она еще позволяет парсить информацию о спутниках (лично мне нравятся такие штуки в GPS приемниках). Еще стоит отметить, что библиотека жутко конфигуряемая — можно включить/выключить парсинг отдельных сообщений и тем самым регулировать потребление памяти в зависимости от задач.
Подключить библиотеку к stm32duino труда не составило, правда чуток подпилить все же пришлось. Но как всегда в примерах все просто и понятно, а в реальном проекте оно сразу не заработало. В частности было неясно в какой момент времени правильно читать из GPS. Вот, например, попытка вычитать данные о спутниках.
for (;;)
{
while(Serial1.available())
{
int c = Serial1.read();
Serial.write(c);
gpsParser.handle(c);
}
if(gpsParser.available())
{
memcpy(satellites, gpsParser.satellites, sizeof(satellites));
sat_count = gpsParser.sat_count;
}
vTaskDelay(10);
}
Время от времени парсер говорит, что данные прибыли — забирайте. Координаты всегда приезжают нормально, а вот со спутниками беда. Забираю, а там нули. Или не нули. Как повезет.
Оказалось нужно было внимательно прочитать документацию. Все дело в дизайне библиотеки. Во имя экономии памяти данные раскладываются по переменным по ходу парсинга. При чем побайтово — пришел байт, обновили переменную. Данные приходят пакетами по несколько сообщений. Библиотека NeoGPS должна знать когда начинается новый пакет, чтобы обнулить внутренние переменные. За это отвечает параметр конфигурации LAST_SENTENCE_IN_INTERVAL
//------------------------------------------------------
// Select which sentence is sent *last* by your GPS device
// in each update interval. This can be used by your sketch
// to determine when the GPS quiet time begins, and thus
// when you can perform "some" time-consuming operations.
#define LAST_SENTENCE_IN_INTERVAL NMEAGPS::NMEA_RMC
Так вот сообщение RMC у меня приходит самым первым в пакете сообщений. Получается что мой код мог прочитать частично распаршеные данные (Возможно это были данные предыдущих пакетов, которые еще не успели обнулится). Или вычитывать нули, если прочитать в неудачное время. Лечится довольно просто: указываем, что в каждом пакете от GPS модуля последнее сообщение у нас GLL.
Спутников много, а фикса все нет и нет. Сверху вниз: количество спутников (отслеживаемые vs неотслеживаемые — не знаю что это значит), HDOP/VDOP, Статус GPS сигнала (словило/не словило)
Кстати, с библиотекой в комплекте обнаружились довольно удобные функции по работе с датой и временем. Так, например, очень легко было прикрутить часовой пояс. Я только храню временнОе смещение в минутах, а остальное легко высчитать по ходу.
void TimeZoneScreen::drawScreen() const
{
// Get the date/time adjusted by selected timezone value
gps_fix gpsFix = gpsDataModel.getGPSFix();
int16 timeZone = getCurrentTimeZone();
NeoGPS::time_t dateTime = gpsFix.dateTime + timeZone * 60; //timeZone is in minutes
...
printNumber(dateBuf, dateTime.date, 2);
printNumber(dateBuf+3, dateTime.month, 2);
printNumber(dateBuf+6, dateTime.year, 2);
Экран выбора часового пояса честно слизан с Hulux’а
Model-View’им
При написания кода теперь нельзя забывать, что мы работам в многопоточной среде. Так, у меня есть поток, который обслуживает GPS: слушает Serial порт, побайтово парсит из него данные. Пакеты приходят раз в секунду. Библиотека знает, когда начинается следующий пакет и перед приемом обнуляет внутренние переменные. Когда пакет полностью принят выставляется флаг available. Данные приезжают на протяжении примерно за полсекунды (там байт 600 на скорости 9600). У нас есть еще полсекунды, чтобы их забрать, прежде чем начнется передача следующего пакета.
Второй поток занимается обслуживанием дисплея. Цикл отрисовки происходит каждые 100-120мс. На каждой итерации программа берет актуальные данные из GPS и отрисовывает то, что сейчас хочет видеть пользователь — координаты, скорость, высоту или что нибудь еще. И тут возникает противоречие: поток дисплея хочет получать данные всегда, тогда как в библиотеке они доступны только полсекунды, а потом перезатираются.
Решение достаточно очевидное: скопировать данные к себе в промежуточный буфер. Естественно данные в этом буфере нужно защитить мутексом (mutex), иначе данные могут быть вычитаны некорректно. Но вот в чем проблема. Данные в потоке GPS появляются хоть и редко, но вычитать их можно быстро (там всего полторы сотни байт после парсинга), мутекс надолго блокировать не нужно. А вот функция рисования может работать довольно долго (до 20мс). Блокировать мутекс на такое длительное время, в общем то, не сильно хорошо. Хотя и не смертельно, в этом конкретном проекте.
Можно, конечно, быстренько заблокировать мутекс, забрать данные в локальную переменную и отпустить мутекс. Но это чревато перерасходом памяти. Еще полторы сотни байт при 20 килобайтах это фигня, но лично меня напрягает сам факт тройной буферизации.
Буфер, кстати, пришлось объявить глобальной переменной ибо он очень большой и вызывает переполнение стека, если объявлять его в функции. На всякий случай потоку рисования выписал стека побольше.
NMEAGPS::satellite_view_t l_satellites[ NMEAGPS_MAX_SATELLITES ];
uint8_t l_sat_count;
void SatellitesScreen::drawScreen()
{
xSemaphoreTake(xGPSDataMutex, portMAX_DELAY);
memcpy(l_satellites, satellites, sizeof(l_satellites));
l_sat_count = sat_count;
xSemaphoreGive(xGPSDataMutex);
display.draw(....)
...
}
С мгновенными значениями, которые можно достать прямо из NMEA потока все просто — библиотека NeoGPS их вычитывает и раскладывает по переменным. Каждый скрин может просто прочитать соответствующую переменную (не забывая про синхронизацию, конечно) и отобразить ее на экране. Но вот с переменными, которые нужно вычислять так просто не получилось.
После долгого размышления я пришел к классической model-view схеме.
Объекты-наследники screen являются вьюшками — они отображают различные данные из модели, но сами данные не производят. Вся логика лежит в классе GPSDataModel. Он отвечает за хранение мгновенных GPS данных (пока не приедут новые данные из NeoGPS). Так же он отвечает за вычисление новых данных, таких как одометры или вертикальная скорость. И последнее, но не менее важное — этот класс сам занимается всей синхронизацией для своих данных.
const uint8 ODOMERTERS_COUNT = 3;
/**
* GPS data model. Encapsulates all the knowledge about various GPS related data in the device
*/
class GPSDataModel
{
public:
GPSDataModel();
void processNewGPSFix(const gps_fix & fix);
void processNewSatellitesData(NMEAGPS::satellite_view_t * sattelites, uint8_t count);
gps_fix getGPSFix() const;
GPSSatellitesData getSattelitesData() const;
float getVerticalSpeed() const;
int timeDifference() const;
// Odometers
GPSOdometerData getOdometerData(uint8 idx) const;
void resumeOdometer(uint8 idx);
void pauseOdometer(uint8 idx);
void resetOdometer(uint8 idx);
void resumeAllOdometers();
void pauseAllOdometers();
void resetAllOdometers();
private:
gps_fix cur_fix; /// most recent fix data
gps_fix prev_fix; /// previously set fix data
GPSSatellitesData sattelitesData; // Sattelites count and signal power
GPSOdometer * odometers[ODOMERTERS_COUNT];
bool odometerWasActive[ODOMERTERS_COUNT];
SemaphoreHandle_t xGPSDataMutex;
GPSDataModel( const GPSDataModel &c );
GPSDataModel& operator=( const GPSDataModel &c );
}; //GPSDataModel
/// A single instance of GPS data model
extern GPSDataModel gpsDataModel;
Т.к. класс модели отвечает за синхронизацию данных между потоками, то в нем живет мутекс, который регулирует доступ к внутренним полям класса. Мне было жутко неудобно (и некрасиво) пользоваться голыми xSemaphoreTake()/xSemaphoreGive(), так что я нарисовал классический автозахватыватель (точнее даже автоотпускатель).
class MutexLocker
{
public:
MutexLocker(SemaphoreHandle_t mtx)
{
mutex = mtx;
xSemaphoreTake(mutex, portMAX_DELAY);
}
~MutexLocker()
{
xSemaphoreGive(mutex);
}
private:
SemaphoreHandle_t mutex;
};
Забрать текущее значение очень просто. Нужно просто вызвать функцию getGPSFix(), которая просто вернет копию данных.
gps_fix GPSDataModel::getGPSFix() const
{
MutexLocker lock(xGPSDataMutex);
return cur_fix;
}
Клиенту не нужно парится про блокировки и все такое. Просто забираем данные и рисуем как надо.
void SpeedScreen::drawScreen() const
{
// Get the gps fix data
gps_fix gpsFix = gpsDataModel.getGPSFix();
// Draw speed
...
printNumber(buf, gpsFix.speed_kph(), 4, true);
В классе модели хранится не только самые последние данные (cur_fix), но также предыдущее значение (prev_fix). Так что, вычисление вертикальной скорости становится тривиальной задачей.
float GPSDataModel::getVerticalSpeed() const
{
MutexLocker lock(xGPSDataMutex);
// Return NAN to indicate vertical speed not available
if(!cur_fix.valid.altitude || !prev_fix.valid.altitude)
return NAN;
return cur_fix.altitude() - prev_fix.altitude(); // Assuming that time difference between cur and prev fix is 1 second
}
С данными про спутники получилось весьма интересно. Данные про спутники живут в массиве структур NMEAGPS::satellite_view_t. Массив весит 150 байт и, как я уже писал, его необходимо несколько раз копировать. Не так, чтобы критично при наличии 20кб оперативы, но все равно это трижды по 150 байт.
В конце концов я понял, что мне не нужны все данные, достаточно скопировать себе только то, что реально используется. В итоге родился вот такой класс.
class GPSSatellitesData
{
// Partial copy of NMEAGPS::satellite_view_t trimmed to used data
struct SatteliteData
{
uint8_t snr;
bool tracked;
};
SatteliteData satellitesData[SAT_ARRAY_SIZE];
uint8_t sat_count;
public:
GPSSatellitesData();
void parseSatellitesData(NMEAGPS::satellite_view_t * sattelites, uint8_t count);
uint8_t getSattelitesCount() const {return sat_count;}
uint8_t getSatteliteSNR(uint8_t sat) const {return satellitesData[sat].snr;}
bool isSatteliteTracked(uint8_t sat) const {return satellitesData[sat].tracked;}
};
Такой класс уже не так обидно лишний раз копировать — он занимает всего 40 байт.
Самой сложной частью схемы получился класс GPSOdometer. Как следует из названия он отвечает за все вычисления связанные с функциональностью одометра.
// This class represents a single odometer data with no logic around
class GPSOdometerData
{
// GPSOdometer and its data are basically a single object. The difference is only that data can be easily copied
// while GPS odometer object is not supposed to. Additionally access to Odometer object is protected with a mutex
// in the model object
// In order not to overcomplicte design I am allowing GPS Odometer to operate its data members directly.
friend class GPSOdometer;
bool active;
NeoGPS::Location_t startLocation;
NeoGPS::Location_t lastLocation;
float odometer;
int16 startAltitude;
int16 curAltitude;
clock_t startTime; ///! When odometer was turned on for the first time
clock_t sessionStartTime; ///! When odometer was resumed for the current session
clock_t totalTime; ///! Total time for the odometer (difference between now and startTime)
clock_t activeTime; ///! Duration of the current session (difference between now and sessionStartTime)
clock_t activeTimeAccumulator; ///! Sum of all active session duration (not including current one)
float maxSpeed;
public:
GPSOdometerData();
void reset();
// getters
bool isActive() const {return active;}
float getOdometerValue() const {return odometer;}
int16 getAltitudeDifference() const {return (curAltitude - startAltitude) / 100.;} // altitude is in cm
clock_t getTotalTime() const {return totalTime;}
clock_t getActiveTime() const {return activeTimeAccumulator + activeTime;}
float getMaxSpeed() const {return maxSpeed;}
float getAvgSpeed() const;
float getDirectDistance() const;
};
// This is an active odometer object that operates on its odometer data
class GPSOdometer
{
GPSOdometerData data;
public:
GPSOdometer();
// odometer control
void processNewFix(const gps_fix & fix);
void startOdometer();
void pauseOdometer();
void resetOdometer();
// Some data getters
GPSOdometerData getData() {return data;}
bool isActive() const {return data.isActive();}
}; //GPSOdometer
Сложность вот в чем. Объект gps_fix, который поступает нам от GPS, может содержать какие то данные, а какие то может и нет. Например координата приедет, а высота нет. А в следующем фиксе может быть наоборот. Поэтому просто сохранять gps_fix не выйдет. Нужно каждый раз смотреть что доступно в новом фиксе, а что нет. Поэтому пришлось городить весьма сложный алгоритм, запоминать координаты, высоты и временные отметки по отдельности.
void GPSOdometer::processNewFix(const gps_fix & fix)
{
Serial.print("GPSOdometer: Processing new fix ");
Serial.println((int32)this);
if(data.active)
{
Serial.println("Active odometer: Processing new fix");
// Fill starting position if needed
if(fix.valid.location && !isValid(data.startLocation))
data.startLocation = fix.location;
// Fill starting altitude if neede
if(fix.valid.altitude && !data.startAltitude) // I know altitude can be zero, but real zero cm altutude would be very rare condition. Hope this is not a big deal
data.startAltitude = fix.altitude_cm();
// Fill starting times if needed
if(fix.valid.time)
{
if(!data.startTime)
data.startTime = fix.dateTime;
if(!data.sessionStartTime)
data.sessionStartTime = fix.dateTime;
}
// Increment the odometer
if(fix.valid.location)
{
// but only if previous location is really valid
if(isValid(data.lastLocation))
data.odometer += NeoGPS::Location_t::DistanceKm(fix.location, data.lastLocation);
// In any case store current (valid) fix
data.lastLocation = fix.location;
}
// Store current altitude
if(fix.valid.altitude)
data.curAltitude = fix.altitude_cm();
// update active time values
if(fix.valid.time)
data.activeTime = fix.dateTime - data.sessionStartTime;
// update max speed value
if(fix.valid.speed && fix.speed_kph() > data.maxSpeed)
data.maxSpeed = fix.speed_kph();
}
//Total time can be updated regardless of active state
if(fix.valid.time && data.startTime)
data.totalTime = fix.dateTime - data.startTime;
}
В этом месте у меня резко увеличился размер флеша — почти на 10кб. В проект приползла куча математического кода — синусы, косинусы, тангенсы, квадратные корни и все такое прочее. Оказалось, что ноги растут из функции NeoGPS::Location_t::DistanceKm() — все это используется в вычислении расстояния на основе координат. Скрипя зубами пришлось согласится, но задумался о контроллере на Cortex M4 — там это хардварно должно вычисляться.
void GPSOdometer::startOdometer()
{
data.active = true;
// Reset session values
data.sessionStartTime = 0;
data.activeTime = 0;
}
void GPSOdometer::pauseOdometer()
{
data.active = false;
data.activeTimeAccumulator += data.activeTime;
data.activeTime = 0;
}
void GPSOdometer::resetOdometer()
{
data.reset();
}
Обратите внимание, что в классе одометра нет никакой синхронизации. Это потому, что вся синхронизация происходит в классе GPSDataModel. Я просто не хотел городить по мутексу в каждом объекте. Но из-за этого мне пришлось усложнить сам класс одометра и разделить на 2 класса: объект с данными (GPSOdometerData) может копироваться по запросу клиентов, тогда как объект управления (GPSOdometer) создаются один раз на каждый одометр. Из-за этого также пришлось один класс сделать friend’ом другому. Возможно я пересмотрю этот дизайн в будущем.
Так выглядит основной экран одометра. Символ точки в шрифт еще не добавил — должно показывать 0.42км. Так же отображается перепад высот — лежа на месте на подоконнике запросто можно перепасть на 18 и более метров.
Другие полезные параметры, которые могут отображаться одометром. На один экран все даже не вместилось — буду делать 2 или даже 3 экрана.
GPSDataModel может управлять всеми одометрами сразу. Эта фича была предложена в коментариях и должна быть удобной — зашел в кафе, выключил все одометры сразу. Вышел — включил их опять.
void GPSDataModel::resumeAllOdometers()
{
MutexLocker lock(xGPSDataMutex);
if(odometerWasActive[0])
odometers[0]->startOdometer();
if(odometerWasActive[1])
odometers[1]->startOdometer();
if(odometerWasActive[2])
odometers[2]->startOdometer();
}
void GPSDataModel::pauseAllOdometers()
{
MutexLocker lock(xGPSDataMutex);
odometerWasActive[0] = odometers[0]->isActive();
odometerWasActive[1] = odometers[1]->isActive();
odometerWasActive[2] = odometers[2]->isActive();
odometers[0]->pauseOdometer();
odometers[1]->pauseOdometer();
odometers[2]->pauseOdometer();
}
void GPSDataModel::resetAllOdometers()
{
MutexLocker lock(xGPSDataMutex);
odometers[0]->resetOdometer();
odometers[1]->resetOdometer();
odometers[2]->resetOdometer();
odometerWasActive[0] = false;
odometerWasActive[1] = false;
odometerWasActive[2] = false;
}
Опять FreeRTOS’им
В целях изучения возможностей FreeRTOS я попробовал посмотреть сколько же на самом деле времени процессор проводит в вычислениях. Для оценки можно использовать ApplicationIdleHook.
У любой РТОС есть так называемый idle поток. Если процессору нечем себя занять — крутится некий бесконечный цикл в отдельной задаче с наименьшим приоритетом. FreeRTOS позволяет добавить некоторой полезности в этот бесконечный цикл и запускать этот хук. Идея измерения загрузки процессора состоит в том, что чем больше процессор проводит времени в idle потоке — тем меньше он загружен другой (полезной) работой.
В интернетах я нашел несколько подходов как можно было бы измерять загрузку процессора.
Одни ребята предлагали в Idle Hook функции крутить некий счетчик и измерять скорость с которой он «наматывает». Чтобы перевести это в проценты нужно полученную скорость поделить на некие эталонное значение.
Но где взять эту эталонную скорость? Для этого нужно погасить все другие потоки и мерять только скорость счетчика в ненагруженной системе. Можно, например, на старте сделать задержку в 1-2 секунды для измерений, но лично меня жутко бесит когда достаточно простые устройства «грузятся» по 5-10 секунд (например, фотоаппараты-мыльницы. грррр).
В другом варианте из интернета предлагалось запустить отдельный таймер и перегрузить макрос входа и выхода в контекст потока. Идея в том, чтобы измерять разницу значений таймера на входе и на выходе в каждый поток и из этого делать вывод о загрузке процессора.
Да, я слышал о Run Time Stats в системе FreeRTOS. Но, как гласит инструкция, оно предназначено для другого. Эта функция позволяет в дебажных целях получить загрузку по каждому отдельному потоку и за весь период работы приложения. Я же хотел измерять мгновенную загрузку процессора.
Я решил попробовать сделать следующий вариант. Не знаю насколько это правильно и будет ли оно вообще работать, когда я прикручу sleep mode. Но на данном этапе работает неплохо.
static const uint8 periodLen = 9; // 2^periodLen ticks - 512 x 1ms ticks
volatile TickType_t curIdleTicks = 0;
volatile TickType_t lastCountedTick = 0;
volatile TickType_t lastCountedPeriod = 0;
volatile TickType_t lastPeriodIdleValue = 0;
volatile TickType_t minIdleValue = 1 << periodLen;
extern "C" void vApplicationIdleHook( void )
{
// Process idle tick counter
volatile TickType_t curTick = xTaskGetTickCount();
if(curTick != lastCountedTick)
{
curIdleTicks++;
lastCountedTick = curTick;
}
// Store idle metrics each ~0.5 seconds (512 ticks)
curTick >>= periodLen;
if(curTick > lastCountedPeriod)
{
lastPeriodIdleValue = curIdleTicks;
curIdleTicks = 0;
lastCountedPeriod = curTick;
// Store the max value
if(lastPeriodIdleValue < minIdleValue)
minIdleValue = lastPeriodIdleValue;
}
}
Функция может вызываться очень часто, много раз за один тик системы (system tick это 1мс). Поэтому первый блок отвечает за подсчет тиков (а не вызовов) в которых вызывался хук. Второй блок сохраняет счетчик каждые 512 системных тиков.
Загрузка процессора это отношение количества не-idle тиков к общему количеству тиков в измеряемом интервале.
float getCPULoad()
{
return 100. - 100. * lastPeriodIdleValue / (1 << periodLen);
}
float getMaxCPULoad()
{
return 100. - 100. * minIdleValue / (1 << periodLen);
}
Да, это может быть не совсем точно. Но в целом для получения некой грубой оценки по загрузке системы вполне покатит. Я собираюсь использовать эти показатели для понижения частоты контроллера чтобы снизить потребление.
К слову, в нормальном режиме загрузка составила около 12.5% и подпрыгивает до 15.5% когда приходят данные от GPS и их нужно парсить. При выключенном дисплее (хотя GPS продолжает парсится) загрузка падает до 0. Это странно. Видимо парсинг GPS на самом деле занимает меньше тика, поэтому каждый тик после этого сваливается в idle task. Всплеск загрузки на 3% возможно объясняется не самим парсингом данных, а пересылкой их в другой поток.
Хотя возможно я где-то тут попросту накосячил.
Показания текущей и максимальной загрузки процессора. Сам экран будет спрятан где нибудь в глубинах меню настроек.
Всяко разно
В этой секции я собрал отдельные проблемки, которые я решал на разных стадиях проекта. Без какой либо особенной последовательности.
- Библиотечная реализация sprintf занимает огого сколько — 13k. Пришлось написать свою реализацию. Я написал небольшой классик, который реализует интерфейс Printable. Так можно “печатать” числа с нужным форматированием на экран и даже в Serial. Получилось весьма симпатичненько и всего пару экранов кода.Форматировщик чисел с плавающей точкой
/// Helper class to print float numbers according to specified options class FloatPrinter : public Printable { char buf[8]; // Print numbers no longer than 7 digits including sign and point symbols uint8 pos; // position in the buffer with the first meaningful char public: FloatPrinter(float value, uint8 width, bool leadingZeros = false, bool alwaysPrintSign = false); virtual size_t printTo(Print& p) const; }; FloatPrinter::FloatPrinter(float value, uint8 width, bool leadingZeros, bool alwaysPrintSign) { // reserve a space for sign uint8 minpos = 0; if(alwaysPrintSign || value < 0) minpos++; // absolute value to print, deal with sign later float v = value; if(v < 0) v = 0. - v; // floating point position will depend on the value uint8 precision = 0; if(v < 100) { v *= 10; precision++; } if(v < 100) // doing this twice { v *= 10; precision++; } uint32 iv = v + 0.5; // we will be operating with integers // Filling the buffer starting from the right pos = width; buf[pos] = ''; bool onceMore = true; // Print at least one zero before dot while((iv > 0 || onceMore) && (pos > minpos)) { pos--; onceMore = false; // Fill one digit buf[pos] = iv % 10 + '0'; iv /= 10; // Special case for printing point // Trick used: if precision is 0 here it will become 255 and dot will never be printed (assuming the buffer size is less than 255) if(--precision == 0) { buf[--pos] = '.'; onceMore = true; } } //Print sign if(value < 0) buf[--pos] = '-'; else if (alwaysPrintSign) buf[--pos] = '+'; } size_t FloatPrinter::printTo(Print& p) const { return p.print(buf+pos); }
- Пока я писал эту функцию нужно было ее как то проверять. Как-то так получилось, что у меня на домашнем компе не установлено никаких компиляторов или IDE кроме ардуино (и Atmel Studio). Поэтому код я писал в браузере на cpp.sh. Я написал небольшую обертку вокруг этого кода, которая запускает функцию с разными параметрами и проверяет результат. Получился такой себе вариант юнит теста, только без необходимости создавать проект, втягивать туда какой нибудь тестовый фреймворк и все такое.Конечно же есть и минусы. Код теста нужно синхронизировать с “продакшен” кодом методом копи-пасты. Благо это не нужно делать очень часто.типа юнит тест
#include #include typedef unsigned char uint8; typedef unsigned int uint32; // This is some kind of a unit test for float value print helper. Code under the test is injected into a test function below via simple copy/paste from FloatPrinter constructor. // This allows executing the code right at C++-in-browser service (such as http://cpp.sh) // I just did not want to set up a development toolchain, create a project file, deal with external libraries, do a dependency injection into tested class, etc :) void test(const char * expectedValue, float value, uint8 width, bool leadingZeros = false, bool alwaysPrintSign = false) { char buf[9]; uint8 pos; printf("Printing %f... ", value); //////////////////////////////////////////////////////// // Begin copy from FloatPrinter //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// // End copy from FloatPrinter //////////////////////////////////////////////////////// if(strcmp(expectedValue, buf+pos) == 0) { printf("%s - PASSEDn", buf+pos); } else { printf("%s - FAILEDn", expectedValue); printf("Got: %sn", buf+pos); printf("Buffer: "); for(int i=0; i<9; i++) printf("%2x ", buf[i]); printf("npos=%dnn", pos); } } int main() { test("0", 0., 4); test("0.10", 0.1, 4); test("0.23", 0.23, 4); test("4.00", 4., 4); test("5.60", 5.6, 4); test("7.89", 7.89, 4); test("1.23", 1.234, 4); test("56.8", 56.78, 4); test("56.8", 56.78, 5); test("123", 123.4, 4); test("568", 567.8, 5); test("12345", 12345., 6); test("-0.10", -0.1, 5); test("-0.23", -0.23, 5); test("-4.00", -4., 5); test("-5.60", -5.6, 5); test("-7.89", -7.89, 5); test("-1.23", -1.234, 5); test("-56.8", -56.78, 5); test("-56.8", -56.78, 6); test("-123", -123.4, 5); test("-568", -567.8, 6); test("-12345", -12345., 7); }
- Использовать Serial.print в конструкторах нельзя — МК уходит в циклический ребут. Скорее всего USB Serial инициализируется несколько позже конструкторов статически размещенных объектов. Из-за этого вызывается неинициализированный код.
- Стандартный синглтончик Майерса принес в проект такое огромное количество кода, что мама не горюй. Более 40к! Там были и эксепшены, и type info, какие то куски C++ ABI и много чего я и слыхом не слыхивал за десятилетия работы программистом.ага, вот эти ребята
GPSDataModel & GPSDataModel::instance() { static GPSDataModel inst; return inst; }
Так что плюсы плюсами, а объекты пришлось разместить в глобальных переменных, прописав где нужно ссылки через extern.
- Шрифты. В прошлой части я уже писал, что шрифты пришлось готовить самостоятельно. У меня есть скрипт, который из картинки генерит нужный код. Но в изначальном варианте данные лежали не в упакованном формате, а потому занимали больше места чем могли бы. Я доработал скрипт, и шрифты удалось упаковать, тем самым выиграв чуток флеша.Так, шрифт 8х12 похудел с 850 до 732 байта, а шрифт 16х22 (нарисовал самостоятельно используя Bodoni MT) уменьшился с 474 до 408. В этом шрифте только цифры, потому он так мало занимает.
- Изначально шрифты у меня располагались в хедере в виде константных массивов. Хедеры инклудятся в соответствующие cpp-шники где я рисую этими шрифтами. Так вот, сцуко, компилер для каждого цппшника дублирует фонты. Я переместил фонты в cpp файл и объем флеша сразу сократился на 9к. 9 килобайт за константу в хедере! 9 килобайт, Карл! Вот и верь после этого отлаженным компиляторам!
- С удивлением обнаружил, что у HardwareSerial нет метода attachInterrupt. У оригинального ардуино его, кстати, тоже нет. Можно использоваться NeoSWSerial, который рекомендуют в документации NeoGPS, но это как то странно использовать софтварный UART при наличии целой кучи хардварных.Я тут просто начитался документации по STM32 — DMA и все такое. Подумал, может и мне пригодится такой режим в целях экономии батареи. Ведь сейчас хоть и со sleep()’ами, но все таки идет постоянный опрос “а не пришло ли чего из GPS?”
- Пришлось озадачится вопросами UX дизайна. Хочется вывести на экран кучу разной информации, но пикселей не так много. Даже с использованием самого маленького шрифта влазит не больше 3 строк по 21 символу.Так, вся информация по одометру попросту не влазит на один экран. И на 2 тоже. Пришлось сделать один основной экран с большими и красивыми буквами. А если пользователю интересно, то в режиме подменю можно будет доступится до более детальной информации на нескольких экранчиках.
- GPS. Библиотека NeoGPS предоставляет некий статус соединения. В стиле “нет сигнала” -> “получили только время” -> “получили 2D Fix” -> “получили 3D Fix”. Возможно это работает с каким нибудь другим GPS модулем, но не с моим. У меня работают только первый и последний пункты.
- GPS. Хотел побырику раздобыть параметр точности координаты. А не тут-то было. В явном виде получить его не удалось, а HDOP/VDOP это только косвенные показатели точности. Буду очень благодарен за толковое разъяснение по этим метрикам.
- Высота может быть отрицательная. Очень удивился когда увидел высоту 65000м, оказалось ГПС после включения давал высоту -500м. Пришлось сделать специальный кейс у себя в коде для корректного отображения отрицательных высот.
- Скорость позиционирования оставляет желать лучшего. Реклама гласит Time To First Fix < 30 секунд, но это, по всей видимости, означает поимку первого спутника, а не первых координат. Время регистрируется почти сразу после включения. А вот координат приходится ждать пару минут. Даже GPS включался несколько минут назад.Возможно модуль не нужно выключать «из розетки» а переводить в какой нибудь глубокий сон. Батарейка на модуле намекает.
- Точность также под вопросом. Высота регистрируется неправильно: +-50м, и только потом медленно ползет куда надо. Да и потом заметно плавает
- При плохом приеме могут возникать большие скачки скорости до 150км/ч, а сам модуль может накрутить на до 7км за час. Нужно будет проверить где нибудь в поле.
Оптимизируем
Напоследок пару слов про оптимизацию потребления. Да, контроллер мощнее, но проблемы все те же. Нужно очень внимательно следить за потреблением памяти ибо одно неосторожное движение может добавить к прошивке пару кило.
Как и ожидалось, на STM32 вылезли все те же проблемы что и на AVR.
- константы, которым забыли написать слово const по прежнему размещаются в ОЗУ (там на полкило будет таких констант. В основном USB дескрипторы)
- 512 байт картинки adafruit, которая загружается в буфер дисплея и никогда не показывается.
- функции по работе с SPI, хотя ничего у меня по SPI не подключено — 512 байт
- всякая фигня из NeoGPS — вычисление високосного года и все такое прочее. Кем-то косвенно юзается — 300 байт
- класс TwoWire (ручная реализация I2C). Это точно не используется, но линкер ее все равно втюхивает — 650 байт
- код по работе с АЦП. Пока не используется, но будет когда-то для измерения параметров батареи. Пока не трогал.
Список далеко не полон. Такое впечатление, что если какой-то объект (тот же TwoWire) объявлен в хедере, то линкер его притягивает в проект независимо от того, используется он реально или нет. Возможно, это можно регулировать настройками линкера, но билд система ардуино не позволяет ничего настраивать. В конце концов я просто закомментировал класс TwoWire в библиотеке Wire и все скомпилилось без проблем.
С кодом SPI чуть сложнее. Дело в том, что создатели библиотеки Adafruit_SSD1306 ничего не знают про С++ интерфейсы написали код и для SPI и для I2C. Причем выбор нужного происходит в рантайме. Поэтому компилятору ничего не остается, кроме как влепить обе реализации в код. Решается чуть более интеллектуальным комментированием кода в библиотеке.
Все остальное по мелочи. Где смог — пропатчил библиотеки, расставил const где нужно. Но в основном оставил все как есть. На данный момент занято 55кб флеша, из них моего кода чуть меньше 7к — все остальное библиотеки. Вот чуть более детально, если кому интересно
Name | Size |
.text section (Code in ROM) | |
System stuff | 320 |
My code | 212 |
NeoGPS | 4056 |
Adafruit SSD1306 | 3108 |
FreeRTOS | 3452 |
Arduino: Wire Library (I2C) | 296 |
My Code | 6744 |
Board init / system stuff | 788 |
libmaple | 3778 |
Arduino (HardwareSerial, Print) | 1978 |
libmaple | 280 |
libmaple USB CDC | 2216 |
libmaple USB CoreLib | 2388 |
math | 12556 |
libc (malloc/free, memcpy, strcmp) | 3456 |
Total: | 45628 |
.data section (RAM) | |
libmaple constants & tables | 820 |
USB stuff & descriptors (after cleanup) | 84 |
Impure data (WTF? Used in FreeRTOS) | 1068 |
malloc stuff | 1044 |
Total: | 3016 |
.rodata section (constants in ROM) | |
NeoGPS constants | 140 |
Adafruit_SSD1306 constants | 76 |
default font | 1280 |
vtables | 120 |
Monospace8x12 font | 1512 |
vtables | 42 |
My classes data + vtables | 886 |
TimeFont | 528 |
My classes data + vtables | 168 |
Arduino + libmaple stuff | 792 |
USB descriptors | 260 |
Math constants | 552 |
Total: | 6356 |
.bss section (Zeroed variables in RAM) | |
stuff | 28 |
display buffer | 512 |
Heap | 8288 |
FreeRTOS | 192 |
My data | 868 |
libmaple + arduino | 168 |
usb | 548 |
malloc stuff | 56 |
usb | 60 |
Total: | 10720 |
name | Size |
CurrentPositionScreen::drawScreen() const::longtitudeString | 17 |
CurrentPositionScreen::drawScreen() const::latitudeString | 19 |
timeZoneScreen | 12 |
odometer1 | 52 |
odometer0 | 52 |
gpsDataModel | 192 |
odometer2 | 52 |
gpsParser | 292 |
lastPeriodIdleValue | 4 |
curIdleTicks | 4 |
lastCountedTick | 4 |
lastCountedPeriod | 4 |
debugScreen | 12 |
speedScreen | 12 |
positionScreen | 8 |
timeScreen | 12 |
screenStack | 20 |
rootSettingsScreen | 8 |
display | 40 |
satellitesScreen | 12 |
screenIdx | 4 |
odometerScreen | 24 |
altitudeScreen | 8 |
Стоит отметить, что сам сгенерированый код получается довольно компактным (хоть и более размашистым, чем на AVR). Я не знаю ассемблера ARM, но выглядит он таким. Оптимизатор, кстати, не так лихо перемешивает код как в случае AVR. Все функции сгруппированы по их изначальному месторасположению — это значительно облегчает чтение.
А вот библиотечные функции libc занимают неприлично много. Я уже писал про 12к на sprintf. Это еще не все. Функции типа strcmp или memset занимают по нескольку экранов ассемблерного кода. Хотел бы я посмотреть что они там делают. Я даже скачал исходники newlib, где эти функции реализованы. Но там на ассемблере и написаны. С минимумом комментариев. Так что понятнее не стало. Можно было бы переписать самостоятельно, но, по моему, переписывать такие штуки это кощунство.
Больше всего, конечно, занимают тригонометрические функции и математика с плавающей точкой. Но если учесть, что всевозможные расчеты это и есть суть прибора, то придется смириться.
Единственная крупная и непонятная для меня часть — malloc/free. В своем коде я ее явно не использую. У FreeRTOS есть своя реализация. Откуда оно лезет неясно. Вызовов я не нашел. Я пробовал откатиться на самый первый коммит когда я спортировал свой проект на STM32 — этот код уже был в прошивке. Скажу больше. Если в пустом проекте подключить Adafruit_GFX уже будет malloc. Вряд ли библиотека тут виновата — я подключал совсем невинный хедер с тайпдефами. Скорее всего это какие то косяки билдсистемы.
В остальном все выглядит довольно прилично.
Послесловие
Ставлю бутылку тому, кто дочитал до этого места (С) студенческая байка
Проект медленно, но уверенно движется к цели. В этой части я переезжал на более мощную платформу ARM/STM32 и, если честно, мне это чертовски понравилось. По прежнему есть много недопонимания как все работает, даташит прочитан процентов на 20. Но это совершенно не мешает двигаться дальше.
Еще один крупный шаг, который я сделал — переезд на FreeRTOS. Код стал существенно проще и более структурированным. А самое главное его легко расширять дальше.
Наконец я подключил GPS приемник. С помощью библиотеки NeoGPS я смог получить все нужные данные и отобразить их на соответствующих экранах. Пришлось, правда, повозится с изобретением внутренней модели данных.
Сейчас я уперся в проблемы с билд системой ардуино. Она хороша для мелких проектов, но мне она жмет буквально со всех сторон. Система практически не конфигурируется и многие вещи происходят без моего ведома. К тому же у меня очень много вопросов со стороны конфигурационного менеджмента: как версионировать изменения в библиотеках? как разложить свои исходники по директориям, чтобы это было удобно? Как залить это в репозиторий, чтобы соратникам можно было с этим работать? В общем это будет первый приоритет в дальнейшей работе.
Только чует моя чуйка, что изменение билд системы повлечет за собой и другие вещи. По всей видимости придется переехать с Atmel Studio на CooCox или другую IDE. Возможно поменяется компилятор. Возможно придется отказываться от фреймворка Arduino. Пока сложно сказать что оно за собой потянет.
Ну а потом будет подключение SD карты, управление питанием, USB Mass Storage Device и много всего интересного.
Если кому понравилось — приглашаю присоединится к проекту. Я также буду рад конструктивным комментариям — они мне очень помогают.
→ Страничка прокта на гитхабе