这段时间学习了下Linux下PCI子系统和如何编写自己的PCI模块。
总的来说,PCI协议清晰还是容易理解的,Linux对PCI子系统的封装也比较好,几乎不用我们接触控制PCI底层。
PCI规范
PCI设备内部都有一个配置空间,大小为256字节的寄存器,其中配置空间头部64字节是PCI标准规定的,剩下的都是厂商自定义
这些都是描述信息,储存着PCI告诉操作系统的主要信息,比如PCI设备是Intel生产的,型号是XXXXX,需要100M的缓存空间。这些信息都是由Linux设备驱动模型中的驱动(Driver)进行处理,由驱动读取PCI配置空间内的信息,再向的Linux内核申请PCI需要的资源。
配置空间中的大部分信息通过名字就知道什么意思了,这里不深入解释了,后面分析驱动的时候用到了再说。
说一下Base Address Registers,Base Address Registers记录了设备所需要的地址空间的类型(memory space或者I/O space),基址以及其他属性。Base Address Registers的格式如下:
这里又不得不提一下memory space或者I/O space,我并不是很了解X86架构,但是在ARM芯片的手册上,我们可以发现内存,以及外设全都位于总线上,共享分割总线的地址。
比如:0xff000000-0x00000000是控制(外设)GPIO的,剩余的是RAM的地址,这就使得本就总线地址空间少的arm,可用RAM更加少了,例如:32位最多4GB的寻址空间,外设占用了2GB,RAM只剩下2GB可用。但是在X86上,外设地址和RAM的地址是独立,也就是说64位的X86CPU,RAM可用总线就是64根,外设可用总线也是64根(打比方而已,实际上外设用不到64根),不存在说要像arm里一样64位要划分32根总线给RAM,32根总线给外设。
RAM对应的就是memory space。
而外设地址就是I/O space。
驱动分析
我这里以Linux内核中8250_pci.c和r8169_main.c文件为例子
分析一下Linux PCI模块的流程
8250_pci.c是PCI转串口的驱动
直接翻到最下面看probe
这是经典的Linux注册驱动的函数,里面最重要的是pciserial_init_one和serial_pci_tbl,分别对应8250_pci PCI驱动的匹配过程以及匹配对象。
static const struct pci_error_handlers serial8250_err_handler = {
.error_detected = serial8250_io_error_detected,
.slot_reset = serial8250_io_slot_reset,
.resume = serial8250_io_resume,
};
static struct pci_driver serial_pci_driver = {
.name = "serial",
.probe = pciserial_init_one,
.remove = pciserial_remove_one,
.driver = {
.pm = &pciserial_pm_ops,
},
.id_table = serial_pci_tbl,
.err_handler = &serial8250_err_handler,
};
module_pci_driver(serial_pci_driver);
先看serial_pci_tbl
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 */
};
#define PCI_VENDOR_ID_ADVANTECH 0x13fe
#define PCI_DEVICE_ID_ADVANTECH_PCI3620 0x3620
static const struct pci_device_id serial_pci_tbl[] = {
/* Advantech use PCI_DEVICE_ID_ADVANTECH_PCI3620 (0x3620) as 'PCI_SUBVENDOR_ID' */
{ PCI_VENDOR_ID_ADVANTECH, PCI_DEVICE_ID_ADVANTECH_PCI3620,
PCI_DEVICE_ID_ADVANTECH_PCI3620, 0x0001, 0, 0,
pbn_b2_8_921600 }
}
我们结合之前给出的256字节寄存器的配置空间,可以发现serial_pci_tbl里是配置空间的信息,现在知道为什么Linux用pci_device_id配对了吧。
接着我们看probe函数,不必过多深入源码,了解函数作用适可而止即可。
我们先看probe函数的形参
(struct pcdevicei_dev *dev, const struct pci_device_id *ent)
第一个是经典的dev,这个不解释了,看第二个pci_device_id,是不是很熟悉,我们猜测 这个地方就储存着驱动目标设备中配置空间的部分信息。
static int
pciserial_init_one(struct pcdevicei_dev *dev, const struct pci_device_id *ent)
{
pci_dev中包含PCI头信息,如:厂商的信息
struct pci_serial_quirk *quirk; //私有自定义
struct serial_private *priv; //私有自定义
const struct pciserial_board *board;//公开自定义
const struct pci_device_id *exclude;//一些PCI头信息,如:厂商的信息
struct pciserial_board tmp;//公开自定义
int rc;
quirk = find_quirk(dev);// 匹配各家厂商的PCI信息,比如Intel
if (quirk->probe) {
rc = quirk->probe(dev);
if (rc)
return rc;
}
if (ent->driver_data >= ARRAY_SIZE(pci_boards)) {//私有数据ent->driver_data
dev_err(&dev->dev, "invalid driver_data: %ld\n",
ent->driver_data);
return -EINVAL;
}
board = &pci_boards[ent->driver_data];
exclude = pci_match_id(blacklist, dev); //return NULL
if (exclude)
return -ENODEV;
rc = pcim_enable_device(dev); //启用PCI
pci_save_state(dev);
if (rc)
return rc;
/*
if (ent->driver_data == pbn_default) {
* Use a copy of the pci_board entry for this;
* avoid changing entries in the table.
*/
memcpy(&tmp, board, sizeof(struct pciserial_board));
board = &tmp;
/*
* We matched one of our class entries. Try to
* determine the parameters of this board.
*/
rc = serial_pci_guess_board(dev, &tmp);
if (rc)
return rc;
} else {
/*
* We matched an explicit entry. If we are able to
* detect this boards settings with our heuristic,
* then we no longer need this entry.
*/
memcpy(&tmp, &pci_boards[pbn_default],
sizeof(struct pciserial_board));
rc = serial_pci_guess_board(dev, &tmp);
if (rc == 0 && serial_pci_matches(board, &tmp))
moan_device("Redundant entry in serial pci_table.",
dev);
}
priv = pciserial_init_ports(dev, board);
if (IS_ERR(priv))
return PTR_ERR(priv);
pci_set_drvdata(dev, priv);
return 0;
}
看一下find_quirk()
static inline int quirk_id_matches(u32 quirk_id, u32 dev_id)
{
return quirk_id == PCI_ANY_ID || quirk_id == dev_id;
}
static struct pci_serial_quirk *find_quirk(struct pci_dev *dev)// 匹配各家厂商的PCI信息
{
struct pci_serial_quirk *quirk;
for (quirk = pci_serial_quirks; ; quirk++)
if (quirk_id_matches(quirk->vendor, dev->vendor) &&
quirk_id_matches(quirk->device, dev->device) &&
quirk_id_matches(quirk->subvendor, dev->subsystem_vendor) &&
quirk_id_matches(quirk->subdevice, dev->subsystem_device))
break;
return quirk;
}
可以看出,dev里也存有PCI设备的配置空间信息。
find_quirk从dev中获取信息并且进行匹配,对应特殊的设备还会进行初始化设定。
pcim_enable_device()启用PCI设备。
在pciserial_init_one()末尾有一serial_pci_guess_board()函数
static int
serial_pci_guess_board(struct pci_dev *dev, struct pciserial_board *board)
{
int num_iomem, num_port, first_port = -1, i;
int rc;
rc = serial_pci_is_class_communication(dev);
if (rc)
return rc;
/*
* Should we try to make guesses for multiport serial devices later?
*/
if ((dev->class >> 8) == PCI_CLASS_COMMUNICATION_MULTISERIAL)
return -ENODEV;
num_iomem = num_port = 0;
for (i = 0; i < PCI_NUM_BAR_RESOURCES; i++) {
if (pci_resource_flags(dev, i) & IORESOURCE_IO) {
num_port++;
if (first_port == -1)
first_port = i;
}
if (pci_resource_flags(dev, i) & IORESOURCE_MEM)
num_iomem++;
}
/*
* If there is 1 or 0 iomem regions, and exactly one port,
* use it. We guess the number of ports based on the IO
* region size.
*/
if (num_iomem <= 1 && num_port == 1) {
board->flags = first_port;
board->num_ports = pci_resource_len(dev, first_port) / 8;
return 0;
}
/*
* Now guess if we've got a board which indexes by BARs.
* Each IO BAR should be 8 bytes, and they should follow
* consecutively.
*/
first_port = -1;
num_port = 0;
for (i = 0; i < PCI_NUM_BAR_RESOURCES; i++) {
if (pci_resource_flags(dev, i) & IORESOURCE_IO &&
pci_resource_len(dev, i) == 8 &&
(first_port == -1 || (first_port + num_port) == i)) {
num_port++;
if (first_port == -1)
first_port = i;
}
}
if (num_port > 1) {
board->flags = first_port | FL_BASE_BARS;
board->num_ports = num_port;
return 0;
}
return -ENODEV;
}
可以看出该函数的作用是获取Base Address Registers里的IORESOURCE_IO以及IORESOURCE_MEM,分别对应I/O space,memory space。
接着在pciserial_init_one()的函数中会调用注册串口的函数。
总结一下8250_pci模块的逻辑
- 8250_pci.c中用数组储存PCI的部分配置空间的信息,如:厂商,型号。以及各自的初始化回调函数,实现一个驱动支持不同型号的设备
- module_pci_driver(serial_pci_driver)向PCI子系统进行驱动注册,注册内容中包含匹配设备后进行初始化的probe函数,以及目标设备的部分配置空间的信息,如:厂商:Intel型号:8250。这样Linux会在发现PCI中含有厂商Intel,型号为8250的设备后,执行我们的probe函数。
- 在8250_pci.c中,通过probe中的形参dev进一步对配置空间的信息进行确定,以及提取出配置空间中Base Address Registers的信息。
- 将上一步提取的配置空间中Base Address Registers信息,注册进串口子系统,将重点放在串口子系统上。
PCI驱动注册全程没有接触到PCI底层,反而重点在串口子系统的注册上