freertos与CMSIS-RTOS v2
CMSIS-RTOS是由ARM公司提供的一套实时操作系统的通用API,它提供了标准化的编程接口,可以与中间件和库组件实现一致的软件层。
CMSIS-RTOS的主要作用是统一不同RTOS的接口,降低嵌入式开发的门槛,提高代码的移植性以及开发效率。
CMSIS-RTOS可以封装FreeRTOS,使得开发者可以使用CMSIS-RTOS的接口来调用FreeRTOS的功能。这样,开发者可以在不同的实时操作系统内核上无缝切换,提高了代码的可移植性。
在STM32CubeIDE中,FreeRTOS通过CMSIS-RTOS接口进行配置和使用,方便快速。CMSIS-RTOS在用户的应用代码和第三方的RTOS Kernel直接架起一道桥梁,如果两个RTOS都实现了CMSIS-RTOS,那么用户的应用程序代码完全可以不做修改。下面将FREERTOS与CMSIS-RTOS v2对比查看。
任务
创建
如果是使用FreeRTOS的原生API
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName, /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
const configSTACK_DEPTH_TYPE usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
-
pxTaskCode:任务函数的入口,任务执行时会调用该函数。 -
pcName:任务的名称,通常用于调试。 -
usStackDepth:任务栈的大小 -
pvParameters:传递给任务的参数,可以是任何类型的指针。 -
uxPriority:任务的优先级,值越大优先级越高。 -
pxCreatedTask:任务创建成功后,返回任务的句柄。如果不需要句柄,可以传NULL。
如果是使用CMSIS-RTOS v2的API
osThreadNew(taskFunction, NULL, NULL);
-
taskFunction: 任务执行的函数指针,与 FreeRTOS 相似,任务将在此函数中执行。 -
NULL: 传递给任务的参数,在此例中没有传递任何参数,所以为NULL。 -
NULL: 任务属性(osThreadAttr_t结构体)。如果不需要特殊属性,可以设置为NULL。该结构体可以设置栈大小、优先级等。
if (mem == 0) {
#if (configSUPPORT_DYNAMIC_ALLOCATION == 1)
if (xTaskCreate ((TaskFunction_t)func, name, (uint16_t)stack, argument, prio, &hTask) != pdPASS) {
hTask = NULL;
}
进入函数内部发现osThreadNew(taskFunction, NULL, NULL)函数内部可以发现还是调用的xTaskCreate
删除
FreeRTOS:
需要使用xTaskCreate来手动删除任务。
void vTaskDelete( TaskHandle_t xTaskToDelete );
CMSIS API 的osThreadNew创建的任务,则不需要手动删除,任务执行完会由IDLE任务自动回收。
队列
队列(queue)可以用于"任务到任务"、"任务到中断"、"中断到任务"直接传输信息。
创建
osMessageQueueId_t osMessageQueueNew(uint32_t msg_count, //队列长度
uint32_t msg_size, //消息大小
const osMessageQueueAttr_t *attr); // null 默认队列属性(动态)
//给定队列属性(静态)
uint8_t ucQueueStorageBuffer[QUEUE_SIZE * ITEM_SIZE];//队列数组
StaticQueue_t xQueueBuffer; //队列元数据指针
osMessageQueueAttr_t attr = {
.name = "StaticQueue", // 队列名称
.cb_mem = &xQueueBuffer, // 队列控制块内存
.cb_size = sizeof(xQueueBuffer), // 队列控制块大小
.mq_mem = ucQueueStorageBuffer, // 队列存储区内存
.mq_size = sizeof(ucQueueStorageBuffer) // 队列存储区大小
};
osMessageQueueId_t queueHandle;
// 创建静态队列
queueHandle = osMessageQueueNew(QUEUE_SIZE, ITEM_SIZE, &attr);
读写队列
原生FreeRTOS有两个版本:在任务中使用、在ISR中使用。CMSIS版本支持直接在ISR中调用。
FREERTOS:
• 任务级
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
• ISR 级
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
CMSIS-RTOS v2:
osStatus_t osMessageQueuePut(
osMessageQueueId_t mq_id,
const void *msg_ptr, // 指向要发送的消息
uint8_t msg_prio, // 消息优先级(0…31),0 = 普通
uint32_t timeout); // 阻塞时间,单位系统 tick
osStatus_t osMessageQueueGet(
osMessageQueueId_t mq_id,
void *msg_ptr, // 接收缓冲区
uint8_t *msg_prio, // 可选带回优先级,NULL = 不关心
uint32_t timeout);
当从 ISR 中调用 osMessageQueuePut 或osMessageQueueGet时,超时参数必须为 0,因为 ISR 中不允许阻塞操作 。
关于队列优先级
进入osMessageQueuePut函数,msg_prio 优先级参数被显式忽略了,在函数内部完全没有被使用
(void)msg_prio; /* Message priority is ignored */
信号量
有时候我们只需要传递状态,这个状态值需要用一个数值表示,比如:
- 卖家:做好了1个包子!做好了2个包子!做好了3个包子!
- 买家:买了1个包子,包子数量减1
- 这个停车位我占了,停车位减1
- 我开车走了,停车位加1
在这种情况下我们只需要维护一个数值,使用信号量效率更高、更节省内存,队列传递的是数据,信号量传递的是计数值。
创建
osSemaphoreId_t osSemaphoreNew(uint32_t max_count, // 最大计数值
uint32_t initial_count,// 初始计数值
const osSemaphoreAttr_t *attr); // 属性
osSemaphoreId_t binary_semaphore;
binary_semaphore = osSemaphoreNew(1, 0, NULL); // 创建一个初始值为 0 的二值信号量
// 创建一个最大值为 10,初始值为 5 的计数信号量
counting_semaphore = osSemaphoreNew(10, 5, NULL);
删除
对于动态创建的信号量,不再需要它们时,可以删除它们以回收内存。
osStatus_t osSemaphoreDelete(osSemaphoreId_t semaphore_id);
释放与获取
/*释放信号量是非阻塞的*/
osStatus_t osSemaphoreRelease(osSemaphoreId_t semaphore_id);
/*获取信号量可添加超时时间*/
osStatus_t osSemaphoreAcquire(osSemaphoreId_t semaphore_id, //信号量ID
uint32_t timeout); //超时时间
与队列同理,关于信号量,CMSIS则支持直接从中断服务例程(ISR)中调用。没有区分为两套函数。
互斥量
要想使用互斥量,需要在配置文件FreeRTOSConfig.h中定义:
#define configUSE_MUTEXES 1
要注意的是,互斥量不能在ISR中使用。
创建
/*创建互斥量*/
osMutexId_t osMutexNew(osMutexAttr_t *attr);
/* 动态创建互斥量 */
osMutexAttr_t attr = {
.name = "MyMutex", // 互斥量名称
.attr_bits = osMutexPrioInherit, // 优先级继承,防止优先级反转
.cb_mem = NULL, // 互斥量地址,NULL表示由内核决定
.cb_size = 0 // 互斥量大小,给0表示默认由内核决定
};
osMutexId_t my_mutex = osMutexNew(&attr); // 创建互斥量
/*静态创建互斥量*/
StaticSemaphore_t my_mutex_buffer; //为互斥量(Mutex)在底层实现上通常基于信号(Semaphore)的机制
osMutexAttr_t attr = {
.name = "MyMutex", //互斥量名称
.attr_bits = osMutexPrioInherit, //优先级继承
.cb_mem = &my_mutex_buffer, //互斥量地址
.cb_size = sizeof(my_mutex_buffer)//互斥量大小
};
osMutexId_t my_mutex = osMutexNew(&attr);
删除
/*删除互斥量*/
osStatus_t osMutexDelete(osMutexId_t mutex_id);
释放与获取
/*释放互斥量*/
osStatus_t osMutexRelease(osMutexId_t mutex_id);
/*获取互斥量*/
osStatus_t osMutexAcquire(osMutexId_t mutex_id,
uint32_t timeout);
优先级反转
优先级反转问题,指的是高优先级任务等待低优先级任务的资源,导致高优先级任务和低优先级任务的优先级效果反转。
优先级继承,即高优先级任务等待低优先级任务释放互斥量时,低优先级任务会临时提升优先级,以快速释放互斥量。 通过此种方式解决优先级反转问题。
递归锁
假设这样的场景:
- 任务A获得了互斥锁M
- 它调用一个库函数
- 库函数要去获取同一个互斥锁M,于是它阻塞:任务A休眠,等待任务A来释放互斥锁!
- 死锁发生!
怎么解决这类问题?可以使用递归锁(Recursive Mutexes),它的特性如下:
- 任务A获得递归锁M后,它还可以多次去获得这个锁
- "take"了N次,要"give"N次,这个锁才会被释放
如果要使用递归锁需要在FreeRTOSConfig.h中定义configUSE_RECURSIVE_MUTEXES为1
osMutexAttr_t attr = {
.name = "RecursiveMutex",
.attr_bits = osMutexRecursive | osMutexPrioInherit, // 启用递归锁和优先级继承
.cb_mem = NULL,
.cb_size = 0
};
/*创建递归锁*/
osMutexId_t my_recursive_mutex = osMutexNew(&attr);
配置 osMutexAttr_t 结构体,可以配置递归互斥锁,优先级继承,健壮互斥锁(FREERTOS无)
#define osMutexRecursive 0x00000001U ///< Recursive mutex.
#define osMutexPrioInherit 0x00000002U ///< Priority inherit protocol.
#define osMutexRobust 0x00000008U ///< Robust mutex.
事件组
创建
/*动态创建事件组*/
osEventFlagsAttr_t attr = {
.name = "MyEventFlags",
.attr_bits = 0,
.cb_mem = NULL,
.cb_size = 0
};
osEventFlagsId_t event_flags = osEventFlagsNew(&attr);
/*静态创建事件组*/
StaticEventGroup_t event_group_buffer;
osEventFlagsAttr_t attr = {
.name = "MyEventFlags",
.attr_bits = 0,
.cb_mem = &event_group_buffer, //事件组内存
.cb_size = sizeof(event_group_buffer) //事件组内存大小
};
osEventFlagsId_t event_flags = osEventFlagsNew(&attr);
删除
/*删除事件组*/
osStatus_t osEventFlagsDelete(osEventFlagsId_t eventFlagsId);
设置事件组
osStatus_t osEventFlagsSet(osEventFlagsId_t eventFlagsId, //事件组句柄
uint32_t flags, //要设置的位的集合
osEventFlagsOption_t options); //设置选项,
//可以是 osEventFlagsSet(仅设置位)
//或 osEventFlagsSetAndClear(设置位并清除所有其他位)。
等待事件组
/*等待事件*/
uint32_t osEventFlagsWait(osEventFlagsId_t eventFlagsId, //事件句柄
uint32_t flags, //等待的事件的位掩码,哪个事件等就给1
osEventFlagsWaitMode_t mode, //等待模式,可以是 osEventFlagsWaitAny(等待任何一个指定的位被设置)
//或 osEventFlagsWaitAll(等待所有指定的位都被设置)
uint32_t timeout); //超时时间
任务通知
我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:
任务通知的优势:
- 效率更高:使用任务通知来发送事件、数据给某个任务时,效率更高。比队列、信号量、事件组都有大的优势。
- 更节省内存:使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。
任务通知的限制:
- 不能发送数据给ISR:
- ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
- 数据只能给该任务独享
- 使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
- 在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
- 无法缓冲数据
- 使用队列时,假设队列深度为N,那么它可以保持N个数据。
- 使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
- 无法广播给多个任务
- 使用事件组可以同时给多个任务发送事件。
- 使用任务通知,只能发个一个任务。
- 如果发送受阻,发送方无法进入阻塞状态等待
- 假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待发送完成。
- 使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
使用任务通知,可以实现轻量级的队列(长度为1)、 邮箱(覆盖的队列)、 计数型信号量、二进制信号量、事件组。
发送
/* 发送通知给指定任务 */
osStatus_t osThreadFlagsSet(osThreadId_t thread_id, // 任务 ID,指定接收通知的任务
uint32_t flags); // 通知值,表示事件的状态
/* 示例:
0x01 表示事件A发生,
0x02 表示事件B发生,
0x03 表示事件A和事件B都发生了。
*/
osThreadFlagsSet(TASK_B_HANDLE, 0x03); // 向任务B发送通知,0x03表示事件A和事件B都发生
等待
/* 等待任务通知 */
osThreadFlagsWait(uint32_t flags, // 要等待的信号标志位,32 位标志数,通常为 0x01 表示事件A,0x02 表示事件B 等
uint32_t options, // 操作选项,如 osFlagsWaitAll(等待所有标志)或 osFlagsWaitAny(等待任意标志)
uint32_t timeout); // 超时时间,单位为毫秒。设置为 osWaitForever 表示无限等待
清除
osThreadFlagsWait 函数在成功返回时(即等到符合条件的标志位后)会自动清除标志位,
当使用 osFlagsWaitAny 等待一组事件中的任意一个时,osThreadFlagsWait 只会清除导致唤醒的那个事件的标志位。如果希望同时清除其他位,就需要osThreadFlagsClear手动处理。
/* 清除指定任务的通知标志位 */
osStatus_t osThreadFlagsClear(osThreadId_t thread_id, // 任务ID,指定要清除标志的任务
uint32_t flags); // 要清除的标志位
综合
// 定义事件标志
#define EVENT_A (1UL << 0)
#define EVENT_B (1UL << 1)
// 任务函数声明
void taskA(void *arg);
void taskB(void *arg);
// 任务A - 发送通知
void taskA(void *arg) {
while(1) {
// 发送事件A和B给任务B
osThreadFlagsSet(*(osThreadId_t*)arg, EVENT_A | EVENT_B);
printf("A: Sent events A&B\n");
osDelay(1000); // 延时1秒
}
}
// 任务B - 接收并处理通知
void taskB(void *arg) {
while(1) {
// 等待任一事件发生
uint32_t flags = osThreadFlagsWait(EVENT_A | EVENT_B, osFlagsWaitAny, osWaitForever);
// 检查并处理事件
if(flags & EVENT_A) printf("B: Got event A\n");
if(flags & EVENT_B) printf("B: Got event B\n");
}
}
软件定时器
软件定时器就是"闹钟"
- 指定时间:启动定时器和运行回调函数,两者的间隔被称为定时器的周期(period)。
- 指定类型,定时器有两种类型:
- 一次性(One-shot timers): 这类定时器启动后,它的回调函数只会被调用一次; 可以手工再次启动它,但是不会自动启动它。
- 自动加载定时器(Auto-reload timers ): 这类定时器启动后,时间到之后它会自动启动它; 这使得回调函数被周期性地调用。
- 指定要做什么事,就是指定回调函数
当FreeRTOS的配置项configUSE_TIMERS被设置为1时,在启动调度器时,会自动创建RTOS Damemon Task,即RTOS守护任务。以前被称为"Timer server",但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task
我们自己编写的任务函数要使用定时器时,是通过"定时器命令队列"(timer command queue)和守护任务交互。能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。
定时器的回调函数是在守护任务中被调用的,守护任务不是专为某个定时器服务的,它还要处理其他定时器。
所以,定时器的回调函数不要影响其他人:
- 回调函数要尽快实行,不能进入阻塞状态
- 不要调用会导致阻塞的API函数,比如 vTaskDelay()
- 可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞。
API接口
// 创建定时器,返回定时器ID
osTimerId_t osTimerNew(osTimerFunc_t func, // 定时器回调函数
osTimerType_t type, // 定时器类型(单次或周期)
void *argument, // 传递给回调函数的参数
const osTimerAttr_t *attr); // 定时器属性(可选)
/*
osTimerOnce: 单次定时器,仅触发一次。
osTimerPeriodic: 周期定时器,会周期性地触发。
*/
// 启动或重启定时器,返回状态码(osOK成功,osErrorParameter无效ID)
osStatus_t osTimerStart(osTimerId_t timer_id, uint32_t ticks);
// 停止定时器,返回状态码(osOK成功)
osStatus_t osTimerStop(osTimerId_t timer_id);
// 删除定时器,返回状态码(osOK成功)
osStatus_t osTimerDelete(osTimerId_t timer_id);
// 检查定时器是否运行,返回1(运行中)或0(未运行)
uint32_t osTimerIsRunning(osTimerId_t timer_id);
中断管理
- ISR的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
ISR要尽量快,否则:
- 其他低优先级的中断无法被处理:实时性无法保证
- 用户任务无法被执行:系统显得很卡顿
- 如果运行中断嵌套,这会更复杂,ISR越快执行约有助于中断嵌套

在ISR中调用API时不进行任务切换,而只是在"xHigherPriorityTaskWoken"中标记一下,除了效率,还有多种好处:
- 效率高:避免不必要的任务切换
- 让ISR更可控:中断随机产生,在API中进行任务切换的话,可能导致问题更复杂
- 可移植性
- 在Tick中断中,调用 vApplicationTickHook() :它运行与ISR,只能使用"FromISR"的函数。
串口示例
下面的例子很常见,在UART的ISR中读取多个字符,发现收到回车符时才进行任务切换。
// 后台任务:逐字节从队列取数据,遇到行尾再处理
extern QueueHandle_t xRxQueue;
extern TaskHandle_t xLineTask; // 处理整行的任务(优先级更高)
void USARTx_IRQHandler(void)
{
BaseType_t xHPW = pdFALSE;
while (USART_ByteReceived()) {
uint8_t ch = USART_Read();
xQueueSendToBackFromISR(xRxQueue, &ch, &xHPW); // 多次调用,不切换
if (ch == '\r' || ch == '\n') {
// 行结束:通知处理任务(或给信号量),这一步可能使其就绪
xTaskNotifyFromISR(xLineTask, 0, eNoAction, &xHPW);
}
}
// 统一在 ISR 尾部决定是否切换(仅一次)
portYIELD_FROM_ISR(xHPW);
}
以上为本人学习freertos所记笔记,有所疏漏,敬请谅解
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)