FreeRTOS是现在比较流行的主要应用于单片机等性能相对较低,对实时性要求又比较高的嵌入式应用场景中的操作系统。通常说FreeRTOS主要指的就是其内核,提供了线程、信号、内存管理等功能。使用FreeRTOS等RTOS可以方便复杂项目开发或是团队合作开发。
在STM32Cube中可以通过图形化的工具来配置使用FreeRTOS,生成初始化代码,省去了自己移植FreeRTOS的步骤,使用起来非常方便。这篇文章将对相关内容做个介绍。
ST官方对这块相关内容有文档进行介绍:
《UM1722 - Developing applications on STM32Cube with RTOS》
可以在官网搜索下载:
https://www.st.com/content/st_com/en.html
另外也可以在下面网站搜索下载到中文版本的:
https://www.stmcu.com.cn/
在STM32Cube中可以通过图形化的工具来配置使用FreeRTOS,但是以这种方式使用时又不是原汁原味的FreeRTOS,它在FreeRTOS和用户应用之间又封装了一层 CMSIS-RTOS ,我们在这种方式下写代码主要是和这一层进行交互:
需要注意的是上面两个文档都是基于CMSIS-RTOS-V1的,现在有CMSIS-RTOS-V2了,两者的API稍有不同,不过在STM32Cube中对此做了一定的兼容处理。这篇文章中主要以CMSIS-RTOS-V2为中间层进行介绍。
本文中使用的主控为 STM32F405RG ,开发环境如下:
STM32CubeIDE - V1.9.0
STM32Cube MCU Package for STM32F4 Series - V1.27.1
这里先以最基础的配置开始使用:
根据上面配置生成初始化代码,在左侧资源管理器中可以看到相关代码文件目录,其中就有FreeRTOS,通常来说我们是不需要手动修改这里的代码的,有需要尽量在图形化配置界面中调整。
生成代码的 main.c 中可以看到引用了 cmsis_os.h 文件, cmsis_os.h 文件中又引用了 cmsis_os2.h 文件,通常我们需要与RTOS交互只要使用这两个文件里面定义的各种接口即可。
main函数中可以看到调用了 osKernelInitialize
和 osKernelStart
两个函数。用户与操作系统相关初始化操作都可以放在这两者间。特别需要注意的是RTOS正常工作时 osKernelStart
是无限循环的,不停的在此处对各个任务进行调度,通常写在这后面的所有操作都不会被执行,如果执行那说明系统运行出错了。
默认配置下会自动创建一个任务,如下图所示,使用 osThreadNew
方法创建了一个任务,该任务执行 StartDefaultTask
这个函数,我们可以对这个函数稍加修改来测试任务运行:
/* USER CODE BEGIN Includes */
#include "stdio.h" // 引入该库为了可以使用 printf 函数
/* USER CODE END Includes */
// ----------------------------------------
/* USER CODE BEGIN 0 */
int __io_putchar(int ch) // 实现该方法,以使printf可以正确工作
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
/* USER CODE END 0 */
// ----------------------------------------
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
osDelay(1000); // 延时1000 rtos时间片,默认情况下即为1000ms
printf("%d\n", (int)osKernelGetTickCount()); // 打印时间
// printf("%d\n", (int)HAL_GetTick());
}
/* USER CODE END 5 */
}
上面延时中时间末尾数字的变化主要是因为程序调度以及串口发送操作等耗时引起的。
上面默认的这个任务是在图形界面中配置生成的,我们也可以直接使用代码来建立任务:
/* USER CODE BEGIN 0 */
osThreadId_t naisuTaskHandle; // 任务ID
const osThreadAttr_t naisuTask_attributes = { // 任务属性参数
.name = "naisuTask",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
void StartNaisuTask(void *argument) // 任务函数
{
for(;;)
{
osDelay(1);
}
}
/* USER CODE END 0 */
/* USER CODE BEGIN RTOS_THREADS */
naisuTaskHandle = osThreadNew(StartNaisuTask, NULL, &naisuTask_attributes); // 创建任务
/* USER CODE END RTOS_THREADS */
上面演示中还在创建任务时向任务传递了参数。
除了建立并运行任务,RTOS中还有暂停、销毁等操作任务的函数。
通常RTOS中的Task(任务)就相当于一般操作系统中的Thread(线程)。RTOS内核最核心的工作就是管理一个个的线程,调度线程运行,以实现对系统CPU和内存等资源的合理利用。使用RTOS从操作而言就是根据功能需求分割创建并运作一个个的任务,其它的功能都是围绕这些任务展开的能。
消息队列主要用于任务间传递数据使用,下面是个简单的演示:
上面演示中使用图形化界面配置生成队列,默认建立的队列中数据是uint16_t类型的。
在一个任务中定期将数据通过 osMessageQueuePut
方法放入队列;另一个任务使用 osMessageQueueGet
不停地等待队列中数据,有数据可用时就取出数据,然后通过串口打印。
实际使用中队列中数据可以是任意类型的,可以通过队列的属性参数来配置。
信号量相当于一个简化版的消息队列,无非传递数据,只是通知一下有资源可用。信号量有二进制信号量和计数型信号量。
二进制信号量只表达资源可用和不可用两种状态,下面是个简单的演示:
上面演示中使用 osSemaphoreRelease
方法来释放一个资源,使用 osSemaphoreAcquire
方法来获取这个资源。
计数型信号量表达资源的数量,下面是个简单的演示:
计数型信号量在创建的时候可以设置计数上限以及初始计数值。
互斥量用于保护数量只有一个的资源在被一个任务所用的时候不会被其它任务抢占。
比如下面情况下没有互斥量的保护输出就会出现问题:
上面两个任务会平等的执行,理想中想要两个任务各自打印出消息,但是两个任务使用的串口是同一个,其中一个任务在使用的时候另一个任务去使用它就会失败或者会覆盖前一个的输出(取决于这里调用的串口库函数逻辑)。
这里使用互斥量就可以解决上面的问题:
对同一个资源使用同一个互斥量,在使用资源前先用 osMutexAcquire
上个锁,这样其它任务就无法在获取到这个互斥量了;使用完成后使用 osMutexRelease
解锁,使用时需要注意谁上锁就由谁解锁。
如果在任务中有可能对同一资源出现嵌套上锁,那就需要使用 Recursive Mutexes
了,要注意上了几次锁就需要解锁几次。
RTOS和普通的操作系统一样也提供了软件定时器功能,下面是个简单的演示:
要注意的是上面演示中在定时器回调函数里进行了串口打印这种耗时操作,通常来说定时器回调函数中不推荐进行耗时操作,更多的是设置一个标识,然后在其它任务中进行操作。
FreeRTOS的很多功能也可以使用ST-LINK进行调试:
STM32 MCU硬件本身中断优先级有0~15可以选择,数值越低优先级越高。
在RTOS中中断嵌套和两个变量相关:
LIBRARY_LOWEST_INTERRUPT_PRIORITY为RTOS的最低优先级,一般保持最低即可
LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY表示如果硬件中断优先级比它高(比如这里就指0~4)则不能在中断回调函数中使用RTOS的API
在STM32Cube中使用FreeRTOS总体来说并不复杂,通过图形界面可以方便的配置FreeRTOS,省去了手动移植的步骤。不过从另一方面来说配置生成的代码中又封装了一个中间层,出发点来说是不错的,但是功能体验上来说并不原汁原味,还多一层封装多几个坑。实际使用时需要结合各自实际情况选择。