1. Blog>
  2. Coding nRF52 with Rust and Apache Mynewt on Visual Studio Code

Coding nRF52 with Rust and Apache Mynewt on Visual Studio Code

by: Dec 01,2020 6698 Views 0 Comments Posted in Activities

Programming Gadgets Nrf52 Rust Mynewt

The nRF52 Microcontroller by Nordic Semiconductor is an awesome gadget with powerful Bluetooth Low Energy networking capability. It’s affordable too… For under $8, I can buy an EBYTE E73-TBB Development Board with onboard nRF52 (photo above). And it works like a supercharged BBC micro:bit (based on the older nRF51) in a smaller, cheaper form factor!

Powered by an Arm Cortex-M4 CPU (hardware floating-point) with 64 KB of RAM and 512 KB of Flash ROM, the nRF52 has plenty of capacity to run modern embedded platforms… Like Apache Mynewt realtime OS and Embedded Rust!

But what tools would we use to code the nRF52? Will we get locked in with proprietary IDEs and programming dongles?

Great News: The nRF52 works with popular open-source tools on Windows and macOS like VSCode, OpenOCD, Rust and ST-Link… I’ll show you how, right now! (ST-Link is not really open-source but it’s only $2!)

What is VSCode? Is it related to Visual Studio? How is Microsoft involved? Read this

Even if you’re new to nRF52, Bluetooth, Mynewt, VSCode, … You’re welcome to skim! We’ll cover a broad range of topics. including…

Bluetooth Low Energy and the iBeacon protocol

Build your own iBeacon with nRF52 and Apache NimBLE

Embedded Rust programming on nRF52

Programming IoT Sensors with Mynewt and Rust

Why Rust not C?

Code, flash and debug the nRF52 with Visual Studio Code and OpenOCD

Remove Flash Protection from your nRF52 with a Raspberry Pi

Plus upcoming topics: Bluetooth Mesh and PineTime Smart Watch!

There’s something for everyone in this article! The source code for this article may be found in the nrf52 branch of this repository…

lupyuen/stm32bluepill-mynewt-sensor

What’s an iBeacon?

There was a time… (When people still shopped at physical retail stores…) You could walk into your favourite store and your phone would “Ding!” with a notification specially customised for you… “Save up to 40% on Baby and Kids Products!”

What is this magic that senses your mere presence… And summons a unique offer… Just for you?

That magic is called iBeacon. It’s a wireless protocol released by Apple in 2013 for detecting Bluetooth Low Energy devices nearby (within a few metres). iBeacon Transmitters are dumb devices planted in the store that broadcast an iBeacon ID that’s specific to the store.

Your phone needs to have an app installed that indicates which iBeacon ID it’s seeking. When your phone detects a nearby iBeacon Transmitter with a matching iBeacon ID, it wakes up the app so that the app can send you a custom notification.

In reality it’s hard to detect Bluetooth 4 iBeacons reliably because of conflicts with WiFi, which also operates in the crowded 2.4 GHz airwaves (see https://www.hindawi.com/journals/misy/2016/8367638/).

Our nRF52 detected as an iBeacon (“My iBeacon”) in the “Locate Beacon” app: https://apps.apple.com/us/app/locate-beacon/id738709014

In this tutorial we’ll program our nRF52 to be an iBeacon Transmitter, because…

1.iBeacon is the simplest Bluetooth LE protocol to implement (and troubleshoot)

2.It’s easy to use our phones to verify that our nRF52 is indeed working correctly as a Bluetooth LE transmitter

Nostalgia… iBeacon actually served a purpose in the real world! (But if you decide to implement iBeacons today, beware of the iBeacon security implications, especially iBeacon spoofing)

nRF52 is Radio-Capable

nRF52 is similar to other microcontrollers (like the STM32 F103 found in Blue Pill)… Except that the nRF52 has 2.4 GHz Radio capabilities not found in most other microcontrollers.

2.4 GHz is used for WiFi… Does this mean that the nRF52 can talk WiFi?

Not quite… WiFi protocols are highly complex, beyond what the nRF52 can handle. Specialised microcontrollers like ESP8266 are better at handling WiFi.

But nRF52 is perfect for Bluetooth Low Energy (LE) protocols, including iBeacon. Note that Bluetooth LE is not compatible with the older standard Bluetooth. So we can’t operate our nRF52 like a classic Bluetooth tethered network device.

nRF52 doesn’t come with hardcoded firmware that enables the Bluetooth LE functions… We need to load our own Bluetooth LE firmware. Let’s discuss two options: Nordic SoftDevice and Apache NimBLE.

Nordic SoftDevice

Most nRF52 developers would probably use Nordic SoftDevice. This is the standard firmware provided by Nordic Semiconductor that implements the Bluetooth LE functions.

Nordic SoftDevice Architecture. From https://infocenter.nordicsemi.com/topic/struct_nrf52/struct/nrf52_softdevices.html

The firmware runs as a base system layer underneath our application code and RTOS.

SoftDevice reserves some hardware resources for itself, like the radio transceiver, some timers and some ROM+RAM.

The remaining resources would be available for our application, which would call the SoftDevice API to perform Bluetooth LE functions and receive notifications.

What if we wish to experiment with the Bluetooth LE implementation… Trace it to see how it works, tweak it to improve it, or even roll out a new Bluetooth LE protocol?

SoftDevice is clearly not meant for experimentation… Apache NimBLE is perfect for that!

Apache NimBLE

Apache NimBLE is an open-source Bluetooth LE stack that completely replaces SoftDevice on nRF51 and nRF52 chipsets. It’s designed to run with the Apache Mynewt embedded OS, so NimBLE feels like a typical Mynewt task.

Apache NimBLE is the Bluetooth LE implementation that we’re adopting for this tutorial.

In this tutorial we’ll often refer to NimBLE as Mynewt… Because NimBLE is so seamlessly integrated with Mynewt. Just note that Mynewt and NimBLE actually belong to two different code repositories…

Mynewt: https://github.com/apache/mynewt-core

NimBLE: https://github.com/apache/mynewt-nimble

Why Visual Studio Code with ST-Link (instead of nRFgo Studio with J-LINK)

nRF52 Development Board connected to ST-Link USB Programmer

If you’re already familiar with nRF52 development tools like nRFgo Studio and J-LINK… This tutorial will open your eyes!

My previous tutorials have been based on open-source tools and affordable, accessible hardware. For this tutorial we’ll be reusing Visual Studio Code and ST-Link (with OpenOCD).

Yes, the open-source tools we use for coding STM32 may also be used for nRF52!

Debugging Embedded Rust on nRF52 with Visual Studio Code and ST-Link

The generic ST-Link V2 USB Adapter costs under $2 (search AliExpress for st-link v2) and works perfectly fine for flashing and debugging the nRF52… Except for removing nRF52 flash protection.

How is ST-Link different from J-LINK, since both are used for flashing and debugging Arm microcontrollers?

ST-Link and J-LINK are both Arm SWD (Serial Wire Debug) Programmers. ST-Link is known as a High-Level Adapter… ST-Link doesn’t implement all SWD functions, just the minimal set of high-level functions needed for flashing and debugging. Thus ST-Link can’t be used for removing the nRF52 flash protection.

Removing nRF52 flash ROM protection with Raspberry Pi

If your nRF52 flash ROM is protected (and ST-Link refuses to flash your device), you may use a Raspberry Pi to remove the protection.

This only needs to be done once (and ST-Link will work fine after that).

Check the instructions in the section “Advanced Topic: Remove nRF52 Flash Protection” at the end of this article.

Welcome nRF52 (and nRF51)! Come join STM32 in the Open Source Party… Rust included!

Mynewt Project Structure based on https://github.com/lupyuen/stm32bluepill-mynewt-sensor/tree/nrf52

Mynewt Project Structure

Mynewt is a lightweight embedded operating system that pulls in only the modules that it needs to create the firmware image. Here are the files in our Mynewt project… (Check this article if you wish to download the source code and browse with Visual Studio Code)

apps: C Source Code for Bootloader and Application

This is where we put our Bootloader and Application source code in C. The Mynewt build script will compile the code here into the Bootloader and Application Firmware Images.

apps/boot_stub/src: C source code for our Bootloader. We’re using a simple Stub Bootloader: Upon startup it doesn’t do anything, it just jumps to the Application.

apps/my_sensor_app/src: C source code for our Application. The iBeacon code is located in ble.c. We’ll cover this in a while.

rust: Rust Source Code

All Rust code is placed in this folder. Mynewt doesn’t support Rust officially (yet), so I created a custom Application Build Script for Mynewt that injects Rust Application code into the Mynewt Application Build. (We’ll soon discover that the Rust code was injected in a sneaky way…)

This means that we can write our main() function in Rust and call other Rust modules and crates. Since Rust supports the calling of C functions, we may call the Mynewt API from Rust as well. (Though calling the Mynewt API through a proper Rust Wrapper is preferred… More about this later)

Here’s the Rust code in our Mynewt Project…

rust/app/src: Rust source code for our Application. The main() function is defined in lib.rs, it’s called when our device starts up. We’ll cover this in a while.

rust/mynewt/src: Rust Wrappers for Mynewt API. It’s possible to call the Mynewt API via extern declarations in Rust, but that wouldn’t be efficient. (Imagine converting Rust strings to null-terminated C strings for every extern call.) Also we wouldn’t be able to exploit the power of Rust Macros, Iterators, Error Handling, …

Thus I have created Rust Wrappers that allow Rust applications to call the Mynewt API in a safe and simple way. We’ll see examples of this in a while.

rust/macros/src: Rust Macros for generating Rust Wrappers. Most of the Rust Wrappers were automatically generated with the bindgen tool and Rust Procedural Macros. These macros are invoked only during Rust compilation, not at runtime.

libs: Custom Mynewt Libraries used by our Application

These are C libraries that I have created to make Mynewt more friendly for embedded developers. semihosting_console allows debugging messages to be displayed in the Visual Studio Code Debugger (without using a serial port). temp_stub is a Mynewt Driver that simulates a Temperature Sensor, used in our Rust application.

rust_app is a Stub Library for injecting the compiled Rust Application code. Our build script will bundle the compiled Rust Application code (including external crates) into rust_app, which gets linked into the Application Firmware. (Remember our main() function in Rust? It gets bundled into rust_app)

Similarly, rust_libcore is a Stub Library for injecting the Rust Core Library into the Application Firmware. The Rust Core Library is part of the Rust Compiler and it’s needed for core functions (like manipulating strings). Note that we’re not using the full Rust Standard Library, which contains lots of code that’s irrelevant for embedded platforms.

hw/bsp: Board Support Packages for Mynewt

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

hw/bsp/nrf52: Board Support Package for our nRF52 microcontroller board. This is a clone of the official ada_feather_nrf52 Board Support Package, with the LED and Button settings customised for the EBYTE E73-TBB Development Board. (You should update these settings to suit your nRF52 development board.)

targets: Bootloader and Application Targets for Mynewt

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

How do we compile an application like my_sensor_app for our nRF52832 development board? We tell Mynewt to create a “Target” for the application, i.e. an instance of my_sensor_app that’s targeted for our Board Support Package hw/bsp/nrf52

The targets folder contains Bootloaders and Applications that have been targeted for specific Board Support Packages…

targets/nrf52_boot: This is the boot_stub Bootloader targeted for nRF52

targets/nrf52_my_sensor: This is the my_sensor_app Application targeted for nRF52. The Application Settings are configured at targets/nrf52_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:      '"nrf52"' # 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:         0 # Disable USART2
  UART_1:         0 # Disable USART1
  UART_2:         0 # Disable USART3
  UART_2_SWAP_TXRX:    0 # Disable swapping of TX/RX pins for USART3
   
  GPS_L70R:        0 # Disable 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:         0 # Disable 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:         0 # Disable port ADC1
  TEMP_STM32:       0 # Disable STM32 internal temperature sensor
  HMAC_PRNG:       0 # Disable HMAC PRNG pseudorandom number generator
  TEMP_STUB:       1 # Enable stub temperature sensor

Application Settings. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/targets/nrf52_my_sensor/syscfg.yml

scripts: Build, Flash and Debug Scripts

scripts/build-app.cmd, .sh: This shell script builds the Rust Application by calling cargo build and bundles the compiled Rust code (with external crates) into the rust_app library.

Then it builds the Application Firmware by running newt build nrf52_my_sensor, injecting rust_app (Rust Application) and rust_libcore (Rust Core Library) into the Application Firmware.

The Rust build is targeted for thumbv7em-none-eabihf (Arm Cortex M4 with Hardware Floating-Point), which is the designation for the Arm processor in the nRF52832 microcontroller.

scripts/nrf52: Contains the build, flash and debug scripts specific to nRF52. The Flash Bootloader, Flash Application and Debug Scripts include OpenOCD scripts (*.ocd) that connect to the nRF52 via ST-Link.

Here’s the OpenOCD command used in the Flash Application Script that connects to nRF52 via ST-Link to flash the Application Firmware…

openocd/bin/openocd \
  -f scripts/nrf52/flash-init.ocd \
  -f interface/stlink.cfg \
  -c "transport select hla_swd" \
  -f target/nrf52.cfg \
  -f scripts/nrf52/flash-app.ocd

The OpenOCD script scripts/nrf52/flash-app.ocd specifies the firmware image file to be flashed (my_sensor_app.img) and the ROM address (0x0000 4000)…

# From https://devzone.nordicsemi.com/f/nordic-q-a/42824/flashing-nrf5832-using-only-st-link-v2-and-openocd
gdb_flash_program enable
gdb_breakpoint_override hard
# Connect to the device.
init
# Enable ARM semihosting to show debug console output.
arm semihosting enable
echo "Stopping..."
reset halt
echo "Flashing Application..."
program bin/targets/nrf52_my_sensor/app/apps/my_sensor_app/my_sensor_app.img verify 0x00004000
# Restart the device.
reset halt
exit

Note that…

Bootloader Code is located at ROM Address 0x0000 0000

Application Code is located at ROM Address 0x0000 4000

repos: Apache Mynewt and NimBLE Source Code

This folder contains the official Mynewt and NimBLE source code in C. We shouldn’t change anything here.

bin: Compiled Bootloader and Application Code

The build scripts produce Bootloader and Application Firmware Images in this folder. These firmware images are used by the Flash Bootloader and Flash Application Scripts to flash the Bootloader and Application to the nRF52.

The Application Firmware is also flashed to the nRF52 when we click Start Debugging.

.vscode: VSCode Settings

tasks.json defines the Build and Flash Bootloader / Application Tasks in VSCode. These tasks invoke the scripts in the scripts folder

launch-nrf52.json contains the nRF52 debugger settings for the Cortex-Debug Extension that we’re using to debug our application. When we run the Build Application script, the script copies launch-nrf52.json to launch.json, which is loaded by the Cortex-Debug debugger.

Create an iBeacon with NimBLE

Let’s look at the application code that calls the NimBLE API to create an iBeacon Transmitter: apps/my_sensor_app/src/ble.c. Why did we code this in C and not Rust? We’ll discuss this in a while.

int start_ble(void) {
  // Set the callback for starting Bluetooth LE.
  ble_hs_cfg.sync_cb = ble_app_on_sync;
  return 0;
}

start_ble() is called by the Rust main() function to start the iBeacon broadcasts in our application.

In Bluetooth LE applications, it’s mandatory to wait for the Host (i.e. Arm Processor) and Controller (i.e. Radio Transceiver) to sync up before performing any Bluetooth LE functions.

Here we set up the ble_hs_cfg.sync_cb callback defined in NimBLE, so that NimBLE will call our function ble_app_on_sync() as soon as the Host and Controller are in sync. (Which happens very quickly upon startup… Just set a breakpoint in ble_app_on_sync() and watch!)

static void ble_app_on_sync(void) {
  // Called upon starting Bluetooth LE.
  // Generate a non-resolvable private address.
  ble_app_set_addr();

  // Advertise indefinitely as an iBeacon.
  ble_app_advertise();
}

When the Host and Controller are in sync, ble_app_on_sync() calls two functions to set up the iBeacon Transmitter…

1.ble_app_set_addr(): Generate a Non-Resolvable Private Address

2.ble_app_advertise(): Advertise indefinitely as an iBeacon

What’s a Non-Resolvable Private Address? Just like any networking protocol, in Bluetooth LE we need to identify ourselves with a network address. Since we’re creating an iBeacon Transmitter with no receive capability, it’s OK to use a temporary random address, i.e. Non-Resolvable Private Address.

static void ble_app_set_addr(void) {
  // Generate a non-resolvable private address.
  ble_addr_t addr;
  int rc;

  rc = ble_hs_id_gen_rnd(1, &addr);
  assert(rc == 0);

  rc = ble_hs_id_set_rnd(addr.val);
  assert(rc == 0);
}

Here’s how we call the NimBLE API ble_hs_id_gen_rnd() to generate that random 6-byte Non-Resolvable Private Address.

Once we have obtained the random address, we tell NimBLE to use it by calling ble_hs_id_set_rnd(). If you’re curious to see the random address, just set a Debugger Breakpoint by clicking the gutter to add a red dot like this…

Setting a breakpoint to observe the randomly-generated 6-byte Non-Resolvable Private Address

Now let’s find out what our nRF52 shall be broadcasting…

Set iBeacon Parameters

iBeacon ID links the iBeacon Transmitter to the Mobile App

Remember our iBeacon sketch? Every iBeacon Transmitter needs to broadcast the following…

1.iBeacon ID: Our iBeacon Transmitter shall broadcast this 16-byte iBeacon ID, which looks like 11111111–1111–1111–1111–111111111111 (in hexadecimal). Upon sensing the iBeacon ID in the airwaves, the phone OS (iOS or Android) will wake up our Mobile App that’s linked to this iBeacon ID.

2.Major ID: A 16-bit number to differentiate iBeacon Transmitters

3.Minor ID: Another 16-bit number to differentiate iBeacon Transmitters

How are Major and Minor IDs used? Let’s say we operate a chain of stores. All stores in the chain would use the same Mobile App, so all iBeacon Transmitters in the stores should broadcast the same iBeacon ID.

How would we identify which store the customer has stepped into? Easy… Just assign a unique Major ID for each store! The app would be able to sense the Major ID from the iBeacon broadcasts and figure out which store you’re at.

Could we identify which part of the store the customer is at? Sure! Just assign a unique Minor ID for each iBeacon Transmitter in the store. Here’s how we broadcast the iBeacon ID, Major ID and Minor ID in NimBLE…

static void ble_app_advertise(void) {
  // Advertise indefinitely as an iBeacon.
  struct ble_gap_adv_params adv_params;
  uint8_t uuid128[16];
  int rc;

  // Arbitrarily set the UUID to a string of 0x11 bytes.
  memset(uuid128, 0x11, sizeof uuid128);

  // Set iBeacon parameters: Major=2, Minor=10, RSSI=-60.
  // RSSI is the Measured Power (RSSI value at 1 Meter). Must be > -126 and < 20.
  rc = ble_ibeacon_set_adv_data(uuid128, 2, 10, -60); // TODO: Verify RSSI for your device.
  assert(rc == 0);

  // Begin advertising as an iBeacon.
  adv_params = (struct ble_gap_adv_params){ 0 };
  rc = ble_gap_adv_start(BLE_OWN_ADDR_RANDOM, NULL, BLE_HS_FOREVER,
              &adv_params, NULL, NULL);
  assert(rc == 0);
}

In our code we used an arbitrary iBeacon ID uuid128 that’s defined as 11111111–1111–1111–1111–111111111111 (in hexadecimal). We pass the iBeacon ID to ble_ibeacon_set_adv_data(), together with Major ID 2 and Minor ID 10. When we call ble_gap_adv_start(), our nRF52 starts advertising itself as an iBeacon.

There’s one more parameter that we passed to ble_ibeacon_set_adv_data()… Measured Power, which is the RSSI value at 1 meter: -60. This value is broadcast by the iBeacon, together with the other IDs.

Since we are diving deep into wireless transmission, let’s study the meaning of RSSI, known to most of us as Signal Strength…

How Near Is Our iBeacon?

Received Signal Strength Indication (RSSI) is a common metric for measuring the Signal Strength of wireless networks like WiFi, NB-IoT and Bluetooth LE. RSSI values are usually negative, and higher values denote stronger signals (RSSI -50 is stronger than RSSI -60).

Why do iBeacons broadcast their RSSI values? So that we may estimate how near they are!

Estimated Proximity for our nRF52 iBeacon in the Locate Beacon app: 0.7 metres

Recall that we set our iBeacon Transmitter’s Measured Power as -60. According to the definition of Measured Power, if we placed our mobile phone 1 metre away from our iBeacon Transmitter, the phone would record the RSSI as -60.

Thus if the RSSI recorded by the phone was -50 or higher, the iBeacon Transmitter would probably be close to the phone (within 1 metre). This gives us a simple way to estimate the distance between the iBeacon Transmitter and our mobile phone.

In the real world, the estimated proximity of iBeacons is not really accurate. Bluetooth LE signals often clash with WiFi in the crowded 2.4 GHz airwaves, so the RSSI values may fluctuate wildly. And when Bluetooth LE signals pass through objects (like human bodies) the RSSI values will drop.

It’s hard to do accurate distance ranging for any kind of wireless signal (WiFi and NB-IoT included). But it’s good to understand what RSSI means, and how wireless signals degrade when transmitting over long distances, passing through obstacles.

For more details on implementing iBeacon Transmitters with NimBLE, check out the complete tutorial at https://mynewt.apache.org/latest/tutorials/ble/ibeacon.html

Testing our nRF52 iBeacon with the “Locate Beacon” mobile app

Test Our iBeacon

Let’s use a real Mobile App to verify that our nRF52 is indeed broadcasting as an iBeacon.

Follow the instructions in this article to flash and debug your nRF52 with an ST-Link V2 adapter.

Start a debug session for nRF52 in Visual Studio Code. Whenever the program pauses at a breakpoint (this will happen twice), click Continue or press F5. Keep the nRF52 powered on.

Install the “Locate Beacon” app on your iPhone…

 Launch the “Locate Beacon” app on your iPhone.

Tap the Gear icon at top right

Tap Add New UUID

Tap the + button at top right

 Enter My iBeacon as the name

For the UUID, enter

11111111–1111–1111–1111–111111111111

Leave the Major, Minor and Power fields empty

Tap Save

Our nRF52 should appear in the list of detected iBeacons as My iBeacon

Tap My iBeacon


Details of our iBeacon appear, including the RSSI and estimated proximity.

How did I figure out that my nRF52’s Measured Power was -60? By trial and error!

I placed my phone 1 metre away from the nRF52, then adjusted the Measured Power value until the estimated distance in the Locate Beacon app showed 1 metre.

Poll A Sensor With Rust

Now let’s talk about Embedded Rust! As we have discovered, our Mynewt Project allows us to embed Rust modules and crates into our Application Firmware (via sneaky substitution of the rust_app and rust_libcore libraries). The main() function is defined in Rust, in fact.

But we haven’t seen Rust in action while creating our iBeacon. How can we be sure that Rust is indeed running properly on our nRF52?

We’ll prove that by polling a Simulated Mynewt Sensor with Rust! Here how we do that…

#[no_mangle]         // Don't mangle the name "main"
extern "C" fn main() -> ! { // Declare extern "C" because it will be called by Mynewt
  // Initialise the Mynewt packages and drivers.
  mynewt::sysinit();

Very first thing in any Mynewt Application: Call sysinit(). From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/lib.rs

Our main() function is defined in the Rust module rust/app/src/lib.rs. The first thing that happens in main(): Call sysinit() to initialise the Mynewt libraries and drivers. (Mynewt programs in C also call sysinit() at startup)

On the nRF52, sysinit() initialises the 2.4 GHz Radio Transceiver and the Stub Temperature Sensor. We’ll talk about this simulated temperature sensor in a while.

  // Start the Server Transport for sending sensor data to CoAP Server over NB-IoT.
  //sensor_network::start_server_transport()
    //.expect("NET fail");

  // Start polling the temperature sensor every 10 seconds in the background.
  app_sensor::start_sensor_listener()
    .expect("TMP fail");

Start polling the simulated temperature sensor. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/lib.rs

Remember we said that Mynewt programs are designed to be portable across microcontrollers? The Rust-Mynewt application we’re studying now actually runs fine on STM32 F103 (Blue Pill) and L476 microcontrollers… But they run a little differently.

On STM32 microcontrollers, our Rust application polls the onboard temperature sensor every 10 seconds and transmits the temperature data to a server (via an NB-IoT module connected to the microcontroller).

On the nRF52 we won’t be transmitting the sensor data to a server, so the code to start the Server Transport has been commented out (for now).

Next, we call start_sensor_listener() (defined in our application module app_sensor.rs) to begin polling the simulated temperature sensor every 10 seconds.

  // Start Bluetooth LE. TODO: Create a safe wrapper for starting BLE.
  extern { fn start_ble() -> i32; }
  let rc = unsafe { start_ble() };
  assert!(rc == 0, "BLE fail");

Start broadcasting as iBeacon. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/lib.rs

Remember our C function start_ble() that initiates the iBeacon broadcasting? This is how we call start_ble() from main(). It needs to be tagged as unsafe because to the Rust Compiler, all C functions are risky and could potentially cause problems (memory corruption, crashing, …) The unsafe tag will be removed once we create a safe and proper Rust Wrapper for NimBLE.

  // Main event loop
  loop {              // Loop forever...
    os::eventq_run(        // Processing events...
      os::eventq_dflt_get()   // From default event queue.
        .expect("GET fail")
    ).expect("RUN fail");
  }
  // Never comes here
}  // End of main() function

Mynewt Event Loop. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/lib.rs

At the end of the main() function we have a standard Mynewt Event Loop to handle Mynewt system events. (Mynewt programs in C also have this Mynewt Event Loop). Without this Event Loop, the NimBLE functions will never get any processing done.

Mynewt Sensor Framework, Enhanced With Rust

Let’s look at start_sensor_listener(), defined in our application module app_sensor.rs...

/// Sensor to be polled: `temp_stub_0` is the stub temperature sensor that simulates a temperature sensor
static SENSOR_DEVICE: Strn   = init_strn!("temp_stub_0");
/// Poll sensor every 10,000 milliseconds (10 seconds)  
const SENSOR_POLL_TIME: u32   = (10 * 1000);  
/// Use key (field name) `t` to transmit raw temperature to CoAP Server
const TEMP_SENSOR_KEY: Strn   = init_strn!("t");
/// Type of sensor: Raw temperature sensor (integer sensor values 0 to 4095)
const TEMP_SENSOR_TYPE: sensor_type_t = sensor::SENSOR_TYPE_AMBIENT_TEMPERATURE_RAW;

Define the name of the Stub Temperature Sensor and the polling interval. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_sensor.rs

At the top of app_sensor.rs we define some constants for polling the sensor.

SENSOR_DEVICE is defined as temp_stub_0, which refers to the Stub Temperature Sensor. The Stub Temperature Sensor works like a regular Mynewt Temperature Sensor… Except that it always returns a hardcoded raw temperature value 1757. Useful for testing sensor applications without connecting a real temperature sensor. (On STM32 this program polls the actual onboard temperature sensor)

SENSOR_POLL_TIME is set to 10,000 milliseconds, or 10 seconds. We’ll be asking Mynewt to poll our simulated temperature sensor every 10 seconds.

What’s Strn? This is a custom string type that I have defined to make passing of strings safer and more efficient. Mynewt APIs require all strings to be null-terminated; Rust strings don’t need the terminating null.

If we pass strings back and forth between Rust and the Mynewt APIs, we could end up creating many clones of the same string, with and without terminating nulls. Or worse… Mynewt could crash because some Rust code has incorrectly passed in a string that’s not null-terminated. So I have wrapped the Mynewt APIs to accept the safer, efficient Strn type that’s always null-terminated.

/// Ask Mynewt to poll or read the temperature sensor and call `aggregate_sensor_data()`
pub fn start_sensor_listener() -> MynewtResult<()> { // Returns an error code upon error.
  console::print("Rust TMP poll\n");

  // Fetch the sensor by name.
  let sensor = sensor_mgr::find_bydevname(&SENSOR_DEVICE)
    .next()       // Fetch the first sensor that matches
    .expect("no TMP"); // Stop if no sensor found

Fetch the Stub Temperature Sensor by name. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_sensor.rs

Here’s the first clue that Mynewt has an awesome Sensor Framework… Mynewt keeps track of all installed sensors by name. sensor_mgr::find_bydevname() is the Mynewt API that returns a list of sensors (i.e. a Sensor Iterator) that match a name (temp_stub_0).

  // At power on, we ask Mynewt to poll our temperature sensor every 10 seconds.
  sensor::set_poll_rate_ms(&SENSOR_DEVICE, SENSOR_POLL_TIME) ? ;

Set the polling interval for the Stub Temperature Sensor. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_sensor.rs

Mynewt recognises every sensor and the type of data that the sensor produces. So let’s ask Mynewt to poll the sensor on our behalf!

By calling sensor::set_poll_rate_ms() we’re kindly asking Mynewt to poll our simulated temperature sensor every 10 seconds. Truly awesome!

  // Create a sensor listener that will call function `aggregate_sensor_data` after polling the sensor data
  let listener = sensor::new_sensor_listener(
    &TEMP_SENSOR_KEY,  // Transmit as field: `t`
    TEMP_SENSOR_TYPE,  // Type of sensor data: Raw temperature (integer from 0 to 4095)
    app_network::aggregate_sensor_data // Call this function with the polled data: `aggregate_sensor_data`
  ) ? ;

Create a Sensor Listener. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_sensor.rs

Now we fill in the nitty-gritty polling details… What shall we do with the temperature sensor data after Mynewt has obtained it? Here we ask Mynewt to call our Rust function aggregate_sensor_data() with the sensor data.

aggregate_sensor_data() is known as a Sensor Listener Function… It’s a function that listens for updated sensor data and acts on the 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(())
}  // End of `start_sensor_listener()` function

Register the Sensor Listener with Mynewt Sensor Framework. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_sensor.rs

To activate the Sensor Listener Function, we call the Mynewt API sensor::register_listener(). Mynewt will begin polling the simulated temperature sensor every 10 seconds and call aggregate_sensor_data() with the polled temperature data.

Polling a sensor in Mynewt is really so easy… in C and in Rust! That’s possible only because Mynewt has a well-designed Sensor Framework.

Handle Sensor Data With Rust

Remember that we don’t transmit sensor data to a server in the nRF52 version of the Rust application (unlike the STM32 version). But we’ll take a peek to understand how our sensor data could have been easily packaged and delivered to an IoT server.

Earlier we asked Mynewt to call aggregate_sensor_data() whenever it has polled our simulated temperature sensor. Let’s see what happens inside aggregate_sensor_data()

/// 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)
  }
}

