Location is a basic part of the most interesting IoT systems. It’s not just about loss prevention; geolocation opens the door to several applications and enhances the management of devices. In many cases, this involves Global Navigation Satellite System (GNSS).
Position is so important in IoT that the latest Zephyr release (v3.6.0) has added a new API for GNSS based on the NMEA0183 standard. Today I’m going to explain the basics of GNSS, what NMEA means, and dive into this awesome new Zephyr feature.
The basics of the NMEA standard
NMEA is the acronym for the National Marine Electronics Association, an organization created before GPS was invented. Its aim is creating better communications with manufacturers. I can assure you that they achieved their goal with GNSS.
Their most extended and well-known norm is the NMEA0183 protocol, which has become the GNSS standard for almost all manufacturers. It facilitates module integration and new applications development.
NMEA data is transmitted from a source such as a GPS module (known as a “Talker”) to equipment, such as our running Zephyr device (known as a “Listener”). One important aspect is that a single talker can communicate to many listeners.
From the electronic perspective, NMEA0183 originally used RS-422, but now uses several interfaces such as UART, USB, RS-232, WIFI, Bluetooth, and more. We can consider NMEA protocol as a common message structure standard. NMEA0183 protocol makes software developer life easier due to the standardization between GNSS devices.
NMEA message structure
Each NMEA sentence contains only ASCII characters, starting with the dollar symbol “$” and ending with the <CR>
(Carriage return) and <LF>
(Line feed) characters. The content of the message is a tuple, separated by commas. This starts with an NMEA identifier, followed by data, and ending by a checksum preceded by an asterisk “*
”.
For a better understanding, let’s examine a popular NMEA sentence from a Quectel module. “GGA” sentence contains the Global Positioning System fix data, time, position, and fix related data for a GNSS receiver.
$GPGGA,102744.00,6155.393269,N,00848.433734,E,1,03,1.6,821.5,M,52.0,M,,*70
- $GPGGA is the sentence identifier which can be split into “GP” which means the GNNS type, in this case GPS, and “GGA” which is the sentence identifier.
- 102744.00 is the time of fix (hhmmss).
- 6155.393269,N is the latitude (ddmm.mmm format, N for North).
- 00848.433734,E is the longitude (dddmm.mmm format, E for East).
- 1 is the fix quality indicator.
- 03 is the number of satellites being tracked.
- 1.6 is the horizontal dilution of precision.
- 821.5,M is the altitude in meters above mean sea level.
- 52.0,M is the height in meters of geoid separation.
- *70 is the checksum (Note: this is not the correct checksum for this payload, we moved the location manually)
All talker devices don’t rely on GPS or on different constellations simultaneously. The most common constellations are GLONASS (GL), BEIDOU(DB or GB) and GALILEO (GA).
Besides GGA, other popular NMEA sentences are:
- RMC: Recommended Minimum Navigation Information
- This contains information similar to GGA such as the latitude and longitude and also the speed over ground (in knots), the date and the magnetic variation (in degrees).
- GSV: Satellites in View
- This NMEA message prints information such as the total number of satellites in view for each constellation and for each satellite its number (PRN), elevation in degrees, azimuth in degrees and Signal to Noise Ratio (SNR) in dB. Each GSV sentence contains the information of more than one satellite.
The previous NMEA sentences are named as “talker sentences”. In addition to that, there are “Proprietary sentences” and “query sentences”. On the one hand, the proprietary sentences start with “$P” and allow manufacturers to define custom NMEA sentences format for custom functions such as power management. On the other hand, the query sentences are the means for listener to request a particular sentence from a talker
Zephyr GNSS API
If you’ve reached this point, it means that you already know and like Zephyr RTOS. If you’re not familiar with Zephyr, I encourage you to familiarize yourself with it by using this getting started guide.
Zephyr RTOS is not just a simple scheduler for simple applications. Zephyr is a whole operating system with drivers, services and common APIs which facilitates the new developments and sensors and boards exchange. I would go beyond and say that Zephyr is even a complete ecosystem with integration in awesome clouds such as Golioth.
A great example of this affirmation is the new Zephyr GNSS support in version v3.6.0. The GNSS API is built upon the modem subsystem, which provides the necessary modules to communicate with modems. The GNSS subsystem covers everything from sending and receiving commands to and from the modem, to parsing, creating and processing NMEA0183 messages.
The source code can be found under the path zephyr/drivers/gnss
, which is divided into specific GNSS module drivers with custom features such as power management, some files with utils and a generic NMEA0183 driver. Covering in detail each of those utils would require a whole new post. However, I will provide a brief overview of each of them:
gnss_dump
- A set of utilities to get, convert and print GNSS information into readable and useful data.
gnss_nmea0183
andgnss_parse
- NMEA0183 utilities such as checksum calculation, parsing a ddmm.mmmm formatted coordinates to nano degrees or parsing the content of GGA, RMC and GSV NMEA messages.
gnss_nmea0183_match
- This code is based on “modem_chat” match handlers, a Zephyr utility to process messages from modems. The callbacks to parse GGA, RMC and GSV messages are defined here.
Zephyr has created a generic NMEA driver which can be used for any NMEA talker. As any other driver, it is instantiated by the DEVICE_DT_INST_DEFINE macro. It includes the following code to call the callbacks defined in the gnss_nmea0183_match.c for corresponding NMEA identifier:
MODEM_CHAT_MATCHES_DEFINE(unsol_matches, MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback), MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback), #if CONFIG_GNSS_SATELLITES MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback), #endif );
GNSS example in Zephyr
Now you know about NMEA0183 and the Zephyr GNSS API, so get your hands dirty and write some code! The full code can be found in my github repository:
https://github.com/jeronimoagullo/Zephyr-GNSS-Sample
KConfig Changes
In this example, I will show you how to configure a microcontroller in Zephyr to use a GNSS module which supports NMEA0183. For this aim, I will use an ESP32S3-Mini-1 development board along with an Air530z GNSS module. We’re going to modify our prj.conf file
- Add the CONFIG_GNSS=y kconfig variable to add the GNSS support.
- Add the variables CONFIG_GNSS_SATELLITES=y to display satellites’ information
- Add CONFIG_GNSS_DUMP_TO_LOG=y to print GNSS information like the fixation status, coordinates and time.
Devicetree Changes
Next, we’ll configure the device tree correctly for our GNSS module. In this case, the Air530z GNSS module uses an UART interface. By default, ESP32S3 has the UART0 configured in pins 43 and 44. However, this UART is configured at 115200 bps and used for both programming the board and by Zephyr to print messages into the serial terminal. This is not a good idea.
What is the solution? The use of another UART, for example, the UART1. However, it is not defined in esp32s3_devkitm.dts. So let ‘s do it!
According to GPIO, the UART1 uses the pins 17 and 18 (you can use others too). I have chosen these default pins and defined them in the device tree overlay using the pin control subsystem. The macro of the pins can be found in the file zephyr/include/zephyr/dt-bidings/pinctrl/esp32s3-pinctrl.h you can find your board pins in the corresponding header file of your board.
&pinctrl { uart1_pins: uart1_pins { group1 { pinmux = <UART1_TX_GPIO17>; output-high; }; group2 { pinmux = <UART1_RX_GPIO18>; bias-pull-up; }; }; };
Then, in the UART node, I have added our above pin definition and a “gnss-nmea-generic” compatible device. I have created an alias to this GNSS device to facilitate the access from the main code.
&uart1 { status = "okay"; current-speed = <9600>; pinctrl-0 = <&uart1_pins>; pinctrl-names = "default"; gnssdev: gnss-nmea-generic { compatible = "gnss-nmea-generic"; }; };
Finally, the main code uses the GNSS_DATA_CALLBACK_DEFINE macro to define a callback for getting the information about satellites in view and another for GNSS information such as time, coordinates and tracking satellites. The following screenshot depicts the application output:
We managed to get the GNSS data!
Final thoughts
We have covered in this post what NMEA means and its importance in GNSS as well as the main NMEA messages, how the new Zephyr GNSS API relies on NMEA, and a sample of how to localize our device using Zephyr. Now, it is your turn to send the GNSS data to Golioth cloud!