PCI
总线应用领域及其广泛并且令人惊奇。不同的
pci
设备有不同的需求以及不同的问题。因此,在
linux
内核中
pci
层支持就非常重要啦。本文档就是想为驱动程序设计开发人员解决
pci
处理中的各种问题。
0 . Pci 设备驱动程序的结构
现在有两种风格的的 pci 驱动程序结构:新风格的驱动(即让 pci 层来做大量设备探测
工作并且支持热插拔功能)和旧风格的驱动(即由驱动程序自己探测设备)。除非你有很好的理由,否则就不要用旧风格写驱动程序。当驱动程序找到所驱动的设备后,将执行以下步骤:
启用设备
访问设备配置空间
检测设备资源(如基地址和中断号)
分配设备资源
与设备通讯
下文将论述以上大部分步骤,其余部分请参考 <linux/pci.h>, 它有不错的注释。
如果没有配置 pci 子系统(即 CONFIG_PCI 没有置位),以下介绍的大部分函数都被定义为内联函数,它们要么是空的,要么返回对应的错误代码以避免在驱动中出现过多的条件宏 ifdefs 。
1. 新风格的驱动程序
新风格的驱动程序只是在初始化时调用 pci_register_driver ,调用时使用一个指向 struct pci_driver 的结构指针。该指针包含以下几个分量:
name 驱动程序名称
id_table 指向一个与驱动程序相关的设备 ID 表的指针。大多数驱动程序应当用 MODULE_DEVICE_TABLE(pci,…) 将该设备 ID 表导出。在调用 prob( ) 时设成 NULL 以让系统检测到所有的 pci 设备。
probe 指向设备检测函数 prob( ) 的指针。该函数将在 pci 设备 ID 与设备 ID 表匹配且还没有被其它驱动程序处理时(一般在对已存在的设备执行 pci_register_driver 或以后又有新设备插入时)被调用。调用时传入一个指向 struct pci_driver 结构的指针和与设备匹配的设备 ID 表做参数。若成功(驱动程序检测到 pci 设备)则返回 0 ,否则返回一个负的错误代码。这个函数总是在上下文之间调用的,因此可以进入睡眠状态的。
remove 指向一个设备卸载函数 remove( ) 的指针。该函数在 pci 设备被卸载时(如在注销设备驱动程序或者手动拔出该设备)被调用。同 probe 一样,该函数也是可以睡眠的。
save_state 在设备被暂停前所保存的设备状态。
suspend 将设备转入低功耗状态而暂停。
sesume 将一个暂停的设备(低功耗状态中)唤醒启动。
enable_wake 允许设备产生唤醒事件以从低功耗状态中恢复。
(请参考 Documentation/power/pci.txt 文件中关于 pci 电源管理以及相关函数的说明)
ID 表是一个 structpci_device_id 类型的数组,该数组以该类型中每一项都为 NULL 时结束。 structpci_device_id 中有以下几个分量:
vendor,device 设备对应的厂商 ID 号和设备 ID 号(或者是 PCI_ANY_ID )
subvendor,subdevice 设备对应的子厂商 ID 号和子设备 ID 号(或者是 PCI_ANY_ID )
class,class_mask 设备对应的类,类掩码表示在比较中采用设备类( class )中的那些位。
driver_data 设备的私有数据。
当一个设备驱动程序存在,只需要调用 pci_unregister_driver 就可由 pci 层调用 remove( )
函数自动将所驱动的所有设备卸载掉。
请将初始和清理函数前加上以下适当的宏(该宏定义在 <linux/init.h> 中):
__init 初始化标记,置于驱动程序中初始化函数前。
__exit 结束标记,在非模块化的驱动程序中将被忽略。
__devinit 设备初始化标记。若编译内核时选择了 CONFIG_HOTPLUG 选项,则等同于 __init 。
__devexit 同 __exit 一样。
一般技巧:
在 module_init()/module_exit( ) 函数前应标上 __init/__exit 标记;
结构体 structpci_driver 不要标上以上任何标记;
ID 表数组应当标上 __devinitdata 标记;
probe( ) 和 remove( ) 函数应当标上 __devinit/exit 标记;
如果你确信不是一个支持热插拔的驱动程序,就可只用 __init/exit __initdata/exitdata ;
一个指向标记了 __devexit 的函数的指针,一定要由 __devexit_p(function_name) 来产生。
它会产生对应的函数名,若没有标记了 __devexit 的函数,则产生 NULL 。
2. 旧风格的 pci 设备驱动程序
旧风格的 pci 设备驱动程序不会用到 pci_register_driver() 去探测设备,而需要手动(由驱动程序)使用以下结构去探测设备:
根据厂商 ID 和设备 ID 探测设备:
struct pci_dev *dev =NULL;
while(dev = pci_find_device(VENDOR_ID,DEVICE_ID,dev))
configure_device(dev);
根据类 ID 探测设备:
pci_find_class(CLASS_ID,dev);
根据厂商 ID 和设备 ID 以及子系统厂商 ID 和设备 ID 探测设备:
pci_find_subsys(VENDOR_ID,DEVICE_ID,SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev) ;
你可以使用常量 PCI_ANY_ID 作为通配符取代 VENDOR_ID 、 DEVICE_ID ,这样你就
可以搜索都所有的设备。如果你要以更复杂的条件探测设备,那就得自己遍历所有已知的
pci 设备:
struct pci_dev *dev;
pci_for_each_dev(dev) {
... do anything you want with dev ...
}
为了向上兼容,也可以用 pci_for_each_dev_reverse(dev) 去遍历设备表。
3. 启用设备
在你对找到的设备进行任何操作之前,你需要调用 pci_enable_device( ) 来启用设备的 I/O
和内存资源,分配不足的资源,如果需要,还要唤醒一个处于暂停状态的设备。需要注意的是,这个操作可能会失败。
如果你想设定设备工作在总线主设备模式,调用 pci_set_master() 会把 PCI_COMMAND
寄存器中的总线主设备允许位置 1 ,并且还会修改延迟计数器中的值。
若你想使用 pci 内存写无效事务,调用 pci_set_swi() ,它会把 PCI_COMMAND 寄存
器中的 Mem_Wr_Inval 位置 1 ,还要确认缓存行长度寄存器( CACHE_LINE_REGISTER )的设置是否正确。要注意检查 pci_set_swi() 的返回值,并不是所有设备都支持 pci 内存写无效的。
4. 如何访问 pci 设备配置空间
必须使用 pci_(read|write)_config_(byte|word|dword) 去访问 pci 设备(由 struct pci_dev *
表示)的配置空间。所有以上的函数在成功时返回 0 ,失败时返回错误代码。大多数驱动程序期望在访问有效的 pci 设备时是不会失败的。
若你要访问的是 pci 配置头标准空间,就可以用符号常量来表示寄存器地址以及位设置,这些符号常量定义在 <linux/pci.h> 中。
若要访问 pci 扩展空间能力寄存器,只需要对每个特定的能力调用 pci_find_capability() 就可以找到相应的寄存器块。
5. 寻址和中断
内存和端口基地址以及中断号不要从 pci 配置空间中读出而要使用 pci_dev 结构体中相
应的值,因为它们可能被映射到内核中。关于如何访问设备内存,请参阅 Documentation/IO-mapping.txt 文件。还需要调用 request_region() 函数和 request_mem_region() 分别申请 IO 地址和设备内存地址范围以确保没有其它驱动使用该设备。
所有的中断处理程序要使用共享中断号,还可以使用参数 devid 将中断号 IRQs 映射为设备数据结构。
6. 其它有用的函数
pci_find_slot() 根据设备所在的总线号和插槽号,探测相应的 pci 设备
pci_set_power_state() 设置 pci 电源管理状态
pci_find_capability() 在设备的能力表中找出指定的能力
pci_module_init() 内联函数,确保驱动程序正确的初始化以及错误处理
pci_resource_start() 返回 pci 地址范围的总线起始地址
pci_resource_end() 返回 pci 地址范围的总线终止地址
pci_resource_len() 返回 pci 地址范围的长度,以字节为单位
pci_set_drvdata() 为 pci_dev 设置私有数据指针
pci_get_drvdata() 从 pci_dev 结构中得到其私有数据指针
pci_set_mwi() 启用设备内存写无效事务
pci_clear_mwi() 禁用设备内存写无效事务
7. 其它提示
当向用户显示 pci 插槽时(如驱动程序想告诉用户它所找到的设备卡),可用
pci_dev->slot_name 。
总是使用一个指向 structpci_dev 的指针来引用某个 pci 设备。所有的 pci 层接口函数都使用该指针。不要使用总线 / 插槽号 / 功能号,它们有特别的用途 —— 有多个主总线的系统说起来是很复杂的。
8. 已弃用的函数
以下的接口函数只是为了向上兼容而暂时保留,请不要在你的新式驱动程序中使用它们:
pcibios_present() 以前在使用,现在你不必测试 pci 子系统是否存在。若不存在 pci 子系统, pci 设备表是空的,并且所有的探测设备函数都将返回 NULL 。
pcibios_(read|write)_* 现已被相应的 pci_(read|write)_* 取代
pcibios_find_* 现已被相应的 pci_find_* 取代
0 . Pci 设备驱动程序的结构
现在有两种风格的的 pci 驱动程序结构:新风格的驱动(即让 pci 层来做大量设备探测
工作并且支持热插拔功能)和旧风格的驱动(即由驱动程序自己探测设备)。除非你有很好的理由,否则就不要用旧风格写驱动程序。当驱动程序找到所驱动的设备后,将执行以下步骤:
启用设备
访问设备配置空间
检测设备资源(如基地址和中断号)
分配设备资源
与设备通讯
下文将论述以上大部分步骤,其余部分请参考 <linux/pci.h>, 它有不错的注释。
如果没有配置 pci 子系统(即 CONFIG_PCI 没有置位),以下介绍的大部分函数都被定义为内联函数,它们要么是空的,要么返回对应的错误代码以避免在驱动中出现过多的条件宏 ifdefs 。
1. 新风格的驱动程序
新风格的驱动程序只是在初始化时调用 pci_register_driver ,调用时使用一个指向 struct pci_driver 的结构指针。该指针包含以下几个分量:
name 驱动程序名称
id_table 指向一个与驱动程序相关的设备 ID 表的指针。大多数驱动程序应当用 MODULE_DEVICE_TABLE(pci,…) 将该设备 ID 表导出。在调用 prob( ) 时设成 NULL 以让系统检测到所有的 pci 设备。
probe 指向设备检测函数 prob( ) 的指针。该函数将在 pci 设备 ID 与设备 ID 表匹配且还没有被其它驱动程序处理时(一般在对已存在的设备执行 pci_register_driver 或以后又有新设备插入时)被调用。调用时传入一个指向 struct pci_driver 结构的指针和与设备匹配的设备 ID 表做参数。若成功(驱动程序检测到 pci 设备)则返回 0 ,否则返回一个负的错误代码。这个函数总是在上下文之间调用的,因此可以进入睡眠状态的。
remove 指向一个设备卸载函数 remove( ) 的指针。该函数在 pci 设备被卸载时(如在注销设备驱动程序或者手动拔出该设备)被调用。同 probe 一样,该函数也是可以睡眠的。
save_state 在设备被暂停前所保存的设备状态。
suspend 将设备转入低功耗状态而暂停。
sesume 将一个暂停的设备(低功耗状态中)唤醒启动。
enable_wake 允许设备产生唤醒事件以从低功耗状态中恢复。
(请参考 Documentation/power/pci.txt 文件中关于 pci 电源管理以及相关函数的说明)
ID 表是一个 structpci_device_id 类型的数组,该数组以该类型中每一项都为 NULL 时结束。 structpci_device_id 中有以下几个分量:
vendor,device 设备对应的厂商 ID 号和设备 ID 号(或者是 PCI_ANY_ID )
subvendor,subdevice 设备对应的子厂商 ID 号和子设备 ID 号(或者是 PCI_ANY_ID )
class,class_mask 设备对应的类,类掩码表示在比较中采用设备类( class )中的那些位。
driver_data 设备的私有数据。
当一个设备驱动程序存在,只需要调用 pci_unregister_driver 就可由 pci 层调用 remove( )
函数自动将所驱动的所有设备卸载掉。
请将初始和清理函数前加上以下适当的宏(该宏定义在 <linux/init.h> 中):
__init 初始化标记,置于驱动程序中初始化函数前。
__exit 结束标记,在非模块化的驱动程序中将被忽略。
__devinit 设备初始化标记。若编译内核时选择了 CONFIG_HOTPLUG 选项,则等同于 __init 。
__devexit 同 __exit 一样。
一般技巧:
在 module_init()/module_exit( ) 函数前应标上 __init/__exit 标记;
结构体 structpci_driver 不要标上以上任何标记;
ID 表数组应当标上 __devinitdata 标记;
probe( ) 和 remove( ) 函数应当标上 __devinit/exit 标记;
如果你确信不是一个支持热插拔的驱动程序,就可只用 __init/exit __initdata/exitdata ;
一个指向标记了 __devexit 的函数的指针,一定要由 __devexit_p(function_name) 来产生。
它会产生对应的函数名,若没有标记了 __devexit 的函数,则产生 NULL 。
2. 旧风格的 pci 设备驱动程序
旧风格的 pci 设备驱动程序不会用到 pci_register_driver() 去探测设备,而需要手动(由驱动程序)使用以下结构去探测设备:
根据厂商 ID 和设备 ID 探测设备:
struct pci_dev *dev =NULL;
while(dev = pci_find_device(VENDOR_ID,DEVICE_ID,dev))
configure_device(dev);
根据类 ID 探测设备:
pci_find_class(CLASS_ID,dev);
根据厂商 ID 和设备 ID 以及子系统厂商 ID 和设备 ID 探测设备:
pci_find_subsys(VENDOR_ID,DEVICE_ID,SUBSYS_VENDOR_ID, SUBSYS_DEVICE_ID, dev) ;
你可以使用常量 PCI_ANY_ID 作为通配符取代 VENDOR_ID 、 DEVICE_ID ,这样你就
可以搜索都所有的设备。如果你要以更复杂的条件探测设备,那就得自己遍历所有已知的
pci 设备:
struct pci_dev *dev;
pci_for_each_dev(dev) {
... do anything you want with dev ...
}
为了向上兼容,也可以用 pci_for_each_dev_reverse(dev) 去遍历设备表。
3. 启用设备
在你对找到的设备进行任何操作之前,你需要调用 pci_enable_device( ) 来启用设备的 I/O
和内存资源,分配不足的资源,如果需要,还要唤醒一个处于暂停状态的设备。需要注意的是,这个操作可能会失败。
如果你想设定设备工作在总线主设备模式,调用 pci_set_master() 会把 PCI_COMMAND
寄存器中的总线主设备允许位置 1 ,并且还会修改延迟计数器中的值。
若你想使用 pci 内存写无效事务,调用 pci_set_swi() ,它会把 PCI_COMMAND 寄存
器中的 Mem_Wr_Inval 位置 1 ,还要确认缓存行长度寄存器( CACHE_LINE_REGISTER )的设置是否正确。要注意检查 pci_set_swi() 的返回值,并不是所有设备都支持 pci 内存写无效的。
4. 如何访问 pci 设备配置空间
必须使用 pci_(read|write)_config_(byte|word|dword) 去访问 pci 设备(由 struct pci_dev *
表示)的配置空间。所有以上的函数在成功时返回 0 ,失败时返回错误代码。大多数驱动程序期望在访问有效的 pci 设备时是不会失败的。
若你要访问的是 pci 配置头标准空间,就可以用符号常量来表示寄存器地址以及位设置,这些符号常量定义在 <linux/pci.h> 中。
若要访问 pci 扩展空间能力寄存器,只需要对每个特定的能力调用 pci_find_capability() 就可以找到相应的寄存器块。
5. 寻址和中断
内存和端口基地址以及中断号不要从 pci 配置空间中读出而要使用 pci_dev 结构体中相
应的值,因为它们可能被映射到内核中。关于如何访问设备内存,请参阅 Documentation/IO-mapping.txt 文件。还需要调用 request_region() 函数和 request_mem_region() 分别申请 IO 地址和设备内存地址范围以确保没有其它驱动使用该设备。
所有的中断处理程序要使用共享中断号,还可以使用参数 devid 将中断号 IRQs 映射为设备数据结构。
6. 其它有用的函数
pci_find_slot() 根据设备所在的总线号和插槽号,探测相应的 pci 设备
pci_set_power_state() 设置 pci 电源管理状态
pci_find_capability() 在设备的能力表中找出指定的能力
pci_module_init() 内联函数,确保驱动程序正确的初始化以及错误处理
pci_resource_start() 返回 pci 地址范围的总线起始地址
pci_resource_end() 返回 pci 地址范围的总线终止地址
pci_resource_len() 返回 pci 地址范围的长度,以字节为单位
pci_set_drvdata() 为 pci_dev 设置私有数据指针
pci_get_drvdata() 从 pci_dev 结构中得到其私有数据指针
pci_set_mwi() 启用设备内存写无效事务
pci_clear_mwi() 禁用设备内存写无效事务
7. 其它提示
当向用户显示 pci 插槽时(如驱动程序想告诉用户它所找到的设备卡),可用
pci_dev->slot_name 。
总是使用一个指向 structpci_dev 的指针来引用某个 pci 设备。所有的 pci 层接口函数都使用该指针。不要使用总线 / 插槽号 / 功能号,它们有特别的用途 —— 有多个主总线的系统说起来是很复杂的。
8. 已弃用的函数
以下的接口函数只是为了向上兼容而暂时保留,请不要在你的新式驱动程序中使用它们:
pcibios_present() 以前在使用,现在你不必测试 pci 子系统是否存在。若不存在 pci 子系统, pci 设备表是空的,并且所有的探测设备函数都将返回 NULL 。
pcibios_(read|write)_* 现已被相应的 pci_(read|write)_* 取代
pcibios_find_* 现已被相应的 pci_find_* 取代