0%

【设备驱动】PCI 设备驱动程序(一)PCI 总线与驱动概述

PCI 总线是目前应用最广泛的计算机总线标准,而且是一种兼容性最强,功能最全的计算机总线。最近需要编写 PCI 设备相关的驱动,这里记录下学习过程,主要会涉及 PCI 总线的拓扑结构,驱动编写的要点等,后续有时间会深入分析源码。

驱动概述

应用程序位于用户空间,驱动程序位于内核空间。linux系统规定,用户空间不可以直接调用内核函数,所以必须经过系统调用,应用程序才可以调用驱动程序的函数。另外应用程序通过系统调用去调用驱动程序的函数,还有一个前提就是驱动程序必须留有接口,这里的接口就是ops函数的操作集合。

image-20240724143217323

驱动最终通过与文件系统相关的系统调用或 C 库函数(本质也是基于系统调用)被访问,而设备驱动的结构也是为了迎合应用程序,提供给应用程序的 SDK API。

sys:linux设备驱动模型中的总线、驱动和设备都可以在 sysfs 文件系统中找到相应的节点。当内核检测到系统中出现新的设备后,内核会在 sysfs 文件系统中为设备生成一项新的记录。

sysfs 是一个虚拟的文件系统,它可以产生一个包括所有系统硬件的层级视图,与提供进程和状态的 proc 文件系统十分类似。可以让用户空间存取,向用户空间导出内核数据结构以及它的属性。

在 linux 内核中,设备和驱动是分开注册的,注册1个设备的时候,并不需要驱动已经存在,而1个驱动被注册的时候,也不需要对应的设备被注册。设备和驱动各自涌入内核,而每个设备和驱动涌入内核的时候,都会寻找另外一半。而正是 bus_type 的 match() 成员函数将两者绑定在一起。

简单来说,设备和驱动就是男男女女,而 bus_type 的 match() 则是牵引红线的月老,它可以识别什么设备与什么驱动,是配对的。一旦成功,xxx_driver 的 probe 设备探测函数就被执行。

PCI 总线协议概述

PCI 拓扑结构

image-20240724144201545

PCI 是 CPU 和外围设备通信的高速传输总线。普通 PCI 总线带宽一般为132MB/s或者264MB/s。

PCI总线体系结构是一种层次式的体系结构,PCI 桥设备占据重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树形结构。对于驱动开发而言,刚开始我们就不对这些物理结构的抽象进行深入研究了,这些 host 和 bridge 都是由内核实现抽象的,我们专注于 PCI 设备的驱动编写即可。

PCI 寻址方式

每个 PCI 外设由 1 个总线编号,1 个设备编号以及 1 个功能编号来标识。PCI 协议规定每个系统支持 256 条总线,每条总线 32 个设备,每个设备最多 8 个功能。此外,对于一些大型系统,Linux 支持 PCI 域,每个 PCI 域支持 256 个总线。所以每个功能都可以由 16 地址来标识,如下:

1
0e3c:00:00.0
  • 0e3c:该功能的域
  • 00:该功能的总线
  • 00:该功能的设备
  • 0:具体功能

这些与 PCI 物理外设相关联的 16 位地址,往往被 Linux 隐藏在了 struct pci_dev 结构中,但有时仍可见,比如使用 lspci 命令或者在 procsysfs 中的布局就是这样组织的。

PCI 配置空间

所以的 PCI 设备都至少要有 256 字节的配置空间,前 64 个字节是标准化的,其余是设备相关的。如下图所示,标准化的配置寄存器中,有些是必需的,有些是可选的。

image-20240724150840915

对于这些寄存器的解释暂时不太需要,我们仅看一些关键的,即驱动程序如何根据配置寄存器区分设备。

  • vendorID:厂商 ID,标识硬件制造商,该值由国际组织维护,每个厂商全球唯一
  • deviceID:由制造商选择,无需注册,用于区分不同设备
  • class:某些驱动程序可能可以支持多个不同签名(vendorID + deviceID)的设备,但同属一个类
  • subsystem vendorID:用于进一步区分设备
  • subsystem deviceID:用于进一步区分设备

在 Linux 中,使用 struct pci_device_id 来描述一个驱动程序支持的 PCI 设备列表,例如:

1
2
3
4
5
6
7
8
9
#define PCI_VENDOR_ID_QEMU		0x1234
#define PCI_KMOD_EDU_VENDOR_ID PCI_VENDOR_ID_QEMU
#define PCI_KMOD_EDU_DEVICE_ID 0x7863

