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
    +关注

    关注

    88

    文章

    11628

    浏览量

    217983
  • PCI
    PCI
    +关注

    关注

    5

    文章

    685

    浏览量

    133684
  • PCI设备
    +关注

    关注

    0

    文章

    9

    浏览量

    8248

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

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

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    Linux驱动开发的必备知识

    内核基础知识: 1、熟悉 Linux 内核的架构、模块系统、进程管理、内存管理等。 了解内核的编译和加载过程。 2、C编程技能: 精通 C 语言编程,包括指针操作、内存管理、结构体等
    发表于 12-04 07:58

    别再装系统了!Linux 镜像到底是什么?篇讲到你怀疑人生

    多小、环境多复杂,如何快速安装、部署和维护 Linux 系统,都是开发者和运维人员必须掌握的核心技能。 这时,“Linux 镜像文件”就显得尤为重要。它就像份完整的系统快照,让你可以在不同设备之间快速迁移、复制,甚至批量部署。
    的头像 发表于 12-03 16:12 405次阅读
    别再装系统了!<b class='flag-5'>Linux</b> 镜像<b class='flag-5'>到底</b>是什么?<b class='flag-5'>一</b>篇讲到你怀疑人生

    驱动芯片到底有多重要?这些黑科技让电子设备飞起来!

    你知道吗? 驱动芯片 就像电子设备的心脏,没有它,你的手机、电脑、智能家电统统都会变成废铁!但这么重要的东西,99%的人却对它无所知。 驱动芯片到底是什么鬼? 简单来说,
    的头像 发表于 11-25 08:37 128次阅读
    <b class='flag-5'>驱动</b>芯片<b class='flag-5'>到底</b>有多重要?这些黑科技让电子设备飞起来!

    【免费送书】成为硬核Linux开发者:《Linux 设备驱动开发(第 2 版)》

    Linux系统的设备驱动开发,直给人门槛较高的印象,主要因内核机制抽象、需深度理解硬件原理、开发调试难度大所致。2021年,本讲解驱动
    的头像 发表于 11-18 08:06 440次阅读
    【免费送书】成为硬核<b class='flag-5'>Linux</b>开发者:《<b class='flag-5'>Linux</b> 设备<b class='flag-5'>驱动</b>开发(第 2 版)》

    【书籍评测活动NO.67】成为硬核Linux开发者:《Linux 设备驱动开发(第 2 版)》

    时代的硬核技能。现在,我们从零起步,进入Linux设备驱动开发的世界吧。从零开始学会Linux设备驱动开发要快速上手Linux
    发表于 11-17 17:52

    学习Linux应该从哪里开始?

    在工业控制、边缘计算、人工智能这些领域,有句几乎约定俗成的话: “会不会 Linux,决定了你能走多远。” 但对很多刚入门的工程师来说,Linux 看起来像个庞大又神秘的世界——命令行、驱动
    的头像 发表于 10-16 09:51 301次阅读
    学习<b class='flag-5'>Linux</b>应该从哪里开始?

    请问编译纯rtos到底是选择Linux+rtos的sdk编译only rtos还是直接使用rtos sdk?

    编译纯rtos到底是选择Linux+rtos的sdk编译only rtos还是直接使用rtos sdk?
    发表于 07-11 07:22

    目前Mirco Python sdk触摸到底支持什么驱动啊 ?

    目前的Mirco Python sdk触摸到底支持什么驱动啊 能不能有资料详细说明 GT911 触摸驱动是否支持啊
    发表于 06-24 07:02

    Debian和Ubuntu哪个好一些

    兼容性对比Debian和Ubuntu哪个好一些,并为您揭示如何通过RAKsmart服务器释放Linux系统的最大潜能。
    的头像 发表于 05-07 10:58 855次阅读

    文给你讲透!DA板卡到底是什么?它和主板又有哪些不同?

    大家好,我是老王,在电子行业干了十几年,今天我就用“大白话”给大家讲讲DA板卡到底是啥,它和咱们常说的“主板”有啥区别。文章里会穿插一些表格和实际案例,保证你读完不仅能懂,还能跟朋友吹牛!
    的头像 发表于 04-24 16:48 1667次阅读
    <b class='flag-5'>一</b>文给你讲透!DA板卡<b class='flag-5'>到底</b>是什么?它和主板又有哪些不同?

    Linux环境再升级:PLIN驱动程序正式发布

    PLIN驱动程序现已正式发布,本文将展示如何安装PLIN驱动程序,以及如何在Linux环境下进行基本的PLIN通信操作,确保您能够快速掌握并应用这新工具。
    的头像 发表于 04-21 15:29 795次阅读
    <b class='flag-5'>Linux</b>环境再升级:PLIN<b class='flag-5'>驱动</b>程序正式发布

    使用Mickledore生成BSP,移动到最新版本的Scarthgap v6.6.52时,驱动程序未构建,怎么解决?

    当我使用 Mickledore 生成 BSP 时,moal.ko WIFI 驱动程序是自动构建的,并且位于 /lib/modules/ 中,并且 WIFI 可以正常工作。 当我移动到最新版
    发表于 03-27 06:49

    驱动电路设计()—— 驱动器的功能综述

    驱动电路设计是功率半导体应用的难点,涉及到功率半导体的动态过程控制及器件的保护,实践性很强。为了方便实现可靠的驱动设计,英飞凌的驱动集成电路自带了一些重要的功能,本系列文章将详细讲解如
    的头像 发表于 02-10 17:05 1375次阅读
    <b class='flag-5'>驱动</b>电路设计(<b class='flag-5'>一</b>)—— <b class='flag-5'>驱动</b>器的功能综述

    迅为RK3568开发板驱动指南Linux中通用SPI设备驱动

    迅为RK3568开发板驱动指南Linux中通用SPI设备驱动
    的头像 发表于 01-23 11:02 3433次阅读
    迅为RK3568开发板<b class='flag-5'>驱动</b>指南<b class='flag-5'>Linux</b>中通用SPI设备<b class='flag-5'>驱动</b>

    PCI2250 PCIPCI桥接器实施指南

    电子发烧友网站提供《PCI2250 PCIPCI桥接器实施指南.pdf》资料免费下载
    发表于 12-23 15:13 1次下载
    <b class='flag-5'>PCI</b>2250 <b class='flag-5'>PCI</b>到<b class='flag-5'>PCI</b>桥接器实施指南