Source: https://mokxi.com/learn/stm32-hal-register-code
Updated: 2026-11-01

Learn

# STM32 HAL and register code, in the browser

Beginner

Written by the Mokxi team, updated November 1, 2026

Start building free Open the editor

Or see every lesson.

- 505 parts on the bench
- 45 boards running now
- 1.00x real time, on every board

Click to open it in the editor

The built-in blink on the Nucleo. The register and HAL programs below drive the same pin, PA5, which is LD2 and D13.

Most STM32 courses do not use the Arduino core. They hand you an STM32CubeIDE project, with main.c, main.h, stm32f4xx_hal_conf.h and an interrupt file, or a page of register code that starts with #include "stm32f4xx.h" and writes RCC->AHB1ENR and GPIOA->ODR by hand. Mokxi builds both, with ST’s own CMSIS device headers and the STM32Cube HAL and LL drivers, and runs them on its simulated Nucleo-F411RE, Black Pill (STM32F411) and Blue Pill (STM32F103).

Paste a main.c into the board’s files, or add the whole CubeIDE project, and press Run. Whatever a pasted file leaves behind, Mokxi writes the way CubeIDE would have, and the build log says each time it did.

The circuit above is the Nucleo’s built-in blink, which uses the Arduino core. To try the HAL, open it in the editor, replace sketch.ino with a main.c, and paste in any example from this page.

## Register code with stm32f4xx.h

stm32f4xx.h is ST’s CMSIS device header: a struct per peripheral at its real address, and a name for every bit, so RCC_AHB1ENR_GPIOAEN is bit 0 of RCC->AHB1ENR and GPIO_MODER_MODER5_0 is the low bit of PA5’s mode. The program below turns on GPIOA’s clock, makes PA5 an output and flips it every 500 ms. On the Blue Pill the header is stm32f1xx.h, and the old Standard Peripheral Library name stm32f10x.h works too, for its registers.

Give the board a main() and that is the whole program: no Arduino core starts, the vector table and startup code are the same as ST’s in every handler name, and SystemCoreClock, SystemCoreClockUpdate() and SysTick are CMSIS’s. The example sets the PLL to 100 MHz first, writing RCC->PLLCFGR and three flash wait states into FLASH->ACR, because Mokxi’s Nucleo always runs its core at 100 MHz.

main.c: blink LD2 with registers (nucleo-blink-registers)

## Register code in a sketch

You can mix the two. A sketch.ino with setup() and loop() that includes stm32f4xx.h keeps Serial, delay() and millis() and gets ST’s register names on top. SystemCoreClock then reads the speed the core really runs at, 100 MHz on the Nucleo, 84 MHz on the Black Pill and 72 MHz on the Blue Pill, so SysTick and baud-rate sums written against it come out right.

sketch.ino: ST’s registers beside the Arduino core

Open this sketch in the editor The code alone: the editor places the board and the parts it can read from the sketch.

## A CubeIDE project: HAL_Init, the clock, then the pins

This is the shape STM32CubeIDE generates. HAL_Init() sets up the flash prefetch and SysTick, SystemClock_Config() brings up the PLL through HAL_RCC_OscConfig() and HAL_RCC_ClockConfig(), and MX_GPIO_Init() makes LD2 an output and B1 an input. LD2_Pin, LD2_GPIO_Port, B1_Pin and B1_GPIO_Port come from main.h.

Paste only main.c and Mokxi adds the main.h CubeIDE writes for the board (LD2 on PA5 and B1 on PC13 on the Nucleo, LED on PC13 on the Black Pill and the Blue Pill), a stm32f4xx_hal_conf.h with the HAL modules your code uses, the SysTick_Handler that calls HAL_IncTick(), and an MSP file that turns each peripheral’s clock and pins on, as CubeMX would. Add your own copy of any of them and yours is used instead. ST’s startup file and CubeIDE’s syscalls.c and sysmem.c are left out with a note: Mokxi’s startup code and C library do their jobs.

main.c: blink LD2, held on while B1 is pressed (nucleo-hal-button)

## printf over the ST-LINK UART

On a Nucleo the ST-LINK’s virtual COM port is USART2, on PA2 and PA3, and in Mokxi that is the Serial Monitor. printf() ends in _write(), so the usual CubeIDE retarget works as it stands: send the bytes with HAL_UART_Transmit(). Defining __io_putchar() instead, the other way tutorials do it, works too. With neither, printf() goes nowhere, as on the board.

main.c: the retarget, from nucleo-hal-printf

## PWM with HAL_TIM and analog input with HAL_ADC

TIM2 channel 1 is on PA5 with alternate function 1, so it can dim LD2 itself. At 100 MHz a prescaler of 100 and a period of 1000 is a 1 kHz PWM, and __HAL_TIM_SET_COMPARE() moves the duty from 0 to 999 and back. HAL_TIM_MspPostInit() puts the pin on the timer.

ADC1 channel 0 is A0, which is PA0. Start a conversion, poll for it, read it: the result is 12 bits, so a potentiometer at a quarter turn reads about 1024, which is 825 mV.

main.c: the fade loop, from nucleo-hal-pwm-fade

## Reading a potentiometer

Wire the potentiometer’s ends to 3V3 and GND and its wiper to A0, then turn it while the program runs. The value prints to the Serial Monitor five times a second.

