0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Linux PCI驱动到底都干了些什么?(一)

Linux阅码场 来源:Linuxer 2020-04-30 15:41 次阅读

首先要明确两个概念:Linux内核 PCI设备驱动和设备本身驱动两部分。工作中所谓的编写设备驱动,其实就是编写设备本身驱动。因为Linux 内核的PCI驱动是内核自带的。

当然,并不是说内核帮咱们写好了Linux PCI驱动我们什么就不用做了,至少你要明白内核大致都干了些什么,这样你才能明白你该干什么,如何完成设备本身的驱动。我们下面就来研究下Linux PCI驱动到底都干了些什么。

Linux PCI 初始化代码逻辑上分为三个部分:

(1)内核的PCI设备驱动程序
这个伪设备驱动程序从总线0开始查询PCI系统并且定位系统中所有的PCI设备和PCI桥。它建立一个可以用来描述这个PCI系统拓朴层次的数据结构链表。并且对所有的发现的PCI桥编号。
(2)PCI BIOS
这个软件层提供在bib-pci-bios归约中描述的服务。虽然Alpha AXP不提供BIOS服务,在其Linux版本中包含了相应的功能。
(3)PCI Fixup
与特定系统相关的PCI初始化修补代码

而这里主要就是探讨Linux内核 PCI设备驱动,会在最后列出一段包含设备本身驱动的示例代码,仅供参考。

一、概述及简介

PCI(Periheral Component Interconnect)有三种地址空间:PCI I/O空间、PCI内存地址空间和PCI配置空间。其中,PCI I/O空间和PCI内存地址空间由设备驱动程序(即上面提到的设备本身驱动)使用,而PCI配置空间由Linux PCI初始化代码使用,这些代码用于配置PCI设备,比如中断号以及I/O或内存基地址。所以这里的PCI设备驱动就是要大致描述对于PCI设备驱动,Linux内核都帮我们做了什么(主),接着就是我们应该完成什么(次)。

(1)Linux内核做了什么

简单的说,Linux内核主要就做了对PCI设备的枚举和配置;这些工作都是在Linux内核初始化时完成的。

枚举:对于PCI总线,有一个叫做PCI桥的设备用来将父总线与子总线连接。作为一种特殊的PCI设备,PCI桥主要包括以下三种:

Host/PCI桥: 用于连接CPU与PCI根总线,第1个根总线的编号为0。在PC中,内存控制器也通常被集成到Host/PCI桥设备芯片中,因此Host/PCI桥通常也被称为“北桥芯片组(North Bridge Chipset)”。

PCI/ISA桥: 用于连接旧的ISA总线。通常,PCI中类似i8359A中断控制器这样的设备也会被集成到PCI/ISA桥设备中。因此,PCI/ISA桥通常也被称为“南桥芯片组(South Bridge Chipset)”

PCI-to-PCI桥(以下称为PCI-PCI桥): 用于连接PCI主总线(Primary Bus)和次总线(Secondary Bus)。PCI-PCI桥所处的PCI总线称为主总线,即次总线的父总线;PCI-PCI桥所连接的PCI总线称为次总线,即主总线的子总线。

图1 PCI系统示意图

下图摘自PCI Local Bus Specification Revision 2.1,可以看到PCI-PCI桥的Class Code(见图3)就是0x060400。

图2 Base class 06h

CPU通过Host/PCI桥与一条PCI总线相连,处在这种配置上的PCI总线称为根总线。PC机中通常只有一个Host/PCI桥,在一条PCI总线的基础上,可以再通过PCI桥连接到其他次一层的总线,例如通过PCI-PCI桥可以连接到另一条PCI总线,通过PCI-ISA桥可以连接到一条ISA总线。

事实上,现代PC机中的ISA总线正是通过PCI-ISA桥连接在PCI总线上的。这样,通过使用PCI-PCI桥,就构筑起了一个层次的、树状的PCI系统结构。对于上层的总线而言,连接在这条总线上的PCI桥也是一个设备。但是这是一种特殊的设备,它既是上层总线上的一个设备,实际上又是上层总线的延伸。

所谓枚举,就是从Host/PCI桥开始进行探测和扫描,逐个“枚举”连接在第一条PCI总线上的所有设备并记录在案。如果其中的某个设备是PCI-PCI桥,则又进一步再探测和扫描连在这个桥上的次级PCI总线。就这样递归下去,直到穷尽系统中的所有PCI设备。