Aggregate temperature data with GPS data. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_network.rs

On the STM32 L476 microcontroller our Rust application not only handles temperature sensor data… It handles GPS latitude / longitude coordinates as well! The application polls the GPS module for the current geolocation (just like any Mynewt sensor) and attaches the geolocation to the temperature data before transmitting to the server.

On nRF52 we’ll settle for less… aggregate_sensor_data() will simply transmit the temperature data without attaching any GPS coordinates. It calls send_sensor_data() to transmit the temperature data…

/// Compose a CoAP JSON message with the Sensor Key (field name), Value and Geolocation (optional) in `val`
/// and send to the CoAP server. For the CoAP server hosted at thethings.io, the CoAP payload shall be encoded in JSON like this:
/// ```json
/// {"values":[
///  {"key":"t",   "value":1715, "geo": { "lat": ..., "long": ... }},
///  {"key":"device", "value":"0102030405060708090a0b0c0d0e0f10"}
/// ]}
/// ```
fn send_sensor_data(val: &SensorValue) -> MynewtResult<()> { // Returns an error code upon error.
  console::print("Rust send_sensor_data: ");
  if let SensorValueType::Uint(i) = val.value {
    console::print_strn(val.key); console::print("="); console::printint(i as i32);
  }
  console::print("\n"); console::flush();

Send sensor data to CoAP server. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_network.rs

This sounds incredulous… But send_sensor_data() was designed to transmit any sensor data in any format that will be understood by our server!

For example, the code here works perfectly for transmitting temperature data to the server at thethings.io. This server accepts CoAP Messages with a JSON Payload that contains the geolocated sensor data.

Yet strangely, send_sensor_data() doesn’t contain any code that’s specific to thethings.io… The sensor data appears to transform itself magically for thethings.io. How is this possible? We’ll learn in a while…

  // Get a randomly-generated device ID that changes each time we restart the device.
  let device_id = sensor_network::get_device_id() ? ;

  // Start composing the CoAP Server message with the sensor data in the payload. This will 
  // block other tasks from composing and posting CoAP messages (through a semaphore).
  // We only have 1 memory buffer for composing CoAP messages so it needs to be locked.
  let rc = sensor_network::init_server_post( strn!(()) ) ? ; // `strn!(())` means use default CoAP URI in `syscfg.yml`

  // If network transport not ready, tell caller (Sensor Listener) to try again later.
  if !rc { return Err(MynewtError::SYS_EAGAIN); }

Compose an outgoing CoAP message. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_network.rs

This is the end of the road for nRF52… We haven’t started a Network Transport (like NB-IoT) that will deliver the sensor data to a server, so nRF52 silently drops the sensor data here.

For STM32, the Rusty trail continues…

// 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 {     
    // Create `values` as an array of items under the root.
    // Assume `val` contains `key: "t", val: 2870, geo: { lat, long }`. 
    // Append to the `values` array the Sensor Key, Value and optional Geolocation:
    // `{"key": "t", "value": 2870, "geo": { "lat": ..., "long": ... }}`
    val,

    // Append to the `values` array the random device ID:
    // `{"key":"device", "value":"0102030405060708090a0b0c0d0e0f10"}`
    "device": &device_id,
  });

  // Post the CoAP Server message to the CoAP Background Task for transmission.
  sensor_network::do_server_post() ? ;

  // The CoAP Background Task will transmit the message in the background.
  Ok(())
}  // End of `send_sensor_data()` function

Compose and transmit the JSON Payload containing the temperature and GPS data. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/nrf52/rust/app/src/app_network.rs

Believe it (or not)… That’s all the code we need to transform our geolocated sensor data into this complicated nested CoAP + JSON format mandated by thethings.io…

{ "values": [
  { "key" : "t",    
   "value": 1757, 
   "geo" : { "lat": 1.2701, "long": 103.8078 }},
  { "key" : "device",
   "value": "l476,bf39a9607e1187f6f3d80d6dd43" }
]}

The secret of the sensor data transformation? It’s in the coap!() macro!

Rust Declarative Macros are incredibly powerful… A Rust macro can transform a simple JSON object into a complicated nested CoAP + JSON monster. The coap!() macro hides the details of the data transformation. That’s why we can transmit any sensor data in any format that will be understood by the server… Just let the coap!() macro handle it!

Thanks to Mynewt, the transmission of sensor data is highly efficient. The CoAP Message is transmitted (over NB-IoT) by a background task in Mynewt. So our application may continue processing sensor data without waiting for the transmission to complete.

Mynewt and Rust are perfectly paired for building safe and efficient embedded systems!

Watch Rust Run On nRF52

Ready to watch Rust run on nRF52? All you need is an nRF52 development board and an ST-Link V2 adapter.

Follow the instructions in this article to flash and debug your nRF52.

The Output Log in Visual Studio Code should look like this…

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

Here’s a video demo of the Application Build and Debug on nRF52…

Video demo of the Application Build and Debug on nRF52

Polling sensors in Mynewt: Rust vs C

Why Embedded Rust Instead of Embedded C?

Take a look at the above Rust code that’s running on our nRF52 for polling the simulated temperature sensor. Compare that with the equivalent C code.

Why is Rust better than C?

Yes the Rust code looks more verbose than C… But that’s a good thing! C programming is so terse that it makes C difficult to learn. Experienced C programmers (like me) are indeed a dying breed.

Rust uses sensible keywords (like fn to denote functions, let for declaring variables), making it easier to learn. Note also that the Rust code doesn’t use any pointers. If you look at the C code, it’s really easy to misuse pointers like listen_sensor and listener… causing more problems and frustration to learners.

Error handling in Rust is done elegantly… We use the ? operator to catch errors and exit early. Compare that with the unsightly assert() in C. What if we forget to check the return code in C? Strange bugs ensue.

Why not code EVERYTHING in Rust… Including Mynewt OS and NimBLE?

There’s an amazing community hard at work creating Rust on Bare Metal. But it will take a while to get it running and tested with real-world applications on nRF52, nRF51, STM32 F103, STM32 F476, RISC-V, …

I’m solving this problem with a different approach by applying Lean Principles

You, the reader, the learner, are my Customer. You wish to build a safe and efficient Embedded Application (hopefully in Rust). What Rust APIs shall I provide you?

I could build a Rust OS from scratch based on Bare Metal Rust, and offer you a Native Rust API. But that’s not very Lean.

Or I could take an embedded OS that’s already available, say Mynewt or Zephyr or FreeRTOS. Wrap it up with a clean Rust API, and give that Wrapped Rust API to you instead. You wouldn’t know the difference between the Native and Wrapped Rust APIs! (Unless I told you)

The Wrapped Rust API won’t be perfect, because as you create new gadgets with the API, you may find the API cumbersome or hard to use. So I’ll take this opportunity to evolve the API iteratively, till we get the Perfect Embedded Rust API. (That’s how I evolved the Mynewt Sensor Framework with Rust Iterators)

Now we’re ready to revamp the OS with Rust and restructure it to implement the Perfect Embedded Rust API in the safest and most efficient way possible.

This, I think, is the right approach for solving the Embedded Rust problem. And it needs to happen soon (based on Mynewt or Zephyr or FreeRTOS or …) so that we may quickly move embedded coders away from unsafe C and onto Rust.

Rust Wrappers for NimBLE

Where are the Rust Wrappers for the NimBLE API? Since the Mynewt Sensor Framework already has Rust Wrappers, it shouldn’t be too difficult to create Rust Wrappers for NimBLE right?

