第1章 开发工具安装和配置
详见《DIDE使用说明书》,该说明书可以在官网有下载链接,在安装DIDE后,首次打开DIDE时,如果你的电脑安装有PDF文档阅读器,则会自动打开。
第2章 Hello led
2.1 获取源码
DJYOS是开源系统,安装DIDE后,首次运行时,DIDE会自动从git服务器上下载最新源码,当服务器上源码更新,也会自动更新。
我们都是跟随“HelloWorld”进入编程的世界,当第一次见到屏幕上显示“Hello World”时,感觉那就是全世界最美的字符。
但在深度嵌入式环境中,想看到“HelloWorld”可不是件简单的事,初始化输出输出终端环境,可不是件简单的事,至少,你需要有LCD,或者驱动串口吧。
在嵌入式板件中,一般都会有LED灯,控制gpio点个灯,比输出“Hello World”要简单得多,这里就用跑马灯来演示djyos下第一个应用程序。
先讲讲下面main.c文件中程序开头include的几个头文件。
os.h,几乎所有的DJYOS应用程序,都需要包含这个头文件,因为这个文件里面包含了所有OS内核数据结构和函数声明。
Cpu_peri.h,这个文件在“djysrc\bsp\cpudrv\cpuname\include”目录下。DJYOS的BSP中,所有CPU片内外设,都在bsp\cpudrv目录下。该文件包含了CPU外设的数据结构定义、变量声明等。凡是需要使用CPU片内外设的程序,都应该包含这个头文件。
stm32f10x.h和LED.h就不说了,大家都懂。
mian.c的文件内容 #include "os.h" #include "cpu_peri.h" #include "stm32f10x.h" #include "LED.h"
#define LED1_GPIOC9 GPIO_Pin_9 #define LED2_GPIOC8 GPIO_Pin_8 #define LED3_GPIOC4 GPIO_Pin_4 #define LED4_GPIOC6 GPIO_Pin_6
void djy_main(void) { while(1) { GPIO_SettoLow(CN_GPIO_C, (1<<4)|(1<<6)|(1<<8)); GPIO_SettoHigh(CN_GPIO_C, (1<<9)); DJY_EventDelay(500*mS);
GPIO_SettoLow(CN_GPIO_C, (1<<4)|(1<<6)|(1<<9)); GPIO_SettoHigh(CN_GPIO_C, (1<<8)); DJY_EventDelay(500*mS);
GPIO_SettoLow(CN_GPIO_C, (1<<6)|(1<<9)|(1<<8)); GPIO_SettoHigh(CN_GPIO_C, (1<<4)); DJY_EventDelay(500*mS);
GPIO_SettoLow(CN_GPIO_C, (1<<9)|(1<<4)|(1<<8)); GPIO_SettoHigh(CN_GPIO_C, (1<<6)); DJY_EventDelay(500*mS); } } |
djy_main函数,是应用程序的入口函数,所有djyos发布版本都会带有这个函数。
按照《DIDE使用说明书》中介绍的方法,编译和烧录后,你的第一个djyos应用程序就完成了,赶紧上电试试吧。
第3章 裁减与配置
djyos提供强大的功能,但每个APP,往往只用到其中一小部分功能,为了减少占用RAM和FLASH,我们需要裁减掉一些组件,或者通过配置,弱化一些组件的功能。
DIDE提供了图形化的方法帮助我们裁减和配置djyos,参见《DIDE使用说明书》。
第4章 shell终端
DJYOS的shell是一种命令行式的shell。它可以接受用户命令,然后调用系统相应的程序,实现用户与操作系统之间的交互。
由于djyos是高度可裁减的,只有勾选的组件,其配套的shell命令才会加入,不同的系统配置,help命令列出的命令列表,会有所不同。
4.1 智能多IO端口
智能多IO端口是djyos的贴心功能。
djyos支持智能多IO端口,例如你可以同时使用串口和Telnet,如果从串口收到shell命令,则命令执行结果从串口输出;如果从Telnet收到shell命令,则命令执行结果从Telnet输出。
智能多IO端口有什么用?
工业设备用户的应用需求五花八门,有时候会提出多shell终端的需求。举一个案例,某工业现场运行的设备,用网络连接到远程服务器,需要在服务器上用Telnet登录执行shell命令;同时,本地设计有调试串口,厂商现场调试时,就需要用调试串口作为shell终端,且不允许碰用户的网络连接。
工业设备的安装现场环境,也会有类似的需求,例如某款电力设备,有些安装条件,现场维护人员只能接触到前面板,有些则只能接触到后面板。设备的前面板有网口,调试串口则安装在后面板。限于结构设计,调试串口无法在前面板引出。如果没有智能多终端,则软件必须按照现场安装条件不同,分成“前面板shell”版本和“后面板shell”版本,增加了版本管理成本。
shell控制台作为用户与操作系统之间的一种交互方式,通过标准IO的stdin输入控制命令,向stdout输出结果,向stderr输出出错信息,因此,使用shell之前,必须安装和配置好输入输出终端设备,详见第12.5.1节。
4.2 配置shell功能
通过DIDE中配置,可以裁减掉shell,也可以配置shell功能的强弱,以调节flash和ram占用,控制成本。
shell的功能,不外乎输出信息,执行命令,调用函数,查看变量,修改变量。配置shell功能的强弱,就是配置shell支持的命令集,调用的函数集,读/写的变量集的大小。
在DIDE的配置界面中,可以配置shell_level参数,SHELL_LEVEL_BASE、SHELL_LEVEL_EXPAND、SHELL_LEVEL_ALL三个级别。
图 4‑1DIDE中配置shell功能
DIDE如何使用,参见《DIDE使用说明书》
每个级别功能呈子集关系。
图 4‑2shell功能包含关系
4.2.1 BASE Level
shell level = BASE,则只支持一组基本的shell命令以及一组全局变量。
BASE组命令的执行函数原型必须是:
bool_t func_name(char *param);
宏ADD_TO_ROUTINE_SHELL用于把“func_name”函数添加到BASE功能组中,用法如下:
ADD_TO_ROUTINE_SHELL(cmdname, func_name,”命令的帮助信息”);
shell中执行BASE组命令的方法:
>cmdname parameter list [回车]
func_name函数将被调用,命令的参数由执行函数自己解释。
宏ADD_TO_ROUTINE_DATA用于被修饰的全局变量添加到BASE功能组,用法如下:
u32 var_name ADD_TO_ROUTINE_DATA;
BASE组全局变量查看和修改方法:
>var_name [回车]
即可查看变量。
>var_name = new value [回车]
即可修改变量。
4.2.2 EXPAND Level
若shell level =EXPAND,则在BASE组的基础上,增加一组基本的shell命令以及一组全局变量。
命令和变量加入到EXPAND组的方法与BASE组基本一样,就是宏的名称不一样,分别是:
ADD_TO_EXPAND_SHELL 和
ADD_TO_EXPAND_DATA
新增全局变量的使用方法与BASE组是一致的。
新增shell命令使用方法与BASE组不一样,BASE组执行函数的原型是固定的,EXPAND组执行函数原型是任意的(参数不能超过10个)。
EXPAND组命令的执行方法:
> cmdname 参数1类型 参数1 参数2类型 参数2 参数3类型 参数3……[回车]
执行函数的每一个参数,都需要在命令行中输入其类型,例如:
void ExpandSh(u32 a, float b);
ADD_TO_EXPAND_SHELL(myname, ExpandSh,"扩展shell命令范例");
在shell中调用时,格式是:
>myname u32 100 f 10.0 [回车]
数据类型表定义如下:
类型 |
说明 |
u8 |
无符号8位 |
u16 |
无符号16位 |
u32 |
无符号32位 |
u64 |
无符号64位 |
s8 |
有符号8位 |
s16 |
有符号16位 |
s32 |
有符号32位 |
s64 |
有符号64位 |
f |
单精度浮点 |
d |
双精度浮点 |
‘a’ |
char |
“ ” |
string |
可见,EXPAND组命令的参数类型,是有限制的,无法使用指针传入的参数,也无法用指针输出参数,因此,参数中包含指针的函数,除非可以用NULL,否则无法在shell命令行调用。
4.2.3 ALL Level
若shell level =ALL,则全部函数都可以调用,全部全局变量均可以查看和修改。
BASE组和EXPAND组的函数,命令名和函数名可以不一样,ALL的函数,shell中输入函数名来调用,以及没有帮助信息。
除此之外,ALL组的函数调用和查看变量和修改变量的方法,与EXPAND组无异,不再赘述。
同样的原因,参数中包含指针的函数,除非可以用NULL,否则无法调用。
4.3 添加shell命令
djyos提供了四个宏,用于把用户定义的函数添加到shell表中去,以及把自己定义的变量添加到shell中。语法很简单,直接上代码,解释都是多余的了。
#include <shell.h> void BaseSh(char *param); ADD_TO_ ROUTINE _SHELL(myname, BaseSh,"常规shell命令范例"); void ExpandSh(u32 a, float b); ADD_TO_EXPAND_SHELL(myname, ExpandSh,"扩展shell命令范例"); u32 var ADD_TO_ROUTINE_DATA; //添加到BASE组 u32 var ADD_TO_EXPAND_DATA; //添加到EXPAND组 |
4.4 使用shell注意事项
4.4.1 输入命令限制
shell的输入命令和命令所带的参数,总长度不能超过255字节,并且拓展shell的命令参数不能超过10个。
shell虽然支持总长度超过255字符的路径名,但由于命令行长度限制,在命令行下,无法操作超过255字节的路径名。
4.4.2 文件和目录不能含有空格
shell支持在文件名和目录名中包含空格,但由于shell以空格作为断字符,故在shell中输入的文件名或者目录名不能包含空格符。
4.4.3 当前工作路径
si和dlsp版本是单进程的,当前工作目录是一个全局概念,在shell中使用cd命令改变当前目录时,需要特别注意,在shell中改变当前路径,应用程序是不知道的,如果应用程序继续使用原来的“当前路径”操作文件系统,将出现不可预料的错误。
4.4.4 函数调用
shell允许你调用任意函数,包括操作系统API、你自己写的任何函数。这个功能有一定的危险性,建议不要在在线运行的设备中使用,只在调试时打开这个功能。它的危险性来自于以下两个方面:
1. 被调用的函数是在shell事件的上下文中执行,shell并不知道被调用的函数所需的栈空间,也就存在栈溢出的可能,栈溢出的危险性,我不说,大家都清楚。你请客,我买单,我不知道你点什么菜,怎知道要带多少钱?
2. 你既然能用shell调用任何函数,也就可以干任何事,干好事坏事,全凭你的良心。
3. 如果被调用函数是不能重入的,而正好别的函数也在调用,结果同样不可预料。
第5章 事件、事件调度和线程调度
5.1 事件
计算机处理的是现实世界中的具体任务,有因才有果,现实生活中的任务不会无缘无故地产生,人们做某一件事肯定是因为发生了某种事件使其需要去做这件事情,这就是事件。计算机中的事件与现实生活中的事件是一致的,CPU不会无缘无故地执行某一段代码,就算是一段包含在一个if语句里的代码被执行,肯定是因为发生了使该条件成立的事件。人走到沙发前是一个事件,智能沙发上的计算机发现这个事件后然后处理这个事件,处理结果是执行调整坐垫到合适位置的操作;人转身面对电视机是一个事件,智能电视机里的计算机发现这个事件然后处理这个事件,处理结果是执行打开电视机的操作;人躺在床上并闭上眼睛,智能家居的计算机发现这个事件然后处理这个事件,处理的结果是执行关灯的操作。以上所述的事件,就是DJYOS操作系统中“事件”的原型。所有这些原型中,都有一个“发现”(或称“检测”)事件和执行一定操作以处理事件的过程,现实系统中,这两个过程可能非常复杂,甚至处于两个不同的学科,其软件实现模块可能会由两个不同专业方向的程序员编写。DJYOS软件模型是:由一个软件模块专门用于监测人的行为,另外一些模块执行开关灯、开关电视机、调整沙发坐垫的操作。检测模块发现人靠近沙发的事件后,不是去调整沙发坐垫,而是把“事件”报告给操作系统了事。操作系统收到该事件后,先把该事件记录在调度队列中,再依据调度算法,当决定要处理该事件时,就分配或创建用于处理该类型事件的线程,并启动该线程,再由这个线程去执行调整沙发坐垫的操作。这样,就使“检测”和“执行”相互独立开来。进程、线程之类的东西只是操作系统内部的秘密,线程作为一个资源,是创建新资源还是使用现有资源来处理事件,完全由操作系统自动完成,应用程序的程序员不知道也不需要关心这些。
DJYOS下程序运行的过程,就是新事件不断发生,然后被处理的过程。在此过程中,操作系统组织、创建、分配线程、进程以及其他资源去满足处理事件所需。每弹出一条事件,DJYOS操作系统就为它分配一个事件控制块,事件处理完毕后收回事件控制块。未处理完毕的事件就会堆积在队列中,操作系统对队列的容量有一定的限制,当队列中事件数量达到上限时,操作系统将拒绝接受新事件。core_config.c文件中的gc_u32CfgEventLimit常量用于确定队列容量,用户可以修改,但不能在程序运行过程中动态改变,最多允许16384个事件。
5.2 事件类型
程序运行期间会发生各种各样的事件,不同种类的事件由各自的线程处理,需要用事件类型去加以区分。DJYOS操作系统为每一类事件分配一个唯一的事件类型ID号,并为每个事件类型分配一个事件类型控制块(struct EventType)。事件类型控制块是静态分配的,其数量须按照内存量和应用程序实际需求合理设定,由工程目录下的core_config.c文件中的gc_u32CfgEvttLimit常量确定,用户可以修改,但不能在程序运行过程中动态改变,最多允许16384个事件类型。
事件类型必须调用DJY_EvttRegist函数登记才能使用,登记时要为该事件类型指定一个事件处理函数,该函数将成为线程的入口函数,还要设定该类型的默认优先级,并且告诉操作系统该函数运行需要多少栈空间。在某些情况下,操作系统会为部分类型事件至少保留一个线程(参见5.4节),当有该类型的事件发生(调用DJY_EventPop)时,操作系统可能会自动创建线程处理该事件,也可能会指挥现有线程(或创建线程)去处理该事件。
5.2.1 独立型和关联型事件
如果某类型事件重叠发生(即事件未处理完成,又发生相同类型事件),产生的多条事件可以用多个线程并行处理的,称为独立型事件。典型的独立型事件是web服务,当web服务器收到多个客户端的服务请求后,这些请求一般是独立的,服务器会创建多个线程独立处理多个请求。独立型事件用EN_INDEPENDENCE表示,该类型的每条事件都需要单独处理,每次弹出独立型事件,都会在事件队列中添加一条事件,事件反复发生而又来不及处理时,事件队列中将积压多条同一类型的事件。独立型事件也有特例,如果在调用DJY_EventPop函数时使用的参数是事件ID,也不会添加新事件,参见第5.7.6节。
反之,如果重叠事件互相关联,必须在一个线程中顺序处理,称之为关联事件。该类型事件一般表示的是物理世界的一种状态,若此类型的事件重复发生,它也只代表系统处于某种状态,不是每次发生的事件都需要单独处理。关联型事件用EN_CORRELATIVE标记,这种事件无论重复发生多少次,事件队列中都只会保留一条事件。这是一种很重要的事件,因为现实世界中有太多的关联型事件,试举数例如下:
1. 快件投递中,当客户有快件需要投递,就会给快递公司电话,同一个地址,无论重复多少次电话,只需派收件员上门一次把积累的快件全部取走就可以了。
2. 在LCD面板显示软件中,在内存中设计了一个显存镜像,应用程序修改显示内容时,修改的是镜像显存,然后发出“显示刷新”类型事件,处理该事件的过程就是把镜像显存中的图像搬到物理显示器上。这是一个典型的关联型事件,无论应用程序修改了多少次镜像显存,都表示“显存被修改”这一物理状态,处理一次“显示刷新”事件将把历史上积累的事件完全清理。
3. 串口通信软件中,缓冲区接收到数据,会发出“缓冲区有数据需处理”类型的事件,无论该类型事件重复发出多少次,都表示这样一个状态,事件处理时只要把缓冲区的所有数据取走,就可以了。
独立型事件的例子也很多,例如:
1. 在百货商店,每进来一个顾客算发生了一个“顾客来了”类型的事件,由于每个顾客都是独特的,所以必须单独服务。
2. 同样是LCD面板显示软件中,当用户需要绘制时,就会发出“屏幕绘制”事件,因为每次绘制的内容都可能不同,所以每条事件都必须单独处理。
3. 通信软件中,应用程序需要发送数据,就把数据准备好,弹出“发送数据”类型的事件,并把数据缓冲区作为事件参数,由于每次事件的数据缓冲区都是独立的,不能把多条事件统一处理,而是每条事件都要单独处理。
5.3 线程
DJYOS以事件为调度单元,理论上,操作系统可以用任何方法处理事件,只要能够调用事件处理入口函数就可以了,当前版本的DJYOS操作系统使用的方案是,创建一个线程执行事件处理入口函数来实现事件处理。注意,线程是操作系统自行创建的、用于执行用户提供的事件处理入口函数的手段,对程序设计者是不可见的,这就隐含了一个事实:操作系统还可以选择其他方案代替线程方案,遗憾的是,笔者至今也没有想到可以替代线程的可行方案。要处理事件,操作系统就要为该事件创建线程,用户在登记事件类型时必须告诉操作系统执行该入口函数需要多少栈空间。在DJYOS系统中,线程是处理事件的执行者,也作为事件的资源而存在——完成该事件需要许多资源,线程是诸多资源之一。
5.4 事件、事件类型与线程的关系
套用面向对象的方法,事件类型相当于C++的类,登记事件类型相当于声明一个类数据类型;事件相当于对象,弹出事件相当于定义对象,同时做一些构造函数的工作;事件处理完成相当于撤销一个对象,同时做一些类似析构函数的工作。
DJYOS中应用程序的运行过程,就是不断地弹出新事件和处理事件的过程。每个事件都必须属于已经登记的事件类型,相同类型的事件使用相同的线程进行处理。
DJYOS操作系统中,线程作为事件的资源而存在,而该事件就是线程的拥有者,因此,任何线程,都不能无缘无故地出生、存在和死亡,它必定与某一类型的某一条事件联系在一起。线程随事件的需要而生,随事件完成而消亡。线程无需登记,也无需有用户建立和启动,它的创建、启动和删除都是由操作系统自动完成的。这与传统操作系统不一样,传统操作系统可以由用户任意创建线程,创建一个毫无意义的线程是允许的。DJYOS系统的调度依据是一个就绪事件队列和若干个同步事件队列,而不是线程队列(有的操作系统也称其为任务队列),DJYOS中根本就没有线程队列。DJYOS的调度针对事件而不是线程,创建一个线程是因为它的拥有者需要处理,线程被切入是因为该线程的拥有者需要被切入,线程被切离是因为它的拥有者被挂起。把线程作为事件的资源的积极意义在于,当某类型的事件连续发生,操作系统将调集更多的资源,为其创建多个线程来处理该事件,如果在多处理机(多核)系统中,把这些线程分配给不同的处理器,处理器本身也就成为一种资源,将极大地方便多处理器系统的管理。而传统的编程方法中,程序员创建若干线程待机,每个线程对应一种或数种事件,待相应的事件发生后,唤醒线程予以处理,这种方式在管理多处理器方面要复杂得多。因此,DJYOS在多处理器系统中,有先天的优越性。
线程的属性必须与事件类型对应,相同类型的事件使用相同的线程处理,不同类型的事件使用不同的线程处理。用户登记一个事件类型时,必须传入操作系统创建用于处理该类型事件的线程所需要的两个关键参数:事件处理入口函数和该函数需要的栈空间。当某类型的事件发生后,操作系统就会在适当的时候创建线程(或分配存在的线程)执行该类型对应的事件处理函数。事件处理完成之后,操作系统会自动回收该线程所占用的资源,必要时还会删除线程。
每一条事件对应一个线程,如果有多条同一类型的事件需要处理,操作系统会创建多个相同的线程同时处理多个相同的事件。这可以更合理地使用计算机资源。虽然在单处理器的情况下,建立多个线程并不会比单个线程长期霸占处理器更充分利用处理器,但可以产生多个相同类型事件并行处理的效果,例如同时绘制多个窗口,特别是,在多处理机(或多核)系统中,可以把频繁发生的同一类型事件分配到不同的处理器上。而传统操作系统下,线程是由程序员创建的,如果程序员只为某项工作创建了一个线程,则该工作再繁忙也只有一个线程为它工作,该线程处理的多项任务只能串行执行。但是这样反复创建线程可能导致资源枯竭,比如处理某事件时需要使用串口,而串口又被其他线程占用,在串口被占用期间发生该事件,操作系统就会再次为其创建线程,该线程开始执行后会因串口资源繁忙而进入阻塞状态,如果事件反复发生,操作系统就会反复为其创建线程,直到消耗完所有内存,造成内存枯竭。为了防止发生资源枯竭事故,在事件类型控制块中提供了vpus_limit成员,表示该类型事件可以同时建立线程的个数。
5.5 线程调度和事件调度
DJYOS系统中,调度是以事件为依据,线程是事件的资源,线程本身是没有优先级的,但上述理论依然适用,只要把其中的“线程”两字改为“事件”就可以了。
在实时系统中,事件的优先级是能否实现实时指标的关键,现实中,大多数事件会继承事件类型的默认优先级,因此确定每一类事件的默认优先级是系统设计的重中之重。
基于事件进行调度,这是DJYOS与传统操作系统最大的区别。图 5‑1是从传统操作系统抽象出来的,无论是简单的OS还是复杂OS,其调度都是基于线程的。图 5‑1中,无论是初始化创建线程,还是线程执行过程中创建线程,都是在创建线程的时候分配线程所需要的资源,栈是其中的必备件。图 5‑2中,弹出事件时,除非新事件的优先级足够高,需要立即处理,否则弹出时不会分配或创建线程,直到该事件应该被处理的时候才创建或分配线程。
图 5‑1传统操作系统调度示意图
图 5‑2事件调度示意图
看起来,除了不能从中断处理函数中创建线程外,两图有很大的相似性,其实不然,传统OS下编程时,不应该频繁地在线程执行过程中创建新线程的,为什么呢?为了创建线程而导致阻塞,而被阻塞的却是当前线程。如果新线程的优先级低于当前线程,那当前线程就冤了,因为一个低优先级的线程,而导致高优先级线程阻塞,这在RTOS中是不允许的,从系统方案设计角度,也是不合理的;即使不被阻塞,高优先级线程执行过程中,花大量时间用于创建低优先级的线程,也是不合理的。所以,传统OS下编程时,所有线程总是在初始化阶段创建,极少在运行过程中创建。而DJYOS则没有这个问题,如果新弹出的事件优先级不够高,根本就不会为他创建线程。这使得DJYOS能够更加优化配置计算机的资源,假设有一个产品,有uart串口,在传统线程模式下, uart通信线程是在初始化时创建的,即使uart通信线没有连接,uart线程也必定占用内存资源。如果uart通信非常频繁地发生,也只能由初始化时创建的那个线程一点一点来处理,即使你有多个cpu核,其他cpu核也只能干着急。那不能创建多个线程吗?可以,线程池技术就是这样干的,究竟创建多少线程才合适,是一个非常令人头疼的问题。在传统OS下,线程池技术可是一门专门的学科哦。而DJYOS的事件调度呢?如果uart通信线上没有数据,则根本不会弹出该事件,也就不会占用任何资源。如果uart口频繁通信,就会频繁弹出事件,若计算机有多个cpu核,则自然而然地会把事件分散到不同的cpu核上,程序员根本不知道线程为何物,就能进行优化的多核编程。
大家都知道vc、cbuilder下编程吧,在桌面上放上一个按钮,然后为按钮点击事件编写处理函数,当用户点击该事件时,处理函数就被执行。这就是事件触发式编程,这是怎么实现的呢?原来,有一个线程一直在后台候着,等待windows发出的消息,一旦收到点击消息,线程便被唤醒执行。我们可以看到,无论该按钮是否被点击,甚至一辈子都不点击,该线程依然要占用系统资源。而DJYOS的方法则不同,只要该事件不发生,则不会占用任何系统资源。VC为什么要实现事件触发式编程呢?是因为现实需要,与传统操作系统需要像VC这样的工具支持才能实现事件触发式编程相比,DJYOS只需要文本编辑器就可以实现。VC这样的工具只能用于较大的系统中,而DJYOS却可以用在单片机中!
传统的基于线程(进程)的调度模式下,操作系统只知道哪个线程(进程)正在占有CPU,却不能知道该线程(进程)在干什么,调度器也只能针对线程(进程)分配CPU,不能针对计算机所处理的具体事务分配CPU。因此,传统操作系统下,程序员必须熟练掌握有关线程(进程)的知识,必须自己为程序需要处理的事务创建线程(进程),清楚在哪些状况下有哪些线程(进程)正在运行。而事件调度则不同,用户可能根本不知道线程(进程)的存在,以及谁正在运行,谁正在等待,实际上,程序员根本不需要关心这些。DJYOS系统中,程序员只知道哪个事件正在被处理,哪些事件已经处理完成,哪些事件正在队列中等待处理。定义一个一个的事件类型并登记到系统中,为每类事件编写处理函数,便是编程的全部工作。当相同类型的多条事件,具有不同的优先级时,在传统操作系统下,要么为每一种可能的优先级建立一个线程(进程)来实现,要么在执行中动态改变线程(进程)的优先级,不管哪种方式,程序员都需要花费大量的时间和精力,以确保事件按正确的优先级得到处理。而DJYOS不同,它先天就是以事件优先级作为调度的基础,只要在产生事件时,直接在事件中做优先级标志就可以了。例如一个串口通信程序,中断函数负责底层接收,当接收到完整数据包后,就发给上层应用程序处理,上层应用程序处理接收到的所有数据包,而数据包中有一个命令字域,不同的命令要求的优先级不同。在传统操作系统下,要么创建一个comm_app线程,在中断函数中把数据传送给该线程的同时根据命令字改变comm_app线程的优先级,这种在中断函数中改变线程优先级的做法,在许多操作系统中是不允许的;另一种方法是,为命令字对应的每一种可能的优先级,均创建一个相同的线程,这些线程除优先级不同外,其它部分完全相同,当中断函数接收到完整数据包时,就根据数据包中的命令字,发消息给相应优先级的线程。这种方法虽然可行,但是会消耗很多CPU资源,且难于在编码阶段穷举所有可能的优先级,一旦命令字发生变化,很可能就需要修改软件源码。而DJYOS系统不一样,程序员只需要定义一个事件类型,并为之编写事件处理函数proc_uart(),当中断函数接收到完整数据包,弹出事件时直接以命令字对应的优先级作为参数就可以,DJYOS的调度系统自动会根据事件优先级域进行调度。
另外,嵌入式系统多是应激系统,应激系统的主要任务是对外界的事件做出正确且及时的反应,从这点看,程序员根本就不用知道进程和线程这些东西,为处理外界事件而建立线程(进程)实际上是不得已而为之,传统操作系统下,你必须做这些工作,你的系统才能正确地为你做些事情。基于事件的调度非常适合这种应激系统,被调度的目标就是反映外部刺激的事件,而不是处理这些外部刺激的线程,符合人们的习惯性思维。即使是非应激系统,或者是非实时系统,基于事件的调度仍然有其优势,“有事就做,没事就坐”是人们最为习惯的思维方式,以事件为调度单位的调度策略显然符合这种思维方式,而与人们习惯性思维相同的调度方式,又是避免人为错误,减少软件bug的有效方法。
现代计算机已经进入“ubiquitous/Pervasive Computing”时代,即普适计算。在普适计算时代,触手可及的计算产品里面也包含着触手可及的计算机程序,这些程序由大量的嵌入式程序员编写,是的,十年前的硬件工程师可以不懂软件,软件工程师可以不懂硬件,而今天的嵌入式技术,除了在一些很特殊的方向如射频设计,已经没有纯粹意义上的软件工程师和硬件工程师了。让这些队伍迅速壮大的、软硬兼顾的“普适计算工程师”们去掌握晦涩难懂的进程和线程技术并灵活应用,恐怕要花费不少的人才培养成本,而使用传统的操作系统去开发嵌入式产品,不理解这些复杂的概念根本就寸步难行,而人才的匮乏又将限制嵌入式产业的发展。DJYOS操作系统不要求程序员操纵线程和进程,程序员只需把需要计算机处理的任务划分为一个个事件类型,并为各种不同类型的事件编写独立的事件处理函数,然后把它登记到系统中就可以了。当事件发生时,发现(检测到)该事件的程序员只要告诉操作系统“某类型事件发生了”,操作系统自动地创建或唤醒合适的线程去处理该事件,而无须程序员亲自创建或者唤醒相应的线程。当然,“普适计算工程师”即使是在DJYOS系统下编程,深入理解线程和进程技术,对开发工作也是很有帮助的。
5.6 典型情景编程对比
DJYOS与传统操作系统在调度方式上的差异,必然导致在其在编程模型上的差异。传统方式下,程序员编写函数以处理需要计算机完成的任务,然后建立线程来运行这些函数,创建线程、启动线程、线程同步、线程暂停、线程休眠、线程终止、线程删除等操作,均由程序员亲自完成。在DJYOS下,认为计算机执行某一段程序,必定是发生了使计算机执行这段程序的事件,计算机的所有操作,均由事件引发。程序员所需要做的工作是,以适当的粒度定义事件类型,为各类型事件编写处理函数,并且在需要的时候弹出事件,在必要的时候调用操作系统提供的事件同步功能。由此而导致了两种截然不同的编程模式,下面,我们通过不同场景的对照,来直观了解一下这两种编程方式。
5.6.1 场景1
一个网络通信端口收发模块,监视网络端口状态以及收发数据流。
这种模块一般是系统启动后即开始运行,只要通信口不关闭,就需要持续接收数据,直至关机,在整个运行期需要不间断地监视网络端口,并做出相应的响应。
5.6.1.1 传统操作系统实现方案
Int thread_comm(int para) { 无限循环,执行通信监视业务; } int main(void) { 创建线程,指定thread_comm为线程入口函数; 启动线程; } |
5.6.1.2 DJYOS实现方案
int event_comm(int para) { 无限循环,执行通信监视业务; } int main(void) { 登记evtt_comm事件类型,指定event_comm为事件处理函数; 弹出evtt_comm类型事件; } |
5.6.1.3 分析
分析上面两个程序模型,可以看到,这种场景下,两种编程方式看似没有区别。但实际上是有区别的,因为事件是直观的,与人类思维模式比较接近;线程是抽象的,与计算机的执行过程接近。越接近人类自然思维,就越容易学习和掌握,编程就越不容易出错。这种区别,从这个简单的例子中不容易体现出来,但如果软件规模比较大、逻辑复杂的系统,这种差别就显示出来了。
现代计算机已经进入“ubiquitous/Pervasive Computing”时代,即普适计算。触手可及的计算产品里面也包含着触手可及的计算机程序,这些程序由大量的嵌入式程序员编写。设计这些产品需要大量的软件工程师,这使得嵌入式程序员的队伍迅速增大,新增的程序员,可能来自各行各业,他们原来在各自的行业中,可能都是非常了不起的专家,比如化工专家、医疗专家等。这些不同领域的专家,却未必是计算机领域的专家,让他们去掌握晦涩难懂的线程技术并灵活应用,恐怕要花费不少的人才培养成本,而使用传统的操作系统开发嵌入式产品,不理解这些复杂的概念根本就寸步难行。DJYOS操作系统不要求程序员操纵线程和进程,程序员只需把需要计算机处理的任务划分为一个个事件类型,并为各种不同类型的事件编写独立的事件处理函数,并且把它登记到系统中就可以了。当事件发生时,发现(检测到)该事件的模块只要告诉操作系统“某类型事件发生了”,不需要管什么线程。从这个角度,DJYOS降低了程序员培训要求,客观地为企业节约了人力资源费用。
5.6.2 场景2
如场景1的通信口监视模块,通信口接收来自外部的请求,把接收到的请求交由服务器处理。为了确保通信口不丢数据包,通信口监视模块的优先级应该比服务模块高。
5.6.2.1 传统操作系统实现方案1
int thread_comm(int para) { while(1) { if(收到请求) { 唤醒thread_server线程 or 释放信号量; 其他代码; } } } int thread_server(int para) { while(1) { 提供收到的请求对应的服务; 线程暂停 or 请求信号量; } } int main(void) { 创建thread_comm线程; 启动thread_comm线程; 创建thread_server线程; 启动thread_server线程; }
|
从上述代码中我们可以看到,thread_server线程无论如何,都占据内存的,即使通信线没有插上,一辈子都收不到请求,也是这样。那么,我们有改进方法吗?能不能收到请求后再创建线程,像传统方案2的代码这样。
5.6.2.2 传统操作系统实现方案2
int thread_comm(int para) { while(1) { if(收到请求) { if(thread_server线程未创建) { 创建thread_server线程; 启动thread_server线程; }else 唤醒thread_server线程 or 释放信号量; } 其他代码; } } int thread_server(int para) { while(1) { 提供收到的请求对应的服务; 线程暂停 or 请求信号量; } } int main(void) { 创建thread_comm线程; 启动thread_comm线程; } |
一般来说,这种方法是不合理的,因thread_comm线程是高优先级线程,thread_server是低优先级线程。创建线程需要很长时间,并且分配栈可能需要使用动态分配内存,执行时间和结果都不确定。在高优先级的thread_comm线程中为低优先级的thread_server线程做执行这些操作,相当于在高优先级的线程中,花很多时间为低优先级线程做事,这是不合理的。
再者,在thread_comm中总是判断thread_server是否创建,也降低了运行效率。
5.6.2.3 DJYOS实现方案
int event_comm (int para) { while(1) { if(收到请求) { 弹出evtt_server类型事件; 其他代码; } } } int event_server (int para) { while(1) { 提供收到的请求对应的服务; 事件同步; } } int main(void) { 登记evtt_comm事件类型; 弹出evtt_comm类型事件; 登记evtt_server事件类型; }
|
表面上,DJYOS下的代码和传统操作系统下方案1的代码类似,但在水面以下,是完全不同的。因为如果event_comm没有收到服务请求,就不会弹出evtt_server事件类型,其所对应的线程就不会被创建,也就不会占用计算机资源。
有读者可能会问了:这样不会出现“传统方案2”中的问题吗?很好,这个担心在DJYOS下是多余的,因为弹出事件仅仅是把事件控制块推进事件调度队列中,需要的时间是非常少的。而创建线程的操作,只有等到该事件必须被处理时,才执行。因event_comm的优先级高于event_server,故不会在event_comm中执行创建线程的操作。
那又有读者可能要问,我要收到服务请求后,需要可靠响应怎么办?的确,创建线程需要分配内存,而分配内存有失败的可能。djyos登记事件类型时,允许静态分配内存的,系统会自动创建线程,等弹出事件之后,就无须创建线程,而是分配线程了。但线程投入运行,仍然是在收到端口发来的数据之后。
5.6.2.4 分析
传统操作系统下,无论任务是否产生,都要占用资源。
DJYOS下,不实际产生任务,就不占用资源。
孰优孰劣,就不用我说了吧。
举个简单例子,某通信产品,依通信口数量不同,有两种型号,A8型有8个通信口,A24型有24个通信口,每个通信口收到的数据,都由独立的线程处理。每个口需要为之创建一个线程,需要分配缓冲区,因此两个型号需要的内存数量和CPU速度是不一样的。
传统操作系统实现该产品的话,因为线程在上电时统一创建,要使软件版本保持一致,就必须为A8和A24型配置相同的内存,付出更高的硬件成本;为了节省成本,A8和A24型会按实际需要配置资源,此时,两个型号的软件版本是不一样的,至少差一个配置常量是不一样的。即使只有一个配置常量不一样,企业也要管理两个不同的版本。
在DJYOS下,未安装的通信口,自动就不占用资源,完全没有上述问题。
5.6.3 场景3
在场景2的基础上,如果频繁收到通信口发来的服务请求,但各个请求之间没有内在的关联,各个请求是可以独立处理的,有许多网络服务器接收的请求,以及云计算中的服务请求,就有这个特点。
5.6.3.1 传统操作系统实现方案
如果使用“传统操作系统方案2”的方法,CPU就只能串行地处理一个个接收到的请求,无论CPU的主频多么高,无论计算机有几个CPU核,都只能如此。
改善的办法是,创建多个线程,把不同的请求分配到不同的线程,但这样做的问题是,你不知道需要创建多少线程,创建的线程要是少了,服务请求密集到达的时候,就不够用;创建多了,就要占用很多资源,即使没有接收到请求,或者请求很少,这些资源也必须占用着。况且,这些活,操作系统内核是不会帮你做的,一切都要应用程序自己张罗。尤其是,在多核环境下,尤其麻烦。于是,就发展出了复杂的线程池技术,用一个用户级的软件组件去管理线程,有兴趣的可以google“线程池”,这里就不再赘述了。
5.6.3.2 DJYOS实现方案
在DJYOS下,天生就能支持优化这种应用。DJYOS的事件类型控制块中,有三个成员是为此类应用服务的:
vpus_res:繁忙时系统为本类型事件保留的线程数量,在登记事件类型时指定,详见djy_evtt_regist函数说明。
vpus_limit:该类型事件同时拥有的线程数量上限,在登记事件类型时指定,详见djy_evtt_regist函数说明。
vpus:本类型事件已经拥有的线程数量。
在这种情况下,只要把5.6.2.3代码中的event_server函数改成如下:
int event_server (int para) { 提供收到的请求对应的服务;//注意,事件入口函数不再是while(1)循环。 } |
并且在登记evtt_server事件类型时,指定该类型为EN_INDEPENDENCE(独立型)就可以了。
当事件类型登记后,vpus=0或1(如果事件类型的优先级高于128),若事件频繁弹出,即频繁收到服务请求,操作系统将为此类事件创建多个线程,直到线程数量达到上限vpus_limit,达到上限后,如果再有新事件弹出,新事件就将阻塞在调度队列中。线程执行完所需服务后,操作系统会查看事件队列中有没有evtt_server类型的事件,如果有,就直接把线程转交给它。如果没有,再查看evtt_server类型事件拥有的线程数量是否超过vpus_res,若超过就销毁该线程,否则保留该线程。
这样,如果频繁收到服务请求,操作系统就为evtt_server类型事件保持较多线程,否则就维持在比较低的水平。
5.6.3.3 分析
本场景所涉及的,是大量相同或相似任务的线程级并行处理的问题。
传统操作系统面对这种应用时,需要在调度器之上,在应用程序这一级,用复杂的线程池技术才能实现线程级并行,整个程序的复杂度大大增加。而DJYOS却在调度器这一级别直接实现了需要用线程池技术才能解决的问题,仅用了非常少的代码量,系统复杂度几乎没有增加。我们都知道,系统越简单,可靠性越高。
5.6.4 场景4
如何实现事件触发式编程,我们知道,VB、VC等可视化编程工具下是事件触发式编程的,这说明,事件触发式编程是我们所需要的。在传统操作系统和DJYOS下,实现事件触发式编程有什么异同呢?
5.6.4.1 传统操作系统实现方案
让我们来看看CBuilder下的一个编程场景:
先优雅地拖一个按钮放到桌面上。
为鼠标点击该按钮的事件编写处理函数。
编码工作就这样完成了,很简单很强大吧。
接着编译、执行,用鼠标点击该按钮,就会执行处理函数。
这种优雅高效的编程方式是怎样实现的。原来,由开发工具创建的一个或多个线程一直在后台候着,等待操作系统弹出鼠标点击事件。操作系统则负责检测并弹出事件,潜伏的线程一旦收到鼠标点击事件,便被唤醒执行。我们可以看到,无论该按钮是否被点击,甚至一辈子都不点击,该线程依然要占用系统资源。
在事件触发是编程环境下,程序员只与程序需要处理的具体事件打交到,其编程过程完全与线程、进程等无关。我们也知道,在传统操作系统上,只有在PC上或者能运行linux、wince等的高端嵌入式平台上才能使用上述便利。为什么呢?究其原因,是因为操作系统是按照线程调度的,必须经过开发工具的包装后,才能转换成事件触发。而这种包装,需要耗费大量的计算机资源,所以只适合在高端平台上使用。再者,这种包装会大大增加软件的复杂性,其复杂性可能带来不可靠以及不可预测因素,也不适合在实时控制系统中应用。因此传统操作系统下,事件触发式只适合于做复杂的界面编程。
5.6.4.2 DJYOS实现方案
无须多说了,DJYOS只提供事件触发式编程模式,无论在高端平台还是在单片机上,都只能用事件触发式编程,区别仅在于,单片机上可能不能提供可视化编程方式。
5.6.4.3 分析
传统操作系统要的面向事件编程,只能在高端平台上实现,是不折不扣的奢侈品,而DJYOS却可以在单片机上,使用简单的开发工具就可以实现,摇身一变成为日用品;
传统OS,即使使用VC、VB之类的工具支持,事件触发式编程主要用于界面编程,不能覆盖所有需求,而DJYOS只需要单片机开发工具,就覆盖全部需求;
传统OS在VB、VC之类工具包装后,用事件触发式编程产生的目标程序尺寸庞大、效率低下,而DJYOS却跟线程编程有同样的效率。
5.7 事件调度相关API
5.7.1 timeout参数说明
凡是会引起阻塞的系统调用,都有一个参数:timeout,表示超时时间(uS),以避免无休止的等待。会引起阻塞的API,当阻塞条件解除时,将会被唤醒,如果阻塞条件一直存在,timeout参数将决定阻塞时间。
若 timeout == CN_TIMEOUT_FOREVER,则表示永久等待;
若 timeout == 0;则不等待,函数返回CN_SYNC_TIMEOUT。
若 timeout == 其他,则事件进入阻塞队列,timeout时间后会被唤醒,函数返回CN_SYNC_TIMEOUT。非tickless模式下,timeout会自动向上取整为CN_CFG_TICK_US(系统心跳)的整数倍。
5.7.2 微秒、纳秒级延时
参见第17章。
5.7.3 DJY_EvttRegist:注册事件类型
u16 DJY_EvttRegist(enum enEventRelation relation,
ufast_t default_prio,
u16 vpu_res,
u16 vpus_limit,
ptu32_t (*thread_routine)(void),
void *Stack,
u32 stack_size,
char *evtt_name);
头文件:
os.h
参数:
relation:事件类型属性选项。EN_INDEPENDENCE表示独立型事件类型,EN_CORRELATIVE表示关联型事件类型。关于独立型和关联型事件的差异,参见第5.2.1节。
default_prio:事件类型的默认优先级。如果在调用DJY_EventPop函数弹出该类型的事件时,如果prio参数为0,则新弹出的事件优先级是default_prio。
vpus_res:本参数用于独立型事件,系统为该事件类型保留的空闲线程数,当一个独立型事件有多个实例时,系统将为其创建多个线程,而事件处理完成后,如果该类型事件拥有的线程数量超过vpus_res,则销毁所用线程,否则保留。
vpus_limit:本参数用于独立型事件,该事件类型拥有的线程数量上限。
thread_routine:线程入口函数(事件处理函数)。
Stack:静态的栈指针,如果用户已经分配了栈,由此传入指针;如果Stack==NULL的话,系统将用malloc分配栈。
StackSize:如果Stack==NULL,在代表执行thread_routine需要的栈尺寸,如果Stack!=NULL;则代表Stack指针指向的内存块的size。
evtt_name:事件类型名。允许是NULL,但如果指定,就不能与其他事件类型名重名,不允许超过31个字符(超出部分将被忽略)。
返回值:
注册成功返回该事件类型ID,数值范围是16384~32767;失败则返回CN_EVTT_ID_INVALID。
说明:
注册一个新的事件类型到系统中,事件类型经过登记后,就可以pop了,否则,系统会拒绝pop该类型事件。下面就事件类型中涉及的一些知识作简要解释。
每次登记一个事件类型系统会分配一个事件类型控制块。事件类型控制块的总量是静态分配的,其数量需按照系统内存量和应用程序实际需求合理设定。
图 5‑3 DIDE中配置内核
事件类型ID一经注册分配,就不会改变,直到调用DJY_EvttUnregist注销之。注销后,该ID可以被重新利用。
优先级
DJYOS允许的优先级是0~255 ,但登记事件类型时只允许使用 5~249 ,0~4 、250~255 是操作系统保留,不允许用户使用。常用的优先级如下表表。
优先级宏定义 |
优先级 |
意义 |
cn_prio_wdt |
1 |
这是DJYOS看门狗专用,由于djyos的 看门狗使用闹钟同步,要每隔一定时间 进行监测,优先级必须高于任何事件的 优先级才能做到。 |
cn_prio_critical |
100 |
实时性要求非常高的事件,使用该优级。 |
cn_prio_real |
120 |
实时性要求比较高的事件,使用该优级。 |
cn_prio_RRS |
200 |
对实时性不是很苛刻的事件。一般情下, 创建事件都使用该优先级。 |
cn_prio_sys_service |
250 |
系统服务需要的优先级。 |
cn_prio_invalid |
255 |
无效优先级。 |
优先级小于128的事件,属于紧急事件,在登记事件类型时,系统将为其创建一个线程,并且运行过程中也至少为它保留一个线程。
线程池功能
参数vpus_res和vpus_limit跟线程池功能相关。基于下面的考虑,系统设定了这两个参数。一方面如果某个独立型的事件频繁弹出,系统需要创建相应多的线程来处理它,但是,线程数量太多的话,会耗光系统资源,因此系统为独立型事件类型设置了创建线程数量的上限vpus_limit。另一方面当事件处理完成后,线程留着也没用了,系统应该销毁它。且慢,由于创建线程会有一定的开销,故系统会保留一些线程,以备再次弹出该类型的事件时,立即把保留的线程分配给该事件。保留的线程数量由vpus_res确定。
注意,只有独立型事件类型才有线程池的机制,关联型事件系统最多只为其创建一个线程。
事件类型的先登记后使用原则
DJYOS的事件类型采用先登记后使用的原则,事件类型登记后,应用程序可以弹出该类型的事件,操作系统通过调度器来决定何时调用事件处理函数。如果是首次弹出该类型事件,必须使用事件类型ID作为参数;如果事件队列中已经有该类型事件,弹出事件时也可以使用事件ID为参数,详见DJY_EventPop函数说明。在事件调度未开始前的初始化过程中,可以登记事件类型和弹出事件,但必须在调用操作系统初始化函数__Djy_InitSys之后,不过这个限制对用户基本没有影响,因为系统在完成CPU核心硬件初始化后,就会立即调用__Djy_InitSys函数。在__Djy_InitSys函数中,登记了系统的第一个默认优先级为 250 的事件类型:系统服务事件类型。
事件参数
每次弹出事件,都可以携带两个ptu32_t类型的参数,该参数保存在事件控制块的param1和param2成员。DJY_GetEventPara函数用于读取正在处理的事件的参数,连续多次调用DJY_EventPop函数的话,后调用的参数将覆盖前面的,而不管早先的参数是否被读取。
evtt_name的用法
如果evtt_name != NULL,则新注册的事件类型是有名字的,调用函数DJY_GetEvttId可以得到事件类型ID,这在模块间交互非常有用。使用名字来交互,可以降解模块间的耦合。假设事件类型A的名称设为NULL,事件B在执行过程中,需要调用DJY_EventPop弹出A类型的事件。由于A没有名称,DJY_EventPop要获得事件类型A的ID,事件B和A的代码就必须在一起编译,如果A和B分属不同模块,将导致模块间耦合。有了名称之后,模块间可以通过DJY_GetEvttId函数获得事件类型A的ID,如此两个模块就可以互相独立开发和编译,项目经理或系统工程师只要管理好命名空间就可以了。
如何确定StackSize
StackSize是调用thread_routine所需要的内存尺寸,如果空间不够,将会发生非常严重且神秘莫测的问题。确定StackSize的方法如下:
1.列出thread_routine直接和间接调用的所有函数,计算每个参数和局部变量(不含静态变量)所需的内存。
2.列出线程入口函数thread_routine调用各级子函数的所有可能路径。
3.把各调用路径所调用的函数所需的内存加起来,就是该路径所需要的内存。若有递归调用,该函数所需内存应该乘以最大可能的递归次数。
4.挑出内存需求最大的一个路径,再乘以一个安全系数,得到stack_size。该安全系数一般取1.2即可,要求特别高的系统,可取1.5。
注意如果发生递归调用,则递归函数所需要的内存还应该乘以最大的迭代次数。同时,请咨询你的BSP工程师,中断处理函数是否使用被中断线程的栈,如是,则每个线程的栈都要加上中断嵌套最深时的中断栈需求。
返回CN_EVTT_ID_INVALID的处理
如果函数返回CN_EVTT_ID_INVALID,说明事件类型控制块已经耗尽,增大工程目录下的core_config.c文件中的常量gc_u32CfgEvttLimit的值可以解决。Shell命令evtt可以查看事件类型控制块的使用情况。
5.7.4 DJY_EvttUnregist:注销事件类型
bool_t DJY_EvttUnregist(u16 evtt_id)
头文件:
os.h
参数:
evtt_id:待注销的事件类型ID。
返回值:
注销成功返回true;失败则返回false。
说明:
注销一个事件类型。如果事件类型仍然在使用之中(尚有弹出的事件未被处理),事件类型则暂时无法完成注销,系统完成对该类型的所有弹出事件之后,自动注销该事件类型。在等待注销期间,系统不允许弹出该类型的新事件。
事件类型成功注销时,事件类型ID和事件类型控制块将被收回,属于该类型的事件处理线程也将被完全销毁。
5.7.5 DJY_RegisterHook注册调度hook函数
bool_t DJY_RegisterHook(u16 EvttID,SchHookFunc HookFunc)
头文件:
os.h
参数:
EvttID:被注册的事件类型ID。
HookFunc:hook函数指针。
返回值:
true = 成功注册, false = 失败。
说明:
多事件调度过程中,会发生事件上下文切换,djyos提供一个机会,让用户注册钩子函数,在事件切入和切离时,该钩子函数都会被调用。无论是切入还是切离,都在当前事件的上下文中调用,使用当前事件的栈空间。注意,切入和切离时调用的是同一个函数,只是参数的值不同。
SchHookFunc类型定义为:
typedef void (*SchHookFunc)(ucpu_t SchType);
参数SchType == EN_SWITCH_IN表示切入。
参数SchType == EN_SWITCH_OUT表示切离。
5.7.6 DJY_EventPop:弹出事件
u16 DJY_EventPop( u16 hybrid_id,
u32 *pop_result,
u32 timeout,
ptu32_t PopPrarm1,
ptu32_t PopPrarm2,
ufast_t prio);
头文件:
os.h
参数:
hybrid _id:事件类型ID或者事件ID。
pop_result:用于返回新事件弹出或处理状态,可以是NULL。
1、 若函数返回了合法的事件ID,且timeout !=0,* pop_result可能的返回值:
CN_SYNC_SUCCESS,表示新事件被处理完成后返回;
CN_SYNC_TIMEOUT,表示新事件未被处理完,超时返回;
CN_STS_EVENT_EXP_EXIT,新事件处理过程中被异常终止;
2、 若函数返回CN_EVENT_ID_INVALID,* pop_result可能的返回值:
EN_KNL_EVTTID_LIMIT,参数hybrid_id是事件类型号,且超过配置的事件类型数量上限。
EN_KNL_EVENTID_LIMIT ,参数hybrid_id是事件号,且超过配置的事件数量上限。
EN_KNL_CANT_SCHED,timeout参数 !=0,但调度处于关闭状态。
EN_KNL_EVTT_UNREGISTER,欲弹出的事件类型未注册,或正在等待注销。
EN_KNL_INVALID_PRIO,非法优先级。
EN_KNL_ECB_EXHAUSTED,没有空闲事件控制块。
timeout:参见第5.7.1节。
PopPrarm1、PopPrarm2,传递给新事件的参数,可以用DJY_GetEventPara函数取出。若新事件是关联型事件,且未及取出该参数,又重复弹出同类型事件,那么最后一次弹出时的参数PopPrarm1、PopPrarm2将覆盖前一次弹出的。
prio:事件优先级。若参数prio == 0,则新事件优先级为注册事件类型时的默认优先级(详见函数DJY_EvttRegist)。若参数prio != 0,则该值为新弹出事件的优先级。
返回值:
事件成功弹出返回事件ID;否则返回CN_INVALID_EVENT_ID。
说明:
向操作系统报告发生某事件类型的事件,操作系统接报后,把事件放进调度队列。在DJYOS系统下,应用程序完成一个完整的事件处理周期是从弹出事件开始的。
事件类型在登记后首次弹出时,hybrid_id必须使用事件类型ID,随后,可以使用事件类型ID,也可以使用事件ID,前提是该事件ID还存在(事件处理完成后,ID会被收回)。对于关联型事件,使用事件ID和事件类型ID是相同的。但对于独立型事件,hybrid_id使用事件ID和事件类型ID的效果完全不同。如果使用事件类型ID,则会分配新的事件控制块,新事件插入就绪队列中,并创建(分配)新的线程去处理这条事件。
如果timeout != 0,当前正在处理的事件将阻塞,直到调用Djy_EventSessionComplete函数,或者事件处理函数自然返回或异常退出,或者超时才解除阻塞。
警告:函数的prio参数只适用于弹出新事件。对于“旧(已存在)事件”,即参数hybrid_id为事件ID或者已弹出过的关联型事件类型ID时,该参数是无效的。
5.7.7 DJY_WaitEvttPop:等待事件弹出
u32 DJY_WaitEvttPop(u16 evtt_id,u32 *base_times, u32 timeout)
头文件:
os.h
参数:
evtt_id:目标事件类型ID。
base_times:输入时设定弹出次数值,目标事件的第“*base_times+1”弹出时,同步条件达到时,如果给NULL,则从调用时的弹出次数+1做同步条件。若base_times !=NULL, *base_times返回目标事件的实际弹出次数。
timeout:参见第5.7.1节。
返回值:
CN_SYNC_SUCCESS:表示等待事件类型的事件弹出次数满足,当前成功唤醒。
CN_SYNC_TIMEOUT:表示阻塞超时。
CN_SYNC_ERROR:表示当前事件阻塞失败(原因如事件类型未注册、事件类型需要被注销以及系统静止调度)。
EN_KNL_EVTTID_LIMIT:事件类型ID出错。
说明:
某事件处理过程中,调用本函数将等待另一个类型的事件被弹出设定的次数。如果被等待的事件未弹出设定次数,则阻塞等待;若已经被弹出设定次数,则不阻塞。注意,被等待的事件类型,也允许是调用者自身。
当前正在处理的事件进入被等待弹出的事件类型的弹出同步队列,等待evtt_id类型的事件弹出次数达到设定值后,系统调度器会唤醒当前事件。
若“*base_times-当前已弹出次数”>0x80000000,也认为同步条件已经达到,如此设计,是为防止出错情况下没玩没了地等。
设定的弹出次数只计有效弹出,有效弹出的判定比较复杂。
1.如果目标事件类型是关联型事件,每次弹出都有效。
2.如果是独立型事件且调用DJY_WaitEvttPop时hybrid_id是事件类型ID,则每次弹出对于弹出同步队列中的非本类型事件都是有效弹出。
3.如果是独立型事件且调用DJY_WaitEvttPop时hybrid_id是事件ID,则对弹出同步队列中事件ID== hybrid_id的事件算有效弹出。
当base_times非NULL时,该参数返回值表示的是目标事件类型的事件已弹出的次数。注意,一般情况下,返回时base_times的值会加1,但会有例外。例外情况:
如果调用DJY_WaitEvttPop时,传入参数*base_times = 100,正常情况下,pop_times=101时,函数将返回,*base_times应该=101。但如果此时目标事件类型的pop_times已经大于101的话,函数将立即返回,*base_times的值将被设为当前的pop_times。
这个函数常见的用法是等待自身再次弹出:
DJY_WaitEvttPop(DJY_GetMyEvttId( ),NULL,CN_TIMEOUT_FOREVER);
源码5‑1弹出同步典型用法
ptu32_t net_service( void ) { while(1) { get_connect_request( ); DJY_EventPop(protocol_evtt_id,...); } } ptu32_t net_socket( void ) { while(1) { get_data_packet( ); DJY_EventPop(protocol_event_id,...); } } ptu32_t net_protocol( void ) { u32 ptimes = 1; while(1) { do_something; DJY_WaitEvttPop(DJY_GetMyEvttId (),&ptimes, CN_TIMEOUT_FOREVER); } } |
net_service收到连接请求后,弹出事件,使用的是事件类型ID;
net_socket收到数据包后,弹出事件,用的是事件ID;
协议处理模块(net_protocal)用于处理网络报文,每次处理完报文后,就等待自身再次被弹出。
协议处理模块(net_protocal)被设计为独立型事件,net_service弹出事件,由于使用的是事件类型ID,每次调用系统都会创建新的事件,每条事件都会有自己的事件ID,并为每条事件创建新的线程来运行net_protocol函数。
net_socket弹出事件时,用的是事件id,此时不会产生新的事件,而是仅仅把protocol_event_id相对应的那一条事件激活。
所有线程处理完当前数据包后,都会在DJY_WaitEvttPop阻塞,等待新的数据包。
进一步地,如果想每收到10个数据包,批量处理一次,怎么办呢?
只要把net_protocol函数改为这样就可以了:
ptu32_t net_protocol( void ) { u32 ptimes = 1; while(1) { do_something; times += 9; DJY_WaitEvttPop(DJY_GetMyEvttId (),&ptimes, 1000000); } } |
DJY_WaitEvttPop在收到10个数据包后才会返回,由于timeout参数被设为1000000,则如果1秒内收到的数据包不足10个,也将返回。
5.7.8 DJY_GetEvttId:查询事件类型ID
u16 DJY_GetEvttId(char *evtt_name)
头文件:
os.h
参数:
evtt_name:事件类型名称。
返回值:
查找成功返回事件类型ID;否则则返回CN_EVTT_ID_INVALID。
说明:
通过事件类型名查找对应的事件类型ID,注意,事件类型不会重名,但允许没有名字。
5.7.9 DJY_SaveLastError:保存线程的最后错误
void DJY_SaveLastError(u32 ErrorCode)
头文件:
os.h
参数:
ErrorCode:错误代码。
返回值:
无。
说明:
应用程序或者操作系统运行遇到错误,通过这个函数将错误信息告诉系统。事件控制块有一个变量,记录该事件处理过程中的最后一个错误代码,重复产生的错误码将互相覆盖。
C库中的errno,也是基于DJY_SaveLastError和 DJY_GetLastError实现的,因此,下面两行代码是等效的:
DJY_SaveLastError (ErrorCode);
errno = ErrorCode;
5.7.10 DJY_GetLastError:获取事件最后一次错误码
u32 DJY_GetLastError(void);
头文件:
os.h
参数:
无。
返回值:
错误代码。
说明:
提取当前事件所记录的最近发生的错误代码。
参考DJY_SaveLastError函数,下面两行代码等价:
n = DJY_GetLastError (ErrorCode);
n = errno;
5.7.11 DJY_SetRRS_Slice:设置轮转周期
void DJY_SetRRS_Slice(u32 slices)
头文件:
os.h
参数:
slices:轮转调度时间片长度,单位为微秒。如果系统运行于非tickless模式,则该值系统自动向上取整为CN_CFG_TICK_US(系统心跳)的整数倍。
返回值:
无。
说明:TIME_BASE_MIN_GAP
设置系统轮转调度时间片。如果系统运行于tickless模式,时间片的系统缺省值为10mS,如果系统运行于非tickless模式,则是1个CN_CFG_TICK_US,即与系统心跳同频率。如果就绪的最高优先级事件有多个,则轮流执行,每条事件处理时间达到slices后,将发生轮转调度,强制切换到下一条。事件处理线程调用DJY_EventDelay(0)相当于主动引发一次轮转调度。如果slices设为0,表示不允许时间片轮转。
5.7.12 DJY_GetRRS_Slice:获取轮转周期
u32 DJY_GetRRS_Slice(void)
头文件:
os.h
参数:
无。
返回值:
当前系统轮转调度时间片长度,单位为uS。
说明:
查询系统当前轮转调度时间片的长度。
5.7.13 DJY_GetRunMode:查询系统运行模式
u32 DJY_GetRunMode(void);
头文件:
os.h
参数:
无。
返回值:
CN_RUNMODE_SI:单进程单映像模式
CN_RUNMODE_DLSP:单进程动态加载映像模式
CN_RUNMODE_MP:多进程模式
CN_RUNMODE_SMP:对称多处理器模式
说明:略
5.7.14 DJY_QuerySch:查询是否允许调度
bool_t DJY_QuerySch(void)
头文件:
os.h
参数:无
返回值:
允许调度返回true,否则返回false。
说明:
检查当前是否允许调度,允许异步信号且运行事件处理函数时,将返回true。
5.7.15 DJY_IsMultiEventStarted:调度是否已经开始
bool_t DJY_IsMultiEventStarted(void)
头文件:
os.h
参数:无
返回值:
调度已经开始则返回true,否则返回false。
说明:
调度已经开始后,就返回true,与是否允许调度无关。本函数用于检查当前运行环境是否仍然在初始化过程中。
5.7.16 DJY_SetEventPrio:设置事件优先级
bool_t DJY_SetEventPrio (ufast_t new_prio)
头文件:
os.h
参数:
new_prio:设置的新优先级。
返回值:
设置成功返回true;失败则返回false。
说明:
设置当前事件的优先级。当前事件处理过程中,可以调用本函数,改变自身的优先级。本函数可能会产生系统调度,如果优先级被改低了,且有更高优先级的事件处于就绪态,将立即调度,阻塞本事件运行。
这个函数只能改变调用者自身的优先级,不能修改其他事件优先级,包括优先级在内,DJYOS没有任何直接改变其他事件的参数的函数。
5.7.17 DJY_RaiseTempPrio:继承事件优先级
bool_t DJY_RaiseTempPrio(u16 event_id);
头文件:
os.h
参数:
event_id:事件id。
返回值:
设置成功返回true;失败则返回false。
说明:
如果g_ptEventRunning的优先级较高,event_id临时以g_ptEventRunning的优先级运行,直到调用DJY_RestorePrio,否则不改变优先级。
5.7.18 DJY_RestorePrio:恢复事件优先级
bool_t DJY_RestorePrio(void);
头文件:
os.h
参数:无
返回值:
设置成功返回true;失败则返回false。
说明:
与RaiseTempPrio函数配套使用。
5.7.19 DJY_EventDelay:事件延时
u32 DJY_EventDelay(u32 u32l_uS)
头文件:
os.h
参数:
u32l_Us:延迟(阻塞)时间,单位是微秒。该值系统自动向上取CN_CFG_TICK_US(系统心跳)的整数倍。
返回值:
实际延时(阻塞)时间,微秒数。
说明:
将当前(正在执行)事件延时(阻塞)u32l_uS微秒后继续运行。
不能获得精度高于tick间隔的延时。注意设置u32l_uS为零时,如果系统存在其他同优先级的事件,则调度器会让其他同优先级的事件将先执行,不管轮转调度是否允许,如果没有相同优先级的就绪事件,则直接返回。
由于参数值取整处理、优先级抢占或关中断等因素,会附加额外的延时时间,函数的实际返回的延时(阻塞)时间要比设定值长。
5.7.20 DJY_EventDelayTo:事件定时
s64 DJY_EventDelayTo(s64 s64l_uS);
头文件:
os.h
参数:
s64l_uS:延时结束时刻。该时刻已开机时刻为基准,单位是微秒。非tickless模式下,参数值系统将自动向上取整CN_CFG_TICK_US的整数倍。
返回值:
实际延时时间,单位微秒。
说明:
将当前时间延时到s64l_uS时刻再继续执行。参数延时结束时刻是一个64位数,理论上需要29万年才会发生溢出,因此不考虑数据溢出问题。
5.7.21 DJY_WaitEventCompleted:等待事件完成
u32 DJY_WaitEventCompleted(u16 event_id,u32 timeout)
头文件:
os.h
参数:
event_id:目标事件ID。
timeout:参见第5.7.1节。
返回值:
CN_SYNC_SUCCESS:指定事件完成处理。
CN_SYNC_TIMEOUT:当前事件阻塞超时。
EN_KNL_CANT_SCHED:当前事件阻塞失败。
EN_KNL_EVENT_FREE:参数event_id出错(不存在)。
说明:
阻塞等待指定事件完成,当指定事件处理完成,或者异常退出,或者超时时间到,系统唤醒当前事件。事件完成的标准:1、事件处理函数返回;2、事件处理函数调用DJY_EventComplete;3、事件处理函数异常退出。
5.7.22 DJY_WaitEvttCompleted:等待事件类型完成
u32 DJY_WaitEvttCompleted(u16 evtt_id,u16 done_times,u32 timeout)
头文件:
os.h
参数:
evtt_id:目标事件类型ID。
done_times:完成次数,当前事件阻塞,直至evtt_id类型的事件处理完成done_times次后唤醒。若done_times == 0,则表示所有evtt_id类型的事件均处理完后,唤醒当前事件。
timeout:参见第5.7.1节。
返回值:
CN_SYNC_SUCCESS:指定事件类型的事件完成处理。
CN_SYNC_TIMEOUT:当前事件阻塞超时。
EN_KNL_CANT_SCHED:当前事件阻塞失败。
EN_KNL_EVTTID_LIMIT,事件类型ID出错(不存在)。
说明:
等待某事件类型的事件被处理完成指定次数,事件完成的标准:1、事件处理函数返回;2、事件处理函数调用DJY_EventComplete;3、事件处理函数异常退出。
5.7.23 DJY_GetEventResult:查询事件处理结果
ptu32_t DJY_GetEventResult(void)
头文件:
os.h
参数:
无
返回值:
当前事件的处理结果。
说明:
一个事件在处理过程中,如果弹出了新事件,本函数可查询新事件的处理结果。要得到处理结果,必须满足两个条件:
1、调用DJY_EventPop函数的参数timeout !=0;
2、调用DJY_EventPop时返回了合法的事件ID,且* pop_result返回CN_SYNC_SUCCESS。
这个返回结果,就是事件处理函数的返回值,或者调用DJY_EventComplete函数时传入的参数。
5.7.24 DJY_GetEventPara:获取事件参数
void *DJY_GetEventPara (ptu32_t *Param1,ptu32_t *Param2)
头文件:
os.h
参数:
Param1:事件参数。
Param2:事件参数。
返回值:
无。
说明:
提取该事件最近一次弹出(DJY_EventPop)时,传递给当前事件的参数。
5.7.25 DJY_EventComplete:事件处理完成
void DJY_EventComplete(ptu32_t result)
头文件:
os.h
参数:
result:传递事件处理结果,将返回给弹出该事件的事件(如果设定了timeout != 0)
返回值:
无。
说明:
通知系统,事件已经处理完成,但未事件处理函数仍将继续运行,等待处理下一条时间,将激活正在等待本事件完成的事件。常见使用方法:
1、 A事件调用DJY_WaitEvttCompleted等待B类型事件完成n次,则B类型事件调用n次本函数后,将激活A事件。
2、 A事件调用DJY_WaitEventCompleted等待B事件完成,则B事件调用本函数,将激活A事件。
3、 A事件调用DJY_EventPop弹出B事件,且timeout != 0,则B事件调用本函数,将激活A事件。
5.7.26 DJY_GetMyEvttId:获取当前事件类型ID
u16 DJY_GetMyEvttId(void)
头文件:
os.h
参数:
无。
返回值:
查询成功返回事件ID,如果当前运行在异步信号ISR中,则返回CN_EVTT_ID_ASYN。如果在实时中断中调用,则返回进入中断前的状态。
说明:略
5.7.27 DJY_GetMyEventId:获取当前事件ID
u16 DJY_GetMyEventId(void)
头文件:
os.h
参数:
无。
返回值:
查询成功返回事件ID,如果当前运行在异步信号ISR中,则返回CN_EVENT_ID_ASYN。如果在实时中断中调用,则返回进入中断前的状态。
说明:略。
5.7.28 DJY_WakeUpFrom:查询当前事件唤醒原因
u32 DJY_WakeUpFrom(void)
头文件:
os.h
参数:
无。
返回值:
当前事件被唤醒原因。
说明:
查询当前事件被唤醒原因。在多线程环境下,事件处理过程可能被反复阻塞和唤醒,当前(正在执行中的)事件肯定是就绪态,调用本函数,可查询自己进入当前状态之前处于什么状态,可能值如下表(在djyos.h中定义):
CN_STS_EVENT_DELAY |
延时到,不包括DJY_DelayUs |
CN_STS_SYNC_TIMEOUT |
超时 |
CN_STS_WAIT_EVENT_DONE |
事件同步 |
CN_STS_EVENT_EXP_EXIT |
事件同步中被同步的目标事件异常退出 |
CN_STS_WAIT_EVTT_POP |
事件类型弹出同步 |
CN_STS_WAIT_EVTT_DONE |
事件类型完成同步 |
CN_STS_WAIT_MEMORY |
从系统堆分配内存同步 |
CN_STS_WAIT_SEMP |
信号量同步 |
CN_STS_WAIT_MUTEX |
互斥量同步 |
CN_STS_WAIT_ASYN_SIGNAL |
异步信号同步 |
CN_STS_WAIT_MSG_SENT |
等待消息发送 |
CN_STS_WAIT_PARA_USED |
等待消息处理完成 |
CN_WF_EVTT_DELETED |
事件类型相关的同步,因目标类型被删除而解除同步。 |
CN_WF_EVENT_RESET |
复位后首次切入运行 |
CN_WF_EVENT_NORUN |
事件还未开始处理 |
以上是位域定义,可能叠加,比如事件是因为同步超时而返回,则有两个bit会置1,一个是CN_STS_SYNC_TIMEOUT,表示超时,另一个表示更具体的原因,例如CN_STS_WAIT_SEMP=1表示在阻塞等待信号量时因超时而被唤醒。注意唤醒后并不一定立即投入运行,还得看优先级说话呢。
5.7.29 关闭调度
关闭调度和关闭异步信号是等同的操作,参见6.4。
关闭实时中断也将禁止调度,参见节6.4。
特别注意,如果是通过原子操作(int_low_atom_start或者int_high_atom_start)进入禁止中断的状态,调度将处于允许状态,你在这种时候如果调用可能引起阻塞的操作,结果将不可预知。因此,原子操作范围内,禁止访问敏感数据,也禁止调用系统服务。
5.7.30 DJY_GetEvttPopTimes:取事件弹出次数
u32 DJY_GetEvttPopTimes(u16 evtt_id);
头文件:
os.h
参数:
evtt_id:事件类型ID。
返回值:该类型事件累计弹出次数,超过32位数后,将回绕到0
5.7.31 DJY_GetEvttName:取事件类型名
bool_t DJY_GetEvttName(u16 evtt_id, char *dest, u32 len);
头文件:
os.h
参数:
evtt_id:事件类型ID。
dest:保存名字的缓冲区,内存由调用者提供。
len:缓冲区长度。
返回值:
成功执行返回true;否则返回false。
说明:略。
5.7.32 DJY_GetEventInfo:获取事件信息
bool_t DJY_GetEventInfo(u16 id, struct EventInfo *info);
头文件:
os.h
参数:
id:事件ID。
info:保存信息的结构指针,struct EventInfo在djyos.h中定义。
返回值:
成功执行返回true;否则返回false。
说明:获取指定事件的信息,如占用的CPU时间等。
第6章 中断系统
使用RTOS的场合,大多都对实时性有要求,甚至有些要求极端高的实时性。一般来说,设计者会把实时性要求很高的部分功能,用中断来实现,从这个意义上,对于RTOS来说,最坏情况下的中断响应延迟,几乎就是该系统的实时性指标。如果操作系统会带来额外的、甚至不确定的中断延时,那么对实时性要求很高的应用,就不能使用操作系统,或者需要增加额外的硬件来处理实时任务。DJYOS在最坏情况下,提供裸跑的中断延迟,实现无以伦比的实时性,使得一些原来只能裸跑的应用,也可以使用操作系统。
6.1 理解中断
中断如闲云野鹤,来去无踪,操作系统根本不知道什么时候会来中断;许多CPU响应中断后,会自动切换到特权级别,中断服务函数可以无法无天,根本不受控制。所有这些,都增加了中断系统的设计难度。很多操作系统,代码逻辑上,把中断视为虎狼,在执行临界代码时,一关了之;功能上,又把中断都看作“需要紧急处理的事件”,为了降低操作系统对实时性的影响,又千方百计地缩短持续关中断时间,为了讨好用户,又企图给用户的ISR函数提供更多的服务。这样,就把中断相关的代码,搞得很复杂。
中断,从形式上看,是一个异步到达的(通知)事件,异步是什么意思呢?异步是相对于代码执行序列的,即CPU根本不知道什么时间会来中断,也不知道中断达到时,自己运行到什么地方、处于什么状态。中断从形式上,并没有告诉我们,它是否需要处理,更没有说是否需要紧急处理。因此,把中断都看作“需要紧急处理的事件”,是没有依据的。
由于CPU是串行执行指令的,如果靠CPU执行指令的方式查询获得事件,比如查询IO口获得上升沿事件。从事件发生到CPU查询到的延迟时间,必然跟cpu的查询周期相关,查询间隔短了,CPU开销太大,间隔长了,又会错过中断,因此,紧急事件只能通过中断来获取。
所以,中断可以用三句话来概括:
1.中断是异步事件。
2.中断不一定是紧急事件。
3.紧急事件必须用中断来通知。
了解了中断的三个特性,我们就知道,并不是所有中断,都需要极速响应的,事实上,绝大多数情况下,只有极少量的中断需要很高实时性的。对不同需求的中断,不能一视同仁,更不能把中断都看成是一个需要紧急处理的事件。
在操作系统支持下,需要接近裸跑的中断延迟吗?
答案显然是肯定的,但是,人们对此似乎很宽容。为什么会宽容呢?当没有汽车时,人们能够容忍马车的速度;没有火车时,人们容忍汽车的速度;没有飞机时,人们容忍火车的速度。当所有操作系统都做不到近乎裸奔的中断延迟时,人们便容忍OS给中断响应带来额外延迟。当DJYOS实现了裸跑实时性的时候,你还会犹豫吗?
为什么要为一些不紧急的事件(例如键盘)分配中断号呢?
设计者经常为键盘分配中断号,对于台式PC来说,键盘也许勉强算紧急事件,因为PC中有很多慢速操作;但对于使用RTOS的嵌入式控制系统来说,却并非紧急事件。为什么呢?因为键盘是人手通过机械按钮实现的,操作系统的反映,只要比人的动作快就可以了,响应太快了,反而可能导致误触发,失去按键防抖功能。那为什么不少人会把按键挂在中断线上,使用中断来响应按键操作呢?答案是,一可以简化软件设计,不需要单独启动一个键盘扫描线程;二可以减轻cpu负担,不用定期扫描键盘;三是低功耗系统特有的,可以用键盘中断来唤醒休眠的cpu。
6.2 中断管理体系
理解了中断,我们就可以着手设计中断系统了。
1.能够为异步事件提供服务,既然是事件,就应该提供与普通事件一样的操作系统服务,使其编码更加容易。
2.为实时中断提供裸跑的中断延迟,即使在最坏情况下,也不能例外。
3.用户用中断处理异步事件,是希望简化系统设计,因此,不能因为异步事件而使调度系统更加复杂。
既企图对所有中断极速响应,又企图给中断响应函数以尽可能多的服务,是不是太贪婪了点?鱼和熊掌,选一样吧。更多更便利的服务,必然需要更多的关中断,直接导致中断延迟加长。最终的结果是:
1.异步事件得不到操作系统的充分支持,对编写ISR程序有诸多限制。
2.真正实时性要求很高的中断,却做不到很快响应。
3.调度器保护临界资源时,分为关调度和关中断两级,使系统更加复杂,完全违背了用户的用中断响应异步事件以简化软件的目的。
DJYOS系统设计中断管理模块时也体现了系统的“九九加一”原则:
1.日常大量存在的、实时性并不是特别高的工作,系统提供最大的便利,让程序员能够简洁地实现。
2.极少遇到的、高难度的、甚至挑战系统实时性和处理能力极限的工作,系统提供最大的灵活性,使问题的解决成为可能。
基于上述原则,在DJYOS系统中,中断被分为两大类,第一类是实时中断,对应现实世界中紧急程度非常高的中断信号,实时中断的响应与前后台系统无异,具有接近前后台系统的实时性,操作系统运行过程中,调度程序永远不会关闭实时中断,只是提供一个接口函数,使线程可以根据需要临时关闭实时中断。在实时中断里,程序员能够像在前后台系统中一样自由自在地编程,除了不能使用操作系统的系统调用外,没有太多的束缚,相应地,操作系统为实时中断提供的服务也最少。实时中断为用户的紧急突发事件提供绿色通道,它的实时性能要远高于其他操作系统的中断体系。
第二类是异步信号,对应紧急程度不是很高的中断信号,异步信号和普通事件没有实质性的差别,内核把他们等同看待。但因硬件特性,异步信号与普通事件还是有区别的,他们的相同点表现在:
1.禁止事件调度和禁止异步事件切换是等同的,也就是说,当禁止调度时,DJYOS把异步信号当作事件一样也禁止了。
2.异步信号ISR可以从堆中分配内存,可以释放和申请信号量,以及使用其他临界资源。
3.异步信号ISR可以使用所有的系统服务。
异步事件和普通事件的不同点表现在:
1.异步信号没有独立的上下文,故ISR不可以被阻塞。虽然允许调用所有的系统调用,但不会发生实际阻塞。例如malloc函数,如果在线程中调用,内存不足时,线程将被阻塞;如果在ISR中调用,线程不足时将直接返回NULL。
2.由于异步信号优先级高于所有普通事件,故其ISR不能像事件处理函数那样,做成死循环的形式。
图6‑1中断结构
一般cpu的中断控制器,都有许多中断输入线,每个中断线对应一个中断号,由一个独立的开关控制。
每个中断线,都允许独立设置为实时中断还是异步信号,该开关可能是硬件开关,也可能是软件开关,依赖于具体硬件以及移植者的选择。
异步信号有一个独立的总使能开关,也就意味着,异步信号是可以整体关断的。
实时中断没有独立总使能开关,异步信号和实时中断有一个公共的总使能开关,意味着,允许用户关闭所有中断。
DJYOS中,“异步信号使能开关”同时又是关调度开关,DJYOS没有独立的调度开关,使得DJYOS的临界区保护代码更加简洁,和用户希望通过“用中断实现异步信号”来简化软件设计的目的,是相适应的。简洁的调度器,也使DJYOS更加可靠。由于关异步信号和关中断等同,使得异步信号处理函数可以使用操作系统提供的全部服务,更加方便易用。
操作系统运行过程中,总中断使能开关时从来不会被关闭的,所以,DJYOS的实时中断处理能力,达到裸跑的速度,这是架构决定的,跟中断响应的代码是否精简无关。
DJYOS中断系统的特点
1.实时中断响应具有裸跑的实时性,使得有些原本不能使用操作系统的应用,可以享受操作系统的服务。
2.编写异步信号编程更加方便,像普通事件一样,允许使用所有操作系统服务。
3.系统更加简洁可靠,合并关闭调度和关闭异步信号的操作后,使系统临界区保护相关的代码大大简化。
6.3 中断使用步骤
6.3.1 CPU中断表
在文件“djysrc\bsp\cpudrv\nameofcpu\include\cpu_peri_int_line.h”中,定义了该cpu支持的所有中断源,用户直接使用即可。
特殊情况:有些cpu,例如ADI的adsp系列dsp,或者TI的C6000系列dsp,cpu支持的中断向量数少于外部中断源数,例如adsp的cpu只支持32个中断向量,而中断源却达到44个,这个表中就只能放32个。
6.3.2 实时中断编程模型
实时中断编程的ISR与落跑没有差异,其响应速度也与裸跑没有差异。
1.编写中断服务程序ISR,像普通C函数一样编写即可。
u32 __uart1_int(ufast_t uart_int_line) { 中断服务代码 } |
2.使用下列语句序列设置中断:
Int_Register(cn_int_line_uart1); Int_IsrConnect(cn_int_line_uart1,__uart1_int); Int_SettoReal(cn_int_line_uart1); Int_ClearLine(cn_int_line_uart1); Int_RestoreRealLine (cn_int_line_uart1); |
第一句,注册中断线到中断系统。
第二句,把ISR函数跟中断线联系起来。
第三句,把相应中断设置成实时中断。
第四句,清一下中断,以免受硬件的初始化状态影响。
第五句,使能中断
上述初始化过程,来自一个实际应用,该案例的MCU使用lpc1225,主频40Mhz,flash速度20Mhz,实际运行速度在20Mhz~40Mhz之间。但要实现2.5Mbps baud的串口通信,如果不能再接收中断发生5uS内响应中断的话,必然丢数据。因此,该应用使用了DJYOS的实时中断机制,实测下来,中断响应时间小于1.5uS,效果很理想。
6.3.3 异步信号的事件模式编程
这种模型的优点是,不需要编写ISR函数,而且在异步信号的处理事件中,编程与普通线程编程一样,没有额外限制。下面以串口服务为例:
1.编写事件处理函数。
Int_Register(cn_int_line_uart1); Int_IsrConnect(cn_int_line_uart1,__uart1_int); Int_SettoReal(cn_int_line_uart1); Int_ClearLine(cn_int_line_uart1); Int_RestoreRealLine (cn_int_line_uart1); |
2.登记事件类型,用于异步信号处理的事件,一般设为关联型事件。
uart1_evtt = DJY_EvttRegist(EN_CORRELATIVE, 100, 0, uart1_event, 0x1000, NULL); |
4. 按照下列指令序列初始化中断号:
Int_EvttConnect(cn_int_line_USART1,uart1_evtt); Int_SettoAsynSignal(cn_int_line_USART1); Int_SetClearType(cn_int_line_USART1,CN_INT_CLEAR_PRE); Int_ClearLine(cn_int_line_USART1); Int_RestoreAsynLine(cn_int_line_USART1); |
第一句,把事件类型与中断号联系起来。
第二句,把中断号设为异步信号。
第三句,设置清中断方式,不可以使用默认的CN_INT_CLEAR_USER。详见6.4.9节。
第四句,清一下中断,以免硬件的初始化状态影响。
第五句,使能中断。
完成以上3步,只要发生uart中断,就会自动弹出uart1_evtt类型的事件。第一次弹出事件后,uart1_event将被执行,处理完相应的事务后,将会在DJY_WaitEvttPop( )函数处阻塞,直到下一次中断的到来。应用程序完全不需要写ISR程序,由于uart1_event是普通的事件处理函数,它跟普通事件处理函数一样,可以得到所有OS服务。
要实现超时处理(即串口长时间收不到数据)也很简单,只需要把:
DJY_WaitEvttPop(djy_my_evtt_id(), NULL,CN_TIMEOUT_FOREVER);
中的CN_TIMEOUT_FOREVER换成具体的时间(微秒数)就可以了。
6.3.4 异步信号的ISR模式编程
这种编程模型,是大家非常熟悉的方式,不同的是,DJYOS的ISR可以调用全部系统服务,唯一的区别是,ISR中调用可能导致阻塞的系统服务时,不会发生实际阻塞。比如,在ISR中调用malloc,但如果内存不足,将返回NULL,不会被阻塞,而在线程中调用的话,将会被阻塞。只要养成良好的编程习惯,检查系统调用的返回值,就不会出错。
异步信号的ISR模式编程步骤如下:
1.编写中断服务程序ISR,像普通C函数一样编写即可。
u32 __uart1_int(ufast_t uart_int_line) { 中断服务代码 } |
2.使用下列语句序列设置中断:
Int_IsrConnect(cn_int_line_uart1,__uart1_int); Int_SettoAsynSignal(cn_int_line_uart1); Int_ClearLine(cn_int_line_uart1); Int_RestoreAsynLine(cn_int_line_uart1); |
第一句,把ISR函数跟中断线联系起来。
第二句,把相应中断设置成异步信号中断。
第三句,清中断,以免硬件的初始化状态影响。
第四句,使能中断。
这个过程,跟实时中断非常相似,就是函数名不一样而已,但实现过程是有很大差别的。这种方式下,__uart1_int函数可以调用全部系统服务,但中断响应延时可能比实时中断长。
6.3.5 异步信号的ISR和事件混合模式编程
这是一种类似把中断处理分为上半部和下半部编程的一种方法,在DJYOS中实现起来特别简单,步骤如下:
1.编写事件处理函数。
voiduart1_event(void) { While(1) { //do something DJY_WaitEvttPop(djy_my_evtt_id(), NULL,CN_TIMEOUT_FOREVER); } } |
2.登记事件类型,用于异步信号处理的事件,一般设为关联型事件。
uart1_evtt = DJY_EvttRegist(EN_CORRELATIVE, 100, 0, uart1_event, 0x1000, NULL); |
3.编写中断服务代码ISR,像普通C函数一样编写即可。
u32 __uart1_int(ufast_t uart_int_line) { 中断服务代码 } |
5. 按照下列指令序列初始化中断号:
Int_EvttConnect(cn_int_line_USART1,uart1_evtt); Int_IsrConnect(cn_int_line_uart1,__uart1_int); Int_SettoAsynSignal(cn_int_line_USART1); Int_ClearLine(cn_int_line_USART1); Int_RestoreAsynLine(cn_int_line_USART1); |
第一句,把事件类型与中断号联系起来。
第二句,把ISR函数与中断号联系起来。
第三句,把中断号设为异步信号。
第四句,清中断,以免硬件的初始化状态影响。
第五句,使能中断。
此后,只要发生uart中断,就会先调用__uart1_int函数,然后自动弹出uart1_evtt类型的事件。可以在__uart1_int( )函数中处理比较紧急的事务,比如copy硬件缓冲区,把其他事情留给uart1_event( )做。第一次弹出事件后,uart1_event将被执行,处理完相应的事务后,将会在DJY_WaitEvttPop ( )函数处阻塞,直到下一次事件的到来。
6.4 中断API
6.4.1 Int_RestoreAsynSignal:使能异步信号
void Int_RestoreAsynSignal(void)
头文件:
os.h
参数:
无。
返回值:
无。
说明:
使能异步信号,Int_ SaveAsynSignal配对使用。
这是一对计数式的开关,Int_ SaveAsynSignal函数使计数器加1,Int_RestoreAsynSignal函数使计数器减1,当计数器减到0时,异步信号被使能,否则禁止。
另外注意,异步信号被使能/禁止等同于“禁止/使能调度”。禁止异步信号期间,如果是tickless模式,定时时钟中断也停止响应;在非tickless模式,心跳时钟也是通过异步信号实现的,系统心跳停止。
在禁止异步信号期间,如果调用可能会引起事件阻塞的函数,则阻塞作用是无效的,该函数会立即返回。例如某事件调用malloc函数时,内存不足且禁止异步信号,函数会立即返回NULL。
这是函数与Int_SaveAsynSignal配对使用,允许嵌套调用。
6.4.2 Int_SaveAsynSignal:禁止异步信号
void Int_SaveAsynSignal(void)
头文件:
os.h
参数:
无。
返回值:
无。
说明:
禁止异步信号,详见Int_RestoreAsynSignal函数。
这是函数与Int_RestoreAsynSignal配对使用,允许嵌套调用。
6.4.3 Int_CheckAsynSignal:查询异步信号状态
bool_t Int_CheckAsynSignal(void)
头文件:
os.h
参数:
无。
返回值:
允许异步信号返回true;禁止异步信号返回false。
说明:
查询异步信号是否允许,也即是否允许调度。注意,这里所查询是逻辑概念上的异步信号,而非实际硬件中断源的状况。调用Int_HighAtomStart或Int_LowAtomStart关闭中断的话,不影响本函数。
6.4.4 Int_RestoreLine:计数使能中断线
bool_tInt_RestoreLine(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
true表示成功操作,false表示操作失败。
说明:
DJYOS用计数器来控制中断线开关状态,允许嵌套调用,调用Int_SaveLine使计数器加1,调用Int_RestoreLine使计数器减1。因此,调用Int_SaveLine必定会使相关中断线禁止,但调用Int_RestoreLine则只有在计数器减至0才使能中断。
函数与Int_SaveLine成对使用,允许嵌套调用。
6.4.5 Int_SaveLine:计数禁止中断线
bool_t Int_SaveLine(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
true表示成功操作,false表示操作失败。
说明:
详情见函数Int_RestoreLine说明。
函数与Int_RestoreLine成对使用,允许嵌套调用。
6.4.6 Int_EnableLine:直接使能中断线
bool_t Int_ EnableLine (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
成功操作返回true;失败则返回false。
说明:
函数与Int_ DisableLine成对使用,不允许嵌套调用。这对函数直接使能或者禁止相应中断线,执行效率较高,是不允许嵌套调用的。
6.4.7 Int_DisableLine:直接禁止异步信号线
bool_t Int_ DisableAsynLine (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
成功操作返回true;失败则返回false。
说明:
函数与Int_ EnableLine成对使用,不允许嵌套调用。这对函数直接使能或者禁止相应中断线,执行效率较高,是不允许嵌套调用的。
6.4.8 Int_CheckLine:检查中断线状态
bool_t Int_CheckLine (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line,中断线号。
返回值:
true表示中断线使能,false表示禁止。
说明:
检查中断线ufl_line是否使能,不区分异步信号和实时信号。
6.4.9 Int_SetClearType:设置清中断方式
bool_t Int_SetClearType (ufast_t ufl_line,ufast_t clear_type);
头文件:
os.h
参数:
ufl_line:中断线编号;
clear_type:设定清中断方式。
返回值:
设置成功返回true;失败则返回false。
说明:
中断响应后,绝大多数硬件都会暂时屏蔽本中断再次响应,直到调用Int_ClearLine清中断。在中断发生到清中断期间重复发生的同一个中断将被忽略,既不响应也不挂起。清中断后,再次发生的中断将会挂起,是立即响应还是等前一次中断返回后再响应,则取决于硬件以及软件设置。DJYOS提供三种清中断方式,任意中断线可根据自身需要设置:
#define CN_INT_CLEAR_PRE 0 //调用ISR之前由系统自动清,默认方式 #define CN_INT_CLEAR_USER 1 //系统不清,由用户在ISR中清 |
只有异步信号可以设置清中断方式,实时中断必须由用户的ISR清中断。
6.4.10 Int_IsrConnect:注册中断响应函数
void Int_IsrConnect(ufast_t ufl_line, u32 (*isr)( ptu32_t ))
头文件:
os.h
参数:
ufl_line:中断线号。
isr:被关联的中断处理函数。
返回值:
操作成功返回true;失败则返回false。
说明:
为中断线ufl_line指定(注册)中断响应函数。中断发生后,系统将调用该响应函数。本函数不区分该中断线是实时中断还是异步信号。
ISR函数的参数,与Int_SetIsrPara函数配合使用,常用于多个中断线共用ISR函数的情景,例如,CPU有8个串口,driver使用同一个ISR处理8个串口中断,每个串口有独立的中断,如果没有参数,ISR函数将不知道是哪个串口请求中断。如果不使用Int_SetIsrPara函数设置参数,默认值是中断号;像前面所述的8个串口共用ISR的情况,可把参数设为串口号。
Int_Register(IntLine); Int_SetClearType(IntLine,CN_INT_CLEAR_AUTO); Int_IsrConnect(IntLine,UART_ISR); Int_SettoAsynSignal(IntLine); Int_ClearLine(IntLine); Int_RestoreAsynLine(IntLine); Int_SetIsrPara(IntLine,SerialNo); |
6.4.11 Int_IsrDisConnect:注销中断响应函数
voidInt_IsrDisConnect(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
无。
说明:
注销中断线ufl_line的中断响应函数。
6.4.12 Int_ SetIsrPara:设置ISR参数
void Int_SetIsrPara(ufast_t ufl_line,ptu32_t para);
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
无。
说明:
设置当编号为ufl_line的中断发生时,调用ISR函数的参数,参见6.4.10节。
6.4.13 Int_EvttConnect:注册异步事件
bool_t Int_EvttConnect(ufast_t ufl_line,uint16_t my_evtt_id)
头文件:
os.h
参数:
ufl_line:中断线号;
my_evtt_id:被关联的事件类型id。
返回值:
true = 成功连接事件类型 , false = 失败。
说明:
将中断线信号转化为系统的异步事件。即设置中断线触发的事件类型。当中断线信号发生后,系统首先调用中断响应函数(如果有)处理中断,然后弹出my_evtt_id的事件类型。同时,系统会将中断响应函数结果(u32类型)和中断线号(u32类型)作为输入参数传递给事件处理函数。
使用本函数,灵活使用DJY_WaitEvttPop函数的base _times参数,可以设定每次中断都激活事件处理函数,还是若干次中断才激活一次,详询第5.7.7节
警告:只有异步信号才能关联事件类型。
6.4.14 Int_EvttDisConnect:注销异步事件
void Int_EvttDisConnect (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线编号。
返回值:
无。
说明:
取消中断线与事件类型的关联关系。
6.4.14 Int_EvttDisConnect:注销异步事件
u32 Int_GetRunLevel(void)
头文件:
os.h
参数:
无。
返回值:
异步信号嵌套深度。
说明:
查询当前异步信号的嵌套深度。0 = 线程中,1=异步信号中断,大于1表示异步信号嵌套级数。
6.4.16 Int_AsynSignalSync:异步信号同步
bool_t Int_AsynSignalSync (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:被同步的中断线号,必须是异步信号类型。
返回值:
设置成功返回true;失败则返回false。
说明:
调用后,将阻塞正在处理的事件的线程,直到指定的中断线的中断发生、响应并返回,然后才激活线程。不管中断线原来状态如何,调用本函数将导致中断线被使能(是直接使能,不是调用Int_RestoreAsynLine),并在线程被激活后恢复禁止状态。
只有异步信号才允许被同步,实时中断是不允许的。
调用前,应该确保相应中断线处于禁止状态。但可以连接ISR、evtt等。
如果连接了ISR且清中断类型设为CN_INT_CLEAR_USER的话,ISR中必须清中断。
如果调用本函数时,被等待的中断已经挂起,则:
1、 函数立即作为同步成功返回,不会被阻塞。
2、 该中断的ISR(用Int_IsrConnect设置)不会被调用。
3、 如果用Int_EvttConnect函数连接了事件类型,该事件类型不会被弹出。
4、 因此,不建议在被异步信号同步的中断线上挂接ISR和连接事件类型。
如果连接了ISR并被调用,则被阻塞事件从阻塞恢复运行后,g_ptEventRunning-> event_result将设置为ISR函数的返回值。
6.4.17 Int_Register:注册中断线
bool_t Int_Register (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:被注册的中断线号,必须小于或等于最大中断线号。
返回值:
注册成功或已经注册返回true;失败则返回false。
说明:
中断注册函数必须在优先于其他具体中断线操作函数之前被调用。
6.4.18 Int_UnRegister:注销中断线
bool_t Int_UnRegister (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:被注销的中断线号,必须小于或等于最大线号。
返回值:
注销成功返回true;失败则返回false。
说明:
原则上,中断函数一经注册,便不再注销,此外与Int_Register函数成对出现,在功能上面主要用于释放注册中断线分配的内存。
6.5 硬件相关API
以下API函数跟硬件相关,代码在BSP包中的int_hard.c中提供,平台移植时,必须实现,具体实现的细节硬件的中断控制器有关。
有些arch,有统一的中断架构,像cortex-m系列的cpu,系统已经提供了从m0~m7所有的int_hard.c,可直接使用。
有些arch,则是cpu核只提供了基本的中断功能,中断控制器是由个厂家提供的,例如ARM9,则需要在平台移植时,根据具体CPU和中断控制器芯片实现。
6.5.1 Int_SetPrio:设置中断线优先级
bool_t Int_SetPrio(ufast_t ufl_line,u32 prio)
头文件:
os.h
参数:
ufl_line:被操作的中断线编号;
prior:新的优先级。
返回值:
设置成功返回true;失败则返回false。
说明:
设定某中断线的抢占优先级。
不同的硬件,优先级的含义也不同,有些中断控制器的优先级是抢占式的,即高优先级的中断能嵌套低优先级中断,像cortex-m3;另一些中断控制器的优先级是查询式的,两个中断一起发生时,先服务高优先级的,例如s3c2440。如果硬件不支持抢占优先级,BSP设计时直接返回false即可。
6.5.2 Int_SettoAsynSignal:设置为异步信号
bool_t Int_SettoAsynSignal(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回:
设置成功返回true;失败则返回false。
说明:
将中断线ufl_line设置为异步信号。如果此中断线正在响应,则设置在系统处理完此次中断线后才会生效。
6.5.3 Int_SettoReal:设置为实时信号
bool_t Int_SettoReal (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回:
设置成功返回true;失败则返回false。
说明:
将中断线ufl_line设置为实时信号。如果此中断线正在响应,则设置在系统处理完此次中断线后才会生效。
6.5.4 Int_ContactAsynSignal:使能异步信号
void Int_ContactAsynSignal(void)
头文件:
int_hard.h
参数:无。
返回:无。
说明:
函数与Int_CutAsynSignal配对使用,直接允许异步信号,不能嵌套使用。
6.5.5 Int_CutAsynSignal:禁止异步信号
void Int_CutAsynSignal(void)
头文件:
int_hard.h
参数:无。
返回:无。
说明:
禁止所有异步信号类型的中断线。
函数与Int_ContactAsynSignal配对使用,直接禁止异步信号,不能嵌套使用。
6.5.6 Int_ContactTrunk:使能总中断
void Int_ContactTrunk(void)
头文件:
int_hard.h
参数:无。
返回:无。
说明:
本函数与Int_CutTrunk配对使用,直接使能总中断,不能嵌套使用。
6.5.7 Int_CutTrunk:禁止总中断
void Int_CutTrunk(void)
头文件:
int_hard.h
参数:无。
返回:无。
说明:
禁止系统的中断功能。
函数与Int_ContactTrunk配对使用。
本函数与Int_ContactTrunk配对使用,直接禁止总中断,不能嵌套使用。
6.5.8 Int_ContactLine:使能中断线
bool_t Int_ContactLine(ufast_t ufl_line)
头文件:
int_hard.h
参数:
ufl_line:中断线号。
返回:无。
说明:
本函数与Int_CutLine配对使用,直接使能选定中断线,不能嵌套使用。
6.5.9 Int_CutLine:禁止中断线
bool_t Int_CutLine(ufast_t ufl_line)
头文件:
int_hard.h
参数:
ufl_line:中断线号。
返回:无。
说明:
禁止中断线。
本函数与Int_ContactLine配对使用,直接禁止选定中断线,不能嵌套使用。
6.5.10 Int_ClearLine:清中断线
bool_tInt_ClearLine (ufast_t ufl_line);
头文件:
os.h
参数:
ufl_line,被清的中断线编号。
返回:
操作成功返回true;失败则返回false。
说明:
清中断线,中断pend后,须执行“清中断线”操作,才能响应后续中断。至于在什么时候执行清中断操作,并无一定的规律,视硬件功能逻辑而定。
中断引擎能够自动调用nt_ClearLine清中断,调用Int_SetClearType函数可以逐个中断设定其清中断方法,设为:
CN_INT_CLEAR_USER:表示系统不清中断,用户须在ISR中清中断,是默认状态。
CN_INT_CLEAR_PRE:表示由系统在调用ISR之前清中断。
6.5.11 Int_QueryLine:查询中断线状态
bool_t Int_QueryLine(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
中断线信号已发生返回true;否则返回false。
说明:
查询中断线信号是否已经发生,本函数用于查询式中断响应。
6.5.12 Int_TapLine:软中断
bool_t Int_TapLine(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
成功软件触发中断返回true;否则返回false。
说明:
用软件的方法触发一次中断,就像该中断真实发生了一样。这个功能依赖于CPU,绝大多数CPU的中断控制器允许软件触发,但也有些CPU不允许,例如freescale的p1020。
6.5.13 Int_EnableNest:使能中断线嵌套
bool_t Int_EnableNest(ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
使能成功返回true;失败则返回false。
说明:
使能中断线被嵌套功能。与Int_DisableNest配对使用。
使能后,相应的中断的ISR执行期间,可能会被别的中断线抢占。但是否支持嵌套功能,由BSP设计者根据硬件的支持情况和实际工程需要决定。
在使能嵌套的情况下,嵌套方式也可能有多种,例如三星的S3C2440,只要嵌套被允许,则低优先级的中断也可以嵌套高优先级的中断,而freescale的P1020,则只有更高优先级的才可以嵌套低优先级的。
这里所说的嵌套,可以是实时中断嵌套实时中断,也可以是异步信号嵌套异步信号,或者是实时中断嵌套异步信号,这些功能由BSP设计者保证。
6.5.14 Int_DisableNest:禁止中断嵌套
bool_t Int_DisableNest (ufast_t ufl_line)
头文件:
os.h
参数:
ufl_line:中断线号。
返回值:
使能成功返回true;失败则返回false。
说明:
禁止中断线嵌套功能。
与Int_EnableNest配对使用。使能后,相应的中断服务期间,可以会被别的中断线抢占。但是否支持嵌套功能,由BSP设计者根据硬件的支持情况和实际工程需要决定。注意,实时中断天生能够打断异步信号,这是无法禁止的。
在使能嵌套的情况下,嵌套方式也可能有多种,例如三星的S3C2440,只要嵌套被允许,则低优先级的中断也可以嵌套高优先级的中断,而freescale的P1020,则只有更高优先级的才可以嵌套低优先级的。
这里所说的嵌套,可以是实时中断嵌套实时中断,也可以是异步信号嵌套异步信号,或者是实时中断嵌套异步信号,这些功能由BSP设计者保证。
6.6 原子操作API
6.6.1 Int_HighAtomStart/Int_HighAtomEnd高级原子操作
进入高级原子操作:
atom_high_t Int_HighAtomStart(void)
离开原子操作:
void Int_HighAtomEnd(atom_high_t high)
头文件:
os.h
参数:
high:总中断的开关状态,是调用Int_HighAtomStart函数的返回值。
返回值:
Int_HighAtomStart函数的返回值表示总中断的开关状态。
说明:
高级原子操作是指期间不容许任何原因打断的操作,一般是因为特定的硬件要求,比如说用gpio产生一个1uS宽度的脉冲。从进入高级原子操作到离开的期间,所有中断被禁止。Int_HighAtomStart函数读当前总中断状态,然后禁止总中断。
Int_HighAtomStart和Int_HighAtomEnd是姊妹函数,必须配套使用。
原子操作的用法:
源码6-1使用高级原子操作
void HighAtomExample(void) { atom_high_t high_atom; high_atom =Int_HighAtomStart(); //硬件要求,必须在关闭总中断条件下操作CTPR寄存器 *(u32*)(cn_core_ctpr_addr) = cn_prior_core_asyn_disable; __asm_disable_tick_int(); Int_HighAtomEnd(high_atom); return; } |
上述代码是典型的受硬件约束而需要使用高级原子操作的。
原子操作函数还可以嵌套使用,方法如下:
源码6-2嵌套使用原子操作
atom_high_thigh_atom1, high_atom2; high_atom1 = Int_HighAtomStart(); do something 1; high_atom2 = Int_HighAtomStart(); do something 2; Int_HighAtomEnd(high_atom2); Int_HighAtomEnd(high_atom1); |
在原子操作包包围的代码内,不允许调用“Int_SaveAsynSignal——Int_RestoreAsynSignal”,也不允许调用任何可能导致阻塞的API。禁止总中断后,所有中断无法响应,调度也被禁止,因此禁止时间应该尽可能地短,最好不要在禁止期间调用任何API。
警告:原子操作是系统提供的快速高效手段,但却很暴力,通过原子操作(Int_HighAtomStart或者Int_LowAtomStart)进入禁止中断的状态,是不可以发生调度的,然而调度器却不知道,调用DJY_QuerySch将返回“允许”状态,你在这种时候如果调用可能引起阻塞的操作,是自讨苦吃,结果将不可预知。
1. 此函数实现与硬件环境相关,在系统移植时确定。
2. 在原子操作过程中,不要调用会引发事件阻塞的操作(函数),一旦发生阻塞,其结果将会不可预知。
6.6.2 Int_LowAtomStart/Int_LowAtomEnd:低级原子操作
低级原子操作进入
atom_low_t Int_LowAtomStart(void)
低级原子操作离开
void Int_LowAtomEnd(atom_low_t low)
头文件:
os.h
参数:
low:异步信号的开关状态,是调用Int_LowAtomStart函数的返回值。
返回值:
Int_LowAtomStart函数的返回值表示异步信号的开关状态。
说明:
函数使用方法参见高级原子操作部分,不再赘述。
低级原子操作跟高级原子操作类似,高级原子操作控制的是总中断开关,低级原子操作控制的是异步信号总开关,其他完全一样,使用方法也一样。
DJYOS中tick是通过异步信号实现的,低级原子操作期间tick自然也不走了。
6.6.3 原子加减法计算
原子加减法计算函数有这些,原型如下:
void atom_uadd32(u32 *base, u32 inc);
void atom_usub32(u32 *base, u32 sub);
void atom_sadd32(s32 *base, s32 inc);
void atom_ssub32(s32 *base, s32 sub);
void atom_uadd64(u64 *base, u64 inc);
void atom_usub64(u64 *base, u64 sub);
void atom_sadd64(s64 *base, s64 inc);
void atom_ssub64(s64 *base, s64 sub);
头文件:
os.h
参数:
base:被加数/被减数的地址。
inc/sub:加数或者减数
返回值:
无。
第7章 紧急代码
所谓紧急代码说起来也比较简单,就是在系统预加载之后执行的一部分代码,这段代码可以完成一些紧急资源的初始化,例如启动过程的喂狗,一些紧急的gpio状态等。
想当年,MCU普遍比较简单,内存容量不大,程序也比较短小,上电/复位后的初始化时间不长。这段时间内,即使系统对外界完全不响应,也关系不大,但是在一些实时控制的领域,比如要求系统上电后要求继电器是闭合或者是打开的状态,这个时候紧急代码就显得格外关键。
紧急代码如何编写想必是大家比较关心的,说起来也比较简单,就是将紧急代码写在critical/critical.c文件中,“void critical(void)”为紧急代码入口函数,这里代码要求用裸机的方式写,一些系统资源是不能用的。
第8章 内存管理
应用程序使用的内存,有两种来源。一种是静态分配,用户编程时定义全局或静态变量,它们由编译器在编译时确定和分配。另一种是动态分配,即程序在运行过程中申请内存,由系统的内存管理机制来确定和分配。前后两种的本质区别是应用程序获得内存的时刻前者是编译时,后者是运行时。
对于第二种来源,其核心内容是DJYOS的内存管理机制。DJYOS有3种动态内存分配策略,分别是:
1.“准静态”分配,这种分配方式虽然是系统运行时分配,但执行效率跟静态分配类似,比编译器静态分配多了对齐损耗而已。
2.“块相联”分配,它把整个内存划分为尺寸的块,一次申请可以分配连续的2的n次方块。
3.“固定块”分配,它把大块内存划分由规格化的小块组成的“内存池“,每次申请分配一个规划化的小块。
“准静态”和“块相联”目前采用统一的用户接口,对应用程序或用户而言,这两种分配策略是透明的。那么,什么时候按照准静态方式,什么时候按照块相联方式分配内存呢?
图 8‑1DIDE中配置动态分配
准静态分配是系统默认支持的,不需要配置,在系统初始化阶段、多事件(线程)调度开始之前,malloc函数肯定是按照准静态分配模式工作的。
如果勾选了“全功能动态分配”选项(如图 8‑1),则多事件(线程)调度启动后,采用
图8-1显示了静态分配和DJYOS的“准静态”和“块相联”分配策略的不同。采用这种不同内存分配策略,申请两块101字节大小的内存,在内存分配上的异同点。其中在“准静态”分配方式中,假设系统要求内存按8字节对齐。
图 8‑2静态分配、准静态分配、块相联分配的效果
8.1 “准静态”分配
“准静态”分配是在系统初始化时,从堆中分配任意尺寸的内存。由于有些模块,在编译时无法确定要多少内存,需要模块运行初始化时才能确定。类似这种模块,虽然它自申请内存直至运行结束都不会释放内存,但让其使用静态定义的方式分配内存仍然是不合理的。相对而言,这种模块,采用准静态分配策略,既高效又快速。“准静态”分配法,从当前堆底开始切割,要多少内存就切多少,不浪费,无需查找空闲内存,执行速度很快,但释放时,如果地址“更高”的内存块没有被释放,则低地址的内存块即使被释放了,也不会被利用。
图 8‑3 准静态分配和释放示意图
如图 8‑3所示,P2虽然被释放了,但由于比它更高地址的P3还没有释放,所以堆底仍然在P3之上,再次执行malloc的话,还是从堆底向上分割,P2所在“空隙”不会得到利用。一旦P3被释放,堆底就立即降到P1之上。
8.2 “块相联”分配
“块相联”分配是将整个内存划分为若干固定大小的page,由2个n次方个page相联组成block,分配是以block为单位进行的。详细的内存分配机制详见《都江堰操作系统原理与实现》中内存管理章节介绍。虽然这样做会使内存的使用率低一些,但是这个分配策略的执行速度很快,而且最重要的是,它的执行时间是确定的,比如一个总共4GB,以1KB为一页的内存系统中,查找1个空闲页,在32位cpu中,最多需要5次搜索(共5次比较、5次乘法和5次加法)就可以完成。
“块相联”分配策略的对内存页的格式“损耗”虽然是不确定的,不过,悄悄告诉你一个秘密,这些所谓的“损耗”,其实就像一个呆板的银行,只能按整百取钱,你要取150,给你取200,多取的50块钱你也是可以花的,而且,Heap_CheckSize函数还能告诉你总共给了你多少钱。
8.2.1 多heap管理
djyos将不同性质的内存,分别组成不同的heap,在同一个heap内,地址不相连的内存,则可以组成多个cessions。在嵌入式系统中,你会同时面对各种不同性质的内存,例如一个双核系统,会有DDR内存、noncache的用于驱动的内存、双核共享内存、双核独享内存,分别为这些内存创建heap进行独立管理,应用程序就能够按照需要从指定的heap中分配内存。
heap划分为多个cessions,除了管理不连续的地址空间外,还可以用来提高内存利用率。举个例子:如果一个heap,只有一个cessions,其page尺寸是32bytes,那么,我要分配180字节的话,就只能向上取256字节。如果我把这个heap划分为两个cessions,两个cessions的page尺寸分别是24和32bytes,那么,内存分配程序就会计算,从32bytes的cessions中分配,就要分配256字节,从24bytes的cessions中分配,就只需要分配192字节。很明显,从page size=24字节的cessions中分配,划算得多。
djyos利用lds文件,把所有未被静态使用的内存,都划入heap,在lds文件中,你会看到图 8‑4这样一段代码,lds文件的语法,这里就不解析了,解析两个概念:
系统heap:即命名为“sys”的heap,在lds文件中必须排首位,也是malloc等没有指定heap的函数的默认heap。
“通用堆”和“专用堆”:当你调用Heap_MallocHeap函数分配内存时,是要指定从哪个heap中分配的。如果该指定的heap是专用堆,则只能从该heap中分配,分配不成功则返回NULL或者阻塞;如果是通用堆,则如果从该heap分配不成功的话,还会从别的heap分配。
cessions:可以看到,sys heap被分为两个cessions,因为他们都是片内ram,是同一性质的内存,其page size有意设置为24和32两个不同的值,可以提高内存利用率。
heap_cp_table : { . = ALIGN(0x08); pHeapList = .;
/* 默认堆,必须排首位,名字必须是“sys” */ LONG(2) /*表示该堆由两段内存组成*/ LONG(0) /*该堆上分配的内存的对齐尺寸,0表示使用系统对齐*/ LONG(0) /*0=通用堆,1=专用堆,如果系统只有唯一一个堆,则只能是通用堆*/ LONG(sys_heap_bottom) /*第一段基址,须符合对齐要求*/ LONG(sys_heap_top) /*第一段地址上限(不含),须符合对齐要求*/ LONG(32) /*第一段页尺寸=32bytes*/ LONG(msp_bottom) /*第二段基址,回收初始化栈,须符合对齐要求*/ LONG(msp_top - 384) /*第二段地址上限(不含),384是留给中断使用的,须符合对齐要求*/ LONG(24) /*第二段页尺寸=24bytes*/ BYTE(0x73) /*'s'*/ BYTE(0x79) /*'y'*/ BYTE(0x73) /*'s'*/ BYTE(0) /*串结束,"sys"是堆的名字*/
. = ALIGN(0x04); LONG(1) /*表示该堆由1段内存组成*/ LONG(0) /*该堆上分配的内存的对齐尺寸,0表示使用系统对齐*/ LONG(0) /*0=通用堆,1=专用堆,如果系统只有唯一一个堆,则只能是通用堆*/ LONG(extram_heap_bottom) /*第一段基址,须符合对齐要求*/ LONG(extram_heap_top) /*第一段地址上限(不含),须符合对齐要求*/ LONG(128) /*第一段页尺寸=128bytes*/ BYTE(0x65) /*'e'*/ BYTE(0x78) /*'x'*/ BYTE(0x74) /*'t'*/ BYTE(0x72) /*'r'*/ BYTE(0x61) /*'a'*/ BYTE(0x6d) /*'m'*/ BYTE(0) /*串结束,"extram"是堆的名字*/ . = ALIGN(0x04); LONG(0) /*结束标志*/ }>ROM1
|
图 8‑4 lds中的heap描述代码
8.2.1 分配策略
带Heap后缀的分配函数:这些函数都有一个参数struct HeapCB *Heap,用于指定从哪个heap中分配内存。如果该heap是“通用堆”,则优先从该heap中分配,如果该heap内存不足,无法分配,就会转而从别的heap中分配;如果该heap是“专用堆”,则只能该heap分配。
不带Heap后缀的分配函数:相当于参数Heap是sys heap的带Heap后缀的函数。
包含多个cessions的堆:分配函数会自动查找从哪个cessions中分配时开销最小,只要该cessions仍然有内存,就从该cessions中分配。
8.3 “固定块”分配
“固定块”分配策略,是一种内存池分配技术。可以形象地描述为“批发”与“零售”的关系。不同于一般系统的固定块分配法,系统只允许“一次批发,卖完即止”。DJYOS允许卖完后,再次批发,甚至允许开店时(内存池初始化时)不进货,等有人买时再批发。零风险的买卖,值得做吧。
需要在DIDE中勾选并配置“固定块分配”模块才能使用,如下图:
图 8‑5 DIDE中配置固定块分配功能
8.4 “准静态”和“块相联”相关API
如上所述“准静态”和“块相联”内存管理策略均使用统一的用户接口。
8.4.1 malloc和Heap_Malloc:分配内存
#define malloc(x) Heap_Malloc(x,CN_TIMEOUT_FOREVER)
void * (*Heap_Malloc)(ptu32_t size,u32 timeout);
头文件:
stdlib.h
参数:
size:申请内存块的大小,以字节计。
timeout:等待时间。申请内存(调用函数)时,内存不足,事件(调用者)将被阻塞并等待其他事件释放内存。如果直至timeout时间之后,内存仍不足则申请失败。在非tickless模式,等待时间系统自动向上取整为整数个ticks。
返回值:
申请成功返回内存首指针;失败则返回NULL。
说明:
从堆中分配size大小的全局内存空间。由于DJYOS系统使用块相联分配算法,size将被向上取整为一个规格化大小的块,规格化方法参见《都江堰操作系统原理与实现》中内存管理章节介绍。如果系统没有满足条件的内存块,且timeout不等于0,则阻塞线程,直到timeout时间到或者有其他线程释放内存致使有合适的内存块。
由于规格化,所获得的内存很可能超过size参数,调用Heap_CheckSize函数可获取实际申请到的内存的大小。
malloc是C标准规定的函数,用宏实现。
Heap_Malloc是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.2 realloc和Heap_Realloc:拓展内存
#define realloc(p,size) Heap_Realloc(p,size,CN_TIMEOUT_FOREVER)
void *(*Heap_Realloc) (void *, ptu32_t NewSize);
头文件:
stdlib.h
参数:
p:原内存首地址。
NewSize:申请内存块的新尺寸,以字节计。
Timeout:等待时间。申请内存(调用函数)时,内存不足,事件(调用者)将被阻塞并等待其他事件释放内存。如果直至Timeout时间之后,内存仍不足则申请失败。等待时间系统自动向上取整为整数个ticks。
返回值:
拓展成功返回内存首指针;失败则返回NULL。
说明:
先判断当前内存p大小是否已经满足新设定的尺寸要求。如果满足,直接返回原内存地址。如果不满足,则先按照NewSize指定的大小分配空间,将原有内存中的数据拷贝到新分配的空间,然后返回新申请内存首地址。
realloc是C标准规定的函数,用宏实现。
M_ Realloc是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.3 free和Heap_Free:释放内存
#define free(x) Heap_Free(x)
v void (*Heap_Free)(void * pl_mem);
头文件:
stdlib.h
参数:
p:内存首地址。
返回值:
无。
说明:
释放申请的内存。
警告:在“准静态”内存管理策略中,必须等地址比被释放的内存更高的所有准静态分配的内存都释放了,新释放的内存空间才能重新得到利用。
M_ Free是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.4 Heap_FreeHeap:释放指定堆内存
void (*Heap_FreeHeap)(void * pl_mem,pHeap_t Heap);
头文件:
stdlib.h
参数:
pl_mem:内存首地址。
Heap:堆句柄(控制块)。
返回值:
申请成功返回内存首指针;失败则返回NULL。
说明:
释放从指定的Heap句柄(控制块)指定的堆空间中申请的内存空间。
警告:在“准静态”内存管理策略中,必须等地址比被释放的内存更高的所有准静态分配的内存都释放了,新释放的内存空间才能重新得到利用。
M_ FreeHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.5 Heap_MallocHeap:从指定heap申请内存
void *(*Heap_MallocHeap)(ptu32_t size,pHeap_t Heap,u32 timeout);
头文件:
stdlib.h
参数:
size:申请内存大小。
Heap:堆句柄(控制块)。
timeout:等待时间。申请内存(调用函数)时,内存不足,事件(调用者)将被阻塞并等待其他事件释放内存。如果直至Timeout时间之后,内存仍不足则申请失败。在非tickless模式,等待时间系统自动向上取整为整数个ticks。
返回值:
申请成功返回内存首指针;失败则返回NULL。
说明:
优先从指定的Heap句柄(控制块)指定的堆空间中申请size大小的内存。如果Heap的属性是“专用”,则只能从该heap中申请内存;如果是“通用”,则优先从该heap申请,如果申请不到,则从其他heap中申请。
M_ MallocHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.6 Heap_MallocLc:申请局部内存
void *(*Heap_MallocLc)(ptu32_t size,u32 timeout);
头文件:
stdlib.h
参数:
size:申请内存大小。
timeout:等待时间。申请内存(调用函数)时,内存不足,事件(调用者)将被阻塞并等待其他事件释放内存。如果直至Timeout时间之后,内存仍不足则申请失败。在非tickless模式,等待时间系统自动向上取整为整数个ticks。
返回值:
申请成功返回内存首指针;失败则返回NULL。
说明:
申请局部内存。所谓局部内存,是指所申请的这种内存的生命周期与事件处理的生命周期是同步的,当事件消亡的时候,仍然没有释放的内存,将被强制释放避免内存泄漏。然而,强制释放是很消耗时间的,实时软件的设计者在内存使用完毕后,应该及时调用Heap_Free函数释放内存,必须避免强制释放这种事情发生。欲在事件生存期外继续使用分配的内存,请调用malloc或Heap_Malloc函数分配全局内存块。
M_ MallocLc是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.7 Heap_MallocLcHeap:从指定heap申请局部内存
void *(*Heap_MallocLcHeap)(ptu32_t size,pHeap_t Heap, u32 timeout);
头文件:
stdlib.h
参数:
size:申请内存大小。
Heap:堆句柄(控制块)。
Timeout:等待时间。申请内存(调用函数)时,内存不足,事件(调用者)将被阻塞并等待其他事件释放内存。如果直至Timeout时间之后,内存仍不足则申请失败。在非tickless模式,等待时间系统自动向上取整为整数个ticks。
返回值:
申请成功返回内存首指针;失败则返回NULL。
说明:
优先从指定的Heap句柄(控制块)指定的堆空间中申请size大小的局部内存。如果Heap的属性是“专用”,则只能从该heap中申请内存;如果是“通用”,则优先从该heap申请,如果申请不到,则从其他heap中申请。
局部内存含义参见Heap_MallocLc函数。
M_ MallocLcHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.8 Heap_FormatSize:换算“规格尺寸”
ptu32_t (*Heap_FormatSize)(ptu32_t size);
头文件:
stdlib.h
参数:
size:内存空间尺寸。
返回值:
规格尺寸。
说明:
将内存空间尺寸换算成系统heap的“规格尺寸”。所谓“规格尺寸”是指,如果你请求分配size尺寸的内存,系统实际分配给你的内存尺寸。
M_ FormatSize是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.9 Heap_FormatSizeHeap:换算指定堆“规格尺寸”
ptu32_t (*Heap_FormatSizeHeap)(ptu32_t size,pHeap_t Heap);
头文件:
stdlib.h
参数:
size:内存空间尺寸。
Heap:堆句柄(控制块)。
返回值:
规格尺寸。
说明:
将内存空间尺寸换算指定heap的“规格尺寸”。所谓“规格尺寸”是指,如果你请求分配size尺寸的内存,系统实际分配给你的内存尺寸。
M_ FormatSizeHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.10 Heap_GetMaxFreeBlock:查询最大块
ptu32_t (*Heap_GetMaxFreeBlockHeap)(pHeap_t Heap);
头文件:
stdlib.h
参数:
无。
返回值:
最大块尺寸(字节数)。
说明:
查询系统heap最大内存块尺寸。
M_ GetMaxFreeBlockHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.11 Heap_GetMaxFreeBlockHeap:查询指定堆最大块
ptu32_t (*M_ GetMaxFreeBlockHeap)(pHeap_t Heap);
头文件:
stdlib.h
参数:
Heap:堆句柄(控制块)。
返回值:
最大块尺寸(字节数)。
说明:
查询指定heap中最大内存块尺寸。
M_ GetMaxFreeBlockHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.12 Heap_GetHeapSize:查询指定堆尺寸
ptu32_t Heap_GetHeapSize (pHeap_t Heap);
头文件:
stdlib.h
参数:
无
返回值:
系统heap尺寸(字节数)。
说明:
如果系统heap由多个cessions组成,则累加起来。
8.4.13 Heap_GetHeapSizeHeap:查询指定堆尺寸
ptu32_t Heap_GetHeapSizeHeap(pHeap_t Heap);
头文件:
stdlib.h
参数:
Heap:堆句柄(控制块)。
返回值:
heap尺寸(字节数)。
说明:
如果指定heap由多个cessions组成,则累加起来。
8.4.14 Heap_FindHeap:查找heap
pHeap_t Heap_FindHeap(const char *HeapName);
头文件:
stdlib.h
参数:
HeapName:heap的名字。
返回值:
heap控制块指针,如果没找到则返回NULL。
说明:
系统中每个heap都是有名字的,可以凭名字找到heap,shell的“heap”命令可以列出所有的heap及名字。
8.4.15 Heap_GetFreeMem:查询sys heap内存总量
ptu32_t (*Heap_GetFreeMem)(void);
头文件:
stdlib.h
参数:
无。
返回值:
系统heap内存总量(字节数)。
说明:
如果sys heap由多个cessions组成,则累加起来。
M_ GetFreeMem是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.16 Heap_GetFreeMemHeap:查询指定heap空闲内存量
ptu32_t (*Heap_GetFreeMemHeap)(pHeap_t Heap);
头文件:
stdlib.h
参数:
Heap:堆句柄(控制块)。
返回值:
可用内存总量(字节数)。
说明:
查询指定堆空间的可用内存总量(字节数)。
Heap_GetFreeMemHeap是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.4.17 Heap_CheckSize:查询内存块尺寸
#define malloc_usable_size(ptr) Heap_CheckSize(ptr)
ptu32_t (*Heap_CheckSize)(void * mp);
头文件:
stdlib.h
参数:
mp:内存首地址。
返回值:
分配给mp指针的实际内存大小(字节数)。
说明:
查询所申请的内存mp的实际大小。由于内存分配函数要对size参数做规格化调整,即将用户申请的内存换算大于且最接近size的“规格化”大小,再分配给用户,malloc函数实际获得的内存块可能比申请量大许多,本函数查询获得的内存块的实际可用尺寸。
malloc_usable_size是newlibc库要求的函数。
M_ CheckSize是个指针,在准静态分配阶段和块相联分配阶段,分别赋值为不同函数。
8.5 “固定块”相关API
8.5.1 Mb_CreatePool:创建内存池
struct MemCellPool *Mb_CreatePool(void *pool_original,u32 capacital,
u32 cell_size,u32 increment,
u32 limit,const char *name);
头文件:
os.h
参数:
pool_original:最初由用户提供的内存空间,不提供设为NULL。
capacital:原始内存池的尺寸,以块为单位,如果pool_original == NULL,则为0。
cell_size:内存块尺寸,若系统有对齐要求,必须为指针长度的整数倍,且最小为两倍指针长度。
increment:当本参数不为零时,所创建的内存池被一但被耗光,会从堆中补充获取increment个的内存块。直至内存池再被消耗完,再次获取。注意,内存池扩大后,即使用户调用mb_free释放了内存,但除非释放了内存池中的全部内存,否则新增的内存不会被收回。
limit:如果increment !=0,limit限制内存池的最大块数,以防其无限制地增加,导致内存耗尽。
name:设置内存池的名称,表示名称的字符串不能是局部变量,无名称则设置为NULL。
返回值:
指向所创建的内存池的指针。
说明:
创建一个内存池,pool_original一般是定义一个大数组,这个数组的大小是capacital乘以cell_size。如果系统有对齐要求,则起始地址至少要按照指针类型对齐,cell_size也要大于等于2倍指针长度。不要用char pool_original[n* cell_size]的方式定义内存,在要求对齐访问的系统中,创建内存池很可能失败,即使不失败,也存在内存访问对齐错误的可能。正确的方法是:struct tar pool_original[n];cell_size = sizeof(struct tar)。
可创建的内存池数量,受图 8‑5限制。
8.5.2 Mb_CreatePool_s:创建静态内存池
struct MemCellPool *Mb_CreatePool_s(struct MemCellPool *pool,
void *pool_original,u32 capacital,
u32 cell_size,u32 increment,
u32 limit,const char *name);
头文件:
os.h
参数:
pool:内存池指针(控制块);
其他参数:参见8.5.1节。
返回值:
指向所创建的内存池的指针。
说明:
与mb_create_pool不同的是,内存池控制块pool由调用者提供,其他与mb_create_pool完全一致。高可靠性的应用中,不应该使用动态分配的方式,静态定义更可靠,然后把指针传递过来。内核中使用的内存池,都是使用静态定义的。
本函数创建的内存池数量,不受图 8‑5限制。
8.5.3 Mb_DeletePool:删除内存池
bool_t Mb_DeletePool(struct mem_cell_pool *pool)
头文件:
os.h
参数:
pool:内存池指针(空间块)。
返回值:
删除成功返回true;失败则为false。
说明:
删除一个内存池,当某内存池不再需要时,可调用本函数。本函数只清理了内存池的信号量和资源结点,内存池缓冲区是调用者提供的,理应由调用者清理。对于一个可扩容的内存池,如果发生了扩容行为,新增加的内存也将随之释放回堆中。
8.5.4 Mb_DeletePool_s:删除静态内存池
bool_t Mb_DeletePool_s(struct mem_cell_pool *pool)
头文件:
os.h
参数:
pool:内存池指针(控制块)。
返回值:
删除成功返回true;失败则为false。
说明:
本函数与Mb_CreatePool_s对应,功能与Mb_DeletePool一致。
8.5.5 Mb_Malloc:申请内存
void *Mb_Malloc(struct mem_cell_pool *pool,uint32_t timeout);
头文件:
os.h
参数:
pool:内存池指针。
timeout:参见第5.7.1节。
返回值:
申请成功返回内存地址;否则返回NULL。
说明:
从指定的内存池中申请一块内存。注意,连续调用本函数,并不能保证连续申请的内存地址是连续的。如果指定的内存池允许扩容,则该内存池耗尽时,将从堆中获取一块内存添加到内存池中。但从堆中扩容内存到内存池中的操作时单向的,一经扩容永不释放,直到内存池被删除。
8.5.6 Mb_Free:释放内存
void Mb_Free(struct mem_cell_pool *pool,void *block);
头文件:
os.h
参数:
block:待释放的内存块指针;
pool:目标内存池。
返回值:
无。
说明:
释放内存,把使用完毕的内存块放回指定的内存池,内存池和内存块必须匹配,否则会发生不可预料的错误,新释放的块链接到 free_list 队列中,而不是放回连续池,也不重新返回系统堆。
8.5.7 Mb_QueryFree:查询内存池空闲量
u32 Mb_QueryFree(struct mem_cell_pool *pool);
头文件:
os.h
参数:
pool:目标内存池。
返回值:
空闲内存块数。
说明:
查询内存池还有多少空闲内存块,对于一个不可扩容的内存池,返回结果就是可供分配的块数;对一个可扩容的,返回的是当前池中的空闲块数。
8.5.8 Mb_QueryCapacital:查询内存池总量
u32 Mb_QueryCapacital(struct mem_cell_pool *pool)
头文件:
os.h
参数:
pool:目标内存池。
返回值:
内存池总内存块数。
说明:
查询内存池总共有多少内存块,对于一个可扩容的内存池,其总容量也包括扩容部分,而不是允许扩容的上限。
第9章 锁
锁的用途是保证临界区数据的安全访问,或者线程与线程、线程与中断间的同步。临界区数据指的是不同线程或者中断可能同时访问的数据区。这里要注意并不是所有的共享数据区都是临界区,只有那些有可能被同时访问的区域,才称作临界区。DJYOS只支持线程之间、线程和异步信号间的同步,不支持线程和实时中断间的同步。另外,线程和异步信号间的同步还可以使用“异步信号同步”功能。DJYOS支持3种锁:
1.调度锁,锁住调度,等同于禁止中断。
2.信号量,适用于允许一个或者有限多个用户同时访问的场合。信号量允许线程之间或中断与线程间的交叉请求和释放,即A线程请求信号量——B线程释放信号量。
3.互斥量,适用于单一资源且需要优先级继承的场合。互斥量的请求者和释放者必须是同源,即同一事件(线程)或者中断处理函数。
信号量(互斥量)控制块使用相同的内存池,称为锁控制块,采用固定块动态分配法。创建信号或者互斥量时,信号或互斥量总数在DIDE中配置,如图 9‑1。另外,为了满足OSEK、MISRA等标准的要求,DJYOS还提供另一对函数用于创建信号或互斥量:Lock_SempCreate_s、Lock_MutexCreate_s,这对函数是静态地创建信号或者互斥量,即要求调用者提供(指定)锁控制块空间,其数量不受图 9‑1限制。
图 9‑1DIDE中配置锁模块
信号量和互斥量的共同点:
|
信号量 |
互斥量 |
信号数 |
1~0xffffffff |
1 |
初始信号数 |
任意 |
1 |
排队顺序 |
优先级或FIFO |
优先级 |
请求和释放① |
可在不同的上下文中 |
必须在相同上下文中 |
优先级继承 |
不支持 |
支持多级优先级继承 |
优先级置顶 |
不支持 |
支持 |
嵌套请求② |
请求数超过可用信号灯数时,会死锁(假设超时timeout =cn_timeout_forever) |
可嵌套请求 |
共同适用的场合 |
非计数型临界区访问保护 |
|
适用场合 |
1.线程间同步; 2.线程间通信; 3.计数型临界区访问。 |
1.须防止优先级翻转的非计数型临界区访问保护; 2.用于自动调整优级。 |
:
①相同上下文的意思是,在同一个线程中,或者在异步信号中。
②一个线程连续多次请求然后连续多次释放,称为嵌套请求。
信号量和互斥量有许多相似的特性,都能够提供临界区安全保护,有许多场合既可用信号量实现,又可以用互斥量实现,但他们在使用上,还是有区别的。
同步和线程间(线程与异步信号间)通信是信号量的拿手绝活,典型用途:
1.A线程阻塞于某信号量(pend),B线程(或异步信号)在合适的时机释放信号量(post),A线程得以继续运行。
2.A线程要访问某临界资源,先获取保护该资源的信号量,访问完毕后释放(post)。期间,如果B线程也来取该信号量,将会被阻塞,直到A释放该线程。
3.A线程要读取数据,但该数据尚未被生产出来,A线程将pend并阻塞在信号量上,B线程(或异步信号中断)生产数据后,释放(post)信号量。A线程解除阻塞并访问数据。
4.某资源允许n个入口,可使用信号量的计数功能,例如使用信号量保护内存池。
信号量的用途要比互斥量广泛得多,除优先级继承功能外,互斥量的所有功能,信号量都可以涵盖。
在对象序列中,信号量和互斥量各有一个目录,分别是“semaphore”和“mutex”。
9.1 锁调度和锁中断
参见6.4节和6.4.1节。
特别注意,原子操作并不锁调度,千万不能在原子操作期间调用可能发生调度的函数,原子操作期间最好不要调用任何系统API。
9.2 信号量API
9.2.1 Lock_SempCreate:创建信号量
struct SemaphoreLCB *Lock_SempCreate(s32 lamps_limit,s32 init_lamp,
u32 sync_order,const char *name);
头文件:
os.h
参数:
lamps_limit:设定可用信号灯的上限,设定值不能为零,semp_limit = CN_LIMIT_UINT32则表示无上限,无限多。
init_lamp:初始可用信号灯数量,取值范围0~ lamps_limit;
sync_order:被信号阻塞事件的排列顺序。sync_order = CN_BLOCK_FIFO表示按申请获取信号的先后顺序排列,sync_order = CN_BLOCK_PRIO表示按事件的优先级排列;
name:信号量的名字,所指向的字符串不能定义为局部变量,无名称即为NULL。
返回值:
新创建的信号量。
说明:
建立一个信号量,信号量须先创建再使用。可以创建的信号量和互斥量的数量总和(两者之和),受图 9‑1限制。
9.2.2 Lock_SempCreate_s:创建静态信号量
struct SemaphoreLCB *Lock_SempCreate_s( struct SemaphoreLCB *semp,
s32 lamps_limit,s32 init_lamp,u32 sync_order,
const char *name);
头文件:
os.h
参数:
semp:信号量控制块指针。
其他参数:同9.2.1节
返回值:
无。
说明:
可靠地创建一个信号量,与Lock_SempCreate函数不同的是,调用者须提供信号量控制块,然后把指针传递过来,高可靠性的应用中,不应该使用动态分配的方式,静态定义更可靠,本函数创建的信号量,不图 9‑1约束。
9.2.3 Lock_SempDelete:删除信号量
bool_t Lock_SempDelete(struct SemaphoreLCB *semp);
头文件:
os.h
参数:
semp:被删除的信号量
返回值:
删除成功返回true;失败返回false。
说明:
删除一个信号量,与Lock_SempCreate函数对应。
9.2.4 Lock_SempDelete_s:删除静态信号
bool_t Lock_SempDelete_s(struct SemaphoreLCB *semp)
头文件:
os.h
参数:
semp:被删除的信号量
返回值:
删除成功返回true;失败返回false。
说明:
删除一个信号量,与Lock_SempCreate_s函数对应。
9.2.5 Lock_SempPend:等待信号量
bool_t Lock_SempPend(struct SemaphoreLCB *semp,u32 timeout)
头文件:
os.h
参数:
semp:信号量指针
timeout:参见第5.7.1节。
返回值:
获取(申请)信号量成功返回true;获取信号量失败,返回false,如超时,信号量检查失败等。具体的错误类型,通过错误码来确定。
说明:
申请获取信号量的信号灯,如果信号量有效且有信号灯可用,则将可用信号灯总量减一,并成功返回true。如果同一个线程反复请求(pend)一个信号量,每次请求都会造成信号灯数量减一,减到零的时候,将被阻塞。这种情况下,由于被阻塞的原因,是本线程早先拿走了信号量,故只能等待自己释放,而自己又被阻塞不能运行,将造成死锁。假如timeout= CN_TIMEOUT_FOREVER,线程将永久失去响应。
9.2.6 Lock_SempPost:发送信号量
void Lock_SempPost(struct SemaphoreLCB *semp)
头文件:
os.h
参数:
semp:信号量指针。
返回值:
无。
说明:
释放一个信号灯,可用信号灯增一。注意,当可用信号灯数量大于lamps_limit后,就不会再增加。
9.2.7 Lock_SempQueryCapacital:查询信号量容量
u32 Lock_SempQueryCapacital(struct SemaphoreLCB *semp);
头文件:
os.h
参数:
semp:被查询的信号量。
返回值:
信号量包含的信号灯总数。
说明:
查询一个信号量包含信号灯的总数。
9.2.8 Lock_SempQueryFree:查询可用信号数
u32 Lock_SempQueryFree(struct SemaphoreLCB *semp)
头文件:
os.h
参数:
semp:被查询的信号量。
返回值:
信号灯总数。
说明:
查询一个信号量可用信号灯的数量,如果大于0,调用Lock_SempPend函数能取得信号量。
9.2.9 Lock_SempCheckBlock:查询是否存在被阻塞事件
bool_t Lock_SempCheckBlock(struct SemaphoreLCB *Semp)
头文件:
os.h
参数:
Semp:信号量指针。
返回值:
存在等待同步的事件返回true;否则fasle。
说明:
查询信号量的同步队列中是否有事件在等待。
9.2.10 Lock_SempSetSyncSort:设置信号量阻塞方式
void Lock_SempSetSyncSort(struct SemaphoreLCB *semp,u32 order)
头文件:
os.h。
参数:
semp:被设置的信号量;
order:被信号阻塞事件的排列顺序。sync_order = CN_BLOCK_FIFO表示按申请获取信号的先后顺序排列,sync_order = CN_BLOCK_PRIO表示按事件的优先级排列。
返回值:
无。
说明:
设置被信号量阻塞的事件,在该信号量同步队列中的排队方式。该设置只影响设置之后新加入的事件,已在队列中的事件不受影响。
9.3 互斥量API
9.3.1 Lock_MutexCreate:创建互斥量
struct MutexLCB *Lock_MutexCreate(char *name)
头文件:
os.h。
参数:
name:互斥量名称,所指向的字符串不能定义为局部变量。无名称则为NULL。
返回值:
新创建互斥量的指针。
说明:
信号量所占内存由系统从锁内存池中分配。互斥量须先创建再使用。可以创建的信号量和互斥量的数量总和(两者之和),受图 9‑1限制。
9.3.2 Lock_MutexCreate_s:创建静态互斥量
struct MutexLCB *Lock_MutexCreate_s(struct MutexLCB *mutex,
char *name)
头文件:
os.h
参数:
mutex:互斥量控制块指针。
name:互斥量名称,所指向的字符串不能定义为局部变量。无名称则为NULL。
返回值:
新创建互斥量的指针。
说明:
静态地创建一个互斥量,与mutex_create函数不同的是,调用者须提供互斥量控制块,然后把指针传递过来,高可靠性的应用中,不应该使用动态分配的方式,静态定义更可靠。内核中使用的互斥量,都是使用Lock_MutexCreate_s创建的。本函数创建的信号量和互斥量的数量总和(两者之和),不受图 9‑1限制。
9.3.3 Lock_MutexDelete:删除互斥量
bool_t Lock_MutexDelete(struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:被删除信号量的指针。
返回值:
删除成功返回true;否则返回false。
说明:
删除一个信号量,与mutex_create函数对应。
9.3.4 Lock_MutexDelete_s:删除静态互斥量
bool_tLock_MutexDelete_s(struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:被删除信号量的指针。
返回值:
删除成功返回true;否则返回false。
说明:
删除一个信号量,与mutex _create_r函数对应。
9.3.5 Lock_MutexPend:等待互斥量
bool_t Lock_MutexPend(struct MutexLCB *mutex,u32 timeout)
头文件:
os.h
参数:
mutex:互斥量指针。
timeout:参见第5.7.1节。
返回值:
获取(申请)互斥量成功返回true;获取信号量失败返回false,如超时,信号量检查失败等。具体的错误类型,通过错误码来确定。
说明:
由于mutex只能被一个事件拥有,owner成员保存了该事件指针,因此嵌套请求是允许的。即如果请求一个处于忙状态的互斥量,如果请求者是拥有者,则不会被阻塞,否则将被阻塞。
9.3.6 Lock_MutexPost:发送互斥量
void Lock_MutexPost(struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:互斥量指针。
返回值:
无。
说明:
释放互斥量,再次强调,互斥量只能被拥有者释放,所以互斥量不适用于线程间或线程与异步信号中断间同步。
9.3.7 Lock_MutexCheckBlock:查询是否存在被阻塞事件
bool_t Lock_MutexCheckBlock (struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:互斥量指针。
返回值:
存在等待同步的事件返回true;否则fasle。
说明:
查询互斥量的同步队列中是否有事件在等待。
9.3.8 Lock_MutexGetOwner:查询占用者
u16 Lock_MutexGetOwner(struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:互斥量指针。
返回值:
互斥量被事件占有,返回该事件ID;否则返回CN_EVENT_ID_INVALID。
说明:
获取占有互斥量mutex的事件的ID。
9.3.9 Lock_MutexQuery:查询互斥量状态
bool_t Lock_MutexQuery(struct MutexLCB *mutex)
头文件:
os.h
参数:
mutex:被查询的互斥量。
返回值:
互斥量可用返回true;不可用则返回false。
查询互斥量是否可用。
第10章 GUI
见《DJYOS图形编程(上)》和《DJYOS图形编程(下)》。
第11章 对象系统
11.1 对象树
djyos的IO系统依托对象系统构建。
内存中构建了一个对象管理系统,对象可能对应文件,也可能对应目录。所有文件均已对象为基础,对象作为软件所要操作的数据集合的高层次抽象,实现了一些公共的管理功能,并为此定义了一套外部接口。对象系统并不知道它如何被用户使用,一个对象,究竟是文件还是目录,是套接字还是SPI总线,是内存池还是信号量,……,对象系统并不知道。特别提示,不能以是否拥有子对象来区分文件与目录,文件也可以有子文件,典型代表是djygui的窗口,窗口和子窗口都是文件。
那怎么区分不同性质的对象呢?在于 struct Object的ObjOps成员,一组性质相同的对象,会用继承的方式共享ObjOps,例如yaffs2,mount点(一个对象)的ObjOps,会被所有被加载的文件或目录对象继承。object.h 中定义了完整的命令集,而具体对象,则根据自己的需要,选择性地实现。例如“lock”对象,就不实现“write”方法。
由此可见,djyos的对象系统,也有面向对象编程的含义。
图 11‑1 对象树组织图
数据结构:
struct Object { char *name; // 对象名;当用于文件系统为文件名或目录名,用于设备是 //设备名,用于gui则是窗口名; ptu32_t ObjPrivate; // 对象私有数据;可以是一个数,也可以指向描述对象的结构; fnObjOps ObjOps; // 对象方法;即对象的操作; u32 inuse; // 多用户同时引用计数 list_t handles; // 对象句柄链表;对象被打开,系统会给用户分配一个句柄; // 句柄的数据结构接入此链表; struct Object *prev,*next, *parent, *child; // 对象关系;构建对象树; };
|
各成员说明:
prev,next,Parent,Child四个指针:构成如图 11‑1树形数据结构。
ObjPrivate:对象私有数据,用来描述该对象的某些属性,有些简单对象可能不使用,也有些对象不太复杂,用一个32位的整数就能表达;复杂些的对象,就要定义私有数据结构,此时,ObjPrivate是指向该结构的指针。
ObjOps:对象操作函数,这里定义了一系列操作对象的命令,与ObjPrivate共同构成该对象的功能。对象系统本身不管对象用途,对象的实际使命,全系于ObjOps。
inuse:对象的引用次数,与图 11‑1中“port”的数量一致。
handles:指向对象的访问句柄的链表。对象本身并不能用“read/write”等函数直接访问,APP要读写对象,必须通过句柄结构。句柄结构参见11.2.38。
11.2 对象API说明
11.2.1 OBJ_GetPrivate取对象私有数据
ptu32_t OBJ_GetPrivate(struct Object *obj);
头文件:
os.h
参数:
obj:目标对象指针。
返回值:
对象的私有指针。
11.2.2 OBJ_SetPrivate设置对象私有数据
void OBJ_SetPrivate(struct Object *obj, ptu32_t Private);
头文件:
os.h
参数:
obj:目标对象指针。
Private:新的私有数据
返回值:
无。
11.2.3 OBJ_GetOps取对象操作函数
fnObjOps OBJ_GetOps(struct Object *obj);
头文件:
os.h
参数:
obj:目标对象指针。
返回值:
对象的操作函数指针。
11.2.4 OBJ_SetOps设置对象操作函数
s32 OBJ_SetOps(struct Object *obj, fnObjOps ops);
头文件:
os.h
参数:
obj:目标对象指针。
ops:新的操作函数指针
返回值:
无。
11.2.5 OBJ_GetCurrent:取当前对象
struct Object * OBJ_GetCurrent(void);
头文件:
os.h
参数:无
返回值:
当前对象指针。
11.2.6 OBJ_SetCurrent :设置当前对象
void OBJ_SetCurrent(struct Object * obj);
头文件:
os.h
参数:
obj:新的当前对象指针。
返回值:
无。
11.2.7 OBJ_GetName:查询对象名
const char *OBJ_GetName(struct Object *Obj);
头文件:
os.h
参数:
Obj:对象指针。
返回值:
成功则返回对象名指针,失败返回false。
说明:略。
11.2.8 OBJ_ReName:修改对象名
char *OBJ_ReName(struct Object * Obj,char *NewName);
头文件:
os.h
参数:
Obj:被修改的资源节点的指针。
NewName:节点新名称的指针。
返回值:
执行成功返回true;失败返回false。
说明:
修改对象名称,本函数执行过程拷贝数据,而是直接使用NewName提供的buffer,因此,NewName应该提供永久buffer,不允许局部数组,也不允许用过就释放的临时空间。
11.2.9 OBJ_CheckName:检查对象名
s32 OBJ_CheckName(const char *name);
头文件:
os.h
参数:
name:被检查的字符串指针。
返回值:
合法(0);非法(-1)。
说明:
检查一个字符串是否合法的对象名:不允许存在'/'、'\'字符,长度不允许超过规定;
11.2.10 OBJ_NewPrev:创建前导对象
struct Object *OBJ_NewPrev (struct Object *loc, fnObjOps ops,
ptu32_t ObjPrivate, const char *name);
头文件:
os.h
参数:
loc:基准对象,新对象是插入在这个节点的previous位置。
ops:新对象的操作函数,可以是NULL。
ObjPrivate:对象的私有数据,与ops共同决定对象功能。
name:对象名称,所指向的字符串不能定义为局部变量,无名称即为NULL。
返回值:
指向新插入对象的指针。
说明:略
11.2.11 OBJ_NewNext:创建后继对象
struct Object *OBJ_NewNext(struct Object *loc, fnObjOps ops,
ptu32_t ObjPrivate, const char *name);
头文件:
os.h
参数:
loc:基准对象,新对象是插入在这个节点的next位置。
ops:新对象的操作函数,可以是NULL。
ObjPrivate:对象的私有数据,与ops共同决定对象功能。
name:对象名称,所指向的字符串不能定义为局部变量,无名称即为NULL。
返回值:
指向新插入对象的指针。
说明:略
struct Object *OBJ_NewHead(struct Object *loc,fnObjOps ops,
ptu32_t ObjPrivate, const char *name);
11.2.12 OBJ_NewChild:创建子对象
struct Object *OBJ_NewChild (struct Object *loc, fnObjOps ops,
ptu32_t ObjPrivate, const char *name);
头文件:
os.h
参数:
loc:基准对象,新对象成为这个对象的子对象,位于子对象的队列尾部。
ops:新对象的操作函数,可以是NULL。
ObjPrivate:对象的私有数据,与ops共同决定对象功能。
name:对象名称,所指向的字符串不能定义为局部变量,无名称即为NULL。
返回值:
指向新插入对象的指针。
说明:略
11.2.13 OBJ_NewHead:创建头对象
struct Object *OBJ_NewHead (struct Object *loc, fnObjOps ops,
ptu32_t ObjPrivate, const char *name);
头文件:
os.h
参数:
loc:基准对象,新对象成为它的兄弟对象,位于队列头部。
ops:新对象的操作函数,可以是NULL。
ObjPrivate:对象的私有数据,与ops共同决定对象功能。
name:对象名称,所指向的字符串不能定义为局部变量,无名称即为NULL。
返回值:
指向新插入对象的指针。
说明:略
11.2.14 OBJ_Detach:分离对象分支
struct Object *OBJ_Detach(struct Object *branch);
头文件:
os.h
参数:
branch:被删除的分支。
返回值:
成功删除返回值指向被删除分支节点的指针;返回值为NULL表示分支不存在。
说明:
注意,本函数只是将这个分支与对象树断开链接,并不释放对象分支所占据的内存。
11.2.15 OBJ_Delete:删除对象
s32 OBJ_Delete(struct Object *obj);
头文件:
os.h
参数:
obj:被删除的对象,注意它不能有子对象。
返回值:
成功(0),失败(-1)。
说明:略。
11.2.16 OBJ_MoveToLast:移动对象至队尾
s32 OBJ_MoveToLast(struct Object *obj);
头文件:
os.h
参数:
Obj:被移动对象的指针。
返回值:
成功(0),失败(-1)。
说明:略。
11.2.17 OBJ_MoveToHead:移动对象至队首
s32 OBJ_MoveToHead(struct Object *obj);
头文件:
os.h
参数:
Obj:被移动对象的指针。
返回值:
成功(0),失败(-1)。
说明:略。
11.2.18 OBJ_MoveToNext:后移对象
s32 OBJ_ChildMoveToNext(struct Object *parent);
头文件:
os.h
参数:
Obj:被移动对象的指针。
返回值:
成功(0),失败(-1)。
说明:略。
11.2.19 OBJ_MoveToPrev:前移对象
s32 OBJ_ChildMoveToPrev(struct Object *parent);
头文件:
os.h
参数:
Obj:被移动对象的指针。
返回值:
成功(0),失败(-1)。
说明:略。
11.2.20 OBJ_InsertToNext:移动对象到指定对象后面
s32 OBJ_InsertToNext(struct Object *loc, struct Object *next);
头文件:
os.h
参数:
Obj:被移动对象的指针。
next:新对象位于它的后面(next指针位置)
返回值:
成功(0),失败(-1)。
说明:略。
11.2.21 OBJ_InsertToPrev:移动对象到指定对象前面
s32 OBJ_InsertToPrev (struct Object *loc, struct Object * prev);
头文件:
os.h
参数:
Obj:被移动对象的指针。
next:新对象位于它的前面(previous指针位置)
返回值:
成功(0),失败(-1)。
说明:略。
11.2.22 OBJ_GetParent:获取父对象
struct Object *OBJ_GetParent(struct Object *obj);
头文件:
os.h
参数:
Obj:基准子对象。
返回值:
指向基准对象的父对象的指针。
说明:
获取基准对象的父对象指针。
11.2.23 OBJ_GetChild:获取子对象
struct Object *OBJ_GetChild(struct Object *obj);
头文件:
os.h
参数:
Obj:基准父对象。
返回值:
子对象的指针。
说明:
获取对象Obj所指向的子对象的指针。注意:当对象Obj存在多个子对象时,返回的是子对象同级队列的队列头。
11.2.24 OBJ_GetPrev:获取前对象
struct Object *OBJ_GetPrev(struct Object *obj);
头文件:
os.h
参数:
Obj:基准对象。
返回值:
基准对象前一个对象的指针(previous位置)。
说明:
基准对象所在的同级资源对象队列上,获取基准对象的前一个对象(previous位置)。
11.2.25 OBJ_GetNext:获取后对象
struct Object *OBJ_GetNext(struct Object *obj);
头文件:
os.h
参数:
Obj:基准对象。
返回值:
基准对象后一个对象的指针(next位置)。.
说明:
基准对象所在的同级资源对象队列上,获取基准对象的后一个对象(next位置)。
11.2.26 OBJ_GetHead:获取队首对象
struct Object *OBJ_GetHead(struct Object *obj);
头文件:
os.h
参数:
Obj:基准对象。
返回值:
基准对象所在同级对象队列的头对象指针。
说明:
获取基准对象所在同级对象队列的队列首对象。
11.2.27 OBJ_Root:获取根对象
struct Object *OBJ_Root (struct Object *obj);
头文件:
os.h
参数:
Obj:基准对象。
返回值:
根对象指针。
说明:略。
11.2.28 OBJ_GetTwig:获取末梢对象
struct Object *OBJ_GetTwig(struct Object *obj)
头文件:
os.h
参数:
Obj:基准对象。
返回值:
对象Obj分支的末梢对象。所谓末梢对象,就是没有子对象的对象。当基准对象本身就是末梢对象,则返回NULL。
说明:
获取对象Obj分支的末梢对象(结点)。当需要删除一个资源树分支时,本函数结合函数OBJ_Del,反复调用,直至本函数返回NULL。例如需要删除一个文件夹或者存在子窗口的图形窗口时,就需要此类操作。
11.2.29 OBJ_GetLevel:查询对象路径深度
s32 OBJ_GetLevel(struct Object *obj);
头文件:
os.h
参数:
Obj:基准对象。
返回值:
基准对象在对象树中的路径深度。
说明:
查看基准对象在资源树上的路径深度,根对象的子对象为0,子子对象为1,依次递增。
11.2.30 OBJ_ForeachChild:遍历子对象
struct Object *OBJ_ForeachChild(struct Object *parent, struct Object *current);
头文件:
os.h
参数:
parent:父对象。
current:当前对象。
返回值:
当前搜索位置的下一个对象指针,如果已经搜索完成,则返回NULL。
说明:
遍历对象(parent)的所有子对象链表,每调用一次,返回一个子对象(current),直至完成;current必须先初始化;如果初始化为父对象,则遍历全部队列成员;如果current初始化为是某个子对象,则只沿着next指针方向遍历其到队列结束,而不是全部队列成员;
11.2.31 OBJ_ForeachScion:遍历一个树枝
struct Object *OBJ_ForeachScion(struct Object *ancester, struct Object *current);
头文件:
os.h
参数:
Ancestor:需要搜索的树枝的祖先对象。
Current:当前搜索位置。,
返回值:
当前搜索位置的下一个对象指针,如果已经搜索完成,则返回NULL。
说明:
逐个返回源对象Ance之下分支的所有对象成员,即遍历整个分支。注意本函数第一次调用时,两个参数必须是源对象。
当需要对资源链表中某一个树枝或者整个链表中的对象逐一进行某种操作时,可,反复调用本函数,第一次调用Current = Ancestor,其后Current = 上次返回值,直到返回空,本函数按父、子、孙、曾孙....的顺序搜索,先搜直系,再搜旁系,确保所有子孙对象都能够访问到,如果对访问顺序有特殊要求,不能使用本函数。
11.2.32 OBJ_SearchSibling:在兄弟对象中搜索对象
struct Object *OBJ_SearchSibling(struct Object *obj, const char *name);
头文件:
os.h
参数:
obj:基准对象。
Name:需要检索的对象的对象名。
返回值:
名称检索成功,返回对象名为Name的对象指针;失败则返回NULL。
说明:
在与基准对象的同级对象之中,检索对象名为Name的对象。
11.2.33 OBJ_SearchChild:在子对象中搜索对象
struct Object *OBJ_SearchChild(struct Object *parent, const char *name);头文件:
os.h
参数:
Parent:基准对象。
Name:对象名称。
返回值:
名称检索成功,则返回对象名为Name的对象指针;否则返回NULL。
说明:
在基准对象的子对象中,检索名称为Name的子对象。
11.2.34 OBJ_SearchScion:在分支对象中搜索对象
struct Object *OBJ_SearchScion(struct Object *ancester, const char *name);
头文件:
os.h
参数:
Ancestor:基准对象。
Name:对象名称。
返回值:
名称检索成功,则返回对象名为Name的对象指针;否则返回NULL。
说明:
在以对象Ancestor为起点的分支上,检索名称为Name的对象。
11.2.35 OBJ_SearchPath:在路径中搜索对象
struct Object *OBJ_SearchPath(struct Object *start, const char *path);
头文件:
os.h
参数:
Start:基准对象,分支的源对象。
Path:对象路径名称。
返回值:
检索成功返回目标对象;否则返回NULL。
说明:
从对象Start为源头(起点)的分支开始,检查路径名与Path一致的对象,注意这里的路径名是从对象Start开始的。
11.2.36 OBJ_MatchPath:匹配路径
struct Object *OBJ_MatchPath(const char *match, char **left);
头文件:
os.h
参数:
match,需匹配的路径;。
left,如果完全匹配,返回NULL;不完全匹配,则返回不匹配部分(保证不以'/'开头)。
返回值:
匹配的对象指针,NULL代表完全不匹配
说明:
与OBJ_ search_path类似,不同的是,找到第一个不匹配的对象就返回。例如,对象树中有"obj1\obj2\",参数match ="obj1\obj2\obj3\obj4",将返回obj2的指针。
11.2.37 OBJ_Ishead:是否是队首对象
s32 OBJ_Ishead(struct Object *obj);
头文件:
os.h
参数:
Obj:对象指针。
返回值:
对象为同级对象队列的队头返回true;否则返回false。
说明:
判断对象Obj是不是其所处的同级对象队列的头部。
11.2.38 OBJ_Islast:是否是队尾对象
s32 OBJ_Islast(struct Object *obj);
头文件:
os.h
参数:
Obj:对象指针。
返回值:
对象为同级对象队列的列尾返回true;否则返回false。
说明:
判断对象Obj是不是其所处的同级对象队列的尾部。
11.2.39 OBJ_Isonduty:是否被使用中
s32 OBJ_Isonduty(struct Object *obj);
头文件:
os.h
参数:
Obj:对象指针。
返回值:
对象被引用次数不为0则true;否则返回false。
说明:略
11.2.40 OBJ_Lock:锁定对象树
bool_t OBJ_Lock(void);
头文件:
os.h
参数:
无
返回值:
锁定=true,出错=false。
说明:
锁定对象树,以防并发操作破坏对象树,在做对象链表做操作时,需要锁定对象树。
11.2.41 OBJ_Unlock:解锁对象树
bool_t OBJ_Unlock (void);
头文件:
os.h
参数:
无
返回值:
锁定=true,出错=false。
说明:
与OBJ_Lock函数配套使用,以防并发操作破坏对象树,对象链表做操作完成后,解锁对象树。
11.2.42 OBJ_SetMultiplexEvent:设置多路复用触发事件
s32 OBJ_SetMultiplexEvent(struct Object *obj, u32 events);
头文件:
os.h
参数:
obj:被触发的对象。
events:触发事件,参考CN_MULTIPLEX_STATUSMSK
返回值:
成功(0);失败(-1);
说明:
多路复用功能,参见第21章。
11.2.43 OBJ_ClrMultiplexEvent:清除多路复用触发事件
s32 OBJ_ClrMultiplexEvent(struct Object *obj, u32 events);
头文件:
os.h
参数:
obj:被触发的对象。
events:被清除的触发事件,参考CN_MULTIPLEX_STATUSMSK
返回值:
成功(0);失败(-1);
说明:
多路复用功能,参见第21章。
第12章 IO体系
djyos的IO体系依托对象系统建立,参见第11章。
djyos的IO体系几乎包含了全部由操作系统管理的元素,包括但不限于:文件、socket、设备、信号量、IIC总线、SPI总线、图形窗口……。
IO体系提供三种API接口:
1、 ansi C标准的库函数接口,这是一种用户态缓冲文件系统。
2、 POSIX规范的文件接口,例如open、write、read等,是否提供缓冲,由具体实现决定,但即使提供缓冲,也在内核态缓冲(多进程环境,单进程没有内核态)。
3、 特定接口,例如读设备文件,处理fread和read外,还可以用ReadDevice函数,这个函数除了与早期版本的APP兼容外,也更加高效。
并不是所有文件都需要提供所有接口,例如信号量文件,就只支持open、show等接口,不支持read、write等接口,调用read和write时,总是返回0。
12.1 文件描述符
12.1.1 文件描述符结构
文件描述符(以下简称fd)是一个非负整数,由open函数返回。fd与文件并不是一一对应的,而是与struct Objecthandle结构一一对应。Open函数将创建一个struct Objecthandle结构,每个struct Objecthandle结构实体必须对应一个struct Object结构实体,反之,struct Object结构实体可以对应多个struct Objecthandle结构实体,对应于一个文件被多次打开。
数据结构定义为:
struct Objecthandle{ list_t list; // 句柄所关联的对象的句柄链表; u32 flags; // 当前文件操作标志; struct Object *HostObj; // 关联的对象 u32 timeout; // 同步访问的timeout,单位us,不超过1.19小时; struct MultiplexObjectCB * pMultiplexHead; // 多路复用目标对象头指针; u32 MultiplexEvents; // 对象的当前访问状态,如可读,可写等。24bit,高8位无效, // 可用于多路复用; ptu32_t UserTag; // 用户标签,由用户设定该标签用途; ptu32_t context; // handle访问上下文 }; |
各成员说明:
list:用于将一个struct Object结构实体对应的struct Objecthandle结构实体链接起来。
flags:当前文件操作标志,如O_RDONLY等,在fcntl.h中定义。
HostObj:指向本结构对应的对象文件。
Timeout:同步访问时的超时时间,参见第5.7.1节。
pMultiplexHead:用于多路复用,原则上,每个fd都可以加入任何多路复用集,多路复用是djyos提供的、类似epoll和select的功能。
MultiplexEvents:fd用于多路复用的状态描述。
UserTag:用户标签,由上层用户使用。
context:文件上下文,由具体文件的实现者使用。
12.1.2 同步IO
同步IO的读和写,都可能导致阻塞,例如对于同步IO的fd,如果没有数据可读,对齐执行read操作时,将阻塞,直到有数据可读,如果一直没有数据可读,将一直阻塞下去。djyos对于这类操作,都设计了timeout参数,例如Device_Read,阻塞达到timeout指定时间时强制返回。这样,利于程序员控制软件的行为。但无论是C库标准函数fread/fwrite等,还是POSIX标准函数read/write等,都没有接口用来传递timeout,而提供C库和POSIX标准接口,又是必须的。怎么办呢?djyos在句柄控制块上设置了一个timeout属性,这样,每个文件描述符,都有自己的timeout属性。对该文件执行read/write等操作时,最长阻塞时间就由timeout属性决定。注意,timeout是句柄的属性,同一个文件,如果打开多个副本,则每个副本都有其自己的timeout属性。
fcntl函数的F_SETTIMEOUT/F_GETTIMEOUT命令,用于设置和读取timeout。Timeout的默认值是CN_TIMEOUT_FOREVER,如果不调用fcntl函数修改,read/write等函数的行为,就与POSIX库一致。同时,djyos还提供了更加方便的函数:Handle_SetTimeOut/ Handle_GetTimeOut用于设置和读取timeout属性。
12.1.3 文件描述符转换
fd是一个非负整数,它是struct Objecthandle指针经一定的规则转换得到。fd与struct Objecthandle一一对应,但POSIX不完全兼容,POSIX规定,每次open,应该分配不大于OPEN_MAX的最小的可用fd,这导致fd分配变得复杂,且看不到对用户有什么好处。
按照POSIX规定,必然需要有一个表格来管理fd,分配fd必定是一个查表的过程,在执行时间上是不确定的,这与实时系统的要求不符。
表格的尺寸是OPEN_MAX,OPEN_MAX的大小,对应着表格占用RAM的多少。对于通用系统来说,由于有丰富的内存,可以配置一个较大的OPEN_MAX来适应绝大多数应用。而嵌入式系统则不然,它的内存很小,意味着每一个应用,都要精打细算地使用RAM,每个应用配置的OPEN_MAX都可能不一样,增加了配置管理的难度。
djyos的OPEN_MAX被固定定义为CN_LIMIT_SINT32,不需要配置;djyos使用了一个简便的方式把struct Objecthandle指针转换为fd,转换过程的运算量非常小且运算时间确定。在地址长度是32位、字节长度<32位的机器上,fd实际上是 struct Objecthandle指针作为无符号数右移1位得到。为什么要右移1位呢?因为指针可能在4G范围,对于超过 0x7fffffff的指针,作为有符号正数解析时,就会被当做负数,很多开源软件以负数为出错条件。而对于字节长度<32位的计算机,指针的bit0天然是0,fd只要左移1bit,就能还原出指针。
djyos的fd对POSIX的符合度:
1、 fd是非负整数,符合。
2、 fd不大于OPEN_MAX,符合。
3、 fd从小到大分配,不符合。
12.2 文件描述符相关API
12.2.1 Handle2fd:句柄转fd
s32 Handle2fd(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被操作的handle
返回值:
文件描述符fd
说明:略。
12.2.2 fd2Handle:fd转句柄
struct Objecthandle *fd2Handle(s32 fd);
头文件:
os.h
参数:
fd:被操作的fd
返回值:
文件句柄
说明:略。
12.2.3 Test_Directory:测试目录标志
s32 Test_Directory(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有目录选项,false= 无。
说明:
检查flags标志中是否含有目录选项
12.2.4 Test_Regular:测试文件标志
s32 Test_Regular(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有文件选项,false= 无。
说明:
检查flags标志中是否含有文件选项
12.2.5 Test_Creat:测试创建标志
s32 Test_Creat(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有创建标志,false= 无。
说明:
检查flags标志中是否含有创建选项
12.2.6 Test_OnlyCreat:测试仅创建标志
s32 Test_OnlyCreat(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有仅创建标志,false= 无。
说明:
检查flags标志中是否仅创建选项
12.2.7 Test_Append:测试追加标志
s32 Test_Append(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有追加标志,false= 无。
说明:
检查flags标志中是否含有追加选项
12.2.8 Test_Trunc:测试截断标志
s32 Test_Trunc(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有截断标志,false= 无。
说明:
检查flags标志中是否含有截断选项
12.2.9 Test_Readable:测试可读标志
s32 Test_Readable(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有可读标志,false= 无。
说明:
检查flags标志中是否含有可读选项
12.2.10 Test_Writeable:测试可写标志
s32 Test_Writeable(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中有可写标志,false= 无。
说明:
检查flags标志中是否含有可写选项
12.2.11 Test_IsBlockComplete:判断阻塞条件
s32 Test_IsBlockComplete(u32 flags);
头文件:
os.h
参数:
flags:被测试标志
返回值:
true = flags中阻塞标志是直至物理层完成传输,false = 否。
说明:
对于同步IO操作,阻塞模式有两种,一种是把数据写入到硬件设备的发送buffer即算完成,另一种是直到硬件设备物理层完成数据传输才算完成,如果本函数返回true,则是后一种。
使用场景:
1、 低功耗应用中,必须等物理层传输完成后,才能关闭硬件以节省功耗。
2、 半双工通信中,发送完成才能切换到接收监控状态。
12.2.12 Handle_IsAppend:句柄是否追加模
s32 Handle_IsAppend(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被测试句柄
返回值:
true = handle按追加模式打开,false = 不是。
说明:
检查句柄是否按追加模式打开
12.2.13 Handle_IsReadable:句柄是否可读模式
s32 Handle_IsReadable(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被测试句柄
返回值:
true = handle按可读模式打开,false = 不是。
说明:
检查句柄是否按可读模式打开
12.2.14 Handle_IsWritable:句柄是否可写模式
s32 Handle_IsWritable(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被测试句柄
返回值:
true = handle按可写模式打开,false = 不是。
说明:
检查句柄是否按可写模式打开
12.2.15 iscontender:对象是否多次打开
s32 iscontender(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被测试句柄
返回值:
true = 多次打开,false = 单次。
说明:
检查句柄对应的文件是否被多次打开,每次打开都会创建一个handle,检查该对象的handle链表即可知是否多次打开。
12.2.16 OBJ_LinkHandle:关联对象和文件句柄
s32 OBJ_LinkHandle(struct Objecthandle *hdl, struct Object *obj);
头文件:
os.h
参数:
hdl:被关联的文件句柄。
obj:被关联的对象。
返回值:
成功(0);失败(-1);
说明:
对象访问必须通过文件句柄,只有关联了文件句柄,对象才能够被读、写、控制,句柄控制结构保存了对象访问的上下文。
12.2.17 Handle_New:新建对象句柄
struct Objecthandle *Handle_New(void);
头文件:
os.h
参数:无
返回值:
true = handle按追加模式打开,false = 不是。
说明:
新建的句柄,并没有跟具体对象关联,还需要调用OBJ_LinkHandle与相关对象关联起来。
12.2.18 Handle_Delete:删除句柄
s32 Handle_Delete(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被删除的句柄
返回值:
成功(0);失败(-1)。
说明:
删除句柄,并回收内存。
12.2.19 Handle_Init:初始化句柄
void Handle_Init(struct Objecthandle *hdl, struct Object *obj, u32 flags, ptu32_t context);
头文件:
os.h
参数:
hdl:被初始化的句柄。
obj:宿主文件对象
flags:句柄打开方式
context:文件访问上下文
返回值:无
说明:略。
12.2.20 Handle_GetName:取宿主对象名
const char *Handle_GetName(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被操作的句柄。
返回值:
对象名,可能是NULL。
说明:略。
12.2.21 Handle_GetContext:取上下文
ptu32_t Handle_GetContext(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被操作的句柄。
返回值:
目标对象的访问上下文。
说明:
一个对象,打开的每个句柄,都有独立的上下文。
12.2.22 Handle_SetContext:设置上下文
void Handle_SetContext(struct Objecthandle *hdl, ptu32_t context);
头文件:
os.h
参数:
hdl:被操作的句柄。
context:新的上下文,将替掉旧的上下文。
返回值:
无。
说明:
一个对象,打开的每个句柄,都有独立的上下文。
12.2.23 Handle_GetHostObjectPrivate:宿主对象私有数据
ptu32_t Handle_GetHostObjectPrivate(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:被操作的句柄。
返回值:
宿主对象的私有数据。
说明:略。
12.2.24 Handle_GetTimeOut:取同步IO超时时间
u32 Handle_GetTimeOut(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:对象句柄
返回值:
同步超时时间。
说明:
同步IO将导致调用者阻塞,djyos凡是会阻塞的系统调用,都有设置超时时间的机制,但无论是C库,还是posix标准,均没有传递超时时间的参数。djyos在句柄控制设置了一个属性,用于保存超时时间。
12.2.25 Handle_SetTimeOut:设置同步IO超时时间
void Handle_SetTimeOut(struct Objecthandle *hdl, u32 timeout);
头文件:
os.h
参数:
hdl:对象句柄
timeout:新的超时时间,单位uS。
返回值:
无
说明:
同步IO将导致调用者阻塞,djyos凡是会阻塞的系统调用,都有设置超时时间的机制,但无论是C库,还是posix标准,均没有传递超时时间的参数。djyos在句柄控制设置了一个属性,用于保存超时时间。
fcntl函数的F_SETTIMEOUT命令,与本函数功能相同。
12.2.26 Handle_GetMode:获取句柄打开模式
u32 Handle_GetMode(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:对象句柄
返回值:
对象打开模式,如O_RDONLY等标识,在fcntl.h中定义。
说明:略
12.2.27 Handle_SetMode:设置句柄打开模式
void Handle_SetMode(struct Objecthandle *hdl, u32 mode);
头文件:
os.h
参数:
hdl:对象句柄
mode:新的模式,如O_RDONLY等标识,在fcntl.h中定义。
返回值:
无
说明:略
12.2.28 Handle_GetHostObj:取宿主对象
struct Object *Handle_GetHostObj(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:对象句柄
返回值:
句柄的宿主对象
说明:略
12.2.29 Handle_Multievents:取多路复用状态
u32 Handle_Multievents(struct Objecthandle *hdl);
头文件:
os.h
参数:
hdl:对象句柄
返回值:
句柄的多路复用状态,例如可读、可写等
说明:略
12.2.30 Handle_SetMultiplexEvent:设置多路复用状态
void Handle_SetMultiplexEvent(struct Objecthandle *hdl, u32 events);
头文件:
os.h
参数:
hdl:对象句柄
events:触发事件,参考CN_MULTIPLEX_STATUSMSK
返回值:
无
说明:
多路复用功能,参见第21章。
12.2.31 Handle_ClrMultiplexEvent
void Handle_ClrMultiplexEvent(struct Objecthandle *hdl, u32 events);
头文件:
os.h
参数:
hdl:被触发的句柄。
events:被清除的触发事件,参考CN_MULTIPLEX_STATUSMSK
返回值:
无
说明:
多路复用功能,参见第21章。
12.2.32 issocketactive:是否多路复用已触发
s32 issocketactive(s32 Fd, s32 mode);
头文件:
os.h
参数:
Fd:目标对象句柄。
mode:mode,所关注的状态,可读、可写、或者其他,参考CN_MULTIPLEX_STATUSMSK
返回值:
1=actived,0=unactive
说明:
这是一个为 select 函数准备的基础函数,为了兼容一些开源软件而设,编程时不建议使用select函数,应该使用更高效更便捷的 Multiplex_Wait 系列函数。
12.3 缓冲文件系统
ansi c标准库规定了一套缓冲文件系统接口,如fopen、fwrite、fread等,它是在用户态建立文件缓冲区。缓冲文件系统的数据结构定义如下:
struct LibcFile { u8 *buf; // buffer,根据配置用时或fopen时动态分配 u8 *current; // 当前指针位置 s32 bufsize; // 读写buf的尺寸 s32 count; // buf中数据量,不包含ungetc buf s32 wrt_start; // buf中被修改的起始地址(含),每次buf初始化时为-1, // 其后首次写入时修改 s32 wrt_end; // buf中被修改的结束地址(不含) s32 flags; // 文件访问状态,参见stdio.h中的 FP_IOREAD 及相关定义 s32 fd; // return from open s32 ungetbuf; // 用于ungetc函数的buf,仅1字符。 off_t FilePos; // 文件当前读写位置,与fd中的位置不一定相同。 char *tmpfname; // 如果是临时文件,指向文件名,否则NULL // 关闭文件时,非NULL则同时删除文件。 } ; typedef struct LibcFile FILE; //C库要求的FILE类型定义,在stdio.h中
|
数据结构和缓冲区关系如下:
FILE结构各成员含义:
buf:用户态缓冲区,尺寸由stdio.h文件中FILEBUFSIZ定义,在单进程模式下,由于不存在内核态和用户态之间copy数据的效率问题,因此并不分配该缓冲区,所有的fwrite和fread将直接调用write和read;如果文件是不可读的流式文件,例如只写的uart文件,也不分配buf。
该buf一般与FILE结构一起分配,以节省调用malloc的次数,提高效率。
current:当前文件指针位置FilePos在buf中的位置指针。
bufsize:如其名。
count:当前缓冲区中剩余数据量。
wrt_start:当前缓冲区中被fwrite改写的数据区起始偏移。
wrt_end:当前缓冲区中被fwrite改写的数据区结束偏移(不含)。
flags:文件访问标志,由stdio.h中FP_IOREAD等常量定义。
fd:调用open时的返回值。
ungetbuf:保存ungetc函数退回的字符,如果连续调用ungetc,只有第一次有效。
FilePOS:文件指针位置,相对于文件开始。注意,本指针与文件描述符内的文件指针并不等同。
tmpfname:临时文件的文件名。创建临时文件时,系统随机生成的文件名保存在此,关闭文件时,将删除文件。
12.3.1 文件位置指针
由于用户态buf,使得文件系统有两个文件指针,且还不一致。
由于fread间接调用了read,引出了文件指针位置的变化变得非常有趣。
假设一个文件,里面保存的内容是text[10000],那么,执行:
fread(buf1, 10, 1, fp);
read(fp->fd, buf2,10);
fread(buf3, 10, 1, fp);
fseek(fp, 10, SEEK_SET);
read(fp->fd, buf4,10);
执行这段程序之后,三次读出的内容,究竟是什么?文件指针究竟指向哪里呢?
buf1好说,就是text[0];
buf2等于text[FILEBUFSIZ],有点令人费解吧?
buf3等于text[10],怎么回头了?
buf4还是等于text[10]。
其实,只要理解了ansi c库是djyfs文件系统核心之上的一个应用,就容易理解以上区别了。缓冲文件系统的fread函数执行时,为了减少调用read的次数,会一次填满整个buf,也就是调用:
read(fp->fd, fp->buf, FILEBUFSIZ);
会把djyfs的文件指针偏移到FILEBUFSIZ的位置,后续的read调用,自然就读到text[FILEBUFSIZ]的内容了。而后续的fread调用,则在用户态的buf中,继续读取。
这种不一致实际上不会对应用带来影响,因为fopen打开的文件,人们只看到缓冲文件系统的文件指针;按POSIX接口打开的文件,则只有read/write能够访问。上述例子只是为了说明问题,事实上程序员是不能直接对fp->fd操作的,fp的定义,理论上对用户是“隐藏”的,不能直接访问,也就无法对其执行read/write了。但有例外,就是stdin/out/err三个,POSIX规定了其值是0/1/2,不需要访问stdin->fd,也知道其值是0,可以直接访问。
12.3.2 Ungetc的实现
ungetc函数退回的字符,并不退回到buf中,而是专门设置了一个变量fp->ungetbuf保存,ungetbuf == EOF则代表buf中没有有效字符。每次读取文件时,先检查ungetbuf。
注意,这是ansi c库的内容,与djyfs核心无关,不影响read函数。
读取、移动文件指针,都会使ungetbuf == EOF。
12.4 缓冲文件系统API
请参考C库说明,也就是C语言相关教科书。
12.5 标准IO
DJYOS支持C库要求的printf族和scanf族函数,函数定义和C库一致,参考c库函数手册即可。
虽然函数定义是一致的,但是DJYOS在实现上,针对嵌入式应用,做了重大改进,且听我慢慢道来。
12.5.1 配置标准IO
printf族函数的输出目标是stdout,scanf族函数的数据来源是stdin,出错信息的输出目标是stderr。嵌入式环境不像桌面系统,输入和输出设备五花八门,就需要配置。djyos的标准开发环境DIDE提供了配置标准IO的功能。
例如,要设置“uart0”为shell终端,配置方法如下:
图 12‑1 DIDE中配置stdio
有些设备,shell命令可能从不同的设备输入,就需要勾选“支持多输入设备”框。
跟随模式的含义:如果stdout设置为跟随stdin,则stdout总是跟最后一次收到shell命令的stdin设备相同。console(shell)通过这个特性,实现了“从哪里接收到的shell命令,结果便输出到哪里”的特性。
stderr也是如此。
如果没有勾选“持多输入设备”,“标准输入设备名”配置的设备,就是唯一的输入设备;如果勾选了,这里只是配置了一个初始设备,系统运行中,还可以调用 add2listenset函数,把更多设备加入输入设备表中。
设备名除了设备文件外,还允许指定磁盘文件。
图 12‑1中设定了uart作为标准IO设备,还要配置好uart,与电脑匹配。
图 12‑2 DIDE中配置uart
Uart驱动默认设置为 波特率115200,无效验,8位数据位,一位停止位,无硬件流控。
在电脑端,打开超级终端,按照“uart0”的参数进行设置,设置成“115200,N,8,1”,无硬件流控模式即可。
12.5.2 可裁减性改进
传统的printf和scanf系列函数,需要有文件系统和设备文件系统支持才可以使用,stdin/stout/stderr都是(FILE*),而嵌入式系统,很多情况下会使用串口作为输入输出端口,且会把文件系统和设备文件系统裁减掉。
而DJYOS的标准IO系统,在裁掉文件系统和设备管理系统后,仍然可以调用printf和scanf系列函数(当然,fprintf和fscanf不能用)。在没有设备管理系统和文件系统时,printf和scanf将按非阻塞模式工作,裁掉磁盘文件系统但保留设备文件系统时,可以按阻塞模式工作。
12.5.3 printf的改进
传统prinf中调用了动态分配内存,而动态分配内存在执行时间和执行结果上,都有不确定性,且还可能会被阻塞。非常不适合在嵌入式环境下使用,特别是有高可靠要求的工业嵌入式控制系统。
DJYOS重新实现了printf函数,不使用动态分配,而是使用一个64字节的临时缓冲区反复传送。
12.5.4 ISR中使用printf
传统printf是不允许在中断ISR中调用的,而DJYOS是允许的,但有些注意事项。
如果stdout指向uart设备,那么,uart设备驱动有一定的缓冲区,缓冲区塞满后,就不能继续接收数据了,后续的printf会被要求阻塞。但是,ISR又不允许阻塞,因此,每次ISR中,调用printf打印的总字符数,超出uart缓冲区长度的部分,将会丢失。还有一个坏消息,进入ISR前,如果uart缓冲区已经有部分数据,ISR中可打印的字符数,会进一步减少,极端情况是一个字符都打印不出来。好消息是,只是打印不出来而已,不会有其他副作用。
第13章 设备文件系统
Djyos设备驱动模型,是从原来的泛设备模块修改而来。虽然从系统架构上讲,泛设备组件作为功能模块之间互相交互接口,有利于设计移植性强的模块,有利于企业处理历史包袱。但泛设备的概念与普通系统差异太大,许多用户感觉左右手接口概念比较拗口。DJYOS崇尚功能全面而又洁易用,不希望拥有难于理解的组件,故而放弃泛设备模块,回归传统的设备驱动架构。
DJYOS系统中,设备驱动模型赋予了广泛的含义,它是被设计成功能模块间互相访问的接口。功能模块可能是硬件,也可能是软件,还可能是软硬件结合的模块,驱动程序不再仅仅是访问硬件的接口。从软硬件联合设计的角度,DJYOS系统并不区分软件模块还是硬件模块,如果完整产品由多个模块组成,任意一个模块在别的模块“眼里”都可以以设备的形式出现的,使用设备的模块并不知道该设备的实现细节,也不知道该设备是由硬件组成的还是由纯软件组成的。某一个功能由软件还是硬件实现并不重要,关键是,它具备需要的功能,并且为别的模块提供了相同的访问和操作接口。
DJYOS不鼓励用户编写设备驱动程序时,不管自身特点,生搬硬套地往操作系统的设备驱动模型上套。操作系统的驱动架构,仅仅是给你提供了一个有一定通用性的模型,但不确保适合于所有的硬件模块,你觉得方便就用,不方便就不用。随DJYOS发行的驱动程序,也有相当部分没有按照Djyos的驱动模型设计,而是根据模块本身特性,为不同类型模块设计不同结构的驱动程序。
1. IIC总线驱动、SPI总线等驱动等,这些驱动都独立于设备驱动模型之外,仅直接描述硬件系统的实际连接。
2. djyfs涉及的存储设备驱动,djygui涉及的显卡驱动,协议栈涉及的网卡驱动,timer模块涉及的定时器驱动,wdt模块涉及的看门狗驱动,日历时钟模块涉及的rtc驱动,都没有套用驱动模型。
Djyos的设备驱动模型,不再区分字符设备、块设备、网络设备。大容量存储设备有其专用的存储介质接口,网络驱动也有其专门的网卡接口,不再与设备驱动模型发生关联。
文件系统本是一种数据组织和读写的软件方案,所涉及的存储器驱动为这个特定方案服务,它规定了特定的接口协议,按这个协议编写具体的存储介质驱动就可以了,并不需要套用Djyos的设备驱动模型。Djyos中没有块设备驱动的概念,文件系统也没有规定存储介质必须按块访问,例如Djyos提供的DFFSD(Flash file system)模块,是按块组织Flash的,而DEFSD(Easy norFlash File system)模块,就没有按块方式组织存储介质。文件和设备不再使用统一的接口函数,文件将不能用open、write和read访问,而是用fopen、fwrite、fread访问。
同样,网络系统的socket接口,也是专用接口,无须硬套设备架构的通用接口,socket的句柄,也是独立的,跟设备驱动架构没有联系。网卡驱动也是按照网络协议栈提出的标准驱动接口进行编程。
除此以外,图形卡的驱动程序,也是djygui自行规定的接口标准。
DJYOS中,设备是依托对象系统实现的,在根对象的子对象中有一个名为“dev”的对象,所有设备均是这个对象的子孙对象。
13.1 编写设备驱动
参见DIDE中的BSP向导。
13.2 使用设备
djyos环境下,程序员使用设备的方式有两种:
1、 使用device模块提供的Device_Open/Device_Close/Device_Read/Device_Write/DevCntl函数。
用这套接口操作设备,速度更快,控制更加精细,C库和posix库没有提供的timeout参数,这套接口提供了。
2、 用fopen/fread/fwrite/fcntl/open/read/write/等
djyos的设备体系基于对象系统构建,跟文件相关的操作,自然都是支持的。
设备文件是驱动程序用Device_Create函数创建的,故无论是Device_Open还是fopen或者是open函数,都只能打开已经存在的设备文件,不能创建文件。
13.3 API说明
13.3.1 Device_Create:创建设备
struct Object *Device_Create(const char *name, fnDevOpen dopen, fnDevClose dclose,
fnDevWrite dwrite, fnDevRead dread, fnDevCntl dcntl,
ptu32_t DrvTag);
头文件:
device.h
参数:
name:新添加设备名称,注意该指针指向的字符串不能是局部变量,不能为NULL,不能包含“\”等字符;
dopen/ dclose/ dwrite/ dread/dcntl:设备操作函数指针组。
DrvTag:设备驱动私有标签,提供给驱动程序使用。也可以在创建设备后,用Device_SetDrvTag函数设置。还可以用fcntl和ioctl函数,在fcntl.h文件中定义了F_SETDRVTAG命令码。
返回值:
成功添加返回该设备指针;否则返回NULL。
说明:
本函数时提供给BSP开发人员用的,他们实现dopen/ dclose/ dwrite/ dread/dcntl这几个函数后,调用Device_Create创建设备,即完成
13.3.2 Device_Delete:删除设备
s32 Device_Delete(struct Object *dev)头文件:
device.h
参数:
dev:待释放设备的指针。
返回值:
成功(0);失败(-1)
说明:
将设备从设备树上删除,并释放该设备的控制块所占内存,使用中的设备时不能删除的。
13.3.3 Device_DeleteByName:删除设备
s32 Device_DeleteByName (const char *name);
头文件:
device.h
参数:
name:待释放设备的名字。
返回值:
成功(0);失败(-1)
说明:
将设备从设备树上删除,并释放该设备的控制块所占内存,使用中的设备时不能删除的。
13.3.4 Device_Open:打开设备
s32 Device_Open(const char *name, s32 flags,u32 timeout);
头文件:
device.h
参数:
name:设备完整路径名
flags:设备打开模式,O_RDONLY等,在fcntl.h中定义。
timeout:参见第5.7.1节。
返回值:
成功打开设备则返回该设备描述符,失败则返回false。
说明:
打开设备。在timeout时间内,通过设备的完整路径名检索设备并返回设备句柄。打开设备也可以用fopen/open函数,但这两个函数没有timeout参数,相当于timeout == CN_TIMEOUT_FOREVER。
本函数会调用驱动程序提供的dopen函数,由调用Device_Create函数时dopen参数传入。
13.3.5 Device_Close:关闭设备
s32 Device_Close(s32 fd);
头文件:
device.h
参数:
fd:设备文件描述符。
返回值:
成功(0);失败(-1)。
说明:
本函数会调用驱动程序提供的dclose函数(如有),由调用Device_Create函数时dclose参数传入。
13.3.6 Device_Read:读设备
s32 Device_Read(s32 fd, void *buf, u32 len, u32 offset, u32 timeout);
头文件:
device.h
参数:
fd:设备文件描述符。
buf:接受数据的缓冲区;
len:读取数据的长度;
offset:读取内容在设备数据区的位置,对于流设备,例如串口,其位置为零;
timeout:参见第5.7.1节。
返回值:
-1表示参数检查出错,其他值为实际读取的数据长度。
说明:
本函数会调用驱动程序提供的dread函数,由调用Device_Create函数时dread参数传入。
13.3.7 Device_Write:写设备
s32 Device_Write(s32 fd, void *buf, u32 len, u32 offset, u32 timeout);
头文件:
device.h
参数:
fd:设备文件描述符。
buf:提供数据的缓冲区;
len:写数据的长度;
offset:写入的内容在设备数据区的位置对于流设备,例如串口,其位置为零;
timeout:参见第5.7.1节。
返回值:
-1表示参数检查出错,其他值为实际写入的数据长度。
说明:
本函数会调用驱动程序提供的dwrite函数,由调用Device_Create函数时dwrite参数传入。
13.3.8 fcntl/ioctl:设备控制
s32 fcntl(s32 fd,s32 cmd,...);
s32 ioctl(s32 fd, s32 cmd, ...);————新的posix标准已经废弃,慎用
头文件:
fcntl函数是fcntl.h;ioctl 是stropts.h
参数:
fd:设备文件描述符。
cmd:控制命令号。
data1:控制命令参数;
data2:控制命令参数。
返回值:
成功调用设备驱动程序提供的控制设备函数,则返回该函数的返回值,含义请参考相应设备的使用手册。否则返回-1。
说明:
本函数会调用驱动程序提供的dcntl函数,由调用Device_Create函数时dcntl参数传入。
cmd 命令用宏定义,对于公共的命令,fcntl函数的在fcntl.h中定义;ioctl函数的在stropts.h中定义。各具体文件系统,又会在本模块内定义一些私有控制命令,如uartctrl.h中,有如下定义:
//串口设备控制命令常数,用于uart_ctrl函数。
#define CN_UART_SET_BAUD (F_DEV_USER+0 ) //设置uartBaud.
#define CN_UART_COM_SET (F_DEV_USER+1 ) //设置串口参数
#define CN_UART_HALF_DUPLEX_SEND (F_DEV_USER+2 ) //发送数据
…………
13.3.9 Device_GetName:取设备名
const char *Device_GetName(struct Object *devo);
头文件:
device.h
参数:
devo:设备文件对象指针。
返回值:
字符串指针。
说明:略
13.3.10 Device_GetDrvTag:取设备驱动标签
ptu32_t Device_GetDrvTag(s32 fd);
头文件:
device.h
参数:
fd:设备文件描述符。
返回值:
设备驱动标签
说明:
设备驱动程序才会用到它。
13.3.11 Device_SetDrvTag:设置设备驱动标签
void Device_SetDrvTag(s32 fd,ptu32_t DrvTag);
头文件:
device.h
参数:
fd:设备文件描述符。
DrvTag:新的标签
返回值:
无
说明:
设备驱动程序才会用到它。
13.3.12 Device_GetUserTag:取设备用户标签
ptu32_t Device_GetUserTag(s32 fd;
头文件:
device.h
参数:
fd:设备文件描述符。
返回值:
设备驱动标签
说明:略
13.3.13 Device_SetUserTag:设置设备用户标签
void Device_SetUserTag(s32 fd,ptu32_t UserTag);
头文件:
device.h
参数:
fd:设备文件描述符。
UserTag:新的标签
返回值:
无
说明:略
第14章 缓冲区
14.1 环形缓冲区
环形缓冲区,是一种类FIFO缓存机制,用于模块间的数据通信。下图显示了环形缓冲区的结构,有如下特点:
图14‑1环形缓冲区示意图
1.缓冲区数据结构只定义了一个字节池指针,即缓冲区空间是静态创建的(由用户提供和维护),初始化以后,这个指针指向实际的缓冲空间首地址。
2. offset_write和offset_read记录的是下一个读(写)位置,是从缓冲区首地址开始的偏移量。
3.len记录了缓冲区中的数据量。从offset_write和offset_read也可以获得len参数,但是这样的话,当offset_write和offset_read相等时,就无法判断是缓冲区满还是缓冲区空。使用len参数也可以加速缓冲区操作。
DJYOS提供了一系列函数用于管理环形缓冲区,缓冲区的操作做了原子保护,因此执行以下并发操作是安全的:
1.A线程读——B线程写;
2.A线程读——异步信号中断ISR写;
3.A线程写——异步信号中断ISR读。
而以下并发操作的结果是不可预料的:
1.A线程读——B线程读;
2.A线程写——B线程写;
3.A线程读——异步信号中断ISR读;
4.A线程写——异步信号中断ISR写。
14.1.1 API说明
14.1.1.1 Ring_Init:创建环形缓冲
void Ring_Init(struct ring_buf *ring, u8 *buf, u32 len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
buf:由用户提供的缓冲区空间。
len:缓冲区长度,以字节计。
返回值:
无。
说明:
初始化一个环形缓冲区,使用这个函数之前,用户应该定义缓冲区内存块(buf)和缓冲区数据结构(ring)。
14.1.1.2 Ring_Create:创建环形缓冲区
voidRing_Init(u32 len)
头文件:
os.h
参数:
len:缓冲区长度,以字节计。
返回值:
无。
说明:
创建环形缓冲区并初始化,与Ring_Init不同,本函数用malloc从heap中分配内存。
14.1.1.3 Ring_Destroy:销毁环形缓冲区
s32 Ring_Destroy(struct RingBuf *pRing);
头文件:
os.h
参数:
pRing:缓冲区指针。
返回值:
-1 == 失败;0 == 成功。
说明:略
14.1.1.4 Ring_ Capacity:获取缓冲容量
u32Ring_Capacity(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
缓冲区尺寸,就是调用Ring_Init时使用的len参数。
说明:
获取环形缓冲区的总容量。
14.1.1.5 Ring_GetBuf:获取缓冲地址
u8 *Ring_GetBuf(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
环形缓冲区的存储区域首地址,就是调用Ring_Init的buf参数。
说明:
获取环形缓冲区的存储区域首地址,这个地址是用户调用Ring_Init时用户设定的地址。
14.1.1.6 Ring_Write:写缓冲区
u32 ring_write(struct ring_buf *ring,u8 *buffer,u32 len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
buffer:待写入的数据指针。
len:待写入的数据长度.单位是字节数。
返回值:
实际写入的字节数。
说明:
将buffer中的len大小的数据写入到环形缓冲区ring。注意,写入到环形缓冲区的数据大小,受到缓冲区剩余尺寸的限制,实际写入的数据大小参见函数返回值。
14.1.1.7 Ring_Read:读缓冲区
u32 Ring_Read(struct ring_buf *ring,u8 *buffer,u32 len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
buffer:接收数据的缓冲区指针。
len:待读出的数据长度.单位是字节数。
返回值:
实际读出的字节数。
说明:
从环形缓冲区ring获取len长度的数据到buffer。注意,获取到数据的大小受到环形缓冲区实际数据大小的限制,实际获取到的数据参见函数返回值。
14.1.1.8 Ring_Check:检查缓冲数据量
u32 Ring_Check(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
环形缓冲区已缓存的数据量。
说明:
获取环形缓冲区中已缓存数据量,以字节为单位。
14.1.1.9 Ring_CheckFree:检查空闲空间
u32 Ring_CheckFree (struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
环形缓冲区空闲空间尺寸
说明:略
14.1.1.10 Ring_IsEmpty:缓冲是否空
bool_t Ring_IsEmpty(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
环形缓冲区为空返回true;反之则为false。
说明:
检查指定的环形缓冲区是否为空。
14.1.1.11 Ring_IsFull:缓冲是否满
bool_t Ring_IsFull(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
环形缓冲区已满返回true;反之则为false。
说明:
检查指定的环形缓冲区是否已满。
14.1.1.12 Ring_Flush:缓冲区重置
void Ring_Flush(struct ring_buf *ring)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
返回值:
无。
说明:
重置环形缓冲区,即将缓冲区中的数据清零。
14.1.1.13 Ring_PseudoRead:伪读数据
u32 Ring_PseudoRead (struct ring_buf *ring,u32 len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
len:伪读数据的大小。
返回值:
实际伪读的数据的大小。
说明:
与Ring_Read函数类似,但并不实际读出数据,只是移动了读指针,等效为把数据读出来扔掉了。
14.1.1.14 Ring_RecedeRead:恢复数据
u32 Ring_RecedeRead(struct ring_buf *ring,u32 len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
len:需恢复数据的大小。
返回值:
实际可恢复数据的大小。
说明:
将环形缓冲区中最近读取的len长度的数据,恢复为未读取,用户通过再次调用Ring_Read函数可以再次读取该段数据。但要注意,可重读数据的大小,受环形缓冲区写行为的影响。因为缓冲区是一个环形,之前被读取的数据区可能会被最近写入的数据覆盖,导致部分乃至全部数据无法恢复。所以实际可恢复数据的大小需参照函数的返回值。
14.1.1.15 Ring_SkipTail:擦除“新”数据
u32 Ring_SkipTail(struct ring_buf *ring,u32 size)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
size:需擦除的新数据大小。
返回值:
实际可擦除的新数据大小。
说明:
擦除环形缓冲区len长度的新数据。所谓“新”数据,是指环形缓冲区缓存的数据中,最近被写入的数据,即offset_write所指向的区域中的数据,而大家知道,offset_wrtie指向的永远是环形缓冲区中最新的数据。但要注意,可删除的数据大小,受环形缓冲区读行为的影响。因为缓冲区是一个环形,最新写入的数据可能会被最近读操作给读走,导致部分乃至全部新数据已无法删除(因为已被读走)。所以实际可删除的新数据的大小需参照函数的返回值。
14.1.1.16 Ring_SearchCh:检索字符
u32 Ring_SearchCh(struct ring_buf *ring, char c)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
c:需查找的字符。
返回值:
查找成功,返回字符C首次出现在环形缓冲区中的相对于最旧数据的偏移量;查找失败则返回CN_LIMIT_UINT32。
说明:
从环形缓冲区最旧的数据(offset_read所指的数据)开始查找字符C,查找比对成功,则返回该字符相对于最旧数据且首次出现的位置。
14.1.1.17 Ring_SearchStr:检索字符串
u32 Ring_SearchStr(struct ring_buf *ring, char *string,u32 str_len)
头文件:
os.h
参数:
ring:环形缓冲区控制块指针。
string:需查找的字符序列,并非’\0’结束的字符串。
str_len:字符序列长度。
返回值:
查找成功,返回字符序列string首次出现相对最旧数据的偏移量;失败,则返回CN_LIMIT_UINT32。
说明:
从环形缓冲区最旧的数据(offset_read所指的数据)开始查找字符序列string,查找比对成功,则返回该字符相对于最旧数据且首次出现的位置。
14.2 线性缓冲区
线性缓冲区也是一种类似于FIFO的缓冲区,它实质上是环形缓冲区的简化版。它与环形缓冲区的区别在于数据不发生环绕,它的读指针总是在缓冲区的起始地址。在实际应用中,经常会有这样的需求,用一个缓冲区收集数据,数据量积累到一定数量或者超时时间到后,再一次处理完所有数据。对于这种应用,使用环形缓冲区虽然也可以满足要求,但环形缓冲区有数据环绕的问题,环绕就是指针到达缓冲区末地址后又从缓冲区首地址的过程。读写环形缓冲区都需要判断数据是否发生环绕,这是很消耗时间的。而使用线性缓冲区则没有这个问题,速度比环形缓冲区快很多。
线性缓冲区每次读都要求完全拷贝缓冲区中所有数据,然后把读指针清零,不支持读任意长度数据,读和写不能异步进行。如果允许不完全读取,读函数把剩余的数据拷贝到缓冲区头部,是一种更安全更通用的做法,但是,这就违背了建立线性缓冲区的初衷。线性缓冲区就是作为一个快速缓冲区来设计的,它省略了一切会影响效率的繁文缛节,拷贝剩余数据的过程将使线性缓冲区的速度比缓存缓冲区还慢,如果需要完全功能的缓冲区,使用环形缓冲区更加理想。
表格14‑1环形缓冲区与线性缓冲区特性比较
项目 |
环形缓冲区 |
线性缓冲区 |
读写方式 |
先进先出 |
先进后出 |
读/写速度 |
慢 |
快 |
同步读写 |
支持 |
不支持 |
任意长度读 |
支持 |
必须一次读完所有数据 |
任意长度写 |
支持 |
支持 |
14.2.1 API说明
14.2.1.1 Line_Init:创建线性缓冲
void Line_Init(struct line_buf *line, u8 *buf, u32 len)
头文件:
os.h
参数:
line:线性缓冲区控制块指针,线性缓冲区控制块由用户提供。
buf:缓冲区指针,缓冲区由用户提供。
len:,缓冲区容量,单位是字节数。
返回值:
无。
说明:
建立线性缓冲区并初始化。注意,线性缓冲区控制块和缓存区都由用户设定。
4.2.1.2 Line_Capacity:获取缓冲容量
u32 Line_Capacity(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
缓冲区容量。
说明:
获取线性缓存区的容量。
14.2.1.3 Line_SkipTail:擦除“新”数据
u32 line_skip_tail(struct line_buf *line,u32 len)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
len:需删除的新数据大小。
返回值:
实际删除的新数据大小。
说明:
删除线性缓冲区中len大小的新数据。所谓“新”数据是以最近一次的写入的数据为基准,离基准越近的数据,则越新。当需删除的数据大小大于缓冲区中已缓存的数据大小时,则以已缓存的数据大小为准。实际删除了的数据量参照函数返回值。
14.2.1.4 Line_Write:写缓冲区
u32 Line_Write(struct line_buf *line,u8 *buffer,u32 len)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
buffer:待写入数据的指针。
len:待写入数据的长度,单位是字节数。
返回值:
实际写入到线性缓冲区的数据长度。
说明:
将buffer中len长度的数据写入到线性缓冲区。如果线性缓冲区的剩余空间不足,则参照函数返回值来获取实际写入的数据长度。
14.2.1.5 Line_Read:读缓冲区
u32 Line_Read(struct line_buf *line,u8 *buffer)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
buffer:接收数据空间。
返回值:
从线性缓冲区中读出的数据量。
说明:
将线性缓冲区中的数据全部读出。
14.2.1.6 Line_GetBuf:获取缓冲地址
u8 *Line_GetBuf(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
线性缓冲区的缓存空间指针。
说明:
获取线性缓冲区的缓存数据空间的指针。
14.2.1.7 Line_Check:检查缓冲使用量
u32 Line_Check(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
线性缓冲区中已缓存的数据量,单位是字节。
说明:
获取线性缓冲区中的已缓存的数据量。
14.2.1.8 Line_IsEmpty:缓冲是否空
bool_t Line_IsEmpty(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
线性缓存区为空,返回true;否则返回false。
说明:
检查线性缓冲区是否为空。
14.2.1.9 Line_IsFull:缓冲是否满
bool_t Line_IsFull(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
线性缓存区已满,返回true;否则返回false。
说明:
检查线性缓冲区是否已满。
14.2.1.10 Line_Flush:缓冲区重置
void Line_Flush(struct line_buf *line)
头文件:
os.h
参数:
line:线性缓冲区控制块指针。
返回值:
无。
说明:
重置线性缓冲区。
14.2.1.11 Line_SearchCh:检索字符
u32 Line_SearchCh(struct line_buf *ring, char c)
头文件:
os.h
参数:
line:线性缓冲区控制块指针;
c:需查找的字符。
返回值:
查找成功,返回C相对于缓存数据底部的偏置;否则返回CN_LIMIT_UINT32。
说明:
从线性缓冲区中从缓存数据底部开始查找字符,成功检索到该字符后返回该字符相对于缓存数据底部且首次出现的偏置。
14.2.1.12 Line_SearchStr:检索字符串
u32Line_SearchStr(struct line_buf *line, char *string,u32 str_len)
头文件:
os.h
参数:
line:线性缓冲区控制块指针;
string:需查找的字符序列,并非’\0’结束的字符串;
str_len:字符序列长度。
返回值:
查找成功,返回字符序列string相对于缓存数据底部的偏置;否则返回CN_LIMIT_UINT32。
说明:
从线性缓冲区中从缓存数据底部开始查找字符序列string,成功比对到该字符序列后,返回该字符序列相对于缓存数据底部且首次出现的偏置。
第15章 低功耗管理
15.1 功耗分级
DJYOS采用了功耗分级低功耗管理策略,根据运行状态和资源消耗决定进入和退出的低功耗级别。
低功耗管理模块将低功耗从低到高分组为L0 ~ L4 ,L0的低功耗级别最低,L4的低功耗级别最高。在实现DJYOS低功耗管理模块时,可根据CPU的低功耗硬件资源,决定采用其中部分或全部功耗分级。
图15‑1低功耗管理模块
用户通过低功耗模块提供的API函数配置进入的低功耗级别,如所示,DJYOS低功耗管理模块通过系统的IDLE线程进入到低功耗状态,根据进入的低功耗的级别不同,退出的方式有差异,如表格所示列出各功耗级别的特点。
功耗级别 |
特点 |
L0 |
idle事件执行时,进入最浅的休眠,一旦有中断(典型是tick),立即唤醒继续执行。 |
L1 |
idle事件执行时,进入深度睡眠,保持所有内存,时钟正常运行,一旦有中断(典型是tick),立即唤醒继续执行,唤醒所需时间将长于L1。 |
L2 |
idle事件执行时,进入深度睡眠,保持内存,时钟停止,仅中断(一般是外部中断或者设备中断)能唤醒CPU。唤醒后,处理中断,然后继续运行,直到下一次进入低功耗状态。 |
L3 |
idle事件执行时,进入深度睡眠,内存掉电,时钟停止,把CPU运行状态保存到栈中,调用回调函数,把全部内存数据保存到非易失存储器中,中断唤醒CPU后,重新启动,把保存到非易失存储器中的内容恢复到RAM,然后从中断处继续运行。 |
L4 |
idle事件执行时,进入深度睡眠,内存掉电,时钟停止,调用回调函数,把关键数据保存到非易失存储器中,中断唤醒CPU后,重新启动并加载运行。 |
15.2 tickless模式
要实现低功耗,那么tickless模式是必须要讲的。
先从最简单的情况说起。
假设有一个非常简单的程序,就一个main事件,其事件处理线程如下:
s32 main(void) { s32 x = 0; while(1) { printf(“helloworld %d\n\r”,x++); DJY_EventDelay(1000000); } } |
假设tick间隔是1mS,那么,上述程序每秒将打印一次,绝大部分事件在等待。但是,虽然软件无所事事,但是,每mS一次tick中断,确是少不了的,每次都去判断一下,1秒到了没,到了则开始下一次打印。
CPU每mS起来一下,这是非常消耗电源的,能不能直接把tick间隔定为1S,不就得了?这是不可以的,系统中,要兼顾控制精度,tick间隔就不能太大。
没有办法了吗?有的,tickless就是,在tickless模式下,定时器中断时间按需设置,定时器中断不会无谓地产生,从而节省了功耗。
djyos提供了tickless模式,可以在DIDE中配置。
15.3 驱动程序
DJYOS低功耗管理模块低功耗实现依赖于具体的CPU底层硬件实现,因此,充分了解具体硬件CPU平台的低功耗相关说明是实现低功耗管理模块的基础,DJYOS低功耗管理模块访问底层驱动使用统一接口。
15.3.1 硬件初始化
CPU在进入低功耗模块前,必须对相应的低功耗模块初始化,由底层驱动实现该部分,一般函数命名为__LP_BSP_HardInit在系统时钟初始化后调用。
15.3.2 进入低功耗状态
低功耗模块会根据低功耗级别,调用由底层提供的相对应的进入低功耗的函数。进入低功耗级别L0到L4的函数如下所示。
L0:void __LP_BSP_EntrySleepL0(void)
L1:void __LP_BSP_EntrySleepL1(void)
L2:void __LP_BSP_EntrySleepL2(void)
L3:void __LP_BSP_EntrySleepL3(void)
L4:void __LP_BSP_EntrySleepL4(void)
15.3.3 保存、恢复低功耗级别
进入低功耗状态前,须将当前的低功耗级别保存到硬件或非易失性存储设备中,恢复时再读取相应的低功耗级别。
u32 __LP_BSP_GetSleepLevel(void)
bool_t __LP_BSP_SaveSleepLevel(u32 SleepLevel)
15.3.4 备份、恢复RAM
DJYOS低功耗级别L3内存已经掉电,因此,进入低功耗L3后,所有上下文和现场数据都丢失。因此,进入低功耗级别L3前,须将上下文保存到RAM,然后将RAM数据保存到非易失性存储设备,待退出低功耗状态后,将从非易失性数据恢复到RAM,即恢复到进入低功耗级别L3前的状态。
void __LP_BSP_AsmSaveReg(struct ThreadVm *running_vm,
bool_t (*SaveRamL3)(void),
void (*EntrySleepL3)(void))
bool_t __LP_BSP_RestoreRamL3(void)
bool_t __LP_BSP_SaveRamL3(void)
15.4 API 说明
15.4.1 ModuleInstall_LowPower:组件初始化
ptu32_t ModuleInstall_LowPower (u32 (*EntrySleepReCall)(u32 SleepLevel),
u32 (*ExitSleepReCall)(u32 SleepLevel))
头文件:
lowpower.h
参数:
EntrySleepReCall,进入低功耗状态前调用的回调函数。
ExitSleepRecall,离开低功耗状态时调用的回调函数
返回值:
当前的低功耗级别。
说明:
初始化低功耗组件,参数分别为进入和离开低功耗状态时调用的回调函数,由调用者以指针的形式传参。
15.4.2 LP_SetSleepLevel:设置低功耗级别
u32 LP_SetSleepLevel(u32 Level)
头文件:
lowpower.h
参数:
Level:CN_SLEEP_L0---CN_SLEEP_L4中的一个。
返回值:
当前的低功耗级别。
说明:
设置当前低功耗级别,CN_SLEEP_L0---CN_SLEEP_L4,设置了低级别后,不会立刻进入低功耗模式,而是在下次进入IDLE事件时再进入低功耗模式。
15.4.3 LP_GetSleepLevel:设置低功耗级别
u32 LP_GetSleepLevel(void)
头文件:
lowpower.h
参数:
返回值:
当前的低功耗级别。CN_SLEEP_L0---CN_SLEEP_L4中的一个。
说明:
设置当前低功耗级别,CN_SLEEP_L0---CN_SLEEP_L4,设置了低级别后,不会立刻进入低功耗模式,而是在下次进入IDLE事件时再进入低功耗模式。
15.4.4 LP_DisableSleep:禁止低功耗次数
u32 LP_DisableSleep (void)
头文件:
lowpower.h
参数:
无。
返回值:
禁止计数器当前的数值。
说明:
调用本函数后,系统不会进入L1及以上级别的低功耗状态.并且使禁止次数计数器+1,须调用同等次数的LP_EnableSleep使计数器减至0后,才允许进入L1及以上级别低功耗状态。
15.4.5 LP_EnableSleep:使能低功耗次数
u32 LP_EnableSleep(void)
头文件:
lowpower.h
参数:
无。
返回值:
禁止计数器当前的数值。
说明:
调用本函数后,禁止次数计数器减1,减至0后,允许进入L1及以上级别低功耗状态。
15.4.6 LP_SetHook:设置钩子函数
void LP_SetHook(u32 (*EntrySleepReCall)(u32 SleepLevel),\
u32 (*ExitSleepReCall)(u32 SleepLevel));
头文件:
lowpower.h
参数:
EntrySleepReCall:进入低功耗之前调用的钩子函数,可以是NULL。
ExitSleepReCall:从低功耗状态返回时调用的钩子函数,可以是NULL。
返回值:
无
说明:因从L4级低功耗返回时,因内存数据已经丢失,设置的钩子函数亦不能幸免,故对于L4级低功耗来说,设置ExitSleepReCall并无意义,不会被调用。
第16章 HMI输入
16.1 框架
DJYOS的标准输入管理模块,可以实现:
1. 键盘、二维鼠标、三维鼠标、字符终端等输入。
2.支持多鼠标、多键盘。
DJYOS的输入管理模块是系统一个独立的功能组件,适合于数据量少、允许一定延迟的人机交互数据的传输,是人机接口功能的一部分,用于连接外部人机接口硬件的输入,例如鼠标、触摸屏、键盘等。它可以连接多个鼠标、键盘等设备,如图16‑1所示。
图16‑1输入设备模型
hmi输入模块时个可选组件,DIDE中,可以配置hmi输入模块:
图16‑2配置hmi模块
16.1.1 输入设备对象
输入设备在对象系统中的目录结构如下:
\——HmiInDev
|——Keyboard
|——touch
|——mouse
16.1.2 输入消息队列
基于djyos的消息队列组件,实现了输入消息队列。
输入消息(例如鼠标点击)产生后,将通过“消息队列”传送给应用程序。从图16‑1可以看到,输入消息会进入消息队列,队列可以有多个,可以为每个输入设备单独指定消息队列,也可以多个设备对应一个消息队列。消息队列和输入设备之间的对应关系,是任意的。标准输入模块初始化时,创建了一个默认消息队列,新创建的设备,都指向这个默认消息队列。应用可以创建输入消息管道(HmiIn_CreatInputMsgQ),并给每个自己刚兴趣的hmi输入设备可以指定管道(HmiIn_SetFocus)。hmi输入设备产生的消息(如按键按下),将被送入其指定管道,若该设备无指定消息队列,则送入默认队列。应用程序可以从默认消息队列中获取消息(HmiIn_ReadDefaultMsg),也可以指定具体的消息队列(HmiIn_ReadMsg)。如果该队列中没有消息,应用程序将被阻塞,直到收到消息、或者超时。
输入消息结构
enum _STDIN_INPUT_TYPE_ { EN_HMIIN_INVALID, //非法设备 EN_HMIIN_KEYBOARD, //键盘 EN_HMIIN_MOUSE_2D, //2d鼠标 EN_HMIIN_MOUSE_3D, //3d鼠标 EN_HMIIN_SINGLE_TOUCH, //单点触摸屏 EN_HMIIN_MUTI_TOUCH, //多点触摸屏 EN_HMIIN_AREA_TOUCH, //区域触摸屏 }; //键盘消息数据结构 struct KeyBoardMsg { s64 time; //按键事件发生时间,US数 u8 key_value[2]; //键值,每个键由2部分组成 }; //2维鼠标数据结构,即目前最常用的鼠标 struct Mouse2D_Msg { s64 time; //鼠标事件发生时间,US数 //指明鼠标事件对应的屏幕,NULL表示默认显示器,如果只有一个屏也可设为NULL。 struct DisplayObj *display; s32 x,y; //表示鼠标指针位置 u16 key_no; //动作的鼠标键号 }; //3维鼠标数据结构 struct Mouse3D_Msg { s64 time; //按键事件发生时间,US数 //指明鼠标事件对应的屏幕,NULL表示默认显示器,如果只有一个屏也可设为NULL。 struct DisplayObj *display; s32 x,y,z; //表示鼠标指针位置 u16 key_no; //动作的鼠标键号 }; //单点触摸屏,即最常用的触摸屏 struct SingleTouchMsg { s64 time; //按键事件发生时间,US数 //指明触摸事件对应的屏幕,NULL表示默认显示器,如果只有一个屏也可设为NULL。 struct DisplayObj *display; s32 x,y,z; //x,y表示触摸位置,z>0标志触摸压力,0标志未触摸 }; //多点触摸屏 struct MultiTouchMsg { s64 time; //按键事件发生时间,US数 }; //区域触摸屏,表示被触摸的是一个区域。 struct AreaTouchMsg { s64 time; //按键事件发生时间,US数 }; union un_input_data { struct KeyBoardMsg key_board; struct Mouse2D_Msg mouse_2d; struct Mouse3D_Msg mouse_3d; struct SingleTouchMsg SingleTouchMsg; struct MultiTouchMsg muti_touch; struct AreaTouchMsg area_touch; }; struct InputDeviceMsg { enum _STDIN_INPUT_TYPE_ input_type; //输入消息类型, s32 device_id; //输入设备id union un_input_data input_data; }; |
16.2 API说明
16.2.1 HmiIn_InstallDevice:安装标准输入设备
s32 HmiIn_InstallDevice(const char *device_name,
enum _STDIN_INPUT_TYPE_ stdin_type,
void *myprivate);
头文件:
Hmi-input.h
参数:
device_name:输入设备名,例如“keyboard”键盘设备。
stdin_type:输入设备类型。
myprivate:输入设备私有数据结构,隐藏着设备驱动的实现信息。
返回值:
安装成功,返回标准输入设备ID;否则返回-1。
说明:
将设备device_name安装为一个标准输入设备。hmi设备写好以后,调用本函数可以把设备安装进系统。被安装的设备,可以是实体设备,也可以是虚拟设备,例如gdd_keyboard组件,就利用显示器和触摸屏做了个虚拟键盘。
16.2.2 HmiIn_UnInstallDevice:卸载输入设备
bool_t HmiIn_UnInstallDevice(const char *device_name)
头文件:
Hmi-input.h
参数:
device_name:输入设备名。
返回值:
卸载成功返回true;失败则返回false。
说明:
卸载一个标准输入设备device_name,同时释放该设备所占据的内存。
16.2.3 HmiIn_CreatInputMsgQ:创建输入消息队列
tpInputMsgQ HmiIn_CreatInputMsgQ(u32 MsgNum,const char *Name)
头文件:
Hmi-input.h
参数:
MsgNum:输入通道容量。
Name:参数保留,未使用。
返回值:
创建成功,返回输入消息队列;失败则返回NULL。
说明:
创建一个输入消息队列,可以继续调用HmiIn_SetFocus设定哪些输入设备产生的消息将送入这个队列。hmi组件初始化时,创建了一个消息队列,并设为默认消息队列。
16.2.4 HmiIn_DeleteInputMsgQ:删除输入队列
bool_t HmiIn_DeleteInputMsgQ(tpInputMsgQ InputMsgQ)
头文件:
Hmi-input.h
参数:
InputMsgQ:输入消息队列。
返回值:
删除成功返回true;失败则返回false。
说明:
删除一个输入消息队列。
16.2.5 HmiIn_SetFocus:设置输入队列
bool_t HmiIn_SetFocus(const char *device_name, tpInputMsgQ FocusMsgQ)
头文件:
Hmi-input.h
参数:
device_name:输入设备名。
FocusMsgQ:输入消息队列。
返回值:
设置成功返回true;失败则返回false。
说明:
为hmi输入设备(device_name)指定输入消息队列。
16.2.6 HmiIn_SetFocusDefault:设置默认输入消息队列
void HmiIn_SetFocusDefault(tpInputMsgQ FocusMsgQ)
头文件:
Hmi-input.h
参数:
FocusMsgQ:缺省输入消息队列。
返回值:
无。
说明:
设置hmi输入设备的默认输入消息队列,所有没有指定输入消息队列的设备,产生的消息,都将送入默认消息队列。
16.2.7 HmiIn_GetFocusDefault:查询默认输入消息队列
tpInputMsgQ HmiIn_GetFocusDefault(void)
头文件:
Hmi-input.h
参数:
无。
返回值:
标准输入模块的默认输入消息队列。
说明:略
16.2.8 HmiIn_InputMsg:输入消息
bool_t HmiIn_InputMsg (s32 stdin_id,u8 *msg_data, u32 msg_size)
头文件:
Hmi-input.h
参数:
stdin_id:输入设备ID。
msg_data:输入消息。
msg_size:输入消息的长度,字节为单位。
返回值:
消息发送成功返回true;否则返回false。
说明:
将一个消息通过输入设备发送给系统,系统将把消息送入相应设备指定的输入消息队列,若无指定,则送入默认输入消息队列。
16.2.9 HmiIn_ReadMsg:读指定队列消息
bool_t HmiIn_ReadMsg(tpInputMsgQ InputMsgQ,
struct InputDeviceMsg *MsgBuf,u32 TimeOut);
头文件:
Hmi-input.h
参数:
InputMsgQ:输入消息队列。
MsgBuf:输入消息。
TimeOut:参见第5.7.1节。
返回值:
消息接收成功返回true;否则返回false。
说明:略
16.2.10 HmiIn_ReadMsg:读默认队列消息
bool_t HmiIn_ReadDefaultMsg(struct InputDeviceMsg *MsgBuf,u32 TimeOut);
头文件:
Hmi-input.h
参数:
MsgBuf:输入消息。
TimeOut:参见第5.7.1节。
返回值:
消息接收成功返回true;否则返回false。
说明:略
16.2.11 HmiIn_CheckDevType:检查设备类型
enum _STDIN_INPUT_TYPE_ HmiIn_CheckDevType(const char *device_name);
头文件:
Hmi-input.h
参数:
MsgBuf:输入消息。
TimeOut:参见第5.7.1节。
返回值:
设备类型枚举值,确定一个hmi输入设备是鼠标还是键盘等。
说明:略
第17章 时间系统
DJYOS的时间管理由三部分组成,分别是日历时间、系统运行时间和定时器:
日历时间:“从一个标准时间点(1970.1.1.0:0:0:0)以来的时间经过的秒数或微秒数”来表示的时间,其所在的时区是本地。
日历时钟是可以手动调的,shell上的“date”和“time”命令,调的就是日历时间。即使不主动地调,地球也会调,因为日历时钟跟地球自转是相关的,地球自转不是匀速的哦。所以,日历时间不是始终匀速向前的,可向前也可能向后“跳跃”。
系统运行时间:从开机算起,系统的运行时间,同时提供uS和cycle两种结果。
日历时间多用于历史记录中打时间戳,而系统运行时间多用于确定时间间隔。
定时器:利用专用定时器或者系统tick时间,为用户提供定时服务,相当于闹钟,定时到的时候,调用用户提供的回调函数。它使用一个硬件定时器,或者调度定时器,虚拟出多个闹钟,这些闹钟可以同时使用。
17.1 时间基准源
djyos时间管理涉及到的时间硬件计时设备有四种:硬件RTC、不间断计时器、调度定时器、硬件定时器。但他们并不都是必须的,最基本的情况下,只有调度定时器也可以。依据硬件配置不同,时间精度可能会受影响。目前许多CPU内部都集成了多个定时器,对于定时器比较丰富的系统,建议为每一类时基使用独立硬件。
图17‑1时间系统结构
为嵌入式系统设计时间系统的困难在于,提供时基的硬件,差别非常大,上图展示了典型硬件配置。最底下那行,是比较典型的4种时基来源,如果你的硬件并非如此,请不要觉得意外,只要理解了djyos的时间系统,实现BSP时灵活处置即可。
最“穷”的情况下,仅需要调度定时器,时间管理系统就可以工作。
以下几个函数,用于“拨动”图17‑1中的几个“开关”。
1. 系统默认选择的是Ticks做所有时间基准。
2. SysTimeConnect函数为“系统时钟”选择时钟源,详见17.3节。
3. Rtc_RegisterDev函数为“日历时钟”选择时钟源,详见17.2节。
4. “软件定时器”时钟源的选择则在DIDE中进行,如下图:
图 17‑2 配置软定时器时钟源
17.1.1 硬件RTC:
硬件RTC是一个能提供万年历的专用硬件,用电池供电,关机期间也能持续运行。一般实现秒级精度,也有实现mS级精度的。有些RTC会提供年月日时分秒甚至毫秒,也有些硬件比较“懒”,只提供了一个32位的计数器,提供一个永远增加的秒数,像stm32的RTC就是如此,年月日时分秒需要自己转换。
17.1.2 64位不间断定时器:
这是一个系统开机后,就持续运行的定时器,为应用程序提供时间戳。系统将维持一个64位时钟,即使时钟源达到1Ghz,也要585年才溢出,确保我们这辈子看不到其溢出。
64位定时器是上层软件看到的定时器位宽,理想情况是,硬件能提供64位计时器,否则,就要用低位宽计时器来模拟。根据定时器位宽、频率、应用需求,定时器分类为长定时器和短定时器,详见第17.3节。
17.1.3 调度定时器
DJYOS的调度定时器支持tick和tickless模式。
在tick模式下,调度定时器固定间隔地触发中断,为系统调度提供定时服务。
tickless模式下,该定时器不再定期中断,而是根据系统调度定时服务的需要,动态调整中断时刻。
调度时基是所有时基中,唯一一个必然存在的。系统运行,用户代码可能导致关闭异步信号(例如在驱动程序中),在tick模式下,如果关闭异步信号持续时间超过tick间隔,将导致丢tick中断,带来一个tick间隔的计时误差,故tick模式下,调度定时器并不适用用于精确定时;
tickless模式情况比较复杂,如果硬件存在满足以下3个条件的定时器:
1、存在不需要停止定时器即可修改的捕获寄存器;
2、有捕获中断且捕获后定时器继续走时;
3、定时器运转一周的时间比系统中最长关中断时间长。
若三条件均满足,则调度时基本身就是一个非常准确的时基,这三个条件并不苛刻,绝大多数CPU内置的定时器都能满足。PS:cortex-m系列的24位systick定时器没有捕获寄存器,不符合条件。
调度时钟是操作系统调度的时间依据,作为调度时基和系统时钟时基,用于以下方面:
1、 调度器计算时间片的基准。
2、 DJY_EventDelay、DJY_EventDelayTo函数延时功能的时基。
3、 超时算法的时基,djyos中所有会引起阻塞的函数,都可以定义超时时间,但C库函数除外,因为函数原型无法传入这个timeout参数。
延迟时间估算:
用户调用DJY_EventDelay(3500),究竟延迟多长时间呢?tick模式和tickless模式估算方法并不一致。
tick模式下,假设ticks间隔是1mS,3~4mS之间,取决于你调用DJY_EventDelay的时刻。
调度时基的最小单位是ticks间隔,参数传入的3500,会自动向上取整tick为4000uS,也就是说,从调用时刻起,第4次tick中断即时间到。
tickless模式下,为不致调度定时器中断过于频密,tickless模式需要配置调度时基精度,若定位100uS,则延迟时间是3400~3600uS之间。
17.1.4 硬件定时器:
用于为软定时器提供时间基准,由调度定时器提供的时基,总是可能有误差的,可从硬件定时器中选择一个做时基,以消除误差。当定时器设定的时刻到,即调用软定时器的回调函数,相当于闹钟。
17.2 日历时间
DJYOS采用一个64位有符号数来统计本地日历时间,其最小计数单位是微秒。用户可以通过不同的API来获取以秒计或微秒计的日历时间。如果系统硬件提供了RTC功能,DJYOS可以直接从硬件RTC获取日历时间,系统设计了相应的注册函数,方便用户移植。从图17‑1中可知,如果没有硬件RTC,系统时钟将为日历时间提供计时基准,每次启动系统,都将恢复到2000年1月1日0时。此时,需要有途径获取当前时间,然后调用Time_SetDateTime或Time_SetDateTimeStr函数,手动设置日历时间。获取时间的方式有多种,典型的是用shell命令,或者用通信方式,比如sntp。
下面的结构体struct tm是日历时间的结构化表示,称作“分解时间”。
struct tm { s32 tm_us;/*微秒-取值区间[0,999999]*/ s32 tm_sec;/* 秒–取值区间为[0,59]*/ s32 tm_min;/* 分 - 取值区间为[0,59]*/ s32 tm_hour;/* 时 - 取值区间为[0,23]*/ s32 tm_mday;/* 一个月中的日期 - 取值区间为[1,31]*/ s32 tm_mon;/* 月份(从一月开始,0代表一月) - 取值区间为[0,11]*/ s32 tm_year;/* 年份,其值从1900开始。*/ s32 tm_wday;/* 星期–取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推。*/ s32 tm_yday;/* 从当年的1月1日开始的天数–取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推。*/ s32 tm_isdst;/* 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。*/ };
|
如上所示,结构体的大部分成员与常见“分解时间”结构体一样,但增加了一个tm_us成员,用于增加日历时间的精度。不过,这个精度能否实现依赖于实际系统配置情况,要看相应的BSP设计手册。
17.2.1 硬件RTC驱动
如果系统硬件提供了RTC功能,该功能一般是电池供电的,要把它连接到系统的日历时间系统,需要按以下过程编写驱动:
1. 初始化RTC硬件,除初始化硬件外,RTC通常是有电池供电的,有些RTC芯片有掉电标志,启动时,如果发现RTC芯片掉电了,init程序应该把时间重新设置为2000/1/1/0/0/0/0。
2. 编写RTC驱动,在time.h文件中,定义了两个函数指针类型fnRtc_GetTime和fnRtc_SetTime,硬件驱动程序要实现这两个回调函数。
3. 调用Rtc_RegisterDev把硬件RTC的操作函数注册到系统中。
虽然DJYOS定义的struct tm 中包含uS,但绝大多数情况,用户只需要精确到秒即可。因此,DJYOS的time模块提供了两套API,分别用于读秒数和微秒数。许多RTC芯片是IIC接口的,读写速度较慢,即使SPI接口也不见得多快。DJYOS在这方面做了优化,如果用户要取uS数,则直接从RTC芯片获取;如果读取的是S,则系统会判断本次读与上次读时间的间隔是否跨秒,若不跨秒则直接返回上次读到的结果,否则从芯片读取。
还有一种常见的用法,驱动程序只需要注册fnRtc_SetTime即可,在调用Rtc_RegisterDev时,第一个参数用NULL。这种情况特别适用于存在“64位不间断定时器”的情况,参见图17‑1所示。这时候,系统只利用RTC带电池的特性,在掉电时继续走时,每次上电重启后,从RTC中读取当前时间,调用settimeofday函数设置到系统中,系统运行时由“64位不间断定时器”提供日历时间。这样,读取日历时钟时,相当于直接读取系统时钟,速度很快。
17.2.1.1 fnRtc_GetTime回调函数
typedef bool_t (*fnRtc_GetTime)(s64 *time);
本回调函数用于从硬件RTC中读取时间,是64位的uS数。如果硬件RTC不支持uS,且你的应用也不需要uS,可以偷懒,用0替代;如果你的应用需要使用uS,则可用上一节最后一段介绍的方法。
17.2.1.2 fnRtc_SetTime回调函数
typedef bool_t (*fnRtc_SetTime)(s64 time);
这是硬件驱动应该实现的另一个回调函数,用于校准时间,特别是,初次上电的RTC,必须校准之后才能使用。
17.2.2 日历时间API
17.2.2.1 Rtc_RegisterDev:注册设备
bool_t Rtc_RegisterDev(fnRtc_GetTime fnGetTime,
fnRtc_SetTime fnSetTime);
头文件:
os.h
参数:
fnGetTime:函数指针,用于实现读取硬件RTC模块时间,可以为NULL。
fnSetTime:函数指针,用于实现设置硬件RTC模块时间,不能为NULL。
返回值:
如果fnSetTime = NULL,则返回false,否则返回true
说明:
将硬件RTC驱动函数fnGetTime和fnSetTime注册进系统的时间管理模块。如果fnGetTime函数非NULL,实际就是维持图17‑1中开关2的默认位置:右边。
17.2.2.2 Time_MkTime:换算日历时间
s64 *Time_MkTime(struct tm *dt)
头文件:
os.h
参数:
dt:格林威治分解时间。
返回值:
64位数表示的日历时间,位是秒。
说明:
17.2.2.3 Time_MkTimeUs:换算日历时间us
s64 *Time_MkTimeUs(struct tm *dt)
头文件:
os.h
参数:
dt:格林威治分解时间。
返回值:
64位数表示的日历时间,单位是微秒。
说明:
7.2.2.4 Time_GmTimeUs:换算格林威治分解时间
struct Tm *Time_GmTimeUs(s64 *time)
头文件:
os.h
参数:
time:64位数表示的日历时间,缺省值为系统的当前日历时间,单位是微秒。
返回值:
格林威治时区分解时间,精度微秒级。时间精度是否能达到uS,取决于日历时钟使用的时钟源的精度。市面上绝大多数的RTC芯片,都只提供秒级精度,少量能达到mS,没见过达到uS即精度的。要达到uS级,一般使用系统时钟做日历时间的时钟源,参见第17.2.1节所述方法。
说明:
将用户输入日历时间转换成格林威治时区的分解时间。如果time == NULL,函数默认使用本地日历时间,整个时间换算精度是微秒,tm表示成与1900年的差值。
注意,由于函数返回值共用一块静态缓冲区,是不可重入的;如果需要在多事件并发情况下使用,应该使用带“_r”后缀的函数。
17.2.2.5 Time_GmTimeUsR:可重入换算格林威治分解时间
struct Tm *Time_GmTimeUsR(s64 *time,struct Tm *result)
头文件:
os.h
参数:
time:64位数表示的日历时间,缺省值为系统的本地日历时间,精度微秒级。
result:格林威治时区的分解时间,精度微秒级;注意,指针不能为空。
返回值:
指向时间转换结果,格林威治时区的分解时间,精度微秒级,tm表示成与1900年的差值。
说明:
将用户输入的日历时间转换成格林威治时区的分解时间,如果time == NULL,函数默认使用本地日历时间,整个转换精度微秒级。与上面的Time_GmTimeUs不同的是,由于返回值空间由用户传入,是线程安全的。
17.2.2.6 Time_GmTime:换算格林威治分解时间
struct Tm *Time_GmTime(s64 *time)
头文件:
os.h
参数:
time:64位数表示的日历时间,缺省值为系统的本地日历时间,精度秒级。
返回值:
分解时间,精度秒级,tm表示成与1900年的差值。
说明:
将用户输入日历时间转换成格林威治时区的分解时间,如果time == NULL,函数默认使用本地日历时间,整个换算精度是秒。
注意,由于函数返回值共用一块静态缓冲区,是不可重入的;如果需要在多事件并发情况下使用,应该使用带“_r”后缀的函数。
本函数与c库的 gmtime功能相同。
17.2.2.7 Time_GmTimeR:可重入换算格林威治分解时间
struct Tm *Time_GmTimeR(s64 *time,struct Tm *result);
头文件:
os.h
参数:
time:64位数表示的日历时间,1970年起,缺省值为系统的本地日历时间,精度秒级。
result:指向时间转换结果,即格林威治时区的分解时间,精度秒级;注意,指针不能为空。
返回值:
格林威治时区的分解时间,精度秒级,tm表示成与1900年的差值。
说明:
将用户输入的日历时间转换成格林威治时区的分解时间,如果time==NULL,函数默认使用本地日历时间,整个换算精度秒级。与上面的Time_GmTime不同的是,本函数是线程安全的。
本函数与c库的 gmtime_r功能相同。
17.2.2.8 Time_LocalTime:换算同时区分解时间
struct Tm *Time_LocalTime(s64 *time)
头文件:
os.h
参数:
time:64位数表示的日历时间,精度是秒。
返回值:
在相同时区下,输入日历时间的分解时间,tm表示成与1900年的差值。
说明:
将用户输入的日历时间转换成相同时区下的分解时间,如果输入的时间是以本地时区为基准,则返回值就是本地时区的分解时间。时间换算精度为秒。
注意,由于函数返回值共用一块静态缓冲区,是不可重入的;如果需要在多事件并发情况下使用,应该使用带“_r”后缀的函数。
本函数与c库的 localtime功能相同。
17.2.2.9 Time_LocalTimeR可重入换算同时区分解时间
struct Tm *Time_LocalTimeR(s64 *time,struct Tm *result)
头文件:
os.h
参数:
time:64位数表示的日历时间,缺省值为系统的本地日历时间,精度秒级;
result:指向在相同时区下,输入日历时间的分解时间。指针不能为空。
返回值:
输入日历时间的分解时间,tm表示成与1900年的差值
说明:
将用户输入的日历时间转换成相同时区下的分解时间,如果输入的时间是以本地时区为基准,则返回值就是本地时区的分解时间。时间换算精度为秒。与函数Time_LocalTime不同的是,本函数是线程安全的。
本函数与c库的 localtime_r功能相同。
17.2.2.10 Time_LocalTimeUs:换算同时区分解时间
struct Tm*Time_LocalTimeUs(s64 *time_us)
头文件:
os.h
参数:
time:64位数表示的日历时间,自1970年起,精度是微秒。
返回值:
在相同时区下,输入日历时间的分解时间,精度是微秒,tm表示成与1900年的差值。
说明:
将用户输入的日历时间转换成相同时区下的分解时间,如果输入的日历时间是以本地时区为基准,则返回值就是本地时区的分解时间。时间转换精度为微秒。
注意,由于函数返回值共用一块静态缓冲区,是不可重入的;如果需要在多事件并发情况下使用,应该使用带“_r”后缀的函数。
17.2.2.11 Time_LocalTimeUsR:可重入换算同时区分解时间
struct Tm *Time_LocalTimeUsR(s64 *time,struct Tm *result);
头文件:
os.h
参数:
time:指向64位表示的日历时间。
result:指向在相同时区下,输入日历时间的分解时间,tm表示成与1900年的差值。
返回值:
在相同时区下,输入日历时间的分解时间。
说明:
将用户输入的日历时间转换成相同时区下的分解时间,如果输入的时间是以本地时区为基准,则返回值就是本地时区的分解时间。时间转换精度为微秒。与函数Time_LocalTimeUs不同的是,本函数是线程安全的。
17.2.2.12 Time_SetDateTimeStr:设置本地日历时间
s32 Time_SetDateTimeStr(char *buf)
头文件:
os.h
参数:
buf:指向格式例如“2011/10/28,22:37:50”字符串所表示的时间。
返回值:
设置成功返回1;否则返回错误码。
说明:
用格式为"2011/10/28,22:37:50"的字符串所表示的时间设置系统本地日历时间。注意设置的时间精度是秒。如果有硬件RTC,新时间也会更新硬件。
17.2.2.13 Time_SetDateTime:设置本地日历时间
void Time_SetDateTime(struct Tm *tm)
头文件:
os.h
参数:
tm,分解时间。
返回值:
无
说明:
用分解时间格式更新(设置)系统本地日历时间,在具备硬件RTC的系统中,如果硬件直接输出分解时间的,硬件驱动调用本函数。因为日历时间只精确到秒,因此tm中的us成员,由硬件驱动自行维护,本函数不读取。如果硬件不维护us成员(调用tm_connect_rtc时参数get_rtc_hard_us=NULL),rtc driver须使用中断方式,以确保秒跳变时立即调用get_rtc_hard_us,系统将以此时刻为起点计算us数。
17.2.2.14 Time_AscTime:转换时间为“字符串”
void Time_AscTime(struct Tm *tm, char buf[])
头文件:
os.h
参数:
tm:分解时间;
buf:用来返回结果的数组地址,至少21字节。
返回值:
以格式“年/月/日,时:分:秒”显示的字符串。
说明:
把一个分解时间换算成“字符串”时间,时间单位为秒。“字符串”时间格式为“年/月/日,时:分:秒”。
本函数与c库的 asctime功能相同。
17.2.2.15 Time_AscTimeMs:转换时间为“字符串”
void Time_AscTimeMs(struct Tm *tm, char buf[]);
头文件:
os.h
参数:
tm:分解时间;
buf:用来返回结果的数组地址,至少24字节。
返回值:
以格式“年/月/日,时:分:秒:毫秒”显示的字符串。
说明:
把一个分解时间换算成“字符串”时间,时间单位是毫秒,“字符串”时间格式“年/月/日,时:分:秒:毫秒”。
17.2.2.16 Time_AscTimeUs:转换时间为“字符串”
void Time_AscTimeUs(struct Tm *tm, char buf[]);
头文件:
os.h
参数:
tm:分解时间
buf:用来返回结果的数组地址,至少27字节。
返回值:
以格式“年/月/日,时:分:秒:毫秒:微秒”显示的字符串。
说明:
把一个分解时间换算成“字符串”时间,时间单位是微秒,“字符串”时间格式“年/月/日,时:分:秒:毫秒:微秒”。
17.2.2.17 Time_Time:获取本地日历时间
s64 Time_Time(s64 *ret)
头文件:
os.h
参数:
ret:指向用于存放系统的本地日历时间的内存空间,指针为空,则无效。
返回值:
本地日历时间。
说明:
获取系统的本地日历时间,时间单位为秒。
本函数与c库的 time功能相同。
17.2.2.18 Time_TimeUs:获取本地日历时间
s64 Time_TimeUs(s64 *ret);
头文件:
os.h
参数:
ret:指向用于存放系统的本地日历时间的内存空间,指针为空,则无效。
返回值:
本地日历时间。
说明:
获取系统的本地日历时间,时间单位为秒。
17.3 系统运行时间
系统运行时间是一个64位单调增计时器,调用相应的API,可读取:
1、64位的系统运行uS数。
2、64位的计数周期数,以及计数频率。
从图17‑1中的开关2用于选择时基,默认拨向右边,调用SysTimeConnect,可以将其拨向左边。
17.3.1 选择调度定时器
tick模式下,精度将受以下因素影响:
1、 ticks中断属于异步信号,系统运行、应用程序运行(特别是硬件driver),都可能会导致关调度(关异步信号),如果关调度持续时间过长,导致ticks丢失的话,每丢失一个tick,就引入一个tick间隔的误差。
2、 应用程序如果关总中断,持续时间过长也会导致丢失tick。
ticks作为时基,但所获得的时间精度和分辨率,并不受ticks间隔限制,计时分辨率能达到ticks计时器的时钟源的频率。
若选中调度时基做系统运行时间的时钟源,需要在cpu.c中实现一个桩函数__DjyGetSysTime。这个函数实现有一定技巧,否则容易带来误差,BSP工程师要特别注意,可参考官方提供的源码。
在tickless模式,如果系统存在符合如17.1.3节所述要求的定时器,则不会出现误差。
17.3.2 选择64位不间断定时器
定时器的硬件就像墙上的时钟,周而复始地运行,运行周期=T1;嵌入式设备启动后,也开始运行,直到设备停止运行(被重新复位、或断电),从启动到停止的持续运行时间=T2。凡是T1<T2有可能成立的,定义为短定时器,反之则是长定时器。
因此,长/短定时器,不能单纯以硬件定时器位数来确定,有些设备,T1也许只有1小时,但每次开机运行时间不长的话,也是长定时器,例如豆浆机,打完一次豆浆就自动关机;又有些设备,即使定时器的定时周期长达一年,但设备却是长年累月不停机的,也是短定时器,例如电冰箱。
17.3.2.1 短定时器时基
如果硬件提供短定时器,需要实现GetSysTime32,函数的原型如下:
typedef u32 (*fnSysTimeHard32)(void);
从第17.3.2.3节可知,定时器的循环周期不宜太小,太小容易带来误差,而输入时钟频率也不宜太低,太低影响时间精度,因此不建议使用<16bit的定时器,也不建议使用低于1Mhz的输入时钟频率。选择定时器参数时,建议T1在200mS以上,不要低于65mS。
实现GetSysTime32函数,驱动程序实现方法如下:
初始化硬件定时器为周而复始自由运行模式,不使能中断,无论加减计数器,重载值=0。
GetSysTime32返回值:如果是加计数器,则直接返回计数值,如果是减计数器,则返回(2的n次方-计数值)。
注意:tickless模式不宜使用短定时器,若硬件不提供长定时器,则系统运行时间由调度定时器提供。原因参见第17.3.2.3节。
17.3.2.2 长定时器时基
如果硬件提供长定时器,需要实现GetSysTime64,函数的原型如下:
typedef s64 (*fnSysTimeHard64)(void);
初始化硬件定时器为周而复始自由运行模式,不使能中断,无论加减计数器,重载值=0。
GetSysTime64返回值:如果是加计数器,则直接返回计数值,如果是减计数器,则返回(0xffffffffffffffff-计数值)。调用SysTimeConnect函数时,Cycle参数没有意义。
注意,长定时器不一定都是64位的,甚至可能低于32位,只要满足长定时器的定义即可。
17.3.2.3 消除短定时器级联错误
系统运行时间永远向前,用短定时器实现,就需要考虑级联的问题。级联,就是把周期性运行的定时器的运行周期数累加值,再加上定时器的当前计数值,作为系统时间。级联如果处理不好就会产生计时错误。
级联定时器通常是初始化一个周期性触发中断的定时器,在定时器中断的ISR中累加时间来实现的,但这种方法存在一个严重的问题,即如果定时器中断发生时,正好处于关中断期间,又恰好在此时来读定时器的话,将得到一个错误的值,定时器位数越高,错误越大。并且可能出现倒挂现象,即新读出的时间,比较早读出的时间还早。
图17‑3短定时器误差
DJYOS改进了方法,在tick模式下,短定时器本身不产生中断,只是周而复始地计数。每次读系统时间,都把计数值读出来,然后与上次读出的值比较,如果新值较小,则累加T。因此,只要读系统时间的间隔小于T期,就能读出准确的时间。当然,我们不能依赖用户读系统时间的间隔总是小于T,因此,在每个tick中断里,都会读一次系统时间,tick间隔需要远小于定时器的循环周期。
只有连续关中断且没有发生用户读系统时间的期间超过T,才会发生产生计时误差,如图17‑4所示。在标红区域内,读取的时间是错误的,第一次关中断后,在一个T时间内,都没有读操作,将带来1T的误差,故t0比实际值小1T,且这个误差是会积累的,以后读时钟,都会有1T的误差;第二次关中断期间,关中断后不到1T,读t1,不会带来新的误差,t1仍然比实际值小1T,读t2时,因与上一次读间隔大于1T,故又引入1T的误差,t2将比实际值小2T。T越大,产生误差的可能性就越小,建议尽可能使用32位以上的定时器做。
图17‑4改进后计时误差示意图
tickless模式下,因没有定期的tick中断,无法定期读系统时间,故无法使用短定时器。
17.3.3 系统运行时间API
17.3.3.1 SysTimeConnect:选择时钟源
void SysTimeConnect(fnSysTimeHard32 GetSysTime32,fnSysTimeHard64 GetSysTime64,
u32 Freq,u32 Cycle)
头文件:
systime.h
参数:
GetSysTime32:读硬件定时器当前计数值,32位及以下定时器用此参数输入,否则NULL。
GetSysTime64:同上,32位硬件定时器用,否则NULL。
Freq:计时器输入时钟频率。
Cycle:=2的n次方,n是定时器位数,如果是64位定时器,输入0即可。
返回值:
true = 正常,false = 异常。
说明:
设定图17‑1中“系统时钟”的时钟源。
17.3.3.2 DJY_DelayUs:uS级延迟
void DJY_DelayUs(u32 time);
头文件:
os.h
参数:
time:延迟时间,单位是uS。
返回值:
无。
说明:
uS级延迟,用指令延时实现。
17.3.3.3 DJY_GetSysTime:查询当前系统运行时间
s64 DJY_GetSysTime(void)
头文件:
os.h
参数:
无。
返回值:
当前系统已运行时间。
说明:
获取自系统开机以来的运行时间,单位是微秒。
17.3.3.4 DJY_GetSysTimeCycle:查询当前系统运行时间
s64 DJY_GetSysTimeCycle(void)
头文件:
os.h
参数:
无。
返回值:
当前系统已运行时间,以计数周期数表示。
说明:
获取自系统开机以来的运行时间,单位是周期数,需要结合DJY_GetSysTimeFreq函数才能确定时间。
本函数执行时间稍微比DJY_GetSysTime快些,对于一些要求比较苛刻的时间间隔测量,或者被测时间间隔本身非常短的,可使用这个函数。
如果使用tick做时基,本函数返回值与DJY_GetSysTime一致。
17.3.3.5 DJY_GetSysTimeFreq:查询当前系统运行时间
u32DJY_GetSysTimeFreq(void)
头文件:
os.h
参数:
无。
返回值:
系统时间的输入时钟频率。
说明:
本函数须与DJY_GetSysTimeCycle函数配合使用。
如果使用tick模式的调度时基做系统时钟源,本函数返回值为1Mhz。
17.4 硬件定时器管理
DJYOS的硬件定时器模块提供如下功能:
1. 管理硬件定时器,并为用户提供通用统一接口。
2. 为软件定时器提供定时服务。
本模块向应用程序(包括软定时器)提供硬件定时器的标准操作接口:申请定时器、释放定时器、设置周期、设置中断、暂停和使能定时器等功能。向下提供硬件驱动注册定时器芯片的接口:注册定时器芯片、注销定时器芯片。硬件接口层定义了时钟芯片参数结构体TimerChip如下(在timer_hard.h中):
struct TimerChip { char *chipname; //chip名字,必须为静态定义 fnHardTimerAlloc TimerHardAlloc; //分配定时器 fnHardTimerFree TimerHardFree; //释放定时器 fnHardTimerGetFreq TimerHardGetFreg; //获取定时器计数频率 fnHardTimerCtrl TimerHardCtrl; //控制定时器 }; |
通过TimerChip中四个钩子函数TimerHardAlloc、TimerHardFree、TimerHardGetFreg、TimerHardCtrl实现对具体时钟芯片的间接操作,这四个钩子函数是由硬件层时钟芯片驱动中实现。
注意,不管硬件上有多个定时器,相同类型或者不同类型,TimerChip结构只有一个,timer接口不予区分,而是由驱动程序区分。
另外,硬件定时器的功能五花八门,应用程序的需求也千变万化,定时器模块不可能完全覆盖,它只提供最常用的功能。具体实现中,须灵活处理,不必强求管理所有硬件定时器。比如硬件共有5个定时器你完全可以单独拿一个做PWM使用,只提供4个定时器给定时器模块。
17.4.1 硬件定时器驱动
要想使用Djyos的定时器组件的功能,用户必须要针对各自使用平台的定时芯片编写底层驱动,这也是移植唯一要做的事情。在BSP中需要完成以下4个函数:然后调用HardTimer_RegisterChip函数把他们注册到系统中。
17.4.1.1 分配定时器
typedef ptu32_t (*fnHardTimerAlloc)(fnTimerIsr timerisr);
头文件:
timer_hard.h
参数:
timerisr:用户实现的定时器中断服务函数,定时期限到后,由定时器中断调用。
返回值:
NULL 分配不成功,否则返回定时器句柄,定时器句柄数据类型为ptu32_t,其具体内容由BSP工程师根据各平台实际情况定义。
说明:
分配定时器函数需要完成以下工作:
1. 选出一个可使用的定时器并对其句柄进行复制;
2. 设置定时器属性,默认状态下:停止计数、设置定时器中断为异步中断并将其挂接到DJYOS的中断系统中,设置定时器周期;
3. 返回定时器句柄。
17.4.1.2 释放定时器
typedef bool_t (*fnHardTimerFree)(ptu32_t timerhandle);
头文件:
timer_hard.h
参数:
timerhandle,待释放定时器句柄。
返回值:
true 成功;false失败。
说明:
该过程基本上是分配的逆向过程,将指定的定时器恢复到分配前的状态,一般要停止计数器、切断中断线等。
17.4.1.3 取定时器时钟频率
typedef u32 (*fnHardTimerFreq)(ptu32_t timerhandle);
头文件:
timer_hard.h
参数:
timerhandle,待释放定时器句柄。
返回值:
定时器的时钟源频率,单位是Hz,最高4Ghz。
说明:
取得频率后,就可以计算所需要的定时时间对应多少个计数值了。
17.4.1.4 控制定时器
typedef bool_t (*fnHardTimerCtrl)(ptu32_t timerhandle,
enum HardTimerCmdCode ctrlcmd,
ptu32_t inoutpara);
头文件:
timer_hard.h
参数:
timerhandle:待操作的定时器句柄;
ctrlcmd:操作命令;参见第17.4.2.6节。
inoutpara:输入输出参数,根据不同的情况而定。
返回值:
true 操作成功;false操作失败。
说明:
该函数用于实现对某个指定定时器进行特定操作,根据操作码实现各自特定操作。
17.4.2 硬件定时器API
17.4.2.1 HardTimer_RegisterChip:注册定时器芯片
bool_t HardTimer_RegisterChip(struct TimerChip *timerchip)
头文件:
timer_hard.h
参数:
timerchip:定时器芯片结构体指针。
返回值:
true 成功 false失败。
说明:
注册定时器芯片到系统定时器模块。该函数由硬件驱动程序调用,注册后,应用程序才能使用硬件定时器。
17.4.2.2 HardTimer_UnRegisterChip:注销定时器芯片
bool_t HardTimer_UnRegisterChip(void)
头文件:
timer_hard.h
参数:无。
返回值:
true 成功 false失败。
说明:
定时器芯片注销,目前而言是没有用,主要是register之后必然有unregister。
17.4.2.3 HardTimer_Alloc:硬件定时器分配
ptu32_t HardTimer_Alloc(u32 cycle, fnTimerIsr timerisr)
头文件:
timer_hard.h
参数:
cycle:指定分配定时器的定时周期,该属性可以使用API函数更改,注意是计数值,不是uS,需要与HardTimer_GetFreq函数得到的频率配合才能确定时间量。
isr:分配的定时器的服务函数,在中断中调用。isr的原型为:typedef u32 (*fnTimerIsr)(ufast_t irq_no);其中irq_no为定时器对应的中断号,在cpu_peri_int_line.h中定义了CPU所有的中断源对应的中断号。另外需要注意的是如果硬件定时器中断属性被设置为实时中断,isr函数不允许调用任何系统API。
返回值:
NULL 分配不成功,否则返回定时器句柄,该句柄的结构由定时器芯片自己定义。
说明:
硬件定时器分配,一般而言刚分配的定时器,处于计时停止,中断禁止状态。
17.4.2.4 HardTimer_Free:硬件定时器释放
bool_t HardTimer_Free(ptu32_t timerhandle);
头文件:
timer_hard.h
参数:
timerhandle,待释放定时器句柄。
返回值:
true 成功;false失败。
说明:
释放一个硬件定时器释放,依赖芯片驱动实现。
17.4.2.5 HardTimer_GetFreq:取定时器时钟频率
u32 HardTimer_GetFreq(ptu32_t timerhandle);
头文件:
timer_hard.h
参数:
timerhandle,待释放定时器句柄。
返回值:
定时器的时钟源频率,单位是Hz,最高4Ghz。
说明:
取得频率后,就可以计算所需要的定时时间对应多少个计数值了。
17.4.2.6 HardTimer_Ctrl:操作硬件定时器
bool_t HardTimer_Ctrl(ptu32_t timerhandle,
enum HardTimerCmdCode ctrlcmd,
ptu32_t inoutpara)
头文件:
timer_hard.h
参数:
timerhandle:待操作的定时器句柄;
ctrlcmd:操作命令;
inoutpara:输入输出参数,根据不同的情况而定。
返回值:
true 操作成功;false操作失败。
说明:
ctrlcmd对应的inoutpara的属性在enum HardTimerCmdCode 中定义说明如下:
EN_TIMER_STARTCOUNT, //使能计数,inoutpara无意义
EN_TIMER_PAUSECOUNT, //停止计数,inoutpara无意义
EN_TIMER_SETCYCLE, //设置周期,inoutpara为u32,待设置的周期(计数值)
EN_TIMER_SETRELOAD, //reload模式or not!,inoutpara=true代表Auto reload
EN_TIMER_ENINT, //中断使能,inoutpara无意义
EN_TIMER_DISINT, //中断禁止,inoutpara无意义
EN_TIMER_SETINTPRO, //中断属性设置,inoutpara为bool_t,true代表实时信号
EN_TIMER_GETTIME, //获取上一次溢出到现在的计数值,inoutpara为u32 *
EN_TIMER_GETCYCLE, //获取定时周期,inoutpara为u32 *,单位为周期数
EN_TIMER_GETID, //取定时器ID,inoutpara为u32 *,(intID<<16) +timerID
EN_TIMER_GETSTATE, //获取定时器状态,inoutpara为u32 *,结果是CN_TIMER_ENCOUNT这5个常量之一。
注意“EN_TIMER_SETINTPRO”方法,当中断属性设置为异步信号,硬件驱动中须调用系统函数Int_ SettoAsynSignal 函数将相应定时器中断设置为异步信号;如果需要设置为实时信号,硬件驱动中须调用系统函数Int_SettoReal函数将相应定时器定时器中断类型设置为实时中断。
注意:如果定时器中断属性设为实时中断,则用户实现的中断服务函数ISR中必须清中断,且不能调用任何系统服务;如果设定为异步信号,则无须清中断,且允许调用全部系统调用。
17.5 软件定时器
硬件提供的定时器,总是有限的,但软件可能需要很多定时器,怎么办呢?既然连我们的手机信号也是分时复用的,为什么我们的定时器不能呢?一个定时器如果仅仅指定一个定时周期,是不是有点太浪费了?毕竟在这个定时期限内它什么也不做,仅仅是计数,为什么我们不试着让她总是在工作状态呢?依次触发,对,这就是我们的工作模式,多个软件定时器依次触发同一个硬件定时器。
如何达到共享定时器的目的?其实很简单,只要将我们需要定时的时间从近到远(近,表示离当前时间很近)依次排列,那么取最近的先定时依次,当第一次定时到的时候再取第二近的时间再次定时。定时时间到的定时器(软件定时器)重新计算下次定时器时间然后继续排队。这样理论上实现了无限多的定时器需求。为了避免讨厌的数据溢出问题,我们使用了一个64位数进行排队。
用硬件定时器提供定时基准,可以获得很高的定时精度,如果您舍不得给它一个硬件定时器,或者实在囊中羞涩,可以按图 17‑2所示,选择调度时钟工作,在tick模式下,其精度就只能达到tick间隔了。
如果用专用定时器,就得考虑频繁中断的问题,即两个定时器的到点时间相距很近怎么办?进一步地,如果很多很多定时器的到点时间很接近怎么办,每个定时器都中断么?这样不但非常占用CPU时间,还影响了其他程序的运行。软件定时器模块因此做了一个限制,即每次中断,都检查一下随后到点的定时器,如果时间差在1000个CPU主频周期内的,都算已经到点,使下一个次中断与本次中断的间隔大于1000个CPU周期。
17.5.1 软件定时器API
17.5.1.1 Timer_Create:创建一个软件定时器
tagTimer * Timer_Create(char *name,u32 timercycle,fnTimerRecall ReCall)
头文件:
timer.h
参数:
name:定时器名字;
timercycle:定时器周期;
ReCall:定时器定时时间到执行HOOk,中断中被调用。
返回值:
NULL分配失败否则返回分配到的定时器句柄。
说明:
创建一个定时器,创建的定时器默认的reload模式,如果需要手动的话,那么创建之后自己设置;创建的定时器还是处于pause状态,需要手动开启该定时器。
17.5.1.2 Timer_Create_s:创建一个静态软件定时器
tagTimer* Timer_Create_s(tagTimer *timer,const char *name,
u32 cycle, fnTimerRecall ReCall)
头文件:
timer.h
参数:
timer:定时器控制块指针,调用者提供。
name:定时器名字;
cycle:定时器溢出周期;
ReCall:定时器定时时间到执行HOOk,中断中被调用;
返回值:
NULL分配失败否则返回分配到的定时器句柄。
说明:
功能与Timer_Create类似,但调用者须提供tagTimer控制块结构体,然后把指针传递过来,高可靠性的应用中,不应该使用动态分配的方式,静态定义更可靠。
17.5.1.3 Timer_Delete:删除一个软件定时器(内核)
bool_t Timer_Delete(TIMERSOFT_HANDLE timerhandle)
头文件:
timer.h
参数:
timerhandle:待删除的定时器句柄。
返回值:
true 成功;false失败。
说明:
删除一个软件定时器。
17.5.1.4 Timer_Delete_s:删除一个软件定时器
tagTimer *Timer_Delete_s(tagTimer *timer)
头文件:
timer.h
参数:
timer:待删除的定时器句柄。
返回值:
true 成功;false失败。
说明:
删除一个软件定时器。
17.5.1.5 Timer_Ctrl:软件定时器控制
bool_t Timer_Ctrl(tagTimer* timer,u32 optcmd, ptu32_t inoutpara);
头文件:
timer.h
参数:
timer:待操作的定时器指针;
optcmd:操作命令,参看下面“说明”
inoutpara:输入输出参数,根据不同的情况而定。
返回值:
true 操作成功;false操作失败。
说明:
optcmd对应的inoutpara的属性定义说明如下:
EN_TIMER_SOFT_START, //使能计数,inoutpara无意义
EN_TIMER_SOFT_PAUSE, //停止计数,inoutpara无意义
EN_TIMER_SOFT_SETCYCLE, //设置周期,inoutpara为u32,待设置的周期(uS数)
EN_TIMER_SOFT_SETRELOAD, //reload模式,true代表自动reload。
17.5.1.6 Timer_SetTag:设置用户标签
bool_t Timer_ SetTag (tagTimer* timer,ptu32_t Tag);
头文件:
timer.h
参数:
timer,定时器指针.
Tag,新的标签
返回值:
true 操作成功;false操作失败。
说明:
用户使用标签,可以用来标识定时器的用途
17.5.1.7 Timer_GetTag:读取用户标签
ptu32_t Timer_ GetTag (tagTimer* timer);
头文件:
timer.h
参数:
timer,定时器指针.
返回值:
用户标签的值。
说明:
用户使用标签,可以用来标识定时器的用途
17.5.1.8 Timer_GetName:读取定时器名
char * Timer_ GetName (tagTimer* timer);
头文件:
timer.h
参数:
timer,定时器指针.
返回值:
定时器名称。
说明:
第18章 看门狗
18.1 看门狗原理
18.1.1 看门狗的硬件
为了提高系统可靠性,嵌入式系统一般都会设计看门狗电路,在软件严重错误、或者硬件受干扰而使系统运行出错的时候,看门狗电路将输出复位信号重置系统。看门狗有CPU内嵌和外置两种,无论哪一种看门狗电路,其原理不外乎如图18‑1所示。系统正常运行时,会定期地执行“喂狗”动作,即将看门狗时钟清零。一旦系统出现异常,无法“喂狗”,看门狗会因其时钟超时而产生超时信号,而这个信号一般作为系统的复位信号。
图18‑1看门狗电路设计原理
图18‑2典型外置看门狗电路
图18‑2是典型的外置看门狗电路图,MAX705内置两个定时器,一个用于控制看门狗溢出时间,时间常数固定为1.6秒,WDI信号的每次改变都会清零定时器,如果wdt信号连续1.6秒没有变化时,MAX706的7脚就输出一个低电平脉冲表示看门狗溢出,该信号用于复位计算机。另一个定时器控制看门狗溢出脉冲的宽度,时间常数固定为200mS。与大多数看门狗芯片一样,MAX705兼具电源监测功能,图中没有画出。也有一些看门狗芯片的定时时间是可以调整的,例如MAX691,绝大多数MCU内置的看门狗,也是可调整的。插上跳线J4可以禁止看门狗复位,用于调试。顺带提一下,有些人设计看门狗电路时,把J4设计成调试时拔掉,运行时插上,这是不对的。跳线帽是机械连接,可能会接触不良,且生产时要多一道插跳线帽的工序,这道工序是人工操作,易遗漏,且遗漏后难于检测。
18.1.2 看门狗的软件原理
计算机系统有大有小,有复杂有简单,但是对于使用看门狗的软件程序,当我们抽其脉络,发现无异于如图18‑3所示。不同的是分支的多少、条件判断的复杂度、各种循环执行的次数和频度,以及中断发生的频度等。软件运行过程中,会有一些关键点,只要这些关键路径正常执行到,基本上可以保证软件是正常的,而看住这些关键点往往需要看门狗的协助,看门狗的数量以及看门狗资源的竞争往往成了软件设计的一个关键点。
图18‑3使用看门狗程序脉络
18.2 启动过程的看门狗
18.2.1 尴尬的看门狗
启动过程,是硬件看门狗系统长久以来的难言之隐,直到DJYOS出现,才得以一洗了之。
硬件看门狗定时器计满即会输出复位脉冲,硬件启动计时的时机有两种,一是上电即启动计时,外置硬件看门狗一般属于这种;二是上电时不启动,需要软件触发一下才启动,启动后则不能关闭,MCU内置的看门狗一般属于这种形式。
很多时候,软件都有个加载和启动过程,在操作系统支持下,尤其如此。这期间正常的喂狗程序往往难于执行,这段时间超过了看门狗的溢出周期怎么办?系统初始化未完成,狗就忍不住要叫,让你的系统无休止地复位,怎么办?
18.2.2 启动代码中喂狗
显然,如果在启动过程中就按时喂狗,问题便烟消云散了。但这往往是一厢情愿,个中苦衷,荣我慢慢道来。
一般来说,系统启动前,首先要执行一个加载器,该加载器会copy代码到内存中(如果在flash中运行就免了),然后初始化data段(包含已初始化变量),最后把bss段(包含未初始化变量)清零,这个过程中,由于wdt代码未加载,喂狗代码并不能调用。如果是高度定制的系统,你可以先单独加载wdt模块,也可以在startup代码中也放置一份wdt代码。这里面,有两个问题:
1.加载器本来是与硬件无关的,但如果你要在其中“夹塞”喂狗代码的话,就变成硬件的附庸了。
2.裸跑系统还好说,如果使用了操作系统,你要找出它的加载器并“夹塞”喂狗代码进去,难度可想而知,特别是,不开源的系统你怎么办?
事情还没完呢,加载器虽然难搞,毕竟是固定的一部分代码,花点功夫还是能搞定。driver以及中间件的初始化过程,如果超过了狗叫间隔,也会让你走投无路。如果你自己写的driver,当然可以改;不是你自己写的,有源码,也可以改;但如果是外购的中间件,没源码,你怎么办?即使你可以改,也不代表没问题,如果所有driver都跟wdt相关,你的整个代码,就会是一张蜘蛛网,各个模块耦合在一起。“低耦合、高内聚”的教诲,你忘了么?
18.2.3 推迟启动看门狗
既然不能在启动过程喂狗,那让看门狗在启动过程闭嘴行不?
推迟启动看门狗计时器,直到初始化完成,这是最容易、最直观的方式了,有一部分看门狗硬件有这种功能,它允许用软件往特定的寄存器写特定的数值后,才能启动,当然,启动以后是不能再关闭的,除非复位,硬件工程师不约而同地把看门狗设置成这样。
这种方案可靠吗?
只要不启动看门狗计时器,狗是永远不会叫的,加载和初始化可以正常完成。但问题是,加载和初始化过程可能出错,导致系统将无法启动怎么办?而此时看门狗尚未开启,狗是永远不会叫的,系统将永远死寂一片。不要以为这是危言耸听,这种事情确实发生过。有些硬件设计考虑不周,或者受到现场骚扰信号干扰,上电复位不彻底,导致在上电过程使系统进入死锁。
另一个问题是,看门狗计时器启动后不能关闭,虽然复位可以关闭它,但需要软件重启怎么办?此时需要执行“漫长”的初始化过程,既不喂狗又没有复位,忠诚的看门狗一定会在你初始化到一半的时候,狂吠起来,你又万劫不复。
18.2.4 启动过程硬件喂狗
不推迟启动看门狗计时器,也不在启动代码中喂狗,而是安排专门的硬件,在启动过程喂狗,看上去很完美。
喂狗的硬件,可以是CPU自带的PWM,许多嵌入式CPU提供这样的功能,但这样一来,跟“推迟启动看门狗计时器”有什么区别呢?毫无区别!
改进的方法,是使用CPLD或FPGA这类可编程器件,实现智能化喂狗,即上电(或复位)后自动喂狗,但必须设计一个最后时限,如果最后时限到达时,初始化还没有完成,狗就必须叫了。系统初始化完成后,把CPLD的看门狗切换到正常模式。
看起来很美了,但事情并不是那么简单。
CPLD增加的成本由谁买单?
系统软重启怎么办?
一个嵌入式应用系统,重启可能有三种情况,一是软重启,即软件自己跳到起始位置,重新加载和初始化;二是CPU复位而其他硬件不复位;三是整个硬件系统复位。
对于前两种情况,外部的CPLD是不知道的,看门狗会一如既往地计时,到点复位CPU。
那只能在重启前,用软件“命令”CPLD重新进入自动喂狗模式。这样行吗?降低了可靠性啊,这等于程序运行中可以关闭看门狗啊。为什么所有MCU内置的看门狗都设计成启动后不能关闭,就是怕软件误操作把看门狗关闭了。
18.2.5 柳暗花明
前面所述的种种尴尬,在DJYOS面前,均不存在。回忆一下在18.2.2节,如果把“在启动代码中喂狗”改为“在启动过程中喂狗”,所有问题是否迎刃而解?那怎么才能在启动过程中喂狗呢?答案是中断。遗憾的是,许多操作系统启动过程都是关中断的,而且没有提供在启动前加载wdt模块到内存的功能,以致即使开中断,也无法使用。
DJYOS提供两个机制,可以实现在启动过程中喂狗:
1.只要在lds文件中把wdt代码放到预加载区域,就会在加载系统前被加载到内存。
2.在系统加载和初始化过程中,实时中断是允许的。
那么,我们就可以这样来实现加载过程中“喂狗”,即在系统上电后,预加载一段代码专门用于实现“启动过程中喂狗”的代码。该代码启动看门狗的同时,额外启动再一个定时器用于看门狗的主动“喂狗”。系统开始正常运行后,用来喂狗的定时器就功成身退了。但启动定时器喂狗的那段代码还在内存中,如果你实在不放心,担心被误触发,可把那段代码删掉。系统启动中喂狗整个工作原理如图18‑4所示。
图18‑4启动中喂狗原理
18.3 看门狗模块设计
18.3.1 功能
DJYOS看门狗模块具有如下功能:
1. 监察应用程序是否定期执行到某关键位置。
2. 监察关键任务是否在规定的时间内完成。
3. 如有硬件看门狗,提供系统启动(加载和初始化)过程中安全喂狗的机制;
4. 允许用户独立设置各独立看门狗狗叫后的处理措施;
5. 与异常处理模块结合,狗叫事件将作为软件异常予以记录。
6. 如有硬件看门狗,可管理硬件看门狗。
可见,DJYOS的看门狗模块,是一个独立的软件模块,它能管理硬件看门狗,但不依赖于硬件看门狗工作。即使用户硬件中没有配置看门狗,也能起到看家护院的作用,大大提高用户软件的可靠性。
18.3.2 基本架构
在层次上,本模块在软件上分为三个层次:芯片驱动层、看门狗硬件抽象层、看门狗虚拟层。芯片驱动层仅仅实现了本芯片的基本驱动,按照硬件抽象层的格式将本芯片注册进系统;硬件抽象层向下提供芯片注册的接口,在本层实现启动中的喂狗功能,对上提供所驱动的硬件芯片的基本信息;用户可根据需要看护的软件路径,获取无数的“看门狗”来满足用户的需求。整个设计层次如图18‑5所示。
图18‑5看门狗软件层次
18.3.1 看门狗硬件接口层
这一层是给BSP团队看的,设计此层的目的是为了让看门狗模块能够实现硬件上的隔离,同时使我们设计的模块很好地满足跨平台移植的需求。本层有两个主要功能:
1, 作为硬件的转接口,向下提供芯片注册接口,向上提供获取硬件参数接口;
2, 实现启动过程中的喂狗。
WdtHal_RegisterWdtChip负责将我们的硬件看门狗注册进系统的内核中,在此之前,我们应该保证看门狗已经初始化完毕。注册之后,看门狗组件就知道如何操作看门狗硬件了。
硬件看门狗的操作分为两个阶段:OS内核启动前、OS内核启动后。OS内核启动前,即我们所谓的OS启动过程中的喂狗操作;OS启动后,由系统的软件看门狗进行管理。在此我们谈谈启动过程中的喂狗,以及如何交给软件看门狗模块进行管理的。
在Critical的代码中,我们先调用WdtHal_BootStart该功能函数,该函数有一个唯一的参数:喂狗时间,如果为0,表示在启动的过程程中不喂狗,否则代表在启动过程中的喂狗时间,在该API中,会启动一个定时器来触发实时中断来完成喂狗。当系统启动后,会启动软件看门狗模块,在该模块中会调用WdtHal_BootEnd来结束定时器的喂狗,后续硬件看门狗就交给软件看门狗模块来操作。
在此你可能有疑问:如果系统启动不成功怎么办?这个依然在我们的掌控之中。还记得调用WdtHal_BootStart时指定的时间么,该时间就是实时中断喂狗的时间,超过该时间,定时器触发的实时中断将不再喂狗,硬件看门狗自然会复位整个系统。
18.3.2 软件看门狗
18.3.2.1 软件看门狗概述
为什么要做软件看门狗?
在传统嵌入式领域,一般的都是直接使用硬件看门狗,但是这样无疑会有更大的负面作用:
1. 当无硬件看门狗时,使用看门狗的策略将无法实施,或者额外增加硬件导致成本增加;
2. 一般的硬件看门狗只有一只,多个关键任务共同使用这一只或者数量有限的硬件看门狗,导致交叉使用,造成任务同步困难;
3. 多任务交叉使用看门狗时,当某些关键任务运行不正常时,有可能不会产生用户所需要的效果,因为其他使用看门狗的任务有可能还在继续喂狗,导致看门狗功能性失效;
4. 传统意义上的看门狗,当狗叫时产生的效果一般只有复位系统,对于那些有能力通过一定手段修复所属任务出现的问题的软件需求而言,爱莫能助;
5. 传统意义上的硬件看门狗的周期一般是固定的,多个任务使用时其留有很大的余量(一般取最大的一个),导致其保护的任务失败时其响应不及时;
6. 传统意义上的看门狗,当看门狗叫时,只能简单的表示自己的任务超时,无法分析出该任务是因为自己的逻辑错误不能喂狗还是因为系统调度原因导致该任务无法及时喂狗。
以上这些问题,在使用DJYOS的看门狗时都会烟消云散。
18.3.2.2 DJYOS的软件看门狗设计
在DJYOS中,所有的看门狗都由看门狗模块负责管理,不论是“硬件狗”还是“软件狗”,都是平等的,拥有相同的属性,工作机理一致,即规定时间内必须去喂狗,不然的话对应的狗就会叫。大家可能感觉晕晕乎乎,举个例子,大家可能就明白了:闹钟大家都知道,闹铃时间一到,就会响。怎么让闹铃不响呢,要么不开启,要么就是提前重置它。但现在的闹钟更强大了,可以设定多个闹铃时间,且某个闹铃时间到达之前,可以独立地重置掉它,即该时间点不闹铃。这么多闹铃时间在很多时候反映的是你的行动计划。比如闹铃时刻6:00前起床,闹铃时刻7:00前叫醒小孩,闹铃时刻7:45前是出门等等。我们设置多个闹铃时刻却并不需要多个闹钟,只需要支持多闹铃时刻,我们就可以理解为“软件看门狗”了。狗叫后,允许你做一些补救措施(执行狗叫善后回调函数)。需要注意的是,那一只唯一的硬件狗和众多软件模拟的狗操作方式完全相同,硬件狗巧妙地利用了狗叫善后实现喂硬件狗!应用程序永远不要企图直接操作硬件狗,而是喂养软件狗,否则,硬件狗撑死了,就看不了门了。
不论你用看门狗来控制关键代码的运行时间,还是看住关键代码会不会跑飞,都可以简单的认为:看门狗就是为了监控规定的任务有没有在规定的时间内完成!因此从这个本质的问题上而言,用看门狗的地方无疑都可以抽象成如图18‑6所示的简单模型。启动一个定时器,在闹钟超时前必须喂狗,否则狗叫!狗叫后,你有机会补救,补救措施自己写(善后函数)。你可以用善后函数返回值告诉告诉看门狗模块,狗叫以后执行什么操作,也可以不提供善后函数,用看门狗参数直接告诉看门狗模块。
图18‑6中,A是用看门狗检查关键代码有没有定期执行到;B除A的功能外,还检查执行结果是否正确;C比B更彻底,检测到致命问题后,不但不喂狗,还要执行相应的错误处理程序;D用于看住限期完成的任务。E是一个善后处理函数,应用程序提供,由看门狗模块在检测到喂狗异常时调用。应用程序可以在此做各种善后操作,例如发出告警信息等,这个函数在看门狗上下文中调用,使用看门狗模块的栈,最好不要在这里做复杂的工作,局部变量中不要定义缓冲区。
图18‑6应用程序看门狗操作典型模型
18.4 看门狗硬件接口层API
18.4.1 WdtHal_RegisterWdtChip:注册看门狗硬件
bool_t WdtHal_RegisterWdtChip(char *chipname, u32 yipcycle,\
bool_t (*wdtchip_feed)(void),\
fnWdtBootFeedStart bootfeedstart,\
fnWdtBootFeedEnd bootfeedend)
头文件:
os.h
参数:
chipname:看门狗芯片名称,须是全局数组,不能是局部字符串,无名称则为NULL;
yipcycle:看门狗“喂狗”周期,单位:uS。可以设置CYCLE的WDT,该值为设置好后的CYCLE。
wdtchip_feed:硬件看门狗的喂狗方法,例如定期清定时器计数;
bootfeedstart:系统boot过程中开始主动周期喂狗,主要功能是使能看门狗、申请定时器并绑定相应的喂狗ISR等,该HOOK由看门狗组件在合适的位置调用;关于启动过程中的喂狗,参见第18.2节。
bootfeedend:结束启动过程中的主动喂狗,在启动完成后由看门狗模块调用。
返回值:
注册成功返回true;失败则为false。
说明:
18.4.2 WdtHal_GetChipPara:查询芯片信息
bool_t WdtHal_GetChipPara(struct WdtHalChipInfo *hardpara)
头文件:
os.h
参数:
hardpara:获取硬件芯片参数。
返回值:
成功返回true;失败则为false。
说明:
获取看门狗芯片的参数,即注册的信息内容。
18.4.3 WdtHal_BootStart:开始“启动喂狗”服务
bool_t WdtHal_BootStart(u32 bootfeedtime);
头文件:
os.h
参数:
bootfeedtime:在启动加载过程中主动喂狗时长,单位为微秒。
返回值:
开启成功返回true;失败或者无需开启则返回false。
说明:
开启启动加载过程的“喂狗”服务。参数bootfeedtime表示需“喂狗”的时长,实际就是启动加载过程所经历时间长度。系统会根据实际的看门狗周期对这个时间长度向上取整为看门狗周期的倍数。如果用户不想在启动加载过程中启动“喂狗”服务,则将输入参数置为零。关于启动过程中的喂狗,参见第18.2节。
18.4.4 WdtHal_BootEnd:结束“启动喂狗”服务
bool_t WdtHal_BootEnd(void);
头文件:
os.h
参数:
无。
返回值:
开启关闭返回true;失败则返回false。
说明:
关闭启动加载过程中的“喂狗”服务,同时释放相关系统定时器。
注意,本函数依赖系统的时钟模块,即CN_CFG_SYSTIMER == 1。
关于启动过程中的喂狗,参见第18.2节。
18.5 看门狗API
18.5.1 Wdt_Create:创建看门狗
tagWdt *Wdt_Create(char *dogname,u32 yip_cycle,\
fnYipHook yiphook,
enum EN_ExpAction yip_action,
u32 ExhaustLevelSet,
u32 ExhaustLimit)
头文件:
wdt.h
参数:
dogname:看门狗名称,不能为空;
yip_cycle:“喂狗”周期,单位是微秒,该值会自动向上取整为系统周期的整数倍;
yiphook:狗叫善后函数,即狗叫的钩子函数,该函数在看门狗发生超时后被执行。函数可以为空,意味着不需要善后函数。应用程序如果提供了yiphook函数,就应该在这个函数中返回后续行动,是复位还是其他,参见enum EN_ExpAction的定义。在调用完本函数之后,还将抛出异常,本函数的返回值将作为Exp_Throw函数的参数。
yip_action:由本看门狗溢出时的后续行动,作为Exp_Throwh函数抛出异常时的输入参数,注意,只有狗叫善后函数yiphook非空,会被yiphook函数的返回值替代。
ExhaustLevelSet:用于分辨狗叫原因,如果发生看门狗溢出,在该狗叫周期内,若相应事件执行时间大于ExhaustLevelSet(uS),则判定为被看护的事件本身逻辑错误,即被监控代码有时间运行却不喂狗;若事件执行时间小于ExhaustLevelSet(uS),则判定为调度问题,例如高优先级事件或者中断长时间占用CPU,导致被监控代码没有机会执行。
ExhaustLimit:因调度而导致狗叫的容忍次数。如果因调度而导致狗叫,说明狗叫并非被监控代码的逻辑错误引起,可以用本函数设定容许次数,超过该限制才采取行动(抛出异常),以免被冤枉,否则只是记录狗叫。
本参数典型用于容许IO延迟,具体如下:
假定被监视代码正常时1秒必须执行一次,则cycle可设为1.2S(留点余量)。但被监控代码在某特定条件下(if语句),要执行IO操作,该IO操作可能被阻塞5秒。则可在执行IO操作前,把ExhaustLimit设为5(用EN_WDTCMD_SETSCHLEVEL命令)。则在6秒内,即使狗叫也只是记录而已。
返回值:
创建成功则返回看门狗指针;否则返回NULL。
说明:
该函数用于创建一个看门狗,该看门狗的控制块空间由系统自行分配,因此不允许用户做擅自做任何的更改。如果没有在规定的周期内(yip_cycle)喂狗,就会认定该看门狗狗叫,执行相应动作。因此,请务必慎重设定周期,留给自己余量。
看门狗叫的钩子函数的返回值类型enum EN_ExpAction在exp_api.h文件定义。具体行为可参看该文件注释。
注意:执行本函数后会自动喂狗一次!
18.5.2 Wdt_Create_s:创建静态看门狗
tagWdt *Wdt_Create_s( tagWdt *wdt,
char *dogname,
u32 yip_cycle,
enum EN_ExpAction yip_action,
u32 ExhaustLevelSet,
u32 ExhaustLimit)
头文件:
wdt_soft.h
参数:
wdt:看门狗控制块指针,不能为空。
其余参数函数Wdt_Create与一致。
返回值:
创建的看门狗指针否则NULL。
说明:
功能与Wdt_Create类似,但调用者须提供看门狗控制块结构体,然后把指针传递过来,高可靠性的应用中,不应该使用动态分配的方式,静态定义更可靠。
18.5.3 Wdt_Delete:删除看门狗
bool_t Wdt_Delete(tagWdt *wdt)
头文件:
wdt_soft.h
参数:
wdt:被删除的看门狗。
返回值:
删除成功返回true;否则返回false。
说明:
删除一只看门狗,并释放看门狗控制块占用的内存块。注意,如果在关调度期间、或者在异步信号ISR中调用本函数,将不会成功。
18.5.4 Wdt_Delete_s:删除静态看门狗
bool_tWdt_Delete_s(tagWdt *wdt)
头文件:
wdt_soft.h
参数:
wdt:被删除的看门狗。
返回值:
删除成功返回true;否则返回false。
说明:
删除一个由Wdt_Create_s创建的看门狗。如果在关调度期间、或者在异步信号ISR中调用本函数,将不会成功。
18.5.5 Wdt_Clean:喂狗
bool_t Wdt_Clean(tagWdt *wdt);
头文件:
wdt_soft.h
参数:
wdt:被喂的看门狗。
返回值:
删除成功返回true;否则返回false。
说明:
应用程序须确保在看门狗的狗叫周期内,调用本函数清看门狗计时器,否则狗叫。
18.5.6 Wdt_Ctrl:设置看门狗
bool_t Wdt_Ctrl(tagWdt *wdt, u32 type, ptu32_t para)
头文件:
wdt_soft.h
参数:
wdt:看门狗指针。
type:设置命令;
para:设置参数,参数含义依据设置命令。
返回值:
设置成功返回true;否则返回false。
说明:
设置看门狗参数。设置命令如下表:
设置命令 |
描述 |
EN_WDTCMD_PAUSE |
暂停一只看门狗,实际上是通过把这个狗的狗叫间隔临时设为近似无穷大来实现的。被监控程序需要执行非常耗时且不确定需要多长时间的操作,且期间不能喂狗的,可以调用本函数临时停止看门狗。例如调用执行时间不确定的第三方库函数。 |
EN_WDTCMD_RESUME |
恢复一只看门狗运行。注意:下次狗叫时间是“当前时间+timeout”,而不是调用wdt_pause的剩余时间。 |
EN_WDTCMD_SET_OWNER |
设置看门狗主人,即被监护的代码所属事件ID |
EN_WDTCMD_SET_CYCLE |
设置看门狗狗叫周期 |
EN_WDTCMD_SET_YIP_ACTION |
设置看门狗的狗叫后动作 |
EN_WDTCMD_SET_HOOK |
设置狗叫钩子函数 |
EN_WDTCMD_SET_CONSUME_LEVEL |
设置狗叫时原因判断标准,参见Wdt_Create函数的说明 |
EN_WDTCMD_SET_SCHED_LEVEL |
设置调度原因导致的连续狗叫次数忍耐限度,参见Wdt_Create函数的说明 |
EN_WDTCMD_INVALID |
无效操作 |
注意:如果在任务中执行该API会导致喂看门狗一次!
第19章 网络
19.1 板件网络配置
当你的网卡驱动将网卡设备注册进协议栈之后,仅仅表示协议栈里面有你这么个网卡设备存在,但是就像你买车还要上牌一样,你要为这个网卡设备设置IP,不然上层是无法使用这个网卡进行数据传递的!djyos源码的bsp中,所有带网卡的demo板,在“djysrc\bsp\boarddrv\demo\APOLLO-STM32F7\drv\boardnetcfg”目录下都会有一个boardnetcfg.c文件,用于配置网卡的IP地址、网关、子网掩码等。
设置好板件的boardnetcfg.c文件后,就可以在DIDE中配置网卡:
图 19‑1 DIDE中配置网络
其中网卡的名字,是安装网卡时命名的,参见第19.3.2.1节。
车只能上一个牌,但网卡可以设置很多IP的,djyos提供的范例中,一般固定IP地址和可配置的IP地址各设置了一个,用户可依葫芦画瓢,设置更多的IP。
同时,该文件中也提供了DHCP动态获得IP地址的例子。
19.2 套接字编程API
19.2.1 BSD兼容API
DJYIP协议栈提供基本兼容BSD的socket接口,参考下述文档。
19.2.1 socket_SetUserTag:设置用户私有数据
ptu32_t socket_SetUserTag (s32 sockfd, ptu32_t UserTag);
头文件:
socket.h
参数:
sockfd:socket文件描述符。
UserTag,新的用户标签
返回值:
原来的UserTag。
说明:
DJYOS的每个socket,允许用户设置一个标签,用于标识用户的特定信息。例如,在并发通信多路复用应用中,用户程序取得socket后,却无从分辨这个socket的用途,不知道下一步怎么执行,有UserTag后,就可以用它来做标识。
19.2.2 socket_GetUserTag:取用户私有数据
ptu32_t socket_GetUserTag (s32 sockfd);
头文件:
socket.h
参数:
sockfd:socket文件描述符。
返回值:
用户标签 UserTag。
说明:
DJYOS的每个socket,允许用户设置一个标签,用于标识用户的特定信息。例如,在并发通信多路复用应用中,用户程序取得socket后,却无从分辨这个socket的用途,不知道下一步怎么执行,有UserTag后,就可以用它来做标识。
19.3 驱动程序
19.3.1 驱动和协议栈的关系
硬件要想接入协议栈,必然要通过驱动,那么驱动又是如何接入协议栈呢?整个流程如图19‑2所示。
图19‑2驱动接口和协议栈数据流图
从图中可以看到,作为写驱动的coder而言,主要关心的问题有两个:
1、 上层是怎么调用我们的驱动提供的函数的。
2、 我们网卡接受的数据是怎么上送给上层的。
3、 上层怎么设置网卡的工作参数的
协议栈提供了一系列的API和驱动程序对接,详见下节:
19.3.2 驱动相关API
19.3.2.1 NetDevInstall:安装网卡
struct NetDev *NetDevInstall(struct NetDevPara *para);
头文件:
socket.h
参数:
para:安装网卡的参数,struct NetDevPara结构定义如下表:
返回值:
新安装的网卡设备指针。
说明:
安装网卡后,就可以用网卡收发数据了,但要作为IP协议的收发设备,还要按第19.1节所示方法,配置IP地址才行。函数接口如下:
name:大家都知道,网卡名字,注意在协议栈中网卡主要靠名字来区分,因此要求不重名,并且是静态的数据(非可变的);网卡名,是不是有似曾相识的感觉?不错,在图 19‑1中提到的,就是它。
iftype:网卡类型,即链路层类型,由 LinkRegister 函数注册,每种类型对应不同的链路层收发函数,由 struct LinkOps 定义,enum enLinkType定义如下:
ifsnd:我们的网卡对外发送函数,这个后文会详细讲解,在此不做赘述;
ifctrl:获取网卡状态和控制网卡的函数,NetDevCtrl函数最终调用的是本函数。
devfunc:向协议栈声明网卡有什么样的硬件加速手段,比方TCP校验等,有硬件处理能力则填写对应的,没有则不用填,用位域表示,定义:
mtu:网卡一次能发送的最大数据包,上层(典型的是IP层)可能以此来分拆数据包。
Private:又是private,和套接字的private有异曲同工之妙,不过这个是面向驱动程序的,你可能有几个相同的网卡需要添加,那么你这几个网卡总需要区分吧,那么怎么区分呢?可以在注册的时候设置上这个Private。当你注册得到一个netdev的时候,使用NetDevPrivate(handle)就能获取你设置的private了,是不是很机智呢。
mac:网卡的mac地址,使用MAC进行通信的网卡则必须提供;如果不是则不必提供,比方LOOP回环网卡;
返回值:
新安装的网卡指针,失败则返回NULL。
说明:
关于安装网卡,需要注意的是,我们不允许同名网卡或者名字为空的网卡存在,否则就会安装失败。
19.3.2.2 NetDevUninstall:卸载网卡
ptu32_t NetDevUninstall(const char *name)
头文件:
socket.h
参数:
name,就是你安装的时候指定的网卡名字
返回值:
true or false。
说明:
有安装网卡,自然有卸载网卡。
19.3.2.3 NetDevName:取网卡名
const char *NetDevName(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
指向网卡名的指针,无此网卡则返回NULL。
说明:
无
19.3.2.4 NetDevGet:取网卡结构指针
struct NetDev *NetDevGet(const char *ifname);
头文件:
socket.h
参数:
ifname,安装网卡时指定的名字,无此网卡则返回NULL。
返回值:
指向网卡控制块的指针。
说明:
无
19.3.2.5 NetDevFunc:取网卡加速功能
u32 NetDevFunc(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
表示网卡加速功能的32位位域标志,由 CN_IPDEV_TCPOCHKSUM 等定义。
说明:
返回值表示网卡的能力,现在许多网卡除了标准网卡的功能外,还能检查和填写chesum,用好了,能极大地提高协议栈的性能。
19.3.2.6 NetDevMtu:取网卡mtu
u16 NetDevMtu(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
网卡mtu。
说明:
无
19.3.2.7 NetDevType:取网卡链路层类型
enum enLinkType NetDevType(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
网卡类型。
说明:
每块网卡的类型在安装网卡时确定,使用过程中不能修改,网卡类型由enum enLinkType 枚举,不同类型有不同的链路层收发函数(struct LinkOps),由 LinkRegister 函数注册。
19.3.2.8 NetDevLinkOps:取网卡链路层收发函数
struct LinkOps *NetDevLinkOps(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
链路层收发函数结构指针。
说明:
由网卡类型对应的收发函数指针。网卡收到数据后,由网卡驱动push数据到链路层,然后调用linkin函数,由该函数进一步向上push,调用路径是:网卡驱动—>NetDevPush—> LinkDeal—> ops->linkin—> LinkPush;
发送数据时,上层(例如IP层)发送—> LinkSend—> ops->linkout—> NetDevSend—> handle->ifsend(网卡驱动提供,NetDevInstall中注册)。
typedef bool_t (*fnLinkIn)(struct NetDev *iface,struct NetPkg *pkg); typedef bool_t (*fnLinkOut)(struct NetDev *iface,struct NetPkg *pkglst,u32 devtask,u16 proto,\ enum_ipv_t ver,ipaddr_t ipdst,ipaddr_t ipsrc); struct LinkOps { fnLinkIn linkin; fnLinkOut linkout; }; |
19.3.2.9 NetDevGetMac:取网卡mac地址
const u8 *NetDevGetMac(struct NetDev *DevFace);
头文件:
socket.h
参数:
DevFace,网卡设备指针
返回值:
网卡的mac地址缓冲区指针。
说明:
网卡的Mac地址,实在调用NetDevInstall安装网卡时指定的,也可以调用NetDevCtrl函数用EN_NETDEV_SETMAC命令修改。
注意,为方便访问,Mac地址在 struct NetDev结构以及硬件驱动使用的硬件控制块中都保存了一份。
19.3.2.10 NetDevRegisterEventHook:设置网卡事件钩子
bool_t NetDevRegisterEventHook(struct NetDev *handle,fnNetDevEventHook hook);
头文件:
socket.h
参数:
handle,网卡设备指针
hook,钩子函数指针,其类型声明如下:
typedef bool_t (*fnNetDevEventHook)(struct NetDev* iface,enum NetDevEvent event);
返回值:
true。
说明:
当发生网卡事件时,将调用hook函数,网卡事件的定义参见第19.3.2.12节。
注意这个钩子函数,不要跟网卡接收钩子函数(由NetDevCtrl函数使用EN_NETDEV_SETHOOK命令注册)混淆,该钩子函数是只要收到数据就调用的。
19.3.2.11 NetDevRegisterEventHook:注销网卡事件钩子
bool_t NetDevUnRegisterEventHook(struct NetDev * handle);
头文件:
socket.h
参数:
handle,网卡设备指针
返回值:
true。
说明:
当发生网卡事件时,将调用hook函数,网卡事件的定义参见第19.3.2.12节。
19.3.2.12 NetDevPostEvent:抛出网卡事件
bool_t NetDevPostEvent(struct NetDev* handle,enum NetDevEvent event);
头文件:
socket.h
参数:
handle,网卡设备指针。
event被抛出的事件,定义如下:
enum NetDevEvent { //THE OLD ONES ARE DEPRECATED EN_NETDEVEVENT_LINKDOWN = 0, //means the phy down or network cable is pullout EN_NETDEVEVENT_LINKUP, //means the phy up or network cable has been inserted ok EN_NETDEVEVENT_IPGET, / means has get ip from dhcp or ppp EN_NETDEVEVENT_IPRELEASE, / means has release ip to dhcp or ppp down EN_NETDEVEVENT_RESET, // means the dev has been reset for some reason EN_NETDEVEVENT_BROAD_OVER, //means the broad over EN_NETDEVEVENT_BROAD_LACK, //means the broad lack EN_NETDEVEVENT_MULTI_OVER, //means the multi over EN_NETDEVEVENT_MULTI_LACK, //means the multi lack, EN_NETDEVEVENT_POINT_OVER, //means the point over EN_NETDEVEVENT_POINT_LACK, //means the point lack, EN_NETDEVEVENT_FLOW_OVER, //means the FLOW over EN_NETDEVEVENT_FLOW_LACK, //means the FLOW lack, EN_NETDEVEVENT_RESERVED, //which means nothing }; |
返回值:
true。
说明:
协议栈运行过程中,将检查通信数据流,发现enum NetDevEvent 定义的事件出现时,将post相应事件,注意这里说的“事件”与“事件调度”的“事件”不是同一个概念。
post 事件后,如果网卡注册了钩子函数(参见第19.3.2.10节),则调用之,否则,如果是debug版本,则打印事件,release则什么都不做。
19.3.2.13 NetDevCtrl:网卡控制
bool_t NetDevCtrl(struct NetDev* handle,enum NetDevCmd cmd, ptu32_t para);
头文件:
socket.h
参数:
handle,网卡设备指针。
cmd,控制命令。
para,控制命令的参数,随cmd的不同,含义不同。
enum NetDevCmd的定义如下:
enum NetDevCmd { EN_NETDEV_SETNOPKG = 0, //PARA IS NOT CARE EN_NETDEV_SETBORAD, //para is int,0 disable else enable EN_NETDEV_SETPOINT, //para is int,0 disable else enable EN_NETDEV_SETMULTI, //para is int,0 disable else enable EN_NETDEV_SETRECV, //para is int,0 disable else enable EN_NETDEV_SETSEND, //para is int,0 disable else enable EN_NETDEV_SETMAC, //para point to an buf which contains the mac //driver must modify the dev struct mac at the same time EN_NETDEV_SETMULTIMAC, //para point to an buf which contains the mac EN_NETDEV_SETHOOK, //para is a receive hook fucntion name EN_NETDEV_GTETMAC, //para point to an buf which used to contain the mac EN_NETDEV_RESET, //para must be true EN_NETDEV_LOWPOWER, //para is int,0 disable else enable EN_NETDEV_ADDRFILTER, //开启网卡Mac地址过滤功能 EN_NETDEV_CMDLAST, //which means the max command }; |
返回值:
true。
说明:
注意EN_NETDEV_SETHOOK命令,改该命令要求提供一个钩子函数,该钩子函数与第19.3.2.10节所述的钩子函数是不一样的,前者只要硬件收到数据就会被调用,例如可用于抓包;后者用于发生指定的网络事件后调用用户的处理函数,例如流量越限等。
19.3.2.14 NetDevSend:发送数据
bool_t NetDevSend(struct NetDev* handle,struct NetPkg *pkglst,u32 netdevtask);
头文件:
socket.h
参数:
DevFace,网卡设备指针。
pkglst,待发送的数据,按包链表组织。
netdevtask,需要网卡完成的任务,如 CN_IPDEV_NONE。利用这个成员,可以充分发挥网卡设备的加速功能,参见第19.3.4.1节。
返回值:
网卡的mac地址缓冲区指针。
说明:
本函数只是个接口,将调用handle->ifsend函数执行数据发送工作。
19.3.2.15 NetDevFlowSet:网卡流控设置
bool_t NetDevFlowSet(struct NetDev* handle, enum EthFramType type,\
u32 llimit,u32 ulimit,u32 period,int enable);
头文件:
socket.h
参数:
DevFace,网卡设备指针。
type,流控类型,定义如下:
typedef enum { EN_NETDEV_FRAME_BROAD = 0, //广播包流控 EN_NETDEV_FRAME_POINT, //多播包流控 EN_NETDEV_FRAME_MULTI, //单播包流控 EN_NETDEV_FRAME_ALL, //包含所有数据包流控 EN_NETDEV_FRAME_LAST, }enNetDevFramType; |
llimit,在period时间内,收到type类型的包数低于此限,即 NetDevPostEvent 网卡事件。
ulimit,在period时间内,收到type类型的包数超过此限,即 NetDevPostEvent 网卡事件。
period,流量统计周期,单位是uS。
enable,流控是否使能,1=使能type类型的包流控,false = 禁止。
返回值:
true = 成功设置,false = 失败。
说明:
网卡收到数据包时,将在网卡接收函数里,调用流量检查功能,检查在period周期内的流量是否越限(上限或下限)。
19.3.2.16 NetDevFrameType:检查数据帧类型
enum EthFramType NetDevFrameType(u8 *buf,u16 len);
头文件:
socket.h
参数:
buf,接收到的帧缓存地址。
len,数据帧缓冲区长度。
返回值:
以太网包的类型,enum EthFramType定义参见19.3.2.15节。
说明:
本函数一般由网卡驱动执行流控或者包过滤时使用。
19.3.2.17 NetDevFlowCtrl:执行流量统计和检查
bool_t NetDevFlowCtrl(struct NetDev* handle,enum EthFramType type);
头文件:
socket.h
参数:
handle,网卡设备指针。
type,被检查的数据帧类型,enum EthFramType定义参见19.3.2.15节。
返回值:
是否
说明:
本函数一般由网卡驱动执行流控或者包过滤时使用。
19.3.2.18 NetDevPrivate:取网卡设备私有数据
void * NetDevPrivate(struct NetDev *iface);
头文件:
socket.h
参数:
iface,网卡设备指针。
返回值:
网卡设备的私有数据指针
说明:
网卡设备的私有数据结构指针,含义参见第19.3.2.1节。
19.3.3 数据包管理
19.3.3.1 数据包链表结构
协议栈中的数据流动,无论是向上还是向下,都是以包链的形式流转,包链节点的由struct NetPkg构成。
图19‑3数据包结构
struct NetPkg 定义如下:
Pkg的结构如图19‑3所示,协议栈维护着一个这样的链表,每次要发送的数据包,就是这个链表的一组连续节点组成。驱动层的发送函数,就从pkglst指针所指的pkg开始,依次取出pkg中的有效数据,从物理层发送出去,直到PkgIsBufferEnd 返回true,先取出的先发。在这里有必要陈述一个问题:为什么要做成这么一个链表结构来表示一帧数据而不是将所有的数据存储一个pkg中?这是由网络的自上而下的发送特点决定的,网络数据包自上而下每经过一个层时,都要加上自己的协议头,如果不构成一个链表结构的话,那么我们在分配PKG的时候必然要预留很多空间,对于缺少资源的嵌入式系统而言,是不合适的;因此在该协议栈中,每当添加一个协议头的时候,我们只需要在当前的链表前添加一个PKG即可。
下面说说这6个函数和宏:
PkgCachedLst和PkgCachedPart:一般来说,fnIfSend函数返回时,pkglst中的数据已经不再需要,协议栈会释放这些数据。但有些网卡,这样做并不方便,特别是一些使用DMA链表的网卡,函数返回后,数据还没有发送完毕,可能仍然需要使用这些数据,就需要在函数返回前,调用这两个函数通知协议栈。PkgCachedPart会hold住一个pkg单元,PkgCachedLst将hold住整个数据包。
PkgTryFreeLst和PkgTryFreePart:如果数据被hold住,驱动使用这些数据完毕时,需要调用这两个函数通知协议栈释放这些节点。
PkgMalloc:这是给接收函数用的函数,用于分配一个tagNetPkg类型的数据结构,同时分配一个尺寸为bufsize的缓冲区。
PkgIsBufferEnd:判断一个pkg是否为一个pkglst的末节点(链表结尾的判断方法务必使用该宏)。
19.3.3.2 数据包内存分配
19.3.4 网卡驱动BSP
19.3.4.1 网卡驱动发送函数
我们在注册网卡的时候已经看到了网卡的发送函数的原型:
Netdev:很好理解,就是我们前面注册得到的网卡句柄,为什么要传递这么一个参数?亲,不要忘记了,你可能有很多块网卡哦,不传递下来的话,你怎么知道使用哪一个网卡呢?不然你怎么获取你设置的网卡的private数据呢?莫非你注册网卡的时候,是同一个ifsnd拷贝成多个ifsnd1 ifsnd2 ifsnd3….估计你老大不会同意的吧。
Pkglst:一个数据包,由若干个pkg单元构成的链,pkglst指向数据包中的第一个pkg节点,参见第19.3.3.1节。
Netdevtask:亲是否还记得我们注册网卡的时候的devfun?其比特位定义和devfun的定义是一致的,这个就是我们协议栈要求驱动要完成的内容,比方做TCP校验和计算、IP校验和计算等,当然如果你注册的时候没有夸下海口,我相信协议栈是不会为难你的。
OK,到此为止我们已经讲解了如何注册网卡驱动以及如何设计我们的发送函数,朦胧之中,我们发现貌似还少一样东西没有做:网卡收到数据之后如何交给协议栈呢?请看下文分解!
19.3.4.2 网卡上送数据
历来发送和接收都是冤家,上文讲述了网卡的发送,那么当我们的网卡接收到数据后(可能中断接收,也可能任务轮询接收,在此不做假设)怎么上送给协议栈?嘿,这个超级简单,如下:
将收到的数据打包为Pkg(将接收到的一帧数据拷贝到Pkg的缓冲区中),需要注意的是:
1、 Pkg需要你自己PkgMalloc获取,flag标志务必设为CN_PKLGLST_END。
2、 该函数返回之后,你自然要调用PkgFree(pkg)来释放掉你申请的Pkg。
3、 网卡接收相关的硬件加速功能,请务必在接收的时候使能!
特别提醒:很多网卡的发送和接收不是相互独立的,在硬件上可能共享很多公用寄存器,那么此时就需要驱动程序自己做好互斥等工作了,接收完数据后,一定要在上送数据前释放互斥锁,以使网卡能正常发送!
19.4 链路层API
19.4.1 Link_RegisterRcvHook:注册链路层协议
bool_t Link_RegisterRcvHook(fnLinkProtoDealer hook,const char *ifname,u16 proto,
const char *hookname);
头文件:
socket.h
参数:
hook,协议处理回调函数,定义如下:
typedef bool_t (*fnLinkProtoDealer)(struct NetDev *iface,struct NetPkg *pkg);
ifname,指定网卡名。
proto,协议号,不可以标准定义的(0800、0806、0835、86dd)相同。
hookname,协议名称
返回值:
true = 成功,false = 失败;
说明:
当802.3协议网卡收到proto类型的协议数据包时,将调用hook函数。
19.4.2 Link_UnRegisterRcvHook:注销链路层协议
bool_t LinkUnRegisterRcvHook(const char *hookname);
头文件:
socket.h
参数:
hookname,协议名称
返回值:
true = 成功,false = 失败;
说明:
无。
19.5 应用层协议
DJYOS支持众多应用层协议,并且在不这。断增加中,在本手册中难于尽述,各应用层协议的手册,可在其源码目录中找到。
19.6 网络报文数据流图
19.6.1 网络报文接收数据流图
图19‑4网络报文接收数据流图
19.6.2 网络报文发送数据流图
图19‑5网络报文发送数据流图
第20章 消息队列
消息队列是事件之间的一种通信机制,可以实现一个事件向另一个事件发送变量等数据。
为了使用DJYOS的消息队列功能,必须在低的中勾选“message queue”组件,可以在创建工程时勾选,也可以在工程属性中勾选。
图 20‑1勾选消息队列组件
DJYOS允许创建多个消息队列,每个消息队列设定为两种管理模式之一:CN_MSGQ_TYPE_PRIO和CN_MSGQ_TYPE_FIFO模式,当因多个事件并发访问同一个消息队列而被阻塞时,不同模式事件的排队方式不同,前者是按访问消息队列的先后顺序排队,先到者排前面;后者是按被阻塞的事件的优先级排队,高优先级的拍前面。
写入到消息队列中的消息,也有两种排队方式:CN_MSGQ_PRIO_NORMAL和CN_MSGQ_PRIO_URGENT方式,前者按自然顺序,即先写入的消息先被读取;后者是紧急消息,直接插队到最前面。
20.1 相关API说明
20.1.1 MsgQ_Create:创建消息队列
struct MsgQueue *MsgQ_Create(u32 MaxMsgs, u32 MsgLength, u32 Options);
头文件:
msgqueue.h
参数:
MaxMsgs:消息队列所能容纳消息的数量上限。
MsgLength:单个消息长度(字节数)。
Options:消息队列属性。CN_MSGQ_TYPE_PRIO表示优先级类型的消息队列;CN_MSGQ_TYPE_FIFO表示先进先出类型的消息队列。
返回值:
创建成功返回消息队列句柄(控制块);失败则返回NULL。
说明:
创建一个消息队列。
20.1.2 MsgQ_Delete:删除消息队列
bool_t MsgQ_Delete(struct MsgQueue *pMsgQ);
头文件:
msgqueue.h
参数:
pMsgQ:消息队列句柄(控制块)。
返回值:
删除成功返回true;失败则返回false。
说明:
删除消息队列,与MsgQ_Create对应。
20.1.3 MsgQ_Send:发送消息
bool_t MsgQ_Send(struct MsgQueue *pMsgQ,u8 *buffer,u32 nBytes,u32 Timeout,
bool_t priority);
头文件:
msgqueue.h
参数:
pMsgQ:消息队列句柄(控制块)。
buffer:消息缓冲区指针。
nBytes:消息大小(字节数),不能超过创建消息队列时指定的MsgLength。
timeout:等待消息队列时间。
priority:消息紧急属性。CN_MSGQ_PRIO_URGENT表示紧急消息,消息将被放于消息队列头部;CN_MSGQ_PRIO_NORMAL表示普通消息,消息则将消息队列尾部。
返回值:
发送成功返回true;失败则返回false。
说明:
通过消息队列发送一个消息,如果消息队列已满,则阻塞,直到有其他线程从消息队列中接收消息,或者超时时间到。特别注意,如果连续发送紧急消息,先前发送的没有及时读取的话,后发送的会排到先发送的前面。
20.1.4 MsgQ_Receive:接受消息
bool_t MsgQ_Receive(struct MsgQueue *pMsgQ,u8 *buffer,u32 nBytes,u32 Timeout)
头文件:
msgqueue.h
参数:
pMsgQ:消息队列句柄(控制块)。
buffer:消息数据空间。
nBytes:消息大小(字节数),不能超过创建消息队列时指定的MsgLength。
Timeout:等待消息的时间。
返回值:
读取成功返回true;失败则返回false。
说明:
从消息队列读取一个消息。
当消息队列中无消息,且设置了等待消息时间,则会引起事件阻塞。
20.1.5 MsgQ_NumMsgs:查询消息数
u32 MsgQ_NumMsgs(struct MsgQueue *pMsgQ)
头文件:
msgqueue.h
参数:
pMsgQ:消息队列句柄(控制块)。
返回值:
消息队列中的消息数。
说明:
查询消息队列中的消息数。
第21章 DjyBus总线
嵌入式系统中,总线通信是很常见的通信方式。因此,在DJYOS中,将总线通信驱动模块标准化和模块化,使其成为操作系统的重要模块,能有效降低程序复杂度,使程序设计、调试和维护等操作简单化。DJYOS的总线驱动模块是一个独立的组件,虽然许多硬件设备需要使用这个组件来与系统连接,但总线驱动模块本身和设备驱动架构并无必然联系。DJYOS小心避免出现大而复杂的组件,努力使系统由小而完备、易于理解、互相没有耦合的组件组件构成。
要使用DjyBus总线,可创建工程时,勾选相应组件,并且配置运行参数即可,如果创建工程时忘了勾选,随时打开工程属性界面,也可以修改配置。
图 21‑1 配置DjyBus组件
21.1 DjyBus架构模型
DjyBus用总线对象树组织各类总线,如IIC总线、SPI总线、PCI总线、USB总线,他们被挂接到“DjyBus”目录下面。每类总线又允许多条总线(有很多cpu可能会有不止一个IIC总线控制器,每条IIC总线控制器又挂接在“IIC”目录下,对象名字建议为“IICn”,其中n是总线编号。每增加或删除一个器件,相应的树上便增加或减少一个对象。
图21‑2DjyBus框架示意图
21.2 DjyBus API说明
系统在创建、删除总线类型等方面提供以下API函数:
21.2.1 DjyBus_BusTypeAdd:添加总线类型
struct Object * DjyBus_BusTypeAdd (char* NewBusTypeName);
头文件:
djybus.h
参数:
NewBusTypeName,总线类型结点的名称。
返回值:
新增的总线类型子目录对象指针,增加失败时返回NULL。
说明:
在“\DjyBus”目录上增加 NewBusTypeName 子目录,该子目录代表一类总线。
21.2.2 DjyBus_BusTypeDelete:删除总线类型
bool_t DjyBus_BusTypeDelete(struct Object * DelBusType);
头文件:
djybus.h
参数:
DelBusType,删除的总线类型子目录指针。
返回值:
true,删除成功;false,删除失败。
说明:
无。
21.2.3 DjyBus_BusTypeFind:查找总线类型
struct Object *DjyBus_BusFind( char *BusTypeName);
头文件:
djybus.h
参数:
BusTypeName,查找的总线类型子目录名称。
返回值:
总线类型对象指针,未查找到返回NULL。
说明:
在“\DjyBus”目录上查找总线类型目录。
21.3 编写IIC总线及器件驱动
DjyBusV1.1.0版本的IIC驱动设计适用性说明如下:
1. IIC工作于主模式,与从设备进行通信;
2.不支持自动重发机制,发生错误时,采用弹出事件的方式告知应用层;
3.不支持仲裁丢失重新竞争机制,采用弹出事件方式告知应用层。
21.3.1 IIC总线框架
DjyBus将IIC程序分成IIC通用接口和底层驱动两个部分,如图21‑3所示。IIC通用接口是不以具体硬件平台而变化的公共代码部分,它为上层应用(通常是挂接在IIC总线上的器件驱动,如PCF8563)提供标准化接口函数,并对IIC总线操作提供各种信号量互斥访问和缓冲区保护。
底层驱动部分与具体硬件平台息息相关,IIC驱动移植到不同的平台时,需要在该层进行硬件适配。DjyBus已经将整个底层驱动的框架搭建完整,移植到不同平台只需要填鸭式的完成寄存器级的工作,使重复性工作尽量降少。
底层驱动程序注册回调函数与IIC总线组件对接,将通用接口与底层隔离,通用接口层可进行独立编译,与具体硬件耦合度降低。
|
图21‑3 IIC驱动框架示意图
21.3.2 IIC总线驱动接口
DJYOS提供的BSP中,如果CPU有IIC设备,则会提供IIC设备驱动,创建工程时,只需勾选“cpu onchip iic”组件,并且配置运行参数即可,如果创建工程时忘了勾选,随时打开工程属性界面,也可以修改配置。
图 21‑4 配置IIC总线硬件驱动
DJYOS提供了标准的IIC驱动模型,但是具体的IIC驱动芯片底层驱动程序不同,因此硬件驱动需要软件开发人员自己开发。
IIC总线为IIC器件提供标准的、一致的应用程序编程接口,并且规范了硬件驱动接口。驱动接口分为总线控制器接口和IIC器件接口两部分,驱动的重点是总线控制器,而器件接口实际上就是配置一下该器件的物理参数。IIC总线驱动需要完成以下任务:
1、 IIC驱动程序编写重点有:初始化IIC控制器,并且把IIC总线添加到DjyBus上;
2、 实现图21-2中函数指针表中5个回调函数;
3、 如果采用中断方式,须编写中断服务函数(实际上也是为4个回调函数服务)。
21.3.2.1 IIC控制器初始化
初始化函数实现步骤如下:
step1:初始化硬件
1、 gpio设置,每块板都会有一个board.c,在该文件中实现初始化IIC所用的gpio函数,供cpu_peri_iic.c文件调用。
2、 IIC控制器硬件的初始化,包括传输速度、IO配置等;
3、 挂载IIC中断到中断系统,并配置中断类型,如配置为异步信号(若只采用轮询方式,则此功能可省略);
step2:添加到对象目录
添加IIC总线的参数类型为struct IIC_Param,由函数IIC_BusAdd完成对IIC总线控制块ICB的初始化和添加IICn目录到“\DjyBus\IICBus”目录。参见第21.3.4.1节。
21.3.2.2 回调函数
1.轮询函数 pWriteReadPoll
如果采用轮询(用IIC_BusCtrl函数、CN_IIC_SET_POLL命令设置)方式收发,5个回调函数中只需要实现这一个,其他指针置为NULL即可。
IICBus模块将在以下情况下使用轮询方式:
1、 用IIC_BusCtrl函数、CN_IIC_SET_POLL命令把器件所述总线收发方式被设为轮询方式,默认值为中断方式。
2、 在禁止调度(即禁止异步信号中断)期间读写IIC器件,强制使用轮询方式。
3、 pGenerateReadStart==NULL,则使用轮询方式读IIC器件;pGenerateWriteStart==NULL,则使用轮询方式写IIC器件。
4、 系统初始化未完成,多事件调度尚未启动期间。
如果使用中断方式收发,且不考虑在2~4三种情况下收发数据,则无须实现本函数,WriteReadPoll指针设为NULL即可。
回调函数说明如下:
typedef bool_t (*WriteReadPoll)(ptu32_t SpecificFlag,u8 DevAddr,u32 MemAddr, u8 MenAddrLen,
u8* Buf, u32 Length,u8 WrRdFlag);
参数:
SpecificFlag:IIC硬件驱动调用IIC_BusAdd时给定的私有数据,通常是寄存器或IIC端口号。
DevAddr:设备地址,低七位有效。
MemAddr:设备内部地址,若为存储设备,则为存储地址。
MemAddrLen:设备内部地址字节数。
Buf:数据缓冲区。
Length:Buf中数据字节数。
WrRdFlag:读写标志,0为写,1为读。
返回值:
true, 执行成功;false,执行失败。
说明:
轮询函数在执行前必须关闭IIC中断,否则将执行失败。
2.启动发送
启动发送函数(WriteStartFunc)是为中断方式收发服务的,轮询方式不需要此函数,置为NULL即可。
启动发送的回调函数WriteStartFunc完成了发送数据时IIC的启动时序,其执行的流程为start---->发送器件地址---->发送内部地址,并开启中断,然后返回。对应在时序上,如图21‑5启动发送所示。
图21‑5启动发送
如图图21‑5启动发送中所示,写时序是以主控制器发送start信号为起始条件,紧接最低位为0从器件地址表示写操作,当收到ACK信号后会将从器件的存储地址(图中地址为2字节,具体多少字节视情况而定)发送到总线,最后发送正式的正文。
回调函数说明如下:
typedef bool_t (*WriteStartFunc)(ptu32_t SpecificFlag,u8 DevAddr,u32 MemAddr,u8 MenAddrLen,
u32 Length,structSemaphoreLCB *IIC_BusSemp);
功能:
产生IIC写数据时序,并发送内部地址
参数:
SpecificFlag,IIC硬件驱动调用IIC_BusAdd时给定的私有数据,通常是寄存器或IIC端口号。
DevAddr,器件地址的前7比特,已将内部地址所占的bit位更新,该函数需将该地址左移一位增加最后一位读/写比特;
MemAddr,存储器内部地址,即发送到总线上的地址,该地址未包含放在设备地址上的比特位;
MenAddrLen,存储器内部地址的长度,字节单位,未包含在设备地址里面的比特位;
Length,发送的数据总量,最后一个字节发送完时,需产生停止时序,并释放信号量;
IIC_BusSemp,总线控制信号量,发送完数据后需底层驱动释放;
返回值:
true,启动发送过程正确,false,发生错误。
3.启动接收
启动发送函数(ReadStartFunc)是为中断方式收发服务的,轮询方式不需要,置为NULL即可。
启动接收的回调函数ReadStartFunc主要完成了IIC时序上面读数据时的总线控制,读时序的时序控制过程如图21‑6启动接收所示。该函数依次实现了写start---->器件地址(写)---->写存储地址---->start(或者restart)---->器件地址(读)的时序过程。在启动接收时序正确完成后,需使能中断(若不使用中断,则需配置接收到数据pop的事件),并配置回复ACK,在中断中接收从器件发送的数据。
图21‑6启动接收
如图所示的时序图中,有两个start时序,可以通过配置repeated来重新启动一次新的时序,而不产生停止位。
回调函数说明如下:
typedef bool_t (*ReadStartFunc)(ptu32_t SpecificFlag,u8 DevAddr,u32 MemAddr,u8 MemAddrLen,
u32 Length,structSemaphoreLCB *IIC_BusSemp);
功能:
完成读时序的启动,并使能中断。
参数:
SpecificFlag,IIC硬件驱动调用IIC_BusAdd时给定的私有数据,通常是寄存器或IIC端口号。
DevAddr,从器件地址的高七比特(同__IIC_GenerateWriteStart参数说明);
MemAddr,存储器件的内部地址(同__IIC_GenerateWriteStart参数说明);
MemAddrLen,存储器件地址长度,字节单位(同__IIC_GenerateWriteStart参数说明);
Length,接收的数据总量,接收数据的倒数第一字节,即count-1,停止产生ACK信号,当接收的字节数为count时,产生停止时序,并释放信号量iic_buf_semp;
IIC_BusSemp,发送完成的缓冲区信号量,告知上层,本次发送已经完成。
返回值:
true,启动读时序成功,false失败。
4.结束传输
结束传输的回调函数__IIC_GenerateEnd主要用于停止当前正在进行的传输,特别是在发生超时传输时,用于停止本次发送或接收,实际上,该函数调用了产生停止时序的函数,使IIC主器件停止本帧数据的传输。启动和停止时序如图21‑7 IIC启动和停止时序所示。
图21‑7 IIC启动和停止时序
总线驱动模块的回调函数是标准接口模块实现IIC总线时序的关键,是与具体的硬件寄存器相关的函数,使用回调函数的方式有效的降低了标准驱动模块对底层驱动的耦合,使编译独立。IIC总线驱动提供的回调函数包括启动写、启动读、停止和控制函数。
5.控制函数
目前,控制IIC的底层驱动只需要实现对IIC总线传输时钟的控制即可,相对较为简单,此处不作详细说明,请参看源码cpu_peri_iic.c。
21.3.2.3 移植建议
由于大部分的IIC控制器的设计基本相似,因此,BSP程序人员可采取下面的步骤快速的完成DJYOS驱动架构下IIC底层驱动的开发。
1. 拷贝其他工程已测试通过的IIC驱动文件cpu_peri_iic.c/cpu_peri_iic.h;
2. 修改cpu_peri_iic.c/cpu_peri_iic.h中与具体IIC寄存器相关的部分;
3. 回调函数的具体实现和中断收发数据。
21.3.3 IIC器件驱动接口
DJYOS统一在目录“djysrc\bsp\chipdrv\xxx”中管理芯片驱动,其中,xxx是具体芯片的文件夹名称。
如何把新添加的器件,集成到DIDE中,参见DIDE的使用手册。
IIC总线初始化完成后,添加一个器件到总线上的过程,非常简单,就是确定一下该器件的寻址特性参数后,调用IIC_DevAdd函数把器件添加到总线上,调用IIC_BusCtrl设置总线参数即可,详见IIC_DevAdd和IIC_BusCtrl函数说明。
器件装载到IIC总线之后,可以通过访问IIC总线实现访问器件,具体就是调用iicbus.h提供的API函数IIC_Write()和IIC_Read()。
djysrc的源码中,有大量的IIC器件驱动,大家可参考之。
21.3.4 IIC总线API函数
21.3.4.1 IIC_BusAdd:添加IIC总线
struct IIC_CB *IIC_BusAdd(struct IIC_Param *NewIICParam);
头文件:
iicbus.h
参数:
struct IIC_Param的定义如下:
typedef bool_t (*WriteReadPoll)(ptu32_t SpecificFlag,u8 DevAddr,u32 MemAddr,\ u8 MenAddrLen,u8* Buf, u32 Length,u8 WrRdFlag); typedef bool_t (*WriteStartFunc)(ptu32_t SpecificFlag,u8 DevAddr,\ u32 MemAddr,u8 MenAddrLen, u32 Length,\ struct SemaphoreLCB *IIC_BusSemp); typedef bool_t (*ReadStartFunc)(ptu32_t SpecificFlag,u8 DevAddr,\ u32 MemAddr,u8 MemAddrLen, u32 Length,\ struct SemaphoreLCB *IIC_BusSemp); typedef void (*GenerateEndFunc)(ptu32_t SpecificFlag); typedef s32 (*IICBusCtrlFunc) (ptu32_t SpecificFlag,u32 cmd,\ ptu32_t data1,ptu32_t data2); struct IIC_Param { char *BusName; //总线名称,如IIC1 u8 *IICBuf; //总线缓冲区指针 u32 IICBufLen; //总线缓冲区大小,字节 ptu32_t SpecificFlag; //指定标记,如IIC寄存器基址 WriteReadPoll pWriteReadPoll; //轮询或中断未开时使用 WriteStartFunc pGenerateWriteStart; //写过程启动 ReadStartFunc pGenerateReadStart; //读过程启动 GenerateEndFunc pGenerateEnd; //结束通信 IICBusCtrlFunc pBusCtrl; //控制函数 }; |
BusName,待安装的总线名字。
IICBuf,收发缓冲区,根据IIC总线通信的特点可知,无论IIC总线主设备有多少从设备,在同一时刻,IIC主设备与从设备的通信只能单一单向,即单点单向通信,因此,接收与发送使用同一个缓冲区。
IICBuf和IICBufLen分别是该IIC总线缓冲区指针和大小,缓冲区由IIC驱动程序提供,必须为全局或静态缓冲区。
SpecificFlag由驱动提供,且由驱动使用,一般是IIC控制寄存器基址或端口号。
5个回调函数:则是IIC总线驱动程序员需重点完成的工作,参见第21.3.2.2节。
返回值:
返回新建立总线IIC总线控制块指针,失败时返回NULL。
说明:
总线控制块所占内存从heap中分配。须先添加IIC总线,然后才能添加IIC器件。
21.3.4.2 IIC_BusDelete:删除IIC总线
bool_t IIC_BusDelete(struct IIC_CB *DelIIC);
头文件:
iicbus.h
参数:
DelIIC:删除IIC控制块指针。
返回值:
true,删除成功;false,删除失败。
说明:
删除在IICBus目录上的IICn目录,若该目录上有器件对象,则删除会不成功。
21.3.4.3 IIC_BusFind:查找IIC总线
struct IIC_CB *IIC_BusFind(char *BusName);
头文件:
iicbus.h
参数:
BusName:查找的总线名称。
返回值:
查找的控制块结点指针,未找到时返回NULL。
说明:
查找IICBus目录上是否存在名为BusName的总线。
21.3.4.4 IIC_DevAdd:添加IIC器件
struct IIC_Device *IIC_DevAdd(const char *BusName ,const char *DevName, u8 DevAddr,
u8 BitOfMaddrInDaddr, u8 BitOfMaddr);
头文件:
iicbus.h
参数:
BusName,新增器件挂接的IIC总线名称。
DevName,器件名称。
DevAddr,器件地址DevAddr是七个比特地址,如0x50,因IIC总线的bit0用于指示读还是写操作,故在总线上体现的地址为0x50(写)/0x51(读)。
BitOfMaddrInDaddr,是指DevAddr的低三个比特中,有多少个bit用于器件内部存储空间寻址,取值范围:0~3。
BitOfMaddr,表示被操作的器件内部地址的总位数,包含器件地址上的比特位;
举例说明:存储大小为128K的铁电,器件地址为0x50,页大小为64K,则地址范围为0x00000 ~ 0x1FFFF,若存储地址占用器件地址的1个比特,则dev_addr为0x50,BitOfMemAddrInDevAddr为1,BitOfMemAddr仍为17,构成了128K的寻址空间。
返回值:
设备控制块结点指针,添加失败时返回NULL。
说明:
在IIC总线结点上增加器件,即总线上挂接的器件,该器件属于总线目录的子对象。所需的器件控制块IIC_Device内存从heap中分配。
关于BitOfMaddrInDaddr和BitOfMaddr,有些大容量存储器件的内部地址比较长,会利用器件地址中的1~3位作为内部地址的一部分,例如一个128KB容量的器件,存储地址寻址空间为17个比特,即0x1FFFF,可能会放一个地址位到器件地址上,即BitOfMaddrInDaddr = 1,则BitOfMaddr - BitOfMaddrInDaddr = 16,这样,传输地址时,就只需要传2个字节,否则要传3个字节(第三个字节只有1bit有效)。
21.3.4.5 IIC_DevDelete:删除IIC器件
bool_t IIC_DevDelete(struct IIC_Device *DelDev)
头文件:
iicbus.h
参数:
DelDev,删除的器件指针。
返回值:
true,删除成功;false,删除失败。
说明:
删除IIC总线上的器件。
21.3.4.6 IIC_DevFind:查找IIC器件
struct IIC_Device *IIC_DevFind(char *DevName)
头文件:
iicbus.h
参数:
DevName,器件名称。
返回值:
器件控制块构体指针。
说明:
查找器件,该器件必然是挂接在相应的IIC总线目录下的对象。
21.3.4.7 IIC_Write:IIC写
s32 IIC_Write(struct IIC_Device *Dev, u32 addr,u8 *buf,u32 len,
bool_t block_option,u32 timeout);
头文件:
iicbus.h
参数:
Dev,器件结构体指针;
addr,发送地址,即指存储地址;
buf,发送数据缓冲区指针;
len,发送数据长度,字节单位;
block_option,阻塞选项;
timeout,超时选项。
返回值:
实际发送字节数,或超时。
说明:
上层调用写API函数,该函数根据Dev对象获得其所在总线对象,调用总线回调函数完成发送启动时序,并且根据阻塞选项,决定是阻塞等待发送完成还是直接返回。timeout参数用于指定函数在确定的时间内完成,超时则强制返回,并停止时序,释放信号量。
21.3.4.8 IIC_Read:IIC读
s32 IIC_Read(struct IIC_Device *Dev,u32 addr,u8 *buf,u32 len,u32 timeout);
头文件:
iicbus.h
参数:
Dev,器件结构体指针;
addr,发送地址,即指存储地址;
buf,发送数据缓冲区指针;
len,发送数据长度,字节单位;
timeout,超时选项。
返回值:
实际读取字节数,或超时。
说明:
读启动函数,该函数首先根据Dev对象查出其所属总线对象,然后调用总线回调函数,启动读取产生读数据时序,并阻塞等待接收完成,当获得接收完成的信号量时,释放总线控制信号量。超时参数timeout用于用户指定在确定时间内完成总线操作,超时则强制返回。
21.3.4.9 IIC_PortRead:IIC端口端读
s32 IIC_PortRead( struct IIC_CB *IIC,u8 *buf,u32 len);
头文件:
iicbus.h
参数:
IIC:总线控制块结构体指针;
buf:读数据缓冲区指针;
len:读数据长度,字节单位。
返回值:
实际读到字节数。
说明:
读缓冲区数据,由底层驱动调用,读取的数据会被写入寄存器发送。
21.3.4.10 IIC_PortWrite:IIC端口端写
s32 IIC_PortWrite(struct IIC_CB *IIC,u8 *buf,u32 len);
头文件:
iicbus.h
参数:
IIC:总线控制块结构体指针;
buf:读数据缓冲区指针;
len:读数据长度,字节单位。
返回值:
实际读字节数。
说明:
写数据到缓冲区,由底层驱动调用。
21.3.4.11 IIC_BusCtrl:IIC总线控制
s32 IIC_BusCtrl(struct IIC_Device *Dev,u32 cmd,ptu32_t data1,ptu32_t data2);
头文件:
iicbus.h
参数:
Dev,器件结构体指针;
cmd:命令;
data1,data2:参数数据,根据具体命令定义不同。
返回值:
状态,-1=参数检查出错,否则由cmd决定,若需要调用用户的pBusCtrl,则是该函数返回值。
说明:
根据cmd命令,解析数据data1和Data2,控制IIC,硬件相关的命令需调用回调函数由底层驱动完成。
21.3.4.12 IIC_ErrPop:IIC弹出错误
s32 IIC_ErrPop(struct IIC_CB *IIC, u32 ErrNo);
头文件:
iicbus.h
参数:
IIC,总线控制块结构体指针。
ErrNo,错误序号,由IIC模块或用户自己确定。
返回值:
状态,无错误时返回CN_EXIT_NO_ERR
说明:
由驱动程序调用的函数,当检测到硬件错误时弹出错误事件类型,并将ErrNo作为参数传递给事件。
21.4 编写SPI总线及器件驱动
DjyBus V1.1.0版本SPII驱动设计适用性说明如下:
1. SPI工作于主模式,与从设备进行通信;
2. 不支持自动重发机制,发生错误时,采用弹出事件的方式告知应用层;
3. 读数据必须只能使用阻塞方式,即接收到足够数据后,函数返回。
21.4.1 SPI总线框架
类似于IIC驱动框架,DjyBus将SPI程序分成SPI通用接口和底层驱动两个部分。
DjyBus将SPI程序分成SPI通用接口和底层驱动两个部分,如图21‑8所示。SPI通用接口是不以具体硬件平台而变化的公共代码部分,它为上层应用(通常是挂接在SPI总线上的器件驱动,如AT25DB321)提供标准化接口函数,并对SPI总线操作提供各种信号量互斥访问和缓冲区保护。
底层驱动部分与具体硬件平台息息相关,SPI驱动移植到不同的平台时,需要在该层进行硬件适配。DjyBus已经将整个底层驱动的框架搭建完整,移植到不同平台只需要填鸭式的完成寄存器级的工作,使重复性工作尽量降少。
底层驱动程序注册回调函数与SPI总线组件对接,将通用接口与底层隔离,通用接口层可进行独立编译,与具体硬件耦合度降低。
|
图21‑8 SPI总线框图
21.4.2 SPI总线驱动接口
DJYOS提供的BSP中,如果CPU有SPI总线,则会提供IIC总线驱动,创建工程时,只需勾选“cpu onchip spi”组件,并且配置运行参数即可,如果创建工程时忘了勾选,随时打开工程属性界面,也可以修改配置。
图 21‑9 配置SPI总线硬件驱动
SPIBus是DjyBus模块的一个子模块,其结构如图21‑8所示,它为SPI器件提供标准的、一致的应用程序编程接口,并且规范了硬件驱动接口。驱动接口分为总线控制器接口和SPI器件接口两部分,驱动的重点是总线控制器,而器件接口实际上就是配置一下该器件的物理参数。
SPI驱动程序编写重点有:
1. 初始化SPI控制器,并且把SPI总线添加到DjyBus上。
2. 实现图21‑8中的5个回调函数。
3. 如果采用中断方式,须编写中断服务函数
21.4.2.1 SPI控制器初始化
Step1:初始化硬件
1. SPI控制器硬件的初始化,包括传输速度、IO配置、时钟等;
2. 挂载SPI中断到中断系统,并配置中断类型,如配置为异步信号(若只采用轮询方式,则此功能可省略);
Step2:添加到对象目录
添加SPI总线函数的参数类型为struct SPI_Param,由函数SPI_BusAdd完成对SPI总线控制块的初始化和添加SPIn目录到DjyBus目录,详见第21.4.4.1节。
21.4.2.2 回调函数
1.轮询函数
如果采用轮询(用SPI_BusCtrl函数、CN_SPI_SET_POLL命令设置)方式收发,5个回调函数中只需要实现这一个,其他指针置为NULL即可。
SPIBus模块将在以下情况下使用轮询方式:
1. 用SPI_BusCtrl函数、CN_SPI_SET_POLL命令把器件所述总线收发方式被设为轮询方式,默认值为中断方式。
2. 在禁止调度(即禁止异步信号中断)期间读写SPI器件,强制使用轮询方式。
3. pTransferTxRx ==NULL,则使用轮询方式收发;
4. 系统初始化未完成,多事件调度尚未启动期间。
如果使用中断方式收发,且不考虑在2~4三种情况下收发数据,则无须实现本函数,pTransferPoll指针设为NULL即可。
回调函数说明如下:
typedef bool_t (*TransferPoll)(ptu32_t SpecificFlag,u8* srcaddr,u32 sendlen,
u8* destaddr,u32 recvlen,u32 recvoff,u8 cs);;
参数:
SpecificFlag:SPI硬件驱动调用SPI_BusAdd时给定的私有数据,通常是寄存器或SPI端口号。
srcaddr:发送数据存储地址。
sendlen:发送数据字节数。
destaddr:接收数据存储地址。
recvlen:接收数据字节数。
recvoff:接收偏移字节数,即认为接收到recvoff字节后的数据为有效,才存储。
返回值:
true,执行成功;false,执行失败。
说明:
轮询方式主要应用于系统调度未启动或对实时性要求不高的场合,这种方法能够简化编程处理,快速实现通信功能。
2.启动收发
启动收发是在使用中断方式收发数据必须实现的回调函数,若使用轮询方式,可将本函数设置为NULL即可。
回调函数说明如下:
typedef ptu32_t (*TransferFunc)(ptu32_t SpecificFlag,u32 sendlen,u32 recvlen,u32 recvoff);
参数:
SpecificFlag:SPI硬件驱动调用SPI_BusAdd时给定的私有数据,通常是寄存器或SPI端口号。
sendlen,发送数据长度,字节单位。
recvlen,接收数据长度,字节单位。
recvoff,接收偏移,接收到多少个字节后开始保护数据,即有用数据。
返回值:
true,中断方式启动通信成功,false,失败。
说明:
该函数功能是配置SPI寄存器,并保存有用参数,使能中断,SPI总线采用的是四线的收发方式,收、发、时钟和片选。TransferFunc实质上是实现了相关的寄存器的配置,并中断使能。当然,SPI总线协议规定,操作设备前必须把对应从设备的CS线拉低。
对于TransferFunc需要完成的功能作如下说明:
1、 保存静态变量,如发送接收数据长度,接收偏移(从接收到的第几个数据开始保存数据);
2、 配置SPI寄存器,使其处于发送和接收的状态;
3、 配置中断使能,并触发中断,在中断中将数据发送接收完成。
3.片选使能
typedef bool_t (*CsActiveFunc)(ptu32_t SpecificFlag, u8 cs);
功能:片选拉低。
参数:
SpecificFlag:SPI硬件驱动调用SPI_BusAdd时给定的私有数据,通常是寄存器或SPI端口号。
cs,片选号。
返回值:true,成功;false则失败。
虽然对于具体的芯片,该函数的实现过程不相同,但是功能是相同的,即拉低片选,选择CS对应的SPI从器件通信。若控制器具有硬件自动片选,硬件驱动可加以利用,提高效率。
4.片选失能
typedef bool_t (*CsInActiveFunc)(ptu32_t SpecificFlag, u8 cs);
功能:片选拉高。
参数:
SpecificFlag:SPI硬件驱动调用SPI_BusAdd时给定的私有数据,通常是寄存器或SPI端口号。
cs,片选号。
返回值:true,成功;false则失败。
5.控制函数
目前,控制函数主要实现对总线的配置,如自动片选、传输速度、SPI时序配置等。应用层将通过调用SPI_BusCtrl接口函数,传递不同的命令和参数,实现对总线的控制。
如果SPI控制器征对每个片选信号都有独立的配置寄存器,则在添加设备时(SPI_DevAddr),配置好每个片选寄存器;若多个片选共用一套配置寄存器,则每次传输都必须重新配置。在结构体SPI_CB中,成员MultiCsReg是用来标记SPI控制器是否具有多套片选寄存器,在调用SPI_BusAdd时,硬件驱动会作相应的标记。
对照SPI协议的时序来讲配置参数会更加的清晰。如图21‑10和图21‑11所示,SPI通信首先需产生CS片选有效,即拉低对应的CS片选。时钟信号SPI_CLK在未通信状态时的电平状态由CHOL决定,为高或者为低。而CPHA决定时序的相位,当CPHA为0时,在SPI_CLK的第一个边沿采样,第二个边沿输出数据;当CPHA为1时,在SPI_CLK的第一个边沿输出数据,第二个边沿采样。
图21‑10 SPI时序CPHA=0
图21‑11 SPI时序CPHA=1
CHOL和CPHA两种配置组成了四种模式,分别为模式0、1、2、3,如表21‑1所示,部分SPI从器件只支持部分模式,需根据具体器件配置成要求的模式。
表21‑1SPI模式
CHOL |
CPHA |
MODE |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
2 |
1 |
1 |
3 |
控制命令用于应用层调用spibus.h的API的控制函数SPI_Ctrl实现参数配置或通信设置,与硬件相关的命令一览表如表21‑2所示。
表21‑2 SPI命令表
命令 |
参数1 |
参数2 |
说明 |
CN_SPI_SET_CLK |
速率,bit/s |
无意义 |
设置传输速度 |
CN_SPI_CS_CONFIG |
tagSpiConfig |
无意义 |
配置相关参数 |
CN_SPI_SET_AUTO_CS_EN |
无意义 |
无意义 |
自动片选使能 |
CN_SPI_SET_AUTO_CS_DIS |
无意义 |
无意义 |
自动片选失能 |
21.4.2.3 移植建议
为了简化编程,提高工作效率, BSP程序人员可采取下面的步骤快速的完成DJYOS驱动架构下SPI底层驱动的开发。
1、 拷贝其他工程已测试通过的SPI驱动文件cpu_peri_spi.c/cpu_peri_spi.h;
2、 修改cpu_peri_spi.c/cpu_peri_spi.h中与具体SPI寄存器相关的部分;
3、 回调函数的具体实现和中断收发数据。
21.4.3 SPI器件驱动接口
DJYOS统一在目录“djysrc\bsp\chipdrv\xxx”中管理芯片驱动,其中,xxx是具体芯片的文件夹名称。
如何把新添加的器件,集成到DIDE中,参见DIDE的使用手册。
SPI总线初始化完成后,添加一个器件到总线上的过程,非常简单,就是确定一下该器件的寻址特性参数后,调用SPI_DevAdd函数把器件添加到总线上,调用SPI_BusCtrl设置总线参数即可,详见SPI_DevAdd和IICSPIBusCtrl函数说明。
器件装载到SPI总线之后,可以通过访问SPI总线实现访问器件,具体就是调用spibus.h提供的API。与IIC不同,IIC有IIC_Write()和IIC_Read()两个函数,但SPI的协议规定了,其收发函数是一体的,只需要一个SPI_Transfer函数。
djysrc的源码中,有大量的SPI器件驱动,大家可参考之。
SPI总线初始化完成后,添加一个器件到总线上的过程,非常简单,就是初始化一下该器件的寻址特性参数,然后调用SPI_DevAdd_s或SPI_DevAdd函数把器件添加到总线上即可。需配置的参数,都在spibus.h文件中定义的struct SPI_Device中描述。struct SPI_Device结构定义如下:
//SPI总线器件结构体 struct SPI_Device { struct Object *HostObj; u8 Cs; //片选信号 bool_t AutoCs; //自动片选 u8 CharLen; //数据长度 u8 Mode; //模式选择 u8 ShiftDir; //MSB or LSB u32 Freq; //速度,Hz }; |
添加器件到总线的过程就是将器件结点挂到相应的“SPIn”总线结点的过程,同时,配置好相应的总线通信参数。现对添加SPI器件要点作如下说明:
1. 定义static struct SPI_Device类型的静态变量;
2. 初始化数据struct SPI_Param的各成员;
3. 调用SPI_DevAdd_s或SPI_DevAdd添加设备到总线结点;
4. 调用SPI_BusCtrl设置总线参数。
21.4.4 SPI总线API
21.4.4.1 SPI_BusAdd:添加SPI总线
struct SPI_CB *SPI_BusAdd(struct SPI_Param *NewSPIParam);
头文件:
spibus.h
参数:
NewSPIParam:新增SPI总线参数,定义如下:
typedef ptu32_t (*TransferFunc)(ptu32_t SpecificFlag,u32 sendlen,u32 recvlen,u32 recvoff); typedef bool_t (*TransferPoll)(ptu32_t SpecificFlag,u8* srcaddr,u32 sendlen, u8* destaddr,u32 recvlen,u32 recvoff,u8 cs); typedef bool_t (*CsActiveFunc)(ptu32_t SpecificFlag, u8 cs); typedef bool_t (*CsInActiveFunc)(ptu32_t SpecificFlag, u8 cs); typedef ptu32_t (*SPIBusCtrlFunc)(ptu32_t SpecificFlag,u32 cmd,ptu32_t data1,ptu32_t data2); struct SPI_Param { char *BusName; //总线名称,如SPI1 u8 *SPIBuf; //总线缓冲区指针 u32 SPIBufLen; //总线缓冲区大小,字节 ptu32_t SpecificFlag; //SPI私有标签,如控制寄存器基址 bool_t MultiCSRegFlag; //SPI控制寄存器是否有多套CS配置寄存器 TransferFunc pTransferTxRx; //发送接收回调函数,中断方式 TransferPoll pTransferPoll; //发送接收回调函数,轮询方式 CsActiveFunc pCsActive; //片选使能 CsInActiveFunc pCsInActive; //片选失能 SPIBusCtrlFunc pBusCtrl; //控制函数 };
|
结构的各成员注释已经很明确,不再一一解析,捡重点说明一下:
SPIBuf,收发缓冲区,SPI主设备与从设备通信符合双向单一规则,即同一时刻,只能与一个从设备发送与接收同时进行,发送数据的同时,也接收到数据。但是绝大多数的主从通信设备的从设备,都满足问答模式,即先发命令,再发送或接收数据的方式,因此本设计只提供一个缓冲区。
MultiCSRegFlag,true表示该SPI控制器的每个片选都有独立的参数寄存器,分别配置好后,就不用每次切换片选时都重新配置寄存器。
返回值:
返回建立的SPI总线控制块指针,失败时返回NULL。
说明:
新增SPI总线对象将挂接到SPIBus目录。
21.4.4.2 SPI_BusDelete:删除SPI总线
bool_t SPI_BusDelete(struct SPI_CB *DelSPI);
头文件:
spibus.h
参数:
DelSPI:待删除SPI控制块指针。
返回值:
true,删除成功;false,删除失败。
说明:
删除在SPIBus目录上的IICn目录,若该目录上有器件对象,则删除会不成功。
21.4.4.3 SPI_BusFind:查找SPI总线
struct SPI_CB *SPI_BusFind(char *BusName);
头文件:
spibus.h
参数:
BusName:查找的总线名称。
返回值:
若成功找到则返回找到的总线控制块指针,未找到时返回NULL。
说明:
查找SPI总线类型结点的子结点中是否存在被查找名称的总线。
21.4.4.4 SPI_DevAdd:添加SPI器件
struct SPI_Device *SPI_DevAdd(const char *BusName ,const char *DevName,u8 cs,u8 charlen,
u8 mode,u8 shiftdir,u32 freq,u8 autocs);
头文件:
spibus.h
参数:
BusName:新增子器件目标总线名称。
DevName:器件名称。
cs:该器件片选号。
charlen:传输的字符宽度,可能的值为4-16比特。
shiftdir:MSB or LSB。
mode:SPI传输模式,根据CHOL和CPHA共有四种模式,参见第21.4.2.2节。
freq:总线传输的时钟频率,单位Hz。
autocs:新增加设备是否自动片选。
返回值:
成功时返回新增SPI器件参数结构体指针,失败时返回NULL。
说明:
在SPI总线结点上增加器件,即总线上挂接器件,该器件属于总线目录的子对象。
21.4.4.5 SPI_DevDelete:删除SPI器件
bool_t SPI_DevDelete(struct SPI_Device *DelDev);
头文件:
spibus.h
参数:
DelDev:删除的器件参数结构体指针。
返回值:
true,删除成功;false,删除失败。
说明:
删除SPI总线上指定器件,从总线子结点中删除,同时释放SPI总线器件参数结构体所占内存。
21.4.4.6 SPI_DevFind:SPI总线上查找器件
struct SPI_Device *SPI_DevFind(char *DevName)
头文件:
spibus.h
参数:
DevName:查找的器件名称。
返回值:
查找的SPI器件参数结构体指针,未找到时返回NULL。
说明:
查找SPI总线类型结点的子结点中是否存在被查找名称器件。
21.4.4.7 SPI_Transfer:SPI数据传输
s32 SPI_Transfer(struct SPI_Device *Dev,struct SPI_DataFrame *spidata,
u8 block_option,u32 timeout);
头文件:
spibus.h
参数:
Dev:器件指针;
spidata:SPI数据结构体;
block_option:阻塞选项,true时,表明最后一次传输为阻塞方式,否则为非阻塞;
timeout:超时参数,单位us。
返回值:
返回发送状态,超时或错误或无错误。
说明:
数据传送函数,完成数据的发送和接收。该函数完成的功能如下:
1. 若器件驱动为了避免组包的麻烦,可先发命令再发送数据,分多次调用,多次调用前后被CSActive和CsInactive函数包裹;
2. 根据Dev查找所属SPI总线;
3. 若缓冲区大于发送字节数,则直接将数据填入缓冲区;
4. 若为阻塞发送,则等待总线信号量。
5. 发生超时或错误时,拉高CS并释放信号量。
21.4.4.8 SPI_CsActive:使能片选信号
bool_t SPI_CsActive(struct SPI_Device *Dev,u32 timeout);
头文件:
spibus.h
参数:
Dev:器件参数结构体指针。
timeout:超时参数,单位us。
返回值:
true,操作成功;false,操作失败。
说明:
使能片选,该函数运行必须获得总线信号量,即SPI_BusSemp,然后根据CS是否具有自己的配置寄存器标志,判断是否需要配置寄存器。若MultiCsReg标志为false,则每次CS使能时,都需要配置寄存器。
21.4.4.9 SPI_CsInactive:禁能片选信号
bool_t SPI_CsInactive(struct SPI_Device *Dev);
头文件:
spibus.h
参数:
Dev:器件参数结构体指针。
返回值:
true,操作成功;false,操作失败。
说明:
禁能片选,同时释放SPI总线信号量。
21.4.4.10 SPI_PortRead:SPI端口端读
s32 SPI_PortRead( struct SPI_CB *SPI,u8 *buf,u32 len);
头文件:
spibus.h
参数:
SPI:SPI控制块结构体指针。
buf:存放读到的数据缓冲区指针。
len:需要读的数据长度,单位字节。
返回值:
读到的字节数。
说明:
读发送缓冲区数据,由底层驱动调用,总线驱动读取到数据后将数据写入寄存器发送,若阻塞发送,则直接读发送缓冲区指向的数据,若非阻塞发送,则需判断是否剩余数据已达到缓冲区边界,若已到达缓冲区边界,则填写缓冲区,并释放阻塞信号量。
21.4.4.11 SPI_ PortWrite :SPI端口端写
s32 SPI_PortWrite(struct SPI_CB *SPI,u8 *buf,u32 len);
头文件:
spibus.h
参数:
SPI:SPI控制块结构体指针。
buf:数据缓冲区指针。
len:数据长度,字节单位。
返回值:
成功写的字节数。
说明:
将接收到的数据写入用户提供的缓冲区中,接收是阻塞方式,因此使用用户的缓冲区。
21.4.4.12 SPI_BusCtrl:SPI总线控制
s32 SPI_BusCtrl(struct SPI_Device *Dev,u32 cmd,ptu32_t data1,ptu32_t data2);
头文件:
spibus.h
参数:
Dev:器件参数结构体指针。
cmd:命令。
data1,data2:参数数据,根据具体命令定义不同。
返回值:
状态,-1=参数检查出错,否则由cmd决定,若需要调用用户的pBusCtrl,则是该函数返回值。
说明:
根据cmd命令,解析数据data1和data2,控制SPI,硬件相关的命令需调用回调函数,这些回调函数由底层驱动完成。
cmd命令在spibus.h中定义,例如CN_SPI_SET_CLK
21.4.4.13 SPI_ErrPop:SPI错误弹出
s32 SPI_ErrPop(struct SPI_CB *SPI, u32 ErrNo);
头文件:
spibus.h
参数:
SPI:总线控制块结构体指针;
ErrNo:错误序号,由SPI模块或用户自己确定。
返回值:
状态,无错误时返回CN_SPI_EXIT_NOERR。
说明:
由驱动程序调用的函数,当检测到硬件错误时弹出错误事件类型,并将ErrNo作为参数传递给事件。
第22章 多路复用
22.1 Multiplex概述
所谓多路复用,就是一个处理程序处理多个传输对象的输入输出数据,例如网络服务器,需要面对成千上万个socket,如果每个socket都创建一个线程去处理的话,占用大量资源,且频繁线程切换也会消耗太多cpu资源,显然是不划算的,Multiplex可以解决这个问题。
DJYOS的select函数,正式依托Multiplex模块完成的。
DJYOS中,Multiplex是一个独立的功能模块,可用于文件、设备、网络socket等对象的多路复用访问,可以把任何fd加入到多路复用体系中。
事件处理过程中(线程运行中),创建MultiplexSets并可以把多个文件描述符fd加入MultiplexSets中,并设定感兴趣的bit,以及触发条件(“与”或者“或”),然后阻塞等待MultiplexSets被触发。MultiplexSets和具体fd可以分别设定触发条件,MultiplexSets的触发条件含义:“或”方式是指,MultiplexSets中的对象,只要有一个被触发,即MultiplexSets被触发。“与”方式是指,MultiplexSets中的对象,全部被触发后,MultiplexSets才被触发。fd的触发条件含义为:“或”方式是指,只要有一个该fd感兴趣的bit被触发,fd就被触发。“与”方式是指,只有全部该fd感兴趣的bit被触发,fd才被触发。每个fd可以单独设定感兴趣的bit。
一个fd,也可以同时被多个MultiplexSets包含,fd被触发时,所有这些MultiplexSets将同时受影响。
22.2 API说明
注意,在对象系统和IO体系中,也有若干函数与多路复用有关,参见第11章和第12章。
22.2.1 Multiplex_Creat:创建多路复用集
struct MultiplexSetsCB *Multiplex_Creat(u32 ActiveLevel);
头文件:
multiplex.h
参数:
ActiveLevel:触发水平,该集合被触发的fd数量达到此数后,触发集合.。
返回值:
新创建的MultiplexSets指针。
说明:
创建一个MultiplexSets,并且设定属性。
22.2.2 Multiplex_AddObject:添加fd到MultiplexSets
bool_t Multiplex_AddObject(struct MultiplexSetsCB *Sets,s32 Fd, u32 SensingBit);
头文件:
multiplex.h
参数:
Sets,被操作的MultiplexSets指针;
Fd,待加入Sets的文件描述符。
SensingBit,对象敏感位标志,bit0~23,24个敏感bit,设为1表示本对象对这个bit标志敏感,参见CN_MULTIPLEX_SENSINGBIT_READ 的定义
bit24~31表示模式,参见 CN_MULTIPLEX_SENSINGBIT_OR 的定义。
返回值:
true=成功,false=失败。
说明:
MultiplexSets中添加一个对象。如果该fd的初始状态是已经触发,则加入到ActiveQ队列,否则加入ObjectQ队列。
22.2.3 Multiplex_DelObject:MultiplexSets中删除fd
bool_t Multiplex_DelObject(struct MultiplexSetsCB *Sets,s32 Fd);
头文件:
multiplex.h
参数:
Sets,被操作的MultiplexSets指针;
Fd,待加入Sets的文件描述符。
返回值:
true=成功,false=失败。
说明:
Multiplex_DelObject是MultiplexAdd的逆函数,从多路复用集中删除某个对象。
22.2.4 Multiplex_Wait:等待MultiplexSets触发
s32 Multiplex_Wait(struct MultiplexSetsCB *Sets, u32 *Status, u32 Timeout);
头文件:
multiplex.h
参数:
Sets,被操作的MultiplexSets指针;
Status,返回对象当前状态的指针,如果应用程序不关心其状态,可以给NULL;
Timeout,阻塞等待的最长时间,单位uS。
返回值:
如果MultiplexSets被触发,则返回MultiplexSets中一个被触发对象的fd,否则返回-1。
说明:
应用程序调用本函数等待集合别触发,注意,并不是只要有一个fd被触发,集合就触发,而是被触发的fd数量达到设定值时(设定值是Multiplex_Creat的参数)才触发。无论有多少fd被触发,本函数只返回一个被触发的fd,应用程序可反复调用Multiplex_Wait函数,直到被触发的fd全部返回。
第23章 黑匣子组件
23.1 异常管理概述
黑匣子软件是用来记录软件和硬件的各类异常行为的,用于事后追查故障,利于分析定位问题,使产品迅速迭代稳定。
所谓异常,就是各种硬件问题及各种软件bug导致的严重错误。异常与硬件紧密结合,不同CPU的硬件异常千差万别,意味着我们没有统一的标准去处理,但是我们可以有统一的方法去管理。本章叙述异常信息的收集、存储、解析、输出以及善后处理接口,同时为具体异常提供钩子,让用户可以写hook函数处理特定异常。
DJYOS的异常管理组件具有以下功能:
1、 Exp_Throw提交异常信息,异常组件内部还会搜集当时OS的运行状态信息,两者共同汇聚成一条异常信息。
2、 如果用户注册有HOOK,异常发生时,该HOOK会被调用,用于特定的处理,通常是善后处理,例如发出告警信息,或者指示下一步的处理方案:复位还是无视等。
3、 提供信息解析接口,你可以在调用Exp_Throw时,直接提交字符串形式的可阅读信息,也可以提交二进制信息,这时候,你应该提交一个解析器,用于把信息解析成人类可读的字符串。
4、 异常信息存储可以重定向,BSP中实现默认的存储方式,用户可以重定向到特定的存储介质中。
异常组件的工作流程如图23‑1所示。
|
图23‑1异常组件工作流程示意图
异常信息的再现,则是利用异常模块留出的调试接口从存储介质中读取指定条目的异常信息,当然各种条件查询只是后话了,因为异常信息条目既然可以读取的话,没有理由做不到条件搜索查询。从上述流程看,异常组件主要有四部分构成:1,留给用户使用的异常信息抛出接口;2,留给用户使用的HOOK(可选项);3,异常存储接口注册接口;4,异常指示执行单元。下面逐一分析各个模块。
23.1.1 抛出异常
函数BlackBox_ThrowExp用于抛出异常,就是当应用程序或者系统运行过程中检测到有软件或者硬件异常时,例如硬件检测到访问非法地址,或者软件检测到严重错误时,可以调用异常API,把异常信息抛出来,之后的工作,都由这个接口帮你搞定。但是前提是你得按照这个接口的标准扔出你要存储的记录,这个接口有两个参数:throwpara,代表我们要抛出的异常信息标准。其结构体如下:
struct BlackBoxThrowPara { u32 BlackBoxType; //异常类型,参考CN_BLACKBOX_TYPE_HARD_START char *DecoderName; //异信息解析器常名字,≤15字节 u8 *BlackBoxInfo; //异常信息地址 u32 BlackBoxInfoLen; //信息长度 enum EN_BlackBoxAction BlackBoxAction; //异常处理类型,参考enum EN_BlackBoxAction }; |
各个成员在此就不赘述了,注释上写的明白。在此有个问题:为什么有解析器名字这么个说法?很简单,当你抛出这个异常信息的时候,我们希望保存这个异常信息的解析器的名字,以便若干年后我们知道用那种解析器来解析这条记录,毕竟我们不能靠辨识你存储的字节流异常信息去猜你到底是什么类型的异常,这也就是为什么后来会有注册解析器这种说法;在该结构体还有个成员BlackBoxAction,该参数表示该类异常发生后的默认后续处理,如“仅仅记录、热重启、冷重启”等。所谓的默认处理方式,就是没有注册hook,或者hook函数返回EN_BLACKBOX _DEAL_DEFAULT时的处理方式,因此,在调用BlackBox_ThrowExp抛出异常时,不允许把处理方式设置成EN_ BLACKBOX_DEAL_DEFAULT。
OK,可以看看这个BlackBox_ThrowExp到底是怎么来处理扔出异常,处理流程如图23‑2所示。
|
图23‑2抛出异常执行流程示意图
23.1.2 异常信息存取
DJYOS有文件系统,那黑匣子模块为啥不用,要这么辛苦专门设计异常信息存取接口呢?登山、跑步、经常锻炼,能祛病强身,但如果一个人得了重病,自然须卧床休息、汤药灌之,让他拖着软绵绵病体去锻炼,无异于找死。嵌入式系统设计也一样,正常情况下,自然是直接调用文件系统接口,简单快捷;但发生异常时,系统的软件或硬件很可能发生了严重故障,而文件系统是个复杂的部件,自然不能确保其能正常运转,故专门设计了一套简单直接的接口。用户在BSP中实现该接口时,宜用简单直接读写存储器的方式,不宜复杂化,尤其应避免与复杂的模块发生关联。
存储介质千差万别,存储手段千变万化,异常组件无法做到统一,但是可以提供统一的接口给异常组件使用。定义的存储接口如下所示,BSP团队根据硬件配置,实现结构中的各函数指针,并调用BlackBox_RegisterRecorder函数注册到BlackBox模块中。
存储器中记录按存入顺序编号,宜用一个u32类型的数字表示记录序号,永远增量,增加到0xffffffff后回绕到0。
struct BlackBoxRecordOperate { fnBlackBox_RecordScan fnBlackBoxRecordScan; fnBlackBox_RecordSave fnBlackBoxRecordSave; fnBlackBox_RecordClean fnBlackBoxRecordClean; fnBlackBox_RecordCheckNum fnBlackBoxRecordCheckNum; fnBlackBox_RecordCheckLen fnBlackBoxRecordCheckLen; fnBlackBox_RecordGet fnBlackBoxRecordGet; }; |
只要填充好该数据结构,然后注册进去就OK了,各函数功能说明如下:
fnBlackBoxRecordScan:开机的时候扫描异常存储记录,获取关键信息方便以后存储。至于具体要获取哪些信息,是由存储驱动决定的,黑匣子模块本身并不关心。
fnBlackBoxRecordSave:保存一条异常信息到存储中,至于如何存储,完全由存储方案实现,一般要求能够循环存储。
fnBlackBoxRecordClean:删除所有异常信息,慎用。
fnBlackBoxRecordCheckNum:检查存储体中有多少条有效记录。
fnBlackBoxRecordCheckLen:获取指定序号条目的记录长度。
fnBlackBoxRecordGet:获取指定需要条目记录的记录内容。
这些函数的原型如下,原型、返回值类型、参数类型中用到的数据结构,都在blackbox.h中定义:
typedef enum EN_BlackBoxDealResult (*fnBlackBox_RecordSave)(\
struct BlackBoxRecordPara *recordpara);
typedef bool_t (*fnBlackBox_RecordClean)(void);
typedef bool_t (*fnBlackBox_RecordCheckNum)(u32 *recordnum);
typedef bool_t (*fnBlackBox_RecordCheckLen)(u32 assignedno, u32 *recordlen);
typedef bool_t (*fnBlackBox_RecordGet)(u32 assignedno,u32 buflen,u8 *buf,\
struct BlackBoxRecordPara *recordpara);
typedef void (*fnBlackBox_RecordScan)(void);
23.1.3 异常行动
虽然你不希望发生异常,但终于还是发生了,收集并保存信息后,接下来怎么办?报异常不是终极目的,采取行动恢复系统功能才是重点。怎么要采取什么行动呢?异常管理模块本身是不知道的,还需要用户告知,BlackBox模块的数据结构enum EN_ BlackBoxAction,定义了可能的行动方式。用户可以用两种方式告知须采取的行动:
1、 调用BlackBox_ThrowExp的时候,通过参数告知。
2、 注册钩子函数,钩子函数的返回值告知,并且返回值将覆盖BlackBox_ThrowExp的参数。
enum EN_BlackBoxAction { EN_BLACKBOX_DEAL_IGNORE = 0, //忽略该异常信息 EN_BLACKBOX_DEAL_DEFAULT, //按默认方式处理 EN_BLACKBOX_DEAL_RECORD, //需要记录该异常 EN_BLACKBOX_DEAL_RESET, //硬件重启,相当于上电启动 EN_BLACKBOX_DEAL_REBOOT, //跳转到启动地址,重新初始化内存和时钟等 EN_BLACKBOX_DEAL_RESTART, //重置sp后和cpu核状态后,跳转到预加载函数 EN_BLACKBOX_DEAL_WAIT, //等待,一般用于调试,将进入死循环 EN_BLACKBOX_DEAL_ERROR, //异常处理出错,动作将被忽略 EN_BLACKBOX_DEAL_LENTH, //lenth }; |
其中,抛出异常时指定的异常类型只能大于等于EN_BLACKBOX_DEAL_RECORD。
23.1.4 调用HOOK函数
异常发生后,BlackBox模块会按部就班地完成一些操作,同时也给机会应用程序执行一些特定的处理,这就是HOOK,一般来说,HOOK用于实现如下功能:
1、 如果可以的话,修复错误(如调关闭非必须功能模块),即使带伤,依然翱翔!
2、 发出告警,例如在通信端口发出告警报文、点亮告警灯等。
3、 提供附加信息给异常处理记录系统,例如能表示应用程序运行情况的状态量等。
4、 指示所要采取的行动。(若无钩子函数,将执行默认的行动,即调用BlackBox_ThrowExp函数时指定的方式)
特别提示:HOOK函数只有一个,不能为所有异常类型提供独立的HOOK函数。
23.2 API说明
23.2.1 BlackBox_ThrowExp:抛出异常信息
enum EN_BlackBoxDealResult BlackBox_ThrowExp(struct BlackBoxThrowPara *throwpara);
头文件:
blackbox.h
参数:
throwpara:抛出的异常信息参数,其定义见第23.1.1节。
返回值:
异常处理需采取的行动,如果有hook,可能会被hook的返回值替代。
说明:
抛出异常,软件的任何地方,只要检测到致命错误,就应该调用它抛出异常。本函数不一定会返回,黑匣子模块收到throwpara信息后,会根据该信息的指示(或者用户注册的hook函数指示),做采取相应的行动,可能的行动当然包括复位或者重启系统,若如此,本函数当然不会返回。
23.2.2 BlackBox _RegisterRecorder:注册异常信息保存方法
bool_t BlackBox_RegisterRecorder(struct BlackBoxRecordOperate *opt);
头文件:
blackbox.h
参数:
opt:需要注册的异常信息存储方法,共6个函数指针,其定义见第23.1.2节。
返回值:
true,成功, false,失败(失败的话会使用BSP默认的处理方法)。
说明:
注册异常信息存储方法,opt参数结构里面指定的处理方法都应该提供,否则的话会注册不成功。注意,首次注册的存储方法,将被作为默认方法保存。当再次注册新的方法后,如果执行了注销函数,存储方法将恢复为默认方法。
23.2.3 BlackBox _UnRegisterRecorder:注销异常信息存储方法
bool_t BlackBox_UnRegisterRecorder(void);
头文件:
blackbox.h
参数:无。
返回值:
true表示成功;false表示失败(失败的话会恢复默认的存储方案)。
说明:
注销异常信息存储方法。
23.2.4 BlackBox _ RecordClear:清空异常记录
bool_t BlackBox_RecordClear(void);
头文件:
blackbox.h
参数:无。
返回值:
true表示成功;false表示失败。
说明:
无。
23.2.5 BlackBox _ RecordCheckNum:检查异常记录数量
bool_t BlackBox_RecordCheckNum(u32 *recordnum);
头文件:
blackbox.h
参数:
recordnum,用于返回记录数量的指针。
返回值:
true表示成功;false表示失败。
说明:
无。
23.2.6 BlackBox _ RecordCheckLen:检查记录长度
bool_t BlackBox_RecordCheckLen(u32 assignedno, u32 *recordlen);
头文件:
blackbox.h
参数:
assignedno,记录序号。
recordlen,用于返回记录数量的指针。
返回值:
true表示成功;false表示失败。
说明:
检查指定序号的记录长度,由于。
23.2.7 BlackBox _ RecordGet:读取一条记录
bool_t BlackBox_RecordGet(u32 assignedno, u32 buflenlimit, u8 *buf, \
struct BlackBoxRecordPara *recordpara);
头文件:
blackbox.h
参数:
assignedno,记录序号。
buflenlimit,buf缓冲区的长度,读取的信息不能超过它。
buf,返回读取的记录。
recordpara,返回异常信息存储时的参数,在此是对buf的各个部分的定义
返回值:
true表示成功;false表示失败。
说明:
无。
23.2.8 BlackBox _RegisterHook:注册HOOK
bool_t BlackBox_RegisterHook(fnBlackBox_Hook fnHookFunc, fnBlackBox_HookParse fnHookParse);
头文件:
blackbox.h
参数:
fnHookFunc:异常钩子函数;
fnHookParse:提供的异常信息解析器。
返回值:
true,成功, false,失败。
说明:
注册APP提供的异常处理HOOK函数,hook函数只能注册一个,只要发生异常,无论是软件异常还是硬件异常,只要调用了BlackBox_ThrowExp函数抛出异常,hook就会被调用。就像人生了病要治一样,计算机系统发生了异常,也是需要处理的,对每一类异常,注册时规定了默认的处理方式,但是,默认处理方式不一定适合所有情况。注册hook函数可以解决这个问题,当异常实际发生时,hook可以分析当时的系统运行情况,对下一步的处理方式,做出更准确的裁决。
hook函数原型如下:
typedef enum EN_BlackBoxAction (*fnBlackBox_Hook)( struct BlackBoxThrowPara *throwpara,\
ptu32_t *infoaddr,u32 *infolen);
参数:
throwpara, 异常抛出者抛出的异常参数;
infoaddr,存储异常信息的地址;
infolen,存储搜集信息长度。
返回值:
本次异常须采取的行动,将覆盖抛出异常时参数所指定的行动。
说明:
异常发生后,hook将由系统自动调用,在发生异常的线程上下文中执行。应用程序可在此做一些善后工作,并可返回附加的异常信息,这些信息将与异常模块收集的硬件和系统异常信息一起保存。该钩子的返回结果是本次异常应该采取的后续行动,将是最终裁决。
特别注意,钩子函数只有一个,应该采用“switch-case”对throwpara->Exptype表示的异常类型分别处理。
hook函数将收集更进一步的信息,可以帮助分析和追溯故障,如果这些信息不是由字符串形式提供,黑匣子模块并不知道如何解读这些信息,就应该在调用Exp_RegisterHook时同时提供tfnExp_HookParse函数,把这些二进制信息解析成人类可阅读的字符串。
typedef bool_t (*tfnExp_HookParse)(struct ExpThrowPara *throwpara,\
ptu32_t infoaddr, u32 infolen,u32 endian);
参数:
throwpara:抛出该异常时给定的参数;
infoaddr:HOOK信息存储地址;
infolen:HOOK信息有效长度;
endian:HOOK信息存储的大小端。
返回值:
true成功 false 失败(没有注册等因素)
说明:
23.2.9 Exp_UnRegisterHook:注销HOOK
bool_t Exp_UnRegisterHook(void);
头文件:
blackbox.h
参数:无。
返回值:
true,成功, false,失败。
说明:
注销APP提供的异常处理HOOK以及异常信息解析器。
23.2.10 BlackBox _ RegisterThrowInfoDecoder:注册异常解析器
bool_t BlackBox_RegisterThrowInfoDecoder(struct BlackBoxInfoDecoder *Decoder);
头文件:
blackbox.h
参数:
decoder:异常信息解析器机构,内含解析函数指针和解析器名字指针。
返回值:
true,成功注册, false,注册失败。
说明:
BlackBox_ThrowExp函数会提供异常信息,如果这些信息不是由字符串形式提供,就应该注册软件异常信息解析器,以方便阅读分析问题。应用程序须保证异常解析器名字,与BlackBox _ThrowExp函数的参数throwpara->DecoderName一致。
异常信息解析函数类型定义如下:
typedef bool_t (*fnBlackBox_ThrowInfoParse)(struct BlackBoxThrowPara *para, u32 endian);
para:异常抛出者抛出异常时使用的参数;
endian:抛出异常信息存储的大小端。
example |
static struct BlackBoxInfoDecoder WdtDecoder; WdtDecoder.MyDecoder = __Wdt_WdtExpInfoDecoder; WdtDecoder.DecoderName = CN_WDT_EXPDECODERNAME; if(false ==BlackBox_RegisterThrowInfoDecoder(&WdtDecoder)) { debug_printf("WDT","Register Wdt Exp Decoder Failed!\n\r"); } |
第24章 Iboot与OTA
24.1 Iboot与OTA的概述
Iboot:Iboot的主要是提供升级App的功能。用户可以把Iboot的理解为一个只运行一些简单功能的App。用户可以根据自己的需求在Iboot中编写升级代码。djyos提供了一种默认的升级方式,下文会做说明。
OTA:是指空中升级,用户可以通过wifi等无线方式把待升级的文件,存入事先指定好的路径下,然后再调升级函数,程序就会自动进行升级。当然用户也可以同其他方,比如USB等等。只要能把待升级文件存入指定好的路径下就可以。
详细的说明大家可以去看《SI模式Iboot设计》文件。
24.2 升级流程
1、 首先通过IDE找到loader组件,根据自己的需求进行配置,配置Iboot版本号,校验方式,和待升级的默认路径。
2、 如果需要升级App的功能,通过DIDE在Iboot的配置功能中,手动勾选loader即可。
3、 如果需要升级Iboot的功能,通过DIDE在App的配置功能中,手动勾选loader即可。
4、 接下来就是如何启动升级了。要升级App需要调函数updateapp();该函数可以在App模式下调用也可以在Iboot模式下调用。该函数的参数为一个字符串,也可以为空。当为空时,执行升级的时候将会去访问IDE配置的默认路径去找待升级文件,并且升级完成后会复位并运行升级后的App。
如果参数为字符串的话,可以输入runiboot + 待升级的App路径,这样升级的时候就会根据你输入的路径去找待升级的文件,并且升级完之后会复位并运行Iboot,也可以只是输入待升级的App路径,这样升级完后会复位并运行升级后的App。
5、 要升级Iboot需要调函数updateiboot();该函数的用法和updateapp是一样的,区别只是这个是原来升级Iboot的。
6、 函数updateapp和函数updateiboot也有对应的shell命令,分别为updateapp和updateiboot。Shell命令后面输入对应的参数。
注意:需要升级App的时候请注意,Iboot模式下也需要能访问到待升级的App文件的路径。需要升级Iboot时,也需要App模式下能访问到待升级Iboot的文件路径。
24.3 API说明
24.3.1 Iboot_GetHardflag 获取硬件上的复位标志
__attribute__((weak)) u8 Iboot_GetHardflag(enum hardflag flag)
头文件:
无
参数:
flag:想获取哪个硬件的复位标志。
返回值:
始终返回0,没有实际意义,有实际意义的是根据参数获取对应标志位后,记录在RAM中了。
说明:
该函数是获取导致复位的硬件标志,需要用户根据不同的环境自己编写,写在initcpuc.c文件当中
24.3.2 Iboot_RewriteAppHeadFileInfo 重写App的信息块
bool_t Iboot_RewriteAppHeadFileInfo(void * apphead,const char*name,u32 filesize)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址;name:新的名字;filesize:新的文件大小。
返回值:
true:成功;false:失败。
说明:
该函数是获取导致复位的硬件标志,需要用户根据不同的环境自己编写,写在initcpuc.c文件当中
24.3.3 Iboot_GetAppHeadSize 获取App信息块的大小
u32 Iboot_GetAppHeadSize(void)
头文件:
Iboot_info.h
参数:
无。
返回值:
信息块大小。
说明:
获取App信息块结构体的大小
24.3.4 Iboot_XIP_AppFileCheck 给App信息块做初步的检查
bool_t Iboot_XIP_AppFileCheck(void * apphead)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址。
返回值:
true:成功;false:失败。
说明:
检查App信息块的前三个字节是否为“djy”
24.3.5 Iboot_XIP_GetAPPStartAddr 获取App的运行地址
void * Iboot_XIP_GetAPPStartAddr(void * apphead)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址。
返回值:
App的运行地址。
24.3.6 Iboot_XIP_GetAPPSize 获取App的文件大小
u32 Iboot_Iboot_XIP_GetAPPSize(void * apphead)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址。
返回值:
App的文件大小。
说明:
获取的大小是文件系统读到的文件大小,在线升级时由文件系统填充编译时由外部工具填充。
24.3.7 Iboot_GetAppSize 获取App的文件大小
u32 Iboot_GetAppSize(void * apphead)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址。
返回值:
App的文件大小。
说明:
获取的是app.bin文件大小,由外部工具填充
24.3.8 Iboot_GetAppName 获取App的文件名
char* Iboot_GetAppName(void * apphead)
头文件:
Iboot_info.h
参数:
apphead:App信息块地址。
返回值:
App的文件名。
24.3.9 Iboot_XIP_IsRamIbootFlag 判断重启后是否强制运行Iboot
bool_t Iboot_XIP_IsRamIbootFlag()
头文件:
Iboot_info.h
参数:
无。
返回值:
true:强制运行Iboot;false:非强制运行Iboot。
说明:
该函数在Iboot_IAP_SelectLoadProgam中调用,用于判断是否运行Iboot。
24.3.10 Iboot_GetIbootBulidTime 获取Iboot的编译时间
static bool_t Iboot_GetIbootBulidTime(u16 *pyear,u8 *pmon,u8 *pday,u8 *phour,u8 *pmin,u8 *psec)
头文件:
无
参数:
pyear:时间“年”的参数地址;pmon “月”的参数地址;pday:“日”;phour:“时”;pmin:“分”;psec:“秒”。
返回值:
true:成功;false:失败。
24.3.11 Iboot_SetAppBulidTime 设置App的编译时间
bool_t Iboot_SetAppBulidTime(u16 pyear,u8 pmon,u8 pday,u8 phour,u8 pmin,u8 psec)
头文件:
Iboot_info.h
参数:
pyear:“年”;pmon “月”;pday:“日”;phour:“时”;pmin:“分”;psec:“秒”。
返回值:
true:成功;false:失败。
说明:
App的编译时间,不能通过软件自动获取,得用户通过该函数自己设置。
24.3.12 Iboot_FillBoardName 填充板件名称到指定地址
static bool_t Iboot_FillBoardName(char* boardname,char* buf,u8 maxlen)
头文件:
无。
参数:
boardname:板件名称;buf:待写入的地址;maxlen:板件名称最大长度。
返回值:
true:成功;false:失败。
说明:
该函数用户不调用,只需要在board-config.h提供板件名即可,板件名由宏DJY_BOARD提供。
24.3.13 Iboot_FillMutualUpdatePathme 填充待升级的文件路径
bool_t Iboot_FillMutualUpdatePathme(char* Path)
头文件:
Iboot_info.h
参数:
Path:待升级的文件路径地址。
返回值:
true:成功;false:失败。
说明:
该函数用会把待升级的文件路径填充到RAM当中,升级时就是把这个路径的文件写到flash中。
24.3.14 Iboot_GetMutualUpdatePath 获取共享信息中的待升级路径
char * Iboot_GetMutualUpdatePath(void)
头文件:
Iboot_info.h
参数:
无
返回值:
待升级的App路径。
24.3.15 Iboot_SiIbootAppInfoInit 初始化iboot与APP共享的信息
bool_t Iboot_SiIbootAppInfoInit()
头文件:
Iboot_info.h
参数:
无。
返回值:
true:成功;false:失败。
说明:
在RAM用一段地址空间是原来记录Iboot和APP的共享信息,比如:复位前运行的模式,升级路径,复位后的运行模式等等。该函数就是初始化这一段内存信息的。
24.3.16 Run_Iboot 运行Iboot
bool_t Run_Iboot(enum runibootmode mode)
头文件:
Iboot_info.h
参数:
mode :启动模式,根据不同的启动模式会在内存中记录不同的信息。
返回值:
true:成功;false:失败。
24.3.17 Run_App 运行App
bool_t Run_App(enum runappmode mode)
头文件:
Iboot_info.h
参数:
mode :启动模式,根据不同的启动模式会在内存中记录不同的信息。
返回值:
true:成功;false:失败。
24.3.18 Iboot_UpdateToRun 升级成功后运行的模式
bool_t Iboot_UpdateToRun()
头文件:
Iboot_info.h
参数:
无。
返回值:
true:成功;false:失败。
说明:
根据内存中的信息,选择升级成功后运行什么模式。
24.3.19 Iboot_ClearResetflag 清除重启不更新的一些标志信息
bool_t Iboot_ClearResetflag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
24.3.19 Iboot_ClearResetflag 清除重启不更新的一些标志信息
bool_t Iboot_SetPreviouResetFlag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
24.3.21 Iboot_SetAppVerFlag 设置App的版本号
bool_t Iboot_SetAppVerFlag(u8 small, u8 medium, u8 large)
头文件:
Iboot_info.h
参数:
small -- xx.xx.__; medium -- xx.__.xx; large -- __.xx.xx
返回值:
true:成功;false:失败。
说明:
App的版本号,得用户通过该函数自己设置。
24.3.22 Iboot_SetSoftResetFlag 设置reset复位标志
bool_t Iboot_SetSoftResetFlag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
24.3.23 Iboot_SetRebootFlag 设置CPU_Reboot复位标志
bool_t Iboot_SetRebootFlag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
24.3.24 Iboot_SetRestartAppFlag 设置restart_app复位标志
bool_t Iboot_SetRestartAppFlag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
24.3.25 Iboot_SetRunFlag和Iboot_SetRunAppFlag
bool_t Iboot_SetRunFlag()和bool_t Iboot_SetRunAppFlag()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
说明:
这两个函数是用来设置复位后运行Iboot和APP模式的。
24.3.26 Iboot_SetRunIbootUpdateApp和Iboot_ClearRunIbootUpdateApp
bool_t Iboot_SetRunIbootUpdateApp()和bool_t Iboot_ClearRunIbootUpdateApp()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
说明:
这两个函数是用来设置和清除复位后运行Iboot并更新App模式标志的。
24.3.27 Iboot_GetUpdateApp 判断是否需要升级App
char Iboot_GetUpdateApp(void)
头文件:
Iboot_info.h
参数:
无
返回值:
0:需要升级App;-1:不需要升级App。
24.3.28 Iboot_SetRunAppUpdateIboot和Iboot_ClearRunAppUpdateIboot
bool_t Iboot_SetRunAppUpdateIboot()和bool_t Iboot_ClearRunAppUpdateIboot()
头文件:
Iboot_info.h
参数:
无
返回值:
true:成功;false:失败。
说明:
这两个函数是用来设置和清除复位后运行App并更新Iboot模式标志的。
24.3.29 Iboot_GetUpdateiboot 判断是否需要升级Iboot
char Iboot_GetUpdateiboot(void)
头文件:
Iboot_info.h
参数:
无
返回值:
0:需要升级Iboot;-1:不需要升级Iboot。
24.3.30 Iboot_SetUpdateSource和boot_GetUpdateSource
bool_t Iboot_SetUpdateSource(char *param)和u32 boot_GetUpdateSource(void)
头文件:
Iboot_info.h。
参数:
param:文件来源
返回值:
true:成功;false:失败。
说明:
这两个函数是用来设置和获取升级程序的来源,默认为0,指升级程序来源于文件。还有1-3是给用户提供的几种方式,用户可以把升级程序写在Iboot中,然后通过这两个函数就能控制,是否运行用户的升级程序。
24.3.31 Iboot_GetRunMode 获取当前运行的模式
char Iboot_GetRunMode(void)
头文件:
Iboot_info.h
参数:
无
返回值:
0:运行iboot;1:运行app;-1:错误
24.3.32 Iboot_GetAppInfo 获取当前内存中的共享信息
void Iboot_GetAppInfo(struct IbootAppInfo *get_info)
头文件:
Iboot_info.h
参数:
get_info:获取共享信息的地址。
返回值:
无。
说明:
获取当前内存中Iboot和App的共享信息,用户可以根据这些信息得知系统的运行状态。
24.3.33 Iboot_SetUpdateRunModet 设置更新完成后运行什么模式
bool_t Iboot_SetUpdateRunModet(u8 mode)
头文件:
Iboot_info.h。
参数:
0 -- 运行iboot;1 -- 运行app。
返回值:
true:成功;false:失败。
24.3.34 Iboot_IAP_IsForceIboot 硬件决定是否强制进入Iboot
__attribute__((weak)) bool_t Iboot_IAP_IsForceIboot()
头文件:
无。
参数:
无。
返回值:
true:强制运行Iboot;false:不强制运行Iboot。
说明:
由硬件决定是否强制进入Iboot,通常会使用一个gpio,通过跳线决定。需要用户根据不同的环境自己编写,写在initcpuc.c文件当中。
24.3.35 Iboot_IAP_SelectLoadProgam 选择加载项目
void Iboot_IAP_SelectLoadProgam(void)
头文件:
无。
参数:
无。
返回值:
无。
说明:
该函数会根据硬件标志(Iboot_IAP_IsForceIboot)和软件标志(内存中的共享信息),选择是执行Iboot还是App。
24.3.36 Iboot_LoadPreload 预加载程序
void Iboot_LoadPreload(void)
头文件:
无。
参数:
无。
返回值:
无。
说明:
加载主加载器、中断管理模块,紧急代码。
24.3.37 loader 内核加载程序
void loader(void)
头文件:
无。
参数:
无。
返回值:
无。
说明:
加载所有操作系统内核代码,以及全部应用程序代码。
24.3.38 loader 内核加载程序
bool_t runiboot(char *param)
头文件:
无。
参数:
无。
返回值:
无。
说明:
加载所有操作系统内核代码,以及全部应用程序代码。
24.3.39 runiboot 设置运行Iboot
bool_t runiboot(char *param)
头文件:
无。
参数:
无。
返回值:
无。
说明:
设置运行Iboot模式,运行该函数后会立即执行Iboot程序。
24.3.40 runapp设置运行App
bool_t runapp(char *param)
头文件:
无。
参数:
无。
返回值:
无。
说明:
设置运行App模式,运行该函数后会立即执行App程序。
24.3.41 runapp_or_runiboot据字符串设置升级成功后是运行的模式
bool_t runapp_or_runiboot(char *mode)
头文件:
无。
参数:
字符串:runapp – 升级成功后运行App,runiboot – 升级成功后运行Iboot。
返回值:
true -- 设置成功;false -- 没有设置。
24.3.42 updateapp据字符串设置升级所需的一些信息
bool_t updateapp(char *param)
头文件:
无。
参数:
字符串:runapp – 升级成功后运行App,runiboot – 升级成功后运行Iboot。后面再加待升级文件的路径
返回值:
true -- 成功;false – 失败。
说明:
参数为空:以默认路径为升级文件,升级成功后运行App。
参数为/yaf/app123.bin:表示以/yaf/app123.bin为升级文件的路径,升级成功后运行App。
参数为runiboot /yaf/app123.bin:表示以/yaf/app123.bin为升级文件的路径,升级成功后运行Iboot。
24.3.43 Iboot_UpdateApp 升级App
bool_t Iboot_UpdateApp(void)
头文件:
无。
参数:
无。
返回值:
true -- 成功;false – 失败。
说明:
如果需要升级App的功能,通过DIDE在Iboot的配置功能中,手动勾选loader即可。
24.3.44 updateiboot据字符串设置升级所需的一些信息
bool_t updateiboot(char *param)
头文件:
无。
参数:
字符串:runapp – 升级成功后运行App,runiboot – 升级成功后运行Iboot。后面再加待升级文件的路径
返回值:
true -- 成功;false – 失败。
说明:
参数为空:以默认路径为升级文件,升级成功后运行Iboot。
参数为/yaf/iboot123.bin:表示以/yaf/ iboot123.bin为升级文件的路径,升级成功后运行Iboot。
参数为runapp /yaf/ iboot123.bin:表示以/yaf/ iboot123.bin为升级文件的路径,升级成功后运行App。
24.3.45 App_UpdateIboot 升级Iboot
bool_t App_UpdateIboot(char *param)
头文件:
无。
参数:
无。
返回值:
true -- 成功;false – 失败。
说明:
如果需要升级Iboot的功能,通过DIDE在App的配置功能中,手动勾选loader即可。