16

极术小姐姐 · 2023年06月16日

粗谈 linux PCI 驱动

0. 目录

1. 前言

2. PCI 总线族

3. PCI 设备类型

4. PCI 特性

5. 列举 PCI 设备

6. PCI 设备配置

  1. linux 驱动

    7.1 注册所支持的设备

    7.2 注册驱动

    7.3 驱动操作函数修饰

    7.4 设备初始化步骤

    7.5 设备使能

    7.6 访问配置寄存器

    7.7 设置 DMA mask size

    7.8 分配缓存一致性的 DMA buffers

    7.9 初始化设备寄存器

    7.10 注册中断处理函数

    7.11 PCI 设备关停

8. 有用的资源

1. 前言

本文基于一个讨论 linux PCI 驱动的 slides:https://bootlin.com/doc/legacy/pci-drivers/pci-drivers.pdf。slides 本身有点老,但里面的知识条理对于我这只菜鸡来说正合适。

因为看完就忘(不仅是忘了内容,甚至会忘了有此 slides 之存在),于是直接给它整理成文章。

本文基于 linux 2.6.27。

2. PCI 总线族

下面的总线都属于 PCI 族:

  • PCI:32 bit 总线,33 或 66 MHz。
  • MiniPCI:插槽更小,用于笔记本电脑。
  • CardBus:外部卡槽,用于笔记本电脑。
  • PIX Extended(PCI-X):比 PCI 插槽要宽,64 bit,但支持插入一个标准 PCI 卡。
  • PCI Express(PCIe or PCI-E):PCI 的当前代,用串行接口取代并行接口。
  • PCI Express Mini Card:应用于较新的笔记本,取代 MiniPCI。
  • Express Card:应用于较新的笔记本,取代 CardBus。

这些技术都是互相兼容的,内核驱动可以使用同一套。内核不需要感知硬件到底用的是什么插槽和总线变种。

3. PCI 设备类型

PCI 总线上的设备类型主要有:

  • 网卡(有线或无线)。
  • SCSI 适配器。
  • 总线控制器:USB、PCMCIA、I2C、FireWire、IDE。
  • 显卡。
  • 声卡。

4. PCI 特性

主要是针对设备驱动开发者。

  • boot 阶段 BIOS 或 linux(如果配置了的话)会自动分配设备的资源(I/O 地址,IRQ 中断线)。PCI/PCIe 的地址分配,参考本号《系统地址映射初始化:基于 PCI 的系统》、《系统地址映射初始化:基于 PCIe 的系统》。
  • 设备驱动只需要读取系统地址空间中的相应配置即可。
  • 大小端:PCI 设备的配置信息是小端的。写驱动的时候要注意(有些内核函数可以提供大小端转化)。

5. 列举 PCI 设备

  1. lspci:列举所有 PCI 设备。

image.png

  1. lspci -tv:列举 PCI 总线设备树。

image.png

  1. PCI 设备树与 /sys 中的数据结构对应

image.png

6. PCI 设备配置

  1. 每个 PCI 设备有一个 256 byte 地址空间长度的配置寄存器。
  2. 可以通过 lspci -x 查看设备的配置:

image.png

  1. 标准的 PCI 配置信息:
  • 偏移 0:Vendor ID。
  • 偏移 2:Device ID。
  • 偏移 10:Class ID(网卡、显卡、桥 ...)。
  • 偏移 16 - 39:Base Address Registers(BAR)0 到 5。
  • 偏移 44:SubVendor ID。
  • 偏移 46:SubDevice ID。
  • 偏移 64 及以上:设备制造商。

这些偏移的定义,在内核的 include/linux/pci\_regs.h。

7. linux 驱动

7.1 注册所支持的设备

drivers/net/ne2k-pci.c:

static struct pci_device_id ne2k_pci_tbl[] = {
  { 0x10ec, 0x8029, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_RealTek_RTL_8029 },
  { 0x1050, 0x0940, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_89C940 },
  { 0x11f6, 0x1401, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Compex_RL2000 },
  { 0x8e2e, 0x3000, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_KTI_ET32P2 },
  { 0x4a14, 0x5000, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_NetVin_NV5000SC },
  { 0x1106, 0x0926, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Via_86C926 },
  { 0x10bd, 0x0e34, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_SureCom_NE34 },
  { 0x1050, 0x5a5a, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_W89C940F },
  { 0x12c3, 0x0058, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Holtek_HT80232 },
  { 0x12c3, 0x5598, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Holtek_HT80229 },
  { 0x8c4a, 0x1980, PCI_ANY_ID, PCI_ANY_ID, 0, 0, CH_Winbond_89C940_8c4a },
  { 0, }
};
MODULE_DEVICE_TABLE(pci, ne2k_pci_tbl);