That’s work in progress. With the Mynewt Sensor Framework I understand clearly how the API is used to read and poll sensors under various situations. The Rust Wrappers for the Mynewt Sensor Framework were designed for these use cases. With the NimBLE API… The use cases are still fuzzy to me.

Some NimBLE applications appear to have lots of repetitive code, like this Bluetooth Mesh application. They could be greatly simplified with Rust Macros. Just like the coap!() Rust Macro we used for composing CoAP messages.

If you would like to help out with the design of the Rust Wrappers for NimBLE, drop me a note!

References

My code was tested on the EBYTE E73-TBB Development Board. The board is based on the EBYTE E73–2G4M04S1B module, which embeds the nRF52832 microcontroller.

Manual for EBYTE E73–2G4M04S1B Module

Manual for EBYTE E73-TBB Development Board (Chinese)


Note: The content and the pictures in this article are contributed by the author. The opinions expressed by contributors are their own and not those of PCBWay. If there is any infringement of content or pictures, please contact our editor (zoey@pcbway.com) for deleting.


Written by

Join us
Wanna be a dedicated PCBWay writer? We definately look forward to having you with us.
  • Comments(0)
You can only upload 1 files in total. Each file cannot exceed 2MB. Supports JPG, JPEG, GIF, PNG, BMP
0 / 10000
    Back to top