当前位置: 首页 > 工具软件 > PCI工具包 > 使用案例 >

Linux设备驱动程序学习(十)——PCI驱动程序

和弘博
2023-12-01

  前面介绍的是最底层的硬件控制,这部分将介绍高级总线架构的一些综述,总线由电气接口和编程接口够成。下面将重点介绍PCI总线的编程接口以及对应的内核函数。

PCI(外围设备互联)接口

  PCI总线是当今普遍使用在桌面以及更大型计算机上的外设总线,而且该总线是内核中得到最好支持的总线。尽管许多计算机用户将PCI看成是一种布置电子线路的方式,但实际上它是一组完整的规范,定义了计算机的各个不同部分之间该如何交互。
  PCI规范涵盖了与计算机接口相关的大部分问题。这里详细介绍PCI驱动程序如何寻找其硬件和获得对它的访问。
  PCI架构的三个主要目标

  • 获得在计算机和外设之间传输数据时更好的性能;
  • 尽可能的平台无关;
  • 简化往系统中添加和删除外设的工作。

PCI总线与配置空间

  PCI 总线体系结构是一种层次式的体系结构。在这种层次式体系结构中,PCI 桥设备占据着重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树型结构。树的顶端是系统的 CPU,它通过一个较为特殊的 PCI 桥设备——Host/PCI 桥设备与根 PCI 总线连接起来。
  作为一种特殊的 PCI 设备,PCI 桥包括以下几种

  • Host/PCI 桥:用于连接 CPU 与 PCI 根总线,第 1 个根总线的编号为0,内存控制器也通常被集成到 Host/PCI 桥设备芯片中,桥通常也被称为“北桥芯片组(North Bridge Chipset)”。
  • PCI/ISA 桥:用于连接旧的 ISA 总线。通常,PCI 中的类似i8359A 中断控制器这样的设备也会被集成到 PCI/ISA 桥设备中,因此,PCI/I称为“南桥芯片组(South Bridge Chipset)”。
  • PCI-to-PCI 桥:用于连接 PCI 主总线(primary bus)与次总线(sPCI 桥所处的 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 结构体以一种倒置树的方式组织起来。

PCI设备

  在 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设备上有三种地址空间: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 只是一种总线,具体的 PCI 设备可以是字符设备、网络设备、USB主机控制器等,因此,一个通过 PCI 总线与系统连接的设备的驱动至少包含以下两部分:

  • PCI 设备驱动
  • 设备本身的驱动

  PCI 驱动只是为了辅助设备本身的驱动,它不是目的,只是手段,PCI 设备本身含有双重以上的身份。

pci_driver结构体

  在 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_driver_id结构体

  在 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设备驱动程序的基本框架:

/* 指明该驱动程序适用于哪一些 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设备的初始化,需要完成以下工作:

  • 检查PCI总线是否被Linux内核支持;
  • 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
  • 读出配置头中的信息提供给驱动程序使用。
      当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有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总线在设计上相当陈旧而且其差劲的性能臭名昭著,但是在支持老主板而速度不是很重要的时候,ISA比PCI要更有优势。

  一个ISA设备可配备有I/O端口,内存区域以及中断线:

  • 尽管 x86 处理器支持 64 KB I/O 端口内存(即处理器有 16 条地址线), 一些老 PC 硬件仅解码最低的 10 位地址线,这限制可用的地址空间为 1024 个端口。
  • 如果 I/O 端口的可用性被限制, 内存存取更加麻烦. 一个 ISA 设备可只使用 640KB 到 1 MB 之间的内存范围和 15 MB 和 16MB 之间的范围给 I/O 寄存器和设备控制。
  • 对 ISA 设备板第 3 个可用资源是中断线. 一个有限数目的中断线被连接到 ISA 总线, 并且它们由所有接口板共享. 结果是, 如果设备不被正确配置, 它们可能发现它们自己在使用同一个中断线。

ISA编程

  对于编程, 内核中没有特别的帮助来易于存取 ISA 设备(像对 PCI 那样有). 你可使用的唯一工具是 I/O 端口和 IRQ 线的注册, 只能通过中断处理来实现, 驱动可探测 I/O 端口, 并且中断线必须被自动探测, 这要通过"自动探测 IRQ 号"技术来实现。

其它PC总线

   PCI 和 ISA 是在 PC 世界中最常用的外设接口, 但是它们不是唯一的. 这里简单介绍一下 PC 市场上的其他总线的特性:

MCA总线

  微通道结构(MCA)是,用在 PS/2 计算机和一些笔记本电脑的IBM 标准. 在硬件层次上, 微通道比 ISA 有更多特性. 它支持多主 DMA, 32-位地址和数据线, 共享中断线, 和地理式寻址来存取每块板的配置寄存器. 这样的寄存器被称为可编程选项选择(POS), 但是它们没有 PCI 寄存器的全部特点. Linux 对 微通道的支持包括输出给模块的函数。

EISA总线

  扩展 ISA (EISA) 总线是一个对 ISA 的 32-位 扩展, 带有一个兼容的接口连接器; ISA 设备板可被插入一个 EISA 连接器. 增加的线在 ISA 接触之下被连接.

VLB总线

  另一个对 ISA 的扩展是 VESA Local Bus(VLB) 接口总线, 它扩展了 ISA 连接器, 通过添加第 3 个知道长度的槽位。一个设备可只插入这个额外的连接器(不用插入 2 个关联的 ISA 连接器), 因为 VLB 槽位从 ISA 连接器复制了所有的重要信号. 这样"独立"的 VLB 外设不使用 ISA 槽位是少见的, 因为大部分设备需要伸到后面板, 使它们的外部连接器是可用的。
  VESA 总线比 EISA , MCA, 和 PCI 总线在它的能力方面更加限制, 并且正在从市场上消失. 没有特殊的内核支持位 VLB 而存在。

 类似资料: