跳至内容
FreeRTOS内核移植02

FreeRTOS内核移植02

2026-06-22

1. 工程准备

  • STM32F401CCU6(小黑板)

  • 系统移植:参考移植文章

这里主要记录很多细节的原理,大多应该都能对应到FreeRTOSConfig.h的配置项上。

多数内容在官方文档上应该都能找到出处:https://freertos.org/zh-cn-cmn-s/Documentation/00-Overview

以前用RT-Thread,这些RTOS基本功能上其实都差不多,学完之后自己认为的一些主要区别如下:

FreeRTOSRT-Thread备注
调度方式抢占优先级
固定时间片
抢占优先级+时间片轮询
支持设定每个任务时间片大小
设备框架没有,基本都可以直接操作裸机接口通用框架

2. 任务处理

2.1 创建

基于个人习惯 这里封装一个任务列表的数组,程序运行后直接调用FreeRTOS_Start启动所有任务。

void FreeRTOS_Start(void)
{
    bsp_init();

    // 粗略计算空间(实际上需要比他小一点 因为还有TCB空间没算进来)
    int need_heap_count = 0;
    for (uint8_t i = 0; i < sizeof(task_list) / sizeof(task_list[0]); i++)
    {
        need_heap_count += task_list[i].uxStackDepth;
    }
    if (need_heap_count * 4 > configTOTAL_HEAP_SIZE)
    {
        printf("heap size is not enough. \r\n");
    }

    // 创建任务
    for (uint8_t i = 0; i < sizeof(task_list) / sizeof(task_list[0]); i++)
    {
        BaseType_t ret = xTaskCreate(task_list[i].pxTaskCode,
                                     task_list[i].pcName,
                                     task_list[i].uxStackDepth,
                                     task_list[i].pvParameters,
                                     task_list[i].uxPriority,
                                     task_list[i].pxCreatedTask);
        if (ret != pdPASS)
        {
            printf("Task [%s] Create Failed! Error Code: %d \r\n", task_list[i].pcName, (int)ret);
        }
    }

    // 启动调度器
    vTaskStartScheduler();
}

2.2 删除

配置包含以下宏定义才会使能使能删除任务的接口,vTaskDelete(),需要传入任务句柄。

#define INCLUDE_vTaskDelete                             1       // 1: 包含删除任务的API

注意点:

  1. 调用删除接口后,会立刻将其从调度队列中删除,但是如果当前已经在执行了,会执行完本轮;
  2. 删除任务后,并不会立刻释放该任务的资源,会在空闲之后,在空闲任务中释放资源;
  3. 如果在任务中删除任务,参数传空,则会删除自身;
  4. 如果需要删除自身,删除之后一定不要再有逻辑,不一定能执行到。

2.3 挂起和恢复

配置包含以下宏定义才会使能使能挂起和恢复任务的接口,vTaskSuspendvTaskResume(),需要传入任务句柄。

#define INCLUDE_vTaskSuspend                            1       // 1: 包含挂起/恢复任务的API

3. 临界区处理

临界区处理有两种

  1. 关闭中断(常用)
  2. 停止调度

关闭中断的临界区处理如下

// 进入临界区
taskENTER_CRITICAL();

/* 临界区处理 */

// 退出临界区
taskEXIT_CRITICAL();

其中调用顺序如下

#define taskENTER_CRITICAL()                 portENTER_CRITICAL()
#define portENTER_CRITICAL()                      vPortEnterCritical()

void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
}

#define portDISABLE_INTERRUPTS()                  vPortRaiseBASEPRI()

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
    uint32_t ulNewBASEPRI;

    __asm volatile
    (
        "   mov %0, %1                                              \n" \
        "   msr basepri, %0                                         \n" \
        "   isb                                                     \n" \
        "   dsb                                                     \n" \
        : "=r" ( ulNewBASEPRI ) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
    );
}

最终会使用到configMAX_SYSCALL_INTERRUPT_PRIORITY关闭中断,因此,需要定义如下宏定义。

#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5       // 允许调用FreeRTOS 进入临界区时关断的最高中断优先级 (0表示所有中断都可调用)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY            (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << 4)      // 寄存器低4位没用

主要可以关闭freertos调用的时钟定时器,这里也就是滴答定时器和调度PendSV定时器,但是这两个定时器的优先级在哪设置的呢。

BaseType_t xPortStartScheduler( void )
{
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
}

// #define portMIN_INTERRUPT_PRIORITY            ( 255UL )
// #define portNVIC_PENDSV_PRI                   ( ( ( uint32_t ) portMIN_INTERRUPT_PRIORITY ) << 16UL )
// #define portNVIC_SYSTICK_PRI                  ( ( ( uint32_t ) portMIN_INTERRUPT_PRIORITY ) << 24UL )

// 把源码中上面那段注释掉 重新写一个(当然默认的255也行 实际上会生效为最低优先级 但是就无法配置了)
#define portNVIC_PENDSV_PRI                   ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI				  ( ( ( uint32_t ) configKERNEL_INTERRUPT_PRIORITY ) << 24UL )

// 然后在配置文件中添加配置项
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15      // 内核级中断(如PendSV/SysTick)优先级 (Cortex-M4中0为最高)
#define configKERNEL_INTERRUPT_PRIORITY                 (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << 4)          // 寄存器低4位没用

如果使用cubemx生成的工程 这段就不用自己改了,默认就是这样的

**结论:**临界区会关闭中断。为了防止需要正常执行的任务停止,所有底层或者实时控制的中断任务,正常是需要比关断的中断的优先级高的。这里configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY可以成为rtos应用和正常应用的分界线。

另一种方式是关闭调度,这个不会影响其他的逻辑,只是在临界区处理过程中不会切换到其他任务执行。

// 停止调度
vTaskSuspendAll();

/* 临界区处理 */

// 恢复调度
vTaskResumeAll();

4. 任务状态

4.1 优先级操作

设置和修改优先级

// 获取任务优先级
UBaseType_t pri = uxTaskPriorityGet(led_task_handle);
printf("led task pri = %d \r\n", pri);

// 设置任务优先级
vTaskPrioritySet(led_task_handle, 5);

4.2 任务数量统计

获取一共有多少个任务

// 获取任务数量(注意有个空闲任务)
UBaseType_t num = uxTaskGetNumberOfTasks();
printf("task num = %d \r\n", num);

4.3 任务状态和剩余空间(*)

获取任务状态,运行、就绪、阻塞等。

// 获取任务状态
eTaskState state1 = eTaskGetState(led_task_handle);
printf("led task state = %d \r\n", (int)state1);

eTaskState state2 = eTaskGetState(printf_task_handle);
printf("printf task state = %d \r\n", (int)state2);

获取任务未用到的最小空间,一般不要让这个值小于总空间的20%-30%。也就是空间利用率大约在70%比较好。

UBaseType_t space1 = uxTaskGetStackHighWaterMark(led_task_handle);
UBaseType_t space2 = uxTaskGetStackHighWaterMark(key_task_handle);
UBaseType_t space3 = uxTaskGetStackHighWaterMark(printf_task_handle);

printf("unuse space = %d  %d  %d \r\n", (int)space1, (int)space2, (int)space3);

5. 配套组件

5.1 队列

使能队列的使用几乎不用配置,只要支持动态创建资源,默认就支持队列。

// 长度10 每个元素大小1字节
queue = xQueueCreate(10, 1);
if (queue == NULL)
{
    printf("queue create failed \r\n");
}

// 填充一个数据 超时时间1000个时间片
uint8_t data[1] = {10};
xQueueSend(queue, data, 1000);

// 取出一个数据  超时时间1000个时间片
uint8_t rData[1];
BaseType_t res = xQueueReceive(queue, rData, 1000);
if (res == pdTRUE)
{
    printf("0x%x \r\n", rData[0]);
}
else
{
    printf("timeout \r\n");
}