static struct pci_device_id ids[] = {
{ PCI_DEVICE(PCI_KMOD_EDU_VENDOR_ID, PCI_KMOD_EDU_DEVICE_ID), },
{ 0, }
};
MODULE_DEVICE_TABLE(pci, ids);

MODULE_DEVICE_TABLE 的作用:将该模块支持的设备以及模块名导出到用户空间(记录在一个文件中),当内核出现热插拔事件时,会扫描这个文件,加载主持的驱动模块。

pci_device_id 结构如下:

1
2
3
4
5
6
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};

PCI 初始化

在系统引导阶段,PCI 设备仅会对配置事务做出响应,此时,固件(或者 Linux 内核)会为每一个 PCI 设备执行配置事务,该过程会为其分配内存空间以及IRQ号等资源,并且将资源记录到 PCI 设备的配置空间中,供驱动程序使用。可以通过 sysfs 文件系统查看设备分配的资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/sys/bus/pci/devices/0e3c:00:00.0
├── ari_enabled
├── broken_parity_status
├── class
├── config
├── consistent_dma_mask_bits
├── device
├── dma_mask_bits
├── driver -> ../../../../../../../../bus/pci/drivers/dxgkrnl
├── driver_override
├── enable
├── irq
├── link
├── local_cpulist
├── local_cpus
├── modalias
├── msi_bus
├── remove
├── rescan
├── resource
├── revision
├── subsystem -> ../../../../../../../../bus/pci
├── subsystem_device
├── subsystem_vendor
├── uevent
└── vendor

其中,irq 代表分配的中断号,resource 代表分配的内存空间,可以继续查看:

1
2
$ cat /sys/bus/pci/devices/0e3c\:00\:00.0/resource 
起始地址(Start Address)结束地址(End Address)标志(Flags)
1
2
$ cat /sys/bus/pci/devices/0e3c\:00\:00.0/irq 
irq_number

PCI 驱动程序

注册驱动程序

为了能够正确注册驱动程序到内核,所有的 PCI 设备必须创建的主要结构体是 struct pci_driver,其结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct pci_driver {
struct list_head node;
char *name; /* 驱动程序的名称 */
struct module *owner;
/* 指向设备驱动程序感兴趣的设备ID的一个列表,包括:
* 厂商ID、设备ID、子厂商ID、子设备ID、类别、类别掩码、私有数据
*/
const struct pci_device_id *id_table;
/* 指向一个函数对于每一个id_table中的项匹配的且未被其他驱动程序处理的设备,
* 在执行pci_register_driver时候调用此函数或者如果是以后插入的一个新设备的话,只要满足上述条件也会调* 用此函数
*/
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id);
/* 指向一个函数当驱动函数程序卸载或者被该驱动程序管理的设备被卸下的时候,调用此函数
*/
int (*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_driver 结构体,只需实现下面四个成员:

1
2
3
4
5
6
static struct pci_driver pci_drv = {
.name = "pci_skel",
.id_table = ids,
.probe = my_proce,
.remove = my_remove
};

一般的,需要在入口函数中注册驱动:

1
2
3
4
5
static int __init pci_init(void)
{
return pci_register_driver(&pci_drv);
}
module_init(pci_init))

在出口函数中注销驱动:

1
2
3
4
5
static int __exit pci_exit(void)
{
return pci_unregister_driver(&pci_drv);
}
module_exit(pci_exit))

激活 PCI 设备

在 PCI 驱动探测(probe)设备时,访问 PCI 设备的任何资源之前(IO 或 中断),必须调用 pci_enable_device 函数,该函数会使能底层的 I/O 和内存,唤醒挂起的设备:

1
2
3
4
5
/* Initialize device before it's used by a driver. Ask low-level code
* to enable I/O and memory. Wake up the device if it was suspended.
* Beware, this function can fail.
*/
int pci_enable_device(struct pci_dev *dev);

访问配置空间

在驱动程序检测到设备之后,通常需要读取和写入三个地址空间:内存端口 以及 配置 空间。其中,范围配置空间尤其重要,因为这是确定设备映射到内存和IO空间哪个位置的唯一方式。那么系统初始化的时候该如何访问这些寄存器?

对于 Linux 驱动程序而言,内核提供了一系列封装好的接口:

