如何让linux重新枚举pci设备

如何让linux重新枚举pci设备,第1张

在Linux下,lspci可以枚举所有PCI设备。它是通过读取PCI配置空间(PCI Configuration Space)信息来实现PCI设备的枚举的。这里,我通过两种方式来简单的模拟一下lspci的功能。一种是通过PCI总线的CF8和CFC端口来枚举(参考PCI总线规范);另一种是利用proc filesystem。

方法一:这种方法需要对端口进行 *** 作,在Linux下,普通应用程序没有权限读写I/O 端口,需要通过iopl或ioperm来提升权限,我的代码里面使用iopl。

[cpp] view plaincopyprint?

/*

* Enum all pci device via the PCI config register(CF8 and CFC).

*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/io.h>

#define PCI_MAX_BUS 255 /* 8 bits (0 ~ 255) */

#define PCI_MAX_DEV 31 /* 5 bits (0 ~ 31) */

#define PCI_MAX_FUN 7 /* 3 bits (0 ~ 7) */

#define CONFIG_ADDRESS 0xCF8

#define CONFIG_DATA 0xCFC

#define PCICFG_REG_VID 0x00 /* Vendor id, 2 bytes */

#define PCICFG_REG_DID 0x02 /* Device id, 2 bytes */

#define PCICFG_REG_CMD 0x04 /* Command register, 2 bytes */

#define PCICFG_REG_STAT 0x06 /* Status register, 2 bytes */

#define PCICFG_REG_RID 0x08 /* Revision id, 1 byte */

void list_pci_devices()

{

unsigned int bus, dev, fun

unsigned int addr, data

//printf("BB:DD:FF VID:DID\n")

for (bus = 0bus <= PCI_MAX_BUSbus++) {

for (dev = 0dev <= PCI_MAX_DEVdev++) {

for (fun = 0fun <= PCI_MAX_FUNfun++) {

addr = 0x80000000L | (bus<<16) | (dev<<11) | (fun<<8)

outl(addr, CONFIG_ADDRESS)

data = inl(CONFIG_DATA)

/* Identify vendor ID */

if ((data != 0xFFFFFFFF) &&(data != 0)) {

printf("%02X:%02X:%02X ", bus, dev, fun)

printf("%04X:%04X", data&0xFFFF, data>>16)

addr = 0x80000000L | (bus<<16) | (dev<<11) | (fun<<8) | PCICFG_REG_RID

outl(addr, CONFIG_ADDRESS)

data = inl(CONFIG_DATA)

if (data&0xFF) {

printf(" (rev %02X)\n", data&0xFF)

} else {

printf("\n")

}

}

} end func

} // end device

} // end bus

}

int main()

{

int ret

/* Enable r/w permission of all 65536 ports */

ret = iopl(3)

if (ret <0) {

perror("iopl set error")

return 1

}

list_pci_devices()

/* Disable r/w permission of all 65536 ports */

ret = iopl(0)

if (ret <0) {

perror("iopl set error")

return 1

}

return 0

}

方法二:这种方法需不需要对端口进行 *** 作,而是利用Linux procfs来实现对PCI 配置空间的访问。

[cpp] view plaincopyprint?

/*

* Enum all pci device via /proc/bus/pci/.

*/

#include <stdio.h>

#include <stdint.h>

#include <stdlib.h>

#include <string.h>

#include <fcntl.h>

#include <unistd.h>

#define PCI_MAX_BUS 255 /* 8 bits (0 ~ 255) */

#define PCI_MAX_DEV 31 /* 5 bits (0 ~ 31) */

#define PCI_MAX_FUN 7 /* 3 bits (0 ~ 7) */

/*

* PCI Configuration Header offsets

*/

#define PCICFG_REG_VID 0x00 /* Vendor id, 2 bytes */

#define PCICFG_REG_DID 0x02 /* Device id, 2 bytes */

#define PCICFG_REG_CMD 0x04 /* Command register, 2 bytes */

#define PCICFG_REG_STAT 0x06 /* Status register, 2 bytes */

#define PCICFG_REG_RID 0x08 /* Revision id, 1 byte */

#define PCICFG_REG_PROG_INTF 0x09 /* Programming interface code, 1 byte */

