前面介绍的是最底层的硬件控制,这部分将介绍高级总线架构的一些综述,总线由电气接口和编程接口够成。下面将重点介绍PCI总线的编程接口以及对应的内核函数。
PCI总线是当今普遍使用在桌面以及更大型计算机上的外设总线,而且该总线是内核中得到最好支持的总线。尽管许多计算机用户将PCI看成是一种布置电子线路的方式,但实际上它是一组完整的规范,定义了计算机的各个不同部分之间该如何交互。
PCI规范涵盖了与计算机接口相关的大部分问题。这里详细介绍PCI驱动程序如何寻找其硬件和获得对它的访问。
PCI架构的三个主要目标:
PCI 总线体系结构是一种层次式的体系结构。在这种层次式体系结构中,PCI 桥设备占据着重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树型结构。树的顶端是系统的 CPU,它通过一个较为特殊的 PCI 桥设备——Host/PCI 桥设备与根 PCI 总线连接起来。
作为一种特殊的 PCI 设备,PCI 桥包括以下几种:
在 Linux 系统中,PCI 总线用 pci_bus 来描述,这个结构体记录了本 PCI 总线的信息以及本 PCI 总线的父总线、子总线、桥设备信息,这个结构体的定义:
struct pci_bus
{
struct list_head node; /* 链表元素 node */
struct pci_bus * parent; /*指向该 PCI 总线的父总线,即 PCI 桥所在的总线 */
struct list_head children; /* 描述了这条 PCI 总线的子总线链表的表头 */
struct list_head devices; /* 描述了这条 PCI 总线的逻辑设备链表的表头 */
struct pci_dev * self; /* 指向引出这条 PCI 总线的桥设备的 pci_dev 结构 */
struct resource * resource[PCI_BUS_NUM_RESOURCES];
/* 指向应路由到这条 PCI 总线的地址空间资源 */
struct pci_ops * ops; /* 这条 PCI 总线所使用的配置空间访问函数 */
void *sysdata; /* 指向系统特定的扩展数据 */
struct proc_dir_entry * procdir; /*该 PCI 总线在/proc/bus/pci 中对应目录项*/
unsigned char number; /* 这条 PCI 总线的总线编号 */
16 unsigned char primary; /* 桥设备的主总线 */
unsigned char secondary; /* PCI 总线的桥设备的次总线号 */
18 unsigned char subordinate; /*PCI 总线的下属 PCI 总线的总线编号最大值*/
19 char name[48];
unsigned short bridge_ctl;
unsigned short pad2;
struct device * bridge;
struct class_device class_dev;
struct bin_attribute * legacy_io;
struct bin_attribute * legacy_mem;
};
系统中当前存在的所有根总线都通过其 pci_bus 结构体中的 node 成员链接成一条全局的根总线链表,其表头由 list 类型的全局变量 pci_root_buses 来描述。而根总线下面的所有下级总线则都通过其 pci_bus 结构体中的 node 成员链接到其父总线的children 链表中。这样,通过这两种 PCI 总线链表,Linux 内核就将所有的 pci_bus 结构体以一种倒置树的方式组织起来。
在 Linux 系统中,所有种类的 PCI 设备都可以用 pci_dev 结构体来描述,由于一个 PCI 接口卡上可能包含多个功能模块,每个功能被当作一个独立的逻辑设备,因此,每一个 PCI 功能,即 PCI 逻辑设备都唯一地对应一个 pci_dev 设备描述符。该结构体为:
struct pci_dev
{
struct list_head global_list; /* 全局链表元素 */
struct list_head bus_list; /* 总线设备链表元素 */
struct pci_bus * bus; /* 这个 PCI 设备所在的 PCI 总线的 pci_bus 结构 */
struct pci_bus * subordinate; /* 指向这个 PCI 设备所桥接的下级总线 */
void *sysdata; /* 指向一片特定于系统的扩展数据 */
struct proc_dir_entry * procent; /* 该 PCI 设备在/proc/bus/pci 中对应的目录项 */
unsigned int devfn; /* 这个 PCI 设备的设备功能号 */
unsigned short vendor; /* PCI 设备的厂商 ID*/
unsigned short device; /* PCI 设备的设备 ID */
unsigned short subsystem_vendor; /* PCI 设备的子系统厂商 ID */
unsigned short subsystem_device; /* PCI 设备的子系统设备 ID */
unsigned int class; /* 32 位的无符号整数,表示该 PCI 设备的类别, bit[7∶0]为编程接口,bit[15∶8]为子类别代码,bit[23∶16]为基类别代码,bit[31∶24]无意义 */
u8 hdr_type; /* PCI 配置空间头部的类型 */
u8 rom_base_reg; /* 表示 PCI 配置空间中的 ROM 基地址寄存器在 PCI 配置空间中的位置 */
struct pci_driver * driver; /* 指向这个 PCI 设备所对应的驱动 pci_driver
结构 */
u64 dma_mask; /* 该设备支持的总线地址位掩码,通常是 0xffffffff */
pci_power_t current_state; /* 当前的操作状态 */
struct device dev; /* 通用的设备接口 */
/* 定义这个 PCI 设备与哪些设备相兼容 */
unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];
int cfg_size; /* 配置空间大小 */
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE];
/*表示该设备可能用到的资源,包括:I/O 端口区域、设备内存地址区域以及扩展 ROM 地址区域 */
unsigned int transparent : 1; /* 透明 PCI 桥 */
unsigned int multifunction : 1; /* 多功能设备 */
/* keep track of device state */
unsigned int is_enabled : 1; /* pci_enable_device 已经被调用? */
unsigned int is_busmaster : 1; /* 设备是主设备? */
unsigned int no_msi : 1; /* 设备可不使用 msi? */
u32 saved_config_space[16]; /* 挂起事保存的配置空间 */
struct bin_attribute * rom_attr; /* sysfs ROM 入口的属性描述 */
int rom_attr_enabled;
struct bin_attribute * res_attr[DEVICE_COUNT_RESOURCE]; /*资源的sysfs 文件*/
};
在 Linux 系统中,所有的 PCI 设备都通过其 pci_dev 结构体中的 global_list 成员链接一条全局 PCI 设备链表pci_devices。另外,同属一条 PCI 总线上的所有 PCI 设备也通过其 pci_dev 结构体中的 bus_list 成员链接成一个属于这条 PCI 总线的总线设备链表,表头则由该 PCI 总线的 pci_bus 结构中的 devices 成员所定义。
PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用,这些代码用于配置 PCI 设备,比如中断号以及 I/O 或内存基地址
PCI 规范定义了 3 种类型的 PCI 配置空间头部,其中 type 0 用于标准的 PCI 设备,type 1 用于 PCI 桥,type 2 用于 PCI CardBus 桥:
/* PCI 头类型 */
#define PCI_HEADER_TYPE 0x0e /* 8 位头类型 */
#define PCI_HEADER_TYPE_NORMAL 0
#define PCI_HEADER_TYPE_BRIDGE 1
#define PCI_HEADER_TYPE_CARDBUS 2
pci_bus 结构体中的 pci_ops 类型成员指针 ops 指向该 PCI 总线所使用的配置空间访问操作的具体实现,pci_ops 结构体的定义:
struct pci_ops
{
int(*read) (struct pci_bus * bus, unsigned int devfn, int where, int size, u32 * val); //读配置空间
int(*write) (struct pci_bus * bus, unsigned int devfn, int where, int size, u32 val); //写配置空间
};
read()和 write()成员函数中的 size 表示访问的是字节、2字节还是4字节,对于write()而言,val 是要写入的值;对于 read()而言,val 是要返回的读取到的值的指针。通过 bus 参数的成员以及 devfn 可以定位相应 PCI 总线上相应 PCI 逻辑设备的配置空间。在 Linux 设备驱动中,可用如下一组函数来访问配置空间:
int pci_bus_read_config_byte (struct pci_bus *bus, unsigned int devfn, int where, u8 *val); //读字节
int pci_bus_read_config_word (struct pci_bus *bus, unsigned int devfn, int where, u16 *val); //读字
int pci_bus_read_config_dword (struct pci_bus *bus, unsigned int devfn, int where, u32 *val); //读双字
int pci_bus_write_config_byte (struct pci_bus *bus, unsigned int devfn, int where, u8 val); //写字节
int pci_bus_write_config_word (struct pci_bus *bus, unsigned int devfn, int where, u16 val); //写字
int pci_bus_write_config_dword (struct pci_bus *bus, unsigned int devfn, int where, u32 val); //写双字
从本质上讲 PCI 只是一种总线,具体的 PCI 设备可以是字符设备、网络设备、USB主机控制器等,因此,一个通过 PCI 总线与系统连接的设备的驱动至少包含以下两部分:
PCI 驱动只是为了辅助设备本身的驱动,它不是目的,只是手段,PCI 设备本身含有双重以上的身份。
在 Linux 内核中,用 pci_driver 结构体来定义 PCI 驱动,该结构体中包含了 PCI设备的探测/移除、挂起/恢复等函数,其定义如下:
struct pci_driver
{
struct list_head node;
char *name;
struct module * owner;
const struct pci_device_id * id_table; /*不能为 NULL,以便 probe 函数调用*/
/* 新设备添加 */
int(*probe) (struct pci_dev * dev, const struct pci_device_id * id);
void(*remove) (struct pci_dev * dev); /* 设备移出 */
int(*suspend) (struct pci_dev * dev, pm_message_t state); /* 设备挂起 */
int(*resume) (struct pci_dev * dev); /* 设备唤醒 */
/* 使能唤醒事件 */
int(*enable_wake) (struct pci_dev * dev, pci_power_t state, int enable);
void(*shutdown) (struct pci_dev * dev);
struct device_driver driver;
struct pci_dynids dynids;
};
对 pci_driver 的注册和注销通过如下函数来实现:
int pci_register_driver(struct pci_driver *driver); //注册
void pci_unregister_driver(struct pci_driver *driver); //销毁
pci_driver 的 probe()函数要完成 PCI 设备的初始化及其设备本身身份(字符、TTY、网络等)驱动的注册。当 Linux 内核启动并完成对所有 PCI 设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有 PCI 设备的拓扑结构,probe()函数将负责硬件的探测工作并保存配置信息。
在 PCI 设备驱动中,也需要定义一个 pci_device_id 结构体数组并导出到用户空间,使热插拔和模块装载系统知道驱动模块所针对的硬件设备。pci_device_id结构体的定义:
struct pci_device_id {
__u32 vendor, device; /* 厂商和设备 ID或 PCI_ANY_ID*/
__u32 subvendor, subdevice; /* 子系统 ID 或 PCI_ANY_ID */
__u32 class, class_mask; /* (类、子类、prog-if) 三元组 */
kernel_ulong_t driver_data; /* 驱动私有数据 */
};
pci_device_id 结构体数组使用宏 MODULE_DEVICE_TABLE 导出到用户空间:
static struct pci_device_id netdrv_pci_tbl[] = {
{0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
{0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, NETDRV_CB },
{0x1113, 0x1211, PCI_ANY_ID, PCI_ANY_ID, 0, 0, SMC1211TX },
{0,}
}
MODULE_DEVICE_TABLE (pci, netdrv_pci_tbl);
在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块,下面是一个典型的PCI设备驱动程序的基本框架:
/* 指明该驱动程序适用于哪一些 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,}
};
MODULE_DEVICE_TABLE(pci, xxx_pci_tbl);
module_init(xxx_init_module);
module_exit(xxx_cleanup_module);
/* 中断处理函数 */
static void xxx_interrupt(int irq, void * dev_id, struct pt_regs * regs)
{
/*PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。*/
}
/* 字符设备 file_operations open 成员函数 */
static int xxx_open(struct inode * inode, struct file * file)
{
/* 在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。*/
request_irq(xxx_irq, &xxx_interrupt, ...));
...
}
/* 字符设备 file_operations ioctl 成员函数 */
static int xxx_ioctl(struct inode * inode, struct file * file, unsigned int cmd, unsigned long arg)
{
...
}
/* 字符设备 file_operations read、write、mmap 等成员函数 */
/* 设备文件操作接口 ,PCI设备驱动程序可以通过xxx_fops 结构中的函数xxx_ioctl( ),向应用程序提供对硬件进行控制的接口。*/
static struct file_operations xxx_fops =
{
owner:THIS_MODULE, /* xxx_fops 所属的设备模块 */
read:xxx_read, /* 读设备操作*/
write:xxx_write, /* 写设备操作*/
ioctl:xxx_ioctl, /* 控制设备操作*/
mmap:xxx_mmap, /* 内存重映射操作*/
open:xxx_open, /* 打开设备操作*/
release:xxx_release /* 释放设备操作*/
};
/* pci_driver 的 probe 成员函数probe探测例程将负责完成对硬件的检测工作*/
static int _ _init xxx_probe(struct pci_dev * pci_dev, const struct pci_device_id * pci_id)
{
pci_enable_device(pci_dev); //启动 PCI 设备
/* 读取 PCI 配置信息 */
Iobase = pci_resource_start(pci_dev, 1);
... pci_set_master(pci_dev); //设置成总线主 DMA 模式
pci_request_regions(pci_dev); //申请 I/O 资源
/* 注册字符设备 */
cdev_init(xxx_cdev, &xxx_fops);
register_chrdev_region(xxx_dev_no, 1, ...);
cdev_add(xxx_cdev);
return 0;
}
/* pci_driver 的 remove 成员函数 */
static int _ _init xxx_release(struct pci_dev * pdev)
{
pci_release_regions(pdev); //释放 I/O 资源
pci_disable_device(pdev); //禁止 PCI 设备
unregister_chrdev_region(xxx_dev_no, 1); //释放占用的设备号
cdev_del(&xxx_dev.cdev); //注销字符设备
... return 0;
}
/* 设备模块信息 */
static struct pci_driver xxx_pci_driver =
{
name:xxx_MODULE_NAME, /* 设备模块名称 */
id_table:xxx_pci_tbl, /* 能够驱动的设备列表 */
probe:xxx_probe, /* 查找并初始化设备 */
remove:xxx_remove /* 卸载设备模块 */
};
在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:
static int _ _init xxx_init_module(void) //加载模块
{
if (!pci_present()) /*驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了*/
return -ENODEV;
if (!pci_register_driver(&demo_pci_driver)) { //注册pci_driver,自动调用xxx_probe方法
pci_unregister_driver(&demo_pci_driver);
return -ENODEV;
}
return 0;
}
static void _ _exit xxx_cleanup_module(void) //卸载模块
{
pci_unregister_driver(&xxx_pci_driver); //注销pci_driver
}
假设用树来表示PCI总线,那么树根就是主机/PCI桥,树叶就是具体的PCI设备,树叶与树枝通过pci_driver连接,而树叶本身的驱动,读写、控制树叶则需要通过其树叶设备本身所属类设备驱动来完成。
ISA总线在设计上相当陈旧而且其差劲的性能臭名昭著,但是在支持老主板而速度不是很重要的时候,ISA比PCI要更有优势。
一个ISA设备可配备有I/O端口,内存区域以及中断线:
对于编程, 内核中没有特别的帮助来易于存取 ISA 设备(像对 PCI 那样有). 你可使用的唯一工具是 I/O 端口和 IRQ 线的注册, 只能通过中断处理来实现, 驱动可探测 I/O 端口, 并且中断线必须被自动探测, 这要通过"自动探测 IRQ 号"技术来实现。
PCI 和 ISA 是在 PC 世界中最常用的外设接口, 但是它们不是唯一的. 这里简单介绍一下 PC 市场上的其他总线的特性:
微通道结构(MCA)是,用在 PS/2 计算机和一些笔记本电脑的IBM 标准. 在硬件层次上, 微通道比 ISA 有更多特性. 它支持多主 DMA, 32-位地址和数据线, 共享中断线, 和地理式寻址来存取每块板的配置寄存器. 这样的寄存器被称为可编程选项选择(POS), 但是它们没有 PCI 寄存器的全部特点. Linux 对 微通道的支持包括输出给模块的函数。
扩展 ISA (EISA) 总线是一个对 ISA 的 32-位 扩展, 带有一个兼容的接口连接器; ISA 设备板可被插入一个 EISA 连接器. 增加的线在 ISA 接触之下被连接.
另一个对 ISA 的扩展是 VESA Local Bus(VLB) 接口总线, 它扩展了 ISA 连接器, 通过添加第 3 个知道长度的槽位。一个设备可只插入这个额外的连接器(不用插入 2 个关联的 ISA 连接器), 因为 VLB 槽位从 ISA 连接器复制了所有的重要信号. 这样"独立"的 VLB 外设不使用 ISA 槽位是少见的, 因为大部分设备需要伸到后面板, 使它们的外部连接器是可用的。
VESA 总线比 EISA , MCA, 和 PCI 总线在它的能力方面更加限制, 并且正在从市场上消失. 没有特殊的内核支持位 VLB 而存在。