
跟着需求学习才能学的更好,所以学习操作系统应当知道为什么要用到操作系统,当你的主函数while()循环中执行的任务过多时,难免会出现这样或那样的问题。那么引入操作系统可能会帮你解决这样的问题。 & b+ _6 o. [& [# g0 j- @: @5 W 无操作系统的程序主要是从主函数开始执行: 以上一期小车程序为例,它的程序执行流程如下: 主函数:
子函数处理比如:
然后将子函数的代入主函数中
没有操作系统下的单片机运行结构中,我们主要的程序设计思路是将我们的需求分成很多子函数,通过主循环依次调用子函数。在执行程序的时候,程序的执行顺序为,先执行相关资源的初始化,执行完毕后所配置的外设资源都已启动,然后进入while循环,先执行CarMove()这个函数,执行完毕之后回到主函数继续执行LCD1602Write()这个函数,如此循环。我们将相关外设资源的初始化称之为驱动层。然后在相关驱动完备的基础上,进入主循环调用相关的功能函数完成用户的需要的功能。我们对这种程序运行机制称之为单线程。 - Q Y0 v! z h% d; j1 T 但是在单线程机制下,主函数中的各个函数之间在程序运行时容易受到上一个函数的影响,如果上一个函数运行时间长,就会导致下面的函数无法执行。当进入一个函数时,必须要等这个函数执行完毕才能够退处再执行下一个函数,如果该函数执行时间过长,则会导致其他的函数都无法执行陷入“罢工”状态。可能大家会说采用中断可以解决这种问题,但是中断机制也仅仅是解决了程序运行的及时性问题,但是没有解决时间问题,因为当子程序中的中断触发,该子程序暂停执行,转而执行中断服务函数,中断服务函数执行完毕,又回到开始的中断点继续执行该子程序。因此中断的引入并没有改变其单线程的本质,中断事件类比于插队事件,一群人排队买票,中断事件就是突然有急事,要求插队先处理它的购票事件。处理完大家依旧按照原先的方式排队购票。(只有单片机的外设中断可以暂时打断子函数的执行。)
在没有操作系统下的单线程运行机制有两个问题,一个是每一个函数顺次调用,后面的函数总是要等前面的函数完成后才能执行,但是如果该函数执行时间较长,后面的函数都要等前面的函数执行完才能执行,好比堵车,前面的车不往前走,后面的车都在后面等着;二是每个函数运行时,只要程序从主循环里转到任何一个函数,这个函数在运行时又转到更底层的函数,在进行这一过程时只能等到调用的程序自然结束,方可执行完毕。 - _" t b; U( F7 `$ ? 我们运用单片机的本质就是操作芯片的相关资源去实现某种功能应用,但是一个项目通常不仅仅是需要单片机的某一个或者两个硬件资源,往往是操作多个硬件资源构成的函数事件相结合。这有可能造成的问题就是单个功能模块测试时功能正常,但多个功能模块合在一起交给单片机处理时就可能会导致原本功能正常的函数合在一起之后就不能正常工作了。究其原因就是不理解单线程的运行机制,很粗糙的将多个函数一股脑以串联的方式放进了主程序,子函数在主程序中堆砌的越长,主函数的执行效率越低。
因此我们要解决的主要问题是如何让含有多个子函数的主循环快速执行,各函数在执行各自程序时不受其他子函数的影响,引入操作系统就是解决上述问题。注:状态机也可以。
操作系统的引入 ]1 S: A; Y# a: C6 @ 操作系统可以视为第三方力量来管理各子函数的执行,比如操作系统让某一子函数执行相应的时间,如果该函数在操作系统给定的时间下没有执行完毕,则暂停转而运行下一个函数。 # B- m- K* k& u8 ]: A 操作系统是一套程序,这套程序可以对每个子函数的进程进行合理的干预,最常规的干预是让每一个进程按照某一节奏运行。 操作系统的基本原理CPU的结构:(寄存器R0-R15\PC\CPSR、运算器) 单片机核心器件是CPU,CPU里面核心的是运算器ALU,运算的数据存放在16个寄存器里,程序放在单片机ROM,ROM也有地址,单片机是通过PC程序计数器来存放CPU即将到ROM读取的程序的地址,利用CPSR当前程序状态寄存器,用来存放当前程序运算的状态。 时间片:是一个计时单位;时钟节拍Tick:也是一个计时单位,是操作系统最小的计时单位;一个时间片=n*Tick。
有了操作系统,每个功能都是自行封闭的函数,函数上加上了while(1)循环,也叫每一个进程都变成的死循环,故主函数中的while(1)中就不能在写程序了。那么在操作系统中如何将某个进程结束呢?操作系统采用的是任务调度,意思是由操作系统按照特定的调度策略在后台对进程进行切换。从C语言角度叫函数,但是在操作系统中叫进程。 操作系统的调度策略:最基本的调度策略是基于时间片轮转。 如果操作系统给某一进程一定的时间,就会出现三种情况,进程运行完毕若干周期,进程刚好运行完,进程没运行完毕。如果每个进程分配10毫秒,系统由三个进程,则一个系统运行周期是30毫秒,那么在第40毫秒则会继续第一个进程未完成的位置继续运行,也就是上次进程切换点的位置继续向后执行10毫秒。但每个进程运行完毕所用的时间是不一样的,有些进程可能都不需要10毫秒的时间,那么这个10毫秒的进程分配时间就没有必要了,因此操作系统应该对即将运行的进程有一个判断,即有没有运行的必要,故纯粹的时间片轮转是不实用的。 1 a9 d7 h2 {+ j2 S6 Q5 ]
如果前一个进程在分配的时间内提前结束进程,那么操作系统需要采用调度算法从众多进程中挑选一个进程送进CPU进行。切换到哪个进程的依据,是把每一个进程当前所处的状态分为5种状态(操作系统休眠态、就绪态‘运行条件具备,但操作系统未切换’、运行态、挂起‘运行条件不具备,需要进程间通信通知’、删除),根据进程将要执行的必要性对其进行切换。操作系统的功能类似一个多路器,如上图所示。任何操作系统在运行时都离不开硬件定时器,这是操作系统与硬件唯一的关系,剩下的就只有软件问题。
操作系统的Tick是由处理器硬件上的一个定时器产生的固定周期,一般为10毫秒。对操作系统的相关函数进行一次调用。所以应当配置定时器中断,在中断处理函数中调用操作系统里的函数,其目的是通知操作系统客观事物发生了改变,操作系统会在每一个Tick周期对每个进程的状态进行统计和修正,作为下次进程调度的依据。 * W/ c) }6 T4 E" F- J3 u 如果多个进程都已就绪,为了使操作系统有序执行,采用基于优先级的进程调度,故操作系统调度之前各进程应具备以下条件:操作系统需要管理的进程个数以及进程的优先级。
UCOSII进程任务切换原理: CPU的寄存器有统用寄存器,程序状态寄存器,程序计数器。哪个进程切换运算,就自动把哪个进程的数据调入到寄存器中。故切换前应对当前数据进行备份(也称现场保存),以便下一次运行时可继续运行。 : t2 ]2 V* e# b) Z UCOSII为了实现进程切换,在每一个进程创建的时候,必须要为每一个进程在单片机的RAM空间留下一段RAM空间,下次再运行到该进程时,再从RAM空间的数据拷贝到寄存器中进行运算。我们将用于保存进程切换时的RAM空间称为进程堆栈,进程堆栈是用于进程切换时备份或保存CPU寄存器的值,理论上多少寄存器就需要多少RAM空间,同时操作系统需要定时器来对操作进程进行刷新。 0 c; {6 w# t! x9 x& w
时钟TICK及源代码结构: 其代码有两个部分,一是与CPU无关的部分(C语言实现),另一个时与CPU有关的部分(由汇编实现主要应用于寄存器操作,另一部分用C语言实现)。
1 准备裸机程序 2 移植要用得到的外设驱动 3 拷贝UCOS源码到工程下 4 向工程添加UCOS源码 程序植入系统应当将外设在裸机条件下调试好,再引入操作系统,否则容易出错。 (注:将程序中的delay()函数删除) + |" x. O( j! X7 g0 H# m% g
将UCOS文件添加到工程中去,建三个分组,一个存放始终不修改的代码(UCOS源码);一个存放跟CPU相关的汇编代码跟C代码;一个存放UCOS的库。 0 y# b) N+ n1 ~
5 头文件不需要添加,编译器会自己寻找,但要添加头文件的搜索路径。
6 编译0错误,0警告 7 添加UCOS的tick / q2 `8 G4 S' d- Z5 r- f+ f. e+ q8 tick函数的异常向量入口地址
同时注意,tick函数是用c语言写的,在汇编语言中调用C函数应当在汇编文件中进行申明。申明如下:
9 添加头文件*include “includes.h”,在主函数上应当初始化OSInit(); 10 建立分组存放用户编写的APP
11 准备用户任务代码 在app.c中创建两个进程
( _. A$ Y3 c3 r3 r' X1 \" E5 b5 ? 在main.c中管理进程函数
|