7.2 注册驱动

注册驱动的操作函数,以及驱动所支持的设备表:

static struct pci_driver ne2k_driver = {
  .name = DRV_NAME,
  .probe = ne2k_pci_init_one,
  .remove = __devexit_p(ne2k_pci_remove_one),
  .id_table = ne2k_pci_tbl,
#ifdef CONFIG_PM
  .suspend = ne2k_pci_suspend,
  .resume = ne2k_pci_resume,
#endif /* CONFIG_PM */
};

static int __init ne2k_pci_init(void)
{
  return pci_register_driver(&ne2k_driver);
}
static void __exit ne2k_pci_cleanup(void)
{
  pci_unregister_driver (&ne2k_driver);
}
  • 驱动的操作函数以及所支持的设备,会在模块加载时被载入。
  • 如果找到一个匹配的设备,PCI 框架代码会调用驱动的 probe() 函数。
  • 非常类似 USB 设备驱动!

7.3 驱动操作函数修饰

  • \_\_init:模块初始化函数。这些代码会在驱动初始化完之后被丢弃。
  • \_\_exit:模块退出函数。如果是静态编译(编到内核中)的驱动,会忽略之。
  • \_\_devinit:probe 函数以及所有初始化函数。如果使能了 CONFIG\_HOTPLUG 内核配置,则此类函数就是个正常的函数,否则等同 \_\_init。
  • \_\_devinitconst:用于设备 ID 表。
  • \_\_devexit:移除时会调用的函数。同 \_\_devinit。
  • 所有引用 \_\_devinit 函数地址的地方,都应该使用 \_\_devexit\_p(fun) 进行声明修饰。如果代码被丢弃的话,此修饰会将函数地址替换为 NULL。

示例:

static struct pci_driver ne2k_driver = {
  .name = DRV_NAME,
  .probe = ne2k_pci_init_one,
  .remove = __devexit_p(ne2k_pci_remove_one),
  .id_table = ne2k_pci_tbl,
  ...
};

7.4 设备初始化步骤

  • 使能设备。
  • 请求 I/O 端口以及 I/O 内存资源。
  • 设置 DMA mask size(coherent 及 streaming DMA 皆需要)。
  • 分配并初始化共享控制数据(pci\_allocate\_coherent())。
  • 初始化设备寄存器(如果需要的话)。
  • 注册 IRQ 处理函数(request\_irq())。
  • 注册进其他子系统(网络、显示、存储,等等)。
  • 使能 DMA 处理引擎。

7.5 设备使能

  1. 在访问设备寄存器之前,驱动需要先执行 pci\_enable\_device(),这会导致:
  • 如果设备在 suspend 状态,则唤醒之。
  • 分配设备的 I/O 和内存区域(如果 BIOS 没有搞定的话)。
  • 为设备分配一个 IRQ(如果 BIOS 没有搞定的话)。

pci\_enable\_device() 可能会失败,所以需要检查其返回值!

pci\_enable\_device(drivers/net/ne2k-pci.c)示例:


static int __devinit ne2k_pci_init_one
(struct pci_dev *pdev, const struct pci_device_id *ent)
{
  ...
  i = pci_enable_device (pdev);
  if (i)
    return i;
  ...
}
  1. 调用 pci\_set\_master() 使能 DMA,这会导致:
  • 通过设置 PCI\_COMMAND 寄存器中的 bus master bit 来使能 DMA。完成后设备便可在地址总线上扮演一个 master 的角色。
  • 如果 BIOS 设置了伪造的值,则修复延迟定时器的值(Fix the latency timer value if it's set to something bogus by the BIOS. 这句话没看明白)。
  1. 如果设备可以使用 PCI Memory-Write-Invalidate transaction(写整个 cache lines),你还可以调用 pci\_set\_mwi():
  • 此函数使能 Memory-Write-Invalidate 的 PCI\_COMMAND bit。
  • 此函数还确保对 cache line size 寄存器进行正确的设置。

7.6 访问配置寄存器

访问 I/O 内存和端口信息。

#include <linux/pci.h>

/* 读接口 */
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);

/* 读示例:drivers/net/cassini.c */
pci_read_config_word(cp­>pdev, PCI_STATUS, &cfg);

/* 写接口 */
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);

