说明:基于STM32F103C8T6最小系统开发板
一、CubeMX创建STM32工程
1.基础配置
使能高速外部时钟HSE

Debug烧录调试选择Serial Wire,TimeBase Source把SysTick改为任一定时器(TIM2)
注:SysTick需要让给FreeRTOS作为心跳帧,TIM2作为HAL的时基

PC13选择输出模式,用板载PC13引脚LED进行验证

2.配置时钟树
将时钟树的HCLK改为72Mhz并确认OK


3.工程配置
Toolchain选择Cmake并勾选.c.h分离


最后点击GENERATE CODE创建完成
二、VSCode点灯
用VSCode打开刚才创建的STM32FreeRTOS_Temp文件夹,系统识别到项目点击"是",并点击Debug


然后移植自己的json通用模板

编写闪烁代码并编译,编译通过并烧录,点灯成功

三、FreeRTOS裁剪
FreeRTOS官网下载2024年版

FreeRTOS目录的FreeRTOS-Kernel 结构如下图所示,我们只到FreeRTOS-Kernel

将整个工程复制到STM32FreeRTOS_Temp工程
1.根目录下把非.c.h文件删除
2.portable文件夹只留下GCC和MemMang,其余均删除

GCC留下ARM_CM3,其余均删除
MenMang只留下heap4.c,其余均删除
3.include文件夹把非点.c.h文件夹删除
4.examples文件夹只保留FreeRTOSConFig.h
将STM32FreeRTOS_Temp\FreeRTOS\examples\template_configuration目录下的FreeRTOSConFig.h剪切到main.c同目录下,并删掉整个examples文件夹
最终FreeRTOS裁剪如下图所示

