ucosii-2- 5330杨璟旭

老子叫甜甜 提交于 2019-12-29 10:04:21

题目要求

阅读附件中的代码,回答:

    1. ucos是如何分层的?
    2. HAL都有哪些代码?
    3. 分析任务是如何切换的?

1. ucos是如何分层的?

一共四层:硬件相关层,驱动接口层,应用接口层,应用层。好的分层会让软件开发相对独立化,分工同步进行。

              

 

 

所有的硬件被抽象化,应用层的程序,在硬件完全更换的情况下,只要硬件相关层被更新,完全可以等同原先的所实现的功能。这样就极大成度上方便了移植。

硬件相关层:在这层中,要尽量所有硬件相关都囊括在其中。不管是GPIO还是定时器,或串行接口。只要提供标准统一的接口,就可以让上层会因此而变的很潇洒。这其中有三个最为重要的接口Open,Close,Ctrl。 Open主要来完成对应硬件初始化,形参中包括了些,初始化的相关参数。Close失能硬件。Ctrl来实现一些控制的修改如:优先级,中断回调函数等等,硬件的不同,内容也大为不同。

驱动接口层:其实在上一层也算是驱动层,只不过因为硬件相关,而把他分离。这层中会用到一个或多个硬件层的接口,进行组合来实现特定功能的程序。这部分程序可举例进行说明。以Flash为列,它这里主要调用硬件层的SPI函数接口,但是主要的写,读指令都是在这里函数中完成的。在这层中需要提供5个标准统一的接口函数:

XXXOpen

XXXClose

XXXWrite

XXXRead

XXXIoCtl

没有被用到的函数,可以为空。本来还需要Install函数来进行动态加载和删除,因为stm32内存一般都很有限,所以舍弃动态分配。而把这5个函数用常量的形式直接编译到ROM中。在驱动的抽象接口层中可以做选择,哪些驱动要加载到内核,哪些不需要。不要的驱动不参与编译。这样有限的资源 可以得到合理的应用。这一层大部分工作可以说属于一次性投入。

应用接口层:主要连接驱动和应用。又是连接应用层模块与模块之间的一层, 这一块有很强的特殊性,第一包括了驱动抽象接口层,第二包括了模块与模块的接口层。第三又与应用层密不可分。

先说下驱动抽象接口,在驱动层时也有提过,这个接口 其实就是通过ID去访问那ID对应的那五个函数。抽象接口也是一次性投入的函数,在设计时对其可靠性要很重视。

模块与模块的接口层,包括模块的接口头文件,这些头文件要求是非常独立的,不能加载模块内的内部头文件,应该包括接口函数的函数声明,在接口中尽量少用到全局变量。如果非要用到可以使用函数的方式进行传递,或ucos消息队列方式。最好用ucos进行传递,因为有很好的互斥保护功能。

应用层:这里所有模块都算是应用层,在前面以经提到过,在模块内所有变量,或函数(接口除处)应该都本地化。在模块内可以有本模块化共用的主头文件,来方便本模块的维护。对硬件的访问其实直接调用应用接口就可完成。

 

2.HAL都有哪些代码?

 

具体流程:

也就是说:

1。app(应用层)代码

2. serveice(framework层,此层是给应用层提供服务的)

3. JNI(java不能访问C/CPP,所以就得使用这个楼,jni中的C/CPP文件编译后就生成一个.so文件(libled_runtime.so),jni还会引用hal中头文件来获取hal中一些函数or变量)

4. HAL(jni中会调用hal中的interface,hal中的代码编译后也会生成一个.so文件(led.default.so))

5. device & driver(硬件设备)

 

HAL 代码结构:

HAL stub的框架比较简单,三个结构体、两个常量、一个函数,简称321架构。

@hardware/libhardware/include/hardware/hardware.h

@hardware/libhardware/hardware.c

1. 三个结构体

 

  0) 三个结构体指的是:hw_module_t, hw_module_methods_t,hw_device_t

  1) led_module_t继承hw_module_t,也就是说led_module_t结构体中有hw_module_t成员变量。每个某块都需要继承hw_module_t。

  2) 每一个模块都有各自独特的函数,led_device_t中的setON之类的。led_device_open中定义led_device_t

重新整理一下的话:

2. 两个变量

#define HAL_MODULE_INFO_SYM             HMI 

#define HAL_MODULE_INFO_SYM_AS_STR      "HMI" 

这个就是led_module_t 的名字罢了,就是为了统一,就是HAL Stub的固定名字。

注册时有用会加载HMI地址。

3. 一个函数

int hw_get_module(const char *id, const struct hw_module_t **module); 

通过module id(led.default.so中的led)获取hw_module_t。

4. HALStub是怎么注册的

在前面中HAL编译后会生成led.default.so,

其中led是硬件ID。可参考hardware.c文件,此文件中进行注册。

  1)hardware.c中会查找有没有*.prop.so或*.default.so文件,

  2)有的话就进行注册,打开此文件,在so代码里有定义的函数名或变量名为HMI,

    dlsym返回其地址hmi,将该地址转化成hw_module_t类型,即,硬件对象。

3. 分析任务是如何切换的

从整个程序运行的时间上来看,分析多种任务调度发生的时机。以下所有图片均可点击放大观察。

所有图中红色箭头表示中断级的任务切换,蓝色箭头表示任务级的中断切换。

image

1.仅有一个任务,这种情况最简单。假设时钟节拍是1000次每秒,由定时中断产生,当节拍的时钟服务程序结束时会调用OSInitExit,退出中断,其中将进行上下文切换,运行当前就绪状态优先级最高的任务,这里当然就是任务A、任务A中的代码比较简单,运行到最后时假设调用OSTimeDly(1)延时一个周期,提示系统放弃CPU控制权,这时将进行任务切换到空闲任务Idle Task。空闲任务优先级最低,是一个死循环,仅仅让一个OSIdleCtr循环一次加一。可以看出,在一个时钟节拍的间隔,这个计数器可能加不止一次。另外uCOS中还有一个统计任务,需配置打开,其中就是利用这个空闲计数器求出CPU有多少时间在空闲任务中,即有多少CPU占空比。通过图示分析可以看出,很明显虽然任务A延时一个时钟周期,1ms,但是实际上将会少于1ms的延时。这就是为什么实际的延时中或多或少都会有延时抖动的现象,下面的很多例子的延时抖动都可能比这种情况更加复杂。

 

image

2.简单的中断,这里假设有一个外部中断,在图示处打断了任务A的运行。外部中断响应后进入服务函数,中断退出时调用OSInitExit进行任务切换,回到刚才被中断的任务。所以刚才的任务A就被延后了一段时间允许,之后任务A再切换到空闲任务。很明显,这个时钟周期内空闲计数器OSIdleCtr会少加一些,最后可以统计出CPU占用率会上升一些。

 

image

3.中断函数中利用OSSemPost发送一个信号量,接受信号量的是任务B,其优先级大于任务A。于是在每次中断服务函数结束时会首先调度任务B执行,按照图中任务B的代码只有一个OSSemPend可能会有等待挂起发送,之后任务B会因为等待接受信号量而调度会任务A继续执行。图中的画出了三个外部中断分别出现在不同时期,其中第三个外部中断将导致任务B执行到一半遇到时钟节拍的产生。这时候任务B会挂起,将执行节拍中断服务程序,经过调度后会发现任务B任然处于就绪状态,所以将继续运行任务B直到结束。最后这里可以看出空闲任务在每个时钟周期被挤得更少了,所以CPU的利用率更多。

 

image

4.跟第三中情况类似,只不过这里任务B中加了一句OSTimeDly(3)延时3个时钟周期,这时可以知道,在任务B延时期间,只剩TaskA和空闲任务,他们将不会因为任务B的延时而被挂起。延时结束时,时钟节拍将首先调度延时结束处于就绪状态的任务B,之后再运行原本的任务A。这种情况也简单的表示了uCOS是怎样充分利用CPU资源的。

 

image

 

5.中断中调用OSTaskResume恢复任务B。这里只是想说明,Resume/Suspend跟Post/Pend的区别,前者是一旦恢复则一直运行,而后者是Post一次,Pend方运行一次。

 

image

6.当用信号量通讯时,利用任务A来发送,控制高优先级的任务B接受信号量。可以看出,任务A发送一次信号量将会时任务B调度执行,此时任务A由于优先级低,被挂起,当任务B运行一次循环后又Pend等待信号量,这时任务B被挂起,由下一个时间周期的任务A发送,周而复始。

 

image

 

7.对于共享资源,可以采用图示方式编写。这种方式表明,当两个任务想要使用共享资源时,每个任务用Pend和Post的组合包围住想要操作的资源(比如一个函数,一个公共变量)。其中一个任务用完资源后执行Post,将会使另一个Pend等待资源的任务得以往下执行,不论二者的优先级如何。图中的任务也包含了每次时钟节拍对任务的延时作用。不过像这种信号量通讯的场合会涉及到优先级反转问题,我们后面会分析。

 

image

8.这里演示了一个比较恶劣的中断嵌套实例,可以看出所有任务/中断级的任务调度只有等最后一层中断嵌套执行结束后才能执行,在中断嵌套中若使用Post或Resume想要执行任务调度也只能等到嵌套结束才能执行。uCOS-II支持中断嵌套,不用担心中断的响应问题,但是一旦中断嵌套之后就会出现很多延时的问题,将会导致整个系统的实时性下降,比如图示的TaskA就因为中断嵌套,并且因为中断嵌套结束后TaskB接受到信号量而执行,使得其延时效果大大超出我们想象,而且中间还缺了一步时钟周期,甚至导致这个周期满负荷(空闲任务不会执行到)。虽然画的有点夸张,但是这不得不引起我们的注意。所以通常推荐中断服务函数写的越短越好,一个好的方法是,清除了中断标志后仅仅发送一个信号量通知别的任务执行,将所有需要花时间运行的工作交给任务而不是留给中断服务函数处理。

 

以上的分析包含了少量几个任务和间可能出现的常见情况,对于大型程序来说,任务的调度和中断的响应可能比这个更加复杂,需要更进一步分析。

【@.2 优先级反转】

image

图示的情况将出现优先级反转。任务A和C需要处理一个共享资源,低优先级的任务C首先得到信号量,处理完成后Post发送信号量通知等待中的任务A得以运行,任务A处理完共享信号量后Post,将使得等待中的任务C运行,而过了一段时间,任务B由于某种原因被恢复为就绪状态,则任务B会抢夺任务C的CPU使用权,待任务B结束后任务C才得以进行,之后任务C发送信号量,通知任务A得以继续运行。

这将会使得任务A等待信号量的时间延长,看上去是优先级低于A的任务B先于任务A执行,这往往也是我们所不希望的。这个问题的原因就是因为任务C的优先级太低,所以解决这个问题时简单的方法就是动态修改进行共享资源操作的任务的优先级即可。于是引入了互斥信号量。

image

互斥信号量跟普通信号量基本一致,区别在于通过Pend得到信号量的任务将会被动态修改其优先级。这里我们一般会设置一个比较高的优先级,如图所示,任务A和任务C交替得到信号量,并且得到信号量时被赋予了一个更高的优先级。这时若任务B进入就绪状态就无法得以运行了。注意到这仅仅是uCOS-II的解决方法,因为uCOS-II不支持多任务同一优先级。

 

【@.3 uCOS中的中断延时】

image

对于中断响应的问题,从硬件上每个CPU并不相同,可以参考我的这篇文章对ARM7系列的中断响应进行一个了解。而uCOS对于中断响应会有一套自己的办法进行统一处理。

之前分析的内容仅仅是从任务级别来进行分析,但实际上更深入一步分析,中断的响应,任务的响应都会有一定时间的延时,以图示为例。

当一个任务运行中得到中断请求,会首先有一个硬件的响应时间。之后硬件会关闭中断标志,一般是在CPU的状态寄存器中的IRQ位置1禁止,随后跳入中断入口地址。之后uCOS会进行自己的特殊处理,对中断进行接管。首先会保存CPU当前寄存器,并通知内核进入中断函数(一般是调用OSInitEnter函数),之后编写代码重新打开中断,允许中断嵌套。之后将进入中断服务函数。这期间的时间就是中断响应时间。

当服务函数结束后,会调用OSInitExit进行任务调度。若此时有新的任务置于就绪状态并且优先级比原任务高,则将会进行上下文切换,恢复CPU寄存器,中断返回后进入新的任务执行。若没有心的高优先级任务,则会原路返回,回到原来的任务继续执行。

可以很明显的看到,经过uCOS接受的中断响应将变慢,但是由于这样能实现中断嵌套和丰富的任务调度,所以这样的中断延时是可以接受的。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!