摘要:RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。
学习一个RTOS,搞懂它的编程的风格很重要,这可以大大提供我们阅读代码的效率。下面我们就以FreeRTOS里面的数据类型、变量名、函数名和宏这几个方面做简单介绍。
在FreeRTOS中,使用的数据类型虽然都是标准C里面的数据类型,但是针对不同的处理器,对标准C的数据类型又进行了重定义,给它们取了一个新的名字,比如char重新定义了一个名字 porCHAR,这里面的port表示接口的意思,就是FreeRTOS要移植到这些处理器上需要这些接口文件来把它们连接在一起。但是用户在写程序的时候并非一定要遵循 FreeRTOS的风格,我们还是可以直接用C语言的标准类型。在FreeRTOS中,int型从不使用,只使用short和long型。在Cortex-M内核的MCU中,short为16位,long为32位。
FreeRtOS中的数据类型重定义 |
---|
/* Type definitions. */
#define portCHAR char
#define portFLOAT float
#define portDOUBLE double
#define portLONG long
#define portSHORT short
#define portSTACK_TYPE uint32_t
#define portBASE_TYPE long
typedef portSTACK_TYPE StackType_t;
typedef long BaseType_t;
typedef unsigned long UBaseType_t;
#if ( configUSE_16_BIT_TICKS == 1 )
typedef uint16_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
/* 32-bit tick type on a 32-bit architecture, so reads of the tick count do
* not need to be guarded with a critical section. */
#define portTICK_TYPE_IS_ATOMIC 1
#endif
/*------------------------------------------------------*/
在FreeRTOS中,定义变量的时候往往会把变量的类型当作前缀加在变量上,这样的好处是让用户一看到这个变量就知道该变量的类型。比如char型变量的前缀是c,short型变量的前缀是s,long型变量的前缀是l,portBASE_TYPE类型变量的前缀是x。还有其他的数据类型,比如数据结构,任务句柄,队列句柄等定乂的变量名的前缀也是ⅹ。如果一个变量是无符号型的那么会有一个前缀u,如果是一个指针变量则会有一个前缀p。因此,当我们定义一个无符号的char型变量的时候会加一个u前缀,当定义一个char型的指针变量的时候会有一个pc前缀。
函数名包含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数则会加一个prv(private)的前缀。特别的,在函数名中加入了函数所在的文件名,这大大的帮助了用户提高寻找函数定义的效率和了解函数作用的目的,具体的举例如下
宏均是由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义,部分举例具体见下表
这里有个地方要注意的是信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。
在贯穿FreeRTOS的整个代码中,还有几个通用的宏定义我们也要注意下,都是表示0和1的宏
TAB键盘等于四个空格键。我们在编程的时候最好使用空格键而不是使用TAB键,当两个编译器的TAB键设置的大小不一样的时候,代码移植的时候代码的格式就会变乱,而使用空格键则不会出现这种问题。
在文件作用域范围的函数前缀为 prv(一般定义是 static)
API 函数的前缀为它们的返回类型,当返回为空时,前缀为 v
返回值类型 + 所在文件 + 功能名称。比如:
vTaskDelete 该函数返回值为 void 型,定义在 tasks.c,作用是 delete。
vTaskPrioritySet()函数的返回值为 void 型,定义在 tasks.c,函数作用是PrioritySet 设置优先级。
xQueueReceive()函数的返回值为 portBASE_TYPE 型,在 queue.c 这个文件中定义,函数作用是 receive 接收。
vSemaphoreCreateBinary()函数的返回值为 void 型,在 Semaphore.h 这个文件中定义,函数作用是 CreateBinary。
RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。
1、OS要做的事情很简单,核心就是任务管理(线程管理)在RTOS这种简单OS中,任务指的就是线程。
2、控制硬件时,应用代码直接调用底层代码,不经过OS的转换,RTOS只进行任务管理和存储管理
在复杂OS中任务就是进程,但是在RTOS这种简单实时OS中不存在进程这个东西,有的只是线程,因此在RTOS中任务就是线程。操作系統是以进程为单位执行任务,进程与线程的关系:进程是线程的容器。
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
我们生活中有许许多多关于进程与线程的小栗子,比如:1.我们使用打开一个微信软件,这个时候就开启了一个进程,
当我们在微信里面进行各种操作(查看朋友圈,扫一扫…),这么多的操作就是线程。
所以我们可以说“进程”是包含“线程”的,“线程”是“进程”的一个子集。*
RTOS其实就是一个线程管理器,所有的线程并发运行,RTOS的“任务管理”会负责线程的调度,因此RTOS就是一个简单的线程管理器,事实上线程的实现原理并不复杂。
每一个线程本质就是函数,当该函数在没有被注册为线程时就是一个普通的函数,我们可以以普通函数的方式去调用,当注册为线程之后就是一个线程函数,线程的特定就是并发运行。
所谓并发运行就是某个线程函数运行了一段时间后就会切换运行其它线程,然再切换运行其它线程,然后再切换回原来的线程接着运行,由于每个线程运行的时间片很短,而且切换的速度又非常快,因此在宏观上我们会感觉到所有的线程都在同时运行,这就是并发运行。
从A线程切换运行其它线程时(vTaskStartScheduler()就是用来开启任务调度的),为了保证能够再一次切换回A线程上,切换时必须保存A线程被中断处的信息(TCB任务控制块来保存),然后才能通过中断信息返回,实际上我们调用普通函数时,为能够返回到调用处(中断处),也必须保存被调用中断处的信息,道理其实都是一样的。
定时器所定的时间片到后,PC就会指向下一个线程的中断处(或者最开始处),CPU开始切换运行该线程的指令,OS所用的定时器就是我们以前课程所介绍过的systick定时器,systick定时器就是专门用来给RTOS干这个事情的。
不过为了让任务能够处理实时性的事件,凡是处理实时性事件的高优先级任务,可以不等当前线程的时间片到而直接抢占CPU运行,总之高优先级的线程(任务)可以抢占低优先级的CPU资源,以保证能够实时的处理实时性事件。
CPU从当前线程切换运行其它线程时,到底切换到哪一个线程,以及高优先级线程如何实现抢占,这都是由RTOS的任务管理来实现的,所以说RTOS的本质其实就是一个线程(任务)管理器。
线程ID:
每个线程(任务)都有一个唯一识别号,这个识别号就是线程ID,任务管理器就是通过这个ID来识别和管理线程的。
Demo: 例程和内核源码
License: 这里面只有一个许可文件“license.txt”,用 FreeRTOS 做产品的话就需要看看这个文
件,但是我们是学习 FreeRTOS,所以暂时不需要理会这个文件。
Source: 文件夹里面包含的是 FreeRTOS 内核的源代码
普通的库函数版本工程
新建名称叫FreeRTOS文件夹
打开FreeRTOS文件夹,并在里面创建src文件夹
在刚刚下载的FreeRTOS源码文件夹下,FreeRTOS/Source 文件夹选中的部分全部复制到工程文件夹的FreeRTOS/src中
在工程文件夹下的FreeRTOS中建立port文件夹
将源码中FreeRTOS/Source中的include文件夹复制到工程文件夹FreeRTOS/port中,同时在源码FreeRTOS/Source/portable中的RVDS和MemMang文件夹复制到工程文件夹FreeRTOS/port中。
在源码 .\FreeRTOSv202112.00\FreeRTOS\Demo\CORTEX_STM32F103_Keil文件夹中,找到FreeRTOSConfig.h并复制到工程下的User文件夹。
用Keil5打开移植好的工程,创建工程文件夹FreeRTOS/src和FreeRTOS/port
在工程下的\FreeRTOS\src文件夹添加全部的.c文件
在工程下的\FreeRTOS\port\RVDS\ARM_CM3文件夹添加port.c
在工程下的\FreeRTOS\port\MemMang文件夹添加heap_4.c
在工程下的User下添加FreeRTOSConfig.h
最后指定头文件路径
最后编译一下,会看到出现一个错误
在错误报告中看到xTaskGetCurrentTaskHandle() 函数未定义,在FreeRTOS.h中搜索xTaskGetCurrentTaskHandle
看到206行的位置将0改为1即可,最后编译
完美通过!!!
最后工程移植好了,剩下的就是代码的事情了。
/* 板级外设头文件 */
#include "stm32f10x.h"
#include "sys.h"
#include "bsp_led.h"
#include "bsp_usart.h"
/* FreeRTOS 相关头文件 */
#include "FreeRTOS.h"
#include "task.h"
/* 任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
static TaskHandle_t LED1_Task_Handle = NULL;
static TaskHandle_t LED2_Task_Handle = NULL;
// 声明函数
static void LED1_Task(void *parameter);
static void LED2_Task(void *parameter);
static void AppTaskCreate(void);
static void BSP_Init(void);
int main()
{
BaseType_t xReturn = pdPASS; // 可以理解为任务ID
BSP_Init(); // 开发板硬件初始化
printf("这是一个[野火]-STM32霸道开发板-FreeRTOS-动态创建多任务!\r\n");
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate", /* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle); /* 任务控制块指针 */
if(pdPASS == xReturn)
vTaskStartScheduler(); // 开启调度器
else
return -1;
while(1);
}
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS; // 任务ID
taskENTER_CRITICAL(); // 进入临界区
/* 创建LED1_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
(const char* )"LED1_Task", /* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED1_Task_Handle); /* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED1_Task任务成功!\r\n");
/* 创建LED2_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
(const char* )"LED2_Task", /* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED2_Task_Handle); /* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED1_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); // 删除AppTaskCreate任务
taskEXIT_CRITICAL(); // 退出临界区
}
static void LED1_Task(void *parameter)
{
while(1)
{
LED1_ON; // LED1 高电平
vTaskDelay(500); // 阻塞500ms
printf("LED_Task Running,LED1_ON\r\n");
LED1_OFF; // LED1 低电平
vTaskDelay(500); // 阻塞500ms
printf("LED_Task Running,LED1_OFF\r\n");
}
}
static void LED2_Task(void *parameter)
{
while(1)
{
LED2_ON; // LED1 高电平
vTaskDelay(500); // 阻塞500ms
printf("LED_Task Running,LED2_ON\r\n");
LED2_OFF; // LED1 低电平
vTaskDelay(500); // 阻塞500ms
printf("LED_Task Running,LED2_OFF\r\n");
}
}
static void BSP_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
// LED初始化
LED_GPIO_Config();
// 串口初始化
USART_Config();
}
在裸机系统中,系统的主体就是main函数里面顺序执行的无限循环,这个无限循环里面CPU按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务,也可以称之为线程。
每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一个死循环中,也不会退出。FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一条”return”语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删除。
FreeRTOS 中的任务是抢占式调度机制,高优先级的任务可打断低优先级任务,低优先
级任务必须在高优先级任务阻塞或结束后才能得到调度。同时 FreeRTOS 也支持时间片轮
转调度方式,只不过时间片的调度是不允许抢占任务的 CPU 使用权。
什么是任务,在 FreeRTOS 中,任务就是一个函数
void Task1(void *parameter)
{
while(1)
{
/* 任务主体 */
vTaskDelay(20); // 阻塞函数,阻塞20ms
}
}
注意:
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为 word,10 表示40 字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个 C 函数。它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS 内部不使用它,仅仅起调试作用。长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。单位是 word,比如传入 100,表示栈大小为 100 word,也就是 400 字节。最大值为 uint16_t 的最大值。怎么确定栈的大小,并不容易,很多时候是估计。精确的办法是看反汇编码。 |
pvParameters | 调用 pvTaskCode 函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1)数值越小优先级越低,如果传入过大的值,xTaskCreate 会把它调整为(configMAX_PRIORITIES –1) |
pxCreatedTask | 用来保存 xTaskCreate 的输出结果:task handle。以后如果想操作这个任务,比如修改它的优先级,就需要这个 handle。如果不想使用该 handle,可以传入 NULL。 |
返回值 | 成功:pdPASS;失败:errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)注意:文档里都说失败时返回值是 pdFAIL,这不对。pdFAIL 是 0,errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 是-1。 |
示例:
// 任务1:
void Task1(void *parameter)
{
const char *pcTaskName = "T1 run!\n";
volatile uint32_t ul; // volatile 用来避免被优化掉
while(1)
{
printf(pcTaskName);
vTaskDelay(1000); // 阻塞1000ms
}
}
// 任务2:
void Task2(void *parameter)
{
const char *pcTaskName = "T2 run!\n";
volatile uint32_t ul; // volatile 用来避免被优化掉
while(1)
{
printf(pcTaskName);
vTaskDelay(1000); // 阻塞1000ms
}
}
main() 函数
int main()
{
BSP_Init();
xTaskCreate(vTask1, "Task 1", 512, NULL, 1, NULL);
xTaskCreate(vTask2, "Task 2", 512, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
while(1);
}
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 任务句柄,使用 xTaskCreate 创建任务时可以得到一个句柄。也可传入 NULL,这表示删除自己。 |
怎么删除任务?举个不好的例子:
注意:
例程:
创建任务 1:任务 1 的大循环里,创建任务 2,然后休眠一段时间
任务 2:打印一句话,然后就删除自己
// task1
void vTask1( void *pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS(100UL);
BaseType_t ret;
/* 任务函数的主体一般都是无限循环 */
while(1)
{
/* 打印任务的信息 */
printf("Task1 is running\r\n");
ret = xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
if (ret != pdPASS)
printf("Create Task2 Failed\r\n");
// 如果不休眠的话, Idle 任务无法得到执行
// Idel 任务会清理任务 2 使用的内存
// 如果不休眠则 Idle 任务无法执行, 最后内存耗尽
vTaskDelay( xDelay100ms );
}
// task2
void vTask2( void *pvParameters )
{
while(1)
{
/* 打印任务的信息 */
printf("Task2 is running and about to delete itself\r\n");
// 可以直接传入参数 NULL, 这里只是为了演示函数用法
vTaskDelete(xTask2Handle);
vTaskDelay(2); // 阻塞2ms
}
}
// main
int main( void )
{
BSP_Init();
xTaskCreate(vTask1, "Task 1", 1000, NULL, 1, NULL);
/* 启动调度器 */
vTaskStartScheduler();
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
while(1);
}
一句话:
使用 uxTaskPriorityGet 来获得任务的优先级:
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask );
参数:xTask 如果为NULL表示获取自己当前任务的优先级
使用 vTaskPrioritySet 来设置任务的优先级:
void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
参数:
xTask:为NULL时表示设置自己当前的任务优先级
uxNewPriority:取值范围是 0~(configMAX_PRIORITIES – 1)。
FreeRTOS中的任务永远处于下面几个状态中的某一个:
结合上图:
阻塞态:
vTaskDelay(): 至少等待指定个数的 Tick Interrupt 才能变为就绪状态,叫相对延时
vTaskDelayUntil(): 等待到指定的绝对时刻,才能变为就绪态,叫绝对延时
通过阻塞实现同步事件的来源:
队列 (queue)
二进制信号量 (binary semaphores)
计数信号量 (counting semaphores)
互斥量 (mutexes)
递归互斥量、递归锁 (recursive mutexes)
事件组 (event groups)
任务通知 (task notifications)
有时候我们需要暂停某个任务的运行,过一段时间以后在重新运行。这个时候要是使用任务删除和重建的方法的话那么任务中变量保存的值肯定丢失了!
如果想要使用任务挂起函数 vTaskSuspend()则必须将宏定义INCLUDE_vTaskSuspend 配置为 1。
void vTaskSuspend( TaskHandle_t xTaskToSuspend )
参数 | 描述 |
---|---|
xTaskToSuspend | 挂起指定任务的任务句柄,任务必须为已创建的任务,可以通过传递 NULL 来挂起任务自己。 |
挂起所有任务的函数
void vTaskSuspendAll( void )
参数 | 描述 |
---|---|
void | 挂起所有任务实际上就是将调度器锁定,调度器被挂起后则不能进行上下文切换,但是中断还是使能的。 当调度器被挂起的时候,如果有中断需要进行上下文切换, 那么这个任务将会被挂起,在调度器恢复之后才执行切换任务。 调度器恢复可以调 用 xTaskResumeAll() 函数,调 用了多少次 的 vTaskSuspendAll() 就 要调用多少 次xTaskResumeAll()进行恢复 |
注意:
无论任务是什么状态都可以被挂起,只要调用了 vTaskSuspend()这个函数就会挂起成功,不论是挂起其他任务还是挂起任务自身。
挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。
如果想要使用任务恢复函数 vTaskResume()则必须将宏定义INCLUDE_vTaskSuspend 配置为 1
void vTaskResume( TaskHandle_t xTaskToResume )
参数 : | 描述 |
---|---|
xTaskToResume | 恢复指定任务的任务句柄。恢复的任务进入就绪态,如果任务是最高优先级则进入运行态,这一切都是由调度器来决定 |
注意:
基本概念:
FreeRTOS 中提供的任务调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的。在系统中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被换出,高优先级任务抢占处理器运行。
团队成员之间要协调工作进度(同步)、争用会议室(互斥)、沟通(通信)。
再举一个例子。在团队活动里,同事 A 先写完报表,经理 B 才能拿去向领导汇报。经理 B 必须等同事 A 完成报表,AB 之间有依赖,B 必须放慢脚步,被称为同步。在团队活动中,同事 A 已经使用会议室了,经理 B 也想使用,即使经理 B 是领导,他也得等着,这就叫互斥。经理 B 跟同事 A 说:你用完会议室就提醒我。这就是使用"同步"来实现"互斥"。
能实现同步、互斥的内核方法有:任务通知(task notification)、队列(queue)、事件组(event group)、信号量(semaphoe)、互斥量(mutex)。
它们都有类似的操作方法:获取/释放、阻塞/唤醒、超时。比如:
A 获取资源,用完后 A 释放资源
A 获取不到资源则阻塞,B 释放资源并把 A 唤醒
A 获取不到资源则阻塞,并定个闹钟;A 要么超时返回,要么在这段时间内因为 B 释放资源而被唤醒。
队列:
里面可以放任意数据,可以放多个数据
任务、ISR 都可以放入数据;任务、ISR 都可以从中读出数据
事件组:
一个事件用一 bit 表示,1 表示事件发生了,0 表示事件没发生
可以用来表示事件、事件的组合发生了,不能传递数据
有广播效果:事件或事件的组合发生了,等待它的多个任务都会被唤醒
信号量:
核心是"计数值"
任务、ISR 释放信号量时让计数值加 1
任务、ISR 获得信号量时,让计数值减 1
任务通知:
核心是任务的 TCB 里的数值
会被覆盖
发通知给谁?必须指定接收任务
只能由接收任务本身获取该通知
互斥量:
数值只有 0 或 1
谁获得互斥量,就必须由谁释放同一个互斥量
队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定阻塞的任务时间 xTicksToWait,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态。消息队列是一种异步的通信方式。