How to connect STM32 to EMQX

We take an STM32 Nucleo-H723ZG, add an MQTT client to it, and connect it to an EMQX Cloud broker over TLS with username/password authentication and per-device topic access. No RTOS, no lwIP, no mbedTLS. Mongoose does the TCP/IP, TLS and MQTT parts.

The project we start from is nucleo-h723zg/minimal, a plain Makefile build with CMSIS headers. The steps are the same for the nucleo-h723zg/cubemx project, and for any other board Mongoose supports.

The setup

The board has two cables going to my workstation. An Ethernet cable goes into a USB-Ethernet dongle, and a USB cable goes to the on-board ST-Link, which I use for flashing and for the serial console.

On the workstation I turned on internet sharing from Wi-Fi to that dongle. That makes the workstation run a DHCP server on the dongle interface, so the board gets an IP address when it boots and can reach the internet through my Wi-Fi. A plain cable to your office router works just as well, of course.

Step 1: Build and flash the minimal project

Prepare your build environment, then clone the repo and flash the minimal tutorial:

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

Start your serial monitor before running make flash, so you catch the boot log. The build fetches the CMSIS headers, compiles the firmware and flashes it with STM32CubeProgrammer CLI. In the log you should see Mongoose start and get an IP address over DHCP. Open that address in a browser and you get a greeting page. The LED blinks too.

main.c is short. It sets up the hardware, creates a Mongoose event manager, starts an HTTP server, and then runs two things in a loop: the network and the blinky:

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();
}

Once Mongoose is in the firmware you get the rest of it for free: OTA, HTTP, MQTT, SMTP, Modbus TCP and so on. Adding any of these is mostly copying code from an existing tutorial. We need MQTT, so that's what we copy.

Step 2: Add the MQTT client

The MQTT client tutorial keeps all its logic in a single file, mongoose_mqtt.c. Its README says you need to copy the file and add two calls. So:

  1. Copy tutorials/mqtt/mqtt-client/mongoose_mqtt.c into the project directory
  2. Add it to the Makefile:
SOURCES = main.c hal.c mongoose_mqtt.c
  1. In main.c, call mg_mqtt_init() after mg_mgr_init(), and mg_mqtt_poll() in the main loop:
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();
}

Step 3: Check it against HiveMQ

What the client does is simple. It connects to the broker, subscribes to one topic, and echoes every message it gets there back to another topic. Out of the box it points at the public HiveMQ broker:

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

The client ID d3 works as a device ID here, and it's part of both topic names. Run make flash and watch the log. The board connects to HiveMQ and subscribes to mg/d3/rx.

Open the HiveMQ WebSocket client, click Connect and subscribe to mg/# so you see everything. Publish hello to mg/d3/rx. The board receives it and sends the response to mg/d3/tx.

Good. MQTT works, so from here on anything that breaks is the EMQX config, not the client.

Step 4: Create an EMQX deployment

Go to EMQX Cloud and sign in. I used my GitHub account. You'll see a few deployment types. The default is Dedicated Flex, which runs on AWS, Google Cloud, Azure or Oracle. For a demo the Serverless plan is enough, and it has a free tier. Pick it and click Deploy.

Open the deployment. The overview page has the MQTT connection info, including the broker address. Copy it.

Step 5: Point the firmware at EMQX

In mongoose_mqtt.c, change the server URL and client ID:

#define MQTT_SERVER_URL "mqtts://YOUR_DEPLOYMENT_ADDRESS:8883"
#define MQTT_CLIENT_ID "d1"

Note mqtts:// and port 8883. That's MQTT over TLS, and the scheme is what tells Mongoose to do the TLS handshake. MQTT_USER is defined as MQTT_CLIENT_ID, so the username becomes d1 automatically. Leave the topics as they are, they now turn into mg/d1/rx and mg/d1/tx.

The CA certificate

The client needs the CA certificate to verify the server, and it expects it as a C string in TLS_CA. EMQX lets you download it as a PEM file from the deployment page. You can convert that by hand, but there's an easier way: the Mongoose TLS helper. Enter your deployment address with port 8883, click "Get CA Certificate", and it fetches the CA from the server. I compared it with the downloaded file and it was the same certificate. Tick "Show as C/C++ constant" and it does all the quoting for you. Paste the result into TLS_CA.

Step 6: Access control in EMQX

EMQX splits access control into two parts: authentication and authorization.

Authentication is the MQTT username and password. That's part of the MQTT protocol itself, every broker has it. In the EMQX console, add a user with username d1 and password d1. Then set the password in mongoose_mqtt.c:

#define MQTT_PASS "d1"

Authorization is not part of MQTT. Each cloud vendor does it their own way, but the idea is always the same: you attach rules to a client ID that say which topics it may publish and subscribe to. In EMQX, add an authorization rule for client ID d1, topic mg/d1/#, action publish and subscribe, permission allow.

Now d1 can only touch topics under mg/d1/. Use one ID per device and every device gets its own little topic tree.

Step 7: Test it

Run make flash again. The log should show a successful connection to EMQX and a subscription to mg/d1/rx.

To send something to the board, use the MQTT client built into the EMQX console. In my deployment it was under Diagnostics. Connect it using the credentials you just added, subscribe to mg/#, and publish anything to mg/d1/rx.

The board gets the message and publishes the response, and the console client shows both: the message we sent and the echo coming back on mg/d1/tx. Done, the STM32 is talking to EMQX.

What's in the firmware

Notice what's not in this project. No Cube framework, no RTOS, no separate TCP/IP stack like lwIP, no TLS library. It's CMSIS headers, a small HAL for clocks and pins, and Mongoose. The firmware is bare metal, and the same code runs unchanged under an RTOS like FreeRTOS or ThreadX.

On the EMQX side there's a lot more you can do with the data once it arrives. For example, you can create a time series table and add a rule that stores everything the device publishes into it, then read that data back through EMQX APIs, including plain HTTP. That's a topic for another article.

Links: