Chapter 2 — Device, Firmware & LoRaWAN Deep Dive
Hardware Platform — Wio-E5 mini Development Board
For this project we selected the Seeed Studio Wio-E5 mini Development Board, built around the STM32WLE5JC System-on-Chip from STMicroelectronics. Unlike traditional LoRa solutions that pair a separate microcontroller with a discrete radio transceiver, the STM32WLE5 integrates both into a single chip — reducing PCB complexity, lowering power consumption, and simplifying firmware development.
The board is well suited to battery-powered industrial IoT deployments: it combines an ARM Cortex-M4 processor with an integrated Semtech LoRa radio, while exposing standard interfaces — UART, SPI, I²C, ADC, PWM, and GPIO — for connecting sensors and peripherals.
Why We Chose This Board
Rather than a generic Arduino-compatible LoRa module, we wanted a platform that could carry a production firmware architecture from prototype through deployment. Our primary requirements were:
- Native LoRaWAN support
- A real-time operating system (FreeRTOS)
- Very low power consumption
- The mature STM32 ecosystem
- An industrial-grade SDK
- Secure OTAA authentication
- Long battery life
- A clean migration path from prototype to production hardware
The Wio-E5 mini satisfied every one of these while remaining compact and cost-effective.
STM32WLE5JC Architecture
At the heart of the board is the STM32WLE5JC, a wireless microcontroller that fuses an ARM Cortex-M4 and a LoRa radio into one System-on-Chip. The Cortex-M4 runs the application firmware and FreeRTOS and manages sensors and peripherals, while the integrated radio handles LoRaWAN communication. Alongside the CPU and radio, the chip provides on-chip Flash and SRAM, multiple communication interfaces (UART, SPI, I²C), an ADC, timers, DMA, and a rich set of low-power modes — everything needed for a complete sensor node.
Traditional LoRa designs connect a separate MCU to an external transceiver over SPI. The STM32WLE5JC eliminates that link entirely: with the radio on-die, there is no external bus to manage, which cuts hardware complexity, PCB footprint, and software overhead while improving performance and lowering power. Developers work directly with the STM32CubeWL LoRaWAN middleware instead of driving a separate radio device.
The result is a highly integrated platform ideal for production IoT: fewer components to fail, lower energy draw, and first-class STM32 tooling — enabling compact, reliable, battery-powered LoRaWAN devices that operate securely in the field for years with minimal maintenance.
Key Hardware Features
FeatureDescriptionMCUSTM32WLE5JCCPUARM Cortex-M4RadioIntegrated Semtech LoRa RFUSBUSB Type-CProgrammingST-LINK (SWD)LoRaWANClass A capableInterfacesUART, SPI, I²C, ADC, PWM, GPIOClock32 MHz TCXO + 32.768 kHz LSEPowerOptimized for ultra-low-power applications
The dual-clock design matters for IoT devices: the high-frequency TCXO delivers the frequency accuracy the radio needs during transmission, while the low-frequency 32.768 kHz crystal drives precise RTC timekeeping during deep sleep.
Development Environment
For firmware development we used STM32CubeIDE. Rather than building a FreeRTOS project from scratch, we leaned on the official Wio-E5 SDK and example applications from Seeed Studio, which include ready-to-run projects for LoRaWAN End Node, FreeRTOS LoRaWAN, FreeRTOS Low Power, and FreeRTOS LoRaWAN with AT commands — a strong base for production firmware.
Our workflow was straightforward:
- Import the provided FreeRTOS LoRaWAN project into STM32CubeIDE.
- Configure LoRaWAN parameters — DevEUI, JoinEUI, AppKey, and the regional frequency plan.
- Compile and debug over ST-LINK, connected through the board’s SWD interface.
Because the SDK already bundles the STM32WL drivers, LoRaWAN middleware, and FreeRTOS, there is no manual RTOS integration to do.
⚠️ One-way step: Before flashing custom firmware, the factory AT-command firmware must be erased with STM32CubeProgrammer by removing the device’s Read Protection (RDP). This permanently erases the preloaded AT firmware, so only perform it when you intend to replace the factory application with your own.
Reference: Seed Wio-E5 mini wiki
Setting Up FreeRTOS
A major reason we chose STM32 is its first-class FreeRTOS integration. Instead of hand-porting the kernel — the scheduler startup, the SysTick/PendSV plumbing, the interrupt configuration — STM32CubeMX generates the entire RTOS project structure for you.
The typical setup flow:
- Create a new STM32WL project in STM32CubeIDE.
- Open the
.iocfile in STM32CubeMX (built in). - Configure the system clock and required peripherals (UART, GPIO, RTC, LPTIM, etc.).
- Enable Middleware → FreeRTOS.
- Choose the API layer: CMSIS-RTOS v2 for new work (native
osThreadNew, message queues, event flags), or the native FreeRTOS API if you preferxTaskCreatedirectly. - Configure the heap scheme, heap size, per-task stack sizes, and task priorities.
- Generate the code.
CubeMX emits the initialization code, osKernelStart(), the interrupt vector mapping (SVC_Handler, PendSV_Handler), and the RTOS hook stubs — so you write application logic, not RTOS glue.
Heads-up on the API version. CMSIS-RTOS v2 is the current recommendation, but the official Seeed Wio-E5 example ships with CMSIS-RTOS v1 (cmsis_os.h, osThreadCreate, osThreadDef). If you import their project directly, that's the API you'll see. It works fine — but v1 is deprecated upstream, worth knowing before you copy its patterns into new code.
One STM32-specific gotcha. Any interrupt whose ISR calls a FreeRTOS ...FromISR() API must have a numerical priority no higher than configMAX_SYSCALL_INTERRUPT_PRIORITY (5 in the Seeed config). On Cortex-M, lower number = higher priority, so this trips up almost everyone at least once. Get it wrong and you'll see a hard fault or a silently corrupted queue — not a clean error.
FreeRTOS Project Structure
For our sensor node we split responsibilities into dedicated tasks that communicate through queues and events rather than shared globals:
Note: this is our target architecture, not the stock example. The official Seeed FreeRTOS example is deliberately minimal — two tasks (a LoRaWAN task and an payload example). We use that as the foundation and grow it into the structure above.
This design gives us:
- Non-blocking sensor acquisition — a slow I²C read never stalls the radio.
- Reliable radio communication — the LoRaWAN stack runs in its own timing context.
- Easy sensor expansion — a new sensor is a new task and a queue producer, not a superloop rewrite.
- Clear separation of responsibilities.
- Deterministic scheduling — priorities decide what runs, not code ordering.
Tasks talk; they don’t share. The safe primitives here are FreeRTOS queues (pass sensor readings by value), semaphores/mutexes (guard the shared SPI bus to the radio), and event flags (signal join complete or downlink ready). Passing data by value through a queue sidesteps most of the race conditions that plague superloop designs.
Inside the Official FreeRTOS Example
A real advantage of the Wio-E5 ecosystem is that Seeed ships an official FreeRTOS LoRaWAN example built on the STM32CubeWL SDK, in the LoRaWan-E5-Node repository. It's actually three projects:
Get Eddie turner’s stories in your inbox
Join Medium for free to get updates from this writer.
ProjectWhat it demonstratesFreeRTOS_LoRaWANOTAA join + periodic uplink under FreeRTOSFreeRTOS_LoRaWAN_ATThe same, exposed through an AT-command interfaceFreeRTOS_LowPowerTickless-idle / low-power operation
Under the hood, the RTOS layer is:
- Kernel: FreeRTOS V10.2.x (ST’s STM32Cube distribution)
- Memory:
heap_4scheme (best-fit with coalescing), 8 KB heap, 1 kHz tick - Allocation: static and dynamic enabled — the idle task uses statically allocated memory, so the node can’t fail to start on a fragmented heap
- Tasks: the LoRaWAN task runs
MX_LoRaWAN_Init()once, then loopsMX_LoRaWAN_Process()behind a shortosDelay(); a second task blinks an LED
Together they show task creation, LoRaWAN stack initialization, OTAA join, uplink transmission, and low-power operation. You can import them straight into STM32CubeIDE — a far better starting point for production firmware than a blank project.
How the low-power part actually works (the piece most write-ups skip): the config sets configUSE_TICKLESS_IDLE = 1. When every task is blocked, FreeRTOS stops the SysTick and calls two hooks — PreSleepProcessing() arms LPTIM1 as a wake timer for the expected idle duration and drops the MCU into sleep via WFI; PostSleepProcessing() stops LPTIM1 and restores the clock tree on wake. That's the mechanism that turns a 1 kHz-ticking RTOS into something a coin cell can run for months.
A subtle trap in the low-power demo: the LED task mixes HAL_Delay() — a blocking SysTick busy-wait — with osDelay(). HAL_Delay doesn't yield to the scheduler, so for its duration the CPU can't enter tickless sleep, partly undercutting the very low-power behavior the demo exists to show. In your own tasks, use osDelay() / vTaskDelay() everywhere so the idle task can actually sleep.
Why We Used FreeRTOS
Many LoRaWAN examples run everything in a single while(1) superloop. That's fine for a demo, but it degrades quickly as the application grows — timing becomes emergent, and any blocking call stalls everything behind it.
A real deployment has to juggle:
- Multiple sensors
- LoRaWAN communication and its strict duty-cycle timing
- Battery / voltage monitoring
- Downlink processing
- Watchdog servicing
- Event logging
- Low-power transitions
FreeRTOS gives us deterministic, priority-based scheduling and modular task isolation, so these run independently and share resources safely through queues, semaphores, and event flags. Just as importantly, it keeps the firmware testable and extendable — a new requirement is usually a new task, not a rewrite of a fragile main loop. And thanks to tickless idle, all that concurrency costs almost nothing in power: the node sleeps whenever there’s no work to do.
Collecting Packets: The SenseCAP M2 Indoor Gateway
Where the Wio-E5 node is a single end device, the M2 is an 8-channel gateway built around a Semtech SX1302 concentrator — so it can hear many nodes transmitting on different channels and spreading factors at once. It runs an OpenWrt-based ChirpStack Gateway OS, administered through the LuCI web UI.
Key gateway features
FeatureDetailConcentratorSemtech SX1302, 8-channelRegion (this deployment)US915 (us915_1)OSChirpStack Gateway OS (OpenWrt + LuCI)BackhaulEthernet / Wi-FiForwarding optionsChirpStack MQTT Forwarder, Semtech UDP Forwarder, Gateway MeshDeploymentIndoor
Inside ChirpStack Gateway OS, three components matter:
- Concentratord — the SX1302 driver; it talks to the radio hardware and emits gateway events (received uplinks, stats).
- MQTT Forwarder — takes those events and publishes them to an MQTT broker.
- UDP Forwarder — the legacy Semtech packet-forwarder path (unused here).
The key design choice: instead of forwarding to a self-hosted ChirpStack Network Server, we point the MQTT Forwarder straight at Azure IoT Hub, which speaks MQTT natively. The gateway is registered in IoT Hub as a device, and every uplink becomes a cloud message.
The End-to-End Path: Node → Gateway → Cloud
Wio-E5 Node ──LoRaWAN RF──► SenseCAP M2 Gateway ──MQTT/TLS──► Azure IoT Hub
(STM32WLE5, (SX1302 + ChirpStack (device-to-cloud
FreeRTOS) MQTT Forwarder) messages)
│
┌──────────┴──────────┐
▼ ▼
Databricks Bronze / Silver / GoldSetting Up the Gateway → Azure IoT Hub
The configuration lives under ChirpStack → MQTT Forwarder → MQTT configuration:
- Register the gateway in Azure IoT Hub. Create a device identity for the gateway and copy its primary key / connection string.
- Generate a SAS token for that device — e.g.
az iot hub generate-sas-token --hub-name <hub> --device-id <gatewayDeviceId> --duration <seconds>, or use Azure IoT Explorer. This token is the MQTT password. (Set a sensible expiry and plan to rotate it.) - Fill in the MQTT configuration:
- Topic prefix: your region ID (here
us915_1), no trailing slash. - Use JSON: enable only while debugging — JSON is human-readable but larger; Protobuf is the production choice.
- Server:
ssl://<your-hub>.azure-devices.net:8883(MQTT over TLS on 8883). - Username:
<your-hub>.azure-devices.net/<deviceId>/?api-version=2021-04-12— Azure's required format. - Password: the SAS token from step 2 (
SharedAccessSignature sr=…&sig=…&se=…). - CA certificate: Azure’s root CA — DigiCert Global Root G2 (Azure migrated off the older Baltimore CyberTrust Root). Getting this wrong is the #1 cause of silent TLS handshake failures.
4. Save & apply, then watch Status → logs to confirm the forwarder connects and publishes.
One important caveat — Azure IoT Hub is not a general MQTT broker. Its native MQTT endpoint accepts device-to-cloud messages only on the fixed topic devices/{deviceId}/messages/events/, and doesn't allow arbitrary topics or QoS 2. ChirpStack's forwarder publishes to its own scheme ({prefix}/gateway/{id}/event/{type}). If uplinks connect but never appear in IoT Hub, this topic mismatch is almost always why. The common production fixes are to bridge through a lightweight broker (Mosquitto) or Azure IoT Edge between the gateway and the hub, or to run a full ChirpStack Network Server and use its Azure integration.
Final Thoughts
This chapter was about building a reliable source of data — the physical and firmware foundation everything downstream depends on. Before a single analytics query can run, something has to sense the world, package the reading, and get it to the cloud without dropping or corrupting it. We assembled that across three layers.
The device — Wio-E5 (STM32WLE5). A single-chip Cortex-M4 + LoRa SoC gave us a compact, ultra-low-power node that runs production firmware, not a demo sketch. The integrated radio means fewer components to fail and years of battery life — the difference between a prototype and something you can actually deploy.
The firmware — FreeRTOS. Instead of a fragile while(1) superloop, we structured the node as isolated tasks communicating through queues and events. That buys three things that directly protect data quality: deterministic timing (uplinks on schedule), non-blocking acquisition (a slow sensor never stalls the radio), and tickless low-power sleep (quiet and efficient between readings). Clean, predictable firmware is what makes the data stream trustworthy.
The transport — LoRaWAN + SenseCAP M2 → Azure IoT Hub. Secure, OTAA-authenticated uplinks travel over long-range LoRaWAN to an 8-channel gateway, which forwards them over MQTT/TLS into Azure IoT Hub. At that point the reading has left the embedded world entirely and become a structured cloud event.
That handoff is the whole point of this chapter. Every decision — the chip, the RTOS architecture, the gateway configuration — exists to guarantee that clean, well-timed, secure telemetry arrives at Azure IoT Hub, ready to be worked on.
What’s Next
From here, the hardware fades into the background. The next chapter begins at Azure IoT Hub and focuses entirely on the data side of the pipeline — how each raw uplink is ingested, decoded, and progressively refined:
- Ingestion of device-to-cloud messages from IoT Hub
- Decoding and normalizing LoRaWAN payloads into structured records
- Processing with Databricks, moving data through the Bronze → Silver → Gold layers
- Turning a stream of sensor readings into clean, queryable, analytics-ready datasets







