Read the fucking source code!
--By 鲁迅
A picture is worth a thousand words.
--By 高尔基
说明:
从本文开始,将会针对PCIe专题来展开,涉及的内容包括:
不排除会包含PCIe外设驱动模块,一切随缘。
作为专题的第一篇,当然会先从硬件总线入手。进入主题前,先讲点背景知识。在PC时代,随着处理器的发展,经历了几代I/O总线的发展,解决的问题都是CPU主频提升与外部设备访问速度的问题:
ISA
、
EISA
、
VESA
和
Micro Channel
等;
PCI
、
AGP
、
PCI-X
等;
PCIe
、
mPCIe
、
m.2
等;
PCIe(PCI Express)
是目前PC和嵌入式系统中最常用的高速总线,PCIe在PCI的基础上发展而来,在软件上PCIe与PCI是后向兼容的,PCI的系统软件可以用在PCIe系统中。
本文会分两部分展开,先介绍PCI总线,然后再介绍PCIe总线,方便在理解上的过渡,开始旅程吧。
PCI总线(Peripheral Component Interconnect,外部设备互联)
,由Intel公司提出,其主要功能是连接外部设备;
PCI Local Bus
,PCI局部总线,局部总线技术是PC体系结构发展的一次变革,是在
ISA总线
和
CPU总线
之间增加的一级总线或管理层,可将一些高速外设,如图形卡、硬盘控制器等从
ISA总线
上卸下,而通过局部总线直接挂接在CPU总线上,使之与高速
CPU总线
相匹配。PCI总线,指的就是
PCI Local Bus
。
先来看一下PCI Local Bus的系统架构图:
从图中看,与PCI总线相关的模块包括:
Host Bridge
,比如PC中常见的North Bridge(北桥)。
图中处理器、Cache、内存子系统通过Host Bridge连接到PCI上,Host Bridge管理PCI总线域,是联系处理器和PCI设备的桥梁,完成处理器与PCI设备间的数据交换。其中数据交换,包含处理器访问PCI设备的地址空间
和PCI设备使用DMA机制访问主存储器
,在PCI设备用DMA访问存储器时,会存在Cache一致性问题,这个也是Host Bridge设计时需要考虑的;此外,Host Bridge还可选的支持仲裁机制,热插拔等;
PCI Local Bus;
PCI总线,由Host Bridge或者PCI-to-PCI Bridge管理,用来连接各类设备,比如声卡、网卡、IDE接口等。可以通过PCI-to-PCI Bridge来扩展PCI总线,并构成多级总线的总线树,比如图中的PCI Local Bus #0
和PCI Local Bus #1
两条PCI总线就构成一颗总线树,同属一个总线域;
PCI-To-PCI Bridge
;PCI桥
,用于扩展PCI总线,使采用PCI总线进行大规模系统互联成为可能,管理下游总线,并转发上下游总线之间的事务;
PCI Device
;PCI总线中有三类设备:PCI从设备,PCI主设备,桥设备。PCI从设备:被动接收来自Host Bridge或者其他PCI设备的读写请求;PCI主设备:可以通过总线仲裁获得PCI总线的使用权,主动向其他PCI设备或主存储器发起读写请求;桥设备:管理下游的PCI总线,并转发上下游总线之间的总线事务,包括PCI桥
、PCI-to-ISA桥
、PCI-to-Cardbus桥
等。
PCI总线是一条共享总线,可以挂接多个PCI设备,PCI设备通过一系列信号与PCI总线相连,包括:地址/数据信号、接口控制信号、仲裁信号、中断信号等。如下图:
AD[31:00]
:地址与数据信号复用,在传送时第一个时钟周期传送地址,下一个时钟周期传送数据;
C/BE[3:0]#
:PCI总线命令与字节使能信号复用,在地址周期中表示的是PCI总线命令,在数据周期中用于字节选择,可以进行单字节、字、双字访问;
PAR
:奇偶校验信号,确保
AD[31:00]
和
C/BE[3:0]#
传递的正确性;
Interface Control
:接口控制信号,主要作用是保证数据的正常传递,并根据PCI主从设备的状态,暂停、终止或者正常完成总线事务:
FRAME#
:表示PCI总线事务的开始与结束;
IRDY#
:信号由PCI主设备驱动,信号有效时表示PCI主设备数据已经ready;
TRDY#
:信号由目标设备驱动,信号有效时表示目标设备数据已经ready;
STOP#
:目标设备请求主设备停止当前总线事务;
DEVSEL#
:PCI总线的目标设备已经准备好;
IDSEL
:PCI总线在配置读写总线事务时,使用该信号选择PCI目标设备;
Arbitration
:仲裁信号,由
REQ#
和
GNT#
组成,与PCI总线的仲裁器直接相连,只有PCI主设备需要使用该组信号,每条PCI总线上都有一个总线仲裁器;
Error Reporting
:错误信号,包括
PERR#
奇偶校验错误和
SERR
系统错误;
System
:系统信号,包括时钟信号和复位信号;
看一下C/BE[3:0]
都有哪些命令吧:
PCI使用三种模型用于数据的传输:
Programmed I/O
:通过IO读写访问PCI设备空间;
DMA
:PIO的方式比较低效,DMA的方式可以直接去访问主存储器而无需CPU干预,效率更高;
Peer-to-peer
:两台PCI设备之间直接传送数据;
PCI体系架构支持三种地址空间:
memory空间
:针对32bit寻址,支持4G的地址空间,针对64bit寻址,支持16EB的地址空间;
I/O空间
PCI最大支持4G的IO空间,但受限于x86处理器的IO空间(16bits带宽),很多平台将PCI的IO地址空间限定在64KB;
配置空间
x86 CPU可以直接访问memory空间
和I/O空间
,而配置空间则不能直接访问;每个PCI功能最多可以有256字节的配置空间;PCI总线在进行配置的时候,采用ID译码方式,使用设备的ID号,包括Bus Number
,Device Number
,Function Number
和Register Number
,每个系统支持256条总线,每条总线支持32个设备,每个设备支持8个功能,由于每个功能最多有256字节的配置空间,因此总的配置空间大小为:256B * 8 * 32 * 256 = 16M;
有必要再进一步介绍一下配置空间:x86 CPU无法直接访问配置空间,通过IO映射的数据端口和地址端口间接访问PCI的配置空间,其中地址端口映射到0CF8h - 0CFBh
,数据端口映射到0CFCh - 0CFFh
;
那具体的配置空间寄存器都是什么样的呢?每个功能256Byte,前边64Byte是Header,剩余的192Byte支持可选功能。有种类型的PCI功能:Bridge和Device,两者的Header都不一样。
Bridge
Device
配置空间中有个寄存器字段需要说明一下:
Base Address Register
,也就是
BAR空间
,当PCI设备的配置空间被初始化后,该设备在PCI总线上就会拥有一个独立的PCI总线地址空间,这个空间就是
BAR空间
,
BAR空间
可以存放IO地址空间,也可以存放存储器地址空间。
先看一下PCIe架构的组成图:
Root Complex
:CPU和PCIe总线之间的接口可能会包含几个模块(处理器接口、DRAM接口等),甚至可能还会包含芯片,这个集合就称为
Root Complex
,它作为PCIe架构的根,代表CPU与系统其它部分进行交互。广义来说,
Root Complex
可以认为是CPU和PCIe拓扑之间的接口,
Root Complex
会将CPU的request转换成PCIe的4种不同的请求(Configuration、Memory、I/O、Message);
Switch
:从图中可以看出,
Swtich
提供扇出能力,让更多的PCIe设备连接在PCIe端口上;
Bridge
:桥接设备,用于去连接其他的总线,比如PCI总线或PCI-X总线,甚至另外的PCIe总线;
PCIe Endpoint
:PCIe设备;
Downstream
端口,灰色的小方块代表
Upstream
端口;
前文提到过,PCIe在软件上保持了后向兼容性,那么在PCIe的设计上,需要考虑在PCI总线上的软件视角,比如Root Complex
的实现可能就如下图所示,从而看起来与PCI总线相差无异:
而Switch
的实现可能如下图所示:
PCIe规范定义了分层的架构设计,包含三层:
Transaction层
Transaction Layer Packet
)的封装与解封装,此外还负责QoS,流控、排序等功能;
Data Link层
Data Link Layer Packet
)的封装与解封装,此外还负责链接错误检测和校正,使用Ack/Nak协议来确保传输可靠;
Physical层
Ordered-Set
包的封装与解封装,物理层处理TLPs、DLLPs、Ordered-Set三种类型的包传输;
数据包的封装与解封装,与网络包的创建与解析很类似,如下图:
来一个更详细的PCIe分层图:
为了兼容PCI软件,PCIe保留了256Byte的配置空间,如下图:
此外,在这个基础上将配置空间扩展到了4KB,还进行了功能的扩展,比如Capability、Power Management、MSI中断等:
草草收场吧,对PCI和PCIe有一些轮廓上的认知了,可以开始Source Code的软件分析了,欲知详情、下回分解!
《Linux PCI驱动框架分析(一)》
;
话不多说,直接开始。
struct pci_host_bridge
描述;
struct pci_dev
描述PCI设备,以及PCI-to-PCI桥设备;
struct pci_bus
用于描述PCI总线,
struct pci_slot
用于描述总线上的物理插槽;
来一张更详细的结构体组织图:
pci_host_bridge
,这个结构一般由Host驱动负责来初始化创建;
pci_host_bridge
指向root bus,也就是编号为0的总线,在该总线下,可以挂接各种外设或物理slot,也可以通过PCI桥去扩展总线;
Linux PCI驱动框架,基于Linux设备驱动模型,因此有必要先简要介绍一下,实际上Linux设备驱动模型也是一个大的topic,先挖个坑,有空再来填。来张图吧:
match
函数),当发现驱动与设备能进行匹配时,就会执行probe函数的操作;
bus_type
会维护两个链表,分别用于挂接向其注册的设备和驱动,而
match
函数就负责匹配检测;
kset/kobject
等内容,建议去看看之前的文章
《linux设备模型之kset/kobj/ktype分析》
既然说到了设备驱动模型,那么首先我们要做的事情,就是先在内核里边创建一个PCI总线,用于挂接PCI设备和PCI驱动,我们的实现来到了pci_driver_init()
函数:
pci_driver_init()
来创建一个PCI总线结构(全局变量
pci_bus_type
),这里描述的PCI总线结构,是指驱动匹配模型中的概念,PCI的设备和驱动都会挂在该PCI总线上;
pci_bus_type
的函数操作接口也能看出来,
pci_bus_match
用来检查设备与驱动是否匹配,一旦匹配了就会调用
pci_device_probe
函数,下边针对这两个函数稍加介绍;
pci_bus_match
函数的调用,实际会去比对
vendor
和
device
等信息,这个都是厂家固化的,在驱动中设置成
PCI_ANY_ID
就能支持所有设备;
pci_device_probe
的执行;
枚举的入口函数:pci_host_probe
pci_scan_root_bus_bridge
开始,首先需要先向系统注册一个
host bridge
,在注册的过程中需要创建一个
root bus
,也就是
bus 0
,在
pci_register_host_bridge
函数中,主要是一系列的初始化和注册工作,此外还为总线分配资源,包括地址空间等;
pci_scan_child_bus
开始,从
bus 0
向下扫描并添加设备,这个过程由
pci_scan_child_bus_extend
来完成;
pci_scan_child_bus_extend
的流程可以看出,主要有两大块:
pci_scan_child_bus_extend
的函数来扫描下一级的总线,从这个过程看,就是一个递归过程。
Depth First Search
)过程,熟悉数据结构与算法的同学应该清楚,这就类似典型的走迷宫的过程;
如果你对上述的流程还不清楚,再来一张图:
暂且写这么多,细节方面不再赘述了,把握大体的框架即可,无法扼住PCI的咽喉,那就扼住它的骨架吧。
先回顾一下PCIe的架构图:
Root Complex
部分,相当于PCI的
Host Bridge
部分;
nwl-pcie
来进行分析;
match
函数),当发现驱动与设备能进行匹配时,就会执行probe函数的操作;
《Linux PCI驱动框架分析(二)》
中提到过PCI设备、PCI总线和PCI驱动的创建,PCI设备和PCI驱动挂接在PCI总线上,这个理解很直观。针对PCIe的控制器来说,同样遵循设备、总线、驱动的匹配模型,不过这里的总线是由虚拟总线
platform
总线来替代,相应的设备和驱动分别为
platform_device
和
platform_driver
;
那么问题来了,platform_device
是在什么时候创建的呢?那就不得不提到Device Tree
设备树了。
device_node
描述的
Device Tree
;
device_node
节点,创建
platform_device
结构,并最终注册进系统,这个也就是PCIe Host设备的创建过程;
我们看看PCIe Host的设备树内容:
pcie: pcie@fd0e0000 {
compatible = "xlnx,nwl-pcie-2.11";
status = "disabled";
#address-cells = <3>;
#size-cells = <2>;
#interrupt-cells = <1>;
msi-controller;
device_type = "pci";
interrupt-parent = <&gic>;
interrupts = <0 118 4>,
<0 117 4>,
<0 116 4>,
<0 115 4>, /* MSI_1 [63...32] */
<0 114 4>; /* MSI_0 [31...0] */
interrupt-names = "misc", "dummy", "intx", "msi1", "msi0";
msi-parent = <&pcie>;
reg = <0x0 0xfd0e0000 0x0 0x1000>,
<0x0 0xfd480000 0x0 0x1000>,
<0x80 0x00000000 0x0 0x1000000>;
reg-names = "breg", "pcireg", "cfg";
ranges = <0x02000000 0x00000000 0xe0000000 0x00000000 0xe0000000 0x00000000 0x10000000 /* non-prefetchable memory */
0x43000000 0x00000006 0x00000000 0x00000006 0x00000000 0x00000002 0x00000000>;/* prefetchable memory */
bus-range = <0x00 0xff>;
interrupt-map-mask = <0x0 0x0 0x0 0x7>;
interrupt-map = <0x0 0x0 0x0 0x1 &pcie_intc 0x1>,
<0x0 0x0 0x0 0x2 &pcie_intc 0x2>,
<0x0 0x0 0x0 0x3 &pcie_intc 0x3>,
<0x0 0x0 0x0 0x4 &pcie_intc 0x4>;
pcie_intc: legacy-interrupt-controller {
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <1>;
};
};
关键字段描述如下:
compatible
:用于匹配PCIe Host驱动;
msi-controller
:表示是一个MSI(
Message Signaled Interrupt
)控制器节点,这里需要注意的是,有的SoC中断控制器使用的是GICv2版本,而GICv2并不支持MSI,所以会导致该功能的缺失;
device-type
:必须是
"pci"
;
interrupts
:包含NWL PCIe控制器的中断号;
interrupts-name
:
msi1, msi0
用于MSI中断,
intx
用于旧式中断,与
interrupts
中的中断号对应;
reg
:包含用于访问PCIe控制器操作的寄存器物理地址和大小;
reg-name
:分别表示
Bridge registers
,
PCIe Controller registers
,
Configuration space region
,与
reg
中的值对应;
ranges
:PCIe地址空间转换到CPU的地址空间中的范围;
bus-range
:PCIe总线的起始范围;
interrupt-map-mask
和
interrupt-map
:标准PCI属性,用于定义PCI接口到中断号的映射;
legacy-interrupt-controller
:旧式的中断控制器;
compatible
字段匹配上后,会调用probe函数,也就是
nwl_pcie_probe
;
看一下nwl_pcie_probe
函数:
pci_host_bridge
结构,最终通过这个
bridge
去枚举PCI总线上的所有设备;
devm_pci_alloc_host_bridge
:分配并初始化一个基础的
pci_hsot_bridge
结构;
nwl_pcie_parse_dt
:获取DTS中的寄存器信息及中断信息,并通过
irq_set_chained_handler_and_data
设置
intx
中断号对应的中断处理函数,该处理函数用于中断的级联;
nwl_pcie_bridge_init
:硬件的Controller一堆设置,这部分需要去查阅Spec,了解硬件工作的细节。此外,通过
devm_request_irq
注册
misc
中断号对应的中断处理函数,该处理函数用于控制器自身状态的处理;
pci_parse_request_of_pci_ranges
:用于解析PCI总线的总线范围和总线上的地址范围,也就是CPU能看到的地址区域;
nwl_pcie_init_irq_domain
和
mwl_pcie_enable_msi
与中断级联相关,下个小节介绍;
pci_scan_root_bus_bridge
:对总线上的设备进行扫描枚举,这个流程在
Linux PCI驱动框架分析(二)
中分析过。
brdige
结构体中的
pci_ops
字段,用于指向PCI的读写操作函数集,当具体扫描到设备要读写配置空间时,调用的就是这个函数,由具体的Controller驱动实现;
PCIe控制器,通过PCIe总线连接各种设备,因此它本身充当一个中断控制器,级联到上一层的中断控制器(比如GIC),如下图:
INTA#, INTB#, INTC#, INTD#
四根中断信号,PCI设备借助这四根信号使用电平触发方式提交中断请求;
Message Signaled Interrupt
) Interrupt:基于消息机制的中断,也就是往一个指定地址写入特定消息,从而触发一个中断;
针对两种处理方式,NWL PCIe
驱动中,实现了两个irq_chip
,也就是两种方式的中断控制器:
irq_domain
对应一个中断控制器(
irq_chip
),
irq_domain
负责将硬件中断号映射到虚拟中断号上;
再来看一下nwl_pcie_enable_msi
函数:
所以,稍微汇总一下,作为两种不同的中断处理方式,套路都是一样的,都是创建irq_chip
中断控制器,为该中断控制器添加irq_domain
,具体设备的中断响应流程如下:
nwl_pcie_leg_handler
,
nwl_pcie_msi_handler_high
,和
nwl_pcie_leg_handler_low
;
chained_irq_enter
进入中断级联处理;
irq_find_mapping
找到具体的PCIe设备的中断号;
generic_handle_irq
触发具体的PCIe设备的中断处理函数执行;
chained_irq_exit
退出中断级联的处理;
file_operation
操作函数集;
《PCI Express Technology 3.0》
《pci local bus specification revision 3.0》
《PCIe体系结构导读》
《PCI Express系统体系结构标准教材》
推荐阅读
进群,请加一口君个人微信,带你嵌入式入门进阶。
在公众号内回复「1024」,即可免费获取学习资料,期待你的关注~