智果芯
服务于百万大学生和电子工程师!

如何快速上手一门物联网操作系统FreeRTOS?

摘要RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。

FreeRTOS的编程风格

学习一个RTOS,搞懂它的编程的风格很重要,这可以大大提供我们阅读代码的效率。下面我们就以FreeRTOS里面的数据类型、变量名、函数名和宏这几个方面做简单介绍。

1、数据类型

FreeRTOS中,使用的数据类型虽然都是标准C里面的数据类型,但是针对不同的处理器,对标准C的数据类型又进行了重定义,给它们取了一个新的名字,比如char重新定义了一个名字 porCHAR,这里面的port表示接口的意思,就是FreeRTOS要移植到这些处理器上需要这些接口文件来把它们连接在一起。但是用户在写程序的时候并非一定要遵循 FreeRTOS的风格,我们还是可以直接用C语言的标准类型。在FreeRTOS中,int型从不使用,只使用shortlong型。在Cortex-M内核的MCU中,short为16位,long为32位。

FreeRTOS中详细的数据类型重定义在portmacro.h这个头文件中实现。

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

/*------------------------------------------------------*/

在编程的时候,如果用户没有明确指定char的符号类型,那么编译器会默认的指定char型的变量为无符号或者有符号。正是因为这个原因,在FreeRtos中,我们都需要明确的指定变量char是有符号的还是无符号的。在keil中,默认char是无符号的,但是也可以配置为有符号的,具体配套过程见下图。

勾选后char就变成了有符号的了

2、变量名

FreeRTOS中,定义变量的时候往往会把变量的类型当作前缀加在变量上,这样的好处是让用户一看到这个变量就知道该变量的类型。比如char型变量的前缀是cshort型变量的前缀是slong型变量的前缀是lportBASE_TYPE类型变量的前缀是x。还有其他的数据类型,比如数据结构,任务句柄,队列句柄等定乂的变量名的前缀也是ⅹ。如果一个变量是无符号型的那么会有一个前缀u,如果是一个指针变量则会有一个前缀p。因此,当我们定义一个无符号的char型变量的时候会加一个u前缀,当定义一个char型的指针变量的时候会有一个pc前缀

3、函数名

函数名包含了函数返回值的类型、函数所在的文件名和函数的功能,如果是私有的函数则会加一个prv(private)的前缀。特别的,在函数名中加入了函数所在的文件名,这大大的帮助了用户提高寻找函数定义的效率和了解函数作用的目的,具体的举例如下

1、vTaskPrioritySet()函数的返回值为void型,在task.c这个文件中定义。

2、xQueueReceive()函数的返回值为portBASE_TYPE型(long),在queue.c这个文件中定义。

3、vSmaphoreCreateBinary()函数的返回值为void型,在semper.h这个文件中定义。

4、宏

宏均是由大写字母表示,并配有小写字母的前缀,前缀用于表示该宏在哪个头文件定义,部分举例具体见下表

FreeRtoS宏定义举例

这里有个地方要注意的是信号量的函数都是一个宏定义,但是它的函数的命名方法是遵循函数的命名方法而不是宏定义的方法。

在贯穿FreeRTOS的整个代码中,还有几个通用的宏定义我们也要注意下,都是表示0和1的宏

FreeRTOS通用宏定义

5、格式

TAB键盘等于四个空格键。我们在编程的时候最好使用空格键而不是使用TAB键,当两个编译器的TAB键设置的大小不一样的时候,代码移植的时候代码的格式就会变乱,而使用空格键则不会出现这种问题。

二、RTOS的简单介绍

RTOS很简单,听起来叫做实时操作系统,有一点吓唬人。但是学起来真的很简单,你不要把他想象的太复杂。这玩意其实就是一个任务调度器,在裸机中程序只有一个死循环,但是使用了RTOS程序中就有了多个死循环,RTOS就是调度每个死循环依次执行,执行的速度很快,看起来就相当于并行执行。

既然叫RTOS肯定是实时操作系统,与他相对应的就叫做OS操作系统,一个实时一个非实时。实际上不管是实时的还是非实时的,事件的响应都是有时间延时的,但是非实时的延时比较长而实时则很短,这种长短对于人来并不敏感,但是对于设备来说却很敏感,特别是对实时性要求很高的设备来说,实时性与否将会非常关键。所以你在初学时不要纠结实时还是非实。

我们在单片机或者其他MCU上使用的都是实时操作系统RTOS,常见实时OS有VXworks、NUcleus、QNX、RT-Linux、Ucos、RTX、embos、FreeRTOS、MQX、Ali0S、LiteOS,其中Ucos、FreeRTOS为比较常用的实时OS,而RTX是ARM公司自己推出的实时OS。常见非实时OS消费类电子的民用产品比如PC、手机、平板等所用OS大多是非实时的,这类产品常用的OS有windows、unⅸx、linux、OS、安卓(基于Linux)。

