How to port FreeRTOS to ARM Cortex-M4
This is a step-by-step guide for getting FreeRTOS up and running on an ARM Cortex-M4 microcontroller using Keil MDK and standard ARM debugging tools.
Step 1 — Download FreeRTOS source
Grab the latest source bundle and reference docs from the official site: freertos.org/a00104.html.
Step 2 — Prepare your debugger
Two debuggers cover almost every Cortex-M4 board you'll meet:
- ST-Link V2 — STMicroelectronics' debugger, ships with Nucleo and Discovery boards.
- Segger J-Link — works on virtually every ARM target; pricier but rock solid.
Install the matching flash utility for your specific MCU.
Step 3 — Lay out the Keil project
Organize the project into three logical groups:
Project/
├── cmsis_core/ ← ARM Cortex-M4 core headers from chip vendor SDK
├── FREERTOS/ ← FreeRTOS source code
└── portable/
├── ARM_CM4F/ ← Cortex-M4 + FPU port files
└── MemMang/ ← heap_1.c / heap_2.c / heap_4.c — pick one
Pick the port files specific to ARM Cortex-M4 (with FPU if your chip has one). Mismatching the port is the most common source of weird hard faults.
Step 4 — Configure FreeRTOS
The single most important file is FreeRTOSConfig.h. At minimum, edit:
- MCU clock frequency (
configCPU_CLOCK_HZ) — must match your real SystemCoreClock or every tick will be wrong. - UART init — bring up a serial port early so you can see what's happening.
- Printf support — implement
fputc()retargeted to your UART soprintfworks.
A minimal Cortex-M4 FreeRTOSConfig.h looks roughly like this:
#define configUSE_PREEMPTION 1
#define configCPU_CLOCK_HZ ((unsigned long) 168000000)
#define configTICK_RATE_HZ ((TickType_t) 1000)
#define configMAX_PRIORITIES 5
#define configMINIMAL_STACK_SIZE ((uint16_t) 128)
#define configTOTAL_HEAP_SIZE ((size_t) (32 * 1024))
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCHECK_FOR_STACK_OVERFLOW 2
#define configUSE_MALLOC_FAILED_HOOK 1
/* Cortex-M4 specific — interrupt priorities */
#define configPRIO_BITS 4 /* STM32F4 has 4 priority bits */
#define configKERNEL_INTERRUPT_PRIORITY (15 << (8 - configPRIO_BITS))
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( 5 << (8 - configPRIO_BITS))
/* Map the kernel handlers to the CMSIS names */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
Step 5 — Pick a heap implementation
FreeRTOS ships five heap allocators in portable/MemMang/. For a Cortex-M4 starter project, pick one:
heap_1.c— simplest; allocate-only, never free. Fine for static task sets.heap_2.c— best-fit; can free but doesn't merge adjacent blocks. Fragments over time.heap_4.c— best-fit with coalescence. The default recommendation for most projects.heap_5.c— like heap_4 but supports multiple memory regions (e.g., internal SRAM + external SDRAM).
Step 6 — Run the demo
Start with the bundled main_blinky example. A minimal main.c looks like this:
#include "FreeRTOS.h"
#include "task.h"
static void vBlinkTask(void *pvParameters)
{
for (;;) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
vTaskDelay(pdMS_TO_TICKS(500));
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
xTaskCreate(vBlinkTask, "Blink", 256, NULL, 1, NULL);
vTaskStartScheduler(); // never returns
for (;;) { /* heap exhausted */ }
}
Once your LED blinks and a second task prints on schedule, you have a working port and can start building on top.
Common pitfalls
- Wrong port directory. Cortex-M4 with FPU =
portable/GCC/ARM_CM4F(orRVDS/ARM_CM4Fin Keil). Without FPU =ARM_CM3. Mixing these gives weird hard faults atvTaskStartScheduler(). - SysTick / PendSV / SVC not mapped. The kernel hijacks these three vectors. If your startup file declares
SysTick_Handler, you must rename it (or use the#definetrick shown above). - Stack overflow. Default 128-word task stacks are tight. Enable
configCHECK_FOR_STACK_OVERFLOW = 2during development and providevApplicationStackOverflowHook(). - Interrupt priority bug. Calling
...FromISRAPIs from an ISR with priority numerically lower thanconfigMAX_SYSCALL_INTERRUPT_PRIORITYis undefined behaviour. See the NVIC post for the why. - Heap exhaustion silent. Set
configUSE_MALLOC_FAILED_HOOK = 1and implementvApplicationMallocFailedHook()— it'll save you hours.