四、CmakeLists.txt添加添加.c文件和指定头文件路径
移植完FreeRTOS之后,如何将VScode添加.c.h文件和指定头文件路径?
VScode.c.h文件和指定头文件路径的添加只与以上两行代码有关
方法一:一个一个添加
cpp
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
User/BSP/led.c
User/BSP/key.c
User/APP/app.c
)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
${CMAKE_SOURCE_DIR}/User/BSP
${CMAKE_SOURCE_DIR}/User/APP
)
方法二(推荐):自动识别.c文件
添加file指令,指定头文件路径的方法照常一个一个添加
cpp
file(GLOB_RECURSE USER_SOURCES
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/FreeRTOS/*.c"
)
# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
${USER_SOURCES}
)
方法三: 自动识别头文件路径(未验证)
CubeMX 同样不会自动扫描你新建的头文件目录。
如果希望所有包含 .h 文件的子目录都自动加入头文件搜索路径,可以添加:
cpp
file(GLOB_RECURSE USER_HEADERS
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/User/*.h"
)
set(USER_INCLUDE_DIRS "")
foreach(HEADER ${USER_HEADERS})
get_filename_component(HEADER_DIR ${HEADER} DIRECTORY)
list(APPEND USER_INCLUDE_DIRS ${HEADER_DIR})
endforeach()
list(REMOVE_DUPLICATES USER_INCLUDE_DIRS)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
${USER_INCLUDE_DIRS}
)
我的最终添加效果如下图

cpp
file(GLOB_RECURSE FREERTOS_SOURCES
CONFIGURE_DEPENDS
"${CMAKE_SOURCE_DIR}/FreeRTOS/*.c"
)
# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
${FREERTOS_SOURCES}
)
# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
# Add user defined include paths
${CMAKE_SOURCE_DIR}/FreeRTOS/include
${CMAKE_SOURCE_DIR}/FreeRTOS/portable/GCC/ARM_CM3
)
五、修改FreeRTOSConfig.h配置
1.将configTICK_TYPE_WIDTH_IN_BITS宏定义从64修改为32
cpp
/*时间以"时钟节拍"(ticks)来衡量,即自实时操作系统内核启动以来,时钟中断执行的次数。
时钟计数保存在名为 TickType_t 的变量中。
configTICK_TYPE_WIDTH_IN_BITS 控制 TickType_t 的类型(从而决定其位宽):
将 configTICK_TYPE_WIDTH_IN_BITS 定义为 TICK_TYPE_WIDTH_16_BITS 时,TickType_t 将被定义(typedef)为无符号 16 位类型。
将 configTICK_TYPE_WIDTH_IN_BITS 定义为 TICK_TYPE_WIDTH_32_BITS 时,TickType_t 将被定义(typedef)为无符号 32 位类型。
将 configTICK_TYPE_WIDTH_IN_BITS 定义为 TICK_TYPE_WIDTH_64_BITS 时,TickType_t 将被定义(typedef)为无符号 64 位类型。*/
#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
修改到此处,编译已经0报错了,如下图

2.修改中断优先级修改前:

修改后:

3.修改系统时钟和心跳帧
| 配置 | 修改值 | 作用 |
|---|---|---|
| configCPU_CLOCK_HZ | 72000000 | CPU 时钟 |
| configTICK_RATE_HZ | 1000 | 系统时钟节拍(FreeRTOS心跳帧) |
4.修改栈溢出检查宏定义由2为0
cpp
#define configCHECK_FOR_STACK_OVERFLOW 0
六、SysTick、PendSV、SVC函数处理
这三个函数被OS系统复用了
注释stm32f1xx_it.c的SysTick、PendSV、SVC函数,然后FreeRTOSConfig.h的末尾处添加以下宏定义
cpp
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

七、创建任务编写代码
mian.c
添加头文件
cpp
/* USER CODE BEGIN Includes */
#include "FreeRTOS.h"
#include "task.h"
/* USER CODE END Includes */
在main函数之前编写两个Task任务
cpp
/* USER CODE BEGIN 0 */
volatile uint32_t Number1 = 0;
volatile uint32_t Number2 = 0;
void LED_Task1(void *argument)
{
while (1)
{
Number1++;
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void LED_Task2(void *argument)
{
while (1)
{
Number2++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/* USER CODE END 0 */
在初始化后创建任务
cpp
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
xTaskCreate(LED_Task1,"LED_Task1",128,NULL,1,NULL);// 创建 LED_Task1 任务
xTaskCreate(LED_Task2,"LED_Task2",128,NULL,1,NULL);// 创建 LED_Task2 任务
vTaskStartScheduler();// 启动 FreeRTOS 调度器
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)//正常情况下不会执行到这里
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
八、最终效果
最后Debug,两个变量正常记数,LED正常闪烁,自此STM32移植FreeRTOS完成

九、FreeRTOS相关知识补充
1.FreeRTOS任务数字越大优先级越高,最低优先级为0
cpp
/*
* configMAX_PRIORITIES 设置可用的任务优先级数量。
* 任务优先级范围为 0 到 (configMAX_PRIORITIES - 1),0 为最低优先级。
*/
#define configMAX_PRIORITIES 5
该宏定义的含义是任务的优先级可以在0、1、2、3、4设置
2.中断优先级NVIC分组
FreeRTOS需要把NVIC全部设置为抢占优先级,选择分组4

记忆方法:
分组4,2^4=16 抢抢抢抢 抢占优先级0-15,响应优先级0
分组3,2^3=8 抢抢抢响 抢占优先级0-7, 响应优先级0-1
分组2,2^2=4 抢抢响响 抢占优先级0-3, 响应优先级0-3
分组1,2^1=2 抢响响响 抢占优先级0-1, 响应优先级0-7
分组0,2^0=1 响响响响 抢占优先级0, 响应优先级0-15
3.相关优先级边界
cpp
#define configPRIO_BITS 4 //MCU为4位中断优先级
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //定义可以安全调用 FreeRTOS 中断 API 的最高抢占优先级边界
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 //定义可以安全调用 FreeRTOS 中断 API 的最高抢占优先级边界
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS)) //15<<4 = 0xF0
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS)) //5<<4 = 0x50
#define configMAX_API_CALL_INTERRUPT_PRIORITY configMAX_SYSCALL_INTERRUPT_PRIORITY
注:后两个带移位的宏是给 Cortex-M 底层使用的编码值
中断优先级分为NVIC中断优先级和FreeRTOS中断优先级,两套相互关联但不完全相同的机制
举例子
configPRIO_BITS = 4 表示MCU实现了4位NVIC优先级,意味着MCU有16 个可编码的优先级等级,FreeRTOS要求NVIC全部设置为抢占优先级,因此选择NVIC分组4,0-15均为抢占优先级。