三、RTOS的特点

1、OS要做的事情很简单,核心就是任务管理(线程管理)在RTOS这种简单OS中,任务指的就是线程。

2、控制硬件时,应用代码直接调用底层代码,不经过OS的转换,RTOS只进行任务管理和存储管理。

任务与进程、线程的关系

在复杂OS中任务就是进程,但是在RTOS这种简单实时OS中不存在进程这个东西,有的只是线程,因此在RTOS中任务就是线程

RTOS的本质

1、其实一个线程管理器

RTOS其实就是一个线程管理器,所有的线程并发运行,RTOS的“任务管理”会负责线程的调度,因此RTOS就是一个简单的线程管理器,事实上线程的实现原理并不复杂。

2、RTOS线程的实现原理

每一个线程本质就是函数,当该函数在没有被注册为线程时就是一个普通的函数,我们可以以普通函数的方式去调用,当注册为线程之后就是一个线程函数,线程的特定就是并发运行。

所谓并发运行就是某个线程函数运行了一段时间后就会切换运行其它线程,然再切换运行其它线程,然后再切换回原来的线程接着运行,由于每个线程运行的时间片很短,而且切换的速度又非常快,因此在宏观上我们会感觉到所有的线程都在同时运行,这就是并发运行

A线程切换运行其它线程时(vTaskStartScheduler()就是用来开启任务调度的),为了保证能够再一次切换回A线程上,切换时必须保存A线程被中断处的信息(TCB任务控制块来保存),然后才能通过中断信息返回,实际上我们调用普通函数时,为能够返回到调用处(中断处),也必须保存被调用中断处的信息,道理其实都是一样的。

时间片

定时器所定的时间片到后,PC就会指向下一个线程的中断处(或者最开始处),CPU开始切换运行该线程的指令,OS所用的定时器就是我们以前课程所介绍过的systick定时器,systick定时器就是专门用来给RTOS干这个事情的。

不过为了让任务能够处理实时性的事件,凡是处理实时性事件的高优先级任务,可以不等当前线程的时间片到而直接抢占CPU运行,总之高优先级的线程(任务)可以抢占低优先级的CPU资源,以保证能够实时的处理实时性事件。

CPU从当前线程切换运行其它线程时,到底切换到哪一个线程,以及高优先级线程如何实现抢占,这都是由RTOS的任务管理来实现的,所以说RTOS的本质其实就是一个线程(任务)管理器。

线程ID

每个线程(任务)都有一个唯一识别号,这个识别号就是线程ID,任务管理器就是通过这个ID来识别和管理线程的。

掌握了这些知识,然后我们再去看用FreeRTOS写得代码,心中就清楚多了。

四、什么是任务

在裸机系统中,系统的主体就是main函数里面顺序执行的无限循环,这个无限循环里面CPU按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务,也可以称之为线程。

void task1(void *pvParameters)
{
   /* 任务主体,无限循环且不能返回 */
    for(;;)
    {
        /* 任务主体代码 */
        vTaskDelay( 2000 );
    }
}

五、任务状态

FreeRTOS中的任务永远处于下面几个状态中的某一个:

1、运行态

当一个任务正在运行时,那么就说这个任务处于运行态,处于运行态的任务就是当前正在使用处理器的任务。如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处于运行态。

2、就绪态

处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起),可以运行的任务,但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!

3、阻塞态

如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态,比如说如果某个任务调用了函数 vTaskDelay()的话就会进入阻塞态,直到延时周期完成。任务在等待队列、信号量、事件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!

4、挂起态

像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态,但是进入挂起态的任务没有超时时间。任务进入和退出挂起态通过调用函数VTaskSuspendo和xTask_Resumed。

任务状态之间的转换

六、任务优先级

每个任务都可以分配一个从0-(configMAX_PRIORITIES-1)的优先级,configMAX_PRIORITIES在文件FreeRTOSConfig.h中有定义,前面我们讲解 FreeRTos系统配置的时候已经讲过了。如果所使用的硬件平台支持类似计算前导零这样的指令(可以通过该指令选择下一个要运行的任务,Cortex-M处理器是支持该指令的),并且宏configUSE_PORT_OPTIMISED_TASK_SELECTION也设置为了1,那么宏configMAX_PRIORITIES不能超过32!也就是优先级不能超过32级。其他情况下宏configMAX_PRIORITIES可以为任意值,但是考虑到RAM的消耗,宏configMAX_PRIORITIES最好设置为一个满足应用的最小值。

