
Hello! Some time ago I got the idea to upgrade my faithful and beloved GPS logger Holux M241. One could look for something interesting in 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 intricacies, thereby building the device of your dreams.
What exactly I build
I described in detail in the first part . At that stage, I was targeting technology - I felt the Arduino in the context of a relatively large project. It turned out there are a lot of nuances that are not particularly affected in ordinary tutorials. In the comments I received a lot of interesting information, for which I am very grateful to the readers. Hopefully 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 switch to more mature technologies - FreeRTOS and STM32 microcontroller. Well, as always, we will disassemble the firmware and see what is written there.
I ask under the cat.
Gps
By this time, I already had an application framework. Everything spun on the Arduino Nano on the ATMega328 controller. It's time to connect my
Beitan BN-880 GPS receiver.
Thoughts about UARTI have some bias towards UART as a low-speed protocol from the last century. Mind, of course, I understand - the interface is as simple as 3 kopecks, it works on everything that moves. What else is needed? I also have a biased attitude to text protocols - messages should also be parsed. Why not to drive the data in binary form? Yes, even the packages? Anyway, people do not read them. And binary packages could greatly simplify processing. Well then, I'm so buzzing.
Seeing the legs of SDA and SCK sticking out of the module, I wanted to cling to them. Hooked on and ... I realized that the data is not so easy to get. I don't even know how. If a UART is used, the GPS receiver simply spills messages, and the recipient needs it. I2C transmission is initiated only by the host. Those. you need to create some kind of request to get an answer. But which one?
Guglezh on the topic of BN-880 I2C for a couple of hours did not give anything useful. The people simply use the UART, and most of the links led to quadcopter forums and mostly quadroopter problems were discussed there.
It was not so easy to get to datasheets. Those. it was not at all clear what module to look for datasheet. By indirect evidence, I found out that the UBlox NEO-M8N module is responsible for the GPS. It turned out that this thing can do so many features that the mother does not worry (there is even a built-in odometer and logger there). But it was necessary to read as many as 350 pages.
Looking through the datasheet, I realized that with a hitch this module is not taken. I had to step on my throat and connect to the already proven UART. And then enter into another problem: on the UART Arduin, there is only one, and it sticks out in the direction of the computer (fill in the firmware). I had to look towards the SoftwareSerial library.
I wrote the simplest “rebirth” of messages from the GPS port to the UART.
TranslucerSoftwareSerial gpsSerial(10, 11);
Messages poured in, but I could not catch satellites. 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
The GPS lay for more than an hour at the window of the 21st floor before it gave sane coordinates. And most of my review is not covered by high-rise buildings. There is a suspicion that some spraying has been applied to the windows, which degrades the signal quality. In any case, near the open window, the satellites, as if, are caught faster.
Once there is a signal, then you can parse. On the open spaces of the Internet, the TinyGPSPlus library first came across. Not connected without hacks. At ArduinoIDE, everything worked, but at Atmel Studio did not want to. I had to manually register the path 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 a display and buttons, everything broke. The device noticeably stupid, clearly skipping the screen rendering. In the port monitor, I began to notice the crumpled messages from the GPS.
The first assumption was that the SoftwareSerial very seriously ate processor resources. So SoftwareSerial needs to be sent to a fire chamber since 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 is connected to the Arduin’s hardware UART, and the software series is used for debug (although it may not even be necessary to debug through UART if you have a screen). But with such a scheme to download the firmware via UART will not work. I had to get a USBAsp programmer.
But a little later, I realized that SoftwareSerial is a voracious thing, but in this case the problem is not in it at all, but in the drawing function. Drawing the current screen takes 50-75ms (plus a bit more overhead). SoftwareSerial is working on interrupt reception on the controller's foot and should not consume much. But his receiving buffer is only 64 bytes, which even at 9600 are filled for 60ms. It turns out, while the program is engaged in drawing the screen, part of the message from the GPS has time to pass by.
In the first half of the article I get a lot of text. I dilute them with pictures. This screen displays the current altitude and vertical speed.ARM
So. With the current approach, I rested on several limitations at once:
- Flash and RAM. Not so much was busy, but you had to constantly remember this
- Only one UART. Additional SoftwareSerial significantly consumes processor resources.
- In one thread, it is obviously impossible to do everything. We need to think about parallelizing tasks.
And it was also necessary to design with the expectation of the future - I still have a USB connection and an SD card.
After the release of the previous part, I received a lot of comments that Arduino sucks and the future of ARM and STM32 controllers. I didn’t really want to leave the Arduino platform. As I said, the framework is fairly simple and straightforward, and I also know ATMega controllers well.
At the same time, switching to STM32 would most likely mean a change of the platform as a whole, a microcontroller, a compiler, a framework, libraries, an IDE, and who knows what else. Those. almost all at once. For a project, this would mean stopping completely, studying documentation for a long time, studying various examples, and only then starting to rewrite everything from scratch.
I began to feel the ways out of the situation, listening to the commentators of the first part. I wanted to find a solution that solved the limitations, gave some groundwork for the future, but it did not require huge resources to move everything at once. Here are a few (in general, independent) things with which I poked around.
- Connect the Sparkfun Pro Micro clone to ATMega32u4 (3.3V, 8MHz). In it, I wanted to touch USB hardware. It took me quite a bit of time to get this thing at all. The staff bootloader didn’t really want to wind up like an Arduino, and the fuse bits were put up in some mysterious way. As a result, with the help of USBAsp, a bootloader from Arduino Leonardo was sewn and everything started up.
- The debug board came to ATMega64. It has 2 times more memory (both flash and RAM) and 2 uart. Basically removes restrictions. Unfortunately, the circuit is not attached to the circuit and what kind of quartz there is also not clear. Until postponed.
- I tried to feel the FreeRTOS port under the AVR. But then Kaku planted Atmel Studio. It turned out that she has 2 types of projects. In one studio works in the Arduino mode, but in this case practically nothing can be changed in the project settings. Those. trite you can not even put FreeRTOS in a subdirectory and include the include path. It can only add all the files in one pile, which personally would annoy me.
The second option is the Generic C ++ Executable project type. The implication is that you need to write on bare C ++. Here you can configure as you please. But first, you need to screw the Arduinov framework, and secondly, it is not clear how to screw the firmware float into the controller. Avrdude stubbornly did not want to overload the microcontroller into the bootloader (although I had a look at the command line from ArduinoIDE using ProcessMonitor). I have a USBasp, but if there is a USB port right on the board, it is not programmable through the programmer.
- Finally, I decoupled the comb on the board with the STM32F103C8T6 and installed the STM32duino according to the instructions . To my surprise, the LED blinker immediately started working. To even more surprise, it took less than 10 minutes to port my project to the new controller !!! Just a couple of inclusives change and pin numbers correct.
It was what you need. I received the power of STM32 (yes! The drawing function now took only 18ms!) And at the same time I could continue to use the arduino framework. This made it possible to continue working on the project, while necessarily smoothly plunging into a new platform, reading the datasheet on the microcontroller in the metro.
Flash gain is, in fact, a very ghostly improvement. The project both occupied a half flush on ATmega32, and takes up almost half on the new STM32 (ok, 26k out of 64k). So it was not worth relaxing. Especially (as they say on the Internet) the compiled code is a bit more sweeping and fills up the flash faster than on the AVR. So just in case I ordered a handkerchief with a 128k flash.
True, there was another surprise waiting for me. The people
on the Internet write that although the controller on the datasheet has 64k flash on board, in fact you can use 128k. Those. it seems the ST produces the same chip, only part of it is labeled as STM32F103C8T6, and the other as STM32F103CBT6 (the same controller, but with 128k flash).
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 the same way. Therefore, dancing with a tambourine and the declaration of constants using PROGMEM are no longer needed. Picked up for the sake of purity of the code. Tables of virtual functions, too, do not need to copy anywhere, because they are also in the same address space.
Another picture to dilute the text. From left to right: direction of motion (now we are not going anywhere), current speed, current altitude. The screen is honestly lapped with the same for Holux M241FreeRTOS
The STM32duino bundle also showed up a FreeRTOS port for my controller (and as many as two - 7.0.1 and 8.2.1). Examples with minimal edits also earned. So it was possible to switch to FreeRTOS without rewriting 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. Everything is like on large 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 - buttons. In a sense, FreeRTOS makes the code much easier - each thread can do some specific task, and, if necessary, notify other threads. So, the task of servicing the buttons fit perfectly into this ideology - listen to your buttons and do not get distracted by anything. And as something will click - notify.
Step asideAlthough the threads and message queues are cool, it seemed to me that the approach of polling the buttons in the loop was not entirely correct - there are interruptions to changing the value on the leg! Well, I also wanted to just try how it works on STM32 :)
static void selButtonPinHandler() { static uint32 lastInterruptTime = 0; if(digitalRead(SEL_BUTTON_PIN))
Instead of prints should have been sending messages about the pressed button.
But to be honest, it turned out cumbersome (this is processing only one button) and even terribly buggy. Some kind of false positives were observed, or vice versa, non-positives. It seems that the millis () function did not work as expected and could return the same values ​​for a fairly long period of time.
I will remind you. In the main program loop, I had a large state machine that controlled the display and listened to the buttons. Adding some sort of logic was accompanied by redrawing half the code, and only the guru of programming state machines was able to understand how it works by simply looking at the code. But since I have an RTOS, everything turned out much easier.
It turned out very compact and clear. The functions are all very linear. Just loop through the buttons and, based on the length of the press, send the corresponding message.
I decided that I would have 3 types of pressing durations:
- Short to select the corresponding menu item
- Long for a special action (for example, resetting the selected parameter)
- Very long press to turn the device on and off
Incidentally, I decided to connect the buttons not to a plus, but to a minus. Naturally pull-up resistors replaced by pull-down. I am not good at electronics and can be mistaken here, but in general I was guided by the following considerations:
- In the released position of the pin button is pressed to zero, which means the current does not flow (even if it is scanty)
- When reading a value from a pin, the value is obtained non-inverted: 1 if the button is pressed, 0 is released
ScreenManager is also greatly simplified. No more need for global display status. The rendering stream deals exclusively with rendering and is controlled by the messages from the buttons. He simply waited for messages in the queue and practiced the received commands. And the wait loop itself was also made through a queue using a timeout in the xQueueReceive function. Those. the function waits for the message, and if nothing happens for a long time, it simply draws the screen as it is
Display stream void vUserInteractionTask(void *pvParameters) { for (;;) {
It turned out, in my opinion, very elegant. Later I added turning off the screen here after a certain timeout (saving the battery), but the code was not significantly complicated.
Handling buttons is also trivial - just parse the message and call the necessary function.
Button handling 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();
The showMessageBox () function is also greatly simplified and is now completely linear.
message box void showMessageBox(const char * text) {
And finally. What kind of device is it if it does not have a blinking light bulb? Need to fix. No matter how ridiculous it was, but it is convenient to monitor whether the device is still working on the blinking diode, or it has hung long ago.
Hello FreeRTOS World! void vLEDFlashTask(void *pvParameters) { for (;;) { vTaskDelay(2000); digitalWrite(PC13, LOW); vTaskDelay(100); digitalWrite(PC13, HIGH); } }
Again gps
Finally, it is time to gnaw at the GPS. Now there is no problem at the same time listening to GPS and doing the rest. To begin, I again wrote a retrofit:
But there was a problem. The messages were formally parsed, only now there was not even time to bite out. Smoking the TinyGPS sources and receiver documentation showed a slight discrepancy between the messages from the GPS module and the fact that it can parse the library.
The UBlox module implements some kind of NMEA protocol extension. Each message begins with a five-letter message identifier.
$GNGGA,181220.00,,,,,0,00,99.99,,,,,,*70
The first 2 letters encode the subsystem that prepared the data: GP for GPS, GL for GLONASS, GA for GALILLEO. But if you use a combination of positioning systems, the messages will begin with GN.
The TinyGPS + library was not designed for this - it could only parse the GP messages. I had to tweak it a bit - I changed the corresponding line in the parser and the time on the screen ran. Only here it all smacked some sort of hack.
Fellow suggested an alternative - the
library NeoGPS . This is a much more feature-rich library. Besides the fact that she can parse messages with different prefixes, she also allows parsing information about satellites (I personally like these things in GPS receivers). It is also worth noting that the library is terribly configurable - you can turn on / off the parsing of individual messages and thereby adjust the memory consumption depending on the tasks.
It was not difficult to connect the library to stm32duino, although it was nevertheless necessary to file a bit. But as always in the examples everything is simple and clear, but in a real project it did not work right away. In particular, it was unclear at what point in time to read correctly from the GPS. Here, for example, an attempt to deduct data on satellites.
Retrieving satellite information 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); }
From time to time, the parser says that the profit data - take it. Coordinates always come normally, but with satellites trouble. I take, and there are zeros. Or not zeros. If we get lucky.
It turned out it was necessary to carefully read the documentation. It's all about the design of the library. In the name of saving memory, the data is expanded in the course of parsing. With that byte byte - came bytes, updated the variable. Data comes in batches of several messages. The NeoGPS library needs to know when a new package starts, in order to zero out internal variables. The configuration parameter LAST_SENTENCE_IN_INTERVAL is responsible for this.
So the RMC message from me comes the very first in the message pack. It turns out that my code could read partially parsed data (Perhaps it was the data from previous packages that had not yet been reset). Or read zeroes if read at the wrong time. It is treated quite simply: we indicate that in each package from the GPS module the last message is GLL.
There are many satellites, but there is no fixation and no. From top to bottom: the number of satellites (monitored vs untraceable - I don’t know what that means), HDOP / VDOP, GPS signal status (word / word)By the way, with the library in the bundle, quite convenient functions for working with date and time were found. For example, it was very easy to fasten the time zone. I only store the time offset in minutes, and the rest is easy to calculate along the way.
Correction of time according to the selected time zone void TimeZoneScreen::drawScreen() const {
The time zone selection screen is honestly lapped with HuluxModel-View
When writing code, we must not forget now that we are working in a multithreaded environment. So, I have a stream that serves GPS: it listens to the Serial port, parsit data from it byte-by-byte. Packages come once a second. The library knows when the next packet starts and resets internal variables before accepting. When a packet is fully accepted, the available flag is set. Data arrives for about half a second (there is 600 bytes at 9600).
We still have half a second to pick them up before the next packet starts.The second thread deals with the maintenance of the display. The cycle of drawing occurs every 100-120ms. At each iteration, the program takes the actual data from the GPS and renders what the user wants to see now - coordinates, speed, altitude, or something else. And here a contradiction arises: the flow of the display always wants to receive data, whereas in the library they are only available for half a second, and then they are overwritten.The solution is quite obvious: copy the data to itself in an intermediate buffer. Naturally, the data in this buffer must be protected by a mutex ( mutex), otherwise data may not be read correctly. But that's the problem. The data in the GPS stream appears, albeit rarely, but you can quickly subtract it (there are only one and a half hundred bytes after parsing); you do not need to block the mutex for a long time. But the drawing function can work for quite a long time (up to 20ms). Blocking a mutex for such a long time is generally not very good. Although not fatal, in this particular project.You can, of course, quickly block the mutex, take the data into a local variable and release the mutex. But it is fraught with memory overruns. Another fifteen hundred bytes with 20 kilobytes is garbage, but personally the very fact of triple buffering strains me.The buffer, by the way, had to be declared a global variable because it is very large and causes a stack overflow if you declare it as a function. Just in case the drawing thread wrote out a bigger stack.Proof of satellite data in the drawing function 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(....) ... }
With instantaneous values ​​that can be obtained directly from the NMEA stream, everything is simple - the NeoGPS library reads them out and decomposes them into variables. Each screen can simply read the corresponding variable (not forgetting the synchronization, of course) and display it on the screen. But with the variables that need to be calculated so simply did not work.After a long thought, I came to the classic model-view scheme.Objects that inherit screen are views - they display various data from the model, but the data itself does not produce. All logic lies in the GPSDataModel class. He is responsible for storing instant GPS data (until new data arrives from NeoGPS). He is also responsible for calculating new data, such as odometers or vertical speed. And last but not least, this class itself deals with all the synchronization for its data.Model class const uint8 ODOMERTERS_COUNT = 3; 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;
Since
the class of the model is responsible for synchronizing data between the streams, then the mutex lives in it, which governs access to the internal fields of the class. I was terribly uncomfortable (and ugly) to use bare xSemaphoreTake () / xSemaphoreGive (), so I drew a classic auto grabber (more precisely, even an autosolder).Mutex locker class MutexLocker { public: MutexLocker(SemaphoreHandle_t mtx) { mutex = mtx; xSemaphoreTake(mutex, portMAX_DELAY); } ~MutexLocker() { xSemaphoreGive(mutex); } private: SemaphoreHandle_t mutex; };
Pick up the current value is very simple. You just need to call the getGPSFix () function, which simply returns a copy of the data.Data retriever gps_fix GPSDataModel::getGPSFix() const { MutexLocker lock(xGPSDataMutex); return cur_fix; }
The client does not need to be soared about locks and all that. Just take the data and draw as needed.Client code void SpeedScreen::drawScreen() const {
The model class stores not only the most recent data (cur_fix), but also the previous value (prev_fix). So, calculating the vertical velocity becomes a trivial task.Vertical Speed ​​Calculator float GPSDataModel::getVerticalSpeed() const { MutexLocker lock(xGPSDataMutex);
With the data about the satellites turned out very interesting. Data about satellites live in an array of NMEAGPS :: satellite_view_t structures. The array weighs 150 bytes and, as I already wrote, it needs to be copied several times. Not so critical in the presence of 20kb operatives, but still it is three times 150 bytes.In the end, I realized that I didn’t need all the data, it’s enough to copy for myself only what is actually used. As a result, such a class was born.Satellite data storage class GPSSatellitesData {
This class is no longer so offensive to copy once again - it takes only 40 bytes.The most difficult part of the scheme is the GPSOdometer class. As the name implies, he is responsible for all calculations related to the functionality of the odometer.Classes of odometer and its data The difficulty is this. The gps_fix object, which comes to us from GPS, may contain some data, and some may not. For example, the coordinate will arrive, but the height is not. And in the next fix may be the opposite. So just save gps_fix will not work. It is necessary to watch every time what is available in the new fix and what is not. Therefore, it was necessary to fence a very complex algorithm, to memorize the coordinates, heights and time stamps separately.Every second odometer data update 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");
In this place my flash size has increased dramatically - by almost 10kb. A lot of mathematical code crawled into the project - sines, cosines, tangents, square roots and all that jazz. It turned out that the legs grow from the NeoGPS :: Location_t :: DistanceKm () function - all this is used in distance calculation based on coordinates. Gritting his teeth had to agree, but thought about the controller on the Cortex M4 - there it must be calculated hardwired.Odometer control void GPSOdometer::startOdometer() { data.active = true;
Note that there is no synchronization in the odometer class. This is because all synchronization takes place in the GPSDataModel class. I just did not want to make a mutex in every object. But because of this, I had to complicate the odometer class itself and divide it into 2 classes: an object with data (GPSOdometerData) can be copied at the request of customers, whereas a control object (GPSOdometer) is created once per odometer. Because of this, I also had to make one class a friend to another. Maybe I will revise this design in the future.
This is the main odometer screen. The character point in the font has not yet added - should show 0.42 km. It also displays the elevation difference - lying on the spot on the window sill, you can easily drop 18 or more meters.
Other useful options that can be displayed by odometer. On one screen, everything did not even fit - I will do 2 or even 3 screens.GPSDataModel can manage all odometers at once. This feature was proposed in the comments and should be convenient - went to the cafe, turned off all odometers at once. Came out - turned them on again.Odometer control all at once 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; }
Again FreeRTOS'im
In order to study the possibilities of FreeRTOS, I tried to see how much time the processor actually spends in calculations. You can use ApplicationIdleHook for evaluation .Any RTOS has a so-called idle stream. If the processor has nothing to occupy itself - a certain infinite loop turns in a separate task with the lowest priority. FreeRTOS allows you to add some usefulness to this infinite loop and run this hook. The idea of ​​measuring the CPU load is that the more the processor spends time in the idle stream, the less it is loaded with other (useful) work.On the Internet, I found several approaches on how to measure CPU usage.Some guys offered to twist a certain counter in the Idle Hook function and measure the speed with which it “pulls”. To translate this into percentages, you need to divide the resulting speed into some reference value.But where to get this reference speed? To do this, you need to extinguish all other flows and measure only the speed of the counter in the unloaded system. You can, for example, at the start, make a delay of 1-2 seconds for measurements, but personally it terribly enrages me when simple devices are “loaded” for 5-10 seconds (for example, cameras, soap boxes, grrrr).In another variant, it was proposed to start a separate timer from the Internet and reload the entry and exit macros into the flow context. The idea is to measure the difference in the values ​​of the timer at the input and at the output in each stream and from this draw a conclusion about the processor load.Yes, I heard about Run Time Stats on FreeRTOS. But, as stated in the instructions, it is intended for another. This function allows you to get a download for each individual stream and for the entire period of the application operation. I wanted to measure instant processor load.I decided to try the next one. I do not know how correct it is and whether it will work at all when I screw up sleep mode. But at this stage it works well.load measurement static const uint8 periodLen = 9;
A function can be called very often, many times in one system tick (system tick is 1ms). Therefore, the first block is responsible for counting ticks (and not calls) in which the hook was called. The second block stores a counter every 512 system ticks.CPU utilization is the ratio of the number of non-idle ticks to the total number of ticks in the measured interval.Calculation of values float getCPULoad() { return 100. - 100. * lastPeriodIdleValue / (1 << periodLen); } float getMaxCPULoad() { return 100. - 100. * minIdleValue / (1 << periodLen); }
Yes, it may not be entirely accurate. But in general, to get a certain rough estimate of the system load, it will completely roll. I'm going to use these indicators to lower the frequency of the controller to reduce consumption.By the way, in normal mode, the load was about 12.5% ​​and jumps up to 15.5% when the data comes from the GPS and they need to be parsed. When the display is off (although the GPS continues to be parsed), the download drops to 0. This is strange. Apparently GPS parsing actually takes less than a tick, so every tick after that falls into the idle task. The 3% load spike is probably not due to the data parsing itself, but by sending it to another stream.Although maybe I'm somewhere here just nakosyachil.
Indications of current and maximum CPU usage. The screen itself will be hidden somewhere in the depths of the settings menu.Different
In this section, I have collected individual problems that I solved at different stages of the project. Without any particular sequence.- The library implementation of sprintf takes as much as 13k. I had to write my own implementation. I wrote a little classic that implements the Printable interface. So you can “print” numbers with the necessary formatting on the screen and even in Serial. It turned out very nice and just a couple of screens of code.
- . - , IDE ( Atmel Studio). cpp.sh . , . , , .
. “” -. .
#include <stdio.h> #include <string.h> 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 //////////////////////////////////////////////////////// <Place Function Body Here> //////////////////////////////////////////////////////// // End copy from FloatPrinter //////////////////////////////////////////////////////// if(strcmp(expectedValue, buf+pos) == 0) { printf("%s - PASSED\n", buf+pos); } else { printf("%s - FAILED\n", expectedValue); printf("Got: %s\n", buf+pos); printf("Buffer: "); for(int i=0; i<9; i++) printf("%2x ", buf[i]); printf("\npos=%d\n\n", 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.
- . , . , . , . , , .
, 812 850 732 , 1622 ( 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 . .
Finally a few words about the optimization of consumption. Yes, the controller is more powerful, but the problems are the same. You need to carefully monitor the memory consumption for one careless movement can add a couple of kilos to the firmware.As expected, the same problems as the AVR got out on the STM32.- Constants that have forgotten to write the word const are still placed in RAM (there will be such constants per half a kilo. Mostly USB descriptors)
- 512 bytes adafruit pictures, which is loaded into the display buffer and never shown.
- functions for working with SPI, although nothing on SPI is connected to me - 512 bytes
- Any NeoGPS stuff is a leap year calculation and so on. Someone indirectly uses - 300 bytes
- TwoWire class (I2C manual implementation). This is definitely not used, but the linker still sniffs it — 650 bytes.
- . , - . Did not touch yet.
The list is far from complete.
It seems that if some object (the same TwoWire) is declared in the header, then the linker attracts it to the project, regardless of whether it is actually used or not. Perhaps this can be adjusted by the settings of the linker, but the Arduino build system does not allow to configure anything. In the end, I just commented out the TwoWire class in the Wire library and everything compiled without problems.The SPI code is a bit more complicated. The fact is that the creators of the Adafruit_SSD1306 library do not know anything about C ++ interfaces wrote code for both SPI and I2C. And the choice of the necessary happens in. Therefore, the compiler has nothing left but to stick both implementations into the code. Solved a little more intelligent commenting code in the library.Everything else on the little things. Where I could - patch the library, put const where needed. But mostly left everything as is. At the moment, 55kb flash is occupied, of which my code is slightly less than 7k - the rest of the library. Here's a little more detail, if anyone is interestedMemory consumption by sectionName | 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 |
Consumption of RAM by my classes and variablesname | Size |
CurrentPositionScreen::drawScreen() const::longtitudeString | 17 |
CurrentPositionScreen::drawScreen() const::latitudeString | nineteen |
timeZoneScreen | 12 |
odometer1 | 52 |
odometer0 | 52 |
gpsDataModel | 192 |
odometer2 | 52 |
gpsParser | 292 |
lastPeriodIdleValue | four |
curIdleTicks | four |
lastCountedTick | four |
lastCountedPeriod | four |
debugScreen | 12 |
speedScreen | 12 |
positionScreen | eight |
timeScreen | 12 |
screenStack | 20 |
rootSettingsScreen | eight |
display | 40 |
satellitesScreen | 12 |
screenIdx | four |
odometerScreen | 24 |
altitudeScreen | eight |
It should be noted that the generated code itself is quite compact (albeit more sweeping than on the AVR). I do not know the ARM assembler, but it looks like this. The optimizer, by the way, is not as famously mixing the code as in the case of AVR. All functions are grouped by their original location - this makes reading much easier.But libc library functions occupy indecently a lot. I already wrote about 12k on sprintf. That's not all. Functions such as strcmp or memset occupy several screens of assembler code. I would like to see what they are doing there. I even downloaded the source code of newlib, where these functions are implemented. But there in the assembler and written. With a minimum of comments. So it did not become clearer. It would be possible to rewrite independently, but, in my opinion, to rewrite such pieces is sacrilege.Most of all, of course, are trigonometric functions and floating-point math. But if you consider that all sorts of calculations this is the essence of the device, you will have to accept.The only major and incomprehensible part for me is malloc / free. I obviously don't use it in my code. FreeRTOS has its own implementation. Where it climbs is unclear. I did not find calls. I tried to roll back to the very first commit when I sped up my project on STM32 - this code was already in the firmware. I will say more.
If in the empty project to connect Adafruit_GFX there will already be malloc. The library is hardly to blame here - I connected a completely innocent heading with taypdefami. Most likely these are some kind of building system jambs.Otherwise, everything looks pretty decent.Afterword
I put a bottle to someone who has read the student bike up to this point.The project is slowly but surely moving towards the goal. In this part, I moved to a more powerful ARM / STM32 platform and, honestly, I liked it damn well. There are still a lot of misunderstandings about how everything works, the datasheet has been read by 20 percent. But this does not prevent us from moving further.Another major step I took was moving to FreeRTOS. The code has become much simpler and more structured. And most importantly it is easy to expand further.Finally, I connected the GPS receiver. With the help of the NeoGPS library, I was able to get all the necessary data and display it on the appropriate screens. It was necessary, however, to tinker with the invention of the internal data model.Now I have rested in problems with the build system arduino. She is good for small projects, but she presses me literally from all sides. The system is practically not configured and many things happen without my knowledge. In addition, I have a lot of questions from the configuration management: how to version the changes in the libraries? How to decompose your source in directories, so that it is convenient? How to upload it to the repository so that allies can work with it? In general, this will be the first priority in further work.It just feels like my chuik, that changing the build of the system will entail other things. Most likely, you will have to move from Atmel Studio to CooCox or another IDE. Perhaps the compiler will change. You may have to give up the Arduino framework. While it is difficult to say that it will pull.Well, and then there will be an SD card connection, power management, USB Mass Storage Device and many other interesting things.If someone liked - invite to join the project. I will also be happy for constructive comments - they really help me.→ Page on gitkhab