How to connect STM32 to Azure IoT Hub

In this article we take an STM32 Nucleo-H723ZG board and connect it to Microsoft Azure IoT Hub over MQTT with mutual TLS. No Azure SDK, no extra TLS library. Just Mongoose and one extra C file copied from the MQTT tutorial.

We'll use the bare-bones nucleo-h723zg/minimal project, a plain Makefile build with CMSIS headers. Everything below works the same way with the nucleo-h723zg/cubemx project, and with any other microcontroller that Mongoose supports.

Step 1: Build and flash the minimal project

Prepare your build environment, clone the Mongoose repo, go to the Nucleo-H723ZG minimal tutorial, and run make flash:

git clone https://github.com/cesanta/mongoose
cd mongoose/tutorials/stm32/nucleo-h723zg/minimal
make flash

The first build downloads the CMSIS headers, then compiles and flashes the firmware. Open a serial monitor and reset the board. You should see the log saying the board got an IP address over DHCP. Open that IP in a browser and you get a greeting page.

If you look at main.c, there isn't much in it: init the Mongoose event manager, start an HTTP listener, then poll in a loop:

struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);

for (;;) {
  mg_mgr_poll(&mgr, 0);
  blink_task();
}

That's our starting point. The nice thing here is that once Mongoose is in your project, every Mongoose tutorial becomes copy-paste material. MQTT, SMTP, Modbus, UDP, Websocket, whatever. That's exactly what we do next.

Step 2: Add the MQTT client

The MQTT client tutorial is a small client that connects to a broker, subscribes to a topic and echoes every received message back to another topic. It handles reconnects and keeps the connection alive with pings. It also does OTA over MQTT, but we won't touch that here.

All the logic lives in one file, mongoose_mqtt.c. To integrate it:

  1. Copy tutorials/mqtt/mqtt-client/mongoose_mqtt.c into the project directory
  2. Add it to the build in the Makefile:
SOURCES = main.c hal.c mongoose_mqtt.c
  1. Call mg_mqtt_init() after mg_mgr_init(), and mg_mqtt_poll() after mg_mgr_poll() in main.c:
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_mqtt_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);

for (;;) {
  mg_mgr_poll(&mgr, 0);
  mg_mqtt_poll(&mgr);
  blink_task();
}

Don't skip step 3. I did in the video, and the firmware happily built and ran without any MQTT at all.

Step 3: Test with a public broker first

Before we switch to Azure, let's check that MQTT itself works. Out of the box mongoose_mqtt.c talks to the public HiveMQ broker:

#define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883"
#define MQTT_CLIENT_ID "d3"
#define MQTT_PUBLISH_TOPIC "mg/" MQTT_CLIENT_ID "/tx"
#define MQTT_SUBSCRIBE_TOPIC "mg/" MQTT_CLIENT_ID "/rx"

Run make flash again. The serial log should show that the client connected and subscribed to mg/d3/rx.

Now open the HiveMQ WebSocket client and click Connect. Subscribe to mg/d3/#, then publish something like hello to mg/d3/rx. The board answers on mg/d3/tx right away.

So the MQTT part is fine. If something breaks later, we know it's the Azure config and not the client.

Step 4: Set up Azure IoT Hub

Now the Azure part. Everything here comes from the "Microsoft Azure IoT Hub" section of the tutorial's README.

Create the hub

In the Azure portal, search for "IoT Hub" and click Create. Pick a globally unique name (I used mg123), a region, and the Free tier, which is plenty for a demo. Click Review + create, then Create, and wait for the deployment to finish.

Generate a device certificate

The device authenticates with an X.509 certificate. A self-signed one is fine here. Generate an EC key and certificate:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -pkeyopt ec_param_enc:named_curve -nodes -keyout device.key -out device.crt -days 3650 -subj "/CN=MYDEVICE"

That leaves you with device.key and device.crt. Now print the certificate thumbprint:

openssl x509 -in device.crt -noout -fingerprint -sha256 | tr -d ':' | cut -d= -f2

Register the device

In the hub, go to Devices and add a new device. Give it an ID (I used d7), choose "X.509 Self-Signed" as the authentication type, and paste the thumbprint into both thumbprint fields. That's how Azure knows to trust this particular self-signed certificate: when the device connects, the hub checks that the certificate fingerprint matches. Click Save.

Point the firmware at Azure

Open mongoose_mqtt.c and replace the HiveMQ defines with this block, setting your hub name and device ID:

#define AZURE_HUB_NAME "mg123"  // Change this
#define AZURE_DEVICE_ID "d7"    // Change this

// Do not change this
#define MQTT_SERVER_URL "mqtts://" AZURE_HUB_NAME ".device.azure-devices.net"
#define MQTT_CLIENT_ID AZURE_DEVICE_ID
#define MQTT_USER AZURE_HUB_NAME ".azure-devices.net/" AZURE_DEVICE_ID "/?api-version=2021-04-12"
#define MQTT_PUBLISH_TOPIC "devices/" AZURE_DEVICE_ID "/messages/events/"
#define MQTT_SUBSCRIBE_TOPIC "devices/" AZURE_DEVICE_ID "/messages/devicebound/#"

Remove the old MQTT_SERVER_URL, MQTT_CLIENT_ID, MQTT_USER and topic defines so they don't clash. Keep MQTT_PASS as an empty string, since the certificate does the authentication, not a password.

Note the mqtts:// scheme. That tells Mongoose to use TLS, and the client then calls mg_tls_init() with the CA, certificate and key on connect. Those are the three things left to fill in.

Set the CA certificate

TLS_CA is the root CA that the IoT Hub server certificate chains to. There are a few ways to get it. The easiest is the Mongoose TLS helper page: enter mg123.device.azure-devices.net:8883 (with your hub name), click "Get CA Certificate", tick "Show as C/C++ constant", and paste the result as TLS_CA.

Set the device key and certificate

TLS_KEY and TLS_CRT are the files we just generated, turned into C string constants. This sed one-liner does the conversion:

sed 's/\r$//; s/.*/  "&\\n"/; $!s/$/ \\/' device.key
sed 's/\r$//; s/.*/  "&\\n"/; $!s/$/ \\/' device.crt

Paste the outputs into TLS_KEY and TLS_CRT.

Also add this to mongoose_config.h, as the README says:

#define MG_ENABLE_CHACHA20 0

Run make flash. After the TLS handshake (give it a few seconds) the log should show a successful connection and a subscription to the devices/d7/messages/devicebound/# topic. The board is now on Azure.

Step 5: Talk to the device from Azure

Azure IoT Hub has two ways of sending something to a device.

Cloud-to-device messages

This is the fire-and-forget option, like a notification. Azure publishes the message to the devicebound topic we subscribed to. In the portal, open your device, click "Message to device", type hello and hit Send. The board gets it, and since our client echoes everything, it publishes a reply to devices/d7/messages/events/.

Direct methods

This one is request/response. Azure publishes the call to a special topic, $iothub/methods/POST/{method name}/?$rid={request id}, and waits for the device to reply on $iothub/methods/res/{status}/?$rid={request id}.

mongoose_mqtt.c already handles this. When it detects an Azure URL, it subscribes to $iothub/methods/POST/# too, and the message handler has a catch-all that answers every method with status 200 and an empty JSON object:

struct mg_str caps[5];  // caps[0] = method name, caps[2] = request id
if (mg_match(mm->topic, mg_str("$iothub/methods/POST/*/?$rid=*"), caps)) {
  // Azure direct method call. Construct a stub response, "{}"
  char topic[128];
  mg_snprintf(topic, sizeof(topic), "$iothub/methods/res/%d/?$rid=%.*s",
              200, (int) caps[2].len, caps[2].buf);
  publish(c, mg_str(topic), mg_str("{}"));
}

To try it, click "Direct method" on the device page in the portal. Enter a method name, say mymethod, and a JSON payload like {"hi": 42}, then click Invoke. You get back status 200 with payload {}, which is exactly what the code sends.

Obviously, a real device would look at caps[0] to dispatch on the method name, parse the payload with mg_json_get_*(), and send back something more useful than {}. For example, this is the code that implements PinRead function. Set the payload to {"pin": 42}, method name to "PinRead", and send a direct method call - you'll get the pin state returned back:

struct mg_str caps[5];  // caps[0] = method name, caps[2] = request id
if (mg_match(mm->topic, mg_str("$iothub/methods/POST/*/?$rid=*"), caps)) {
  char topic[128], response[100] = "{}";
  int code = 500;  // Failure by default
  if (mg_strcmp(caps[0], mg_str("PinRead")) == 0) {
    int pin = (int) mg_json_get_long(mm->data, "$.pin", -1);
    int val = hal_gpio_read(pin);
    mg_snprintf(response, sizeof(response), "{%m:%d}", MG_ESC("val"), val);
  }
  mg_snprintf(topic, sizeof(topic), "$iothub/methods/res/%d/?$rid=%.*s",
              200, (int) caps[2].len, caps[2].buf);
  publish(c, mg_str(topic), mg_str(response));
}

Wrapping up

That's the whole thing: an STM32 talking to Azure IoT Hub over MQTT with mutual TLS, receiving cloud-to-device messages and answering direct method calls. The changes to the original project were one copied C file, two function calls, and a handful of defines.

Links: