Migrating from ESP32 to STM32: Code Compatibility and Porting Guide

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.

ESP32 and STM32 development boards side by side comparison

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.

Pin mapping diagram showing ESP32 to STM32 peripheral connections


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:

  1. 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.

  2. Priority levels: ESP-IDF uses a flat priority scheme. STM32 FreeRTOS uses NVIC priority levels — ensure that configMAX_SYSCALL_INTERRUPT_PRIORITY is set correctly to prevent priority inversion issues.

  3. 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

  1. I2C address shifting: ESP-IDF uses 7-bit I2C addresses, while STM32 HAL uses 8-bit addresses (7-bit shifted left). The address 0x48 becomes 0x48 << 1 = 0x90 in STM32 HAL calls.

  2. Logging: ESP-IDF's ESP_LOGI is replaced with printf redirected to UART. For production, consider implementing a proper logging system with log levels.

  3. Delay function: vTaskDelay(5000 / portTICK_PERIOD_MS) becomes HAL_Delay(5000). If using FreeRTOS on STM32, prefer osDelay(5000) to yield the CPU.

  4. Initialization: STM32 requires explicit initialization of all peripherals through CubeMX-generated MX_*_Init() functions and clock configuration via SystemClock_Config().

Flowchart showing ESP32 to STM32 firmware porting process


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

  1. STMicroelectronics — STM32Cube HAL Documentation: https://www.st.com/en/embedded-software/stm32cube-mcu-mpu-packages.html
  2. Espressif — ESP-IDF Programming Guide: https://docs.espressif.com/projects/esp-idf/en/latest/
  3. FreeRTOS — STM32 Integration Guide: https://www.freertos.org/RTOS-Cortex-M3-M4.html
  4. STM32duino — Arduino Core for STM32: https://github.com/stm32duino/Arduino_Core_STM32
  5. Microchip — ATWINC1500 WiFi Module Datasheet: https://www.microchip.com/wwwproducts/en/ATWINC1500

Table of Contents

Translate »

Get Component Availability Updates

Receive periodic availability notes, BOM sourcing guidance and supply-chain updates.