Keywords: ESP32 to STM32, code migration, porting guide, MCU migration, firmware porting
Keywords: ESP32 to STM32, code migration, porting guide, MCU migration, firmware porting
Meta Description: Learn how to migrate firmware from ESP32 to STM32 with this practical porting guide covering architecture differences, HAL comparison, peripheral mapping, RTOS considerations, and real code examples.
Introduction
The ESP32 has earned a well-deserved reputation as a powerful, low-cost microcontroller with built-in WiFi and Bluetooth. However, many projects eventually outgrow the ESP32's ecosystem — whether due to real-time processing requirements, power consumption targets, industrial certification needs, or the desire for a more robust peripheral set. Migrating from ESP32 to STM32 is a natural transition that many embedded engineers face.
This guide walks through the practical steps of porting firmware from ESP32 (using ESP-IDF or Arduino framework) to STM32 (using STM32Cube HAL or Arduino Core). We cover architecture differences, hardware abstraction layer (HAL) comparison, peripheral mapping, communication interface differences, RTOS considerations, and strategies for mitigating the loss of built-in WiFi and Bluetooth connectivity.
By the end of this guide, you will have a clear roadmap for executing a successful ESP32-to-STM32 migration with minimal code rewrites and maximum design reuse.
Architecture Differences: Xtensa Dual-Core vs ARM Cortex-M
Understanding the fundamental architectural differences between ESP32 and STM32 is the first step in planning a migration. The ESP32 uses a dual-core Xtensa LX6 processor, while STM32 families use ARM Cortex-M cores ranging from M0 to M7.
Processor Architecture Comparison
| Feature | ESP32 | STM32F4 (Cortex-M4) | STM32H7 (Cortex-M7) |
|---|---|---|---|
| Core | Xtensa LX6 dual-core | ARM Cortex-M4F | ARM Cortex-M7 |
| Clock Speed | 160-240 MHz | 84-180 MHz | 400-550 MHz |
| Flash | 4 MB (external) | 512KB-2MB (internal) | 1-2MB (internal) |
| SRAM | 520 KB | 128-384 KB | 564-1024 KB |
| FPU | Yes (single precision) | Yes (single precision) | Yes (double precision) |
| Cache | Yes (instruction/data) | Yes (instruction/data) | Yes (D-cache + I-cache) |
| WiFi | Built-in | None | None |
| Bluetooth | Built-in (BLE + Classic) | None | None |
| DMA | Yes | Yes (stream-based) | Yes (MDMA + DMA) |
Key Architectural Migration Considerations
Dual-core to single-core: Many ESP32 applications use FreeRTOS to distribute tasks across two cores. When migrating to a single-core STM32, both cores' tasks must run on one processor. This may require task priority reconfiguration and careful analysis of CPU load. If dual-core processing is essential, consider the STM32H7 series, which features a Cortex-M7 + Cortex-M4 asymmetric dual-core architecture.
Memory model: The ESP32 executes code from external SPI flash via a cache, which introduces variable execution timing. STM32 devices execute from internal flash with deterministic access times. This is advantageous for real-time applications but limits available code space. Applications relying on large firmware images (common with WiFi stacks) may need code optimization or external memory mapping on STM32.
Floating-point performance: Both ESP32 and STM32F4 have single-precision FPU units. However, STM32H7's double-precision FPU and higher clock speeds make it significantly faster for signal processing and control algorithms. If your application uses heavy floating-point math, the STM32 will likely outperform the ESP32.
HAL Comparison: ESP-IDF vs STM32Cube HAL
The Hardware Abstraction Layer (HAL) is where most migration effort occurs. ESP-IDF and STM32Cube HAL have fundamentally different design philosophies and API structures.
API Design Philosophy
| Aspect | ESP-IDF | STM32Cube HAL |
|---|---|---|
| Language | C | C |
| Style | Function-based, handle-driven | Function-based, handle-driven |
| Configuration | menuconfig (Kconfig) | STM32CubeMX (graphical) |
| Initialization | Component init functions | MSP (MCU Support Package) callbacks |
| Interrupt Handling | ISR registration via esp_intr_alloc | NVIC + IRQHandler overrides |
| Code Generation | Manual or template-based | Auto-generated by CubeMX |
| RTOS Integration | FreeRTOS (built-in) | FreeRTOS (CMSIS-RTOS2 wrapper) |
GPIO Initialization Example
ESP32 (ESP-IDF):
#include "driver/gpio.h"
void init_gpio(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << GPIO_NUM_5),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_5, 1);
}
STM32 (HAL):
#include "stm32f4xx_hal.h"
void init_gpio(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}
The STM32 HAL requires explicit clock enablement for each peripheral (__HAL_RCC_GPIOx_CLK_ENABLE()), which is handled automatically by the ESP32 power management system. This is a common source of migration errors — forgetting to enable peripheral clocks will result in silent failures.
Peripheral Mapping: GPIO, UART, SPI, and I2C
Peripheral porting requires careful pin and configuration mapping. Unlike the ESP32's flexible matrix that allows routing most functions to any GPIO, STM32 devices have fixed peripheral-to-pin mappings defined by alternate function (AF) tables.
GPIO Migration
| Feature | ESP32 | STM32 |
|---|---|---|
| Pin flexibility | Any function on any pin (via GPIO matrix) | Fixed alternate function mapping |
| Voltage levels | 3.3V | 3.3V (most), 1.8V capable (some) |
| Drive strength | Configurable | Configurable (speed modes) |
| Internal pull-up/down | Yes | Yes |
| 5V tolerance | No | Yes (on most pins, marked FT in datasheet) |
| Maximum output current | 40 mA per pin | 25 mA per pin (8 mA recommended) |
Migration tip: Create a pin mapping table early in the migration process. List every ESP32 GPIO used, its function, and the corresponding STM32 pin with the correct alternate function. This table becomes the reference for both schematic design and software configuration.
UART Porting
ESP32 UART initialization:
#include "driver/uart.h"
void init_uart(void) {
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
uart_driver_install(UART_NUM_1, 1024, 0, 0, NULL, 0);
uart_param_config(UART_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, 17, 16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
STM32 UART initialization:
#include "stm32f4xx_hal.h"
UART_HandleTypeDef huart1;
void init_uart(void) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
SPI and I2C Considerations
| Feature | ESP32 SPI | STM32 SPI | ESP32 I2C | STM32 I2C |
|---|---|---|---|---|
| Max Speed | 80 MHz | 50 MHz (F4) / 100 MHz (H7) | 1 MHz | 1 MHz (Fast+) |
| DMA Support | Yes | Yes (stream-based) | Yes | Yes |
| Hardware CS | Limited | Yes (NSS pin) | N/A | N/A |
| Multi-master | No | Yes | Yes | Yes |
| FIFO | 64 bytes | 1 byte (F4) / 16 bytes (F7/H7) | N/A | N/A |
The STM32 SPI peripheral offers more hardware features but requires more careful configuration. The ESP32's SPI driver handles CS toggling in software by default, while STM32 can use hardware NSS management for automatic CS control, reducing CPU overhead.
RTOS Considerations: FreeRTOS Migration
Both ESP-IDF and STM32Cube support FreeRTOS, which simplifies task migration. However, there are important differences in FreeRTOS configuration and behavior between platforms.
FreeRTOS Configuration Differences
| Feature | ESP-IDF FreeRTOS | STM32Cube FreeRTOS |
|---|---|---|
| FreeRTOS Version | v10.x (customized) | v10.x (CMSIS-RTOS2) |
| Dual-core support | Yes ( SMP-aware) | No (single-core) |
| Tick rate | 100 Hz (default) | 1000 Hz (default) |
| Task stack size | Larger (WiFi stack) | Smaller (tunable) |
| Heap allocator | heap_4 (modified) | heap_4 (default) |
| vTaskDelay units | Ticks | Ticks (CMSIS: ms) |
| Interrupt priority | Single-level (simplified) | NVIC priority levels |
Task Migration Example
ESP32 task:
void sensor_task(void *pvParameters) {
while (1) {
read_sensor();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// Creation
xTaskCreate(sensor_task, "sensor", 4096, NULL, 5, NULL);
STM32 task (CMSIS-RTOS2):
void sensor_task(void *argument) {
while (1) {
read_sensor();
osDelay(100);
}
}
// Creation (in defaultTask or main)
osThreadNew(sensor_task, NULL, &sensor_task_attr);
Key migration notes:
-
Stack sizes: ESP32 tasks typically use larger stacks (4KB+) due to WiFi/BLE stack overhead. STM32 tasks can use smaller stacks (512B–2KB) since there is no wireless stack. Profile stack usage with
uxTaskGetStackHighWaterMark()to optimize. -
Priority levels: ESP-IDF uses a flat priority scheme. STM32 FreeRTOS uses NVIC priority levels — ensure that
configMAX_SYSCALL_INTERRUPT_PRIORITYis set correctly to prevent priority inversion issues. -
Tick rate: The default STM32 tick rate of 1000 Hz provides 1ms resolution but increases overhead. For power-sensitive applications, consider lowering to 100 Hz and using hardware timers for precise timing.
WiFi and BLE Loss: Mitigation Strategies
The most significant challenge in migrating from ESP32 to STM32 is the loss of built-in wireless connectivity. This requires both hardware and software changes.
Wireless Module Integration Options
| Solution | Protocol | Interface | Cost | Complexity | Recommended |
|---|---|---|---|---|---|
| ESP8266 AT module | WiFi | UART | Low | Low | Yes (simple WiFi) |
| ATWINC1500 | WiFi | SPI | Medium | Medium | Yes (MCU-grade) |
| ESP32-C3 as coprocessor | WiFi + BLE | UART/SPI | Low | Medium | Yes (full stack) |
| STM32WB55 | BLE | Internal | Medium | High | Yes (BLE only) |
| nRF52840 module | BLE | UART/SPI | Medium | Medium | Yes (BLE only) |
| Wiznet W5500 | Ethernet | SPI | Low | Low | Yes (wired) |
Coprocessor Approach: ESP32-C3 as Wireless Modem
A popular migration strategy is using an ESP32-C3 as a wireless coprocessor connected to the STM32 via UART or SPI. The ESP32-C3 handles all WiFi and BLE protocols, while the STM32 focuses on real-time processing and application logic.
Architecture:
[STM32 Main MCU] <--UART/SPI--> [ESP32-C3 Coprocessor] <---> [WiFi/BLE]
| |
Application logic AT commands or
Real-time control custom protocol
Sensor reading
Software approach: Use an AT command interface or a custom binary protocol over UART. The ESP32-C3 firmware handles WiFi connection, TCP/UDP sockets, and BLE GATT operations, exposing them through simple serial commands to the STM32.
This approach offers the best of both worlds: STM32's real-time capabilities and peripheral richness combined with ESP32's mature wireless stack. The trade-off is increased BOM cost and PCB complexity.
Ethernet Alternative
For stationary applications, replacing WiFi with Ethernet using the Wiznet W5500 or a built-in MAC (STM32F107, STM32F7, STM32H7) provides a reliable, deterministic network connection. Ethernet eliminates wireless interference issues and simplifies EMC compliance.
Real Code Example: Porting a Temperature Logger
To illustrate the migration process, let us port a simple temperature logger from ESP32 to STM32. The application reads an I2C temperature sensor every 5 seconds and logs the data.
ESP32 Original Code
#include "driver/i2c.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#define I2C_MASTER_SCL_IO 22
#define I2C_MASTER_SDA_IO 21
#define TEMP_SENSOR_ADDR 0x48
#define TAG "TEMP_LOGGER"
void i2c_master_init(void) {
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_IO,
.scl_io_num = I2C_MASTER_SCL_IO,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = 100000,
};
i2c_param_config(I2C_NUM_0, &conf);
i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0);
}
float read_temperature(void) {
uint8_t data[2];
i2c_master_read_from_device(I2C_NUM_0, TEMP_SENSOR_ADDR, data, 2, 100 / portTICK_PERIOD_MS);
int16_t raw = (data[0] << 8) | data[1];
return raw / 256.0f;
}
void app_main(void) {
i2c_master_init();
ESP_LOGI(TAG, "Temperature logger started");
while (1) {
float temp = read_temperature();
ESP_LOGI(TAG, "Temperature: %.2f C", temp);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
STM32 Ported Code
#include "stm32f4xx_hal.h"
#include <stdio.h>
#define TEMP_SENSOR_ADDR 0x48
extern I2C_HandleTypeDef hi2c1;
float read_temperature(void) {
uint8_t data[2];
HAL_I2C_Master_Receive(&hi2c1, TEMP_SENSOR_ADDR << 1, data, 2, 100);
int16_t raw = (data[0] << 8) | data[1];
return raw / 256.0f;
}
void temperature_logger_task(void) {
printf("Temperature logger started\r\n");
while (1) {
float temp = read_temperature();
printf("Temperature: %.2f C\r\n", temp);
HAL_Delay(5000);
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART2_UART_Init();
temperature_logger_task();
while (1) {}
}
Migration Notes for This Example
-
I2C address shifting: ESP-IDF uses 7-bit I2C addresses, while STM32 HAL uses 8-bit addresses (7-bit shifted left). The address
0x48becomes0x48 << 1 = 0x90in STM32 HAL calls. -
Logging: ESP-IDF's
ESP_LOGIis replaced withprintfredirected to UART. For production, consider implementing a proper logging system with log levels. -
Delay function:
vTaskDelay(5000 / portTICK_PERIOD_MS)becomesHAL_Delay(5000). If using FreeRTOS on STM32, preferosDelay(5000)to yield the CPU. -
Initialization: STM32 requires explicit initialization of all peripherals through CubeMX-generated
MX_*_Init()functions and clock configuration viaSystemClock_Config().
Build System Migration: From ESP-IDF to STM32CubeIDE
The build system transition from ESP-IDF (based on CMake and menuconfig) to STM32CubeIDE (based on Eclipse and GCC) requires adjusting your project structure and configuration approach.
| Build Aspect | ESP-IDF | STM32CubeIDE |
|---|---|---|
| Build system | CMake + Ninja | Make or CMake (GCC-based) |
| Configuration | menuconfig (Kconfig) | STM32CubeMX (graphical) |
| Project structure | components/ directory | Drivers/, Core/, Application/ |
| Linker script | Auto-generated | Auto-generated (editable) |
| Flash tool | esptool.py | STM32CubeProgrammer / OpenOCD |
| Debugging | JTAG (ESP-Prog) | SWD (ST-Link) |
| Library management | IDF Component Registry | STM32Cube_FW |
Migration tip: Use STM32CubeMX to generate the initial project structure, then migrate application logic component by component. Start with GPIO, then UART (for debug output), then I2C/SPI peripherals. Test each peripheral independently before integrating the full application.
FAQ
Is it difficult to migrate from ESP32 Arduino to STM32 Arduino?
The Arduino Core for STM32 (STM32duino) supports many STM32 boards and provides a similar API to ESP32 Arduino. Basic GPIO, UART, I2C, and SPI functions port directly. However, ESP-specific libraries (WiFi.h, BLEDevice.h, ESP32-specific timer APIs) will not work. The migration difficulty depends heavily on how many ESP-specific APIs your code uses.Which STM32 series is the best replacement for ESP32?
For general-purpose applications with similar performance, the STM32F4 series (Cortex-M4, 180 MHz) is a good starting point. For higher performance needs, consider the STM32H7 (Cortex-M7, 550 MHz). If low power is the priority, the STM32L4 or STM32U5 series offers excellent efficiency. For wireless needs, the STM32WB series provides built-in Bluetooth Low Energy.How do I handle the loss of WiFi when migrating to STM32?
Three common approaches: (1) Use an external WiFi module like the ATWINC1500 or ESP8266 AT module connected via UART/SPI. (2) Use an ESP32-C3 as a wireless coprocessor with a serial command interface. (3) Switch to Ethernet using the Wiznet W5500 or an STM32 with built-in Ethernet MAC. Choose based on your wireless requirements, cost constraints, and PCB complexity budget.Can I use FreeRTOS on STM32 just like on ESP32?
Yes. STM32Cube includes FreeRTOS (via CMSIS-RTOS2 abstraction) and STM32CubeMX can generate FreeRTOS projects automatically. The API differs slightly (osDelay vs vTaskDelay, osThreadNew vs xTaskCreate), but the underlying FreeRTOS kernel is the same. Task priorities, stack sizes, and tick rates may need adjustment for the STM32's single-core architecture.What tools do I need for STM32 development?
You need an ST-Link debugger/programmer (V2 or V3) for flashing and debugging via SWD. For software, STM32CubeIDE provides a complete IDE with integrated CubeMX configuration. Alternative toolchains include Keil MDK, IAR Embedded Workbench, or VS Code with the STM32 extension. An oscilloscope and logic analyzer are also essential for peripheral debugging.How long does a typical ESP32 to STM32 migration take?
For a moderate-complexity project (GPIO + UART + I2C + SPI + FreeRTOS), expect 2-4 weeks of engineering effort. Adding wireless module integration can extend this to 4-8 weeks. The migration is faster if the original code is well-structured with clear hardware abstraction layers. Projects with heavy ESP-specific dependencies (WiFi mesh, ESP-NOW, deep sleep with WiFi wake) will require significantly more rework.References
- STMicroelectronics — STM32Cube HAL Documentation: https://www.st.com/en/embedded-software/stm32cube-mcu-mpu-packages.html
- Espressif — ESP-IDF Programming Guide: https://docs.espressif.com/projects/esp-idf/en/latest/
- FreeRTOS — STM32 Integration Guide: https://www.freertos.org/RTOS-Cortex-M3-M4.html
- STM32duino — Arduino Core for STM32: https://github.com/stm32duino/Arduino_Core_STM32
- Microchip — ATWINC1500 WiFi Module Datasheet: https://www.microchip.com/wwwproducts/en/ATWINC1500