【Linux开发】【DSP开发】Linux设备驱动之——PCI 总线
PCI总线概述
随着通用处理器和嵌入式技术的迅猛发展,越来越多的电子设备需要由处理器控制。目前大多数CPU和外部设备都会提供PCI总线的接口,PCI总线已成为计算机系统中一种应用广泛、通用的总线标准[1]。Linux因其开放源代码以及稳定的性能,越来越受到广大用户青睐。同时,基于Linux内核的嵌入式操作系统应用势头强劲,开发基于Linux的设备驱动程序,具有很强的实用性和可移植性。
PCI总线概述
PCI(Peripheral ComponentInterconnect)总线,即外部设备互连,是现在流行的一种连接PC和外围设备的总线结构。PCI提供了一组完整的总线接口规范,可以在33MHz时钟频率、32 bit数据总线宽度的条件下达到峰值132Mb/s的传输速率;它能支持一种称为线性突发的数据传输模式,可确保总线不断满载数据;采用总线主控与同步操作,显著改善PCI的性能;PCI独立于处理器的结构,用户可随意增添外围设备,以扩展电脑系统而不必担心在不同时钟频率下会导致性能下降。
从图中我们可以看出PCI 总线架构主要被分成三部分:
1.PCI 设备。符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio 、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
2.PCI 总线。PCI 总线在系统中可以有多条,类似于树状结构进行扩展,每条 PCI 总线都可以连接多个 PCI 设备/ 桥。上图中有两条 PCI 总线。
3.PCI 桥。当一条 PCI 总线的承载量不够时,可以用新的 PCI 总线进行扩展,而 PCI 桥则是连接 PCI 总线之间的纽带。图中的 PCI 桥有两个,一个桥用来连接处理器、内存以及 PCI 总线,而另外一条则用来连接另一条 PCI 总线。
PCI设备与驱动
1. PCI相关结构体
Linux 提供了三类数据结构用以描述 PCI 控制器、PCI 设备以及 PCI 总线。
PCI 控制器:
struct pci_controller {
struct pci_controller *next; //该属性指向下一个 PCI 控制器
struct pci_bus *bus; // 该属性标志了当前 PCI 控制器所连接的 PCI 总线
struct pci_ops *pci_ops; //该属性标志了当前 PCI 控制器所对应的 PCI 配制空间读写操作函数
struct resource *mem_resource; //该属性标志了当前 PCI 控制器所支持的 Memory 地址区间
unsigned long mem_offset; //地址偏移量
struct resource *io_resource; //该属性标志了当前 PCI 控制器所支持的 IO 地址空间。
unsigned long io_offset; //偏移量
unsigned long io_map_base; //PCI 设备的 IO map 基地址
unsigned int index; //该属性标志 PCI 控制器的编号。
/* For compatibility with current (as of July 2003) pciutils
and XFree86. Eventually will be removed. */
unsigned int need_domain_info; //域信息
int iommu; //MMU
/* Optional access methods for reading/writing the bus number
of the PCI controller */
int (*get_busno)(void);
void (*set_busno)(int busno);
};
PCI 总线:
struct pci_bus {
struct list_head node; /* node in list of buses */
struct pci_bus *parent; /* parent bus this bridge is on */
struct list_head children; //总线连接的所有 PCI 子总线链表。
struct list_head devices; //总线连接的所有 PCI 设备链表
struct pci_dev *self; //该属性标志了连接的上行 PCI 桥
struct list_head slots; /* list of slots on this bus */
struct resource *resource[PCI_BUS_NUM_RESOURCES];
//该属性标志了 Memory/IO 地址空间。
struct pci_ops *ops; //该属性标志了总线上所有 PCI 设备的配制空间读写操作函数。
void *sysdata; //指向系统特定的扩展数据
struct proc_dir_entry *procdir; /* directory entry in /proc/bus/pci */
unsigned char number; /* bus number */
unsigned char primary; /* number of primary bridge */
unsigned char secondary; /* number of secondary bridge */
unsigned char subordinate; /* max number of subordinate buses */
char name[48];
unsigned short bridge_ctl; /* manage NO_ISA/FBB/et al behaviors */
pci_bus_flags_t bus_flags; /* Inherited by child busses */
struct device *bridge;
struct device dev;
struct bin_attribute *legacy_io; /* legacy I/O for this bus */
struct bin_attribute *legacy_mem; /* legacy mem */
unsigned int is_added:1;
};
PCI 设备:
每种类的PCI设备都可以用结构类型pci_dev来描述。更为准确地说,应该是每一个PCI功能,即PCI逻辑设备都唯一地对应有一个pci_dev设备描述符。该数据结构的定义部分属性如下(include/linux/pci.h):
struct pci_dev {
struct list_head bus_list; /* node in per-bus list */
struct pci_bus *bus; /* bus this device is on */
struct pci_bus *subordinate; /* bus this device bridges to */
void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */
struct pci_slot *slot; /* Physical slot this device is in */
unsigned int devfn; //标志了设备编号和功能编号。
unsigned short vendor; //属性标志了供应商编号
unsigned short device; //标志 设备编号
unsigned short subsystem_vendor; //这是一个16无符号整数,表示PCI设备的子系统厂商ID
unsigned short subsystem_device;
struct pci_driver *driver; //指向这个PCI设备所对应的驱动程序定义的pci_driver结构
u8 revision; /* PCI revision, low byte of class word */
u8 hdr_type; /* PCI header type (`multi' flag masked out) */
u8 pcie_type; /* PCI-E device/port type */
u8 rom_base_reg; /* which config register controls the ROM */
u8 pin; /* which interrupt pin this device uses */
}
总线设备链表元素bus_list:每一个pci_dev结构除了链接到全局设备链表中外,还会通过这个成员连接到其所属PCI总线的设备链表中。每一条PCI总线都维护一条它自己的设备链表视图,以便描述所有连接在该PCI总线上的设备,其表头由PCI总线的pci_bus结构中的 devices成员所描述。
hdr_type:8位符号整数,表示PCI配置空间头部的类型。其中,bit[7]=1表示这是一个多功能设备,bit[7]=0表示这是一个单功能设备。Bit[6:0]则表示PCI配置空间头部的布局类型,值00h表示这是一个一般PCI设备的配置空间头部,值01h表示这是一个PCI-to-PCI桥的配置空间头部,值02h表示CardBus桥的配置空间头部
rom_base_reg:8位无符号整数,表示PCI配置空间中的ROM基地址寄存器在PCI配置空间中的位置。ROM基地址寄存器在不同类型的PCI配置空间头部的位置是不一样的,对于type 0的配置空间布局,ROM基地址寄存器的起始位置是30h,而对于PCI-to-PCI桥所用的type 1配置空间布局,ROM基地址寄存器的起始位置是38h
2.PCI设备与驱动关系
PCI设备通常由一组参数唯一地标识,它们被vendorID,deviceID和class nodes所标识,即设备厂商,型号等,这些参数保存在 pci_device_id结构中。每个PCI设备都会被分配一个pci_dev变量,内核就用这个数据结构来表示一个PCI设备。
所有的PCI驱动程序都必须定义一个pci_driver结构变量,在该变量中包含了这个PCI驱动程序所提供的不同功能的函数,同时,在这个结构中也包含了一个device_driver结构,这个结构定义了PCI子系统与PCI设备之间的接口。在注册PCI驱动程序时,这个结构将被初始化,同时这个 pci_driver变量会被链接到pci_bus_type中的驱动链上去。
在pci_driver中有一个成员struct pci_device_id *id_table,它列出了这个设备驱动程序所能够处理的所有PCI设备的ID值。
3.PCI设备与驱动的绑定过程
下面描述一下对于PCI设备与驱动绑定的过程。首先在系统启动的时候,PCI总线会去扫描连接到这个总线上的设备,同时为每一个设备建立一个pci_dev结构,在这个结构中有一个device成员,并将这些pci_dev结构链接到PCI总线描述符上的devices链。如下图所示:
第二步是当PCI驱动被加载时,pci_driver结构体将被初始化,这一过程在函数pci_register_driver中:
drv->driver.bus = &pci_bus_type;
drv->driver.probe = pci_device_probe;
最后会调用driver_register(&drv->driver)将这个PCI驱动挂载到总线描述符的驱动链上。同时在注册的过程中,会根据pci_driver中的id_table中的ID值去查看该驱动支持哪些设备,将这些设备挂载到pci_driver中的devices链中来。如下图所示:
对于不同的设备,可能驱动程序也不一样,因此,对于上图中的Dev3,可能就需要另外一个驱动程序来对其进行驱动。所以当加载了Dev3的驱动程序时,其示意图如下图所示:
PCI设备驱动程序
1.基本框架
在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。
上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。
<span style="font-size: 16px;">/* 指明该驱动程序适用于哪一些PCI设备 */
static struct pci_device_id xxx_pci_tbl [] __initdata = {
{PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},
{0,}
};
/* 对特定PCI设备进行描述的数据结构 */
struct xxx_card {
unsigned int magic;
/* 使用链表保存所有同类的PCI设备 */
struct xxx_card *next; /* ... */
}
/* 中断处理模块 */
static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
/* ... */
}
/* 设备文件操作接口 */
static struct file_operations demo_fops = {
owner: THIS_MODULE, /* demo_fops所属的设备模块 */
read: xxx_read, /* 读设备操作*/
write: xxx_write, /* 写设备操作*/
ioctl: xxx_ioctl, /* 控制设备操作*/
mmap: xxx_mmap, /* 内存重映射操作*/
open: xxx_open, /* 打开设备操作*/
release: xxx_release /* 释放设备操作*/
/* ... */
};
/* 设备模块信息 */
static struct pci_driver demo_pci_driver = {
name: xxx_MODULE_NAME, /* 设备模块名称 */
id_table: xxx_pci_tbl, /* 能够驱动的设备列表 */
probe: xxx_probe, /* 查找并初始化设备 */
remove: xxx_remove /* 卸载设备模块 */
/* ... */
};
static int __init xxx_init_module (void)
{
/* ... */
}
static void __exit xxx_cleanup_module (void)
{
pci_unregister_driver(&xxx_pci_driver);
}
/* 加载驱动程序模块入口 */
module_init(xxx_init_module);
/* 卸载驱动程序模块入口 */
module_exit(xxx_cleanup_module);</span>
2.初始化
在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:
- 检查PCI总线是否被Linux内核支持;
- 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
- 读出配置头中的信息提供给驱动程序使用。
当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:
<span style="font-size: 16px;">static int __init xxx_init_module (void)
{
/* 检查系统是否支持PCI总线 */
if (!pci_present())
return -ENODEV;
/* 注册硬件驱动程序 */
if (!pci_register_driver(&demo_pci_driver)) {
pci_unregister_driver(&demo_pci_driver);
return -ENODEV;
}
/* ... */ return 0;
}</span>
驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。
<span style="font-size: 16px;">static int __init xxx_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)
{
struct xxx_card *card;
/* 启动PCI设备 */
if (pci_enable_device(pci_dev))
return -EIO;
/* 设备DMA标识 */
if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {
return -ENODEV;
}
/* 在内核空间中动态申请内存 */
if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {
printk(KERN_ERR "pci_demo: out of memory\n");
return -ENOMEM;
}
memset(card, 0, sizeof(*card));
/* 读取PCI配置信息 */
card->iobase = pci_resource_start (pci_dev, 1);
card->pci_dev = pci_dev;
card->pci_id = pci_id->device;
card->irq = pci_dev->irq;
card->next = devs;
card->magic = DEMO_CARD_MAGIC;
/* 设置成总线主DMA模式 */
pci_set_master(pci_dev);
/* 申请I/O资源 */
request_region(card->iobase, 64, card_names[pci_id->driver_data]);
return 0;
}</span>
3.OPEN
在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。
<span style="font-size: 16px;">static int xxx_open(struct inode *inode, struct file *file)
{
/* 申请中断,注册中断处理程序 */
request_irq(card->irq, &xxx_interrupt, SA_SHIRQ,
card_names[pci_id->driver_data], card)) {
/* 检查读写模式 */
if(file->f_mode & FMODE_READ) {
/* ... */
}
if(file->f_mode & FMODE_WRITE) {
/* ... */
} /* 申请对设备的控制权 */
down(&card->open_sem);
while(card->open_mode & file->f_mode) {
if (file->f_flags & O_NONBLOCK) {
/* NONBLOCK模式,返回-EBUSY */
up(&card->open_sem);
return -EBUSY;
} else {
/* 等待调度,获得控制权 */
card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);
up(&card->open_sem);
/* 设备打开计数增1 */
MOD_INC_USE_COUNT;
/* ... */
}
}
}</span>
4.数据的读写和控制
PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:
<span style="font-size: 16px;">static int xxx_ioctl(struct inode *inode, struct file *file,
unsigned int cmd, unsigned long arg)
{
/* ... */ switch(cmd) {
case DEMO_RDATA:
/* 从I/O端口读取4字节的数据 */
val = inl(card->iobae + 0x10); /* 将读取的数据传输到用户空间 */
return 0;
} /* ... */
}</span>
事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。
5.中断处理
PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。
<span style="font-size: 16px;">static void xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct xxx_card *card = (struct xxx_card *)dev_id;
u32 status;
spin_lock(&card->lock);
/* 识别中断 */
status = inl(card->iobase + GLOB_STA);
if(!(status & INT_MASK))
{
spin_unlock(&card->lock);
return; /* not for us */
}
/* 告诉设备已经收到中断 */
outl(status & INT_MASK, card->iobase + GLOB_STA);
spin_unlock(&card->lock); /* 其它进一步的处理,如更新DMA缓冲区指针等 */
}</span>
6.释放和卸载
释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:
<span style="font-size: 16px;">static int xxx_release(struct inode *inode, struct file *file)
{
/* ... */ /* 释放对设备的控制权 */
card->open_mode &= (FMODE_READ | FMODE_WRITE); /* 唤醒其它等待获取控制权的进程 */
wake_up(&card->open_wait);
up(&card->open_sem); /* 释放中断 */
free_irq(card->irq, card); /* 设备打开计数增1 */
MOD_DEC_USE_COUNT; /* ... */
}</span>
卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:
<span style="font-size: 16px;">static void __exit xxx_cleanup_module (void)
{
pci_unregister_driver(&xxx_pci_driver);
}
<span class="atitle"></span></span>
参考文章:http://www.ibm.com/developerworks/cn/linux/l-pci/index.html
http://www.ibm.com/developerworks/cn/linux/l-cn-pci/index.html
http://bbs.chinaunix.net/thread-2010492-1-1.html
【Linux开发】【DSP开发】Linux设备驱动之——PCI 总线的更多相关文章
- Linux设备驱动开发基础--阻塞型设备驱动
1. 当一个设备无法立刻满足用户的读写请求时(例如调用read时,设备没有数据提供),驱动程序应当(缺省的)阻塞进程,使它进入等待(睡眠)状态,知道请求可以得到满足. 2. Linux内核等待队列:在 ...
- linux驱动开发( 五) 字符设备驱动框架的填充file_operations结构体中的操作函数(read write llseek unlocked_ioctl)
例子就直接使用宋宝华的书上例子. /* * a simple char device driver: globalmem without mutex * * Copyright (C) 2014 Ba ...
- Linux应用程序访问字符设备驱动详细过程【转】
本文转载自:http://blog.csdn.net/coding__madman/article/details/51346532 下面先通过一个编写好的内核驱动模块来体验以下字符设备驱动 可以暂时 ...
- arm-linux字符设备驱动开发之---简单字符设备驱动
一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 1.字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面 ...
- linux下自动加载设备驱动程序模块
假设你的设备驱动程序为:yourdrivername.ko 1 cp yourdrivername.ko /lib/modules/"version"/kernel/driver ...
- 【VS开发】【DSP开发】浅谈Linux PCI设备驱动(二)
我们在 浅谈Linux PCI设备驱动(一)中(以下简称 浅谈(一) )介绍了PCI的配置寄存器组,而Linux PCI初始化就是使用了这些寄存器来进行的.后面我们会举个例子来说明Linux PCI设 ...
- 【DSP开发】【Linux开发】Linux下PCI设备驱动程序开发
PCI是一种广泛采用的总线标准,它提供了许多优于其它总线标准(如EISA)的新特性,目前已经成为计算机系统中应用最为广泛,并且最为通用的总线标准.Linux的内核能较好地支持PCI总线,本文以Inte ...
- 【分享】4412开发板-嵌入式Linux开发须要掌握的基础知识和技能
本文转自迅为电子论坛:http://www.topeetboard.com 1.Linux 基础 安装Linux操作系统 Linux文件系统 Linux经常使用命令 Linux启动过程具体解释 熟悉L ...
- Linux设备驱动模型之platform(平台)总线详解
/********************************************************/ 内核版本:2.6.35.7 运行平台:三星s5pv210 /*********** ...
随机推荐
- C++中虚函数继承类的内存占用大小计算
计算一个类对象的大小时的规律: 1.空类.单一继承的空类.多重继承的空类所占空间大小为:1(字节,下同): 2.一个类中,虚函数本身.成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空 ...
- spring + junit 测试
spring + junit 测试 需要一个工具类 package com.meizu.fastdfsweb; import org.junit.runner.RunWith; import org. ...
- 1、Socket通信
[TCP] 服务器端:无目标插座升级为有目标插座后,就可以通过有目标的插座收发数据 客户端: 实战:此案例有利于理解Socket的工作流程. 缺点:服务器只能接收1个客户端的连接,因为只写了一个Acc ...
- httpclient个人理解
httpclient:模拟浏览器发送请求,服务器会响应数据,用心区域网内 不同系统间的请求调用 依赖 httpclient.jar和httpcore.jar需要同时纯在 <dependency ...
- c++顺序表(数组)查找最大最小值
#include <iostream> #define MAXSIZE 100 using namespace std; void find(int a[],int m) {int min ...
- 未关闭虚拟机直接关闭vmware引发的一系列问题——Windows下linux虚拟机
虚拟机长时间挂起重新打开时卡顿,无法开启,脑抽直接关闭了vmware软件引起的一系列问题. 原因是关闭了vmware,但是相应的虚拟机并没有关闭,所以虚拟机不能重开 会出现如下提示 解决方案如下: 1 ...
- Spring Boot教程(三十九)使用MyBatis注解配置详解(2)
增删改查 MyBatis针对不同的数据库操作分别提供了不同的注解来进行配置,在之前的示例中演示了@Insert,下面针对User表做一组最基本的增删改查作为示例: public interface U ...
- Spring Boot教程(三十八)使用MyBatis注解配置详解(1)
之前在Spring Boot中整合MyBatis时,采用了注解的配置方式,相信很多人还是比较喜欢这种优雅的方式的,也收到不少读者朋友的反馈和问题,主要集中于针对各种场景下注解如何使用,下面就对几种常见 ...
- 教程:myeclipse在线安装svn插件
SVN 版本控制,相信开发过程中都很多有用到,今天在myeclipse 在线安装了SVN插件.下面是具体步骤,记录下,希望对有需要的朋友提供帮助. 要求: Myeclispe,电脑能连接互联网 步骤: ...
- 协程系列之Event Loops
Event Loops 事件循环 事件是由程序的一部分在特定条件下发出的消息,循环是在某种条件下完成并执行某个程序直到它完成的构造,因此,事件循环是一个循环,它允许用户订阅事件传输并注册处理程序/回调 ...