【Stm32f1实时操作系统移植】FreeRTOS移植实战,保姆级小白教程
文章目录
O、说在前面的话
之前的移植流程都忘记差不多了,尽量留存博客笔记确实可以方便自己回忆。
平台:keil5-MDK、Win11电脑、STM32F103C8T6最小系统板
开发环境未配置的可以去看keil5安装的博客
一、名词解释(简单版)
1.裸机
什么是裸机?
裸机是指没有操作系统的单片机程序。程序直接在硬件上运行,所有代码都在一个 main 函数的大循环(Super Loop)中执行,如下代码块所示。
int main(void)
{
/* 初始化 */
SystemInit();
GPIO_Init();
USART_Init();
Timer_Init();
/* 超级循环 */
while(1)
{
Key_Scan(); // 扫描按键
LED_Process(); // 处理LED
UART_Process(); // 处理串口数据
Sensor_Read(); // 读取传感器
delay_ms(10); // 延时
}
}
裸机的特点
简单直接:代码逻辑清晰,容易理解和调试
资源占用极少:几乎没有额外开销,适合资源受限的MCU(如51、AVR)
实时性不可控:如果一个函数执行时间过长,其他任务就会被阻塞
任务间耦合度高:所有功能在一个循环里,牵一发而动全身
不适合复杂系统:当任务数超过5~8个时,代码会变得难以维护
裸机的适用场景
非常简单的小项目(如单LED闪烁、按键控制)
资源极度受限的芯片(如1KB RAM以下的MCU)
对实时性要求不高、任务少的场合
2.操作系统(RTOS)
什么是RTOS
RTOS(Real-Time Operating System)是一种能够保证任务在确定时间内完成的操作系统。它不是Linux那种“分时系统”,而是“实时系统”——任务的响应时间是可预测的。
RTOS的核心概念:
| 概念 | 说明 | 类别 |
|---|---|---|
| 任务(Task) | 独立的执行单元,每个任务有自己的栈和优先级 | 就像公司里的不同部门 |
| 调度器(Scheduler) | 决定哪个任务在什么时候运行 | 像项目经理安排工作 |
| 优先级(Priority) | 任务的紧急程度,数字越大优先级越高 | 像VIP客户优先服务 |
| 延时(Delay) | 任务主动放弃CPU,进入休眠 | 像员工请假休息 |
| 阻塞(Block) | 任务等待某个条件(如队列、信号量) | 像等电梯 |
| 上下文切换 | 保存当前任务状态,切换到另一个任务 | 像切换工作台 |
RTOS的典型结构
void LedTask(void *pvParameters)
{
for(;;)
{
LED_Toggle();
vTaskDelay(500); // 主动让出CPU,500ms后再回来
}
}
void UartTask(void *pvParameters)
{
for(;;)
{
xQueueReceive(uart_queue, &data, portMAX_DELAY); // 阻塞等待数据
Process_Data(data);
}
}
int main(void)
{
xTaskCreate(LedTask, "LED", 128, NULL, 2, NULL);
xTaskCreate(UartTask, "UART", 256, NULL, 1, NULL);
vTaskStartScheduler();
while(1);
}
RTOS的特点
任务独立:每个任务写成一个函数,互不干扰
实时性强:高优先级任务能在确定时间内得到响应
资源开销适中:通常需要2~10KB RAM,几百字节到几KB Flash
调试相对复杂:需要理解任务调度、优先级反转等概念
适合复杂系统:当任务超过5个时,RTOS的优势明显
RTOS的适用场景
多个任务需要“同时”运行的场景
有严格时间要求的控制系统(如无人机、机器人)
通信协议栈较多的物联网设备
需要模块化开发的团队项目
参考链接: 完整教程:STM32之FreeRTOS移植(重点)
二、移植流程
为方便小白理解,从新建工程开始移植FreeRTOS实时操作系统。
1.使用keil工具新建工程
如图2.1.1所示,打开Keil,点击project选择 new project 新建工程。


如图2.1.3是创建完成Keil工具界面,2.1.4是新建工程自动生成工程文件夹的结构。


2.规范工程文件夹并拷贝文件
文件夹结构这样不够完整和清晰,我们修改下文件夹结构,新建文件夹并规范化命名如图2.2.1。