/* 写示例:drivers/net/s2io.c */
/* Clear "detected parity error" bit
pci_write_config_word(sp­>pdev, PCI_STATUS, 0x8000);
  • 每个 PCI 设备最多有 6 个 I/O 或内存区域,通过 BAR0 到 BAR5 来描述。
  • 访问 I/O 区域的基地址:
#include <linux/pci.h>
long iobase = pci_resource_start(pdev, bar);
  • 访问 I/O 区域的大小:

long iosize = pci_resource_len(pdev, bar);
  • 预留 I/O 区域:

request_region(iobase, iosize, “my driver”);

或者简单点:

pci_request_region(pdev, bar, “my driver”);

或者更简单点:

pci_request_regions(pdev, “my driver”);

示例代码(drivers/net/ne2k-pci.c):

ioaddr = pci_resource_start (pdev, 0);
irq = pdev->irq;

if (!ioaddr || ((pci_resource_flags (pdev, 0) & IORESOURCE_IO) == 0))
{
  dev_err(&pdev->dev, "no I/O resource at PCI BAR #0\n");
  return -ENODEV;
}

if (request_region (ioaddr, NE_IO_EXTENT, DRV_NAME) == NULL) {
  dev_err(&pdev->dev, "I/O resource 0x%x @ 0x%lx busy\n", NE_IO_EXTENT, ioaddr);
  return -EBUSY;
}

7.7 设置 DMA mask size

  • 对于拥有超过(或少于)(那不就是 whatever 么?) 32 bit 总线 master capability 的设备,对此设备调用 pci\_dma\_set\_mask() 声明之。
  • 特别是对于使用 64 bit DMA 的 PCI-X 和 PCIe 兼容设备来说,驱动必须调用此函数
  • 如果设备可以直接寻址系统 RAM 中高于 4G 物理地址的“缓存一致性内存”,则通过 pci\_set\_consistent\_dma\_mask() 注册之。

示例(drivers/net/wireless/ipw2200.c):

err = pci_set_dma_mask(pdev, DMA_32BIT_MASK);

if (!err)
  err = pci_set_consistent_dma_mask(pdev, DMA_32BIT_MASK);
if (err) {
  printk(KERN_WARNING DRV_NAME ": No suitable DMA available.\n");
  goto out_pci_disable_device;
}

7.8 分配缓存一致性的 DMA buffers

至此,已完成 DMA mask size 的分配。

  • 如果你准备使用缓存一致性的 buffers,则分配之。
  • 详情参考内核 Documentation/DMA-API.txt。

7.9 初始化设备寄存器

如果设备需要的话:

  • 设置一些 "capability" 域。
  • 进行一些 vendor specific 的初始化工作或复位。

示例:清除 pending 的中断。

7.10 注册中断处理函数

  1. 调用 request\_irq() 时需要传入 IRQF\_SHARED flag,因为 PCI 中断线是可共享的。
  2. 中断注册同时会使能中断,故在中断注册的时间点上,需要:
  • 确保设备已完成全部初始化工作并准备好处理中断。
  • 确保设备在调用 request\_irq() 之前没有 pending 的中断。
  1. 实际调用 request\_irq() 的地方取决于设备类型,以及其所属的子系统(比如网络、显示、存储 ...)。
  2. 驱动随后被注册进其所属的子系统。

7.11 PCI 设备关停

在 remove() 函数中,你通常需要对在设备初始化(probe() 函数)中所做工作进行逆操作:

  1. 禁能设备产生中断:如果你不禁能设备中断的话,系统可能会收到 spurious 中断,并最终禁用掉整个中断线。该中断线上的其他设备就遭殃了!
  2. 释放 IRQ。
  3. 停止所有 DMA 活动:需要在关闭 IRQs 之后再做(原 slides 这句话后面还有一个注:could start new DMAs,这句话没看明白)。
  4. 释放 DMA buffers:先释放 streaming buffers,然后再释放 consistent buffers。
  5. 从其他子系统中取消注册。
  6. 通过 io\_unmap() 取消映射 I/O 内存及端口。
  7. 通过 pci\_disable\_device() 关闭设备。
  8. 取消注册 I/O 内存和端口:如果这一步不做的话,会无法重新加载驱动。

8. 有用的资源

lspci:展示 PCI 总线与设备信息。

setpci:支持操作 PCI 设备配置寄存器。

作者: 戴胜冬
文章来源:窗有老梅

推荐阅读

欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式客栈专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
2882
内容数
267
分享一些在嵌入式应用开发方面的浅见,广交朋友
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息