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所记笔记,有所疏漏,敬请谅解

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