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.

STM32 Blue Pill F103 STM32 L476
Microcontroller STM32F103C8T6 STM32L476RCT6
CPU Arm Cortex M3 Arm Cortex M4
RAM 20 KB 96 KB + 32 KB
ROM 64 KB 256 KB
Floating-Point Software Hardware
view raw f103-to-f476.md hosted with ❤ by GitHub

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

# For USART3. To swap USART3 TX/RX so that it behaves like LPUART1, insert "if (cfg->suc_uart == USART3) { cr2 |= USART_CR2_SWAP; }" at repos/apache-mynewt-core/hw/mcu/stm/stm32_common/src/hal_uart.c, line 470
UART_2:
description: 'Whether to enable USART3'
value: 0
UART_2_PIN_TX:
description: 'TX pin for USART3, PB10'
value: MCU_GPIO_PORTB(10)
UART_2_PIN_RX:
description: 'RX pin for USART3, PB11'
value: MCU_GPIO_PORTB(11)
UART_2_PIN_CTS:
description: 'CTS pin for USART3, PB13'
value: -1
UART_2_PIN_RTS:
description: 'RTS pin for USART3, PB14'
value: -1
UART_2_SWAP_TXRX:
description: 'Swap TX/RX pins for USART3 if set to 1'
value: 0
view raw bsp-syscfg.yml hosted with ❤ by GitHub

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.

#if MYNEWT_VAL(UART_2)
static struct uart_dev hal_uart2;
static const struct stm32_uart_cfg uart2_cfg = {
.suc_uart = USART3,
.suc_rcc_reg = &RCC->APB1ENR1,
.suc_rcc_dev = RCC_APB1ENR1_USART3EN,
.suc_pin_tx = MYNEWT_VAL(UART_2_PIN_TX),
.suc_pin_rx = MYNEWT_VAL(UART_2_PIN_RX),
.suc_pin_rts = MYNEWT_VAL(UART_2_PIN_RTS),
.suc_pin_cts = MYNEWT_VAL(UART_2_PIN_CTS),
.suc_pin_af = GPIO_AF7_USART3,
.suc_irqn = USART3_IRQn
};
#endif
...
void hal_bsp_init(void) {
...
#if MYNEWT_VAL(UART_2)
rc = os_dev_create((struct os_dev *) &hal_uart2, "uart2",
OS_DEV_INIT_PRIMARY, 0, uart_hal_init, (void *)&uart2_cfg);
assert(rc == 0);
#endif
view raw hal_bsp.c hosted with ❤ by GitHub

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/hw/bsp/stm32l4/src/hal_bsp.c

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…

int
hal_uart_config(int port, int32_t baudrate, uint8_t databits, uint8_t stopbits,
enum hal_uart_parity parity, enum hal_uart_flow_ctl flow_ctl)
{
...
#if !MYNEWT_VAL(MCU_STM32F1)
hal_gpio_init_af(cfg->suc_pin_tx, cfg->suc_pin_af, 0, 0);
hal_gpio_init_af(cfg->suc_pin_rx, cfg->suc_pin_af, 0, 0);
if (flow_ctl == HAL_UART_FLOW_CTL_RTS_CTS) {
hal_gpio_init_af(cfg->suc_pin_rts, cfg->suc_pin_af, 0, 0);
hal_gpio_init_af(cfg->suc_pin_cts, cfg->suc_pin_af, 0, 0);
}
#endif
//// Patch at line 468 of hal_uart.c
#if MYNEWT_VAL(UART_2_SWAP_TXRX)
if (cfg->suc_uart == USART3) { cr2 |= USART_CR2_SWAP; } //// Swap TX/RX so that USART3 behaves like LPUART1
#endif // UART_2_SWAP_TXRX
u->u_regs = cfg->suc_uart;
u->u_regs->CR3 = cr3;
u->u_regs->CR2 = cr2;
u->u_regs->CR1 = cr1;
view raw hal_uart.c hosted with ❤ by GitHub

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/patch/hal_uart.c#L468-L471

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…

## Create Bootloader Target for STM32 L476
newt target create stm32l4_boot
newt target set stm32l4_boot bsp=hw/bsp/stm32l4
newt target set stm32l4_boot app=apps/boot_stub
newt target set stm32l4_boot build_profile=debug
## Create Application Target for STM32 L476
newt target create stm32l4_my_sensor
newt target set stm32l4_my_sensor bsp=hw/bsp/stm32l4
newt target set stm32l4_my_sensor app=apps/my_sensor_app
newt target set stm32l4_my_sensor build_profile=debug

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

syscfg.vals:
###########################################################################
# CoAP Server Settings
# CoAP host e.g. 104.199.85.211 (for coap.thethings.io)
COAP_HOST: '"104.199.85.211"'
# CoAP UDP port, usually port 5683
COAP_PORT: 5683
# CoAP URI e.g. v2/things/IVRiBCcR6HPp_CcZIFfOZFxz_izni5xc_KO-kgSA2Y8 (for thethings.io, the last part is the Thing Token)
COAP_URI: '"v2/things/IVRiBCcR6HPp_CcZIFfOZFxz_izni5xc_KO-kgSA2Y8"'
###########################################################################
# Network Settings
DEVICE_TYPE: '"l476"' # Device type that will be prepended to the Device ID. thethings.io converts the raw temperature depending on the device type.
NBIOT_BAND: 8 # Connect to this NB-IoT band
SENSOR_NETWORK: 1 # Enable Sensor Network library
SENSOR_COAP: 1 # Send sensor data to CoAP server
COAP_CBOR_ENCODING: 0 # Disable CBOR encoding of CoAP payload
COAP_JSON_ENCODING: 1 # Use JSON to encode CoAP payload for forwarding to thethings.io
RAW_TEMP: 1 # Use raw temperature (integer) instead of floating-point temperature values, to reduce ROM size
###########################################################################
# Hardware Settings
HARDFLOAT: 1 # Enable hardware floating-point support for STM32L476RC
LOW_POWER: 0 # Disable low power support
UART_0: 1 # Enable USART2 for GPS module
UART_1: 0 # Disable USART1
UART_2: 1 # Enable USART3 for NB-IoT module
UART_2_SWAP_TXRX: 1 # Swap TX/RX pins for USART3 so that USART3 behaves like LPUART1
GPS_L70R: 1 # Enable driver for Quectel L70R GPS module
GPS_L70R_UART: 0 # Connect to Quectel L70R module on USART2
GPS_L70R_ENABLE_PIN: MCU_GPIO_PORTA(1) # GPIO Pin PA1 enables and disables the GPS module. Set to -1 for no pin.
BC95G: 1 # Enable driver for Quectel BC95-G NB-IoT module
BC95G_UART: 2 # Connect to Quectel BC95-G module on LPUART1 i.e. USART3 with TX/RX pins swapped
BC95G_ENABLE_PIN: MCU_GPIO_PORTA(0) # GPIO Pin PA0 enables and disables the NB-IoT module. Set to -1 for no pin.
ADC_1: 1 # Enable port ADC1
TEMP_STM32: 1 # Enable STM32 internal temperature sensor
HMAC_PRNG: 1 # Enable HMAC PRNG pseudorandom number generator
view raw syscfg-l476.yml hosted with ❤ by GitHub

Application Configuration. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/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).

/// Sensor to be polled: `gps_l70r_0` is the Quectel L70-R GPS module
static GPS_DEVICE: Strn = init_strn!("gps_l70r_0");
/// Poll GPS every 11,000 milliseconds (11 seconds)
const GPS_POLL_TIME: u32 = (11 * 1000);
/// Use key (field name) `geo` to transmit GPS geolocation to CoAP Server
const GPS_SENSOR_KEY: Strn = init_strn!("geo");
/// Type of sensor: Geolocation (latitude, longitude, altitude)
const GPS_SENSOR_TYPE: sensor_type_t = sensor::SENSOR_TYPE_GEOLOCATION;
/// Ask Mynewt to poll the GPS sensor and call `aggregate_sensor_data()`
/// Return `Ok()` if successful, else return `Err()` with `MynewtError` error code inside.
pub fn start_gps_listener() -> MynewtResult<()> { // Returns an error code upon error.
// Start the GPS driver.
console::print("Rust GPS poll\n");
start_gps_l70r() ? ;
// Fetch the sensor by name.
let sensor = sensor_mgr::find_bydevname(&GPS_DEVICE)
.next() // Fetch the first sensor that matches
.expect("no GPS"); // Stop if no sensor found
// At power on, we ask Mynewt to poll our GPS sensor every 11 seconds.
sensor::set_poll_rate_ms(&GPS_DEVICE, GPS_POLL_TIME) ? ;
// Create a sensor listener that will call function `aggregate_sensor_data` after polling the sensor data
let listener = sensor::new_sensor_listener(
&GPS_SENSOR_KEY, // Transmit as field: `geo`
GPS_SENSOR_TYPE, // Type of sensor data: GPS Geolocation
app_network::aggregate_sensor_data // Call this function with the polled data: `aggregate_sensor_data`
) ? ;
// Register the Listener Function to be called with the polled sensor data.
sensor::register_listener(sensor, listener) ? ; // `?` means in case of error, return error now.
// Return `Ok()` to indicate success. This line should not end with a semicolon (;).
Ok(())
}
view raw gps_sensor.rs hosted with ❤ by GitHub

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/rust/app/src/gps_sensor.rs

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

/// Aggregate the sensor value with other sensor data before transmitting to server.
/// If the sensor value is a GPS geolocation, we remember it and attach it to other sensor data for transmission.
pub fn aggregate_sensor_data(sensor_value: &SensorValue) -> MynewtResult<()> { // Returns an error code upon error.
if let SensorValueType::Geolocation {..} = sensor_value.value {
// If this is a geolocation, save the geolocation for later transmission.
unsafe { CURRENT_GEOLOCATION = sensor_value.value }; // Current geolocation is unsafe because it's a mutable static
Ok(())
} else {
// If this is temperature sensor data, attach the current geolocation to the sensor data for transmission.
let transmit_value = SensorValue {
geo: unsafe { CURRENT_GEOLOCATION }, // Current geolocation is unsafe because it's a mutable static
..*sensor_value // Copy the sensor name and value for transmission
};
// Transmit sensor value with geolocation and return the result
send_sensor_data(&transmit_value)
}
}
/// Current geolocation recorded from GPS
static mut CURRENT_GEOLOCATION: SensorValueType = SensorValueType::None;

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/rust/app/src/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…

// Compose the CoAP Payload using the coap!() macro.
// Select @json or @cbor To encode CoAP Payload in JSON or CBOR format.
let _payload = coap!( @json {
// Append to the `values` array the Sensor Key, Value and optional Geolocation:
// `{"key": "t", "value": 2870, "geo": { "lat": ..., "long": ... }}`
val,
...
});

From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/rust/

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.

{
// VSCode Debugger Config for Cortex Debug Extension. We use Cortex Debug because it shows more details, e.g. the STM32 Peripherals.
"version": "0.2.0",
"configurations": [
{
// Cortex Debug Configuration: https://marcelball.ca/projects/cortex-debug/cortex-debug-launch-configurations/
"name": "STM32L476RCT6",
"type": "cortex-debug",
"request": "launch",
"servertype": "openocd",
"cwd": "${workspaceRoot}",
// Application Executable to be flashed to Blue Pill before debugging. Note that the Application ELF image does not contain a valid Image Header. So we must bypass the Bootloader, shown below.
"executable": "bin/targets/stm32l4_my_sensor/app/apps/my_sensor_app/my_sensor_app.elf",
"device": "STM32L476RCT6",
// Arm System View Description, downloaded from https://www.st.com/en/microcontrollers-microprocessors/stm32l476rc.html#
"svdFile": "scripts/STM32L4x6.svd",
"configFiles": [
// Tell OpenOCD to open the ST Link connection to STM32 MCU.
"interface/stlink-v2.cfg",
"target/stm32l4x.cfg",
// Tell OpenOCD to run our custom debug commands.
"scripts/stm32l4/debug.ocd"
],
"preLaunchCommands": [
// Before loading the Application, run these gdb commands.
// Set timeout for executing openocd commands.
"set remotetimeout 60",
// This indicates that an unrecognized breakpoint location should automatically result in a pending breakpoint being created.
"set breakpoint pending on"
],
"postLaunchCommands": [
// After loading the Application, run these gdb commands.
"break main", // Break at main()
"break __assert_func", // Break for any assert failures
"break os_default_irq" // Break for any unhandled interrupts
]
}
]
}

Cortex-Debug Configuration. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/.vscode/launch-stm32l4.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…

ADC create adc1
TMP create temp_stm32_0
ADC open
STM read int temp sensor
RND personalise with hw id 22 00 29 00 04 50 42 41 46 36 39 20
RND seed with temp entropy 31 e0 01 f1 f0 01 f1 f1 00 f1 00 ff 2f 1e 10 2f f0 1e 10 00 1f 00 10 0f f1 1f 01 f1 f0 10 1e 1f
NET hwid 22 00 29 00 04 50 42 41 46 36 39 20
NET standalone node
NBT create bc95g_0
NET svr bc95g_0
GPS create gps_l70r_0
Rust TMP poll
Rust GPS poll
NET start
NET svr bc95g_0
[
AT> AT
AT< Verified
AT< REBOOT_CAUSE_SECURITY_RESET_PIN
AT< Neul
AT= OK
AT> NCONFIG=AUTOCONNECT,FALSE
AT= OK
AT> QREGSWT=2
AT= OK
AT> NRB
AT< REBOOTING
AT< �z ,'`
AT< Boot: Unsigned
AT< Security B.. Verified
AT< Protocol A.. Verified
AT< Apps A...... Verified
AT< REBOOT_CAUSE_APPLICATION_AT
AT< Neul
AT= OK
AT> AT
AT= OK
AT> NBAND=8
AT= OK
]
GPS satellites: 5
ADC open
STM read int temp sensor
Rust send_sensor_data
NET payload size 145
{"values": [{"key": "t","value": 929,"geo": {"lat": 1.271608,"long": 103.808128}},{"key": "device","value": "l476,bf39a9607e1187f6f3d80d6dd43"}]}
NET view your sensor at
https://blue-pill-geolocate.appspot.com?device=l476,bf39a9607e1187f6f3d80d6dd43
[
NBT send udp
AT> AT
AT= OK
AT> CFUN?
AT< +CFUN:0
AT= OK
AT> CFUN=1
AT= OK
AT> CGATT=1
AT= OK
AT> CEREG=0
AT= OK
AT> CEREG?
AT= +CEREG:0,2
AT= OK
AT> CEREG?
AT= +CEREG:0,2
AT= OK
AT> CEREG?
AT= +CEREG:0,2
AT= OK
AT> CEREG?
AT= +CEREG:0,2
AT= OK
AT> CEREG?
AT= +CEREG:0,2
AT= OK
AT> CEREG?
AT= +CEREG:0,1
AT= OK
AT> CGATT?
AT= +CGATT:1
AT= OK
AT> NSOCR=DGRAM,17,0,1
AT= 1
AT= OK
AT> NSOSTF=1,104.199.85.211,5683,0x200,217,
NBT send mbuf 217...
58 02 00 02 56 49 b4 b8 c9 68 4d 22 b2 76 32 06 74 68 69 6e 67 73 0d 1e 49 56 52 69 42 43 63 52 36 48 50 70 5f 43 63 5a 49 46 66 4f 5a 46 78 7a 5f 69 7a 6e 69 35 78 63 5f 4b 4f 2d 6b 67 53 41 32 59 38 11 32 51 32 ff
7b 22 76 61 6c 75 65 73 22 3a 20 5b 7b 22 6b 65 79 22 3a 20 22 74 22 2c 22 76 61 6c 75 65 22 3a 20 39 32 39 2c 22 67 65 6f 22 3a 20 7b 22 6c 61 74 22 3a 20 31 2e 32 37 31 36 30 38 2c 22 6c 6f 6e 67 22 3a 20 31 30 33 2e 38 30 38 31 32 38 7d 7d 2c 7b 22 6b 65 79 22 3a 20 22 64 65 76 69 63 65 22 2c 22 76 61 6c 75 65 22 3a 20 22 6c 34 37 36 2c 62 66 33 39 61 39 36 30 37 65 31 31 38 37 66 36 66 33 64 38 30 64 36 64 64 34 33 22 7d 5d 7d
NBT send mbuf OK
AT> ,2
AT= 1,2
AT< 17
AT= OK
AT> NSOCL=1
AT= OK
AT> CFUN=0
AT= OK
]
view raw nbiot-gps.log hosted with ❤ by GitHub

Adapter Output Log from https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/l476/logs/standalone-node.log

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!