但是,如果在中断中调用添加元素动作(后面的信号量也是一样的),需要使用中断接口。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 判断是不是串口1 中断
    if (huart->Instance == USART1)
    {
        // 将接收到的单字节数据推入 RTOS 队列  在中断中必须使用带 FromISR 后缀的 API
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        xQueueSendFromISR(uart1_rx_queue, (void *)&uart1_temp_recv, &xHigherPriorityTaskWoken);

        // 重新开启中断
        HAL_UART_Receive_IT(&huart1, &uart1_temp_recv, 1); // 开启中断

        // 如果入队操作唤醒了更高优先级的任务,请求上下文切换
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

另外,比如上面这个例子,串口中断优先级必须比RTOS管理的中断优先级低,也就是configMAX_SYSCALL_INTERRUPT_PRIORITY这个中断优先级需要能真的能控制串口中断

5.2 信号量

信号量实现挺有意思,实际上就是个队列,只不过只使用了长度参数,不在乎队列具体数据。

从数据类型定义可以看出来 其实用的就是队列。

接口也比较简单。

QueueHandle_t sem = NULL;

// 初始化
sem = xSemaphoreCreateBinary();

// 释放信号量
xSemaphoreGive(sem);

// 获取信号量
BaseType_t xEntryTimeSet = xSemaphoreTake(sem, 1000);
if (xEntryTimeSet == pdTRUE)
{
    printf("------ \r\n");
}
else
{
    printf("timeout \r\n");
}

也有计数信号量,也就是可以多次释放和获取的信号量,但是需要定义宏定义

#define configUSE_COUNTING_SEMAPHORES                   1       // 1: 启用计数信号量

信号量还可以用户处理资源争抢问题,比如,多个地方都会用到打印,实际上都是调用的串口发送,如果多个线程同时发送,发出去的数据有可能会被打断从而乱掉。

因此,可以给发送函数的前后添加一个信号量获取和释放,保证不会被打断。

但是就又有另一个问题了,优先级反转。

5.3 互斥量

解决信号量导致的优先级反转问题,只需要修改为互斥量即可,数据类型定义是一样一样的。

配置宏定义

#define configUSE_MUTEXES                               1       // 1: 启用互斥量 (支持优先级继承)

用法

QueueHandle_t sem = NULL;

// 二值信号量
sem = xSemaphoreCreateBinary();

// 计数信号量(最大计数值 初始计数值)
sem = xSemaphoreCreateCounting(10, 0);

// 互斥信号量
sem = xSemaphoreCreateMutex();

5.4 队列集

为了解决每个队列处理都需要一个任务等待处理的问题,可以使用一个任务同时等待多个队列到来数据。

比如一个任务,同时接收串口命令和CAN命令等。

其实也可以管理信号量,只是实际上从来没有这种需求

配置宏

\#define configUSE_QUEUE_SETS               1    // 1: 启用队列集 (允许同时阻塞等待多个队列/信号量)

用法

// 创建队列集(管理2个队列)
QueueSetHandle_t queue_set = xQueueCreateSet(2);
// 需要管理的队列添加到队列集
xQueueAddToSet(key_queue, queue_set);
xQueueAddToSet(uart1_rx_queue, queue_set);
    
    
// 任务中同时等待两个队列 哪个等到了处理哪个
uint8_t data[1];
QueueSetMemberHandle_t queue_handler = xQueueSelectFromSet(queue_set, 0xFFFFFFFF);
if (queue_handler == key_queue)
{
    xQueueReceive(key_queue, data, 0);
    printf("key data = %x \r\n", data[0]);
}
else if (queue_handler == uart1_rx_queue)
{
    xQueueReceive(uart1_rx_queue, data, 0);
    printf("uart data = %x \r\n", data[0]);
}

5.5 事件标志组

用于同时监测不同事件或者多个事件触发。

// 创建事件标志组
EventGroupHandle_t event_group;
event_group = xEventGroupCreate();

// 标记事件触发
xEventGroupSetBits(event_group, 1 << 0);
xEventGroupSetBits(event_group, 1 << 1);

// 等待事件组状态
// 等待事件标志组 0x03一共两个bit 获取到之后不清除 等待所有事件都必须成立 等待时间很长
xEventGroupWaitBits(event_group, 0x03, pdFALSE, pdTRUE, 0xFFFFFFFF);

// 手动清除
xEventGroupClearBits(event_group, 0x01);

5.6 直接任务通知

新东西,以前没听说过。

基本上前面的东西都能用这个替代,目测可读性其实不如前面的,但是性能好一点。

5.7 软件定时器

宏定义配置

注意这里定时器其实也是有个任务的,如果优先级过低有可能任务多的时候会影响定时器准确度。

// 软件定时器
#define configUSE_TIMERS                                1       // 1: 启用软件定时器功能
#define configTIMER_TASK_PRIORITY                       ( configMAX_PRIORITIES - 1U ) // 定时器服务任务的优先级 (设为最高)
#define configTIMER_TASK_STACK_DEPTH                    configMINIMAL_STACK_SIZE // 定时器服务任务的栈大小 (与空闲任务相同)
#define configTIMER_QUEUE_LENGTH                        10U     // 定时器命令队列长度 (最多可缓存10条定时器操作命令)

基础用法,其他接口参考官网文档,比较简单。

包括定时器的启动、停止、设置参数等接口。

timer1 = xTimerCreate("timer1", 1000, pdTRUE, NULL, timer1_callback);
xTimerStart(timer1, 0);

void timer1_callback(TimerHandle_t xTimer)
{
    printf("--------------- \r\n");
}

6. 低功耗模式

目前工业品从来没用过,用到了再看吧。

7. 内存管理解析

有几种选择:

  1. 在移植的时候,有5个文件选择,一半默认选择了第四种;
  2. 移植完成后,可以通过配置选择静态或者动态创建,一般动态创建用的多;

五种选择区别如下:

  • heap_1—— 最简单,不允许释放内存。
  • heap_2—— 允许释放内存,但不会合并相邻的空闲块。
  • heap_3—— 简单包装了标准 malloc() 和 free(),以保证线程安全。
  • heap_4—— 合并相邻的空闲块以避免碎片化。包含绝对地址放置选项。
  • heap_5—— 如同 heap_4,能够跨越多个不相邻内存区域的堆。

一般默认选第四个,第一个也有大量用的,别的用的不到。

查看内容状态有两个可能用到的API

// 查看当前剩余的总内存空间
xPortGetFreeHeapSize();

// 查看历史最低剩余空间
xPortGetMinimumEverFreeHeapSize();

8. 配置文件

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/******************************************************************************/
/* 系统基础配置                      . ****************************************/
/******************************************************************************/

// 基础调度器配置
#define configUSE_PREEMPTION                            1       // 1: 启用抢占式调度 (强烈建议)
#define configUSE_TIME_SLICING                          1       // 1: 启用时间片轮转
#define configUSE_PORT_OPTIMISED_TASK_SELECTION         1       // 1: 使用硬件CLZ指令优化任务选择 (Cortex-M4支持)

// 系统时钟与节拍配置
#define configCPU_CLOCK_HZ                              (84000000UL) // STM32F401 主频 (请根据实际时钟树调整)
#define configTICK_RATE_HZ                              ((TickType_t)1000) // 系统心跳 1ms

// 任务与内存配置 (针对64KB RAM的F401)
#define configMAX_PRIORITIES                            (10)     // 最大优先级数 (0~9)
#define configMAX_TASK_NAME_LEN                         (16)    // 任务名最大长度
#define configMINIMAL_STACK_SIZE                        (128)   // 空闲任务栈大小 (单位:字, 即512字节)
#define configSUPPORT_STATIC_ALLOCATION                 0       // 1: 允许静态创建任务 (需手动提供内存)
#define configSUPPORT_DYNAMIC_ALLOCATION                1       // 1: 允许动态创建任务 (从FreeRTOS堆中分配)
#define configTOTAL_HEAP_SIZE                           (4096)  // FreeRTOS内部堆大小 (分配4KB)
#define configAPPLICATION_ALLOCATED_HEAP                0       // 0: 由FreeRTOS内部自动分配堆内存 (若为1则需用户手动定义ucHeap数组)
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP       0       // 0: 任务栈从主堆(configTOTAL_HEAP_SIZE)中分配
#define configUSE_MINI_LIST_ITEM                        0       // 0: 使用标准的列表项结构体 (1: 使用精简版以节省RAM)

// 钩子函数与调试
#define configUSE_IDLE_HOOK                             0       // 0: 关闭空闲任务钩子函数
#define configUSE_TICK_HOOK                             0       // 0: 关闭Tick心跳钩子函数
#define configCHECK_FOR_STACK_OVERFLOW                  0       // 0: 关闭栈溢出检测 (设为1或2时需实现vApplicationStackOverflowHook)
#define configUSE_MALLOC_FAILED_HOOK                    0       // 0: 关闭内存分配失败钩子函数
#define configUSE_DAEMON_TASK_STARTUP_HOOK              0       // 0: 关闭定时器服务任务启动钩子函数
#define configIDLE_SHOULD_YIELD                         1       // 1: 空闲任务让出CPU给同优先级的用户任务

// 高级配置
#define configUSE_TICKLESS_IDLE                         0       // 1: 启用Tickless低功耗模式 (需配合底层硬件实现)
#define configTICK_TYPE_WIDTH_IN_BITS                   TICK_TYPE_WIDTH_32_BITS // Tick计数器类型宽度 (32位MCU推荐32位)
#define configTASK_NOTIFICATION_ARRAY_ENTRIES           1U      // 每个任务的通知数组条目数 (1表示兼容旧版单通知机制)
#define configQUEUE_REGISTRY_SIZE                       0U      // 队列/信号量注册表大小 (0: 禁用,>0时用于调试工具查看)
#define configENABLE_BACKWARD_COMPATIBILITY             1       // 1: 启用旧版API名称兼容 (如vTaskDelayUntil替代vTaskDelayUntil等)
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS         0       // 0: 每个任务的本地存储指针数量 (0: 禁用)
#define configSTACK_DEPTH_TYPE                          size_t  // 任务栈深度变量的数据类型 (使用size_t防止大栈溢出)
#define configMESSAGE_BUFFER_LENGTH_TYPE                size_t  // 消息缓冲区长度变量的数据类型 (使用size_t支持更大消息)
#define configUSE_NEWLIB_REENTRANT                      0       // 0: 不使用Newlib重入结构体 (若使用printf等C库函数且有多任务访问,建议设为1)

/******************************************************************************/
/* 功能选项配置                      . ****************************************/
/******************************************************************************/

// 软件定时器
#define configUSE_TIMERS                                1       // 1: 启用软件定时器功能
#define configTIMER_TASK_PRIORITY                       ( configMAX_PRIORITIES - 1U ) // 定时器服务任务的优先级 (设为最高)
#define configTIMER_TASK_STACK_DEPTH                    configMINIMAL_STACK_SIZE // 定时器服务任务的栈大小 (与空闲任务相同)
#define configTIMER_QUEUE_LENGTH                        10U     // 定时器命令队列长度 (最多可缓存10条定时器操作命令)

// 组件选择配置
#define configUSE_TASK_NOTIFICATIONS                    0       // 1: 启用任务通知机制 (轻量级,可替代二值/计数信号量等)
#define configUSE_MUTEXES                               0       // 1: 启用互斥量 (支持优先级继承)
#define configUSE_RECURSIVE_MUTEXES                     0       // 1: 启用递归互斥量 (允许同一任务多次获取)
#define configUSE_COUNTING_SEMAPHORES                   0       // 1: 启用计数信号量
#define configUSE_QUEUE_SETS                            0       // 1: 启用队列集 (允许同时阻塞等待多个队列/信号量)
#define configUSE_APPLICATION_TASK_TAG                  0       // 1: 启用任务标签功能 (可为任务附加自定义上下文指针)
#define configUSE_STREAM_BUFFERS                        0       // 1: 启动连续字节流

// 接口支持 (按需开启,节省Flash)
#define INCLUDE_vTaskPrioritySet                        1       // 1: 包含运行时修改任务优先级的API
#define INCLUDE_uxTaskPriorityGet                       1       // 1: 包含获取任务优先级的API
#define INCLUDE_vTaskDelete                             1       // 1: 包含删除任务的API
#define INCLUDE_vTaskSuspend                            1       // 1: 包含挂起/恢复任务的API
#define INCLUDE_vTaskDelayUntil                         1       // 1: 包含绝对延时API (用于精确周期性任务)
#define INCLUDE_vTaskDelay                              1       // 1: 包含相对延时API
#define INCLUDE_xTaskGetSchedulerState                  1       // 1: 包含获取调度器当前状态的API
#define INCLUDE_xTaskGetCurrentTaskHandle               1       // 1: 包含获取当前正在运行任务句柄的API
#define INCLUDE_uxTaskGetStackHighWaterMark             1       // 1: 包含获取任务栈剩余最小水位的API (用于栈大小调优)
#define INCLUDE_xTaskGetIdleTaskHandle                  1       // 1: 包含获取空闲任务句柄的API
#define INCLUDE_eTaskGetState                           1       // 1: 包含获取任务当前状态的API
#define INCLUDE_xTimerPendFunctionCall                  0       // 1: 包含从ISR中挂起函数调用到定时器任务的API
#define INCLUDE_xTaskAbortDelay                         1       // 1: 包含强制中止任务延时状态的API
#define INCLUDE_xTaskGetHandle                          1       // 1: 包含通过任务名获取任务句柄的API
#define INCLUDE_xTaskResumeFromISR                      1       // 1: 包含在中断服务函数(ISR)中恢复任务的API

/******************************************************************************/
/* 特殊配置                          . ****************************************/
/******************************************************************************/

// 中断行为配置
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY         15      // 内核级中断(如PendSV/SysTick)优先级 (Cortex-M4中0为最高)
#define configKERNEL_INTERRUPT_PRIORITY                 (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << 4)          // 寄存器低4位没用
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    5       // 允许调用FreeRTOS 进入临界区时关断的最高中断优先级 (0表示所有中断都可调用)
#define configMAX_SYSCALL_INTERRUPT_PRIORITY            (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << 4)     // 寄存器低4位没用
#define configMAX_API_CALL_INTERRUPT_PRIORITY           configMAX_SYSCALL_INTERRUPT_PRIORITY    // configMAX_SYSCALL_INTERRUPT_PRIORITY 的别名 (向后兼容)

// 运行状态统计配置
#define configGENERATE_RUN_TIME_STATS                   0       // 0: 关闭任务运行时间统计 (开启需额外配置时钟源)
#define configUSE_TRACE_FACILITY                        0       // 0: 关闭Trace调试宏 (开启后支持vTaskList等可视化调试函数)
#define configUSE_STATS_FORMATTING_FUNCTIONS            0       // 0: 关闭统计信息的字符串格式化函数
#define configKERNEL_PROVIDED_STATIC_MEMORY             1       // 1: 允许内核为空闲任务和定时器任务自动提供静态内存

/******************************************************************************/
/* 中断接口定义                      . ****************************************/
/******************************************************************************/

// 将FreeRTOS内部处理函数重定向到CMSIS标准中断名称
#define xPortPendSVHandler                              PendSV_Handler    // 可挂起系统服务 (用于任务上下文切换)
#define vPortSVCHandler                                 SVC_Handler       // 系统服务调用 (用于启动调度器等)
#define xPortSysTickHandler                             SysTick_Handler   // 系统心跳定时器 (用于产生系统节拍)

#endif /* FREERTOS_CONFIG_H */