main.c: the read loop, from nucleo-hal-adc

## What runs, and what does not yet

These run on the simulated chips: GPIO, EXTI interrupts with HAL_GPIO_EXTI_Callback(), SysTick and HAL_Delay(), the NVIC, the UARTs (USART1 and USART2 on the F411, USART1 to USART3 on the F103), ADC1, and the timers that drive pins (TIM1 to TIM5 and TIM10 on the F411, TIM1 to TIM4 on the F103). Everything in the HAL builds, with the HAL and LL drivers for ADC, CORTEX, CRC, DMA, EXTI, FLASH, GPIO, I2C, IWDG, PWR, RCC, RTC, SPI, TIM, UART, USART and WWDG.

SPI, I2C, I2S, DMA, the RTC, the watchdogs and CRC are not simulated yet. A project that calls them builds, and the build log says which ones the simulated chip does not have. When the program reaches one, the board stops and the Serial Monitor shows firmware crashed at pc=..., or your HardFault_Handler runs if you wrote one. The flash interface and the power controller are not simulated either, but the HAL’s writes to them only need to read back, so Mokxi gives them RAM of the same layout: HAL_Init() and every SystemClock_Config() run.

The clock tree is not simulated: the core always runs at the board’s own speed, 100 MHz on the Nucleo, 84 MHz on the Black Pill and 72 MHz on the Blue Pill, whatever the RCC registers say. A program that sets those speeds, as the examples here do, keeps exact time. One that runs at another, such as CubeMX’s 84 MHz default on the Nucleo or no clock setup at all, which is the 16 MHz HSI, still runs, but its HAL_Delay() and baud rates are off by the ratio of the two speeds.

STM32 peripherals in the simulator, for HAL and register code
Peripheral | Runs | Notes

GPIO and EXTI | Yes | HAL_GPIO_*, HAL_GPIO_EXTI_Callback(), GPIOx->ODR, IDR, BSRR.

SysTick, NVIC | Yes | HAL_Delay(), HAL_GetTick(), HAL_NVIC_*, interrupt priorities.

USART | Yes | HAL_UART_Transmit(), printf through _write(); USART2 is the Nucleo’s Serial Monitor.

ADC1 | Yes | HAL_ADC_Start(), PollForConversion(), GetValue(); 12 bits.

Timers | Yes | HAL_TIM_PWM_*, HAL_TIM_Base_*, output compare on the timers that reach pins.

RCC | Yes | Every register reads back and the oscillators report ready; the core speed is fixed.

FLASH, PWR | As RAM | Wait states, prefetch and voltage scale are stored and read back.

SPI, I2C, I2S, DMA, RTC, watchdogs, CRC | Not yet | They build; the board stops at the first access.

Open this circuit in the editor. STM32 Nucleo-F411RE: LD2 and an LED on D13 (PA5), blinking, wired as it runs at the top of this page. Change a part or a value and run it again.
Open the circuit

## Questions

Can I run an STM32CubeIDE project in a browser?

Yes, on the simulated Nucleo-F411RE, Black Pill and Blue Pill. Add main.c and the rest of the project to the board’s files and press Run. It builds against ST’s own CMSIS headers and STM32Cube HAL, and anything the project leaves out, such as main.h or stm32f4xx_hal_conf.h, is filled in with CubeIDE’s defaults and named in the build log.

Why does my code say 'stm32f4xx.h' file not found?

On a board that is not an STM32F4 it is the wrong header: the Blue Pill is an STM32F103, whose header is stm32f1xx.h. On the Nucleo-F411RE and the Black Pill, stm32f4xx.h and stm32f4xx_hal.h are there and build.

Does printf work with the HAL?

Yes. Define _write() to send the bytes with HAL_UART_Transmit() on USART2, or define __io_putchar(), and printf() prints to the Serial Monitor, as the Nucleo’s ST-LINK virtual COM port does on the desk.

Which STM32 peripherals does the simulator run?

GPIO, EXTI, SysTick, the NVIC, the USARTs, ADC1 and the timers that drive pins. SPI, I2C, DMA, the RTC and the watchdogs build but are not simulated yet.

Can I use ST’s registers and the Arduino core together?

Yes. A sketch.ino with setup() and loop() can include stm32f4xx.h or stm32f4xx_hal.h and use GPIOA->ODR or HAL_GPIO_WritePin() beside Serial and delay().

Related

## Keep going

Blink an LED with Arduino: Wiring and the Resistor Arduino PWM Fade: analogWrite, Pins and the Curve Arduino Interrupts: attachInterrupt on Pin 2 and 3 Arduino Assembly Language: AVR Assembly Online Open the Nucleo blink in the editor, then replace sketch.ino with a main.c The STM32 Nucleo-F411RE in Mokxi The Black Pill (STM32F411) in Mokxi The STM32 Blue Pill in Mokxi The project page, with the full sketch Beginner

Sources

## Where the facts on this page come from

- STMicroelectronics: the STM32F4 HAL and LL drivers (BSD-3-Clause), the copy Mokxi builds with
- STMicroelectronics: the STM32F4 CMSIS device headers (Apache-2.0)
- ST RM0383: the STM32F411 reference manual, with every register on this page

## Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.

Start building free Open the editor
