NB-IoT GPS Tracker running on the Ghostyu NB-EK-L476 Developer Kit with STM32L476RCT6 microcontroller, Quectel L70-R GPS module and Quectel BC95 NB-IoT module. Photo taken during the field test at Berlayer Creek Boardwalk (and mangrove swamp) in Singapore.

Build an NB-IoT GPS Tracker on STM32 L476 with Apache Mynewt and Embedded Rust

Plus Quectel L70-R GPS, Quectel BC95 NB-IoT modules and thethings.io

The Completed NB-IoT GPS Tracker Application showing geolocation and sensor data (raw temperature)

Let’s build an NB-IoT GPS Tracker! A simple gadget that determines its current location based on received GPS signals… And transmits the location to a server via NB-IoT.

We shall take an existing Apache Mynewt Embedded OS + Embedded Rust project from the article Rust Rocks NB-IoT! STM32 Blue Pill with Quectel BC95-G on Apache Mynewt… And extend it with a GPS module: Quectel L70-R.

The project already includes the Quectel BC95 NB-IoT module for transmitting sensor data in CoAP format over NB-IoT.

STM32 Blue Pill F103 doesn’t support floating-point computations in hardware, so it will be inefficient to compute floating-point latitude/longitude coordinates for the GPS module.

We’ll upgrade the STM32 F103 microcontroller to an STM32 L476 because it supports floating-point computations in hardware.

Thankfully there’s already a developer kit that contains the STM32 L476 microcontroller, Quectel L70-R GPS module and Quectel BC95 NB-IoT module… Ghostyu NB-EK-L476 Developer Kit, as described in the article Quick Peek of Huawei LiteOS with NB-IoT on Ghostyu NB-EK-L476 Developer Kit (STM32L476RCT6)”. Perfect for our NB-IoT GPS Tracker!

Alternatively we may create your own kit based on the same STM32L476RCT6 microcontroller and Quectel modules.

Upgrading from STM32 Blue Pill F103 to L476

How hard is it to switch microcontrollers and migrate a Mynewt + Rust project from STM32 Blue Pill F103 to L476? As we shall find out… Not that hard!

Below is the overall map of changes made to the Mynewt + Rust project while building the NB-IoT GPS Tracker. We’ll study each block in the subsequent sections.

Even if you’re new to Mynewt or Rust, feel free to skim this article! While building the NB-IoT GPS Tracker we made changes to a number of components in Mynewt OS and the Rust application… You’ll soon understand how the various parts of Mynewt and Rust work together to create an IoT gadget.

The complete source code is available at the l476 branch of this repository…


Changes made for Mynewt Board Support Package

Mynewt Board Support Package

Every Mynewt project begins with a Board Support Package… Either take an existing one, or adapt from one. Check out the Board Support Packages

A Board Support Package contains information, scripts and drivers necessary to build Mynewt for our microcontroller and the associated peripherals on our microcontroller board: flash memory, LEDs, UART ports, …

For this project, we customised an existing Board Support Package nucleo-l476rg by copying into hw/bsp/stm32l4 and renaming it. (The previous project used the Board Support Package hw/bsp/bluepill-64kb)

Our board is similar to the Nucleo L476RG, except that the Nucleo has 1 MB of flash ROM while ours has only 256 KB. Here’s how we resized the flash ROM by editing hw/bsp/stm32l4/bsp.yml

Customising a Board Support Package for STM32 L476 on Mynewt

The Board Support Package for the Nucleo defines two UART ports…

  • The first is named UART_0 which points to port USART2 (connected to the Quectel BC95 NB-IoT module)
  • The second is named UART_1 which points to port USART1 (not used)

(Yep don’t confuse the Mynewt port name with the actual UART port)

For our board we need a third port…

  • UART_2 which points to port LPUART1 (connected to the Quectel L70-R GPS module)

Here’s how we add UART_2 by editing hw/bsp/stm32l4/syscfg.yml

Adding UART_2 to Board Support Package Configuration. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/hw/bsp/stm32l4/syscfg.yml

But wait… This says that UART_2 will point to port USART3, not LPUART1!

Mynewt doesn’t have the driver code to support LPUARTs (Low Power UART) on the STM32 platform, so we used a quick hack…

The STM32L476 Datasheet says that LPUART1 and USART3 share the same TX and RX pins, except that they are flipped. (How odd!) STM32 programs may configure the UART port such that the TX and RX pins are swapped… That’s how we disguised USART3 as LPUART1! More about swapping TX and RX in a while.

We complete the UART customisation by adding the above code to hw/bsp/stm32l4/src/hal_bsp.c. We are now ready to use our custom Board Support Package!


Changes made for Mynewt Hardware Abstraction Layer

Mynewt Hardware Abstraction Layer

Let’s talk about the Hardware Abstraction Layer (HAL) in Mynewt… Because there are two definitions of HAL used in Mynewt that may confuse us…

General HAL for STM32 in repos/apache-mynewt-core/hw/mcu/stm/stm32_common

1️⃣ The first type of HAL is the General HAL that abstracts all the hardware on our microcontroller. The General HAL enables us to write Mynewt applications that are portable across microcontrollers. Here’s an example of a General HAL that abstracts the GPIO interface: https://mynewt.apache.org/latest/os/modules/hal/hal_gpio/hal_gpio.html

Vendor-Specific HAL for STM32 in repos/apache-mynewt-core/hw/mcu/stm/stm32l4xx

2️⃣ The second type of HAL is the Vendor-Specific HAL provided by a microcontroller maker (like STMicroelectronics) that abstracts the differences between various models of microcontrollers, e.g. STM32 F103 vs L476. Here’s a Vendor-Specific HAL: https://github.com/apache/mynewt-core/tree/master/hw/mcu/stm/stm32l4xx/src/ext/Drivers/STM32L4xx_HAL_Driver/Src

From now on, when we mention “HAL” we refer to 1️⃣ General HAL.

Recall that we need to patch the UART HAL so that the TX and RX pins are swapped for port USART3 (so that it behaves like LPUART1). The STM32L4x6 Reference Manual says that we may set the SWAP bit of UART register CR2 to swap the TX/RX pins.

The UART HAL for STM32 is implemented in apache-mynewt-core/hw/mcu/stm/stm32_common/src/hal_uart.c (inside the repos folder of our project). Edit the file at line 468 to set the SWAP bit of CR2 like this…

You may refer to this patched version of hal_uart.c. This patch depends on two conditions…

1️⃣ UART_2_SWAP_TXRX must be set to 1 in the syscfg.yml configuration file. Remember that UART_2 is the Mynewt name for port USART3.

2️⃣ UART port must be USART3

Where is USART_CR2_SWAP defined? In the Vendor-Specific HAL.

In Visual Studio Code, the patch should look like this…

Patching hal_uart.c to swap TX/RX pins for USART3


Mynewt Targets

Changes made for Mynewt Targets

Mynewt Applications are designed to be portable across microcontrollers… The same application may be recompiled to run on STM32 Blue Pill F103, STM32 F476, or even BBC micro:bit (based on Nordic nRF51).

To compile a Mynewt Application for a specific microcontroller board, we run the newt target command like this…

This creates two targets stm32l4_boot (for the bootloader) and stm32l4_my_sensor (for the application)…

1️⃣ stm32l4_boot is based on the Stub Bootloader located at apps/boot_stub

2️⃣ stm32l4_my_sensor is based on our platform-indepedent Sensor Application located at apps/my_sensor_app (which will be merged with the Rust application during compilation)

Both targets stm32l4_boot and stm32l4_my_sensor point to the same Board Support Package for STM32 L476 (hw/bsp/stm32l4), so they will produce Bootloader and Application Firmware Images that will run on our STM32 L476 board.

(The previous project used targets bluepill_boot and bluepill_my_sensor)

Instead of running the newt target command to create the targets, we may copy the target folders ourselves to create the targets. Be sure to update target.yml to point to the correct Board Support Package (see above).

The package name pkg.name in pkg.yml should be updated. C Compiler Directives and Linker Directives should be specified in pkg.yml, as shown above.


Changes made for Mynewt Libraries

Mynewt Libraries

Let’s look at the changes made to the Mynewt libraries and drivers (located in libs) that are used by our NB-IoT GPS Tracker application. Recall that the previous project has already provided libraries for transmitting sensor data in CoAP format over NB-IoT, also for reading the STM32 internal temperature sensor.

1️⃣ gps_l70r: Driver for the Quectel L70-R GPS module. This is a new driver that connects to the UART port to receive geolocation information (latitude, longitude, altitude) from the GPS module. It calls tiny_gps_plus to parse the NMEA messages generated by the GPS module.

2️⃣ tiny_gps_plus: TinyGPS++ library that was ported from Arduino. It parses NMEA messages generated by the GPS module to obtain geolocation information.

3️⃣ custom_sensor: We added a custom Sensor Data Type for the GPS geolocation, which contains latitude, longitude and altitude. This enables the gps_l70r GPS driver to be polled like any other sensor (e.g. internal temperature sensor) via Mynewt’s Sensor Framework.

