STM32 Firmware Development: From Prototype to Production

Reading time: 5 minutes

Last modified:

This guide is for developers moving from Arduino to STM32, and for engineers starting firmware for an IoT product that needs to ship.

Why STM32

Arduino abstracts away the hardware. That works until you need to configure a specific UART baud rate, run two peripherals on the same SPI bus, or squeeze a device into a 2μA sleep current. STM32 gives you direct peripheral control — UART, SPI, I2C, CAN, ADC — all configurable at the register level through HAL or bare CMSIS.

Key reasons engineers choose STM32:

  • FreeRTOS integration ships inside STM32CubeIDE. No third-party port needed.
  • Ultra-low-power variants: STM32L4 hits 2μA in Stop 2 mode. That number determines whether a battery lasts two years or two weeks.
  • Production track record: STM32 runs in medical monitors, industrial controllers, and automotive ECUs. Silicon supply, long-term part availability, and vendor documentation are reliable.
  • Single toolchain: STM32CubeMX configures your pins and clock tree, generates initialization code, and hands it to CubeIDE — you write application logic, not boilerplate.

Step 1: Tools and First Project

Download STM32CubeIDE from st.com — it is free and bundles the GCC ARM toolchain, ST-Link drivers, and a debug perspective. No license key.

For the eval board, start with the Nucleo-L476RG. It has a built-in ST-Link debugger, Arduino-compatible headers for breadboarding, and an STM32L4 — the same family you would use in a real IoT product.

Open CubeMX inside CubeIDE:

  1. Select your Nucleo board. CubeMX pre-configures the ST-Link UART and onboard LED.
  2. Set PA5 to GPIO Output (that is the green LED on L476RG).
  3. Enable USART2 in Asynchronous mode at 115200 baud — it routes to the ST-Link virtual COM port, so you get a serial monitor without a USB-UART adapter.
  4. Click Generate Code. CubeMX writes main.c, peripheral init files, and a Makefile.
  5. Build and flash with the Run button. The first flash takes under 10 seconds over SWD.

Step 2: GPIO and UART Basics

The generated main.c has a while(1) loop waiting for your code. Three calls cover 90% of basic IO:

/* Blink the LED */
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);

/* Send a debug string over UART */
uint8_t msg[] = "Hello\r\n";
HAL_UART_Transmit(&huart2, msg, sizeof(msg) - 1, HAL_MAX_DELAY);

/* Read a button state */
GPIO_PinState state = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);
if (state == GPIO_PIN_RESET) {
    /* button pressed — PC13 is active low on Nucleo */
}

HAL_MAX_DELAY blocks until transmission completes. For production code, use DMA-based transmit — HAL_UART_Transmit_DMA() — so the CPU does not stall during a long UART frame.

Step 3: Adding a Real Sensor

Most IoT sensors talk over UART, I2C, or SPI, or output an analog voltage that you read through the ADC. For an analog pH sensor, the electrode outputs 0–3V corresponding to pH 0–14. Wire the output to PA0 (ADC1 channel 5 on L476RG) and read it:

HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint32_t raw = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);

/* Map 12-bit ADC (0–4095) to pH 0–14 */
float voltage = raw * 3.3f / 4095.0f;
float ph = voltage * (14.0f / 3.3f);

Calibration matters more than the formula. Take readings in pH 4.0 and pH 7.0 buffer solutions, record the raw ADC values, then fit a two-point linear correction. A ±0.1 pH accuracy is achievable with a stable reference voltage and proper shielding on the analog input trace.

Step 4: Low-Power Sleep Modes

A device drawing 10mA from a 2000mAh cell lasts 8 days. At 2μA in sleep with 10ms wake bursts every 60 seconds, the same cell lasts 2+ years. That difference is the STM32 power mode stack.

STM32L4 has four modes:

Mode Current Wake sources
Run 10–30mA
Sleep 1–5mA Any interrupt
Stop 2 2μA RTC, EXTI, LPUART
Standby 0.3μA RTC alarm, WKUP pin

Stop 2 is the working mode for most IoT firmware. The CPU halts, RAM retains state, and the RTC keeps ticking. To enter it:

/* Suspend SysTick to prevent immediate wake */
HAL_SuspendTick();

/* Enter Stop 2 — WFI waits for interrupt */
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);

/* Execution resumes here after wake */
HAL_ResumeTick();

/* Reconfigure clocks — Stop 2 resets to MSI */
SystemClock_Config();

A common wake source is an accelerometer interrupt. The ADXL345 has a programmable activity threshold — it pulls an EXTI line when motion exceeds the threshold. The STM32 sleeps at 2μA, the accelerometer runs at 23μA waiting for motion, and the system wakes only when needed. This is the same pattern in our GPS asset tracker which reaches a 2.2-year battery life on 3× AA cells.

Step 5: From Prototype to Production PCB

The Nucleo board gets you to a working firmware. A production device needs a custom PCB.

Key decisions when moving off the eval board:

STM32 package: LQFP-48 or LQFP-64 are hand-solderable and well-documented. QFN-32 shrinks board area but requires a reflow oven. Pick the package your contract manufacturer can handle.

Decoupling capacitors: Place 100nF ceramic caps on every VDD pin, within 0.5mm of the pin. Miss this and you will chase noise on ADC readings for days.

Programming header: Keep a 4-pin SWD header on every production board — SWDIO, SWDCLK, GND, 3.3V. A $10 Tag-Connect footprint eliminates the through-hole header cost in volume. Without SWD access, a firmware bug means desoldering the chip.

Boot mode: BOOT0 pin low at reset boots from flash. Pull it low with a 10kΩ resistor. Leave a jumper footprint to pull it high for UART bootloader access during factory programming.

For production flashing, two options:

  • ST-Link gang programmer via a bed-of-nails jig — fast, 5 seconds per board, works at volume.
  • UART bootloader using STM32 factory-resident bootloader — no external programmer needed, slower, acceptable for low volumes.

Need Firmware Built?

CimpleO’s embedded engineering team handles STM32 and ESP32 firmware, PCB design review, and cloud backend — from eval board to production-ready hardware. Tell us about your project.

Часто задаваемые вопросы

Which STM32 series should I start with?

STM32F103 (Blue Pill) for learning — cheap, widely documented, strong community. STM32L4 for production IoT where battery life matters — ultra-low-power peripherals and deep sleep at 2μA. STM32H7 for compute-heavy tasks like signal processing or motor control.

Do I need FreeRTOS or can I write bare-metal firmware?

Bare-metal is simpler and appropriate for single-task firmware (e.g., a GPS tracker that sleeps, wakes, reads a fix, transmits, sleeps again). FreeRTOS makes sense when you have 3+ concurrent tasks that need timing guarantees — sensor reading, communication, and UI running independently.

How do I implement OTA firmware updates on STM32?

STM32 has a built-in bootloader that can receive firmware over UART, USB, or CAN. For wireless OTA, implement a dual-bank flash scheme: receive new firmware into bank B while running from bank A, verify the checksum, then swap. STM32L4 and STM32H7 support this natively.

What tools do I need for STM32 development?

STM32CubeIDE (Eclipse-based, free, from ST) for writing, building, and debugging. STM32CubeMX for pin configuration and clock tree setup. An ST-Link v2 debugger ($10 clone or built into Nucleo boards) for JTAG/SWD flashing and debugging.

CimpleO embedded
STM32 Firmware Development: From Prototype to Production
JUN 09 2026 · 5 MIN
Table of Contents