1
2
3
4
5
6
int pci_read_config_byte(const struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(const struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(const struct pci_dev *dev, int where, u32 *val);
int pci_write_config_byte(const struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(const struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(const struct pci_dev *dev, int where, u32 val);

其中,dev 代表了 PCI 设备, where 代表了配置空间的偏移,上述函数一次分别可以读写 1,2,4 个字节。

使用上述函数的首选方式是利用 <linux/pci.h> 中定义的宏取代 where,例如:

1
2
3
4
#define PCI_INTERRUPT_LINE	0x3c	/* 8 bits */
#define PCI_INTERRUPT_PIN 0x3d /* 8 bits */
#define PCI_MIN_GNT 0x3e /* 8 bits */
#define PCI_MAX_LAT 0x3f /* 8 bits */

对于i386结构的处理器,PCI总线的设计者在I/O地址空间保留了8个字节用于这个目的,那就是0xCF8~0xCFF,这8个字节的地址空间构成了两个32位的寄存器,第一个是“地址寄存器”0xCF8,第二个是“数据寄存器”0xCFC,要访问配置空间的寄存器时,CPU先向地址寄存器写入目标地址,然后通过数据寄存器进行读写数据不过,写入地址寄存器的目标地址是一种包括总线号、设备号、功能号以及配置寄存器地址的综合地址。每个PCI设备最多有8个功能,所以设备号和功能号组合在一起又被称作“逻辑设备”号。

访问 BAR 空间

在内核中,PCI 设备的 BAR 空间已经被纳入到通用资源管理中,因此不需要通过读取配置空间区访问 BAR 空间,可以直接使用内核提供的方法获取:

1
2
3
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);

分别返回第 bar 个空间中 第一个和最后一个 可用 地址,以及资源相关的标志。

资源的相关标志来定义单个资源的某些特性,例如:

1
2
3
4
5
6
IORESOURCE_IO
IORESOURCE_MEM
如果相关的 IO 区域存在,将设置这些标志
IORESOURCE_PREFETCH
IORESOURCE_READONLY
表明区域是否为可预取的或是写保护的

PCI 中断

很容易就可以使用 PCI 中断。在 Linux 引导阶段,计算机固件(或内核)已经为设备分配了一个唯一的中断号,驱动程序只需要使用就好了。中断号保存在寄存器 60(PCI_INTERRUPT_LINE)中,该寄存器为一个字节宽,这允许多达 256 个中断线。

驱动程序无需检测中断号,因为从 PCI_INTERRUPT_LINE 中找到的值肯定是正确的。

如果设备不支持中断,寄存器 61(PCI_INTERRUPT_PIN)是 0;否则为非 0。但是,驱动程序知道自己的设备是否是中断驱动的,所以,它无需读取该寄存器。

PCI 设备驱动框架

最后,以一个 PCI 设备驱动的框架结束本文:

模块入口与出口

此部分注册和注销 PCI 驱动。

1
2
3
4
5
6
7
8
9
10
11
12
static int __init pci_init(void)
{
return pci_register_driver(&pci_drv);
}

static void __exit pci_exit(void)
{
pci_unregister_driver(&pci_drv);
}

module_init(pci_init);
module_exit(pci_exit);

PCI 驱动对象

1
2
3
4
5
6
static struct pci_driver pci_drv = {
.name = MODULE_NAME,
.id_table = ids,
.probe = pci_probe,
.remove = pci_remove,
};

PCI 设备匹配表

1
2
3
4
5
6
7
8
static struct pci_device_id ids[] = {
{
.vendor = PCI_VENDOR_ID_QEMU,
.device = PCI_DEVICE_ID_QEMU,
},
{ 0 },
};
MODULE_DEVICE_TABLE(pci, ids);

探测函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int pci_probe(struct pci_dev *pdev, const struct pci_device_id *ids){
int retval = 0;

pr_debug("New pci device probing\n");

retval = pci_enable_device(pdev);
if (retval < 0) {
dev_err(&pdev->dev, "can't enable PCI device\n");
return -ENODEV;
}

pci_read_config_byte(pdev, PCI_REVISION_ID ,&pci_prv.reversion);
pr_debug("Revision: %d\n", pci_prv.reversion);

pci_read_config_byte(pdev, PCI_INTERRUPT_LINE ,&pci_prv.int_line);
pr_debug("Interrupt line: %d\n", pci_prv.int_line);

pci_read_config_byte(pdev, PCI_INTERRUPT_PIN ,&pci_prv.int_pin);
pr_debug("Interrupt pin: %d\n", pci_prv.int_pin);

retval = map_bars(pdev);
if (retval < 0) {
dev_err(pdev->dev, "can't map bars\n")
return retval;
}

return 0;
}

获取 BAR 空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int map_bars(struct pci_dev *pdev)
{
int retval = 0;
int i;

for (i = 0; i < PCI_BAR_NUMBER; ++i) {
retval = map_sigal_bar(pdev, i);
if (retval == 0) {
continue;
} else if (retval < 0)
return -EINVAL;
}

return retval;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int map_sigal_bar(struct pci_dev *pdev, int idx)
{
resource_size_t bar_start;
resource_size_t bar_len;
void *__iomem bar_map;

bar_start = pci_resource_start(pdev, idx);
bar_len = pci_resource_len(pdev, idx);

if (bar_len == 0)
return 0;


bar_map = pci_iomap(pdev, idx, bar_len);
pr_info("BAR%d at 0x%llx mapped at 0x%p, length=%llu(0x%llx)\n", idx, (u64)bar_start, \
bar_map, (u64)bar_len, (u64)bar_len);

pci_prv.bar_start[idx] = bar_map;
pci_prv.bar_len[idx] = bar_len;

return (int)bar_len;
}

完整模块代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pci.h>

#define macro

#define MODULE_NAME "pci_skel"
#define PCI_VENDOR_ID_QEMU 0x1234
#define PCI_DEVICE_ID_QEMU 0x7863

#define PCI_BAR_NUMBER 6

struct pci_private_t {
/* 1st 16 bytes */
uint16_t vendor_id;
uint16_t device_id;
uint16_t command_reg;
uint16_t state_reg;
uint8_t reversion;
uint32_t class : 24;
uint32_t cacheline : 8;
uint8_t delay_timer;
uint8_t header_type;
uint8_t BIST;

/* 2ed 16 bytes*/
void *__iomem bar_start[PCI_BAR_NUMBER];

/* 3nd 16 bytes*/
// uint32_t bar4;
// uint32_t bar5;
uint16_t card_bus;
uint8_t subsys_vendor;
uint8_t subsys_device;

/* 4th 16 bytes*/
uint16_t extend_rom;
uint32_t reserved[2];
uint8_t int_line;
uint8_t int_pin;
uint8_t min_gnd;
uint8_t max_lat;

/* 冗余信息 */
uint32_t bar_len[PCI_BAR_NUMBER];
} __packed;

static struct pci_private_t pci_prv;

static struct pci_device_id ids[] = {
{
.vendor = PCI_VENDOR_ID_QEMU,
.device = PCI_DEVICE_ID_QEMU,
},
{ 0 },
};
MODULE_DEVICE_TABLE(pci, ids);

static int map_sigal_bar(struct pci_dev *pdev, int idx)
{
resource_size_t bar_start;
resource_size_t bar_len;
void *__iomem bar_map;

bar_start = pci_resource_start(pdev, idx);
bar_len = pci_resource_len(pdev, idx);

if (bar_len == 0)
return 0;


bar_map = pci_iomap(pdev, idx, bar_len);
pr_info("BAR%d at 0x%llx mapped at 0x%p, length=%llu(0x%llx)\n", idx, (u64)bar_start, \
bar_map, (u64)bar_len, (u64)bar_len);

pci_prv.bar_start[idx] = bar_map;
pci_prv.bar_len[idx] = bar_len;

return (int)bar_len;
}

static int map_bars(struct pci_dev *pdev)
{
int retval = 0;
int i;

for (i = 0; i < PCI_BAR_NUMBER; ++i) {
retval = map_sigal_bar(pdev, i);
if (retval == 0) {
continue;
} else if (retval < 0)
return -EINVAL;
}

return retval;
}

static int pci_probe(struct pci_dev *pdev, const struct pci_device_id *ids){
int retval = 0;

pr_debug("New pci device probing\n");

retval = pci_enable_device(pdev);
if (retval < 0) {
dev_err(&pdev->dev, "can't enable PCI device\n");
return -ENODEV;
}

pci_read_config_byte(pdev, PCI_REVISION_ID ,&pci_prv.reversion);
pr_debug("Revision: %d\n", pci_prv.reversion);

pci_read_config_byte(pdev, PCI_INTERRUPT_LINE ,&pci_prv.int_line);
pr_debug("Interrupt line: %d\n", pci_prv.int_line);

pci_read_config_byte(pdev, PCI_INTERRUPT_PIN ,&pci_prv.int_pin);
pr_debug("Interrupt pin: %d\n", pci_prv.int_pin);

retval = map_bars(pdev);
if (retval < 0) {
dev_err(&pdev->dev, "can't map bars\n");
return retval;
}

return 0;
}

static void pci_remove(struct pci_dev *pdev){

}

static struct pci_driver pci_drv = {
.name = MODULE_NAME,
.id_table = ids,
.probe = pci_probe,
.remove = pci_remove,
};

static int __init pci_init(void)
{
return pci_register_driver(&pci_drv);
}

static void __exit pci_exit(void)
{
pci_unregister_driver(&pci_drv);
}

module_init(pci_init);
module_exit(pci_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Huashan Sun <huashan.sun@qq.com>");