这里再对自然生成的和手动添加的文件夹做个统一介绍:
| 文件名 | 描述 |
|---|---|
| CORE | 内核文件 |
| STARTUP | 根据主控芯片容量选择的启动文件 startup_stm32f10x_md.s |
| Library | 标准库函数 |
| System | 串口usart、延时函数等文件 |
| BSP | 板级外设驱动文件,如板载LED、软件IIC、SPI驱动(板子简单可和Hardware合并不细分) |
| Hardware | 外设驱动文件,OLED、LED、蜂鸣器、传感器等外设 |
| FreeRTOS | FreeRTOS核心文件 |
| DebugConfig | 存放调试配置缓存。每次进入调试模式(Debug)时,Keil 会生成临时的调试脚本或配置 |
| Listings | 存放编译列表文件。包括 .lst 汇编列表、.map 内存映射文件。排查 FreeRTOS 的 HardFault、看内存占用时,这里的 .map 文件非常有用 |
| Object | 存放最终编译产物。包括 .o 目标文件、.axf 可执行文件(调试用)、.hex / .bin(烧录固件),最终烧录到 STM32 的文件就在这里 |
文件夹创建好之后,拷贝文件到文件夹,有变动的文件夹文件如下。
①.CORE:

②.STARTUP:

这里我保留了全部.s文件,实际上只需要用到主控芯片对应的一个.s文件就可以了,都保留的话移植到其他同系列不同规格芯片的时候在keil里修改这个文件夹里添加的.s文件即可。如果想要极致压缩工程大小可以只保留需要的.s文件。
③.Library:

标准库函数全部拷贝过来即可
④.System:

一点简易的串口和延时函数
⑤.User:

⑥.FreeRTOS:
这里着重介绍这个文件夹也就是FreeRTOS核心文件的移植和配置。解压完官方文件如图2.2.7。

如图2.2.8点击官方文件FreeRTOS->Source,这里面的include文件夹直接拷贝到我们工程下的FreeRTOS文件夹。同时绿框的3个.c文件,list.c、queue.c、tasks.c文件也需要拷贝。


此外还需要官方portable中MemMang文件夹的一个内存管理实现.c文件,我们直接选择其中的heap_4.c文件拷贝。最后得到图2.2.10所示内容。

到这里还不够,我后来编译发现报错缺少一个FreeRTOSConfig.h头文件。
我们在官方文件夹中找到demo例程,如图2.2.11找到对应芯片和开发工具的文件夹,在里面找到FreeRTOSConfig.h文件拷贝到自己工程的FreeRTOS文件夹下。

最终工程的FreeRTOS文件夹文件内容如图2.2.12所示。
FreeRTOS:

二级子目录include:

二级子目录portable:

完成上述文件拷贝整理流程后,接下来到keil工具内配置好一切相关设置。
3.Keil工具配置
工具配置如果配置的有问题也会导致移植报错、编译报错等问题,所以我还是贴出了较为详细的流程,熟练工具使用的可以自行跳过
如图2.3.1,打开keil点击蓝框的管理项目工程文件工具,然后在弹出的窗口点击绿框按钮,新建、修改分支名称,并点击黄框按钮在对应的分支项目内添加对应的文件,这里项目名称和文件夹的名称命名一样方便查找和管理。最终红框将全部.c文件、部分.h文件添加到工程项目内。

如图2.3.2红框是已经配置好的工程项目结构。随后选择绿框的魔法棒工具,在弹出的Target窗口,ARM Compiler选择version 5。

如图2.3.3,在绿框c/c++选项窗口界面,Define输入USE_STDPERIPH_DRIVER宏定义,然后点include Paths右侧三点,给工程添加头文件路径,添加结果如图2.3.4所示。


