一、FreeRTOS简介
FreeRTOS 是一个可裁剪、可剥夺型的多任务内核,而且没有任务数限制。FreeRTOS 提供了实时操作系统所需的所有功能,包括资源管理、同步、任务通信等。
FreeRTOS 是用 C 和汇编来写的,其中绝大部分都是用 C 语言编写的,只有极少数的与处理器密切相关的部分代码才是用汇编写的,FreeRTOS 结构简洁,可读性很强!最主要的是非常适合初次接触嵌入式实时操作系统学生、嵌入式系统开发人员和爱好者学习。
最新版本 V9.0.0(2016年),尽管现在 FreeRTOS 的版本已经更新到 V10.4.1 了,但是我们还是选择 V9.0.0,因为内核很稳定,并且网上资料很多,因为 V10.0.0 版本之后是亚马逊收购了FreeRTOS之后才出来的版本,主要添加了一些云端组件,一般采用 V9.0.0 版本足以。
FreeRTOS官网:http://www.freertos.org/
代码托管网站:https://sourceforge.net/projects/freertos/files/FreeRTOS/
RTOS的多个工作流如下图示:
RTOS工作原理如下图示:
RTOS通用组件如下图示:
RTOS的特点:
- 更好的事件实时处理机制
- 更高效利用CPU资源
- 通用的任务管理框架
FreeRTOS介绍
FreeRTOS是RTOS的一种,尺寸非常小,可运行于微控制器上。微控制器是尺寸小,资源受限的处理器,它在单个芯片上包含了处理器本身、用于保存要执行的程序的只读存储器(ROM或Flash)、所执行程序需要的随机存取存储器(RAM),一般情况下程序直接从只读存储器执行
微控制器用于深度嵌入式应用,一般都有非常明确、专门的工作。尺寸的限制以及专用的终端应用等性质,令其很少能使用完整的RTOS实现。因此FreeRTOS仅为内核提供了实时调度功能、任务间通信、时序和同步原语。更准确地说,它是一个实时内核,或实时执行器。命令控制台界面、网络栈等额外的功能可作为附加组件
在实际使用FreeRTOS的时候我们需要根据自已的需求来配置FreeRTOS,不同架构的MCU在使用的时候配置也不同,下面介绍FreeRTOS配置文件详解
- 内核配置一
- 内核配置二
- 内存管理
- 任务运行信息获取配置
- 软件定时器
- 中断优先级
- 函数Include配置
二、硬件设计
本实验通过freertos创建两个任务来分别控制LED2和LED3的亮灭,需要用到的硬件资源
- LED2和LED3指示灯
- 串口
三、STM32CubeMX设置
1、基础设置
- RCC设置外接HSE,时钟设置为72MHz;TIM3的时钟挂载在APB1 Time Clocks上为72MHz。
- 打开串口中断,波特率设置为115200.
- PB5/PE1设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平。
2、FreeRtos设置
(1)、在 System Core 中选择 SYS ,对 Timebase Source 进行设置,选择 TIM1 作为HAL库的时基(除了 SysTick 外都可以)。
在基于STM32 HAL的项目中,一般需要维护的 “时基” 主要有2个:
- HAL的时基,SYS Timebase Source
- OS的时基(仅在使用OS的情况下才考虑)
而这些 “时基” 该去如何维护,主要分为两种情况考虑:
裸机运行:
可以通过 SysTick(滴答定时器)或 (TIMx)定时器 的方式来维护 SYS Timebase Source,也就是HAL库中的 uwTick,这是HAL库中维护的一个全局变量。在裸机运行的情况下,我们一般选择默认的 SysTick(滴答定时器) 方式即可,也就是直接放在 SysTick_Handler() 中断服务函数中来维护。
带OS运行:
前面提到的 SYS Timebase Source 是STM32的HAL库中的新增部分,主要用于实现 HAL_Delay() 以及作为各种 timeout 的时钟基准。
在使用了OS(操作系统)之后,OS的运行也需要一个时钟基准(简称“时基”),来对任务和时间等进行管理。而OS的这个 时基 一般也都是通过 SysTick(滴答定时器) 来维护的,这时就需要考虑 “HAL的时基” 和 “OS的时基” 是否要共用 SysTick(滴答定时器) 了。
如果共用SysTick,当我们在CubeMX中选择启用FreeRTOS之后,在生成代码时,CubeMX一定会报如下提示:
强烈建议用户在使用FreeRTOS的时候,不要使用
SysTick
(滴答定时器)作为 “HAL的时基”,因为FreeRTOS要用,最好是要换一个!!!如果共用,潜在一定风险。
(2)、在 Middleware 中选择 FREERTOS 设置,并选择 CMSIS_V1 接口版本
CMSIS是一种接口标准,目的是屏蔽软硬件差异以提高软件的兼容性。RTOS v1使得软件能够在不同的实时操作系统下运行(屏蔽不同RTOS提供的API的差别),而RTOS v2则是拓展了RTOS v1,兼容更多的CPU架构和实时操作系统。因此我们在使用时可以根据实际情况选择,如果学习过程中使用STM32F1、F4等单片机时没必要选择RTOS v2,更高的兼容性背后时更加冗余的代码,理解起来比较困难。
(3)、在 Config parameters 进行具体参数配置。
Kernel settings:
- USE_PREEMPTION: Enabled:RTOS使用抢占式调度器;Disabled:RTOS使用协作式调度器(时间片)。
- TICK_RATE_HZ: 值设置为1000,即周期就是1ms。RTOS系统节拍中断的频率,单位为HZ。
- MAX_PRIORITIES: (默认):可使用的最大优先级数量。设置好以后任务就可以使用从0到(MAX_PRIORITIES - 1)的优先级,其中0位最低优先级,(MAX_PRIORITIES - 1)为最高优先级。
- MINIMAL_STACK_SIZE:(默认128): 设置空闲任务的最小任务堆栈大小,以字为单位,而不是字节。如该值设置为128 Words,那么真正的堆栈大小就是 128*4 = 512 Byte。
- MAX_TASK_NAME_LEN:(默认16): 设置任务名称最大长度。
- IDLE_SHOULD_YIELD: 默认Enabled 空闲任务放弃CPU使用权给其他同优先级的用户任务。
- USE_MUTEXES:默认Enabled 为1时使用互斥信号量,相关的API函数会被编译。
- USE_RECURSIVE_MUTEXES:默认Enabled 为1时使用递归互斥信号量,相关的API函数会被编译。
- USE_COUNTING_SEMAPHORES:默认Enabled 为1时启用计数型信号量, 相关的API函数会被编译。
- QUEUE_REGISTRY_SIZE:(默认8) 设置可以注册的队列和信号量的最大数量,在使用内核调试器查看信号量和队列的时候需要设置此宏,而且要先将消息队列和信号量进行注册,只有注册了的队列和信号量才会在内核调试器中看到,如果不使用内核调试器的话此宏设置为0即可。
- USE_APPLICATION_TASK_TAG:(默认Disable) 为1时可以使用vTaskSetApplicationTaskTag函数。
- ENABLE_BACKWARD_COMPATIBILITY:(默认Enabled ) 为1时可以使V8.0.0之前的FreeRTOS用户代码直接升级到V8.0.0之后,而不需要做任何修改。
- USE_PORT_OPTIMISED_TASK_SELECTION:(默认Enabled ) FreeRTOS有两种方法来选择下一个要运行的任务,一个是通用的方法,另外一个是特殊的方法,也就是硬件方法,使用MCU自带的硬件指令来实现。STM32有计算前导零指令吗,所以这里强制置1。
- USE_TICKLESS_IDLE: 置1(Built in functionality enabled):使能低功耗tickless模式;置0:保持系统节拍(tick)中断一直运行。假设开启低功耗的话可能会导致下载出现问题,因为程序在睡眠中,可用ISP下载办法解决。
- USE_TASK_NOTIFICATIONS: (默认Enabled )为1时使用任务通知功能,相关的API函数会被编译。开启了此功能,每个任务会多消耗8个字节。
- RECORD_STACK_HIGH_ADDRESS:(默认Disable) 为1时栈开始地址会被保存到每个任务的TCB中(假如栈是向下生长的)。
Memory management settings:
- Memory Allocation: Dynamic/Static 支持动态/静态内存申请
- TOTAL_HEAP_SIZE: 默认,设置堆大小,如果使用了动态内存管理,FreeRTOS在创建 task, queue, mutex, software timer or semaphore的时候就会使用heap_x.c(x为1~5)中的内存申请函数来申请内存。这些内存就是从堆ucHeap[configTOTAL_HEAP_SIZE]中申请的。
- Memory Management scheme: 内存管理策略 heap_4。
Hook function related definitions:
- USE_IDLE_HOOK: 默认, 置1:使用空闲钩子(Idle Hook类似于回调函数);置0:忽略空闲钩子。
- USE_TICK_HOOK: 默认, 置1:使用时间片钩子(Tick Hook);置0:忽略时间片钩子。
- USE_MALLOC_FAILED_HOOK: 默认, 使用内存申请失败钩子函数。
- CHECK_FOR_STACK_OVERFLOW: 默认, 大于0时启用堆栈溢出检测功能,如果使用此功能用户必须提供一个栈溢出钩子函数,如果使用的话此值可以为1或者2,因为有两种栈溢出检测方法。
Run time and task stats gathering related definitions:
- GENERATE_RUN_TIME_STATS: 启用运行时间统计功能。
- USE_TRACE_FACILITY: 启用可视化跟踪调试。
- USE_STATS_FORMATTING_FUNCTIONS: 与宏configUSE_TRACE_FACILITY同时为1时会编译下面3个函数prvWriteNameToBuffer()、vTaskList()、vTaskGetRunTimeStats()。
Co-routine related definitions:
- USE_CO_ROUTINES: 启用协程Enabled 。
- MAX_CO_ROUTINE_PRIORITIES: 协程的有效优先级数目2。
Software timer definitions:
- USE_TIMERS: 启用软件定时器Enabled 。
Interrupt nesting behaviour configuration:
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 中断最低优先级。
- LIBRARY_LOWEST_INTERRUPT_PRIORITY: 系统可管理的最高中断优先级。
(4)、创建任务Task,在 Tasks and Queues 进行配置。
默认空闲任务是在系统无其它任务执行时执行。
然后我们创建两个LED任务。
- Task Name: 任务名称
- Priority: 优先级,在 FreeRTOS 中,数值越大优先级越高,0 代表最低优先级
- Stack Size (Words): 堆栈大小,单位为字,在32位处理器(STM32),一个字等于4字节,如果传入512那么任务大小为512*4字节
- Entry Function: 入口函数
- Code Generation Option: 代码生成选项
- Parameter: 任务入口函数形参,不用的时候配置为0或NULL即可
- Allocation: 分配方式:Dynamic 动态内存创建
- Buffer Name: 缓冲区名称
- Conrol Block Name: 控制块名称
(5)、输入工程名,选择工程路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码。
四、程序编程
生成的程序中的freertos.c文件下就可以看到STM32CubeMX已经创建了LED1和LED2两个任务的创建和调度,直接在任务中编写代码即可运行。以下为freertos.c原始文件:
void MX_FREERTOS_Init(void) {
/* definition and creation of defaultTask */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
/* definition and creation of LED1 */
osThreadDef(LED1, LED1_Task02, osPriorityIdle, 0, 128);
LED1Handle = osThreadCreate(osThread(LED1), NULL);
/* definition and creation of LED2 */
osThreadDef(LED2, LED2_Task03, osPriorityIdle, 0, 128);
LED2Handle = osThreadCreate(osThread(LED2), NULL);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
/* USER CODE END RTOS_THREADS */
}
/* USER CODE END Header_LED1_Task02 */
void LED1_Task02(void const * argument)
{
/* USER CODE BEGIN LED1_Task02 */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END LED1_Task02 */
}
/* USER CODE END Header_LED2_Task03 */
void LED2_Task03(void const * argument)
{
/* USER CODE BEGIN LED2_Task03 */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END LED2_Task03 */
}
该文件的顶端定义了任务的ID,任务的调用都和这个ID相关。
/* USER CODE END Variables */
osThreadId defaultTaskHandle;
osThreadId LED1Handle;
osThreadId LED2Handle;
如果用了time1作为时基,在main.c文件下可以看到调用了定时器中断回调函数,如果需要用其他定时器可以在这里添加定时器程序
(1)、任务创建
任务创建的方式分为静态创建和动态创建,不同之处是利用不同的运行内存。
在创建之前需要引用osThreadDef定义一下任务
osThreadDef(name, thread, priority, instances, stacksz)
/// 创建一个具有函数、优先级和堆栈要求的线程定义。
/// \param name 线程函数的名称。
/// \param thread 任务函数名称。
/// \param priority 线程函数的初始优先级。
/// \param instances 可能的线程实例数。
/// \param stacksz 线程函数的堆栈大小(以字节为单位)。
//例:
osThreadDef(LED1, LED1_Task02, osPriorityIdle, 0, 128);
//即创建了一个名为LED1的线程定义,调用函数为LED1_Task02。
使用动态/静态内存的方法创建一个任务。
函数 | osThreadCreate (const osThreadDef_t *thread_def, void *argument) |
---|---|
参数 | thread_def: 引用由osThreadDef定义的任务 argument: 任务入口函数形参 |
返回值 | 成功返回任务ID,失败返回0 |
例:创建任务3(LED3_Task04)
//1,先宏定义任务ID
osThreadId LED3Handle;
//2、定义任务函数
void LED3_Task04(void const * argument);
//3、在void MX_FREERTOS_Init(void)中创建任务线程
void MX_FREERTOS_Init(void)
{
//****省略*****//
//创建任务LED3
osThreadDef(LED3, LED3_Task04, osPriorityIdle, 0, 128);
LED3Handle = osThreadCreate(osThread(LED3), NULL);
}
//4、编写任务函数
void LED3_Task04(void const * argument)
{
for(;;)
{
printf("this LED3 run \r\n");
osDelay(800);
}
}
//注,此任务仅作举例使用。
(2)、任务删除
删除任务。任务被删除后就不复存在,也不会再进入运行态。
函数 | osThreadTerminate (osThreadId thread_id) |
---|---|
参数 | thread_id: 被删除任务的ID |
返回值 | 错误码(osStatus) |
例:
osThreadTerminate (LED1Handle); //彻底删除任务LED1Handle
(3)、任务延时函数
(1).osDelay
相对延时函数。用于阻塞延时,调用该函数后,任务将进入阻塞状态,进入阻塞态的任务将让出 CPU 资源。
函数 | osStatus osDelay (uint32_t millisec) |
---|---|
参数 | millisec: 毫秒数 |
返回值 | 错误码 |
要想使用该函数必须在 Include parameters
中把 vTaskDelay
选择 Enabled
来使能。
例:
void vTaskA( void * pvParameters )
{
while (1)
{
// ...
// 这里为任务主体代码
// ...
/* 调用相对延时函数,阻塞 1000 个 tick */
osDelay( 1000 );
}
}
(2)、osThreadTerminate
绝对延时函数。常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。
函数 | osStatus osDelayUntil (uint32_t *PreviousWakeTime, uint32_t millisec) |
---|---|
参数 | PreviousWakeTime: 任务上一次离开阻塞态(被唤醒)的时刻。这个时刻被用作一个参考点来计算该任务下一次离开阻塞态的时刻 millisec: 毫秒数 |
返回值 | 错误码 |
要想使用该函数必须在 Include parameters
中把 vTaskDelayUntil
选择 Enabled
来使能。
例:
//注意:在使用的时候要将延时时间转化为系统节拍,在任务主体之前要调用延时函数。
void vTaskA( void * pvParameters )
{
/* 用于保存上次时间。调用后系统自动更新 */
static portTickType PreviousWakeTime;
/* 设置延时时间,将时间转为节拍数 */
const portTickType TimeIncrement = pdMS_TO_TICKS(1000);
/* 获取当前系统时间 */
PreviousWakeTime = osKernelSysTick();
while (1)
{
/* 调用绝对延时函数,任务时间间隔为 1000 个 tick */
osDelayUntil( &PreviousWakeTime,TimeIncrement );
// ...
// 这里为任务主体代码
// ...
}
}
在生成的两个LED任务中编写运行函数程序。任务1计数到10 时删除任务2
//任务LED1
void LED1_Task02(void const * argument)
{
/* USER CODE BEGIN LED1_Task02 */
/* Infinite loop */
int i=0;
for(;;)
{
HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5); //LED1状态每500s翻转一次
printf("this LED1 run %d\r\n",i);
i++;
if(i==10)
{
if(osThreadTerminate (LED2Handle)==0); //彻底删除任务LED2Handle
printf("删除 LED2Handle\r\n");
}
osDelay(1000);
}
/* USER CODE END LED1_Task02 */
}
//任务LED2
void LED2_Task03(void const * argument)
{
/* USER CODE BEGIN LED2_Task03 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_5); //LED1状态每500s翻转一次
printf("this LED2 run \r\n");
osDelay(600);
}
/* USER CODE END LED2_Task03 */
}
五、下载验证
编译无误后下载到板子上,可以看到三个任务都能够正常运行,当任务1计数到10 时将任务2删除了之后,任务2不再继续运行
六、参考文献
STM32CubeMX学习笔记(28)——FreeRTOS实时操作系统使用(任务管理)_stm32 rtos 任务固定时间运行-CSDN博客