STM32 HAL and register code, in the browser
Beginner
Or see every lesson.
- 505parts on the bench
- 45boards running now
- 1.00xreal time, on every board
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.
#include "stm32f4xx.h"
int main(void)
{
clock_100mhz();
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER &= ~GPIO_MODER_MODER5;
GPIOA->MODER |= GPIO_MODER_MODER5_0; /* 01: general purpose output */
while (1) {
GPIOA->ODR ^= GPIO_ODR_OD5;
delay_ms(500);
}
}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.
#include "stm32f4xx.h"
void setup() {
Serial.begin(115200);
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER = (GPIOA->MODER & ~GPIO_MODER_MODER5) | GPIO_MODER_MODER5_0;
Serial.println(SystemCoreClock);
}
void loop() { GPIOA->ODR ^= GPIO_ODR_OD5; delay(250); }Open this sketch in the editorThe 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.
#include "main.h"
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
while (1) {
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_RESET) {
/* B1 pulls PC13 low while it is pressed. */
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);
HAL_Delay(10);
} else {
HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
HAL_Delay(250);
}
}
}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.
UART_HandleTypeDef huart2;
int _write(int file, char *ptr, int len)
{
(void)file;
HAL_UART_Transmit(&huart2, (uint8_t *)ptr, (uint16_t)len, HAL_MAX_DELAY);
return len;
}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.
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
int32_t duty = 0;
int32_t step = 20;
while (1) {
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (uint32_t)duty);
duty += step;
if (duty <= 0 || duty >= 999) {
step = -step;
}
HAL_Delay(10);
}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.
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) {
uint32_t raw = HAL_ADC_GetValue(&hadc1);
uint32_t mv = raw * 3300U / 4095U;
printf("A0 = %lu (%lu mV)\r\n", (unsigned long)raw, (unsigned long)mv);
}
HAL_ADC_Stop(&hadc1);
HAL_Delay(200);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.
| 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 circuitQuestions
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().
Keep going
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.