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:
- Copy
tutorials/mqtt/mqtt-client/mongoose_mqtt.cinto the project directory - Add it to the build in the
Makefile:
SOURCES = main.c hal.c mongoose_mqtt.c
- Call
mg_mqtt_init()aftermg_mgr_init(), andmg_mqtt_poll()aftermg_mgr_poll()inmain.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:
- MQTT client tutorial, including the Azure IoT Hub and Azure Event Grid instructions
- STM32 tutorials
- MQTT on a microcontroller, if you want more background on why MQTT works the way it does