M5Stack produce a suite of pilot-suitable modular IoT devices, including the Atom DTU NB-IoT. The NB-IoT DTU (Narrow Band Internet of Things - data transmission unit) comes in a small 64 * 24 * 29mm case with a DIN rail clip on mounting and support for RS-485 including 9-24V power (or USB-C power).

The kit base has a SIM7020G modem and the ESP32-based Atom Lite (which also supports WiFi) is included with a very resonable price. The device has built in MQTT, supports secure public certificate TLS connections, and supports IPv6.

While the physical unit is ready for pilot deployment (and the M5Stack website has several commerical deployment case studies), there is no pre-written firmware for the device, so some up front development is needed.

As well as reviewing the strengths and weaknesses of the device, I will also provide some sample code for a proof-of-concept using an Env III environment sensor to transmit temperature, humidity, and air pressure to an MQTT test server using MQTTS (with server certificates), over IPv6, over NB-IoT.

M5Stack Atom DTU NB-IoT with Telstra SIM card



The Atom DTU NB-IoT is packaged ready for deployment, with a clip on DIN rail mounting and support for RS-485 including 9-24V power. Alternatively it can be powered by USB-C, and there is an optional TailBat accessory for a 190mAh battery.

Although modular and designed for prototyping, the M5Stack components are not bare bones exposed circuit boards and so suitable as is for pilot deployment to actual working sites, or even a full scale roll out.

Global NB-IoT support

Narrow Band Internet of Things (NB-IoT) is a Low-Power Wide-Area Network (LPWAN) technology that operate on the telecommunications carrier network.

NB-IoT coverage is significally greater than 4G and 5G mobile coverage, which means devices can operate in many remote areas, and even penetrate underground.

Telstra NB-IoT coverage

The Atom NB-IoT DTU uses the SIM7020G modem, which supports global NB-IoT bands, including Telstra. The modem has built in clients for MQTT (including Azure IoT) and LwM2M, including certificate based security.

IPv6 support

Both the Atom Lite and the SIM7020G modem support IPv6, including over NB-IoT, and including via the built in clients (e.g. MQTT). Used in conjunction with an IPv6 test MQTT server you can deploy an IPv6 end-to-end solution.

Internet traffic is currently getting over 40% IPv6 on the Google statistics, which means it is important to ensure (and test) your solution supports IPv6. Within a few years IPv4 will be in the minority.

An increasing number of networks are moving to IPv6 single stack only, for example Telstra's consumer mobile network is now IPv6-only. Without the users even noticing! Want to check? If you are on Telstra mobile (make sure you are not on WiFi) go somewhere like https://whatismyv6.com/.

The Telstra Wireless Application Development Guidelines section 7.5 requires that all new IOT/M2M devices and new end-to-end applications support IPv6 natively, and that all systems will be configured as either dual stack or single stack IPv6. Also, all large scale deployments are expected to be single stack IPv6, and IPv6 support is a requirement if you want your devices to be Telstra certified.

Secure connections

The SIM7020G supports public server certificate authentication, including for HTTPS and MQTTS, allowing communication with servers to be secure. It also supports client certificates (although this is not yet implemented in the MQTT access library).

Modular expansion

The M5Stack family includes many expansion modules, with the Atom Lite unit including a Grove expansion ports for I2C, I/O, or UART connections, and the NB-IoT DTU base adding a second Grove I2C connection and an RS-485 connection.

There are many Grove based sensors and expansions available, including the Env III environment sensor which is used in the proof-of-concept below.

Built-in Wi-Fi

The ESP32 has built-in WiFi support (in addition to the NB-IoT DTU), allowing for an easy field commissioning system to be implemented if needed.

Pathway to production

The M5Stack family is based on the popular ESP32 chip, as well as standard protocols and expansion modules, allowing an easy path from a modular pilot production to mass production if needed.

Unboxing the M5Stack Atom DTU NB-IoT Kit


Firmware programming required

The M5Stack family does not come with pre-packaged firmware, so will requiring programming to develop a suitable sensor application based on the specific devices, communication protocols, and sensors being used.

Over-the-air upgrade programming required

This includes not only the NB-IoT connection itself and the management of sensors and telemetry, but also any over-the air configuration, development of any field commissioning system (using the WiFi), and development of any over-the-air firmware upgrades.

Development of such a system is not a trivial exercise, although a pilot program may get away with a less robust implementation and skip some features (e.g. firmware upgrades may not be needed for a pilot).

Enclosure is not weather or dust resistent

While the enclosure is not exposed, and M5Stack has deployed it into factories, there is no water or dust protection, for example the SIM slot is exposed.

Note that there is an alternate M5Tough product, that includes a resistent enclosure.

Once a pilot project has been proven, a production variation could also be developed using the base components and deployed in a custom enclosure.

No physical security

The device does not have any anti-tamper mechanisms or physical security, with the serial debugging port easily accessible. This should be acceptable for a pilot program, although a production system could use a custom enclosure if needed with tamper detection wired up to one of the other I/O ports. Tamper monitoring would also need to be programmed into the firmware.

No built in battery

Neither the Atom or the DTU NB-IoT has a built in battery. There is a TailBat exapansion available, however it is only a small capacity battery, so the current unit requires a power source for deployment.

For a production solution you could design a custom enclosure and battery solution, and both the chip and modem support deep sleep, although you would also need to write the firmware to control this.

Testing the Atom DTU NB-IoT

Device setup

The test setup includes:

  • Atom DTU NB-IoT Kit, which includes the NB-IoT base and an Atom Lite
  • Env III environment sensor, via the Grove connector
  • Atom TailBat, for testing the unit without the USB-C connector

For programming, the USB-C connector is used to download the firmware. The serial connection can also be used to monitor the device logs; the LED is also used as a status indicator (useful for when it is running on battery)

M5Stack Atom DTU NB-IoT with Env III environment sensor and TailBat battery

SIM card

You will need an NB-IoT SIM card from your telecommunications carrier to use in the device.

For example, in Australia you can use one of the IoT Data SIM plans from Telstra

The DTU has a SIM card slot on the side, so you can just insert a Micro SIM card directly. The slot is spring loaded, so push again if you need to remove the SIM.

Programming setup

For programming I am using:

  • Visual Studio Code
  • With the PlatformIO extension installed

This is a cross platform environment and a bit more flexible than the Arduino IDE although there are one or two extra steps -- you need to list the libraries you are using in the platformio.ini file (which also means that different projects can have different libraries), and need to explicitly add #include <Arduino.h> in the program file.

Visual Studio Code has built in support for Git, so it is easy to version control your project.

MQTT test server

To test, you can follow the instructions in a previous article to set up Mosquitto server running in Azure. This will set up a server, with TLS support (MQTTS) via a Let's Encrypt certificate, and full IPv6.

Once set up you can run an MQTT subscriber in a console, to listen for messages from the device:

$mqttPassword = 'YourSecurePassword'
mosquitto_sub -h mqdev01-0xacc5.australiaeast.cloudapp.azure.com -t '#' -F '%I %t [%l] %p' -v -p 8883 -u mqttuser -P $mqttPassword

You can also SSH into the server and follow the logs to see the incoming connections.

Sample code

Full sample is available on Github at https://github.com/sgryphon/iot-demo-build/tree/main/m5stack/m5atom_mqtt_sensor

Some of the coding sections are highlighted in this article, and if you want to test yourself, just download the full code.


You will need to change the configuration for the telecommunications carrier you are using and the host name of your MQTT test server.

const char apn[] = "telstra.iot";
const char server[] = "mqdev01-0xacc5.australiaeast.cloudapp.azure.com";

If you are using a different test server setup, you may also need to change the certficate and device name or other details. Note that the SIM7020G MQTT client only supports a maximum server name length of 50 characters, so you may need to check the length.

Running the sample code

To run the code, use the PlatformIO console to compile (run) and upload your code (with the Atom device connected via USB).

First set an environment variable with the MQTT password (you don't want this checked in to your source code repository). The command line also sets the version automatically based on the Git repository. It is important to have some kind of versioning (preferably automatic) to keep track of devices, available in any debug output and in the device registration properties.

export PIO_MQTT_PASSWORD=YourMqttPassword3
(export PIO_VERSION=$(git describe --tags --dirty); pio run --target upload)

Once it is uploaded you can monitor the debug output via the serial connection.

pio device monitor --baud 115200

LED status light

The Atom Lite has an RGB LED, which is used to indicate the application status:

  • Yellow: initial start up
  • Green: successful modem initialisation and connection to the NB-IoT network
  • Blue: transmitting, or receiving, data
  • Red: if there is a critical error a failure flag is set and the LED is turned red.

This allows a basic check of whether the device is working on not, even without serial monitoring (e.g. if on battery).


When the code is run you can see the serial debug output (on the left, in VS Code), publishing telemetry messages.

On the top right you can see the logs from the test MQTT server, including the connection from the Mosquitto observer (2407:8800:...cb49) logging in as 'mqttuser', and a connection from the NB-IoT device (2001:8004:...c38f) as dev00001 every 60 seconds. The server is only accessible via TLS, and you can see all connections are on port 8883, and using IPv6 only.

The bottom right shows the Mosquitto subscriber and the messages published from the device. The first message, on start up, sends device registration details including the manufacturer, model, version, IMEI, and IP Address. Subsequent messages every 60 seconds contain the telemetry with temperature, humidity, and air pressure.

Atom sending telemetry over MQTT - TLS - IPv6 - NB-IoT

Coding details

Security certificates

The SIM7020G MQTT client supports full Transport Layer Security (TLS) through public certificates, however resource constrainted IoT devices don't usually include the root certificate authorities, which means you need to load the certificates you need as part of the firmware.

For our test MQTT server we are using Let's Encrypt, so we need to load the root public certificate for the Internet Security Research Group (used by Let's Encrypt). We are including it as a file, set in platformio.ini:

board_build.embed_txtfiles =

The setup code then creates variables to reference the embedded certificate:

extern const uint8_t
    root_ca_pem_start[] asm("_binary_src_certs_ISRG_Root_X1_pem_start");
extern const uint8_t
    root_ca_pem_end[] asm("_binary_src_certs_ISRG_Root_X1_pem_end");
const String root_ca((char *)root_ca_pem_start);

Which we configure the modem with, once it has started up. The modem itself handles the TLS connection.

bool ca_success = modem.setRootCA(root_ca);
if (!ca_success) {
  Serial.println("Certificate failed");
  failed = true;

Note that if something goes wrong we write a debug message, set the failed flag to true, and then set the LED to red.

The proof-of-concept code doesn't have any error handling beyond that, although if there is a problem setting the certificate the device may be no longer functional without manual update of the firmware, so clearly indicating this (the red LED) is important.

Password handling

Passwords should not be stored in source control, although they will necessarily be stored on the device itself. For this proof-of-concept we are compiling the MQTT password into the firmware, although a full rollout may use the built in WiFi capabilities for field provisioning to set the password (or to set bootstrap credentials, which are then used to set the operational password over the air).

The configuration settings (password and the version) are passed in using environment variables, which are mapped in the platformio.ini file to corresponding build flags.

build_flags = 

The build flags are then formatted as strings and stored in variables for use in the application.

#define ST(A) #A
#define STR(A) ST(A)
const char mqtt_password[] = STR(PIO_MQTT_PASSWORD);
const char version[] = STR(PIO_VERSION);

Manual trigger

Device registration properties are sent on start up, and the button on the M5 Atom is also configured so that if it is pressed the next message routine is triggered immediately, and the properties are included again. This type of manual trigger for the send routine, which includes checking for incoming commands, is useful for testing, especially if you normally have a long send interval.

It is particularly useful if you have commands you can send that update settings (such as the send interval) and want them applied immediately. (The proof-of-concept code doesn't have any settings update code).

if (M5.Btn.wasPressed()) {
  send_properties = true;
  next_message_ms = 1;

Note that the Arduino-based device code has a single loop, wihtout asynchronous events, so rather than an event handler there is a function to check if the button has been pressed (since the last time it was checked) that is run as part of the main loop.

MQTT topics

The MQTT topics are hard coded in the proof-of-concept code. They are using a format based on the AWS IoT guidelines.

const char publish_topic[] = "dt/demo/m5/dev00001/senml";
const char subscribe_topic[] = "cmd/demo/m5/dev00001/#";

The dt prefix is for data messages, and the cmd for command messages. There are application (set to demo) and context indicators, and then the device name is included in the topic. This is the username that is used to authenticate to the MQTT server, and the server is configured so that devices can only publish to data topics for their username (and subscribe and publish to their command topic).

This is based on the least privileges security principle, so that if a device's credentials are compromised they can't publish to the data topics of other devices, or subscribe to the commands sent to other devices. If a message is received on the dev00001 topic we know which device it was sent from.

MQTT messages

Telemetry is sent with a hard coded template, using the RFC 8428 SenML (Sensor Measurement Lists) format.

An example telemetry message, formatted for readability:


The base name (bn) field is prefixed to subsequent name (n) fields, and the time is unspecified (the Atom Lite does not have a real time clock) so the time the message is received is used, leading to resolved records like {"n":"dev00001_temperature","t":1663654738,"u":"Cel","v":28.13}, which uniquely identifies the device, the specific measurement, the time, and the value.

SenML is a good choice for an arbitrary independent format as it is self-describing and includes the relevant metadata such as the units used for measurements. For example signal strength of -111 could be either very poor (if it was dBm) or very good (if it was dBW), so it is important to know which is being used. The SenML standard units are dBW, although RFC 8798 does allow dBm as a secondary units option.

Units can be based on external metadata, such as the well defined units in a standard such as LwM2M (Lightweight Machine to Machine) or agreed in a descriptive document such as an Azure IoT DTDL device twin specification, but having them explicitly part of the record makes it self-describing when it is used out of context.

The registration properties message uses the same standard format, although in this case all values are strings (vs):

  {"n":"model","vs":"Atom Lite; Iot Demo"},

Usually the device name (i.e. username), and therefore topic, would be based on an identifier such as the serial number imei861518040875915, so that it can be generated on the device and registered with the host using that known name. For the proof-of-concept however I am using a pre-created device identifiers on the MQTT test server dev00001.


The SIM7020G library includes a built in MQTT client, although other devices may need to use a third party MQTT library if they only provide a TLS connection; or even third party MQTT and TLS if all they provide is a TCP socket.

The modem is configured with the secondary serial port (Serial1), then the SIM7020 TCP client is initialised with the modem. The SIM7020 MQTT client is then initialised with the client (to get the modem details), server, port, and with TLS enabled. (The certificate is set later.)

References to the device specific objects are assigned for general modem and general MQTT client capabilities. This common interface allows the specific devices to be changed (or even replace the hardware MQTT client with a software one).

During setup() we start the serial port, with the specific hardware pins for the Atom Lite, and then start the modem connection to our telecommunications provider APN (access point name).

SIM7020GsmModem sim7020(Serial1);
SIM7020TcpClient sim7020tcp(sim7020);
SIM7020MqttClient sim7020mqtt(sim7020tcp, server, port, true);
GsmModem &modem = sim7020;
MqttClient &mqtt = sim7020mqtt;

void setup() {
  Serial1.begin(115200, SERIAL_8N1, 22, 19);

The main loop() calls modem.loop() to process the connection steps, and later any incoming messages. Telemetry processing is not executed until the modem is ready (determined by checking modem.modemStatus()), with some additional setup (such as the TLS server certificate) configured

void loop() {
  if (!ready) {
    if (modem.modemStatus() >= ModemStatus::PacketDataReady) {
      ready = true;
      bool ca_success = modem.setRootCA(root_ca);      

Once the modem is ready we configure an interval to send telemetry (the proof-of-concept is 60 seconds), and then it is some simple API calls to connect, subscribe (for any incoming messages), and then publish the telemetry.

  if (next_message_ms > 0 && millis() > next_message_ms) {
    int8_t rc = mqtt.connect(mqtt_user, mqtt_user, mqtt_password);
    mqtt.publish(publish_topic, message_buffer);

The code also sets a timeout (5 seconds in the PoC) after which to disconnect. The loop continues to run during this time, so that incoming messages can be processed. The code also sets and then clears the LED to indicate the system activity

Cloud to device messages

Similar to the button press handling, there are no event handlers for incoming messages, with the modem.loop() reading the message and setting a variable with topic of any received message. Like the button, the variable is cleared when read, i.e. it works like an incoming queue.

The sample code simply outputs any received message to the debug log, and also sets the LED to flash blue to indicate the data was received.

  String receive_topic = mqtt.receiveTopic();
  if (receive_topic.length() > 0) {
    String receive_body = mqtt.receiveBody();
    Serial.printf("Received [%s]: %s\n", receive_topic.c_str(),
    led_off_ms = now + 200;

To send a downstream message, wait until the device connects (as there is no retained messages configured), and then use Mosquitto to publish a message to the command topic for the device:

$mqttPassword = 'YourSecretPassword'
mosquitto_pub -h mqdev01-0xacc5.australiaeast.cloudapp.azure.com -t 'cmd/demo/m5/dev00001/update/senml' -p 8883 -u mqttuser -P $mqttPassword -m '[{\"n\":\"dev00001_interval\",\"v\":60}]'

SIM7020G modem testing

During prototyping I also tested the SIM7020G modem directly using AT commands over a serial connection. To set this up I removed the Atom Lite module (the small 25 x 25mm module at the top of the picture) and connected the modem and power pins to a small breadboard with a USB power supply and a USB to serial converter.

I was able to power up the modem and directly send AT commands to test out the functionality, following the SIM7020G documentation.

M5Stack Atom DTU NB-IoT with Telstra SIM card


M5Stack devices include finised enclosures, and the modular components can be connected without any soldering. The Atom DTU has some nice features like the DIN rail mounting that make it easy to deploy.

This makes the devices and modules good for prototyping all the way through to a full pilot program, or even a possible production deployment. While the Atom DTU NB-IoT is not suitable for remote outdoors deployment, there are still many scenarios where LPWAN features are useful.

Some examples:

  • Indoors environmental monitoring such as temperature sensitive products where you want to maintain records even in the event of a power outage. For example if you have hardwired sensors in a cold storage room and power goes out you will also lose the sensor readings and may have to write the produce off. However with battery powered NB-IoT devices you can continue monitoring even when physical connections are down.
  • Remote building monitoring. While not suitable for outdoors, you could monitor remote buildings or structures that are already weather protected. For example a pumping station that may have power but be too remote for effective WiFi.
  • Mobile equipment monitoring, such as generators, display signs, rack equipment, or utility trailers -- these will often have power readily available, but need mobile connectivity. For example monitoring of mobile generators.

Devices are not expensive with the kit, including the NB-IoT connection and the Atom Lite, costing around AUD 55.00. The Env III sensor is only AUD 10.00, and the battery (if needed) AUD 20.00.

The hardware has full support for secure connections (certificates) and modern IPv6 connections. The built in support for MQTT in the SIM7020G makes connections easy to program.

The main drawback of the M5Stack system is the need to program the firmware, unlike an off-the-shelf sensor system. This is not a small undertaking and writing the firmware could well outweigh the costs of the hardware itself unless you are planning a very large deployment.

I expect to be using the M5Stack for a lot of prototype and pilot projects exploring specific platforms such as Azure IoT, AWS IoT, and LwM2m, including Azure Digital Twins, for future articles.