#define PCICFG_REG_SUBCLASS 0x0A /* Sub-class code, 1 byte */

#define PCICFG_REG_BASCLASS 0x0B /* Base class code, 1 byte */

#define PCICFG_REG_CACHE_LINESZ 0x0C /* Cache line size, 1 byte */

#define PCICFG_REG_LATENCY_TIMER 0x0D /* Latency timer, 1 byte */

#define PCICFG_REG_HEADER_TYPE 0x0E /* Header type, 1 byte */

#define PCICFG_REG_BIST 0x0F /* Builtin self test, 1 byte */

#define PCICFG_REG_BAR0 0x10 /* Base addr register 0, 4 bytes */

#define PCICFG_REG_BAR1 0x14 /* Base addr register 1, 4 bytes */

#define PCICFG_REG_BAR2 0x18 /* Base addr register 2, 4 bytes */

#define PCICFG_REG_BAR3 0x1C /* Base addr register 3, 4 bytes */

#define PCICFG_REG_BAR4 0x20 /* Base addr register 4, 4 bytes */

#define PCICFG_REG_BAR5 0x24 /* Base addr register 5, 4 bytes */

#define PCICFG_REG_CIS 0x28 /* Cardbus CIS Pointer */

#define PCICFG_REG_SVID 0x2C /* Subsystem Vendor ID, 2 bytes */

#define PCICFG_REG_SDID 0x2E /* Subsystem ID, 2 bytes */

#define PCICFG_REG_ROMBAR 0x30 /* ROM base register, 4 bytes */

#define PCICFG_REG_CAPPTR 0x34 /* Capabilities pointer, 1 byte */

#define PCICFG_REG_INT_LINE 0x3C /* Interrupt line, 1 byte */

#define PCICFG_REG_INT_PIN 0x3D /* Interrupt pin, 1 byte */

#define PCICFG_REG_MIN_GNT 0x3E /* Minimum grant, 1 byte */

#define PCICFG_REG_MAX_LAT 0x3F /* Maximum lat, 1 byte */

void list_pci_devices()

{

unsigned int bus, dev, fun

//printf("BB:DD:FF VID:DID(RID)\n")

for (bus = 0bus <= PCI_MAX_BUSbus++) {

for (dev = 0dev <= PCI_MAX_DEVdev++) {

for (fun = 0fun <= PCI_MAX_FUNfun++) {

char proc_name[64]

int cfg_handle

uint32_t data

uint16_t vid, did

uint8_t rid

snprintf(proc_name, sizeof(proc_name),

"/proc/bus/pci/%02x/%02x.%x", bus, dev, fun)

cfg_handle = open(proc_name, O_RDWR)

if (cfg_handle <= 0)

continue

lseek(cfg_handle, PCICFG_REG_VID, SEEK_SET)

read(cfg_handle, &data, sizeof(data))

/* Identify vendor ID */

if ((data != 0xFFFFFFFF) &&(data != 0)) {

lseek(cfg_handle, PCICFG_REG_RID, SEEK_SET)

read(cfg_handle, &rid, sizeof(rid))

vid = data&0xFFFF

did = data>>16

printf("%02X:%02X:%02X", bus, dev, fun)

if (rid >0) {

printf(" %04X:%04X (rev %02X)\n", vid, did, rid)

} else {

printf(" %04X:%04X\n", vid, did)

}

}

} // end func

} // end device

} // end bus

}

int main(int argc, char **argv)

{

list_pci_devices()

return 0

}

这两种方法各有优缺点,第一种方法方便移植到其他OS,第二种就只适用于Linux。但是,第一种方法需要对I/O port进行直接 *** 作。第二种就不需要。

注意:执行这两段代码时,需要超级用户(root) 权限。

补充:今天在枚举 Westmere-EP Processor(Intel Xeon Processor 5500 Series(Nehalem-EP))的 IMC(Integrated Memory Controller)时发现一个问题。lspci无法枚举到IMC设备。Westmere-EP 是 Intel 新的处理器架构。和以往的CPU不一样,它把Memory Controller集成到了CPU里面。IMC控制器被映射到了PCI总线上,Bus Number 是0xFE~0xFF,procfs(/proc/bus/pci/)下没有这几个设备。但是,通过 CF8/CFC 端口可以枚举到这些设备。

3. 这段代码是在驱动中可以用来查找特定的pci device,并且返回一个pci_dev的结构体变量。通过这样一个struct变量,内核提供的接口函数可以直接套用,如pci_read_config_word(),pci_write_config_word()等。

[cpp] view plaincopyprint?

void list_pci_device()

{

struct pci_dev *dev

struct pci_bus *bus,*childbus

list_for_each_entry(bus, &pci_root_buses, node) { //globle pci_root_buses in pci.h

list_for_each_entry(dev, &bus->devices, bus_list) { // for bus 0

printk("%02X:%02X:%02X %04X:%04X\n",dev->bus->number,dev->devfn >>3, dev->devfn &0x07,dev->vendor,dev->device)

}

list_for_each_entry(childbus, &bus->children,node) { // for bus 1,2,3,...

list_for_each_entry(dev, &childbus->devices, bus_list) {

printk("%02X:%02X:%02X %04X:%04X\n",dev->bus->number,dev->devfn >>3, dev->devfn &0x07,dev->vendor,dev->device)

}

}

}

LINUX设备驱动程序是怎么样和硬件通信的?下面将由我带大家来解答这个疑问吧,希望对大家有所收获!

   LINUX设备驱动程序与硬件设备之间的通信

设备驱动程序是软件概念和硬件电路之间的一个抽象层,因此两方面都要讨论。到目前为止,我们已经讨论详细讨论了软件概念上的一些细节,现在讨论另一方面,介绍驱动程序在Linux上如何在保持可移植性的前提下访问I/O端口和I/O内存

我们在需要示例的场合会使用简单的数字I/O端口来讲解I/O指令,并使用普通的帧缓冲区显存来讲解内存映射I/O。

I/O端口和I/O内存

计算机对每种外设都是通过读写它的寄存器进行控制的。大部分外设都有几个寄存器,不管是在内存地址空间还是在I/O地址空间,这些寄存器的访问地址都是连续的。

I/O端口就是I/O端口,设备会把寄存器映射到I/O端口,不管处理器是否具有独立的I/O端口地址空间。即使没有在访问外设时也要模拟成读写I/O端口。

I/O内存是设备把寄存器映射到某个内存地址区段(如PCI设备)。这种I/O内存通常是首先方案,它不需要特殊的处理器指令,而且CPU核心访问内存更有效率。

I/O寄存器和常规内存

尽管硬件寄存器和内存非常相似,但程序员在访问I/O寄存器的时候必须注意避免由于CPU或编译器不恰当的优化而改变预期的I/O动作。

I/O寄存器和RAM最主要的区别就是I/O *** 作具有边际效应,而内存 *** 作则没有:由于内存没有边际效应,所以可以用多种 方法 进行优化,如使用高速缓存保存数值、重新排序读/写指令等。

编译器能够将数值缓存在CPU寄存器中而不写入内存,即使储存数据,读写 *** 作也都能在高速缓存中进行而不用访问物理RAM。无论是在编译器一级或是硬件一级,指令的重新排序都有可能发生:一个指令序列如果以不同于程序文本中的次序运行常常能执行得更快。

在对常规内存进行这些优化的时候,优化过程是透明的,而且效果良好,但是对I/O *** 作来说这些优化很可能造成致命的错误,这是因为受到边际效应的干扰,而这却是驱动程序访问I/O寄存器的主要目的。处理器无法预料某些 其它 进程(在另一个处理器上运行,或在在某个I/O控制器中发生的 *** 作)是否会依赖于内存访问的顺序。编译器或CPU可能会自作聪明地重新排序所要求的 *** 作,结果会发生奇怪的错误,并且很难调度。因此,驱动程序必须确保不使用高速缓冲,并且在访问寄存器时不发生读或写指令的重新排序。

由硬件自身引起的问题很解决:只要把底层硬件配置成(可以是自动的或是由Linux初始化代码完成)在访问I/O区域(不管是内存还是端口)时禁止硬件缓存即可。

由编译器优化和硬件重新排序引起的问题的解决办法是:对硬件(或其他处理器)必须以特定顺序的 *** 作之间设置内存屏障(memory barrier)。Linux提供了4个宏来解决所有可能的排序问题:

#include <linux/kernel.h>

void barrier(void)

这个函数通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当前CPU寄存器中的所有修改过的数值保存到内存中,需要这些数据的时候再重新读出来。对barrier的调用可避免在屏障前后的编译器优化,但硬件完成自己的重新排序。

#include <asm/system.h>

void rmb(void)

void read_barrier_depends(void)

void wmb(void)

void mb(void)

这些函数在已编译的指令流中插入硬件内存屏障具体实现方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读 *** 作一定会在后来的读 *** 作之前完成。wmb保证写 *** 作不会乱序,mb指令保证了两者都不会。这些函数都是barrier的超集。

void smp_rmb(void)

void smp_read_barrier_depends(void)

void smp_wmb(void)

void smp_mb(void)

上述屏障宏版本也插入硬件屏障,但仅仅在内核针对SMP系统编译时有效在单处理器系统上,它们均会被扩展为上面那些简单的屏障调用。

设备驱动程序中使用内存屏障的典型形式如下:

writel(dev->registers.addr, io_destination_address)

writel(dev->registers.size, io_size)

writel(dev->registers.operation, DEV_READ)

wmb()

writel(dev->registers.control, DEV_GO)

在这个例子中,最重要的是要确保控制某种特定 *** 作的所有设备寄存器一定要在 *** 作开始之前已被正确设置。其中的内存屏障会强制写 *** 作以要求的顺序完成。

因为内存屏障会影响系统性能,所以应该只用于真正需要的地方。不同类型的内存屏障对性能的影响也不尽相同,所以最好尽可能使用最符合需要的特定类型。

值得注意的是,大多数处理同步的内核原语,如自旋锁和atomic_t *** 作,也能作为内存屏障使用。同时还需要注意,某些外设总线(比如PCI总线)存在自身的高速缓存问题,我们将在后面的章节中讨论相关问题。

在某些体系架构上,允许把赋值语句和内存屏障进行合并以提高效率。内核提供了几个执行这种合并的宏,在默认情况下,这些宏的定义如下:

#define set_mb(var, value) do {var = valuemb()} while 0

#define set_wmb(var, value) do {var = valuewmb()} while 0

#define set_rmb(var, value) do {var = valuermb()} while 0

在适当的地方,<asm/system.h>中定义的这些宏可以利用体系架构特有的指令更快的完成任务。注意只有小部分体系架构定义了set_rmb宏。

使用I/O端口

I/O端口是驱动程序与许多设备之间的通信方式——至少在部分时间是这样。本节讲解了使用I/O端口的不同函数,另外也涉及到一些可移植性问题。

I/O端口分配

下面我们提供了一个注册的接口,它允允许驱动程序声明自己需要 *** 作的端口:

#include <linux/ioport.h>

struct resource *request_region(unsigned long first, unsigned long n, const char *name)

它告诉内核,我们要使用起始于first的n个端口。name是设备的名称。如果分配成功返回非NULL,如果失败返回NULL。

所有分配的端口可从/proc/ioports中找到。如果我们无法分配到我们要的端口集合,则可以查看这个文件哪个驱动程序已经分配了这些端口。

如果不再使用这些端口,则用下面函数返回这些端口给系统:

void release_region(unsigned long start, unsigned long n)

下面函数允许驱动程序检查给定的I/O端口是否可用:

int check_region(unsigned long first, unsigned long n)//不可用返回负的错误代码

我们不赞成用这个函数,因为它返回成功并不能确保分配能够成功,因为检查和其后的分配并不是原子 *** 作。我们应该始终使用request_region,因为这个函数执行了必要的锁定,以确保分配过程以安全原子的方式完成。

*** 作I/O端口

当驱动程序请求了需要使用的I/O端口范围后,必须读取和/或写入这些端口。为此,大多数硬件都会把8位、16位、32位区分开来。它们不能像访问系统内存那样混淆使用。

因此,C语言程序必须调用不同的函数访问大小不同的端口。那些只支持映射的I/O寄存器的计算机体系架构通过把I/O端口地址重新映射到内存地址来伪装端口I/O,并且为了易于移植,内核对驱动程序隐藏了这些细节。Linux内核头文件中(在与体系架构相关的头文件<asm/io.h>中)定义了如下一些访问I/O端口的内联函数:

unsigned inb(unsigned port)

void outb(unsigned char byte, unsigned port)

字节读写端口。

unsigned inw(unsigned port)

void outw(unsigned short word, unsigned port)

访问16位端口

unsigned inl(unsigned port)

void outl(unsigned longword, unsigned port)

访问32位端口

在用户空间访问I/O端口

上面这些函数主要是提供给设备驱动程序使用的,但它们也可以用户空间使用,至少在PC类计算机上可以使用。GNU的C库在<sys/io.h>中定义了这些函数。如果要要用户空间使用inb及相关函数,则必须满足正下面这些条件:

编译程序时必须带有-O选项来强制内联函数的展开。

必须用ioperm(获取单个端口的权限)或iopl(获取整个I/O空间)系统调用来获取对端口进行I/O *** 作的权限。这两个函数都是x86平台特有的。

必须以root身份运行该程序才能调用ioperm或iopl。或者进程的祖先进程之一已经以root身份获取对端口的访问。

如果宿主平台没有以上两个系统调用,则用户空间程序仍然可以使用/dev/port设备文件访问I/O端口。不过要注意,该设备文件的含义与平台密切相关,并且除PC平台以处,它几乎没有什么用处。

串 *** 作

以上的I/O *** 作都是一次传输一个数据,作为补充,有些处理器实现了一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字、双字。这些指令称为串 *** 作指令,它们执行这些任务时比一个C语言编写的循环语句快得多。下面列出的宏实现了串I/O:

void insb(unsigned port, void *addr, unsigned long count)

void outsb(unsigned port, void *addr, unsigned long count)从内存addr开始连续读/写count数目的字节。只对单一端口port读取或写入数据

void insw(unsigned port, void *addr, unsigned long count)

void outsw(unsigned port, void *addr, unsigned long count)对一个16位端口读写16位数据

void insl(unsigned port, void *addr, unsigned long count)

void outsl(unsigned port, void *addr, unsigned long count)对一个32位端口读写32位数据

在使用串I/O *** 作函数时,需要铭记的是:它们直接将字节流从端口中读取或写入。因此,当端口和主机系统具有不同的字节序时,将导致不可预期的结果。使用inw读取端口将在必要时交换字节,以便确保读入的值匹配于主机的字节序。然而,串函数不会完成这种交换。

暂停式I/O

在处理器试图从总线上快速传输数据时,某些平台(特别是i386)就会出现问题。当处理器时钟比外设时钟(如ISA)快时就会出现问题,并且在设备板上特别慢时表现出来。为了防止出现丢失数据的情况,可以使用暂停式的I/O函数来取代通常的I/O函数,这些暂停式的I/O函数很像前面介绍的那些I/O函数,不同之处是它们的名字用_p结尾,如inb_p、outb_p等等。在linux支持的大多数平台上都定义了这些函数,不过它们常常扩展为非暂停式I/O同样的代码,因为如果不使用过时的外设总线就不需要额外的暂停。

平台相关性

I/O指令是与处理器密切相关的。因为它们的工作涉及到处理器移入移出数据的细节,所以隐藏平台间的差异非常困难。因此,大部分与I/O端口相关的源代码都与平台相关。

回顾前面函数列表可以看到有一处不兼容的地方,即数据类型。函数的参数根据各平台体系架构上的不同要相应地使用不同的数据类型。例如,port参数在x86平台上(处理器只支持64KB的I/O空间)上定义为unsigned short,但在其他平台上定义为unsigned long,在这些平台上,端口是与内存在同一地址空间内的一些特定区域。

感兴趣的读者可以从io.h文件获得更多信息,除了本章介绍的函数,一些与体系架构相关的函数有时也由该文件定义。

值得注意的是,x86家族之外的处理器都不为端口提供独立的地址空间。

I/O *** 作在各个平台上执行的细节在对应平台的编程手册中有详细的叙述也可以从web上下载这些手册的PDF文件。

I/O端口示例

演示设备驱动程序的端口I/O的示例代码运行于通用的数字I/O端口上,这种端口在大多数计算机平台上都能找到。

数字I/O端口最常见的一种形式是一个字节宽度的I/O区域,它或者映射到内存,或者映射到端口。当把数字写入到输出区域时,输出引脚上的电平信号随着写入的各位而发生相应变化。从输入区域读取到的数据则是输入引脚各位当前的逻辑电平值。

这类I/O端口的具体实现和软件接口是因系统而异的。大多数情况下,I/O引脚由两个I/O区域控制的:一个区域中可以选择用于输入和输出的引脚,另一个区域中可以读写实际的逻辑电平。不过有时情况简单些,每个位不是输入就是输出(不过这种情况下就不能称为“通用I/O"了)在所有个人计算机上都能找到的并口就是这样的非通用的I/O端口。

并口简介

并口的最小配置由3个8位端口组成。第一个端口是一个双向的数据寄存器,它直接连接到物理连接器的2~9号引脚上。第二个端口是一个只读的状态寄存器当并口连接打印机时,该寄存器 报告 打印机状态,如是否是线、缺纸、正忙等等。第三个端口是一个只用于输出的控制寄存器,它的作用之一是控制是否启用中断。

如下所示:并口的引脚

示例驱动程序

while(count--) {

outb(*(ptr++), port)

wmb()

}

使用I/O内存

除了x86上普遍使的I/O端口之外,和设备通信的另一种主要机制是通过使用映射到内存的寄存器或设备内存,这两种都称为I/O内存,因为寄存器和内存的差别对软件是透明的。

I/O内存仅仅是类似RAM的一个区域,在那里处理器可以通过总线访问设备。这种内存有很多用途,比如存放视频数据或以太网数据包,也可以用来实现类似I/O端口的设备寄存器(也就是说,对它们的读写也存在边际效应)。

根据计算机平台和所使用总线的不同,i/o内存可能是,也可能不是通过页表访问的。如果访问是经由页表进行的,内核必须首先安排物理地址使其对设备驱动程序可见(这通常意味着在进行任何I/O之前必须先调用ioremap)。如果访问无需页表,那么I/O内存区域就非常类似于I/O端口,可以使用适当形式的函数读取它们。

不管访问I/O内存是否需要调用ioremap,都不鼓励直接使用指向I/O内存的指针。相反使用包装函数访问I/O内存,这一方面在所有平台上都是安全的,另一方面,在可以直接对指针指向的内存区域执行 *** 作的时候,这些函数是经过优化的。并且直接使用指针会影响程序的可移植性。

I/O内存分配和映射

在使用之前,必须首先分配I/O区域。分配内存区域的接口如下(在<linux/ioport.h>中定义):

struct resource *request_mem_region(unsigned long start, unsigned long len, char *name)

该函数从start开始分配len字节长的内存区域。如果成功返回非NULL,否则返回NULL值。所有的I/O内存分配情况可从/proc/iomem得到。

不再使用已分配的内存区域时,使用如下接口释放:

void release_mem_region(unsigned long start, unsigned long len)

下面函数用来检查给定的I/O内存区域是否可用的老函数:

int check_mem_region(unsigned long start, unsigned long len)//这个函数和check_region一样不安全,应避免使用

分配内存之后我们还必须确保该I/O内存对内存而言是可访问的。获取I/O内存并不意味着可引用对应的指针在许多系统上,I/O内存根本不能通过这种方式直接访问。因此,我们必须由ioremap函数建立映射,ioremap专用于为I/O内存区域分配虚拟地址。

我们根据以下定义来调用ioremap函数:

#include <asm/io.h>

void *ioremap(unsigned long phys_addr, unsigned long size)

void *ioremap_nocache(unsigned long phys_addr, unsigned long size)在大多数计算机平台上,该函数和ioremap相同:当所有I/O内存已属于非缓存地址时,就没有必要实现ioremap的独立的,非缓冲版本。

void iounmap(void *addr)

记住,由ioremap返回的地址不应该直接引用,而应该使用内核提供的accessor函数。

访问I/O内存

在某些平台上我们可以将ioremap的返回值直接当作指针使用。但是,这种使用不具有可移植性,访问I/O内存的正确方法是通过一组专用于些目的的函数(在<asm/io.h>中定义)。

从I/O内存中读取,可使用以下函数之一:

unsigned int ioread8(void *addr)

unsigned int ioread16(void *addr)

unsigned int ioread32(void *addr)

其中,addr是从ioremap获得的地址(可能包含一个整数偏移量)返回值是从给定I/O内存读取到的值。

写入I/O内存的函数如下:

void iowrite8(u8 value, void *addr)

void iowrite16(u16 value, void *addr)

void iowrite32(u32 value, void *addr)

如果必须在给定的I/O内存地址处读/写一系列值,则可使用上述函数的重复版本:

void ioread8_rep(void *addr, void *buf, unsigned long count)

void ioread16_rep(void *addr, void *buf, unsigned long count)

void ioread32_rep(void *addr, void *buf, unsigned long count)

void iowrite8_rep(void *addr, const void *buf, unsigned long count)

void iowrite16_rep(void *addr, const void *buf, unsigned long count)

void iowrite32_rep(void *addr, const void *buf, unsigned long count)

上述函数从给定的buf向给定的addr读取或写入count个值。count以被写入数据的大小为单位。

上面函数均在给定的addr处执行所有的I/O *** 作,如果我们要在一块I/O内存上执行 *** 作,则可以使用下面的函数:

void memset_io(void *addr, u8 value, unsigned int count)

void memcpy_fromio(void *dest, void *source, unsigned int count)

void memcpy_toio(void *dest, void *source, unsigned int count)

上述函数和C函数库的对应函数功能一致。

像I/O内存一样使用I/O端口

某些硬件具有一种有趣的特性:某些版本使用I/O端口,而其他版本则使用I/O内存。导出给处理器的寄存器在两种情况下都是一样的,但访问方法却不同。为了让处理这类硬件的驱动程序更加易于编写,也为了最小化I/O端口和I/O内存访问这间的表面区别,2.6内核引入了ioport_map函数:

void *ioport_map(unsigned long port, unsigned int count)

该函数重新映射count个I/O端口,使其看起来像I/O内存。此后,驱动程序可在该函数返回的地址上使用ioread8及其相关函数,这样就不必理会I/O端口和I/O内存之间的区别了。

当不需要这种映射时使用下面函数一撤消:

void ioport_unmap(void *addr)

这些函数使得I/O端口看起来像内存。但需要注意的是,在重新映射之前,我们必须通过request_region来分配这些I/O端口。

为I/O内存重用short

前面介绍的short示例模块访问的是I/O端口,它也可以访问I/O内存。为此必须在加载时通知它使用I/O内存,另外还要修改base地址以使其指向I/O区域。

下例是在MIPS开发板上点亮调试用的LED:

mips.root# ./short_load use_mem=1 base = 0xb7ffffc0

mips.root# echo -n 7 >/dev/short0

下面代码是short写入内存区域时使用的循环:

while(count--) {

iowrite8(*ptr++, address)

wmb()

}

1MB地址空间之下的ISA内存

最广为人知的I/O内存区之一就是个人计算机上的ISA内存段。它的内存范围在64KB(0xA0000)到1MB(0x100000)之间,因此它正好出现在常规系统RAM的中间。这种地址看上去有点奇怪,因为这个设计决策是20世纪80年代早期作出的,在当时看来没有人会用到640KB以上的内存。

Linux下PCI设备驱动开发

1. 关键数据结构

PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。

在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:

pci_driver

这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):

struct pci_driver {

struct list_head node

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)

int (*save_state) (struct pci_dev *dev, u32 state)

int (*suspend)(struct pci_dev *dev, u32 state)

int (*resume) (struct pci_dev *dev)

int (*enable_wake) (struct pci_dev *dev, u32 state, int enable)

}

pci_dev

这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的

硬件信息,包括厂商ID、设备ID、各种资源等:

struct pci_dev {

struct list_head global_list

struct list_head bus_list

struct pci_bus *bus

struct pci_bus *subordinate

void*sysdata

struct proc_dir_entry *procent

unsigned intdevfn

unsigned short vendor

unsigned short device

unsigned short subsystem_vendor

unsigned short subsystem_device

unsigned intclass

u8 hdr_type

u8 rom_base_reg

struct pci_driver *driver

void*driver_data

u64 dma_mask

u32 current_state

unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE]

unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE]

unsigned intirq

struct resource resource[DEVICE_COUNT_RESOURCE]

struct resource dma_resource[DEVICE_COUNT_DMA]

struct resource irq_resource[DEVICE_COUNT_IRQ]

charname[80]

charslot_name[8]

int active

int ro

unsigned short regs

int (*prepare)(struct pci_dev *dev)

int (*activate)(struct pci_dev *dev)

int (*deactivate)(struct pci_dev *dev)

}

2. 基本框架

在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

/* 指明该驱动程序适用于哪一些PCI设备 */

static struct pci_device_id demo_pci_tbl [] __initdata = {

{PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,

PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},

{0,}

}

/* 对特定PCI设备进行描述的数据结构 */

struct demo_card {

unsigned int magic

/* 使用链表保存所有同类的PCI设备 */

struct demo_card *next

/* ... */

}

/* 中断处理模块 */

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

/* ... */

}

/* 设备文件 *** 作接口 */

static struct file_operations demo_fops = {

owner: THIS_MODULE, /* demo_fops所属的设备模块 */

read: demo_read,/* 读设备 *** 作*/

write: demo_write,/* 写设备 *** 作*/

ioctl: demo_ioctl,/* 控制设备 *** 作*/

mmap: demo_mmap,/* 内存重映射 *** 作*/

open: demo_open,/* 打开设备 *** 作*/

release:demo_release/* 释放设备 *** 作*/

/* ... */

}

/* 设备模块信息 */

static struct pci_driver demo_pci_driver = {

name: demo_MODULE_NAME,/* 设备模块名称 */

id_table: demo_pci_tbl,/* 能够驱动的设备列表 */

probe: demo_probe,/* 查找并初始化设备 */

remove: demo_remove/* 卸载设备模块 */

/* ... */

}

static int __init demo_init_module (void)

{

/* ... */

}

static void __exit demo_cleanup_module (void)

{

pci_unregister_driver(&demo_pci_driver)

}

/* 加载驱动程序模块入口 */

module_init(demo_init_module)

