FreeRTOS内核移植02
1. 工程准备
STM32F401CCU6(小黑板)
系统移植:参考移植文章
这里主要记录很多细节的原理,大多应该都能对应到FreeRTOSConfig.h的配置项上。
多数内容在官方文档上应该都能找到出处:https://freertos.org/zh-cn-cmn-s/Documentation/00-Overview
以前用RT-Thread,这些RTOS基本功能上其实都差不多,学完之后自己认为的一些主要区别如下:
| FreeRTOS | RT-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
注意点:
- 调用删除接口后,会立刻将其从调度队列中删除,但是如果当前已经在执行了,会执行完本轮;
- 删除任务后,并不会立刻释放该任务的资源,会在空闲之后,在空闲任务中释放资源;
- 如果在任务中删除任务,参数传空,则会删除自身;
- 如果需要删除自身,删除之后一定不要再有逻辑,不一定能执行到。
2.3 挂起和恢复
配置包含以下宏定义才会使能使能挂起和恢复任务的接口,vTaskSuspend、vTaskResume(),需要传入任务句柄。
#define INCLUDE_vTaskSuspend 1 // 1: 包含挂起/恢复任务的API
3. 临界区处理
临界区处理有两种
- 关闭中断(常用)
- 停止调度
关闭中断的临界区处理如下
// 进入临界区
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. 内存管理解析
有几种选择:
- 在移植的时候,有5个文件选择,一半默认选择了第四种;
- 移植完成后,可以通过配置选择静态或者动态创建,一般动态创建用的多;
五种选择区别如下:
- 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 */