快速入门基于stm32 CubeMX生成的freeRTOS操作系统开发
1.freeRTOS
1.1 使用cubemx 移植free rtos
1.1.1 使用cubemx配置free rtos过程
1 在中间件中选择freertos
2 在interface接口选择中有两个freertos的版本 这里我选择的是cmsis_v2版本
3 接口介绍
cmsis 接口里面包含了gpio IIC等系统外设的了通用接口一套通用的接口避免了为某款单片机单独设计函数 使用更加便捷。
1.1.2 通过cubemx配置的freertos 项目组结构的构成
通过cubemx配置的freertos的工程组里多了一个freertos.c的文件这是系统自动生成的任务,有任务调度时自动调用。
在工程组里有一个关于freertos的组这里存放的是freertos 所有的组件我们主要就是学习这些组件的使用方法
1.2 FreeRTOS任务
1.2.1 FreeRTOS任务的概念
可以把任务比作一个函数
-
任务(Task):独立的执行单元,拥有自己的堆栈和优先级。
-
任务状态:运行(Running)、就绪(Ready)、阻塞(Blocked)、挂起(Suspended)。
-
任务优先级:数值越大优先级越高,
configMAX_PRIORITIES
定义了最大优先级数量(在FreeRTOSConfig.h
中配置)
1.2.2 任务的基础使用方法
1.认识创建任务函数osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes)
在cubemx生成的代码中的freertos.c文件中有这样一个接口
这是官方提供的创建任务接口函数
函数原型:
osThreadId_t osThreadNew(osThreadFunc_t func, // 任务函数指针void * arg, // 传递给任务函数的参数const osThreadAttr_t *attr // 任务属性(优先级、堆栈等)
);
参数说明
func
作用:任务函数指针,指向任务的具体实现代码。
类型:osThreadFunc_t
,本质是 void (*)(void *argument)
。
void StartDefaultTask(void *argument) {while (1) {// 任务逻辑代码osDelay(100); // CMSIS-RTOS 的延时函数}
}
arg
-
作用:传递给任务函数的参数,可以是任意类型的指针(如结构体)。
-
示例:
uint32_t task_param = 123; osThreadNew(StartDefaultTask, &task_param, &attr);
attr
-
作用:任务属性配置,定义任务的优先级、堆栈大小、名称等。
-
类型:
const osThreadAttr_t *
,指向一个预定义的结构体。
返回值
-
类型:
osThreadId_t
-
作用:返回任务句柄(Task Handle),用于后续任务管理(如挂起、恢复、删除)。
2.认识任务函数
FreeRTOS 任务函数必须是一个 无限循环函数,否则任务执行完成后会触发断言错误(Assertion)。其通用结构如下:
void vTaskFunction(void *pvParameters) {// 初始化代码(可选)// 无限循环for (;;) {// 任务逻辑代码// 阻塞或延时,释放 CPU 资源vTaskDelay(pdMS_TO_TICKS(100)); }// 如果任务意外退出,需删除自身(一般不会执行到这里)vTaskDelete(NULL);
}
关键点
-
无限循环:任务必须包含一个永不退出的循环(如
for(;;)
或while(1)
),否则任务退出时会触发错误。 -
阻塞调用:在循环中必须包含 FreeRTOS 的阻塞函数(如
vTaskDelay
,xQueueReceive
等),否则任务会独占 CPU,导致低优先级任务无法运行。
任务函数的参数
任务函数通过 void *pvParameters
接收参数,参数在任务创建时通过 xTaskCreate()
或 osThreadNew()
传递。
// 定义任务参数结构体
typedef struct {uint32_t led_pin; // LED 引脚uint32_t delay_ms; // 闪烁间隔
} TaskParams_t;// 任务函数
void vBlinkTask(void *pvParameters) {TaskParams_t *params = (TaskParams_t *)pvParameters;while (1) {HAL_GPIO_TogglePin(GPIOA, params->led_pin);vTaskDelay(pdMS_TO_TICKS(params->delay_ms));}
}// 创建任务时传递参数
int main() {TaskParams_t params = {GPIO_PIN_5, 500}; // 参数初始化xTaskCreate(vBlinkTask, "BlinkTask", 128, ¶ms, 1, NULL);vTaskStartScheduler();
}
3.利用自动生成的任务点亮一个led灯
在freertos.c中有一个默认的任务StarDefaultTask我们可以直接在里面写程序作用类似于main.c中的while(1)
这里我用的是用的是STM32G431RB的一款学习板上面集成了很多外设比如led灯大家只要使用对应的引脚就行
void StartDefaultTask(void *argument)
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */for(;;){HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_8);osDelay(500); //休眠函数释放cpu资源,并且根据指定时间进入休眠}/* USER CODE END StartDefaultTask */
}
事物图就不展示
1.3 串口重定向
1.3.1 理解重定向
在计算机和嵌入式开发中,重定向(Redirection) 的核心含义是 改变数据流的默认方向,使其从一个目标转向另一个目标。这个概念广泛应用于操作系统、编程和硬件交互中。
串口重定向的意义在于在没有屏幕的微型控制器中清晰呈现数据结果 方便调试
1. 默认情况
当使用标准库函数 printf
时,其默认输出目标是 控制台(如电脑屏幕)。但在没有操作系统的嵌入式系统中(如STM32),printf
并不知道如何输出到屏幕。
2. 问题
STM32需要通过串口(UART)向外发送数据(例如调试信息),但 printf
无法直接使用串口硬件。
3. 重定向的作用
通过 重写底层函数(如 fputc
或 _write
),将 printf
的默认输出从“屏幕” 重定向 到“串口”。
-
用户调用
printf("Hello")
-
实际执行的是
HAL_UART_Transmit
,将数据通过串口发送出去。
1.3.2 重写printf函数
在系统生成的串口文件usart.c中包含头文件 #include <stdio.h> // 标准输入输出头文件
在使用printf函数时会使用到核心函数fputc所以我们重写的其实是fputc
fputc函数原型
int fputc(int ch,FILE* f)
使用HAL_UART_Transmit函数重写fputc
// 实现串口重定向
// printf ————>fputc:打印一个字节 FILE *文件流指针
int fputc(int ch,FILE* f)
{HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,1000);return ch;
}
在freertos.c的默认任务中验证
void StartDefaultTask(void *argument)
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */for(;;){unsigned int a=1;printf("helloc%d\r\n",a); //串口重定向验证/*char text[20];sprintf(text,"hello worldc\r\n");HAL_UART_Transmit(& huart1,(uint8_t *)text,sizeof(text),50);*/HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_8);osDelay(500); //休眠函数释放cpu资源,并且根据指定时间进入休眠}/* USER CODE END StartDefaultTask */
}
在串口助手中每500ms会收到信息
1.4 任务优先级
1.4.1 任务优先级分类
任务优先级从低到高一共有49个优先级这里我截取了一部分 优先级由枚举类型表示(enum)最低优先级从8到56数字越高优先级越高
在cubemx配置任务的时候我们可以主动设置优先级
在task and queue设置中add添加任务
再添加的任务中默认优先级为最低级
1.4.2 优先级实验
在我们自己生成的任务和系统自己的默认任务中都写入串口发送的功能(系统默认任务优先级高于我们自己生成的任务)
void StartDefaultTask(void *argument) //系统默认任务
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */for(;;){unsigned int a=1;printf("helloc%d\r\n",a);/*char text[20];sprintf(text,"hello worldc\r\n");HAL_UART_Transmit(& huart1,(uint8_t *)text,sizeof(text),50);HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_9);*/osDelay(1000); //休眠函数释放cpu资源,并且根据指定时间进入休眠}/* USER CODE END StartDefaultTask */
}/* USER CODE BEGIN Header_myfunTask1 */
/**
* @brief Function implementing the myTask1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_myfunTask1 */
void myfunTask1(void *argument)
{/* USER CODE BEGIN myfunTask1 *//* Infinite loop */for(;;){printf("chenchangchun\r\n");osDelay(1000);}/* USER CODE END myfunTask1 */
}
在串口助手中可以观察到现象系统默认任务·被先执行
总结优先级大的任务被先执行
1.5 任务的栈
栈的核心作用
-
保存任务运行时的上下文
-
当任务被切换(例如调度器切换任务或发生中断)时,任务的当前状态(如寄存器、返回地址、局部变量等)会被压入栈中。恢复任务时,这些数据会从栈中弹出,确保任务能从中断点继续执行。
-
-
存储函数调用链的临时数据
-
与普通程序栈类似,任务的栈存储函数调用时的返回地址、局部变量、函数参数等,支持嵌套函数调用。
-
-
处理中断时的临时现场保护
-
若未使用独立的中断栈(由
configUSE_TASK_FPU
等配置决定),中断处理可能会复用当前任务的栈来保存上下文。
-
-
保障任务独立性
-
每个任务有独立的栈空间,避免多任务因共享栈导致的数据混乱。
-
栈中存储的具体内容
-
函数调用信息
-
返回地址:函数调用完成后需要返回的位置(如
PC
寄存器的值)。 -
局部变量:函数内部定义的临时变量。
-
函数参数:超过寄存器数量的参数会通过栈传递。
-
-
任务上下文信息
-
CPU 寄存器:在任务切换时,所有需要保存的寄存器(如
R0-R12
、LR
、PC
、PSR
等)会被压入栈。 -
浮点寄存器(如果启用 FPU):通过配置
configUSE_TASK_FPU
保存浮点运算状态。
-
-
中断上下文
-
若中断使用任务栈,中断服务程序(ISR)的寄存器现场会被压入当前任务的栈。
-
-
临时数据
-
运算中间结果、编译器生成的临时变量、栈对齐填充(部分架构要求栈对齐)。
-
1.6 动态创建任务和静态创建任务
动态创建任务系统会自动任务栈和TCB使用简单,但是这样不利于内存管理容易造成内存碎片化这里讲一下静态创建方法
静态创建任务函数原型:xTaskCreateStatic
TaskHandle_t xTaskCreateStatic(TaskFunction_t pvTaskCode, // 任务函数指针const char * const pcName, // 任务名称configSTACK_DEPTH_TYPE usStackDepth, // 栈大小(单位:字)void *pvParameters, // 任务参数UBaseType_t uxPriority, // 任务优先级StackType_t *pxStackBuffer, // 用户提供的栈内存缓冲区StaticTask_t *pxTaskBuffer // 用户提供的 TCB(任务控制块)内存缓冲区
);
参数说明
参数 | 说明 |
---|---|
pvTaskCode | 任务函数的入口地址(例如 vTaskFunction ) |
pcName | 任务的名称(用于调试,长度由 configMAX_TASK_NAME_LEN 定义) |
usStackDepth | 任务栈的深度(单位是字,例如 32 位系统下 100 字 = 400 字节) |
pvParameters | 传递给任务函数的参数(可以是结构体指针或 NULL) |
uxPriority | 任务优先级(0 最低,configMAX_PRIORITIES-1 最高) |
pxCreatedTask | 返回任务句柄的指针(可用于删除任务、修改优先级等) |
pxStackBuffer | 用户手动分配的栈内存数组(大小需 ≥ usStackDepth ) |
pxTaskBuffer | 用户手动分配的 TCB 内存(大小由 configMINIMAL_STACK_SIZE 等配置决定) |
函数原型了解即可我们一般使用api接口osThreadNew
#define CTR_TASK_STACK_BLEN 1024
typedef struct {//unsigned char SysStackBuf[SYS_TASK_STACK_BLEN]; /* sys堆栈空间 *///StaticTask_t SysTaskCtlBlk; /* 用于静态创建sys *///osSemaphoreId SEM_Ctr; /* SEM_Handle *///StaticSemaphore_t RunSemCtlBlk; /* 用于静态创建 */unsigned char CtrStackBuf[CTR_TASK_STACK_BLEN]; /* ctr堆栈空间预创建一块栈的缓存区 */StaticTask_t CtrTaskCtlBlk; /* 用于静态创建ctr *///unsigned char QueStackBuf[QUE_TASK_STACK_BLEN]; /* 消息队列堆栈 *///StaticTask_t QueTaskCtlBlk; /* 用于静态创建Que *///unsigned char AsrStackBuf[ASR_TASK_STACK_BLEN]; /* 消息队列堆栈 *///StaticTask_t AsrTaskCtlBlk; /* 用于静态创建ASR */
}SYS_BLOCK;
extern SYS_BLOCK g_SysBlock;
SYS_BLOCK g_SysBlock = {0};
osThreadId_t starictask; //osThreadId_t任务句柄 osThreadAttr_t ThreadAttr = {.name = "mystatictask", // 任务名称.priority = (osPriority_t) osPriorityHigh, // 任务优先级.stack_mem = g_SysBlock.CtrStackBuf, // 静态分配的栈内存.stack_size = CTR_TASK_STACK_BLEN, // 栈大小(字节).cb_mem = &g_SysBlock.CtrTaskCtlBlk, // 静态分配的任务控制块.cb_size = sizeof(StaticTask_t), // 控制块大小};void mystatictask(void *argument) ;// 任务初始化
void mystatictask(void *argument) //静态创建的任务函数
{/* USER CODE BEGIN myfunTask1 *//* Infinite loop */for(;;){printf("mystatictask\r\n");osDelay(1000);}/* USER CODE END myfunTask1 */
}
在MX_FREERTOS_Init(void)中创建任务
osThreadNew(mystatictask,NULL,&ThreadAttr);
静态创建的任务优先级是最高的所以最先执行
1.7 任务调度算法
1.7.1 抢占式调度
顾名思义 抢占式调度就是高优先级优先执行freertos中默认是抢占式调度
核心机制
-
调度权强制收回:高优先级任务可随时抢占低优先级任务的 CPU 使用权,无需等待当前任务主动释放。
-
中断驱动:通过硬件定时器中断或外部事件触发调度器决策。
特点
-
实时性强:高优先级任务(如紧急中断)能立即响应。
-
优先级反转风险:需通过优先级继承/天花板等机制避免。
-
上下文切换开销:频繁抢占会增加系统负载。
1.7.2 抢占式调度实验
三个任务的优先级由高到低分别为mystatictask ,StartDefaultTask , myfunTask1 我们只生成任务StartDefaultTask , myfunTask1而在优先级最低的myfunTask1中生成任务mystatictask按照抢占式调度逻辑将先执行任务StartDefaultTask,然后执行任务myfunTask1但是在任务myfunTask1中生成了高优先级的任务mystatictask所以任务myfunTask1将被抢占变成最后执行
void MX_FREERTOS_Init(void) {defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);myTask1Handle = osThreadNew(myfunTask1, NULL, &myTask1_attributes);}void StartDefaultTask(void *argument) //系统默认任务
{for(;;){unsigned char a=1;printf("taksk%d\r\n",a);osDelay(1000); //休眠函数释放cpu资源,并且根据指定时间进入休眠}}void myfunTask1(void *argument)
{for(;;){osThreadNew(mystatictask,NULL,&ThreadAttr);unsigned char b=2;printf("task%d\r\n",b);osDelay(1000);}
}void mystatictask(void *argument) //静态创建的任务函数
{for(;;){printf("mystatictask\r\n");osDelay(1000);}
}
效果如图所示
1.7.3 时间片轮转
核心机制
-
固定时间片分配:每个任务轮流执行一个固定时长的时间片(如 10ms)。
-
公平性保障:所有同优先级任务均分 CPU 时间,避免饥饿。
特点
-
公平性优先:适合多任务平等共享 CPU 的场景。
-
时间片选择关键:时间片过长会导致响应延迟,过短会增加切换开销。
-
通常与优先级结合:不同优先级队列内部分别轮转。
1.7.4 时间片轮转实验
我们让两个任务的优先级保持相同 这里我们选择任务StartDefaultTask和任务myfunTask1我们将任务StartDefaultTask的时间片消耗完让他不执行后面的打印语句
void MX_FREERTOS_Init(void) { defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);myTask1Handle = osThreadNew(myfunTask1, NULL, &myTask1_attributes);}
void StartDefaultTask(void *argument) //系统默认任务
{int i=0;for(;;){printf("taksk1start\r\n");for(i=0;i<=10000000;i++); // 消耗任务时间片printf("taksk1end\r\n");osDelay(1000); //休眠函数释放cpu资源,并且根据指定时间进入休眠}}
void myfunTask1(void *argument)
{ for(;;){printf("task2start\r\n");printf("task2end\r\n");osDelay(1000);}
}
在图片结果中可以看到本应该在任务1中打印的第二句话应为时间片消耗完而没有执行
1.7.5 协作式调度
核心机制
-
任务主动让出 CPU:任务需显式调用释放函数(如
taskYIELD()
)切换上下文。 -
无强制抢占:调度器无法中断正在运行的任务。
特点
-
低开销:无中断驱动的上下文切换,适合资源受限系统。
-
依赖任务合作:设计不良的任务可能导致 CPU 独占(如死循环无释放)。
-
确定性差:高优先级任务可能被阻塞无法及时响应。
1.8 队列
1.8.1 队列在freertos中的作用
在freertos中队列可以使任务进入阻塞状态用于状态切换,跟重要的是可以进行进程之间的信息传递。
消息队列,是一种用于任务与任务间、中断和任务间传递一条或多条信息的数据结构,实现了任务接收来自其他任务或中断的不固定或固定长度的消息。
任务从队列里面读取消息时,如果队列中消息为空,读取消息的任务将被阻塞;否则任务就读取消息并且处理。用户还可以指定阻塞任务时间 xTicksToWait(),在指定阻塞时间内,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。
有多个消息发送到消息队列时,通常将先进入队列的消息先传给任务,也就是说,任务一般读取到的消息是最先进入消息队列的消息,即先进先出原则(FIFO),但也支持后进先出原则(LIFO)
1.8.2 动态方法创建一个队列
与任务的创建方法相同队列也有静态创建和动态创建两种方法动态创建方法比较简单我们直接在cubemx中手动配置
在queue项中add添加
选择动态创建
在生成的代码中会有任务的句柄名和任务属性赋值
osMessageQueueId_t myQueue01Handle; // 队列句柄名用于获取队列的转台和操作队列
const osMessageQueueAttr_t myQueue01_attributes = { // 对队列属性赋值.name = "myQueue01"
};
然后再MX_FREERTOS_Init(void)中创建队列
myQueue01Handle = osMessageQueueNew (10, sizeof(int), &myQueue01_attributes);
需要注意的是生产者队列任务的优先级和等待时间需要小于等于消费者(因为需要让消费队列及时处理队列消息)
// 生产者任务
void StartDefaultTask(void *argument) {int local_i = 0; // 使用局部变量for(;;) {if (local_i <= 10) {int data_to_send = local_i; // 创建数据副本osStatus_t status = osMessageQueuePut(myQueue01Handle,&data_to_send, // 发送副本0, // 优先级参数50);if (status == osOK) {printf("Producer sent: %d\r\n", data_to_send);local_i++;}else {printf("Producer error: %d\r\n", status);}}osDelay(800);}
}// 消费者任务
void myfunTask1(void *argument) {int received_data; // 局部变量存储接收的数据for(;;) {osStatus_t status = osMessageQueueGet(myQueue01Handle,&received_data, // 存储到局部变量NULL,50);if (status == osOK) {printf("Consumer received: %d\r\n", received_data);}else {printf("Consumer error: %d\r\n", status);}osDelay(1000);}
}
1.8.3 静态创建一个队列
与静态创建任务类似的是静态创建队列也需要手动配置控制块内存和队列元素存储区内存只要配置正确就可以用了
osMessageQueueId_t myQueuestatic; //队列句柄int a[256];static const osMessageQueueAttr_t myQueueAttr = {.name = "StaticQueue", // 队列名称(调试用).mq_mem = a, // 指向队列元素储存区.mq_size = QUE_TASK_STACK_BLEN, // 储存区大小.cb_mem= &g_SysBlock.QueTaskCtlBlk, // 指向控制块.cb_size = sizeof(StackType_t) // 控制块大小
};
1.9 信号量
信号量类型及使用场景
信号量类型 | 最大计数值 | 典型应用场景 |
---|---|---|
二进制信号量 | 1 | 任务同步、中断与任务通信 |
计数信号量 | >1 | 资源池管理(如缓冲区、连接池) |
互斥锁(Mutex) | 1 | 临界区保护、防止优先级反转 |
#include "cmsis_os2.h"// 定义信号量句柄
osSemaphoreId_t binSemaphore; // 二进制信号量
osSemaphoreId_t countSemaphore; // 计数信号量
osSemaphoreId_t mutexSemaphore; // 互斥锁// 信号量属性配置
const osSemaphoreAttr_t binSem_attr = {.name = "BinarySemaphore"
};const osSemaphoreAttr_t countSem_attr = {.name = "CountingSemaphore"
};const osSemaphoreAttr_t mutex_attr = {.name = "MutexSemaphore"
};void initialize_semaphores(void) {// 创建二进制信号量(初始值=0,最大计数值=1)binSemaphore = osSemaphoreNew(1, 0, &binSem_attr);// 创建计数信号量(初始值=5,最大计数值=10)countSemaphore = osSemaphoreNew(10, 5, &countSem_attr);// 创建互斥锁(初始值=1,最大计数值=1)mutexSemaphore = osSemaphoreNew(1, 1, &mutex_attr);if (!binSemaphore || !countSemaphore || !mutexSemaphore) {printf("信号量创建失败!\r\n");Error_Handler();}
}
二进制信号量使用示例(任务同步)
// 任务A:事件触发者
void TaskA(void *argument) {for (;;) {// 执行某些操作...osDelay(500);// 释放二进制信号量(通知TaskB)osStatus_t status = osSemaphoreRelease(binSemaphore);if (status != osOK) {printf("TaskA信号量释放失败: %d\r\n", status);}}
}// 任务B:事件响应者
void TaskB(void *argument) {for (;;) {// 等待二进制信号量(最多等待1秒)osStatus_t status = osSemaphoreAcquire(binSemaphore, 1000);if (status == osOK) {printf("TaskB: 收到事件通知!\r\n");// 处理事件...} else if (status == osErrorTimeout) {printf("TaskB: 等待超时!\r\n");} else {printf("TaskB错误: %d\r\n", status);}}
}