在这16个可编码优先级的框架下,5为边界,5-15表示这个优先级范围,FreeRTOS的API函数才可以调用,0-4用于非OS中断,遵循数字越小优先级越高,这与任务优先级相反
4、实际工程里应该怎么分配中断优先级?
实例,假设 FreeRTOS 系统调用边界为 5
| 中断 | NVIC 优先级示例 | 是否允许 FromISR |
|---|---|---|
| 极低延迟的硬件采样中断 | 2 | 不允许 |
| 高速定时器中断 | 3 | 不允许 |
| USART1 接收中断 | 6 | 允许 |
| DMA1 通道中断 | 7 | 允许 |
| EXTI 按键中断 | 10 | 允许 |
| FreeRTOS SysTick | 15 | 内核专用 |
5.SysTick、PendSV、SVC 三个异常
| 异常 | 主要作用 |
|---|---|
| SysTick | 产生操作系统节拍 |
| PendSV | 执行任务上下文切换 |
| SVC | 用于启动第一个任务等内核操作 |
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
#define xPortSysTickHandler SysTick_Handler
关系:SysTick中断---SVC检查内核是否需要调度----PendSV切换任务
6.任务严格按照固定周期执行标准写法
cpp
TickType_t lastWakeTime = xTaskGetTickCount();
while (1)
{
LED_Task();
vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(10));
}
十一、附录
| 宏定义 | 推荐起点 | 作用 |
|---|---|---|
configUSE_PREEMPTION |
1 | 开启抢占式调度 |
configUSE_TIME_SLICING |
1 | 同优先级任务时间片轮转 |
configCPU_CLOCK_HZ |
SystemCoreClock |
CPU 时钟频率 |
configTICK_RATE_HZ |
1000 | 系统 Tick 频率 |
configMAX_PRIORITIES |
5 | 最大任务优先级数量 |
configMINIMAL_STACK_SIZE |
128 | 空闲任务最小栈深度 |
configTOTAL_HEAP_SIZE |
4096 | FreeRTOS 堆大小,字节 |
configMAX_TASK_NAME_LEN |
16 | 任务名称最大长度 |
configUSE_16_BIT_TICKS |
0 | 旧版配置中选择 32 位 Tick |
configUSE_MUTEXES |
1 | 启用互斥锁 |
configUSE_COUNTING_SEMAPHORES |
1 | 启用计数信号量 |
configUSE_TIMERS |
1 | 启用软件定时器 |
configTIMER_TASK_PRIORITY |
3 | 软件定时器服务任务优先级 |
configTIMER_QUEUE_LENGTH |
10 | 软件定时器命令队列长度 |
configTIMER_TASK_STACK_DEPTH |
256 | 定时器任务栈深度 |
configCHECK_FOR_STACK_OVERFLOW |
2 | 开启栈溢出检查 |
configUSE_MALLOC_FAILED_HOOK |
1 | 启用内存申请失败钩子 |
configUSE_IDLE_HOOK |
0 | 是否启用空闲任务钩子 |
configUSE_TICK_HOOK |
0 | 是否启用 Tick 钩子 |
configSUPPORT_DYNAMIC_ALLOCATION |
1 | 允许动态创建任务 |
configSUPPORT_STATIC_ALLOCATION |
0 | 是否允许静态创建任务 |