A dedicated Arduino-based device with power saving functionality that provides location awareness.
When hiking in remote areas such as mountains, deserts, or forests, reaching a destination may become a nightmare if we lose our guidance tools. Many of us have likely experienced a dead battery situation for our smartphone due to its continuous provision of GPS and compass directions for extended periods in a journey. On the other hand, cellular services based on GSM and CDMA may not provide the sufficient coverage for voice and Internet communication, thus hindering the mapped directions unless the routing applications are set for offline operation.
The broad overview
Being in a remote or rural area may require a direction aid to reach a destination. GPS and compass built in many smartphone brands and models may fail short for direction assistance due to battery running out quickly, thus losing any xG communication possibility as well. A dedicated device providing location awareness is proposed with power-saving features to optimise battery life.
In this article, I present a versatile and cheap solution to location awareness which provides GPS information on the user's current location and their preset destination. It tracks the progress of the distance and direction to the destination, and with the help of an embedded compass, the user can adjust his orientation during their journey.
How it works
The solution core is an MCU, the ATmega328, which is embedded in popular Arduino boards such as the UNO and the Nano with 32Kbytes of flash, 2Kbytes of SRAM and 1Kbytes of EEPROM.
A couple of sensors are employed: the GPS Neo-6M and the HMC5883L compass modules alongside a 0.91" OLED screen and a few push buttons for interacting with the MCU.
The GPS module uses serial interface while the compass and the OLED modules use I2C. The project is powered by a Lithium battery providing 3.7V and 3000mAh and a power booster module is integrated with it to provide a step-up to 5V for powering the UNO, the sensors, and the display modules (see Figures 1 and 2).
Despite the booster module supporting a wide range of input voltage (3V - 30V) and adjustable output voltage (4 - 35V), with a maximum current of 3A, it overloaded the battery and it heated up not as anyone would expect. I even experimented with a different module but I reached the same unfortunate result. Luckily though, all participating components in the project are 5V tolerant, so I attempted to power them directly from the battery, and amazingly all worked fine. Despite the misfortune, I decided to keep the circuit layout as is with the booster subject to further investigation.
The GPS Module Challenge
It is obvious that Neo-6M, like many GPS modules, interfaces to the MCU via serial communication.
Since the MCU is flashed via the only hardware serial interface TX and RX on D0 and D1 respectively, I had to experiment with the software serial alternative on other interface pins. However, due to code size limitation, which I shall elaborate on later, this alternative did not work and I had to stick to the hardware serial which went perfect except when loading the code up to the MCU. This is where I had to remove the GPS connection and release the serial pins for Arduino IDE Upload operation, and then reinstate the GPS connection afterwards.
The MCU Challenge
Using an Arduino board such as the UNO or the Nano posed great restrictions on the project code footprint. The ATmega328 core of both models has a limited SRAM of 2 Kbytes. The Adafruit_GFX graphics library I used for driving the I2C OLED required 1.1 Kbytes of that memory, which is 1127 bytes. So, my code had to fit in the margin of 920 bytes for its global and local variables.
Whenever possible, I refrained from passing functions return values to variables. Instead, I inserted the functions themselves into the conditional statements or as parameters to other functions. Here is an example:
Int get_interrupt = digitalRead( INTERRUPT_PIN);
If ( get_interrupt == 1 ) { do something}
Replaced by the following improved code:
If ( digitalRead(INTERRUPT_PIN) == 1 ) { do something}
The other serious consumer of SRAM was the constant texts I sent to the serial monitor of Arduino IDE using the Serial.print() and to the OLED display using the display.print() statements
Note: The argument applies to Println() as well.
The solution was to move such constant texts to the flash memory which had plenty of spare segments in it. The F() macro achieved this requirement as in this example statements:
Serial.print("Default destination longitude is kept in eeprom!"); // puts the text permanently in SRAM
display.print("Default destination longitude is kept in eeprom!"); // while running code
Serial.print(F("Default destination longitude is kept in eeprom!")); // puts the text in SRAM temporarily
display.print(F("Default destination longitude is kept in eeprom!")); // while executing the print function
Here is the compilation output of Arduino IDE showing 1225 bytes have been spared for the graphics library. That was a win by a neck situation.
Sketch uses 25078 bytes (77%) of program storage space. Maximum is 32256 bytes.
Global variables use 823 bytes (40%) of dynamic memory, leaving 1225 bytes for local variables. Maximum is 2048 bytes.
Calibrating the Magnetometer Sensor
There are three steps to do this job for the specific HMC5883L sensor I am using in the project. The first is a calibration program1 that needs to be uploaded to the MCU and asks for the traditional rotation of the magnetometer (the compass module) for 3 rounds then it gives us offset and gain values for the 3 axis to put into our application code (see below the calibration output from the Arduino IDE serial monitor).
13:55:15.976 -> Gain updated to = 1.22 mG/bit
13:55:16.008 -> Calibrating the Magnetometer ....... Gain
13:55:16.072 -> x_gain_offset = 0.99
13:55:16.072 -> y_gain_offset = 1.01
13:55:16.105 -> z_gain_offset = 0.97
13:55:16.138 -> Calibrating the Magnetometer ....... Offset
13:55:16.171 -> Please rotate the magnetometer 2 or 3 times in complete circles within one minute .............
13:55:46.261 -> Offset x = -276.04 mG
13:55:46.292 -> Offset y = 145.93 mG
13:55:46.324 -> Offset z = 321.81 mG
The second is the correction for magnetic declination2 specific to the user's area which can be obtained from several sources such as http://www.magnetic-declination.com. The correction value is used to preset the compass in the setup() section of the project's code sketch (see the code example here).
Compass.SetDeclination(4, 37, 'E');
// 4 degrees & 37 minutes East for Cairo/Egypt
I finally tried to find out the true North using the project assembly in order to determine the compensation needed for the sensor readings.
With the circuit powered off, and the sensor removed to avoid any interference, I used my iPhone's compass to set the orientation for the project as depicted in the picture. Then, I fixed the sensor with its X-axis pointing to North.
In the code, I set the sensor to do its measurements based on the X-axis with its Z-axis pointing downwards, meaning the X-Y plane is due to be horizontal. The net shift in the sensor readings was 12 degrees which I added to the reported readings.
Power Saving Issues
Running on battery requires the optimum power saving strategy. Luckily, the ATmega328 supports several sleep modes3 which can be of use in this project to extend the battery up time.
I envisaged the project to operate in three states:
- The first in which all components are fully powered so that the user can track their location continuously via GPS and move towards the destination using the compass. This state should not last for extended periods to avoid draining the battery.
- The second state comes into effect in which the user puts all components to sleep, except for the GPS to avoid long synching with satellites when the system is awakened. This state can become practical whenever the user is momentarily not paying attention to the system due to his activities, for example crossing obstacles.
- The final state is powering down all components as this can save the power during the user's rest durations.
Operating the Project Board
The first thing the user needs to do is to register the longitude and latitude coordinates of his waypoint (destination). One way to do this is to write them in the default settings within the code then compile and upload the code to the UNO.
Another way requires no coding experience but the physical presence at the destination location which presumably shall be the departure and the return points of the journey. In this case, the user may power on the board by pressing the Power toggle button and then put it to power saving mode by momentarily pressing the Sleep push button.
The GPS module will then attempt to catch up and sync with satellites. When the flashing LED of the module starts blinking, the user may then awaken the board by momentarily pressing the Wakeup push button, and keeps pressing the Destination push button till the display shows a successful registration message.
Internally, the MCU is coded to store the destination coordinates in its EEPROM that provides non-volatility of data. Whenever the MCU reboots, it will check if the EEPROM is empty (so it applies the preset defaults) or has data other than 0's (so it applies the stored coordinates).
Pressing the Reset push button, on the other hand, restores the preset defaults to the project board (that is a factory reset). The user can then release the buttons and the board shall run in normal direction tracking mode.
Powering off the board afterwards can save the battery till the commencement of the return journey.
Here is the serial monitor notification of factory reset case:
Destination coordinates set successfully to current location!
- current_lat: 30.114032 - current_lng: 31.613510
- Distance: 0.00 m 0.000 km
- Bearing: 0.00
- Course: N
- Speed: 0.07 km/h
- Heading: 325
During the return journey, the user may keep the board fully functional by just powering it up. Whenever the user does not require immediate tracking of their whereabouts, then they may put the board to sleep..
For extended inactivity periods of the journey, powering off the board is recommended, however, upon rebooting, the GPS module may take a few minutes to sync and direction tracking resumes as normal.
Below is a sample of the Arduino's serial monitor printout for normal tracking mode, and Figure 3 illustrates samples of displays on the OLED screen.
Starting ........!
I2C dispaly setup success!
- Default destination latitude: 21.422487
- Default destination longitude: 39.826206
Arduino - GPS module
No GPS data received: check wiring
- current_lat: 30.113920 - current_lng: 31.613407
- Distance: 1268405.62 m 1268.406 km
- Bearing: 137.73
- Course: SE
- Speed: 0.31 km/h
- Heading: 323
The Arduino Sketch
The traditional Arduino sketch structure starts with library header files and global variables and function declarations. As I mentioned earlier, the global variables have to be cut short to fit the SRAM of the MCU, so only a few can be found there.
Keeping best practices of software development in mind, I made the code as structured as possible so I dedicated a header file for each of the sensors and the OLED display each of which specifies its relevant libraries and functions with the global variables alongside defined constant values. Such separation of the code segments according to their role would help the reader follow up the code and reuse it.
On the other hand, the main ino file of the sketch has been minimized in size by a couple of hundred statements shorthanded by including them in functions gps_position(), gps_speed(), get_heading(), which were declared in other header files in the sketch.
Further minimisation could have been achieved for the main ino file if I extracted the OLED display statements into separate functions in the oled.h header file. I skipped that part in order to focus on the core activities of the project.
It is worthwhile to annotate on some code segments that greatly influence the operation of the project.
In the setup() section of the sketch, I declared the LED indicators and the buttons wired to the MCU digital pins. Note that all button pins are configured as PULLUP since they are either floating or grounded. So, wiring them to the internal MCU pull-up resistors was necessary otherwise the pins will always sense logic "1" despite button pressing.
pinMode(LED_PIN, OUTPUT);
pinMode(INTERRUPT_PIN, OUTPUT);
pinMode(SLEEP_PIN, INPUT_PULLUP);
pinMode(WAKE_PIN, INPUT_PULLUP);
pinMode(DESTINATION_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
Then, I configured the magneto sensor, starting with initialising the I2C interface of the MCU, then set the magnetic declination for my area and the sensitivity of the sensor, which can be reduced in steps if it fluctuates extensively. And finally, the axis of heading (X or Y). (Note: Possible values are: 088, 130 (default), 190, 250, 400, 470, 560, 810)
Initialising the OLED comes next, as it is wired to the I2C as well. This part is simple and straightforward. However, I made the activation of the OLED essential for the project to
function by using an endless while() loop, since without it the user cannot track his whereabouts.
Using the EEPROM of the MCU for handling the destination coordinates is initialised in the setup() section to make sure it contains either their default settings in (gps.h file), supposedly made by the factory or the committed coordinates by the user of the current destination location. The two defined constants DEST_LAT and DEST_LNG are the factory presets.
Finally, comes the configuration of the wakeup interrupt, which is LOW activated by pressing the Wakeup button, and the wakeUpNow() function declared in power.h file would service the interrupt and awakens the MCU.
attachInterrupt(0, wakeUpNow, LOW);
In the loop() section, the interpretations of the push buttons are emphasized.
Pressing the Sleep button invokes the sleepNow() declared in power.h file, within which the sleep_mode() function causes the MCU to go into POWER_DOWN sleep. On the other hand, pressing the Destination button causes the code segment starting with statement number 87 to execute, invoking the EEPROM.put() functions to write down the current location coordinates into the EEPROM for the alleged destination. If not pressed, code segment starting with statement number 67 comes into effect by which the project board will operate in the tracking mode. This code segment invokes the gps_position() function in gps.h file, which displays the destination coordinate, the user current coordinates, the remaining distance to the destination, the bearing (angle to the destination) and the course (N/S/E/W and derivatives). In this segment, the speed of the user is also measured by the gps_speed() function in gps.h as well.
Also in the tracking mode, the function get_heading() declared in compass.h is invoked to display the compass with the user orientation and course. The user can benefit from all displays combined to correct their movement orientation to home on the destination as quickly as possible.
Testing Battery Life
I examined the project board for continuous tracking operation using a Lithium Ion 3000mAh 3.7V battery that was fully charged at 4.2V. The battery drained gracefully after 16 hours, causing the board to cease functioning. I consider this result a promising prospect for the project. On the other hand, I came across batteries with capacity exceeding 5000mAh which I imagine would prolong the board uptime even more.
Project Constrains
I would imagine the final product of this position tracker to be weatherproof with adequate battery lifetime suitable for the planned journey.
Fitting the sensors into the product casing should account for the bulky GPS antenna which is loosely coupled to the GPS breakout board via cable fitted with a MCX connector.
The magneto sensor requires isolation from electrical and magnetic interference in order to function properly, so it should be carefully placed in the casing.
The buttons should be sturdy and easily accessible to the user and be properly separated to avoid unintended pushes.