4️⃣ mynewt_rust: Helper functions were added to allow the Rust application to read the new Geolocation Sensor Data Type

5️⃣ buffered_serial: Library that buffers data from the UART port for easier parsing. Used by the GPS driver gps_l70r and the Quectel NB-IoT driver bc95g

6️⃣ adc_stm32l4: We added this driver to support the Analog-To-Digital Converter (ADC) found in the STM32 L476 microcontroller. The ADC driver is used by the internal temperature sensor temp_stm32

7️⃣ temp_stm32: This is the driver for the internal temperature sensor. The driver has been updated to use adc_stm32l4 to read the internal temperature sensor on the STM32 L476 microcontroller.

With these updates, our Rust application shall be able to poll the GPS sensor for the geolocation, attach the geolocation to the internal temperature sensor data, and transmit the geolocated sensor data to the CoAP Server at thethings.io over NB-IoT.


Application Configuration

Where do we configure the settings for the libraries? In the System Configuration File for our Target Application targets/stm32l4_my_sensor/syscfg.yml

(The System Configuration File for the previous project was at targets/bluepill_my_sensor/syscfg.yml)

Here we enable the drivers for the GPS and NB-IoT modules, by setting GPS_L70R and BC95G to 1. We specify the UART port for the GPS and NB-IoT modules like this…

GPS_L70R_UART: 0  #  Connect to Quectel L70R module on USART2
BC95G_UART: 2 # Connect to Quectel BC95-G module on LPUART1
# i.e. USART3 with TX/RX pins swapped

Why 0 and 2? Because the Mynewt UART ports are named like this…

UART_0 → USART2
UART_1 → USART1
UART_2 → USART3

Recall that we need to swap the TX/RX pins for port USART3 so that it behaves like LPUART1. We have applied a patch in hal_uart.c that swaps the pins only if UART_2_SWAP_TXRX is set to 1

# Swap TX/RX pins for USART3 so that USART3 behaves like LPUART1
UART_2_SWAP_TXRX: 1

On the Ghostyu developer kit, the GPS module is disabled by default. The GPS driver needs to set Pin PA1 to high to enable the GPS module. We specify the pin like this…

# GPIO Pin PA1 enables and disables the GPS module
GPS_L70R_ENABLE_PIN: MCU_GPIO_PORTA(1)

Rust Application

Changes made for Rust Application

In the previous project we have created an Embedded Rust application that polls the internal temperature sensor every 10 seconds and transmits the temperature to the CoAP server (at thethings.io). Now we’ll extend the Rust application to transmit the GPS Geolocation (latitude, longitude) together with the temperature.

We reused the Rust modules in rust/app/src from the previous application…

1️⃣ lib.rs: Main program that starts the sensors and networking

2️⃣ app_sensor.rs: Poll the internal temperature sensor temp_stm32 and call app_network.rs to transmit the data

3️⃣ app_network.rs: Compose a CoAP Message with a JSON Payload to transmit the temperature data to the CoAP server at thethings.io

We added a new module gps_sensor.rs that instructs Mynewt to poll our GPS sensor every 11 seconds. This is possible because our GPS driver gps_l70r has been integrated with Mynewt’s Sensor Framework so that it works like any other sensor (such as the internal temperature sensor).

Now we have two sources of sensor data in our Rust application…

1️⃣ Internal Temperature Sensor Data, generated by the temp_stm32 driver

2️⃣ GPS Geolocation (latitude, longitude, altitude), generated by the gps_l70r driver

To conserve NB-IoT packets, we’ll aggregate the two types of sensor data and transmit as a single CoAP message. That’s why the Sensor Listeners for both sensors have been updated to forward the polled sensor data to a new function aggregate_sensor_data() defined in app_network.rs

When aggregate_sensor_data() receives a GPS Geolocation, it doesn’t transmit the data to the server. Instead it stores the GPS Geolocation.

When aggregate_sensor_data() receives the temperature sensor data, it attaches the stored GPS Geolocation to the sensor data, and transmits the geolocated sensor data like this…

{ "values": [
{ "key" : "t",
"value": 960,
"geo" : { "lat": 1.2701, "long": 103.8078 }}
...
]}

This JSON Payload format is specified by thethings.io for transmitting sensor data (temperature t = 960) with attached geolocation (latitude = 1.2701, longitude = 103.8078).

The Rust code for creating the JSON Payload is exactly the same as before, based on the coap! macro…

Geolocated Sensor Data rendered in a dashboard at thethings.io

How is this possible? Because the coap! macro has been updated to emit the GPS Geolocation attached to the sensor data in val (if the GPS Geolocation exists).

The screen shows how the Geolocated Sensor Data transmitted by our device would be rendered in a dashboard at thethings.io.

That’s the power of creating a Domain-Specific Language (like coap!) in Rust… We simply focus on the intent of transmitting sensor data, and let the macro decide how to generate the right payload to match our intent.

One more enhancement implemented in the Mynewt Rust Wrapper: The polling of Mynewt sensors in the Rust application has been made simpler by introducing Rust Iterators


Mynewt Build and Flash Scripts

Changes made for Mynewt Build and Flash Scripts

Mynewt provides the command-line tool newt for building the firmware image and flashing it to the microcontroller. On Windows, newt requires the MSYS2 and MinGW toolchains to be installed.

I wanted to simplify the installation of the Mynewt build tools on Windows (and make them more robust), so I chose to write my own build and flash scripts based on pure Windows, without MSYS2 and MinGW. Earlier I have experimented with newt on Windows Subsystem for Linux but the performance was awful so I dropped it.

The custom build scripts for STM32 L476 are located in the scripts/stm32l4 folder (except for build-app, which is located in scripts). The *.cmd scripts are for Windows, *.sh scripts are for macOS and Linux (though not extensively tested for Linux). *.ocd are the OpenOCD scripts for flashing and debugging (they control the ST-Link interface).

1️⃣ build-boot.cmd, .sh: Build the bootloader by running newt build stm32l4_boot

2️⃣ build-app.cmd, .sh: Build the application by running newt build stm32l4_my_sensor. Also builds the Rust application and injects the compiled Rust code into the build.

Previously the Rust build for STM32 Blue Pill F103 was targeted for thumbv7m-none-eabi (Arm Cortex M3), now the Rust build for STM32 L476 is targeted for thumbv7em-none-eabihf (Arm Cortex M4 with Hardware Floating-Point)

3️⃣ image-app.cmd, .sh: Create the application firmware image: newt create-image -v stm32l4_my_sensor 1.0.0

4️⃣ flash-boot.cmd, .sh: Flash the bootloader with OpenOCD

5️⃣ flash-boot.ocd: OpenOCD script for flashing the bootloader

6️⃣ flash-app.cmd, .sh: Flash the application with OpenOCD

7️⃣ flash-app.ocd: OpenOCD script for flashing the application

8️⃣ flash-init.ocd: OpenOCD initialisation script called by flash-boot.ocd and flash-app.ocd

9️⃣ debug.ocd: OpenOCD script for debugging the application. Called by the Cortex-Debug Extension in .vscode/launch-stm32l4.json

The newt command in the Windows scripts refer to the Windows build of newt, which works without MSYS2 and MinGW.

Visual Studio Code Tasks in the Task Runner

The above scripts are linked into .vscode/tasks.json so that they will appear as Tasks in Visual Studio Code.

If we’re using the Task Runner Extension, the tasks will appear like this.

(The custom build and flash scripts from the previous project are located in the scripts folder)


Visual Studio Code Configuration

Changes made for Visual Studio Code Configuration

Cortex-Debug is the debugger we’re using in Visual Studio Code to debug our application. We added a new debugger configuration .vscode/launch-stm32l4.json for STM32 L476.

The debugger configuration specifies the OpenOCD scripts and GDB commands for debugging the application. Note that launch.json, the current debugger configuration, is overwritten by launch-stm32l4.json whenever the application is rebuilt. So we should never edit launch.json.

(The debugger configuration from the previous project is at .vscode/launch-bluepill.json)


Connect the Hardware

To build the NB-IoT GPS Tracker we’ll need the following hardware…

1️⃣ Ghostyu NB-EK-L476 Developer Kit, as described in the article Quick Peek of Huawei LiteOS with NB-IoT on Ghostyu NB-EK-L476 Developer Kit (STM32L476RCT6)

The kit includes the STM32L476RCT6 microcontroller, Quectel BC95 NB-IoT module (with antenna) and Quectel L70-R GPS module (with antenna).

Alternatively: You may assemble your own kit based on the same STM32 microcontroller and Quectel modules.

Remember to select a Quectel NB-IoT module that works with your local NB-IoT Frequency Band. You may consider this Quectel BC95-G Global NB-IoT Module (breakout board with antenna), which supports all global bands.

To connect the components in your custom kit, refer to the Ghostyu NB-EK-L476 schematics. Note that the Quectel NB-IoT module may require additional power, like tapping the 5V pin from ST-Link instead of sharing the ST-Link’s 3.3V pin with the microcontroller.

2️⃣ ST-Link V2 USB Adapter: Under $2, search AliExpress for st-link v2

