Connect STM32 Blue Pill to NB-IoT with Quectel BC95-G and Apache Mynewt

In the previous article we learnt the AT commands for sending sensor data to a CoAP Server via a Quectel NB-IoT module. Now let’s build an IoT sensor with a real microcontroller — STM32 Blue Pill — and a real NB-IoT module — Quectel BC95-G!

Why are we building it with Apache Mynewt realtime operating system?

Because handling the AT commands for the Quectel module is a little complicated… Imagine we’re polling the temperature sensor every 10 seconds and transmitting the sensor data via NB-IoT. But the Quectel module hasn’t responded to our AT command yet. Do we give up and crash the application?

Fortunately Mynewt is fully capable of multitasking — it will wait for the AT command to complete (or cancel it in case of timeout). It has a built-in CoAP library for composing CoAP messages. And drivers for many sensors. So Mynewt is perfect for building NB-IoT devices!

Here’s what we’ll be building today…

The complete source code is located here…

Hardware Required

We’ll need the following hardware…

1️⃣ STM32 Blue Pill

2️⃣ ST-Link V2 USB Adapter

3️⃣ Quectel BC95-G Global NB-IoT Module (breakout board with antenna). I ordered mine here. The manual in Chinese is here.

4️⃣ 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!

Blue Pill connected to Quectel BC95-G and ST-Link

Connect Blue Pill to Quectel Module

Connect Blue Pill to Quectel BC95-G and ST-Link as follows…

Both yellow jumpers on Blue Pill should be set to the 0 position, as shown in the above photo.

Note that we are powering the Quectel module with 5V from ST-Link instead of 3.3V from Blue Pill. That’s because the module requires more power than Blue Pill can provide. (How did I find out? Because the module kept restarting when I powered it from Blue Pill.)

Check the documentation for your Quectel breakout board to confirm that it supports 5V. (Mine does)

Insert the NB-IoT SIM 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!

Don’t connect ST-Link to your computer yet, we’ll need to install the ST-Link driver in a while.

Install Mynewt

Follow the instructions below to install the Mynewt build and application files on Windows…

The NB-IoT Program