FreeRTOSConfig.h

优先级数字越低表示任务的优先级越低,0的优先级最低,configMAX_PRIORITIES-1的优先级最高。空闲任务的优先级最低,为0。

FreeRTOS调度器确保处于就绪态或运行态的高优先级的任务获取处理器使用权,换句话说就是处于就绪态的最高优先级的任务才会运行。当宏configUSE_TIME_SLICING定义为1的时候多个任务可以共用一个优先级,数量不限。默认情况下宏configUSE_TIME_SLICING在文件FreeRTOS.h中已经定义为1。此时处于就绪态的优先级相同的任务就会使用时间片轮转调度器获取运行时间。为了方便也可以定义到FreeRTOSConfig.h中。

七、任务控制块

FreeRTOS的每个任务都有一些属性需要存储,FreeRTOS把这些属性集合到一起用一个结构体来表示,这个结构体叫做任务控制块:TCB_t,在使用函数 xTaskCreate()创建任务的时候就会自动的给每个任务分配一个任务控制块。在老版本的FreeRTOS中任务控制块叫做tskTCB,新版本重命名为TCB_t,但是本质上还是tskTCB,本教程后面提到任务控制块的话均用TCB_t表示,此结构体在文件tasks.c中有定义,如下

任务控制块通俗的理解是:FreeRTOS的有很多任务对吧,任务1运行到时间片的时间后,赶紧去运行任务2,然后在运行任务3。几个任务一遍跑完了之后,又回过头来在接住上一次运行的地方接着跑,但是接着上一次哪一个地方开始跑呢?是不是需要用一个东西把上次运行的地方信息保存一下,而这个东西就是任务控制块:TCB_t

八、任务堆栈

FreeRTOS之所以能正确的恢复一个仼务的运行就是因为有任务堆栈在保驾护航,任务调度器在进行仼务切换的时候会将当前任务的现场(CPU寄存器值等)保存在此仼务的任务堆栈中,等到此任务下次运行的时候就会先用堆栈中保存的值来恢复现场,恢复现场以后仼务就会接着从上次中断的地方开始运行。

创建任务的时候需要给任务指定堆栈,如果使用的函数xTaskCreated创建任务(动态方法)

的话那么任务堆栈就会由函数xTaskCreate()自动创建,后面分析xTaskCreate()的时候会讲解。如果使用函数xTaskCreateStatic()创建仼务(静态方法)的话就需要程序员自行定义仼务堆栈,然后堆栈首地址作为函数的参数puxStackBuffer传递给函数,如下

注意:我们不管是使用函数xTaskCreate()还是xTaskCreateStatic()创建任务都需要指定任务堆栈大小。任务堆栈的数据类型为StackType_tStackType_t本质上是uint32_t,在 portmacro.h中有定义,如下:

#define portSTACK_TYPE    uint32_t;//宏定义portSTACK_TYPE为 uint32_t
typedef portSTACK_TYPE   StackType_t;//起别名,然后再给portSTACK_TYPE起一个StackType_t的名字

这里为啥要讲任务的优先级和任务的堆栈呢?

因为我们在创建一个任务是这两点是必须要设置的,不管你动态创建任务还是静态创建任务优先级和堆栈大小都要提前声明。所以这里讲一下,到后面看程序就清楚多了。

#define START_TASK_PRIO        1               //任务优先级
#define START_STK_SIZE         128         //任务堆栈大小
TaskHandle_t StartTask_Handler;        //任务句柄

//创建开始任务
xTaskCreate((TaskFunction_t )start_task,            //任务函数
            (const char*    )"start_task",          //任务名称
            (uint16_t       )START_STK_SIZE,        //任务堆栈大小
            (void*          )NULL,                  //传递给任务函数的参数
            (UBaseType_t    )START_TASK_PRIO,       //任务优先级
            (TaskHandle_t*  )&StartTask_Handler);   //任务句柄   

在学习任何一个RTOS都要了解它的编程风格、区别于其他OS的特点、任务状态、任务优先级、任务控制块、和任务堆栈。如果一上来就看源码、学习API可能会有点不知所云,云里雾里。今天的分享就到这里,下期继续FreeRTOS的学习!

赞(1) 打赏
未经允许不得转载:智果芯 » 如何快速上手一门物联网操作系统FreeRTOS?

评论 2

  1. #0

    keil的主题文件能分享一个吗(终于找到好看的了

    匿名3年前 (2021-10-12)回复
    • 在微信公众号:果果小师弟,后台回复 :MDK美化,即可获取配色文件。

      智果芯3年前 (2021-10-12)回复

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