3️⃣ NB-IoT SIM from your local NB-IoT network operator. Many thanks to StarHub for sponsoring the NB-IoT SIM that I used for this tutorial!

Pins connected from the SWD Debug Port to ST-Link: SWDIO, SWCLK, VCC, GND

SWD Debug Port on Ghostyu NB-EK-L476

Connect the following pins from the SWD Debug Port to ST-Link: SWDIO, SWCLK, VCC, GND.

Ghostyu STM32 L476 Development Kit powered by both ST-Link and micro-USB port. At top left is a USB hub.

For the Ghostyu NB-EK-L476, you need to connect the micro-USB port on the board to another USB port on your computer. This means that the board will have two power sources: 3.3V power from ST-Link, and 5V power from micro-USB port. This ensures that the NB-IoT module will have sufficient power.

NB-IoT Module Power Supply for Ghostyu NB-EK-L476. From https://drive.google.com/file/d/14KZqMO6cj20eKqM85iJNth8vf_WbI5VM/view?usp=sharing

SIM partially exposed to show the unusual orientation

Insert the NB-IoT SIM into the Quectel NB-IoT Breakout Board according to the orientation shown in the photo. (Yes the SIM notch faces outward, not inward)

Remember: Always connect the antenna before powering up the NB-IoT module!

If you’re using Windows: Make sure that the ST-Link Driver has been installed before connecting ST-Link to your computer


Install and Run the NB-IoT GPS Tracker

Follow the instructions in this article to install, build and run our application on the STM32 L476 development board (or equivalent).

While running the application, the Console Log should look like this…

Here you can see the program reading the internal temperature sensor.

The temperature sensor data is combined with the GPS geolocation into a JSON Payload.

The JSON Payload is then transmitted as a CoAP Message to the CoAP server at thethings.io.

In the Console Log, there is a URL displayed: http://blue-pill-geolocate.appspot.com/...

Clicking this URL opens a web page that displays the computed temperature (in degrees Celsius) and the GPS geolocation, based on the sensor data received by thethings.io from your device over NB-IoT. This URL is randomly-generated each time we restart our device.

We have installed a script at thethings.io that pushes the computed temperature and geolocation to blue-pill-geolocate.appspot.com. For more details, check the section “Advanced Topic: WiFi Geolocation with thethings.io and Google App Engine” in the article Connect STM32 Blue Pill to ESP8266 with Apache Mynewt.

Computed temperature and the GPS geolocation

Does the transmission of geolocation work when the device is on the move (say, when mounted on a vehicle?) If the device moves between NB-IoT cells, will the transmission still succeed?

Yes! I have tested the NB-IoT GPS Tracker while walking around… Geolocation updates are indeed transmitted correctly to thethings.io as I walk.

Field testing of the NB-IoT GPS Tracker

Here’s a video demo of the NB-IoT GPS Tracker…


thethings.io Configuration

If you’re happy to view the sensor data using my server at blue-pill-geolocate.appspot.com, you may skip this section. If you wish to configure your own account for thethings.io, read on…

Follow the steps in the three videos below to configure your thethings.io account and install the Cloud Code Scripts. Click CC to view the instructions.

Note: There is one additional Cloud Code Function to be installed: push_sensor_data. Copy the code from this link to create a new Cloud Code Function named push_sensor_data.

The Thing Token should be updated in the COAP_URI setting of the System Configuration File targets/stm32l4_my_sensor/syscfg.yml

thethings.io Configuration: Part 1 of 3. Click CC to view the instructions.

thethings.io Configuration: Part 2 of 3. Click CC to view the instructions.

thethings.io Configuration: Part 3 of 3. Click CC to view the instructions.

At the dashboard page, create a new Widget with the following settings…

The geolocated temperature will be shown in the Widget. Here’s how it looks…

thethings.io dashboard with geolocated temperature sensor data

Here’s a video demo of thethings.io dashboard with geolocated temperature sensor data…

Video demo of thethings.io dashboard with geolocated temperature sensor data


What’s Next?

We have created an NB-IoT GPS Tracker in only 84 KB of Flash ROM (with debugging symbols included). Mighty impressive considering that it includes Mynewt Embedded OS and the Embedded Rust Libraries!

STM32 L476 is definitely a good choice for building an NB-IoT GPS Tracker. Mynewt did a great job of shielding the microcontroller-specific details and making embedded applications truly portable. And the application was created with only a few lines of Rust application code… Rust is incredibly productive for creating embedded applications!

For my next article I shall be taking a break from STM32 (since we have proved that it’s so easy to port Mynewt applications across STM32 microcontrollers)… And explore Nordic nRF52832 with Mynewt and Rust!