We have just installed a simple program that reads Blue Pill’s internal temperature sensor every 10 seconds and sends the data to a CoAP Server ( over the NB-IoT network.

Function main()

main() in apps/my_sensor_app/src/main.c is the function called upon device startup. Mynewt applications are required to call sysinit() to start the system services and drivers, including the drivers for the internal temperature sensor and the NB-IoT module.

start_sensor_listener() is called next to set up the polling schedule for the temperature sensor. We’ll study this in a while.

start_server_transport() is called to connect the NB-IoT module to the NB-IoT network. This may take a few seconds to complete, so start_server_transport() will perform the connection as a background task. start_server_transport() is part of the Sensor Network Library.

The main event loop appears at the end of the main() function. This is required by Mynewt for processing system events.

Functions start_network_sensor() and handle_sensor_data()

start_sensor_listener() in apps/my_sensor_app/src/sensor.c is called by main() to set the polling schedule for the temperature sensor.

sensor_set_poll_rate_ms() is the Mynewt system function that’s called to set the temperature sensor (temp_stm32_0) to be polled every 10,000 milliseconds (or 10 seconds)

What happens when Mynewt has polled the sensor data?

In the call to sensor_register_listener() (another Mynewt system function), we instruct Mynewt to call our function handle_sensor_data() whenever it has polled for new sensor data.

Every 10 seconds after Mynewt has obtained the raw sensor data from the temperature sensor, Mynewt calls handle_sensor_data() in apps/my_sensor_app/src/sensor.c to work on the raw sensor data.

handle_sensor_data() wraps the raw sensor data into a sensor_value. Here we specify that the raw sensor data should be transmitted as an integer with field name t. It then calls send_sensor_data() to transmit the sensor value.

Why do we transmit the raw sensor data as an integer value (like 1715) instead of a floating-point value (like 32.1 degrees Celsius)?

Remember that we are creating an embedded application for a constrained, low-power microcontroller with little ROM and RAM. On constrained devices, it takes a lot of ROM and RAM to convert temperature values from integer to floating-point.

Hence we conserve device resources when we transmit sensor values in their raw, integer forms and let the IoT cloud ( convert the values into floating-point.

Function send_sensor_data()

send_sensor_data() in apps/my_sensor_app/src/network.c is called by handle_sensor_data() to transmit sensor data. In this program we’re transmitting sensor data to the CoAP server at requires our sensor data to be in this JSON format…


By calling the macros CP_ROOT, CP_ARRAY, CP_ITEM_STR and CP_ITEM_INT_VAL, the program composes a JSON message in the above format based on our raw temperature value t.

When do_server_post() is called, the JSON message is encoded as the payload of a CoAP message. The CoAP message is transmitted as a UDP packet to over the NB-IoT network.

What’s device in the JSON message? We’ll find out in a while.

Run The Program

1️⃣ Click Debug → Start Debugging

2️⃣ Click View → Output

Select Adapter Output to see the Blue Pill log

3️⃣ The debugger pauses at the line with LoopCopyDataInit

Click Continue or press F5

4️⃣ The debugger pauses next at the main() function.

Click Continue or press F5

The program should now poll the internal temperature sensor every 10 seconds and transmit to Let’s study the Blue Pill execution log…

Check The Log

The log from our Blue Pill should look like this. When we see this in the log…


It means that the program has sent this AT command to the NB-IoT module…


Followed by Carriage Return CR 0x0d and Line Feed LF 0x0a characters. Then the NB-IoT module responded with…


Since AT+ is present in all AT commands, we won’t show the prefix AT+ in the log. All the AT commands below are explained in my previous article.

When the program starts, it disables NB-IoT module’s auto-connection (NCONFIG=AUTOCONNECT,FALSE) and reboots the NB-IoT module (NRB).

It selects NB-IoT Frequency Band 8 (NBAND=8), enables the NB-IoT radio transceiver (CFUN=1) and starts attaching to the NB-IoT network (CGATT=1)

The NB-IoT Frequency Band depends on your country and your NB-IoT network operator. Check with your NB-IoT network operator for the Frequency Band to use.

The NB-IoT Band is configured here: targets/bluepill_my_sensor/syscfg.yml

The program queries the NB-IoT registration status (CEREG?). The response +CEREG:0,2 means that the NB-IoT module is still registering with the NB-IoT network.

The program continues to query the registration status (CEREG?). In a few seconds, we get the response +CEREG:0,1 which means that the NB-IoT module has registered with the NB-IoT network.

We send CGATT? to check whether we have been attached to the NB-IoT network. The response +CGATT:1 means that we have been successfully attached to the NB-IoT network. We may start transmitting data to the network.

Before transmitting, we ask the NB-IoT module to allocate a local UDP port (NSOCR=DGRAM,17,0,1). The module returns local port 1.

Next the program reads the Blue Pill’s internal temperature sensor (every 10 seconds) and obtains the raw temperature value 1757.

The program composes the CoAP message with JSON payload (described earlier)…

{"values": [
{"key": "device",
"value": "ac913c..."},
{"key": "t",
"value": 1757}

…And transmits the CoAP message via the AT command NSOST=… (not shown in the log).

The hex numbers 58 02 00 01 ... are the bytes of the encoded CoAP message. You may decode the CoAP message with Wireshark as explained in the previous article.

The CoAP message is transmitted by the NB-IoT module to the CoAP server at Notice that the message includes a device ID ac913c... This is a random number that’s transmitted in every CoAP message.

When we Ctrl-Click the URL in the log…

Web page with computed temperature

…We see a web page with the computed temperature value in degrees Celsius. That’s because has converted the raw temperature into the actual temperature (in degrees Celsius). We have installed a script at that pushes the computed temperature to, so that we could see the computed temperature.

The URL (and the random number) changes each time we restart the program. More details about the setup for may be found in the previous article.

What’s Next?

In the next article we’ll create more NB-IoT devices… this time with Visual Rust!

Also we’ll be checking out these exciting NB-IoT developer kits with onboard low-power STM32 microcontrollers and Quectel NB-IoT modules. Stay tuned!

NB-IoT developer kit with onboard Quectel BC35-G Global NB-IoT module and STM32L431RCT6 microcontroller. From


The following Quectel documents were very useful for understanding the AT commands. Download them from (free registration required)

  1. Quectel BC95-G Hardware Design V1.3: Details of the BC95-G pins
  2. Quectel BC95 & BC95-G & BC68 Application Design Guide V1.1: Designing applications for BC68
  3. Quectel BC95-G & BC68 AT Commands Manual V1.4: AT commands
  4. Quectel BC95-G & BC68 CoAP Application Note V1.0: AT commands for CoAP. Unfortunately I was not able to use the AT commands here to transmit the payload correctly (the transmitted payload was always empty). So I decided to encode the CoAP messages myself.

💎 Advanced Topic: Quectel BC95-G Driver for Mynewt

The Mynewt driver I have created for Quectel BC95-G is located here…

driver.cpp contains the main logic for the driver. It sends AT commands and handles responses and timeouts.

ATParser and BufferedSerial libraries (ported from mbed) are called by driver.cpp to parse the AT responses from the Quectel module. The dynamic heap memory allocation in the original mbed version has been replaced by static memory buffers, to reduce RAM and ROM size.

creator.cpp contains the driver creation code required by Mynewt

transport.cpp provides the OIC (Open Interconnect Consortium) network transport required for transmitting CoAP messages via Mynewt’s OIC framework

syscfg.yml defines one configuration setting: NBIOT_BAND, the NB-IoT band (defaults to 8)

pkg.yml instructs Mynewt to call bc95g_create() to initialise the driver during startup

Why do we use mbufs when transmitting sensor data? Like in bc95g_socket_tx_mbuf()?

An mbuf (“memory buffer”) is a chain of memory blocks that’s optimised for transmitting network messages. Recall the structure of our CoAP message from the previous article… Every CoAP message has a Preamble and an Options Header that are usually fixed in length for the session. But the Payload of the message may vary, depending on the sensor data.

Shall we keep reallocating and deallocating the memory blocks for the Preamble, Options and Payload every time we transmit a CoAP message?

No, we may actually reuse the same mbuf to hold the fixed-length Preamble and Options. But depending on the sensor data, we’ll attach one or more mbufs to the chain to hold the entire Payload. This speeds up the composition of CoAP messages.

That’s why in the driver code we see the program walking through each mbuf in the chain and transmitting each mbuf. This keeps the networking code highly efficient, just like early versions of the Unix operating system.