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.1
选择完工程文件保存位置和工程名后,如图2.1.2,选择使用的芯片,我这里用的是F103c8t6。

在这里插入图片描述

图2.1.2

如图2.1.3是创建完成Keil工具界面,2.1.4是新建工程自动生成工程文件夹的结构。
在这里插入图片描述

图 2.1.3

在这里插入图片描述

图 2.1.4

2.规范工程文件夹并拷贝文件

文件夹结构这样不够完整和清晰,我们修改下文件夹结构,新建文件夹并规范化命名如图2.2.1。
在这里插入图片描述

图 2.2.1
*这里面多出的**keilkilll.bat**也是新增进来的用于清理编译生成的文件的可执行脚本,编译生成的文件会占用很大的储存空间,此脚本可用于工程完成后清理空间,或在压缩传输工程前减小体积便于发送,一般写工程都会塞一个进来*

这里再对自然生成的和手动添加的文件夹做个统一介绍:

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

文件夹创建好之后,拷贝文件到文件夹,有变动的文件夹文件如下。
①.CORE:
在这里插入图片描述

图 2.2.2

②.STARTUP:
在这里插入图片描述

图 2.2.3

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

③.Library:
在这里插入图片描述

图 2.2.4

标准库函数全部拷贝过来即可
④.System:
在这里插入图片描述

图 2.2.5

一点简易的串口和延时函数
⑤.User:
在这里插入图片描述

图 2.2.6

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

图 2.2.7

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

图 2.2.8
随后还需要官方portable文件夹中的文件,需要根据开发环境(编译器相关文件)选择。如图2.2.9我们找到 FreeRTOS->Source->portable中的keil文件夹,里面提示我们看RVDS文件夹,在RVDS文件夹中找到主控芯片对应的文件也就是ARM_CM3文件夹,拷贝里面的文件==port.c、portmacro.h==到工程的portable文件夹中。

在这里插入图片描述

图 2.2.9

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

在这里插入图片描述

图 2.2.10

到这里还不够,我后来编译发现报错缺少一个FreeRTOSConfig.h头文件。

我们在官方文件夹中找到demo例程,如图2.2.11找到对应芯片和开发工具的文件夹,在里面找到FreeRTOSConfig.h文件拷贝到自己工程的FreeRTOS文件夹下。
在这里插入图片描述

图 2.2.11

最终工程的FreeRTOS文件夹文件内容如图2.2.12所示。
FreeRTOS:
在这里插入图片描述

图 2.2.12

二级子目录include:
在这里插入图片描述

图 2.2.12

二级子目录portable:
在这里插入图片描述

图 2.2.12

完成上述文件拷贝整理流程后,接下来到keil工具内配置好一切相关设置。

3.Keil工具配置

工具配置如果配置的有问题也会导致移植报错、编译报错等问题,所以我还是贴出了较为详细的流程,熟练工具使用的可以自行跳过

如图2.3.1,打开keil点击蓝框的管理项目工程文件工具,然后在弹出的窗口点击绿框按钮,新建、修改分支名称,并点击黄框按钮在对应的分支项目内添加对应的文件,这里项目名称和文件夹的名称命名一样方便查找和管理。最终红框将全部.c文件、部分.h文件添加到工程项目内。
在这里插入图片描述

图 2.3.1

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

图 2.3.2

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

图 2.3.3

在这里插入图片描述

图 2.3.4

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

图 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.1.

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

图 3.1.2

问题原因分析:
核心问题:两个“同名”的函数,抢着干活
我们把问题简化一下:
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
在这里插入图片描述

图 3.1.3

(2)如图3.1.4在FreeRTOSConfig.h中末尾新增下列宏定义。

#define vPortSVCHandler    SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

在这里插入图片描述

图 3.1.4

此时编译完烧录就可以发现LED灯闪烁,程序正常运行了。

解决方法2.修改向量表直接使用FreeRTOS的中断函数

如图3.1.5,在蓝框startup_stm32f10x_md.s向量表中找到红框(参照黄色字体)SVC_Handler、PendSV_Handler、SysTick_Handler三个函数
在这里插入图片描述

图 3.1.5
将
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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