自此工具配置完成。找AI随便生成一个main函数,创建两个任务一个定时点灭板载LED灯,另外一个任务发送数据。如图2.3.5所示,编译无报错。

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stm32f10x.h"
/* 任务句柄 */
TaskHandle_t led_task_handle = NULL;
TaskHandle_t print_task_handle = NULL;
/* 队列句柄 */
QueueHandle_t msg_queue = NULL;
/* 前向声明 */
void LedTask(void *pvParameters);
void PrintTask(void *pvParameters);
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); // 4位抢占优先级,0位子优先级
/* 硬件初始化 */
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
/* 创建队列:能存 5 个整数 */
msg_queue = xQueueCreate(5, sizeof(uint32_t));
/* 创建任务 */
xTaskCreate(LedTask, "LED", 128, NULL, 2, &led_task_handle);
xTaskCreate(PrintTask, "PRINT", 256, NULL, 1, &print_task_handle);
/* 启动调度器 */
vTaskStartScheduler();
/* 永远不会到这里 */
while(1);
}
/* 任务1:LED 闪烁,每秒发送一个消息到队列 */
void LedTask(void *pvParameters)
{
uint32_t count = 0;
for(;;)
{
/* 翻转 LED */
GPIOC->ODR ^= GPIO_Pin_13;
/* 发送计数值到队列 */
count++;
xQueueSend(msg_queue, &count, 0);
/* 延时 500ms */
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* 任务2:从队列接收消息并打印 */
void PrintTask(void *pvParameters)
{
uint32_t received_value;
for(;;)
{
/* 等待队列消息(无限等待) */
if(xQueueReceive(msg_queue, &received_value, portMAX_DELAY) == pdTRUE)
{
/* 收到消息后,模拟打印(这里用翻转另一个引脚代替) */
/* 在实际项目中,这里可以是串口输出 */
/* 做一个短延时,模拟处理时间 */
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
三、运行测试代码
编译无报错,随后通过烧录器下载程序到板子,发现LED灯并没有如代码预期实现的定时闪烁,我们进入Debug定位下什么原因。如图3.1.1,程序从main函数开始没问题,

如图3.1.2点击红框继续运行后,再点击绿框停止运行程序发现程序卡死在void HardFault_Handler(void),故障中断死循环中,所以没有执行后面的程序了,这是移植FreeRTOS操作系统过程中会遇到的一个经典问题。

问题原因分析:
核心问题:两个“同名”的函数,抢着干活
我们把问题简化一下:
FreeRTOS 提供了三个核心函数,它们负责任务的切换和系统心跳:
vPortSVCHandler:启动第一个任务
xPortPendSVHandler:执行任务切换
xPortSysTickHandler:提供系统时钟节拍
ST官方启动文件 里,为这三个异常定义了“弱”入口,名字分别是:
SVC_Handler、PendSV_Handler、SysTick_Handler
stm32f10x_it.c 文件里,你(或者库)重新定义了这三个函数,即使函数内容是空的。
当异常发生时,MCU会去哪里?
它会去向量表里找对应的函数名。向量表里写的是 SVC_Handler 这个名字。
现在,编译器面临一个选择:
链接器找到了 SVC_Handler(在 stm32f10x_it.c 里,是个空函数)
链接器也找到了 vPortSVCHandler(在 FreeRTOS 的 port.c 里,是真正的实现)
但问题是:向量表里只写了 SVC_Handler,它不认识 vPortSVCHandler。
所以,当第一个任务要启动时,CPU跳转到 SVC_Handler,结果发现那里是个空函数或者一个 while(1),系统立刻就进入死循环了(HardFault)。
解决方法1.注释原中断函数,重映射到操作系统的中断函数
注释掉原函数,并在FreeRTOSConfig.h中增加映射定义。
(1)如图3.1.3在stm32f10x_it.c中注释掉红框的三个函数SVC_Handler、PendSV_Handler、SysTick_Handler

(2)如图3.1.4在FreeRTOSConfig.h中末尾新增下列宏定义。
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

此时编译完烧录就可以发现LED灯闪烁,程序正常运行了。
解决方法2.修改向量表直接使用FreeRTOS的中断函数
如图3.1.5,在蓝框startup_stm32f10x_md.s向量表中找到红框(参照黄色字体)SVC_Handler、PendSV_Handler、SysTick_Handler三个函数

将
DCD SVC_Handler
DCD PendSV_Handler
DCD SysTick_Handler
修改为:
DCD vPortSVCHandler
DCD xPortPendSVHandler
DCD xPortSysTickHandler
并且在绿框位置(开头位置)添加:
EXTERN vPortSVCHandler
EXTERN xPortPendSVHandler
EXTERN xPortSysTickHandler
如上操作也可以上实现程序正常运行,此篇完结。
最后放上源码便于大家参考、调试、学习:
FreeRTOS移植STM32F103C8T6资料
源码分享: 百度网盘
提取码:MCYC
----来自一位不愿意透露姓名的百度用户的分享
有什么问题可以留言或者私信找我,我看到就会回,没回就是没看到。

其他参考资料/博客/文章:
参考链接: 超详细的FreeRTOS移植全教程——基于stm32
参考链接: 【FreeRTOS】调用vTaskStartScheduler立即进入HardFault_Handler问题解决
参考链接: Keil环境下STM32定位hardfault位置方法(addr2line工具)和遇到的情况
更新信息:
发布于:2026.9.30
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)