Linux 平台下PCI 接口视频采集卡的
驱动程序编写技术
李根深 邢汉承
0 引 言
Linux 由于其具备开放源码和免费的优点而且有一流的程序员设计和开发加上测试的开
放性非常可靠和稳定因而越来越多的人开始使用它来开发应用程序在应用程序的开发
中多媒体相关的应用倍受人们关注尤其是视频相关的技术更是吸引了业界的兴趣通常
情况下视频是通过在普通的PC 机中或者嵌入式系统中插入一块相应的视频捕捉卡得到的
为了使硬件工作就必须编写相应的驱动程序使得该卡纳入到操作系统的统一管理下并提
供卡与操作系统的接口现在市面上流行的视频采集卡有很多大部分都是PCI 接口的不
同的卡具有的功能也有区别比如有的卡具备硬件压缩功能可以将采集到的视频信息压缩
成MPEG 流有的甚至具备多路采集功能等等不一而足本文就针对PCI 接口的视频采集卡讲
述其一般编写技术
1 PCI 接口的驱动
1.1 PCI 配置空间
PCI 接口的驱动程序和PCI 总线密不可分现在的很多机器在机器启动的时候驻留ROM 中
的BIOS 通常已经完成了对PCI 总线的扫描并对PCI 总线上的各个设备进行自动配置同时
Linux 操作系统本身的PCI 初始化代码也提供了这一功能系统中的每个设备包括PCI-PCI 桥
都有一个配置结构我们称之为配置空间(Confiuration Space)
配置头部为我们提供了标识和控制设备的手段PCI 配置空间包含厂商标识设备标识
状态命令分类代码基地址寄存器中断引脚中断连线等域写PCI 接口的驱动程序
就是利用这些域来与设备进行沟通
1.2 PCI 驱动程序的结构
尽管PCI 卡设备变化万千但是控制他们的驱动程序的结构却是相似的大致上由下面
几个部分组成:
1.2.1 探测设备
顾名思义探测设备就是要找到驱动需要控制的设备随着内核的不断升级目前主要
有两种风格的设备探测方式两种方式的区别在于老的风格是手工探测而新的方式则把探
测的任务交给系统中的PCI 层由它来自动完成这样一来可以支持设备的热插拔因此非
常的方便和灵活一般在内核中开发驱动程序建议使用新的也就是自动探测方式如果出于
向后兼容的需要则仍然需要采用老的手工方式进行探测下面分别对这两种方式进行介绍
(1) 新的探测方式: 在驱动初始化代码中通过调用内核提供的一个函数
pci_register_driver 完成探测任务该函数的原型如下: int pci_register_driver(struct
pci_driver *) struct pci_driver 结构是个关键通过填写该结构的成员我们通知了PCI
层需要被探测的设备的相关信息该结构包含如下成员:
name:描述该驱动程序的名字
id_table:指向设备驱动程序感兴趣的设备ID 的一个列表
Probe:指向一个函数对于每一个与id_table 中的项匹配的且未被其他驱动程序处理的
设备在执行pci_register_driver 时候调用此函数或者如果是以后插入的一个新设备的话
只要满足上述条件也调用此函数
Remove: 指向一个函数当该驱动程序卸载或者被该驱动程序管理的设备被卸下的时候
将调用此函数
Save_state:用于在设备被挂起之前保存设备的相关状态
Suspend:挂起设备使之处于节能状态
Resume:唤醒处于挂起态的设备
enable_wake:使设备能够从挂起态产生唤醒事件
(2) 老的探测设备方式相对来说这种方式已经应用比较广泛了主要是利用
pcibios_xxx 类型的函数来实现手工探测可以利用厂商id 和设备id,或者设备分类代码
或者是厂商id 设备id 系统厂商id 子系统设备id 四者联合这三种方式进行探测相应
的函数分别使用
pci_find_device(VENDOR_ID, DEVICE_ID, dev)
pci_find_class(CLASS_ID, dev)
pci_find_subsys(VENDOR_ID, DEVICE_ID, SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev)
1.2.2 使能设备
使能设备的含义就是使得设备处于工作状态通过这一步就可以在驱动程序中申请设备
上的I/O 端口空间或者I/O 内存空间并加以使用使能设备只要调用pci_enable_device
就可以了另外如果设备需要作为总线主设备的话还需要调用pci_set_master 这样设备
才可以作为总线主设备存取目标设备的内容
1.2.3 申请资源
使能设备之后该设备上的I/O 端口空间或者存储空间就可以被申请了Linux 把I/O 空
间和内存空间都看成是系统中的资源使用之前必须显示地调用相关的函数进行申请申请
实际上是在系统中进行登记以避免出现资源使用的混乱局面对于使用中断的设备来说通
常还要申请中断号由于中断号只有16 个并且好几个都已经被用做特定用途PCI 接口设
备一般都是申请共享中断申请的时候需要指定相应的中断号和中断处理函数的地址以及设
备特定的参数设备特定的参数通常可以取做该设备对应的pci_dev 结构的指针该参数在
处理中断的时候会传给中断处理程序由于多个设备共享中断所以在中断处理程序的开始
要检查是否是该设备产生的中断如果该设备没有产生中断的话应该立即退出中断处理程序
的执行以便中断能够得到及时处理另外需要DMA 传送的设备来说还必须申请DMA 通道
对于视频采集卡而言一般都需要申请中断号和DMA 这里值得一提的是I/O 端口空间或者
存储空间以及中断号应该从探测设备而获得的pci_dev 结构中直接获得而不应该通过读设
备的配置空间寄存器获得因为内核可能对这些资源重新进行了映射申请了这些资源之后
可以在/proc 子目录下查看相应的文件了解到设备的资源使用情况
一般而言该设备的中断号会出现在/proc/interrupts 文件中设备的端口资源会在
/proc/ioports 中出现I/O memory 资源后会在/proc/iomem 中得到体现DMA 通道则在
/proc/dma 中反映出来
1.2.4 与设备通信
与设备通信可能是最为关键的部分了设备驱动程序的很大一部分工作就表现在这方
面具体的操作因设备而异需要参考具体设备的硬件资料去做但不外乎是通过读写设备
的端口或者存储空间来达到通信的目的Linux 下读写设备的I/O 端口是使用inb,outb 等函
数来实现的而读写I/O 存储空间可以使用readb,writeb 等函数读写I/O 的时候需要注意
的是有时候为了确保正确的执行顺序和防止编译器优化可以调用rmb,wmb 或者mb 等函数来
强制操作以预定的方式进行
1.2.5 注册设备
Linux 下外部设备分为字符设备和块设备以及网络设备字符设备和块设备的区别在于:
对字符设备发出读/写请求时,实际的硬件I/O 一般就紧接着发生了,块设备则不然,它利用一
块系统内存作缓冲区, 当用户进程对设备发出请求时缓冲区如果能满足用户的要求,就返回
请求的数据,如果不能,就调用相应函数来进行实际的I/O 操作块设备是主要针对磁盘等慢
速设备设计的, 以免耗费过多的CPU 时间来等待网络设备顾名思义通常是指网卡一类的设
备这类设备具有一定的特殊性因此单独划为一类视频采集卡这类设备按照定义应该划
做字符设备之列在系统内部I/O 设备的存取通过一组固定的入口点来进行这组入口点是
由每个设备的设备驱动程序提供的一般来说字符型设备驱动程序能够提供如下几个入口
点我们需要通过调用函数register_chrdev 来注册字符设备建立设备存取的入口点
设备的存取入口点主要是由register_chrdev 中的指向struct file_operations 结构的
指针参数来决定的该结构的形式如下
static struct file_operations video_fops = {
owner:THIS_MODULE,
llseek:video_lseek,
read:video_read,
write:video_write,
ioctl:video_ioctl,
mmap:video_mmap,
open:video_open,
release:video_release,
poll:video_poll,
};
从上面大家可以看出结构的大部分成员实际上都是些指向函数的指针这些成员的名
称跟Linux 系统调用完全相同事实上在应用程序中进行系统调用时如果相应的文件描述
符指向该驱动程序所代表的特殊字符文件的话那么内核的系统调用将调用该结构中对应成
员所指向的函数来真正实现与硬件的交互Register_chrdev 还有一个参数是驱动的主设备
号通过注册该主设备号和相应的结构建立了一对一的映射建立字符特殊设备文件时需
要输入主设备号和次设备号主设备号唯一决定和该文件相关的驱动程序而次设备号则用
来区分被同一个驱动程序所驱动的不同设备或者同一设备的不同驱动方式
2 驱动程序的开发方式
Linux 下驱动以两种方式存在一种方法可以将驱动编译进内核这时候需要修改内核的
配置文件然后重新编译内核使用起来非常不灵活,而且对于调试来说有时候往往会出现
致命的错误另外一种方式则是以模块的形式存在模块是随着Linux 内核的逐步开发而出
现的模块的优势在于它可以动态地被加载和卸载当被加载的时候才与内核进行连接此
时就能够使用内核所提供的所有函数了这种方式具备极大的灵活性方便了驱动程序的调
试特别地由于视频采集卡的驱动对于系统启动毫无关系所以建议采用模块的方式开发驱
动程序值得提醒的是如果视频采集卡驱动程序需要使用下面提到的V4L 和V4L2 的话则必
须以模块的方式编写每个模块都有一个入口和出口函数当加载模块的时候调用入口函数
通常在这里进行设备的注册工作与此对应在卸载模块的时候调用出口函数完成设备的注
销工作模块的管理工作由modutils 包来进行管理里面包括实用工具insmod(加载模
块),rmmod 卸载模块modprobe 主动探测模块间的依赖关系并加载模块等
3 V4L 和V4L2
V4L 和V4L2 是Linux 下开发视频采集驱动的一套规范这套规范采用分层的方法给驱动
的开发提供了清晰的模型和一致的接口V4L 和V4L2 是上层而实际的硬件相关部分是下层
3.1 V4L
V4L 代表video for Linux,它是Alan Cox 为了给Linux 下的视频采集设备驱动的编写提
供统一的接口而设计的一套规范,它使得所有的视频采集设备驱动程序都纳入它的管理之
中给驱动程序的编写提供了极大的方便在RedHat Linux 的2.x 发行版本中缺省的内核
包含了它实际上它是通过一个内核模块的方式而存在的其相关的源文件是
drivers/media/video/videodev.c 文件以及一个头include/Linux/videodev.h 它为Linux
下的视频设备提供了一套API 和相关的标准V4L 将视频设备分为四类第一类是GRABBER 类
型就是常规的视频采集设备第二类是VTX 类型这类代表文字电视广播teletext 接
受设备第三类是vbi 类型代表垂直消隐间隔(Vertical Blanking Interval)设备第四
类是radio 类型代表无线电接受设备所有的视频设备特殊文件的主设备号都是一致的
都是81 但是不同的硬件类型其特殊文件的次设备号不一致V4L 规定第一种类型的设备文
件次设备号的范围在0-63 区间内第二种类型的设备文件次设备号范围在192-223 区间内
第三种类型的设备文件次设备号在224-239 范围内第四种类型的设备文件次设备号则在
64-127 范围内做了这样的限定之后可以从设备文件的次设备号中判断该设备到底属于那种
类型V4L 和特定的设备驱动是通过它定义的一些数据结构和相关的函数关联起来的其中关
键的数据结构是video_device 结构其定义如下:
struct video_device
{
struct module *owner;
char name[32];
int type;
int hardware;
int (*open)(struct video_device *, int mode);
void (*close)(struct video_device *);
long (*read)(struct video_device *, char *, unsigned long, int noblock);
/* Do we need a write method ? */
long (*write)(struct video_device *, const char *, unsigned long, int noblock);
#if LINUX_VERSION_CODE >= 0x020100
unsigned int (*poll)(struct video_device *, struct file *, poll_table *);
#endif
int (*ioctl)(struct video_device *, unsigned int , void *);
int (*mmap)(struct video_device *, const char *, unsigned long);
int (*initialize)(struct video_device *);
void *priv; /* Used to be 'private' but that upsets C++ */
int busy;
int minor;
devfs_handle_t devfs_handle;
};
该结构对应于驱动程序控制的一个设备其中的minor 成员代表该设备对应的次设备号
其他的open,close,read,write,ioctl,mmap initialize 等成员都代表相应的函数指针这
些函数指针都是指示特定设备的相关操作V4L 利用一个统一的接口注册了另一个设备操作结
构实际上就是上面1.2.5 节注册设备里面提到的那个示例这个统一的设备操作结构里的
驱动接口函数所做的事情就是根据被打开特殊设备文件的次设备号来定位该设备文件对应的
struct video_device 从而实现对特定设备的控制为了实现这种定位V4L 定义了一个静
态数组static struct video_device *video_device[256],每个特定的设备都在该数组里占
据一项编写驱动程序时 需要通过V4L 提供的一个函数video_register_device 将每个所
要控制的硬件设备数据结构填充到该静态数组中调用该函数时候应该提供设备特定数据结
构以及设备类型
3.2 V4L2
V4L 当初开发的时候由于种种原因考虑的方面相对比较欠缺而且支持的硬件也比较
少Bill Dirks 基于V4L 的不足之处重新设计了一套API 和数据结构把它称为Video For
Linux Two(V4L2),相对于V4L 来说其可扩展性和灵活性都得到了大大的提高并且能够支
持更多的设备V4L2 对V4L 进行了彻底的改造很多关键的API 发生了改变因此并不与V4L
向后兼容为了能让老的设备驱动程序能够在V4L2 下工作必须做相关的移植工作V4L2
下的代表特定设备的关键数据结构发生了变化其定义如下
struct v4l2_device
{
char name[32];
int type;
int minor;
int (*open)(struct v4l2_device *v,int flags, void **idptr);
void (*close)(void *id);
long (*read)(void *id,char *buf, unsigned long count, int noblock);
long (*write)(void *id,const char *buf, unsigned long count, int noblock);
int (*ioctl)(void *id,unsigned int cmd, void *arg);
int (*mmap)(void *id,struct vm_area_struct *vma);
int (*poll)(void *id,struct file *file, poll_table *table);
int (*initialize)(struct v4l2_device *v);
void *priv; /* may be used by the driver */
int busy; /* open count maintained by videodev.c */
void *v4l2_priv; /* for V4L2 use */
int v4l2_reserved[4]; /* for V4L2 use */
#if (LINUX_VERSION_CODE >= 0x020300) || defined(CONFIG_FS_DEVFS)
devfs_handle_t devfs_handle;
char devfs_devname[16];
#endif
};
从上面可以看到很多成员的类型发生了变化与设备操作相关的函数参数中多了个id 参
数这个参数的作用在于标记设备的某次打开V4L2 允许某些情况下设备被多次打开通过
引进id 参数就能够使得设备的多次打开得以区分开来V4L2 能够支持更多的设备它对设备
的类型定义了相应的一些宏能够支持带有压缩等功能更加复杂的芯片
4 视频采集卡设备中的芯片初始化和控制
编写视频采集卡设备的驱动首要的是要弄懂视频流的走向一般的PCI 视频采集卡大致
是这样的模拟视频信号从外部进入到卡设备上的Decoder 芯片完成模拟到数字的转换功
能然后经过转换的数字信号进入具有PCI 接口的芯片中在这里可以通过DMA 到达内存
中然后更进一步的处理可以将其保存到硬盘上或者送到显卡的缓冲区进行预览假如设
备卡上接有压缩芯片如MPEG 编码芯片的话那么从Decoder 出来的信号有一路就到达了压缩
芯片进行压缩处理压缩后的数据流同样到达具有PCI 接口的芯片然后再DAM 到内存以进一
步处理Decoder 芯片是通过具PCI 接口的芯片来控制的最常见的方式就是通过I2C 总线来
控制I2C 总线是Philips 公司为了实现集成芯片之间的有效控制而设计的一个简单的两线双
向总线其中一条是串行数据线(SDL) 另一条是串行时钟线(SCL) 每一个连接到该总线的
芯片都通过一个唯一的地址来进行软件寻址芯片之间存在着主从关系在视频采集卡的例
子中一般来说具PCI 接口的芯片扮演着主设备的角色而Decoder 则扮演着从设备的角
色PCI 接口芯片就通过I2C 对Decoder 进行初始化和控制
Decoder 的初始化就是设置视频格式方面的参数如水平同步垂直回扫采集的制式等
具体的细节需要参阅芯片的数据手册如果带压缩芯片的话也需要通过其它的总线方式对
压缩芯片进行初始化和控制如对于比较流行的PCI 接口芯片philips saa7146 而言大都
通过它的DEBI 数据扩展总线接口来初始化和控制压缩芯片
从上面的分析可以看出对于视频采集卡而言对PCI 接口芯片的初始化和控制起着至
关重要的作用通常我们需要对其进行如下编程(1) 使其跟DMA 控制器联系起来以便能够
进行DMA 传输(2) 使其知道以什么方式触发PCI 中断(3) 撰写PCI 中断处理代码以便在
中断发生时进行实际的数据传输经过这一系列的编程操作之后PCI 视频采集卡设备就可以
实现它应该具备的功能了
5 调试
驱动程序的调试目前主要有两种方式一种是通过在程序中适当的地方调用printk 函数
打印相关消息以便事后观察来达到调试的目的printk 函数是内核提供的一个输出函数相
当于用户应用程序中的printf 这种调试方式相对来说简单方便而且效果也不错另外一
种方式是远程调试即通过串行总线在另外一台机器上借助于kgdb 工具进行调试kgdb 是专
门用于调试核心态程序的工具这种方式虽然调试的灵活性大大增加但是调试成本大大增
加了而且由于是在远程调试实施起来非常麻烦,需要比较高的技巧
6 小结
PCI 视频采集卡设备的驱动程序牵涉到很多知识只有对Linux 内核很多的实现机制有深
入的领会并且熟悉视频的相关知识结合对硬件的透彻理解才能编写出一个实用的稳定
可靠的驱动程序笔者通过对一个基于Philips saa7146 芯片的实际采集卡所做的实践对这
类驱动程序的编写进行了比较全面的总结希望对从事同类开发的人员有所裨益
阅读(640) | 评论(0) | 转发(0) |