/* 卸载驱动程序模块入口 */

module_exit(demo_cleanup_module)

上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

3. 初始化设备模块

在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

检查PCI总线是否被Linux内核支持;

检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。

读出配置头中的信息提供给驱动程序使用。

当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化 *** 作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:

static int __init demo_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

}

驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。

static int __init demo_probe(struct pci_dev *pci_dev, const struct

pci_device_id *pci_id)

{

struct demo_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

}

4. 打开设备模块

在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。

static int demo_open(struct inode *inode, struct file *file)

{

/* 申请中断,注册中断处理程序 */

request_irq(card->irq, &demo_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

/* ... */

}

}

}

5. 数据读写和控制信息模块

PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

static int demo_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

}

/* ... */

}

事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等 *** 作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的 *** 作一方面可以通过把I/O内存重新映射后作为普通内存进行 *** 作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。

6. 中断处理模块

PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

struct demo_card *card = (struct demo_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缓冲区指针等 */

}

7. 释放设备模块

释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:

static int demo_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

/* ... */

}

8. 卸载设备模块

卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:

static void __exit demo_cleanup_module (void)

{

pci_unregister_driver(&demo_pci_driver)

}

小结

PCI总线不仅是目前应用广泛的计算机总线标准,而且是一种兼容性最强、功能最全的计算机总线。而Linux作为一种新的 *** 作系统,其发展前景是无法估量的,同时也为PCI总线与各种新型设备互连成为可能。由于Linux源码开放,因此给连接到PCI总线上的任何设备编写驱动程序变得相对容易。本文介绍如何编译Linux下的PCI驱动程序,针对的内核版本是2.4。


欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/yw/7104562.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-04-01
下一篇2023-04-01

发表评论

登录后才能评论

评论列表(0条)

    保存