其结果,是在内存中建立起一棵代表着这些PCI总线和设备的PCI树。每个PCI设备(包括PCI桥设备)都由一个pci_dev结构体来表示,而每条PCI总线则由pci_bus结构来表示。你有通过PCI桥建立起的硬件设备树,我有内存中通过数据结构构建的软件树,很和谐。

配置:PCI设备中一般都带有一些RAMROM 空间,通常的控制/状态寄存器和数据寄存器也往往以RAM区间的形式出现,而这些区间的地址在设备内部一般都是从0开始编址的,那么当总线上挂接了多个设备时,对这些空间的访问就会产生冲突。

所以,这些地址都要先映射到系统总线上,再进一步映射到内核的虚拟地址空间。而所谓的配置就是通过对PCI配置空间的寄存器进行操作从而完成地址的映射(只完成内部编址映射到总线地址的工作,而映射到内核的虚拟地址空间是由设备本身的驱动要做的工作)。

(2)Linux内核怎么做的

这里首先要说明的是,对于PCI的设备初始化(即上面提到的枚举和配置工作),PC机的BIOS和Linux内核都可以做。一般而言,只要是采用PCI总线的PC机,其BIOS就必须提供对PCI总线操作的支持,因而称为PCI BIOS。

而且最早Linux内核也是通过这种BIOS调用的方式来获取系统中的PCI设备信息的,但是不是所有的平台都有BIOS(比如某些嵌入式系统),并且在实践中也发现有些母板上的PCI BIOS存在这样那样的问题,所以后来就改由Linux内核自己动手了,自己动手丰衣足食呵呵。

不过,Linux内核还是很体贴的在make menuconfig的选项里为我们提供了自己选择的权利,即PCI access mode,里面提供了四个选项分别是BIOS、MMconfig、Direct和Any。Direct方式就是抛开BIOS而由内核自己完成初始化工作的意思。

二、开始我们的枚举与配置之路

前面提到了PCI有三种地址空间,其中的PCI配置空间是给Linux内核中的PCI初始化代码用的,也就是我们这里的枚举与配置时用到的。那么这个PCI配置空间里放的是什么东西呢,显然应该是寄存器,称为配置寄存器组。当PCI设备上电时,硬件保持未激活状态。即该设备只会对配置事务做出响应。上电时,设备上不会有内存和I/O端口映射到计算机的地址空间;其他设备相关的功能,例如中断报告,也被禁止。

PCI标准规定每个设备的配置寄存器组最多可以有256字节的连续空间,其中开头的64字节的用途和格式是标准的,称为配置寄存器的头部。系统中提供一些与硬件有关的机制,使得PCI配置代码可以检测在一个给定的PCI总线上所有可能的PCI配置寄存器头部,从而知道哪个PCI插槽上目前有设备,哪个插槽上暂无设备。这是通过读PCI配置寄存器头部上的某个域完成的(一般是“Vendor ID" 域)。如果一个插槽上为空,上述操作会返回一些错误返回值,如0xFFFFFFFF。

这种头部(指64字节头部)又有三种,其中“0型”(type 0)头部用于一般的PCI设备,“1型”头部用于各种PCI-PCI桥, “2型”头部是用于PCI-CardBus桥的,CardBus是笔记本电脑中使用的总线,我们不关心。

而64字节头部中的16个字节中又包含着有关头部的类型、设备的种类、设备的一些性质、由谁制造等等信息。根据这16个字节中提供的信息,来确定应该怎样进一步解释和处理剩余头部中的48个字节。对于这16个字节的地址,include/linux/pci.h中定义了这样一些常数:

#define PCI_VENDOR_ID 0x00 /* 16 bits */#define PCI_DEVICE_ID 0x02 /* 16 bits */ #define PCI_COMMAND 0x04 /* 16 bits */ #define PCI_STATUS 0x06 /* 16 bits */ #define PCI_CLASS_REVISION 0x08 /* High 24 bits are class, low 8 revision */ #define PCI_REVISION_ID 0x08 /* Revision ID */ #define PCI_CLASS_PROG 0x09 /* Reg. Level Programming Interface */ #define PCI_CLASS_DEVICE 0x0a /* Device class */ #define PCI_CACHE_LINE_SIZE 0x0c /* 8 bits */ #define PCI_LATENCY_TIMER 0x0d /* 8 bits */ #define PCI_HEADER_TYPE 0x0e /* 8 bits */

对应我们的图3(见下)中的前16字节。而且我们也看到了紧挨着PCI_HEADER_TYPE(即存放头部类型的寄存器)下面定义的就是上面提到的三种类型的头部:

#define PCI_HEADER_TYPE_NORMAL 0#define PCI_HEADER_TYPE_BRIDGE 1#define PCI_HEADER_TYPE_CARDBUS 2

在Linux系统上,可以通过cat /proc/pci 等命令查看系统中所有PCI设备的类别、型号以及厂商等信息,那就是从这些寄存器来的。下面是在虚拟机中用lspci -x命令的信息截取(lspci命令也是使用/proc文件作为其信息来源):

00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00 10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19 30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

首先要说明的是PCI寄存器是小端字节序格式的。那么根据最下面的PCI配置寄存器组的结构(图4),显然这个Host bridge的Vendor ID是0x8086,我不说你也能猜到这个Vendor就是Intel。

这里有个问题要先说清楚,就是这些寄存器的地址问题,不然往后就进行不下去了。配置寄存器可以让我们来进行配置以便完成PCI设备上的存储空间的访问,但这些配置寄存器本身也位于PCI设备地址空间中,如何访问这部分空间也就成了我们整个初始化工作的一个入口点,就像每个可执行程序都要有入口点一样。

PCI采用的办法是让所有设备的配置寄存器组都采用相同的地址,由所在总线的PCI桥在访问时附加上其他条件来区分。而CPU则通过一个统一的入口地址向“宿主--PCI桥”发出命令,由相应的PCI桥间接的完成具体的读写。对于i386结构的处理器,PCI总线的设计者在I/O地址空间保留了8个字节用于这个目的,那就是0xCF8~0xCFF。

这8个字节构成了两个32位的寄存器,第一个是“地址寄存器”0xCF8,第二个是“数据寄存器”0xCFC。要访问某个设备中的某个配置寄存器时,CPU先往地址寄存器中写入目标地址,然后通过数据寄存器读写数据。不过,写入地址寄存器的目标地址是一种总线号、设备号、功能号以及设备寄存器地址在内的综合地址。格式如下图:

图3 写入地址寄存器0xCF8的综合地址

这里的总线号、设备号和功能号是对配置寄存器地址的扩充,就是上面提到的附加的其他条件。

首先每个PCI总线都有个总线号,主总线的总线号为0,其余的则由CPU在枚举阶段每当探测到一个PCI桥时便为其指定一个,依次递增。设备号一般代表着一块PCI接口卡(更确切的说是PCI总线接口芯片),通常取决于插槽的位置。PCI接口卡上可以有若干个功能模块,这些功能模块共用一个PCI总线接口芯片,包括其中用于地址映射的电子线路,以降低成本。

从逻辑的角度说,每个“功能”实际上就是一个设备(看过USB设备驱动的人很眼熟吧 ,呵呵),所以设备号和功能号合在一起又可以称作“逻辑设备号”,而每块卡上最多可以容纳8个设备。

显然,这些字段(指整个32bit)结合在一起就惟一确定了系统中的一项PCI逻辑设备。开始时,只有0号总线可以访问,在扫描0号总线时如果发现上面某个设备是PCI桥,就为之指定一个新的总线号,例如1,这样1号总线就可以访问了,这就是枚举阶段的任务之一。

现在请读者考虑一个问题:当我们拿到一块PCI网卡,把它插到PC的主板上,打算写个这个网卡的驱动。那么第一步该干什么呢?读者可以回顾前面的内容,既然我们说Linux内核帮我们做了设备的枚举和配置工作,那么我在写网卡驱动之前是不是可以先看看Linux内核对我们的这个PCI网卡设备完成的枚举工作的结果呢?或者直白些说,我把网卡插上了,现在Linux内核有没有识别出这块设备呢?注意识别出设备跟能正常使用设备是不同的概念,这很好理解。

安装过PC网卡驱动的人都知道,当设备的驱动没有安装时,我们在设备管理器中是可以看到这个设备的,不过上面是一个黄色的大问号。而在Linux系统中,我们可以通过lspci命令来查看。

下面是在LDD3的PCI驱动那一章截取的一段内容: lspci 的输出( pciutils 的一部分, 在大部分发布中都有)和在 /proc/pci 和 /porc/bus/pci 中的信息排布. PCI 设备的 sysfs 表示也显示了这种寻址方案, 还有 PCI 域信息,当显示硬件地址时, 它可被显示为 2 个值( 一个 8-位总线号和一个 8-位 设备和功能号), 作为 3 个值( bus, device, 和 function), 或者作为 4 个值(domain, bus, device, 和 function); 所有的值常常用 16 进制显示.

例如, /proc/bus/pci/devices 使用一个单个16位字段(来便于分析和排序), 而 /proc/bus/busnumber 划分地址为3个字段. 下面内容显示了这些地址如何显示, 只显示了输出行的开始 :

$ lspci | cut -d: -f1-3000000.0 Host bridge 000000.1 RAM memory 000000.2 RAM memory 000002.0 USB Controller 000004.0 Multimedia audio controller 000006.0 Bridge 000007.0 ISA bridge 000009.0 USB Controller 000009.1 USB Controller 000009.2 USB Controller 00000c.0 CardBus bridge 00000f.0 IDE interface 000010.0 Ethernet controller 000012.0 Network controller 000013.0 FireWire (IEEE 1394) 000014.0 VGA compatible controller $ cat /proc/bus/pci/devices | cut -f1 0000 0001 0002 0010 0020 0030 0038 0048 0049 004a 0060 0078 0080 0090 0098 00a0 $ tree /sys/bus/pci/devices/ /sys/bus/pci/devices/ |-- 000000.0 -> ../../../devices/pci0000:00/000000.0 |-- 000000.1 -> ../../../devices/pci0000:00/000000.1 |-- 000000.2 -> ../../../devices/pci0000:00/000000.2 |-- 000002.0 -> ../../../devices/pci0000:00/000002.0 |-- 000004.0 -> ../../../devices/pci0000:00/000004.0 |-- 000006.0 -> ../../../devices/pci0000:00/000006.0 |-- 000007.0 -> ../../../devices/pci0000:00/000007.0 |-- 000009.0 -> ../../../devices/pci0000:00/000009.0 |-- 000009.1 -> ../../../devices/pci0000:00/000009.1 |-- 000009.2 -> ../../../devices/pci0000:00/000009.2 |-- 00000c.0 -> ../../../devices/pci0000:00/00000c.0 |-- 00000f.0 -> ../../../devices/pci0000:00/00000f.0 |-- 000010.0 -> ../../../devices/pci0000:00/000010.0 |-- 000012.0 -> ../../../devices/pci0000:00/000012.0 |-- 000013.0 -> ../../../devices/pci0000:00/000013.0 |--000014.0->../../../devices/pci0000:00/000014

所有的 3 个设备列表都以相同顺序排列, 因为 lspci 使用 /proc 文件作为它的信息源。拿 VGA 视频控制器作一个例子, 0x00a0 意思是 000014.0 当划分为域(16位), 总线(8位), 设备(5位)和功能(3位).为什么0x00a0对应的是000014.0呢,这就要看图2中的内容了,根据图2中的寄存器对应0x00a0就代表着总线(8位), 设备(5位)和功能(3位).

0x00a0=0000000010100000,很容易看出高8位是总线号也就是0。剩下的0xa0=10100000,可以看出如果低3位表示功能号,那么剩下的10100就是设备号,补全成8位的值就是00010100即0x14.

图4 PCI配置寄存器组

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11230

    浏览量

    208937
  • PCI
    PCI
    +关注

    关注

    4

    文章

    662

    浏览量

    130184
  • PCI设备
    +关注

    关注

    0

    文章

    9

    浏览量

    8125

原文标题:PCI设备驱动(一)

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    RTOS与Linux到底有什么区别

    很多做嵌入式开发的小伙伴存在这样的疑惑:RTOS与Linux到底有什么区别?
    的头像 发表于 10-29 09:53 326次阅读

    pci内存控制器感叹号有影响吗

    PCI内存控制器感叹号通常表示硬件设备存在一些问题,这可能会对您的计算机性能产生影响。以下是一些可能的原因和解决方法: 驱动程序问题:感叹号可能表示
    的头像 发表于 10-10 16:56 1764次阅读

    驱动到空调、机器人应用,剖析格力的电机布局

    电子发烧友网报道(文/吴子鹏)电机俗称马达,是种将电能转换为机械能的装置,主要作用是产生驱动转矩,作为用电器或各种设备的动力源。电机在家用电器、工业设备、农业设备、军工设备以及交通工具中无处不在
    的头像 发表于 10-09 00:10 5862次阅读
    从<b class='flag-5'>驱动到</b>空调、机器人应用,剖析格力的电机布局

    LED驱动器应用的一些指南和技巧

    电子发烧友网站提供《LED驱动器应用的一些指南和技巧.pdf》资料免费下载
    发表于 09-25 11:35 0次下载
    LED<b class='flag-5'>驱动</b>器应用的<b class='flag-5'>一些</b>指南和技巧

    linux系统的设备驱动般分几类

    Linux系统的设备驱动是操作系统与硬件设备之间的桥梁,负责实现操作系统与硬件设备之间的通信和控制。Linux系统的设备驱动可以分为以下几类: 字符设备
    的头像 发表于 08-30 15:13 348次阅读

    Linux设备驱动程序分类有哪些

    Linux设备驱动程序是操作系统与硬件设备之间的桥梁,负责实现硬件设备与操作系统之间的通信和控制。Linux设备驱动程序的分类繁多,可以根据不同的标准进行分类。 按硬件类型分类
    的头像 发表于 08-30 15:11 441次阅读

    linux驱动程序如何加载进内核

    ,需要了解Linux内核的基本概念和API。以下是一些关键概念: 1.1 内核模块:Linux内核模块是种动态加载和卸载的代码,可以在不重新启动系统的情况下加载和卸载。
    的头像 发表于 08-30 15:02 382次阅读

    linux驱动程序的编译方法是什么

    Linux驱动程序的编译方法主要包括两种: 与内核起编译 和 编译成独立的内核模块 。以下是对这两种方法的介绍: 、与内核起编译 与内
    的头像 发表于 08-30 14:46 428次阅读

    linux驱动程序运行在什么空间

    Linux 驱动程序是操作系统的部分,负责管理硬件设备与操作系统之间的交互。驱动程序运行在内核空间(Kernel Space),这是操作系统的核心部分,与用户空间(User Spac
    的头像 发表于 08-30 14:37 304次阅读

    Linux 驱动开发与应用开发,你知道多少?

    Linux驱动开发与应用开发的区别开发层次不同:Linux驱动开发主要是针对硬件设备进行编程,处于操作系统内核层,直接与硬件交互,为上层
    的头像 发表于 08-30 12:16 618次阅读
    <b class='flag-5'>Linux</b> <b class='flag-5'>驱动</b>开发与应用开发,你知道多少?

    PCI2250 PCIPCI桥数据表

    电子发烧友网站提供《PCI2250 PCIPCI桥数据表.pdf》资料免费下载
    发表于 07-10 09:34 0次下载
    <b class='flag-5'>PCI</b>2250 <b class='flag-5'>PCI</b>到<b class='flag-5'>PCI</b>桥数据表

    请问esp32s3怎么添加nt35510驱动到新工程中?

    请问esp32s3怎么添加nt35510驱动到新工程中
    发表于 06-27 06:30

    Nvidia与AMD新芯片,突破PCIe瓶颈

    AMD 和 Nvidia 的 GPU 依赖 PCI 总线与 CPU 进行通信。CPU 和 GPU 有两个不同的内存域,数据必须通过 PCI 接口从 CPU 域移动到 GPU 域(并返
    的头像 发表于 03-08 14:15 609次阅读
    Nvidia与AMD新芯片,突破PCIe瓶颈

    通过JTAG启动Linux的方法和脚本

    存储器(QSPI Flash,eMMC 等)上的镜像,直接启动到 Linux。但当板子调试时,经常需要通过 JTAG 把 SoC 器件启动到 Linux。这篇文章将分享通过 JTAG
    的头像 发表于 12-22 10:27 1394次阅读
    通过JTAG启动<b class='flag-5'>Linux</b>的方法和脚本

    linux驱动程序的主要流程和功能

    驱动程序是用于控制和管理硬件设备的软件模块,它主要负责与设备进行交互,通过操作设备的寄存器和接口,实现对硬件的控制和访问。在Linux系统中,驱动程序是实现与硬件设备交互的个关键部分
    的头像 发表于 12-08 14:56 2262次阅读