freeRTOS学习笔记
FreeRTOS介绍
官网:https://freertos.org/
-
任务调度:FreeRTOS通过任务调度器管理多个任务,支持不同优先级的任务,实现任务的有序执行。
-
任务通信和同步:提供了队列、信号量等机制,支持任务之间的通信和同步,确保数据的安全传递。
-
内存管理:提供简单的内存管理机制,适用于嵌入式环境,有效利用有限的内存资源。
-
定时器和中断处理:支持定时器功能,能够处理中断,提供了可靠的实时性能。
-
开发社区:拥有庞大的用户社区,开发者可以在社区中获取支持、解决问题,并分享经验。
-
可移植性:设计注重可移植性,可以轻松地移植到不同的硬件平台,提高了代码的重用性。
任务调度
-
抢占式调度:FreeRTOS任务优先级:优先级数值越大,优先级越高高优先级任务阻塞(延时或等待),会让其他就绪态中优先级最高的任务执行
-
时间片轮转:任务优先级相同的,按照时间片调度,每个任务执行一个时间片(一次系统时钟中断)任务执行不足一个时间片(或阻塞),会立即切换到其他任务执行
-
任务状态:
Ø 运行态:当任务实际执行时,它被称为处于运行状态。如果运行 RTOS 的处理器只有一个内核, 那么在任何给定时间内都只能有一个任务处于运行状态。注意在STM32中,同一时间仅一个任务处于运行态。
Ø 就绪态:准备就绪任务指那些能够执行(它们不处于阻塞或挂起状态), 但目前没有执行的任务, 因为同等或更高优先级的不同任务已经处于运行状态。
Ø 阻塞态:如果任务当前正在等待延时或外部事件,则该任务被认为处于阻塞状态。
Ø 挂起态:类似暂停,调用函数 vTaskSuspend() 进入挂起态,需要调用解挂函数vTaskResume()才可以进入就绪态。
只有就绪态可转变成运行态
命名规范:
数据类型重定义
portmacro.h
port+类型
portCHAR | char |
portSHORT | short |
portLONG | long |
portTickType | unsigned short int / unsigned int |
portBASE_TYPE | long |
变量名
类型缩写+变量名
cXX char
pcXX char *
xXXX portBASE_TYPE
函数名
返回值类型+文件名+功能名
vTaskPrioritySet()
xQueueReceive()
vSemaphoreCreateBinary()
宏
头文件名+宏名
前缀 | 对应头文件 |
---|---|
port | portable.h |
task | task.h |
pd | projdefs.h |
config | FreeRTOSConfig.h |
err | projdefs.h |
FreeRTOS移植
(1) 下载源码包
- 官网地址:https://www.freertos.org/
(2) source (基础的freeRTOS功能文件)
(3) portable (与内核硬件交互的文件 ) => 要根据芯片的内核类型修改不同的文件
(4) demo中的对应使用的芯片版本的config.h文件 => 选择对应的芯片型号STM32F103 选择
(5) include (添加所有的文件声明)
FreeRTOS的任务创建和删除案例
0. 准备
- 选择时钟时不选Systic
- stm32f1xx_it.c
- 添加头文件
#include "FreeRTOS.h" #include "task.h"
- 注释掉函数
SVC_Handler
和PendSV_Handler
- 在SysTick_Handler写
// /* USER CODE BEGIN PendSV_IRQn 1 */ extern void xPortSysTickHandler( void ); // /* USER CODE END PendSV_IRQn 1 */ // } /** * @brief This function handles System tick timer. */ void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE END SysTick_IRQn 0 */ /* USER CODE BEGIN SysTick_IRQn 1 */ if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xPortSysTickHandler(); } /* USER CODE END SysTick_IRQn 1 */ }
- 在
FreeRTOSConfig.h
中宏定义#define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler #define INCLUDE_xTaskGetSchedulerState 1
1. 动态创建
(1) 运行函数的本体 函数指针 task1
(2) 任务的名称 -> 随便起 只能用来看 没啥用
(3) 提供栈大小 -> 推荐使用的最小空间是128
(4) 优先级 -> 越大优先级越高
(5) 句柄 -> 填写进去一个指针 会对应的赋值 -> 是当前任务的唯一标识
(6) 函数指针的参数 -> 参数可以是任意类型的指针
例:
// 启动任务需要使用到的参数配置
// 1. 任务运行的函数指针
void Start_Task(void *pvParameters);
// 2. 任务名称
// 3. 任务堆栈大小
#define START_TASK_STACK_DEPTH 128
// 4. 传递进函数的参数 NULL
// 5. 任务的优先级
#define START_TASK_PRIORITY 1
// 6. 任务句柄 用于标识对应的任务
TaskHandle_t start_task;
...
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
START_TASK_STACK_DEPTH,
NULL,
START_TASK_PRIORITY,
&start_task);
printf("启动任务创建程序\n");
2. 动态创建任务的流程
(0)配置文件中 #define configSUPPORT_DYNAMIC_ALLOCATION 1
(1) main -> freeRTOS_start() -> 创建启动任务 -> 启动调度器 ( 相当于打开freeRTOS操作系统 )
(2) 一旦启动调度器 -> 之后的代码都不会按照顺序执行了 -> 会根据优先级抢占执行
(3) 在启动任务当中 -> 进入临界区 提高当前代码的优先级 -> 任意创建不同优先级的任务 而不会被抢占 -> 记得删除自身
(4) 之后的任务都会严格按照调度器优先级来运行
#include "FreeRTOS_demo.h"
// 创建3个task任务
// (1) 每0.5s翻转一次LED1
// (2) 每0.5s翻转一次LED2
// (3) 接收按键的信息 => 能够删除task1
// 启动任务需要使用到的参数配置
// 1. 任务运行的函数指针
void Start_Task(void *pvParameters);
// 2. 任务名称
// 3. 任务堆栈大小
#define START_TASK_STACK_DEPTH 128
// 4. 传递进函数的参数 NULL
// 5. 任务的优先级
#define START_TASK_PRIORITY 1
// 6. 任务句柄 用于标识对应的任务
TaskHandle_t start_task;
// task1的参数配置
void Task1(void *pvParameters);
#define TASK1_STACK_DEPTH 128
#define TASK1_PRIORITY 2
TaskHandle_t task1;
// task2的参数配置
void Task2(void *pvParameters);
#define TASK2_STACK_DEPTH 128
#define TASK2_PRIORITY 3
TaskHandle_t task2;
// task3的参数配置
void Task3(void *pvParameters);
#define TASK3_STACK_DEPTH 128
#define TASK3_PRIORITY 4
TaskHandle_t task3;
void Free_RTOS_Start(void)
{
/* 1. 创建第一个启动任务 作用是将3个task任务启动起来 */
xTaskCreate((TaskFunction_t)Start_Task,
(char *)"Start_Task",
START_TASK_STACK_DEPTH,
NULL,
START_TASK_PRIORITY,
&start_task);
printf("启动任务创建程序\n");
/* 2. 启动freeRTOS调度器 */
vTaskStartScheduler();
}
// 启动任务用于将3个task创建出来
void Start_Task(void *pvParameters)
{
/* 3. 已经进入到freeRTOS系统中 之后创建的任务task都会直接调度生效 */
// 3.0 进入临界区 -> 将之后的代码的优先级提升到最高 -> 一直持续到退出临界区
taskENTER_CRITICAL();
// 3.1 创建task1
xTaskCreate((TaskFunction_t)Task1,
(char *)"Task1",
TASK1_STACK_DEPTH,
NULL,
TASK1_PRIORITY,
&task1);
printf("task1创建完成\n");
// 3.2 创建task2
xTaskCreate((TaskFunction_t)Task2,
(char *)"Task2",
TASK2_STACK_DEPTH,
NULL,
TASK2_PRIORITY,
&task2);
printf("task2创建完成\n");
// 3.3 创建task3
xTaskCreate((TaskFunction_t)Task3,
(char *)"Task3",
TASK3_STACK_DEPTH,
NULL,
TASK3_PRIORITY,
&task3);
printf("task3创建完成\n");
/* 4. 启动任务在完成了任务创建之后 需要将自己删除 */
vTaskDelete(NULL);
/* 5. 退出临界区 */
taskEXIT_CRITICAL();
}
// 完成task1的具体任务
void Task1(void *pvParameters)
{
while (1)
{
printf("任务1正在运行\n");
// 每次调用使用 翻转LED1
LED_Toggle(LED1_Pin);
// 延迟500ms
vTaskDelay(500);
}
}
void Task2(void *pvParameters)
{
while (1)
{
printf("任务2正在运行\n");
// 每次调用使用 翻转LED2
LED_Toggle(LED2_Pin);
// 延迟500ms -> 不能使用hal库的延时
vTaskDelay(500);
}
}
void Task3(void *pvParameters)
{
while (1)
{
printf("任务3正在运行\n");
if (Key_Detect() == KEY1_PRESS)
{
// 按键1被按下
// 加健壮性判断
if (task1 != NULL)
{
// 删除task1
vTaskDelete(task1);
task1 = NULL;
}
}
// 高优先级的任务必须要有对应的阻塞 -> 分一部分时间片给低优先级
vTaskDelay(10);
}
}
3. 静态创建任务
创建静态任务的宏定义:
#define configSUPPORT_STATIC_ALLOCATION 1
/* 软件定时器相关定义 */
#define configUSE_TIMERS 1 /* 使能软件定时器, 默认: 0。为1时需要定义下面3个 */
#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) /* 软件定时器任务的优先级 */
#define configTIMER_QUEUE_LENGTH 5 /* 软件定时器命令队列的长度 */
#define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2) /* 软件定时器任务的栈空间大小 */
- xTaskCreateStatic( ) ---- 静态创建任务函数
例:
// task1的参数配置
void Task1(void *pvParameters);
#define TASK1_STACK_DEPTH 128
#define TASK1_PRIORITY 2
TaskHandle_t task1;
StackType_t task1StackBuffer[TASK1_STACK_DEPTH];
StaticTask_t task1TCB;
...
task1 = xTaskCreateStatic((TaskFunction_t)Task1,
(char *)"Task1",
TASK1_STACK_DEPTH,
NULL,
TASK1_PRIORITY,
task1StackBuffer,
&task1TCB);
另外,创建静态函数还需要实现两个不可见任务:
- 空闲任务
- vApplicationGetIdleTaskMemory()
StackType_t IdleTaskStackBuffer[configMINIMAL_STACK_SIZE];
StaticTask_t IdleTaskTCBBuffer;
/* 空闲任务内存分配 */
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskStackBuffer = IdleTaskStackBuffer;
*ppxIdleTaskTCBBuffer = &IdleTaskTCBBuffer;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
- 定时器任务
- vApplicationGetTimerTaskMemory()
StackType_t TimerTaskStackBuffer[configMINIMAL_STACK_SIZE];
StaticTask_t TimerTaskTCBBuffer;
/* 软件定时器内存分配 */
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskStackBuffer = TimerTaskStackBuffer;
*ppxTimerTaskTCBBuffer = &TimerTaskTCBBuffer;
*pulTimerTaskStackSize = configMINIMAL_STACK_SIZE;
}
任务的挂起与恢复
任务的挂起与恢复的API函数
vTaskSuspend():挂起任务, 类似暂停,可恢复
vTaskResume():恢复被挂起的任务
xTaskResumeFromISR():在中断中恢复被挂起的任务
void vTaskSuspend( TaskHandle_t xTaskToSuspend );
void vTaskResume( TaskHandle_t xTaskToResume );
挂起与恢复所需宏定义:
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_xResumeFromISR 1
中断管理
主要API:
taskENTER_CRITICAL() //进入临界段。
taskEXIT_CRITICAL() //退出临界段。
portDISABLE_INTERRUPTS() // 中断屏蔽失能
portENABLE_INTERRUPTS() // 中断屏蔽使能
/* FreeRTOS可管理的最高中断优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
- FreeRTOSconfig.h
/* 中断嵌套行为配置 */
#ifdef __NVIC_PRIO_BITS
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 4
#endif
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* FreeRTOS可管理的最高中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
#define configMAX_API_CALL_INTERRUPT_PRIORITY configMAX_SYSCALL_INTERRUPT_PRIORITY
- 案例:设置管理的优先级范围:5~15。使用两个定时器,一个优先级为4,一个优先级为6。两个定时器每1s,打印一段字符串,当关中断时,停止打印,开中断时持续打印。
- main.c
...
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
/* USER CODE END Callback 0 */
if (htim->Instance == TIM4) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */
if (htim->Instance == TIM2)
{
// 定时器2触发
printf("定时器2在运行 优先级是4\r\n");
}
else if (htim->Instance == TIM3)
{
// 定时器3触发
printf("定时器3在运行 优先级是6\r\n");
}
/* USER CODE END Callback 1 */
}
- demo.c
void Task3(void *pvParameters)
{
while (1)
{
// 周期性屏蔽中断 -> 周期性打开
// 屏蔽优先级为configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 到configLIBRARY_LOWEST_INTERRUPT_PRIORITY 5-15
uint32_t delay = 50000000;
while (delay--)
;
// 关闭中断之前运行一会
printf("关闭中断\r\n");
portDISABLE_INTERRUPTS();
// 关闭中断之后运行一行
delay = 50000000;
while (delay--)
;
printf("开启中断\r\n");
portENABLE_INTERRUPTS();
}
}
实验现象:
时间片调度
- FreeRTOSconfig.h
#define configUSE_TIME_SLICING 1
#define configUSE_PREEMPTION 1
// 对于相同优先级的任务 每次分配运行的时间等于 1/configTICK_RATE_HZ
#define configTICK_RATE_HZ ((TickType_t)1000)
任务相关API
uxTaskPriorityGet : 获取任务优先级
vTaskPrioritySet : 设置优先级
uxTaskGetNumberOfTasks : 获取任务数量
uxTaskGetSystemState : 获取状态信息
vTaskList : 使用list表格打印信息
xTaskGetHandle : 通过用户名称获取任务句柄
uxTaskGetStackHighWaterMark : 获取任务栈的历史最小值
vTaskGetRunTimeStats : 时间统计函数
- FreeRTOSconfig.h
// 定义能够设置优先级
#define INCLUDE_vTaskPrioritySet 1
// 定义能够获取优先级
#define INCLUDE_uxTaskPriorityGet 1
// 获取状态信息
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
// 获取句柄
#define INCLUDE_xTaskGetHandle 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
- FreeRTOS_demo.c
...
TaskStatus_t pxTaskStatusArray[5];
char listBuff[500];
void Task2(void *pvParameters)
{
while (1)
{
/* 1. 获取任务优先级 */
UBaseType_t taskPriority = uxTaskPriorityGet(task1);
printf("task1的优先级是%d\r\n", taskPriority);
taskPriority = uxTaskPriorityGet(task2);
printf("task2的优先级是%d\r\n", taskPriority);
/* 2. 设置优先级 */
// 注意如果把别的任务的优先级调高 要注意这个任务里面有没有阻塞
// 有可能调高之后导致卡在那个任务中 没办法再运行下面的代码
vTaskPrioritySet(task2, 4);
taskPriority = uxTaskPriorityGet(task2);
printf("修改之后的task2的优先级是%d\r\n", taskPriority);
/* 3. 获取任务数量 */
UBaseType_t numOfTasks = uxTaskGetNumberOfTasks();
printf("任务数量为%d\r\n", numOfTasks);
/* 4. 获取状态信息 */
uint32_t total_runtime;
UBaseType_t taskNums;
taskNums = uxTaskGetSystemState(pxTaskStatusArray, numOfTasks, &total_runtime);
// 接收到的全部任务
printf("任务名称\t任务编号\t当前状态\t当前优先级\t基础优先级\t句柄\r\n");
for (uint16_t i = 0; i < taskNums; i++)
{
printf("%s\t\t%d\t\t%d\t\t%d\t\t%d\t\t%d\r\n",
pxTaskStatusArray[i].pcTaskName,
pxTaskStatusArray[i].xTaskNumber,
pxTaskStatusArray[i].eCurrentState,
pxTaskStatusArray[i].uxCurrentPriority,
pxTaskStatusArray[i].uxBasePriority,
(uint32_t)pxTaskStatusArray[i].xHandle);
}
/* 5. 使用list表格打印信息 -> 最推荐使用的方法查看整个freeRTOS的任务运行情况 */
vTaskList(listBuff);
printf("%s\r\n", listBuff);
/* 6. 通过用户名称获取任务句柄 */
TaskHandle_t task1_handle = xTaskGetHandle("Task1");
printf("%d\r\n", task1_handle);
/* 7. 获取任务栈的历史最小值 -> 栈空间的使用达到最大的时候剩余的空间 */
UBaseType_t waterMark = uxTaskGetStackHighWaterMark(task1);
printf("task1的历史最小值为%d\r\n", waterMark);
// 末尾添加阻塞
vTaskDelay(10000);
}
}
- 现象:
时间相关函数:
- FreeRTOS_demo.c
// 完成task1的具体任务
// 低优先级的任务只能从剩下的50%时间中占比
// 运行时间250ms 延迟时间是500ms -> 33% / 2 = 17%
void Task1(void *pvParameters)
{
while (1)
{
// printf("任务1正在运行\n");
// 每次调用使用 翻转LED1
LED_Toggle(LED1_Pin);
HAL_Delay(250);
// 延迟500ms
vTaskDelay(500);
}
}
char runTimeBuff[500];
// 只有高优先级的任务 会直接从总时间中划分一部分
// 如果运行时间是10ms 阻塞时间也是10ms -> total占比50%
void Task2(void *pvParameters)
{
while (1)
{
uint8_t res = Key_Detect();
if (res == KEY1_PRESS)
{
vTaskGetRunTimeStats(runTimeBuff);
printf("%s\r\n", runTimeBuff);
}
HAL_Delay(10);
vTaskDelay(10);
}
}
- 现象:
延时函数
vTaskDelay():相对延时。从执行vTaskDelay()函数开始,直到指定延时的时间结束。
xTaskDelayUntil():绝对延时。将整个任务的运行周期视为一个整体,适用于需要以固定频率定期执行的任务。
消息队列
xQueueCreate : 创建队列
xQueueSend : 往队列的尾部写入消息
xQueueReceive : 从队列头部读取消息,并删除消息
- FreeRTOS_demo.c
// 队列使用是线程安全的 直接放在全局变量中使用即可
QueueHandle_t queue1;
QueueHandle_t big_queue;
// 提前创建队列
// 0.1 创建能够存储一个字节的队列
// 队列的长度 元素的大小
// 源码结构:
// (1) 计算队列本体的大小 + Queue_t头信息结构体的大小作为整个队列的大小
// (2) 根据本身传入的参数 填写对应头信息的值 => 给Queue_t头信息结构体赋值
queue1 = xQueueCreate(1, sizeof(uint8_t));
// 0.2 创建一个能够存储长字符串的队列
big_queue = xQueueCreate(2, sizeof(char *));
...
char str[100] = {"asdhkasjdhkahsdasjdkhakshd"};
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key;
uint8_t value = 8;
// char *str = "asdhkasjdhkahsdasjdkhakshd";
// C语言底层 识别字符串指针和字符数组有本质区别
// 字节数组 str 0号元素的地址 -> 字节的地址 str[0] -> 第一个元素的值
// 将对应的字节数组的地址声明为字符串指针的时候
// char * str1 = str;
// 必须要声明出 后面的值是一个字符串地址
char *str1 = &str[0];
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
// 按下key1往queue1中写入uint8
// 队列的句柄 创建队列的时候声明的元素类型的指针
taskENTER_CRITICAL();
// 源码:
// (0) 声明需要用到的变量 校验填写的参数
// -> 优先级最高 进入临界区 如果条件判断成功能写入 队列不满 (1) traceQUEUE_SEND( pxQueue ); prvCopyDataToQueue -> 条件成功写入数据
// (2) 队列满了 -> 判断是否需要等待 -> 不等待直接返回队列满的错误
// -> 需要等待 先标记一个等待时间
// 退出临界区
// 挂起调度器
// 设置等待 -> 阻塞自身
// 恢复调度器 返回 -> 调度别的任务
BaseType_t send_r = xQueueSend(queue1, &value, portMAX_DELAY);
// 还没来得及运行下面的打印呢 就task2被打断了
if (send_r == pdPASS)
{
printf("成功写入数据到queue1\n");
}
else
{
printf("失败写入数据到queue1\n");
}
taskEXIT_CRITICAL();
}
// 按下key3往big_queue中写入长字符串
else if (key == KEY3_PRESS)
{
// 如果队列存储的元素是字符串 也要写对应元素的地址
BaseType_t send_r = xQueueSend(big_queue, &str1, portMAX_DELAY);
if (send_r == pdPASS)
{
printf("成功写入数据到big_queue\n");
}
else
{
printf("失败写入数据到big_queue\n");
}
}
vTaskDelay(10);
}
}
void Task2(void *pvParameters)
{
uint8_t res = 0;
while (1)
{
// 直接读取queue1 -> 如果读到数据打印
// 如果队列为空 会判断是否有等待时间 有的话阻塞自身等待
// 读队列数据源码:
// 进入临界区 优先级最高 满足队列不为空 (1) prvCopyDataFromQueue( pxQueue, pvBuffer ); traceQUEUE_RECEIVE( pxQueue );
// (2) 队列为空 -> 判断等待时间是否为0 -> 不愿意等 -> 返回错误 队列为空
// (3) 队列不为空 -> 标记等待时间 -> 退出临界区 挂起调度器 -> 进入等待 阻塞自身
// 返回 调度别的任务
BaseType_t rece_r = xQueueReceive(queue1, &res, portMAX_DELAY);
// 一旦有数据写入 -> 因为读取数据的优先级更高 所以强制打断了低优先级的任务
if (rece_r == pdPASS)
{
printf("读取queue1成功,读到的数据是%d\n", res);
}
else
{
printf("读取queue1失败\n");
}
}
}
void Task3(void *pvParameters)
{
char *buff;
while (1)
{
// 读取队列中的数据 读不到等待 会阻塞自身
BaseType_t rece_r = xQueueReceive(big_queue, &buff, portMAX_DELAY);
if (rece_r == pdPASS)
{
printf("读取bigqueue成功,读到的数据是%s\n", buff);
}
else
{
printf("读取bigqueue失败\n");
}
}
}
- 现象:
信号量
二值信号量
xSemaphoreCreateBinary : 使用动态方式创建二值信号量
xSemaphoreGive : 释放信号量,才创建二值信号量时先释放一下
xSemaphoreTake : 获取信号量
- FreeRTOS_demo.c
QueueHandle_t sem_bin;
// 创建二值信号量 => 本质就是一个队列 长度为1 => 元素的个数是0
// 只关心队列的itemSize 0,1
// 如果为0 => take的时候就会阻塞 调用give 0 -> 1
// take就不阻塞
sem_bin = xSemaphoreCreateBinary();
// 二值信号量一般创建之后都要释放一下
xSemaphoreGive(sem_bin);
...
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
// 每一次按下key1 => 执行一次释放信号量
key = Key_Detect();
if (key == KEY1_PRESS)
{
HAL_Delay(10);
if (Key_Detect() == KEY1_PRESS)
{
// 释放
printf("释放一次\n");
xSemaphoreGive(sem_bin);
}
}
vTaskDelay(100);
}
}
void Task2(void *pvParameters)
{
BaseType_t res = 0;
while (1)
{
// 一直获取信号量 -> 获取成功就打印出来
// 获取不成功 => 一直等待 阻塞自身
// (1) 第一次获取直接获取成功
// (2) 第二次再次获取的时候 就阻塞了
res = xSemaphoreTake(sem_bin, portMAX_DELAY);
if (res == pdPASS)
{
printf("获取信号量成功\n");
}
else
{
printf("获取信号量失败\n");
}
}
}
- 现象:
计数型信号量
xSemaphoreCreateCounting : 创建计数信号量
xSemaphoreGive : 获取信号量的计数值
- FreeRTOS_demo.c
QueueHandle_t sem_count;
// 创建计数信号量
sem_count = xSemaphoreCreateCounting(100, 0);
...
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
// 每一次按下key1 => 执行一次释放信号量
key = Key_Detect();
if (key == KEY1_PRESS)
{
HAL_Delay(10);
if (Key_Detect() == KEY1_PRESS)
{
// 释放 -> 计数型信号量的计数值 +1
printf("释放一次\n");
xSemaphoreGive(sem_count);
}
}
vTaskDelay(100);
}
}
void Task2(void *pvParameters)
{
BaseType_t count = 0;
while (1)
{
// 一直读取计数型信号量的值 根据值的不同 选择不同的条件判断
count = uxSemaphoreGetCount(sem_count);
printf("计数器的值为%d\n",count);
if (count < 20)
{
printf("只能去欢乐谷\n");
}
else if (count < 50)
{
printf("只能去日本\n");
}
else if (count < 100)
{
printf("可以去英国\n");
}
else
{
printf("随便去哪浪\n");
}
vTaskDelay(2000);
}
}
- 现象:
互斥信号量
- 互斥信号量是包含优先级继承机制的二进制信号量,解决优先级翻转问题
xSemaphoreCreateMutex : 使用动态方法创建互斥信号量
队列集
在config中配置:
#define configUSE_QUEUE_SETS 1
xQueueCreateSet : 创建队列集
xQueueAddToSet : 队列添加到队列集中
xQueueSelectFromSet : 获取队列集中有有效消息的队列
- FreeRTOS_demo.c
QueueSetHandle_t queue_set;
QueueHandle_t queue;
QueueHandle_t sem_bin;
/* 0.创建队列集 队列和信号量 */
// 填写队列集中有几个队列
queue_set = xQueueCreateSet(2);
// 创建队列
queue = xQueueCreate(5, sizeof(uint8_t));
// 创建信号量
sem_bin = xSemaphoreCreateBinary();
// 将队列和信号量放入到队列集中
xQueueAddToSet(queue, queue_set);
xQueueAddToSet(sem_bin, queue_set);
...
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
uint8_t value = 8;
BaseType_t res = 0;
while (1)
{
key = Key_Detect();
// 写入数据的操作
// (1) 按下key1 队列写入值
if (key == KEY1_PRESS)
{
res = xQueueSend(queue, &value, portMAX_DELAY);
if (res == pdPASS)
{
printf("向队列中写入%d成功\n", value);
}
}
// (2) 按下key2 释放二值信号量
else if (key == KEY2_PRESS)
{
res = xSemaphoreGive(sem_bin);
if (res == pdPASS)
{
printf("释放信号量成功\n");
}
}
}
}
void Task2(void *pvParameters)
{
uint8_t value = 0;
while (1)
{
// 队列集的好处是 -> 同时获取多个队列或信号量的值 不会让多个队列信号量直接互相阻塞
// 使用队列集读取两个队列的数据
// 队列集的句柄 等待时间
QueueSetMemberHandle_t xReturn = xQueueSelectFromSet(queue_set, portMAX_DELAY);
if (xReturn == queue)
{
// 读取队列里面新添加的数据
xQueueReceive(queue, &value, portMAX_DELAY);
printf("接收到队列中添加的数据%d\n", value);
}
else if (xReturn == sem_bin)
{
xSemaphoreTake(sem_bin, portMAX_DELAY);
printf("获取信号量成功\n");
}
}
}
- 现象 :
事件标志组
xEventGroupCreate : 使用动态方式创建事件标志组
xEventGroupSetBits : 设置事件标志位
xEventGroupWaitBits : 等待事件标志位
- FreeRTOS_demo.c
EventGroupHandle_t event_group;
#define EVENT_BIT0 (1 << 0)
#define EVENT_BIT1 (1 << 1)
/* 创建event_group */
event_group = xEventGroupCreate();
if (event_group != NULL)
{
printf("创建事件标志组成功\n");
}
...
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
// 必须要消抖才能看出来清空的效果
key = Key_Detect();
if (key == KEY1_PRESS)
{
xEventGroupSetBits(event_group, EVENT_BIT0);
}
else if (key == KEY2_PRESS)
{
xEventGroupSetBits(event_group, EVENT_BIT1);
}
}
}
void Task2(void *pvParameters)
{
EventBits_t res = 0;
while (1)
{
// 读取事件标志组
// 句柄 等待的位 读取完成之后释放需要清除为0 是否需要等待所有的位 等待时间
// (1) 本身方法会挂起阻塞 -> 阻塞停止的条件 取决于xWaitForAllBits
// (2) 判断满足不同条件的结果 -> 返回值那些位置为1
res = xEventGroupWaitBits(event_group, EVENT_BIT0 | EVENT_BIT1, pdPASS, pdFAIL, portMAX_DELAY);
if (res & EVENT_BIT0)
{
printf("按键1被按下\n");
}
else if (res & EVENT_BIT1)
{
printf("按键2被按下\n");
}
// 必须等待完成多个条件之后 代码才会继续往下
res = xEventGroupWaitBits(event_group, EVENT_BIT0 | EVENT_BIT1, pdPASS, pdPASS, portMAX_DELAY);
printf("两个按键都被按下过\n");
}
}
- 现象 :
任务通知
模拟信号量
xTaskNotifyGive : 发送通知,不带通知值
ulTaskNotifyTake : 获取任务通知,可选退出函数时对通知置清零或减1
- FreeRTOS_demo.c
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
/* 任务1按下key1 发送任务通知 */
key = Key_Detect();
if (key == KEY1_PRESS)
{
HAL_Delay(100);
if (Key_Detect() == KEY1_PRESS)
{
// 发送任务通知 -> 给那个任务发送通知
printf("按键按下 准备给task2发送任务通知\n");
xTaskNotifyGive(task2);
}
}
}
}
void Task2(void *pvParameters)
{
while (1)
{
/* 任务2挂起等待通知 接收到通知之后 执行后续的代码 */
// 在获取到之后释放要清空
ulTaskNotifyTake(pdPASS, portMAX_DELAY);
printf("获取到任务通知 代码能够继续向下执行\n");
}
}
- 现象
模拟消息邮箱
- eAction选eSetValueWithOverwrite,模拟计数型信号量
- eAction选eSetBits,模拟事件标志组
xTaskNotify : 发送通知,带有通知值
/** eNoAction = 0, 不对值进行修改 只是简单的通知 eSetBits, 只对对应的位进行设置 相当于或计算 eIncrement, 对原有的值+1 eSetValueWithOverwrite, 直接覆盖写入通知值 不管之前的通知值有没有被读取 eSetValueWithoutOverwrite 等待之前的通知值被读取使用之后 再进行覆盖写入 */ xTaskNotify(xTaskToNotify, ulValue, eAction);
xTaskNotifyWait : 获取任务通知,可获取通知值和清除通知值的指定位
- FreeRTOS_demo.c
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
/* 将按键按下得到的值封装起来 通过任务通知发送给task2 */
key = Key_Detect();
/**
eNoAction = 0, 不对值进行修改 只是简单的通知
eSetBits, 只对对应的位进行设置 相当于或计算
eIncrement, 对原有的值+1
eSetValueWithOverwrite, 直接覆盖写入通知值 不管之前的通知值有没有被读取
eSetValueWithoutOverwrite 等待之前的通知值被读取使用之后 再进行覆盖写入
*/
// 通知的task句柄 通知的value值 eAction具体的动作
if (key != 0)
{
printf("按键已经按下 ,将按键值通知给task2\n");
xTaskNotify(task2, key, eSetValueWithOverwrite);
}
}
}
void Task2(void *pvParameters)
{
uint32_t res = 0;
while (1)
{
/* 接收对应的任务通知 根据得到的值不同 进行不同的动作 */
// 进入等待需要清空的位 退出时需要清空的位 接收value值的地址 等待的时间
xTaskNotifyWait(0, 0xffffffff, &res, portMAX_DELAY);
if (res & KEY1_PRESS)
{
printf("接收到通知,是key1按下 翻转led1\n");
LED_Toggle(LED1_Pin);
}
else if (res & KEY2_PRESS)
{
printf("接收到通知,是key2按下 翻转led2\n");
LED_Toggle(LED2_Pin);
}
}
}
- 现象:
模拟事件标志组
- FreeRTOS_demo.c
#define NOTIFY_BIT0 (1 << 0)
#define NOTIFY_BIT1 (1 << 1)
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
// 按下key1 -> 在任务通知的第0位标记为1
printf("按键1被按下 发送事件1通知\n");
xTaskNotify(task2, NOTIFY_BIT0, eSetBits);
}
else if (key == KEY2_PRESS)
{
// 按下key2 -> 在任务通知的第1位标记为1
printf("按键2被按下 发送事件2通知\n");
xTaskNotify(task2, NOTIFY_BIT1, eSetBits);
}
}
}
void Task2(void *pvParameters)
{
uint32_t event_group = 0; // 用作记录最终的结果值
uint32_t res = 0; // 用作每一次的通知接收值
while (1)
{
// 等待接收task2自己的任务通知
xTaskNotifyWait(0, 0xffffffff, &res, portMAX_DELAY);
printf("已经接收到通知值为%d\n", res);
event_group |= res;
if (event_group == (NOTIFY_BIT0 | NOTIFY_BIT1))
{
event_group = 0;
printf("同时实现两个条件\n");
LED_Turn_On(LED1_Pin);
}
}
}
- 现象
软件定时器
软件定时器 | 硬件定时器 |
---|---|
FreeRTOS提供的功能来模拟定时器,依赖系统的任务调度器来进行计时和任务调度 | 由芯片或微控制器提供,独立于 CPU,可以在后台运行,不受任务调度器的影响 |
精度和分辨率可能受到任务调度的影响 | 具有更高的精度和分辨率 |
不需要额外的硬件资源,但可能会增加系统的负载 | 占用硬件资源,不会增加 CPU 的负载 |
- API
xTimerCreate : 动态方式创建软件定时器
xTimerStart : 开启软件定时器定时
xTimerStop : 停止软件定时器定时
- FreeRTOSconfig.h
/* 软件定时器相关定义 */
#define configUSE_TIMERS 1 /* 1: 使能软件定时器, 默认: 0。使能后需指定下面3个 */
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) /* 定义软件定时器任务的优先级 */
#define configTIMER_QUEUE_LENGTH 5 /* 定义软件定时器命令队列的长度*/
#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小*/
案例 :
task1:用于按键扫描,并对软件定时器进行开启、停止操作
- FreeRTOS_demo.c
TimerHandle_t timer1_handle;
TimerHandle_t timer2_handle;
// 回调函数在触发之后 通过优先级进入freeRTOS的就绪队列 谁的优先级高调度谁
// -> 软件定时器默认的优先级一般都是最高的
// 如果程序中 有普通任务的优先级高于定时器任务 会造成定时器阻塞无法正常工作
// 如果程序中 有普通任务的优先级和定时器任务一样高 不会影响定时器工作
// 一定要在配置文件中给定时器设置最高任务优先级 -> max - 1
uint16_t count1 = 0;
void timer1_callback(TimerHandle_t xTimer)
{
printf("定时器1执行了%d次\n",count1++);
}
uint16_t count2 = 0;
void timer2_callback(TimerHandle_t xTimer)
{
printf("定时器2执行了%d次\n",count2++);
}
/* 创建定时器 */
// 0.1 一次性定时器
// 定时器名称 定时器周期 释放自动重装载 定时器id 使用的回调函数
uint8_t id1 = 1;
timer1_handle = xTimerCreate("timer1", 500, pdFALSE, (void *)&id1, timer1_callback);
// 0.2 周期性定时器
uint8_t id2 = 2;
timer2_handle = xTimerCreate("timer2", 2000, pdTRUE, (void *)&id2, timer2_callback);
...
// 完成task1的具体任务
void Task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
HAL_Delay(50);
if (Key_Detect() == KEY1_PRESS)
{
/* 启动定时器 */
// 对定时器的启动停止等操作 本质上都是往定时器服务队列中发送对应的消息
// 定时器服务会周期性读取队列中的消息 执行对应的任务
printf("按键1被按下 启动定时器\n");
xTimerStart(timer1_handle, portMAX_DELAY);
xTimerStart(timer2_handle, portMAX_DELAY);
}
}
else if (key == KEY2_PRESS)
{
HAL_Delay(50);
if (Key_Detect() == KEY2_PRESS)
{
printf("按键2被按下 准备停止定时器\n");
xTimerStop(timer1_handle, portMAX_DELAY);
xTimerStop(timer2_handle, portMAX_DELAY);
}
}
}
}
- 实验现象
Tickless模式
- Tickless 模式的工作原理:
空闲任务检测:FreeRTOS 会通过空闲任务(Idle Task)来检测系统是否有任务需要执行。如果没有任务需要执行,系统可以进入休眠状态。
时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器。
时钟中断处理:在时钟中断处理函数中,FreeRTOS 将检查任务的状态并决定是否继续执行。
休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断。在休眠状态下,处理器可以进入更低功耗的模式。
任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务。
- FreeRTOSConfig.h
// 打开系统低功耗
#define configUSE_TICKLESS_IDLE 1
// 配置进入到低功耗之前需要做的用户自定义事件
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
// 退出低功耗之后需要做的事件
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()
其中两个回调函数:
// 进入低功耗模式之前可以做的事情
// 用户只需要根据自己的需求 将一些硬件设备的时钟关闭来实现降低功耗
// 不要重复调用__wfi方法来进入睡眠模式 系统会在合适的位置自动进入
void PRE_SLEEP_PROCESSING()
{
...
}
// 退出低功耗模式之后能够做的事情
void POST_SLEEP_PROCESSING()
{
...
}
// 调度运行的逻辑
// (1) 运行task2 阻塞自身在获取二值信号量的位置
// (2) 运行task1 没有按键按下 进入到delay阻塞 -> 100ms
// (3) 系统会自动判断100ms > configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认2ms
// (4) 先执行POST_SLEEP_PROCESSING方法 -> 进入睡眠模式之前 用户自定义的省电
// (5) 进入睡眠模式 等待唤醒
// (6) delay100ms时间到了 会时钟中断唤醒 -> 自动修正时钟的计数+100 -> Free_RTOS_Start 用户自定义的唤醒之后的工作
// (7) 有按键按下 -> task1 释放二值信号量 -> task2抢占CPU 获取二值信号量
{
printf("按键2被按下 准备停止定时器\n");
xTimerStop(timer1_handle, portMAX_DELAY);
xTimerStop(timer2_handle, portMAX_DELAY);
}
}
}
}
- 实验现象
[外链图片转存中...(img-91MTqypP-1730985086910)]
### Tickless模式
- Tickless 模式的工作原理:
> - 空闲任务检测:FreeRTOS 会通过空闲任务(Idle Task)来检测系统是否有任务需要执行。如果没有任务需要执行,系统可以进入休眠状态。
>
> - 时钟中断:当有任务需要执行时,系统会启动时钟中断,唤醒处理器。
>
> - 时钟中断处理:在时钟中断处理函数中,FreeRTOS 将检查任务的状态并决定是否继续执行。
>
> - 休眠状态:如果没有任务需要执行,系统可以进入休眠状态,关闭时钟中断。在休眠状态下,处理器可以进入更低功耗的模式。
>
> - 任务唤醒:当有任务需要执行时,系统会再次启动时钟中断,唤醒处理器,然后执行相应的任务。
- FreeRTOSConfig.h
```c
// 打开系统低功耗
#define configUSE_TICKLESS_IDLE 1
// 配置进入到低功耗之前需要做的用户自定义事件
#define configPRE_SLEEP_PROCESSING(x) PRE_SLEEP_PROCESSING()
// 退出低功耗之后需要做的事件
#define configPOST_SLEEP_PROCESSING(x) POST_SLEEP_PROCESSING()
其中两个回调函数:
// 进入低功耗模式之前可以做的事情
// 用户只需要根据自己的需求 将一些硬件设备的时钟关闭来实现降低功耗
// 不要重复调用__wfi方法来进入睡眠模式 系统会在合适的位置自动进入
void PRE_SLEEP_PROCESSING()
{
...
}
// 退出低功耗模式之后能够做的事情
void POST_SLEEP_PROCESSING()
{
...
}
// 调度运行的逻辑
// (1) 运行task2 阻塞自身在获取二值信号量的位置
// (2) 运行task1 没有按键按下 进入到delay阻塞 -> 100ms
// (3) 系统会自动判断100ms > configEXPECTED_IDLE_TIME_BEFORE_SLEEP 默认2ms
// (4) 先执行POST_SLEEP_PROCESSING方法 -> 进入睡眠模式之前 用户自定义的省电
// (5) 进入睡眠模式 等待唤醒
// (6) delay100ms时间到了 会时钟中断唤醒 -> 自动修正时钟的计数+100 -> Free_RTOS_Start 用户自定义的唤醒之后的工作
// (7) 有按键按下 -> task1 释放二值信号量 -> task2抢占CPU 获取二值信号量
// (8) 如果还是没有按键按下 -> 运行delay100 -> 倒头就睡