往期内容
本文章相关专栏往期内容,PCI/PCIe子系统专栏:
- 嵌入式系统的内存访问和总线通信机制解析、PCI/PCIe引入
- 深入解析非桥PCI设备的访问和配置方法
- PCI桥设备的访问方法、软件角度讲解PCIe设备的硬件结构
- 深入解析PCIe设备事务层与配置过程
- PCIe的三种路由方式
- PCI驱动与AXI总线框架解析(RK3399)
- 深入解析PCIe地址空间与寄存器机制:从地址映射到TLP生成的完整流程
- PCIe_Host驱动分析_地址映射
Uart子系统专栏:
- 专栏地址:Uart子系统
- Linux内核早期打印机制与RS485通信技术
– 末片,有专栏内容观看顺序interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
– 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
- 往期内容
- 1.PCIe控制器的资源
- 2.回顾设备配置空间
- 2.1 设备信息
- 2.2 base address
- 3.设备扫描过程
- 3.1 构造pci_dev结构体 -- 核心
- 3.2 代码分析
- 1.分配pci_dev结构体
- 2.读取设备信息
- 3.读BAR
- 4.分配地址空间
分析的文件仍然是:pcie-rockchip.c
详情见上一篇:PCIe_Host驱动分析_地址映射-CSDN博客
1.PCIe控制器的资源
在上一篇文章中,讲了host驱动时如何去解析设备树获取得到设备树的资源:pcie控制器的寄存器地址、region0地址、IO/Memory的映射需要的pcie_addr_base和cpu_addr_base等资源,其中三类资源记录在链表中:
- 总线资源:就是总线号,从0到0x1f
- 内存资源:CPU地址基地址为0xfa000000,PCI地址基地址为0xfa000000,大小为0x1e00000
- IO资源:CPU地址基地址为0xfbe00000,PCI地址基地址为0xfbe00000,大小为0x100000
解析设备树时,把资源记录在这个链表里:
res链表中记录的资源最终会放到pci_bus->bridge->windows链表里,如下图记录:
2.回顾设备配置空间
本节内容参考:PCI_SPEV_V3_0.pdf
(资料下载)
使用PCI/PCIe的目的,就是为了简单地访问它:像读写内存一样读写PCI/PCIe设备。
提问:
- 使用哪些地址读写设备?
- 这些地址的范围有多大?
- 是像内存一样访问它,还是像IO一样访问它?
每个PCI/PCIe设备都有配置空间,就是一系列的寄存器,对于普通的设备,它的配置空间格式如下。
里面有: -----可以看深入解析非桥PCI设备的访问和配置方法_pcie 寄存器访问从wire 使用配置请求-CSDN博客
- 设备ID
- 厂家ID
- Class Code:哪类设备?存储设备?显示设备?等待
- 6个Base Address Register:
2.1 设备信息
介绍:
2.2 base address
BAR用于:
-
声明需要什么类型的空间:内存、IO、32位地址、64位地址?
-
声明需要的空间有多大
-
保存主控分配给它的PCI空间基地址
-
基址寄存器(BAR,Base Address Registers):配置空间中的基址寄存器(BARs)定义了PCIe设备的寄存器(或内存)映射。它指定了设备的内存空间或I/O空间在系统地址空间中的位置。这些寄存器address告诉操作系统设备的寄存器或数据缓冲区在哪个地址范围,从而可以进行内存映射I/O(MMIO)或端口映射I/O操作。
- 内存映射寄存器:通过配置空间中的BAR,设备的寄存器可以映射到系统的内存地址空间,主机可以通过对这些内存地址进行读写来操作设备的寄存器。
地址空间可以分为两类:内存(Memory)、IO:
- 对于内存,写入什么值读出就是什么值,可以提前读取
- 对于IO,它反应的是硬件当前的状态,每个时刻读到的值不一定相同
BAR的格式如下:
- 用于内存空间
- 用于IO空间:
在PCIe设备的配置空间中,基址寄存器(BAR,Base Address Register)用于指示设备在系统内存地址空间或I/O地址空间中所需的地址范围。这些寄存器告诉操作系统设备需要多少内存或I/O空间,但要知道它们申请的具体空间大小是有一个明确的机制的。
BAR的结构
- 每个BAR寄存器是32位或64位宽,存储设备的基地址和一些控制信息。配置空间中通常有6个BAR寄存器(BAR0到BAR5),但设备可以选择只使用其中的一部分。
- BAR寄存器中的低位被用来存储设备的控制位,而高位则用于设备申请的基地址。BAR寄存器可以指定设备是需要内存映射(Memory-mapped I/O)还是I/O映射(Port I/O)的地址空间。
BAR申请空间大小的机制
为了知道PCIe设备需要申请的内存或I/O空间的大小,操作系统或主机使用一个简单的探测机制:
-
写全1到BAR寄存器:操作系统在设备初始化阶段,会向每个BAR寄存器写入全1(
0xFFFFFFFF
)。这一步的作用是屏蔽基地址部分,只保留空间大小的相关位。 -
读取BAR寄存器的返回值:设备会根据它需要的内存或I/O空间大小,返回一组值。这些返回的值中会有低几位为固定的0(这些位用于对齐),而高位部分表明设备需要的地址空间的大小。
-
计算空间大小:返回的值并不是直接告诉你设备需要的空间大小,而是告诉你它的地址掩码。通过这一返回值,操作系统可以计算设备需要的空间大小。具体计算方法如下:
- 首先,取出返回值中的有效位(低位为0的位保留对齐要求,不参与大小计算)。
- 然后,将该值按位取反(bitwise NOT),并加1,得到设备实际需要的空间大小。例如,如果BAR返回的值为
0xFFFFFFF0
,则所需空间大小为(~0xFFFFFFF0 + 1)
,即16字节。
-
分配空间并写回BAR:操作系统根据返回的大小值,为设备分配一个合适的地址范围,然后将这个分配的基地址写入BAR寄存器中。低几位的0确保地址对齐,满足设备的对齐要求。
具体计算示例
假设某PCIe设备的一个BAR寄存器在写入0xFFFFFFFF
后返回了0xFFFFF000
。
-
解析返回值:
- 高位部分
0xFFFFF000
表示该设备需要的空间大小。因为设备返回的低12位是0,这意味着该设备需要的空间大小是4KB(2^12 字节)。
- 高位部分
-
计算大小:
- 使用公式:
大小 = ~0xFFFFF000 + 1
,即大小 = 0x00000FFF + 1 = 0x1000
,即4096字节(4KB)。
- 使用公式:
32位 vs. 64位 BAR
- 32位 BAR:如果BAR是32位的,返回的值就是设备所需的32位地址空间范围。如果设备需要更大的空间,则可能会使用多个BAR寄存器。
- 64位 BAR:有些PCIe设备可能需要更大的内存空间,在这种情况下,设备会使用连续的两个BAR寄存器,将它们组合成一个64位地址来表示设备所需的地址范围。系统通过检查BAR寄存器中的一个特殊标志(在32位BAR的下两位中),判断它是否为64位BAR。如果是,则会组合连续的两个BAR寄存器来构成64位地址空间。
内存 vs I/O 空间
BAR寄存器还可以指示设备请求的地址空间类型:
- 内存映射空间:如果BAR的类型字段指示设备需要内存映射地址空间,返回的大小值将告诉系统设备需要的内存大小,操作系统将其映射到系统的内存空间。
- I/O映射空间:如果BAR指示设备需要I/O空间,返回的值将告诉系统设备需要的I/O端口空间的大小,操作系统将其映射到I/O端口空间。
BAR寄存器用于指定PCIe设备的内存或I/O空间的基址以及所需的地址范围。
操作系统通过向BAR写入0xFFFFFFFF
,读取返回值来确定设备需要的空间大小。
通过位操作(取反和加1)计算出设备申请的内存或I/O空间的实际大小。
BAR寄存器既可以是32位也可以是64位,根据设备需求分配相应的地址空间。
这种机制使操作系统能够灵活分配设备所需的地址资源,同时确保设备按照其需要的地址对齐要求运行。
3.设备扫描过程
3.1 构造pci_dev结构体 – 核心
扫描PCIe总线,对每一个PCIe桥、PCIe设备,都构造出对应的pci_dev:
- 填充pci_dev的各项成员,比如VID、PID、Class等
- 分配地址空间、写入PCIe设备
/*
* The pci_dev structure is used to describe PCI devices.
*/
struct pci_dev {
struct list_head bus_list; /* node in per-bus list */
struct pci_bus *bus; /* bus this device is on */
struct pci_bus *subordinate; /* bus this device bridges to */
void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */
struct pci_slot *slot; /* Physical slot this device is in */
unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
u8 revision; /* PCI revision, low byte of class word */
u8 hdr_type; /* PCI header type (`multi' flag masked out) */
........省略.........
struct pci_driver *driver; /* which driver has allocated this device */
u64 dma_mask; /* Mask of the bits of bus address this
device implements. Normally this is
0xffffffff. You only need to change
this if your device has broken DMA
or supports 64-bit transfers. */
........省略.........
unsigned int irq;
struct cpumask *irq_affinity;
struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
}
struct pci_dev
是 Linux 内核中用于描述 PCI 设备的核心数据结构之一。它包含了一个 PCI 设备的所有必要信息,内核通过这个结构体来管理和操控 PCI 设备。
1. PCI 设备的总线和系统信息
struct list_head bus_list;
- 设备所在总线链表中的节点,便于将设备组织到一个链表中,方便总线上的设备管理。
struct pci_bus *bus;
- 设备所在的 PCI 总线对象的指针,通过这个指针可以找到设备所属的总线。
struct pci_bus *subordinate;
- 如果这个设备是一个 PCI 桥接设备(Bridge),则这个字段指向它所连接的子总线。通过这个字段可以访问子总线的设备。
void *sysdata;
- 系统特定的扩展字段,允许特定架构或平台存储自定义的系统数据。
struct proc_dir_entry *procent;
- 设备在
/proc/bus/pci
中的条目,用于用户态通过/proc
文件系统访问 PCI 设备的信息。
struct pci_slot *slot;
- 指向 PCI 插槽的指针,表示设备所在的物理插槽位置。
2. 设备标识信息
unsigned int devfn;
- 编码后的设备和功能号,设备编号和功能编号被组合成一个值,遵循 PCI 规范中的设备和功能编号编码规则。
unsigned short vendor;
unsigned short device;
- 设备的厂商 ID 和设备 ID,分别用于标识设备制造商和设备本身的型号。
unsigned short subsystem_vendor;
unsigned short subsystem_device;
- 子系统的厂商 ID 和子系统设备 ID,这两个字段通常在服务器和嵌入式设备中用于标识子系统设备。
unsigned int class;
- 设备的类别码,用来描述设备的功能。它由基础类、子类和编程接口组成,分别表示设备的大类、小类和具体接口。
u8 revision;
- PCI 设备的修订版本号,表示该设备的硬件版本。
u8 hdr_type;
- PCI 头部类型,表示该设备的配置空间头部类型。还可以用来标记设备是否为多功能设备。
3. 设备中断相关
unsigned int irq;
- 设备使用的中断号,用于表示设备分配的中断线。
struct cpumask *irq_affinity;
- 中断亲和性掩码,表示设备的中断关联到哪个 CPU 上处理。多核系统中可以为设备的中断设置特定的 CPU 来处理。
4. 设备资源信息
struct resource resource[DEVICE_COUNT_RESOURCE];
struct resource { // ioport.h
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
unsigned long desc;
struct resource *parent, *sibling, *child;
};
1. resource_size_t start
● 资源的起始地址,表示该资源在 CPU 物理地址空间中的起始位置。
● 对于内存资源,这是内存区域的起始地址;对于 I/O 资源,这是 I/O 端口的起始地址。
2. resource_size_t end
● 资源的结束地址,表示该资源在 CPU 物理地址空间中的结束位置。
● start 和 end 通常表示该资源所占用的连续地址范围。
3. const char *name
● 资源的名字,可以是一个描述性字符串,用于标识该资源。它有助于调试和追踪系统中的资源分配情况。
4. unsigned long flags
● 资源的属性标志,使用一系列标志位来描述资源的类型和特性。常见的标志包括:
○ IORESOURCE_MEM:表示这是一个内存资源。
○ IORESOURCE_IO:表示这是一个 I/O 端口资源。
○ IORESOURCE_IRQ:表示这是一个中断资源。
○ IORESOURCE_BUSY:表示该资源已被分配并正在使用。
5. unsigned long desc
● 资源的描述字段,通常用于保存有关资源的其他具体信息。该字段具体如何使用取决于资源类型和实现需求。
6. struct resource *parent
● 指向父资源的指针,用于构建资源的层次结构。例如,一个 PCI 设备可能是一个更大内存资源的子资源。
7. struct resource *sibling
● 指向同一级别其他资源的指针,用于表示当前资源在父资源下的兄弟资源。
8. struct resource *child
● 指向子资源的指针,用于构建资源的层次结构。如果一个资源被分配给多个子资源(比如不同的内存区域),这些子资源通过这个字段来进行关联。
-
设备的资源描述符数组,包含了设备的 I/O 端口、内存映射地址和扩展 ROM 等资源。
DEVICE_COUNT_RESOURCE
是该设备能使用的最大资源数量,通常包括 I/O 空间、内存空间和扩展 ROM。 -
这个结构体广泛用于内核中,尤其是在设备驱动程序和系统资源管理中。对于 PCI 设备、内存管理或 I/O 端口操作来说,
resource
结构体可以描述某个资源在系统中占用的地址范围。 -
CPU 视角的物理地址:
start
和end
描述的是从 CPU 角度看到的物理地址空间(cpu_addr_phycical)。这些地址并不能直接通过指针访问。如果要从内核代码中访问这些地址,需要通过ioremap
将物理地址映射到内核的虚拟地址空间中。 -
示例:
- 当你想访问某个 PCI 设备的寄存器时,通常你会首先获取设备的资源信息,并用
ioremap
将资源的物理地址映射到虚拟地址空间。 structresource *res = pci_resource_start(pdev, BAR0); // 获取 PCI 设备 BAR0 的资源
void __iomem *mapped_addr = ioremap(res->start, resource_size(res));
- 在这段代码中,
pci_resource_start
会返回设备的 BAR0 基地址,ioremap
会将这个物理地址映射到虚拟地址空间,之后你可以通过mapped_addr
访问设备的寄存器。
- 当你想访问某个 PCI 设备的寄存器时,通常你会首先获取设备的资源信息,并用
3.2 代码分析
要找到这4个核心代码:
- 分配pci_dev
- 读取PCIe设备的配置空间,填充pci_dev中的设备信息
- 根据PCIe设备的BAR,得知它想申请什么类型的地址、多大?
- 分配地址,写入BAR
关键代码分为两部分:
- 读信息、得知PCIe设备想申请多大的空间
rockchip_pcie_probe
bus = pci_scan_root_bus(&pdev->dev, 0, &rockchip_pcie_ops, rockchip, &res); // \pci\probe.c
pci_scan_root_bus_msi // \pci\probe.c
pci_scan_child_bus // \pci\probe.c
pci_scan_slot // \pci\probe.c
dev = pci_scan_single_device(bus, devfn); // \pci\probe.c
dev = pci_scan_device(bus, devfn); //
struct pci_dev *dev;
dev = pci_alloc_dev(bus);
pci_setup_device
pci_read_bases(dev, 6, PCI_ROM_ADDRESS);
pci_device_add(dev, bus);
📎probe.c
- 分配空间
rockchip_pcie_probe
pci_bus_size_bridges(bus); // \pci\setup_bus.c
pci_bus_assign_resources(bus); // \pci\setup_bus.c
__pci_bus_assign_resources
pbus_assign_resources_sorted
/* pci_dev->resource[]里记录有想申请的资源的大小,
* 把这些资源按对齐的要求排序
* 比如资源A要求1K地址对齐,资源B要求32地址对齐
* 那么资源A排在资源B前面, 优先分配资源A
*/
list_for_each_entry(dev, &bus->devices, bus_list)
__dev_sort_resources(dev, &head);
// 分配资源
__assign_resources_sorted
assign_requested_resources_sorted(head, &local_fail_head);
📎setup-bus.c
1.分配pci_dev结构体
1. **rockchip_pcie_probe**
该函数是 Rockchip PCIe 驱动的入口函数。通常会初始化 PCIe 控制器硬件,并调用 PCI 核心框架来进行 PCI 总线扫描。关键调用是:
bus = pci_scan_root_bus(&pdev->dev, 0, &rockchip_pcie_ops, rockchip, &res);
pci_scan_root_bus
是用于扫描 PCI 根总线的函数。pdev->dev
是设备结构体,0
是总线号,rockchip_pcie_ops
是与该硬件相关的操作集合,rockchip
是设备私有数据,res
是与总线相关的资源信息。- 此函数返回根总线的
pci_bus
结构体指针,表示 PCI 总线被成功扫描。
2. **pci_scan_root_bus**
pci_scan_root_bus
是 PCI 核心框架中的函数,负责扫描和初始化根总线设备。
pci_scan_root_bus_msi()
此处调用了 pci_scan_root_bus_msi
来支持 MSI (Message Signaled Interrupts)。该函数将进一步调用 pci_scan_child_bus
。
3. **pci_scan_child_bus**
pci_scan_child_bus
是扫描子总线的核心函数。
pci_scan_slot()
- 在这个函数中,它将扫描每个插槽。每个插槽可以对应多个设备(多功能设备)。
4. **pci_scan_slot**
pci_scan_slot
函数负责扫描某个插槽内的所有功能(function),并检查是否有有效的 PCI 设备存在。
pci_scan_single_device(bus, devfn)
- 对于插槽的每个设备功能,调用
pci_scan_single_device
函数,试图扫描设备。
5. **pci_scan_single_device**
pci_scan_single_device
尝试扫描单个 PCI 设备。如果成功,会调用 pci_scan_device
来完成设备的初始化。
dev = pci_scan_device(bus, devfn);
- 这一步实际执行设备的扫描。它通过设备函数号
devfn
来识别特定设备,执行后续设备初始化。
6. **pci_scan_device**
pci_scan_device
是实际初始化 PCI 设备的核心函数。此时,PCI 总线已经检测到设备存在。接下来要做的就是分配和设置 pci_dev
结构体。
struct pci_dev *dev;
dev = pci_alloc_dev(bus);
pci_alloc_dev
分配并初始化pci_dev
结构体,该结构体用于表示一个 PCI 设备。它将设备的信息与总线关联,并初始化一些基本字段。
pci_setup_device(dev);
pci_setup_device
是初始化 PCI 设备的核心函数,读取设备配置空间并设置设备的相关信息。- 此函数会读取 PCI 配置空间的各种寄存器,包括
PCI_VENDOR_ID
和PCI_DEVICE_ID
,并根据这些信息填充pci_dev
结构体。
7. **pci_read_bases(dev, 6, PCI_ROM_ADDRESS)**
在 pci_setup_device
函数中调用 pci_read_bases
,用于读取 PCI 设备的基地址寄存器(BAR),从而知道设备的 I/O 地址和内存映射地址。通常,PCI 设备有多个 BAR(最多 6 个),每个 BAR 用于映射设备的资源。
pci_read_bases(dev, 6, PCI_ROM_ADDRESS);
pci_read_bases
用于读取 PCI 设备的基地址寄存器(Base Address Registers, BARs),并将设备的 I/O 或内存资源注册到内核中。PCI_ROM_ADDRESS
是设备可能包含的扩展 ROM 地址。
8. **pci_device_add(dev, bus)**
pci_device_add(dev, bus);
- 当设备初始化完成后,调用
pci_device_add
将设备添加到 PCI 总线设备列表中,并注册到内核设备模型中。这一步实际上让设备进入内核设备框架,使得后续可以绑定驱动并进行管理。
2.读取设备信息
3.读BAR
pci_read_bases
函数会调用__pci_read_base
,__pci_read_base
只是读BAR,算出想申请的空间的大小:
-
读BAR,保留原值
-
写0xFFFFFFFF到BAR
-
在读出来,解析出所需要的地址空间大小,记录在pci_dev->resource[ ]里
- pci_dev->resource[ ].start = 0;
- pci_dev->resource[ ].end = size - 1;
4.分配地址空间
-
从哪里分配得到地址空间?
- 在设备树里指明了CPU地址、PCI地址的对应关系,这些作为"资源"记录在pci_bus里
- 读BAR时,在pci_dev->resource[]里记录了它想申请空间的大小
-
分配得到的基地址,要写入BAR
代码调用关系如下:
- 把要申请的资源, 按照对齐要求排序,然后调用assign_requested_resources_sorted,代码如下:
/* 把要申请的资源, 按照对齐要求排序
* 然后调用assign_requested_resources_sorted
*/
rockchip_pcie_probe
pci_bus_size_bridges(bus);
pci_bus_assign_resources(bus);
__pci_bus_assign_resources
pbus_assign_resources_sorted
/* pci_dev->resource[]里记录有想申请的资源的大小,
* 把这些资源按对齐的要求排序
* 比如资源A要求1K地址对齐,资源B要求32地址对齐
* 那么资源A排在资源B前面, 优先分配资源A
*/
list_for_each_entry(dev, &bus->devices, bus_list)
__dev_sort_resources(dev, &head);
// 分配资源
__assign_resources_sorted
assign_requested_resources_sorted(head, &local_fail_head);
-
assign_requested_resources_sorted
函数做两件事- 分配地址空间
- 把这块空间对应的PCI地址写入PCIe设备的BAR
- 代码如下:
assign_requested_resources_sorted(head, &local_fail_head);
pci_assign_resource
ret = _pci_assign_resource(dev, resno, size, align);
// 分配地址空间
__pci_assign_resource
pci_bus_alloc_resource
pci_bus_alloc_from_region
/* Ok, try it out.. */
ret = allocate_resource(r, res, size, ...);
err = find_resource(root, new, size,...);
__find_resource
// 从资源链表中分配地址空间
// 设置pci_dev->resource[]
new->start = alloc.start;
new->end = alloc.end;
// 把对应的PCI地址写入BAR
pci_update_resource(dev, resno);
pci_std_update_resource
/* 把CPU地址转换为PCI地址: PCI地址 = CPU地址 - offset
* 写入BAR
*/
pcibios_resource_to_bus(dev->bus, ®ion, res);
new = region.start;
reg = PCI_BASE_ADDRESS_0 + 4 * resno;
pci_write_config_dword(dev, reg, new);
在 rockchip_pcie_probe
函数中,PCIe 控制器探测和初始化过程中,有一个关键步骤是为 PCIe 设备分配资源并将这些资源的基地址写入 PCI 配置空间中的基地址寄存器(BAR)。下面是对上述调用关系的进一步理解:
1. **pci_bus_size_bridges**
& **pci_bus_assign_resources**
-
pci_bus_size_bridges(bus)
用于计算 PCIe 总线上设备的资源需求。 -
pci_bus_assign_resources(bus)
是为设备分配资源的函数。 -
- 主要调用了
__pci_bus_assign_resources
,进一步处理资源分配和对齐操作。
- 主要调用了
2. **__pci_bus_assign_resources**
-
__pci_bus_assign_resources
调用了pbus_assign_resources_sorted
来对资源进行排序和分配。设备的pci_dev->resource[]
中保存了该设备申请的资源,接下来会根据资源的对齐要求来排序。- 对齐要求是指设备的资源地址必须满足一定的对齐标准。例如,某些设备可能需要 1K 对齐,而另一些设备可能只需要 32 字节对齐。对齐要求高的资源优先分配,以确保资源的合理使用。
-
_dev_sort_resources
- 资源会按对齐要求排序,比如 1K 对齐的资源优先于 32 字节对齐的资源。
- 排序后,调用
__assign_resources_sorted
来为这些设备分配资源。
3. **assign_requested_resources_sorted**
该函数执行两个关键任务:
- 分配地址空间。
- 将分配的 PCI 地址写入 PCI 设备的 BAR。
它调用了 pci_assign_resource
来完成这些任务。
4. **pci_assign_resource**
分配地址空间:pci_assign_resource
负责实际的资源分配,它调用 _pci_assign_resource
函数,后者进一步调用 __pci_assign_resource
。
__pci_assign_resource
会计算和分配地址空间,最后通过allocate_resource
函数从资源链表中分配地址。
allocate_resource(r, res, size, ...);
-
allocate_resource
尝试从给定的资源池中分配符合要求的内存或 I/O 地址空间,并将地址信息更新到pci_dev->resource[]
中。res->start
和res->end
是为资源分配的物理地址范围。
5. **pci_update_resource**
-
地址空间分配完成后,调用
pci_update_resource
函数将这些资源的基地址写入 PCI 配置空间中的 BAR。 -
其中,
pci_std_update_resource
是具体的写入 BAR 的函数。 -
pci_std_update_resource
-
它首先将分配给设备的 CPU 地址 转换为 PCI 地址。由于 CPU 地址和 PCI 地址通常不一致,需根据系统架构中的偏移量(
offset
)进行转换。- PCI 地址 = CPU 地址 -
offset
- PCI 地址 = CPU 地址 -
-
-
pcibios_resource_to_bus
pcibios_resource_to_bus
负责将设备的物理地址(CPU 地址)转换为 PCI 总线地址(PCI 地址)。转换后的地址会保存在region.start
中。
-
pci_write_config_dword(dev, reg, new)
- 最终通过
pci_write_config_dword
将 PCI 地址写入 PCI 设备的配置空间中对应的 BAR 寄存器。BAR 地址从PCI_BASE_ADDRESS_0
开始,每个 BAR 占用 4 个字节的配置空间。 reg
是 BAR 的寄存器地址(从PCI_BASE_ADDRESS_0
开始),new
是要写入 BAR 的 PCI 地址。
- 最终通过
- 资源分配:通过
pci_bus_size_bridges
和pci_bus_assign_resources
,PCI 桥和设备资源的需求得到计算,并开始为设备分配 I/O 和内存资源。 - 对齐排序:在分配资源时,根据对齐要求对设备的资源请求进行排序,高对齐要求的资源优先分配。
- 地址空间分配:调用
pci_assign_resource
和相关函数从资源池中为设备分配地址空间,并更新pci_dev->resource[]
。 - BAR 写入:通过
pci_update_resource
和pci_write_config_dword
,将分配的 PCI 地址写入设备的 BAR 寄存器。
通过这个过程,PCIe 设备的资源得到了有效分配,BAR 寄存器被正确写入,从而确保设备能够正常访问系统的内存和 I/O 区域。这也是设备驱动程序与硬件交互的基础。