使用操作系统主要就是因为操作系统的管理功能,可以更加有效的管理硬件的资源,而且操作系统的多线程运行管理,是一个很棒的功能,之前只能再while(1)中执行的单线程的处理,现在可以将各个功能分开成多个任务,能够更加有效的进行任务的调用,而且实时操作系统可以有更快的任务相应速度,实时性很强,如果我们自己编写,很麻烦,而这有写好的,成品的操作操作系统,何必花大把的时间去自己编写,何不直接用成品呢!
用户角度看,任务的状态共有五种:休眠态、就绪态、运行态、等待态、中断服务态;
休眠态:指任务已存在寄存器中,但不受系统的管理,可以通过OSTaskCreate();创建任务接收系统的管理,当不需要这个任务时,可以调用OSTaskCreate();删除任务,删除实际上是使该代码无法获得CPU的使用权。
就绪态:任务准备运行时,就进入了就绪态,任务就绪表根据任务的优先级顺序对任务进行排序。
运行态:CPU会调用当前就绪态中优先级最高的任务,使其获得CPU的使用权,但是此时如果有更高优先级的任务就绪,CPU会立即收回任务,调用更高优先级的任务,使其获得CPU的使用权。
等待态:当用户调用使其进入等待某个事件的函数时,任务就会进入等待态,并自动放入等代表,直到其等待的事件发生,就会自动进入就绪态,并放入就绪表,当ucos-iii系统服务会判断这个就绪任务的优先级是否最高,如果是,CPU会剥夺当前的任务的CPU使用权,而刚就绪的任务会获得CPU的使用权。
中断服务态:CPU允许中断,当中断发生,由于中断服务程序的优先级最高,所以CPU会保存当前正在运行的任务的状态,然后进入中断服务程序,中断服务程序推荐写的尽量的少,最好只是发送某个消息、信号,某个任务在消息队列中收到消息后,任务会进入就绪态,此时中断服务程序结束,CPU查看任务就绪表中是否有更高优先级任务就绪,如果有,更高优先级的任务会进入到运行态,CPU会进入到更高优先级任务运行,如果就绪表中没有更高优先级的任务,CPU会恢复到之前运行的任务的状态,恢复现场,回到之前运行的任务继续运行。
ucos-iii将最低优先级分配给了空闲任务,最高优先级分配给了中断队列处理任务。任务的优先级在ucos-iii中是以就绪任务优先级位映射表和就绪任务列表表现的。就绪表由两部分组成,一个就绪优先级位映射表, 用来记录哪个优先级下有任务就绪,一个就绪任务列表,记录每一个优先级下的所有任务。当一个优先级下有任务就绪,那么对应的优先级位就会置一。采用这样的结构,再使用计算前导零,就会很迅速的计算出哪个优先级下有任务就绪。
任务调度就是中止当前正在运行的任务转而去执行其他的任务。
任务调度的类型可以分为可剥夺型调度,时间片轮转调度。ucos-iii中使用的主要是可剥夺型调度,只有当同一优先级下有多个任务时,才会使用时间片轮转调度。
UCOSIII是可剥夺型内核,因此当一个高优先级的任务准备就绪,并且此时发生了任务调度,那么这个高优先级的任务就会获得CPU的使用权!
UCOSIII中的任务调度是由任务调度器来完成的,任务调度器有2种:任务级调度器和中断级调度器。
任务级调度器为函数OSSched()。 调度器和任务切换执行期间中断是关闭的
中断级调度器为函数OSIntExit(),当退出外部中断服务函数的时候使用中断级任务调度。
可剥夺型任务调度又分为直接发布和延迟发布两种。
直接发布:
任务A正在运行,外设产生中断请求,该请求对应的中断服务程序运行,关闭中断,中断服务程序向外发布消息或者信号,开启中断,任务B正在等待该消息,任务B收到消息进入就绪态,任务调度器查找任务就绪表中优先级最高的任务,如果没有比A优先级更高的任务,就运行任务A,否则转而运行更高效优先级的任务。此过程中关中断来保护发布消息或者信号的过程。发布消息的代码放在了中断级运行。
延迟发布:
任务A正在运行,外设产生中断请求,该请求对应的中断服务程序运行,中断服务程序将要发布的函数调用和相关参数或者信号或消息存入“中断队列”,关闭中断,“中断队列处理任务”开始运行,将发布函数调用和相关参数提取出来,重新开启中断,锁定任务调度器,将发布函数调用和相关参数发布出去,挂起自身,解锁任务调度器。任务调度器查找任务就绪表中优先级最高的任务,如果没有比A优先级更高的任务,就运行任务A,否则转而运行更高效优先级的任务。此过程中关中断保护的是“中断队列处理任务”从“中断队列”提取发布函数调用信息的过程,锁定任务调度器是保护“中断队列处理任务”发布函数调用信息的过程。延迟发布巧妙的将发布函数调用信息的代码放在了任务级来操作,虽然减少了关中断时间,但是也延长了任务时间。注意:中断队列处理任务是系统内部任务,任务的优先级为最高(0);
UCOSIII允许一个优先级下有多个任务,每个任务可以执行指定的时间(时间片),然后轮到下一个任务,这个过程就是时间片轮转调度,当一个任务不想在运行的时候就可以放弃其时间片。
时间片轮转调度器为:OS_SchedRoundRobin()。
如图,3个同一优先级任务,每个任务都是4个时间片。
1、释放信号量或者发送消息,也可通过配置相应的参数不发生任务调度。
2、使用延时函数OSTimeDly()或者OSTimeDlyHMSM()。
3、任务等待的事情还没发生(等待信号量,消息队列等)。
4、任务取消等待。
5、创建任务。
6、删除任务。
7、删除一个内核对象。
8、任务改变自身的优先级或者其他任务的优先级。
9、任务通过调用OSTaskSuspend()将自身挂起。
10、任务解挂某个挂起的任务。
11、退出所有的嵌套中断。
12、通过OSSchedUnlock()给调度器解锁。
13、任务调用OSSchedRoundRobinYield()放弃其执行时间片。
14、用户调用OSSched()。
当UCOSIII需要切换到另外一个任务时,它将保存当前任务的现场到当前任务的堆栈中,主要是CPU寄存器值,然后恢复新的现场并且执行新的任务,这个过程就是任务切换。
任务切换分为两种:任务级切换和中断级切换。
任务级切换函数为:OSCtxSw()。
中断级切换函数为:OSIntCtxSw()。
两者的不同点就是,任务级切换有两步,第一步保存当前CPU运行的任务的状态,将其堆栈压入RAM中,第二步将新任务的栈压入CPU。而中断级是假设当前运行的任务的状态已经被保存过了,所以只做了将中断的堆栈压入CPU中。两者的堆栈指针也不同,任务有任务的堆栈区,中断有中断自己的堆栈区。
任务同步的方式有:单向同步和双向同步,双向不同不能被用来ISR和任务之间的同步。
任务同步的方法有:
- 信号量–更多被用来实现任务间的同步以及任务和ISR之间的同步。
- 任务信号量–使用的是任务内嵌的信号量,
- 事件标志组–任务与多个事件的发生同步,可以有“或”和“与”两种同步方式。
- 与多任务同步–通过广播信号量的方式使得多个任务同时开始执行。
任务同步的方式:
- 单向同步(信号量)–任务向任务,ISR向任务发送信号量
- 双向同步(任务信号量)–任务A正在执行,发布任务B的任务信号量,任务A通过等待内嵌信号量来与任务B同步,因为任务B还没有执行,任务A的执行被阻塞,等待任务A信号量发布。ucos-iii将任务B执行,发布任务A的任务信号量,因为任务B的任务信号量被发布了,所以任务B就与任务A同步了。
当一个任务等待信号量、互斥信号量、事件标志组、或者消息队列时,该任务就被加入任务挂起表,或者等待表中。
任务挂起表中的任务也是按照优先级排序的,高优先级的任务放在前面, 低优先级的任务放在后面。
以下是用到任务挂起表的几种内核对象,事件标志组、互斥信号量、信号量、消息。每个内核对象的头部都有三个相同的数据域,这三个数据域合起来叫做OS_PEND_OBJ
。
任务挂起表实际上并不指向任务的控制块OS_TCB,而是指向一个OS_PEND_DATA类型的数据结构,其在任务被放入任务挂起表时会被动态分配到改任务的堆栈空间中。
共享资源保护的方法有:
互斥型信号量–只有任务才能使用互斥型信号量,任务对共享资源的访问有截止时间,建议使用互斥型信号量。
1.1信号量
信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。
信号量用于控制对共享资源的保护,但是现在基本用来做任务同步用。
要想获取资源的任务必须执行“等待”操作,如果该资源对应的信号量有效值大于1,则任务可以获得该资源,任务继续运行。如果该信号量的有效值为0,则任务加入等待信号量的任务表中。如果等待时间超过某一个设定值,该信号量仍然没有被释放掉,则等待信号量的任务就进入就绪态,如果将等待时间设置为0的话任务就将一直等待该信号量
1.2关/开中断
独占共享资源最简单也是最快捷的方法就是关中断和开中断,当访问共享资源的速度很快,以至于访问共享资源所花的时间小于中断的关闭时间时,可以使用关、开中断方法。但是不推荐此方法,此方法会影响到中断延迟。
关、开中断是一个任务和一个中断服务程序共享变量或者数据结构的唯一方法。
1.3调度器上/解锁
如果某项任务不需要和中断服务程序共享变量或数据结构,可以使用调度器上锁、解锁的方法访问共享资源。 这样,关中断的时间被都降到最短,似的ucos-iii能够非常快速的响应中断请求。
在系统锁住调度器的期间,系统依然响应中断,如果中断唤醒了的更高优先级线程,调度器并不会立刻执行它,直到调用解锁调度器函数才尝试进行下一次调度。同中断锁一样把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和中断锁有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应。所以在使用调度器上锁的方式进行任务同步时,需要考虑好任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式进行同步。
中断:应内部或外部异步事件的请求中止当前任务,而去处理异步事件所要求的任务的过程叫做中断
中断优先级最高。
关闭中断后,CPU将忽略所有的中断请求,但终端控制器会将这些中断请求锁存,并在CPU重新打开后立即产生中断请求。
CPU处理中断的模式有两种:
作为一个RTOS操作系统,内存管理是必备的功能,因此UCOSIII也就内存管理能力。通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但是在嵌入式事实操作系统中最好不要这么做,多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。
UCOSIII中提供了一种替代malloc()和free()函数的方法,UCOSIII中将存储空间分成区和块,每个存储区有数量不等大小相同的存储块,在一个系统中可以有多个存储区。一般存储区是固定的,在程序中可以用数组来表示一个存储区,比如u8 buffer[20][10],就表示一个拥有20个存储块,每个存储块10个字节的存储区。
//定义一个存储区
OS_MEM INTERNAL_MEM;
//存储区中存储块数量
#define INTERNAL_MEM_NUM 5
//每个存储块大小
#define INTERNAL_MEMBLOCK_SIZE 100
//内存池使用内部RAM
CPU_INT08U Internal_RamMemp[INTERNAL_MEM_NUM][INTERNAL_MEMBLOCK_SIZE];
OSMemCreate((OS_MEM* )&INTERNAL_MEM,//需要在代码最前面定义,指向存储区
(CPU_CHAR* )"Internal Mem",//存储区名字
(void* )&Internal_RamMemp[0][0],//存储区基地址
(OS_MEM_QTY )INTERNAL_MEM_NUM,//有多少个存储块可用,建议宏定义
(OS_MEM_SIZE)INTERNAL_MEMBLOCK_SIZE,//每个存储块大小,建议宏定义
(OS_ERR* )&err);
//申请存储块
OSMemGet((OS_MEM*)&INTERNAL_MEM,
(OS_ERR*)&err);
//释放存储块
OSMemPut((OS_MEM* )&INTERNAL_MEM,
(void* )internal_buf,
(OS_ERR* )&err);
如果某个内存区可以不用再被释放,可以使用malloc()来动态分配。当分区没有空闲的存储块时,就必须让任务等待空闲的任务块,但是ucos-iii不支持,不过我们可以使用信号量的方法间接的实现该功能。
UCOSIII中的任务是一个无限循环并且还是一个抢占式内核,为了使高优先级的任务不至于独占CPU,可以给其他优先级较低任务获取CPU使用权的机会,UCOSIII中除空闲任务外的所有任务必须在合适的位置调用系统提供的延时函数,让当前的任务暂停运行一段时间并进行一个任务切换。
延时函数有两种,OSTimeDly()和OSTimeDlyHMSM()。
OSTimeDly()函数有三种工作模式:相对模式、周期模式和绝对模式。
OSTimeDlyHMSM()函数仅在相对模式下工作。
延时任务任务可通过在其他任务中调用函数OSTimeDlyResume()取消延时而进入就绪状态,此函数最后会引发一次任务调度。
UCOSIII定义了一个CPU_INT32U类型的全局变量OSTickCtr来记录系统时钟节拍数,在调用OSInit()时被初始化为0,以后每发生1个时钟节拍,OSTickCtr加1。
OSTimeSet()允许用户改变当前时钟节拍计数器的值,慎用!!!!!
OSTimeGet()用来获取动迁时钟节拍计数器的值。
定时器本质是递减计数器,当计数器减到零时可以触发某种动作的执行,这个动作通过回调函数来实现。当定时器计时完成时,定义的回调函数就会被立即调用,应用程序可以有任意数量的定时器,UCOSIII中定时器的时间分辨率由一个宏OS_CFG_TMR_TASK_RATE_HZ,单位为HZ,默认为100Hz。
注意!一定要避免在回调函数中使用阻塞调用或者可以阻塞或删除定时器任务的函数。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方法直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
单次定时器从初始值(也就是OSTmrCreate()函数中的参数dly)开始倒计数,直到为0调用回调并停止。单次定时器的定时器只执行一次。
定时器也可以在定时完成前停止计时,也可以重新触发单次定时器的计时,这功能可以用在实现看门狗等安全守护程序。
创建定时器的时候我们可以设定为周期模式,当倒计时完成后,定时器调用回调函数,并重置计数器重新开始计时,一直循环性下去。如果在调用函数OSTmrCreate()创建周期定时器时让参数dly为0,那么定时器每个周期就是period。
周期定时器也可以设定为带初始延迟时间的运行模式,使用函数OSTmrCreate() 参数dly来确定第一个周期,以后的每个周期开始时将计数器值重置为period。
内核对象是指,任务、信号量、互斥型信号量、事件标志组、消息队列、定时器和存储块。
前面我们讲过都是等待单个内核对象,包括:信号量、互斥信号量、消息队列和事件标志等。在UCOS–III中允许任务同时等待多个信号量和多个消息队列,也就是说,UCOS–III不支持同时等待多个事件标志组或互斥信号量。
一个任务可以等待任意数量的信号量和消息队列,第一个信号量或消息队列的发布会导致该任务进入就绪态。
一个任务可以调用函数OSPendMulti()函数来等待多个对象,并且可以根据需要指定一个等待超时值,函数OSPendMulti(),举个例子
OSPendMulti((OS_PEND_DATA* )pend_multi_tbl,//需定义的数组,数组的例子在此函数下面
(OS_OBJ_QTY )CORE_OBJ_NUM, //内核对象数量
(OS_TICK )0, //0就是一直等待下去
(OS_OPT )OS_OPT_PEND_BLOCKING,//对象未发送时任务挂起等待,OS_OPT_NON_PEND_BLOCKING就是对象未发送直接返回
(OS_ERR* )&err); //同样是返回的错误信息
OS_PEND_DATA pend_multi_tbl[CORE_OBJ_NUM];//定义一个数组,数组大小推荐使用宏定义方式定义
pend_multi_tbl[0].PendObjPtr=(OS_PEND_OBJ*)&Test_Sem1;//等待信号Test_Sem1
pend_multi_tbl[1].PendObjPtr=(OS_PEND_OBJ*)&Test_Sem2;//等待信号Test_Sem2
pend_multi_tbl[2].PendObjPtr=(OS_PEND_OBJ*)&Test_Q;//等待消息Test-Q