, ,
(华中科技大学 自动化学院, 武汉 430074)
随着社会生产力的发展,社会、科技等领域传递的信息量越来越大。为保证这些信息的可靠传递就需要高精度的时间同步。并且随着某些领域的深入发展如电力系统、数字通信系统、军事领域等,它们对时间同步的同步精度的要求也越来越高。目前实现时间同步的方法主要有3种:无线电波授时、卫星授时和网络授时[1]。其中无线电授时和网络授时的授时精度比较低约为几毫秒,无法满足当下的需求。而卫星授时具有同步精度相对较高,时刻同步精度可以达到纳秒级,并且GPS信号不受地理环境,地域限制的影响具有准确性和可靠性。
Linux作为开源的操作系统因为其优良的稳定性,支持多任务、多使用者,安全性高等特点越来越受到程序员的青睐。PCI总线是当下使用最为广泛的总线协议之一,其在Linux下的驱动程序也备受关注。Linux操作系统因为其开源的特色,使得驱动程序在各个平台间的移植变得简单。
PCI同步时钟卡,其系统框图如图1所示。该设备以单片机为整个系统的控制单元,主要用于处理接收到的时间信号并控制双口RAM数据的读写。可编程逻辑CPLD用于产生高精度的同步绝对时标,将信息存储于双口RAM中。PC机通过PCI总线访问双口RAM中的数据,同时设定单片机和PC机访问双口RAM的通讯协议,以防止双方读写双口RAM产生混乱的情况[2]。PC机获取双口RAM时间数据有两种方式:一种是查询模式,PC机主动读取双口RAM时间数据;另一种是中断模式,通过外部事件触发中断一旦接收到中断信号就读取时间数据。本文主要介绍中断方式。
图1 同步时钟卡原理框图
Linux将I/O设备分为2类:字符设备和块设备。字符设备支持按字节或字符来读写数据,应用程序可以顺序从设备中读取数据。块设备支持按块(512字节)读写数据,应用程序可以随机访问设备数据,块设备并不支持基于字符的寻址[3]。PCI同步时钟卡属于字符设备。
在Linux中一个cdev结构描述一个字符设备驱动程序,当系统调用模块加载函数时,系统就会相应实例化一个cdev结构体。该结构体中主要包括设备号(dev_t)和文件操作集合(file_operations)两个部分。dev_t中存放有设备驱动程序所分配的初始主设备号和次设备号。file_operations结构体作为用户空间与内核空间交互的接口,使得用户空间能够对Linux进行系统调用。例如在用户空间中调用open()、read()等函数时,Linux会通过file_operations找到对应的操作函数xxx_open()、xxx_read()等函数完成对设备的操作。
2.1.1 设备号申请与释放
Linux操作系统是基于文件概念的,即它把所有的外部设备都当成文件来操作,我们把这类文件称为设备文件[4]。一个设备文件对应一个唯一确定的设备号,设备号由2个部分组成主设备号和次设备号。主设备号它标识了设备的类型,即同一类设备具有相同的主设备号并且他们共享相同的文件操作集合。次设备号用于标识同类设备中的一个特定设备。Linux提供了两种申请设备号的方法,register_chrdev_region (dev_t from
,unsigned count, const char *name)和alloc_chrdev_region(dev_t *dev, unsigned
baseminor,unsigned count,const char *name)。第一个属于静态申请设备号的方式,它需要指定设备号。第二种属于动态申请设备号的方式,它的设备号由系统主动分配无需指定。第二种方法能够有效避免设备号重复导致的申请失败。
相应的释放设备号函数为:unregister _chrdev_region(dev_t from,unsigned count)。
2.1.2 file_operations结构体
file_operations是字符设备驱动程序设计的主要部分,它的成员函数xxx_open()、xxx_read()等会在应用程序进行系统调用时被内核调用到。其主要部分如下所示。
struct file_operations {
struct module *owner;
int (*xxx_open) (struct inode *, struct file *);
ssize_t (*xxx_read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*xxx_write) (struct file *,const char __user *, size_t, loff_t *);
int (*xxx_release) (struct inode *, struct file *);
……
}
file_operations的成员函数选择是根据设备所需要完成的操作来选择相应的函数。
Linux内核中断处理机制如下图所示。为了减少中断信号的丢失同时提高中断响应的效率,Linux把中断处理程序分为两个部分:顶半部和底半部。
顶半部主要起登记的作用,一般情况下它只读取寄存器中的中断状态,并且在清除中断标志位后把那个设备发生的中断记录到底半部执行队列中去。这样,顶半部的工作量会很少执行速度就会很快,从而可以服务更多的中断请求。
底半部负责完成中断是需要完成的操作。一般顶半部会被设计成不可中断,底半部则可以被新的中断事件打断。底半部一般会在CPU空闲的时刻由系统主动调用执行。
2.2.1 申请和释放中断
在Linux下若想使用带有中断功能的设备,就应当在其设备驱动中完成相应的操作:申请和释放中断资源。Linux内核提供request_irq()和free_irq()函数完成对中断资源的申请和释放。
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
irq为要申请的硬件中断号。在PCI设备中该中断号是由系统对PCI初始化时完成分配,为此在系统对PCI设备探测时应该把探测到的中断号放到对应的设备结构体中去。
handler是中断发生时,系统调用的中断处理函数(顶半部)。
flags是中断触发方式以及处理方式。在触发方面,可以选择上升沿(IRQF_TRIGGER)、下降沿(IRQF_FALLING)、高电平(IRQF_TRIGGER _HIGH)、低电平(IRQF_TRIGGER_LOW)等。在处理方面,若是设置为IRQF_SHARED,表示申请的中断号支持其它设备申请即共享中断号;dev是要传递给中断服务程序的私有数据,当设备申请为共享中断时一般设置为这个设备结构体;当设备独占中断号时可以设置为NULL。
与request_irq()相对应的释放函数为
void free_irq(unsigned int irq, void *dev_id);
free_irq()中参数应当与request_irq()相同。
2.2.2 底半部机制
Linux实现底半部的机制主要有tasklet、工作队列和软中断[4]。软中断和 tasklet运行于中断上下文, 因此软中断和 tasklet 处理函数中不能睡眠;而工作队列运行于内核进程上下文,因此工作队列处理函数中允许睡眠。本文需要在中断过程中睡眠为此着重介绍工作队列。
要使用工作队列首先要定义一个工作队列和一个底半部执行函数:
struct work_struct pciszk_wq;
void pciszk_do_work(struct work_struct *work);
在初始化函数中通过INIT_WORK()初始化该队列并将工作队列与底半部执行函数绑定:
INIT_WORK(&pciszk_wq,pciszk_do_work);
最后要在顶半部处理函数中调度工作队列,该声明函数为:
schedule_work(&pciszk_wq);
在Linux操作系统中将程序执行状态分为两级:内核空间和用户空间。在内核空间中内核程序可以进行任何操作,而在用户空间中用户程序不允许直接对硬件和内存访问。用户态只能通过系统调用进入内核态完成对硬件的操作。因此当硬件中断到来时处于用户空间的应用程序无法直接得知该信号的到来,Linux提供了3种方式来处理:阻塞I/O,非阻塞轮询I/O和异步通知。
阻塞I/O与非阻塞I/O的区别在于前者在资源不可操作时系统会将该进程睡眠去执行其它进程,等到该资源可操作时再唤醒,而后者则会一直访问直至资源可获取。异步通知类似于硬件中断原理,在设备资源可获得时内核主动通知应用程序,避免应用程序查询设备状态。这3种方法并没有优劣之分,应根据自身需求合理选择。本文采用阻塞I/O的方法,阻塞I/O主要使用等待队列实现。
当用户空间执行read()、write()等系统调用时,如果设备的资源不能获取,那么该获取资源的进程就会进入睡眠,此时CPU将执行其它进程,直至资源可以获取后该进程将被唤醒,这一过程被称为阻塞操作,如图2所示[4]。Linux内核提供等待队列这一功能来实现阻塞进程的唤醒。
图2 阻塞I/O
等待队列操作如下所示。
定义并初始化一个等待队列:
wait_queue_head_t wqh;
init_waitqueue_head(&wqh);
设置等待事件:wait_event_interruptible (queue, condition); 该函数表示当等待队列queue被唤醒时,若condition为假则继续阻塞,若condition为真则唤醒进程。该条件可以自行确立即可以查询某一硬件状态也可以使用软件的办法。
唤醒等待事件:wake_up_interruptible (wait_queue_head_t *queue);该函数一般在某些操作完成后需要唤醒等待进程的地方调用。如在中断服务程序调用,那么当中断到来并响应时等待进程将被唤醒。
PCI有3种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间[5]。其中PCI配置空间中存放这PCI设备的配置信息,如PCI设备ID号、所使用I/O或内存基地址、中断号等。这些信息是在Linux扫描到PCI设备后主动写入到对应空间。
PCI设备总共有6个BAR的基地址寄存器(BAR0~BAR5),一般BAR0与BAR1为配置寄存器,BAR2~BAR5为本地地址空间。访问这些空间前要判断它是使用I/O空间还是内存空间,两者之间的访问方式有所不同。并且如果是内存空间还需要将它映射到核心虚地址空间中,然后才能根据映射得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。具体使用那个空间可以根据具体的PCI接口芯片数据手册查询。例如,本文使用的PCI9052芯片,BAR1使用I/O空间,BAR2使用内存空间。
pci_driver结构体中包含着PCI设备驱动的重要信息,它的结构如下所示。
struct pci_driver{
char * name;
const struct pci_device_id * id_table;
int (* probe) (struct pci_dev * dev, const struct pci_device_id * id);
void (* remove) (struct pci_dev *dev);
……
}
其中id_table中存放着该设备驱动支持的PCI设备ID号。probe函数为探测函数,主要用于申请空间、保存探测到的PCI配置信息如中断号、寄存器基地址等。remove函数用于注销设备。
pci_register_driver(struct pci_driver *driver)用于注册PCI设备,当调用该函数时系统会扫描所有的PCI设备,当有设备的ID号与id_table中的ID号相同时就会调用一次probe函数。相对应的注销函数为pci_unregister_driver(struct pci_driver * driver)。
3.3.1 中断信号
PCI设备使用INTA 、INTB 、INTC 和INTD 信号向系统发送中断请求,一般对于具有单一功能的PCI设备仅仅使用INTA 信号。对于PCI9052接口芯片,它的中断信号是在LINTi1或LINTi2接收到信号时产生的。
3.3.2 中断注册与处理
Linux驱动程序在使用中断前,需要向系统注册中断。使用request_irq(…)函数完成对中断号的申请、中断处理函数的申明。如果该中断号被其它设备使用了,应当先释放该中断号再向系统申请PCI中断。再根据数据手册查看中断使能位的偏移地址,完成对中断的使能开启中断。例如PCI9052其中断使能位的偏移地址为0x4ch。
当中断信号来时,中断处理函数除做一些基本操作外应当唤醒被阻塞的读操作。此时用户空间才能通过系统调用从内核空间中获取数据。如果在等待队列中使用软件的方法确立条件,那么在唤醒阻塞进程前应该先将唤醒条件置为真。在读操作完成后应该将唤醒条件置为假,等待下一次的中断到来。
本文在unbuntu14.04操作系统下进行开发,使用unbuntu14.04自带GCC编译器编译。驱动程序、应用程序和Makefile文件可以使用vim或gedit编译器编写。
将Makefile文件与驱动程序放到同一目录下,在终端下进入该目录执行make命令。该目录中会得到很多文件,其中后缀为.ko的文件为该驱动程序的可执行文件。执行以下步骤:
1)sudo insmod xxx.ko
将模块加载进内核,如果成功可以执行cat /proc/device 查看该设备的设备号,执行ls /dev/驱动名 查看该设备的设备文件,执行 cat /proc/interrupts 查看该设备的中断号。
2)gcc xxx.c -o xxx 编译应用程序得到可执行程序xxx
3)su 获取权限
4)./xxx 运行应用程序
实验结果如图3所示。图中的第二、三列数据表示日期和时间,其中时间信息后面四位表示200 us时间刻度;第四列数据表示此时PCI同步时钟卡与多少卫星同步对时,如果显示为0说明这时此时的时间信息来自于同步时钟卡的实时时钟;第五列数据表示时钟源信息的质量字节;第六列表示同步时钟卡的工作模式,有slave和master两种工作模式;第七列数据表示同步时钟卡的数据来源:GPS或者RTC。
图3 实验结果图
为了测试其中断功能,使用硬件电路生成一个周期为2.5 kHz的信号替代外部事件作为中断信号源。从图3可以看到每隔400 μs的时间刻度就读取一次时钟卡的信息并显示。经过长时间的测试没有发现丢失中断信号的现象。
采用PCI9052接口芯片实现对数据的传输,并且设计了硬件电路产生时间刻度以及将时间刻度发送到上位机的数据交互电路,以此达到上位机与同步时钟卡的数据传输的目的。在Linux下通过中断方式获取PCI同步时钟卡的时间信息,经测试表明中断信号未出现丢失现象,数据的传输也稳定可靠。工作在中断方式下的PCI同步时钟卡能够快速有效的判断电力系统的故障,为故障的排查提供有力的依据。
本文以Unbuntu14.04操作系统为平台,以PCI同步时钟卡为实际应用实例,着重描述了Linux下字符设备驱动和其中断机制的开发过程。Linux因为其开源,安全,可移植性高等优势,有越来越多的公司和个人使用它,这使得在Linux下开发设备拥有更加广阔的市场前景。 基于INTx的PCI中断一般会由多个设备共同使用,当某一设备的中断信号到来时内核不仅需要快速响应还要通过轮询的方式判断中断来源并调用中断处理函数,这样大大降低了系统效率。并且PCI设备通常仅有4个中断引脚,当PCI设备具有多种功能时中断引脚就会被复用。因此设备驱动程序必须查询设备产生的具体事件,这就会降低中断处理速度。为解决以上问题提高中断运行效率可以采用MSI中断机制[6]。
[1] 张治炼. 基于GPS授时的本地同步时钟的设计[D]. 成都:电子科技大学, 2012.
[2] 黄华华, 魏 丰, 邓林杰. PCI同步时钟卡的WDF驱动程序设计[J]. 数字技术与应用, 2015(5):147-150.
[3] DANIEL P. BOVET, MARCO CESATI. 深入理解Linux内核(第三版)[M]. 北京:中国电力出版社, 2007.
[4] 宋宝华. Linux设备驱动开发详解[M]. 北京:机械工业出版社, 2016.
[5] 杨兵见, 魏 丰, 陈永志. Linux下的PCIE同步时钟卡的设备驱动程序开发[J]. 计算机测量与控制, 2017, 25(1):98-104.
[6] 王 齐. PCI Express 体系结构导读[M]. 北京:机械工业出版社, 2010.