往期内容
本文章相关专栏往期内容,PCI/PCIe子系统专栏:
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.事务层TPL格式
-
- [2.1 Posted和Non-Posted](#2.1 Posted和Non-Posted)
- [2.2 TPL通用格式](#2.2 TPL通用格式)
- [2.3 配置中重要的相关字段](#2.3 配置中重要的相关字段)
- 3.桥设备和非桥设备的配置空间
- 4.非桥(Type0)
- 5.桥(Type1)
- 6.配置过程示例
1.PCIe系统硬件结构
《PCI Express_ Base Specification Revision 4.0 Version 0.3 ( PDFDrive )》,P78。
从PCIe协议栈中三层的相关介绍,配置过程主要关注事务层,要关注TLP中怎么表示这些内容:
- 要做什么?内存读、内存写、IO读、IO写、配置读、配置写?
- 内存读写/IO读写:哪个地址?
- 配置读写:哪个"Bus/Device/Function/Register"?
- 数据?
2.事务层TPL格式
传输到设备配置空间的数据格式(非最终达到设备的数据格式,还有经过两层的包装)
2.1 Posted和Non-Posted
Post的意思是"邮寄"、"投递"。
PCIe有两类事务:
- Posted:主设备访问目标设备时,主设备发出信号后就不再理会后续过程,也就是"发射后不管"。使用这种方式,在数据未到达目标设备前,主设备就可以结束当前操作,效率更高。适用于"内存写"等场合。
- Non-Posted:主设备访问目标设备时,主设备发出信号后必须等待后续结果。比如"内存读",必须得到返回的数据,才能结束当前操作。
在PCIe系统中:
-
Posted:内存写
-
Non-Posted:内存读、IO读、IO写、配置读、配置写
-
对于Non-Posted,使用Split传送方式
- Split含有:分离
- 被拆分为两部分,发出请求报文、得到完成报文
2.2 TPL通用格式
下图来自《PCI Express_ Base Specification Revision 4.0 Version 0.3 ( PDFDrive )》。
-
类型:你是读内存还是写内存?读IO还是写IO?读配置还是写配置?
- 在Header里面有定义
-
地址:对于内存读写、IO读写,地址保存在Header里
-
Bus/Dev/Function/Regiser:对于配置读写,这些信息保存在Header里
-
数据:对于内存读、IO读、配置读,先发出请求,再得到数据
- 分为2个阶段:读请求报文、完成报文
- 读请求报文:不含数据
- 完成报文:含数据
2.3 配置中重要的相关字段
既然是去配置设备,那肯定是和配置PCI设备一样,要有Bus nmber(设备所属的bus号)、device number(配置哪个设备)、Function(设备作用选择)、Reg(访问设备相应的配置空间)。配置,那肯定配置写/读的选择,以及是Type1命令(桥转发)还是Type0命令(作用于配置的设备),而这些都在TPL中去指定,才能传输到设备去真正配置它。
- 首先是W/R、配置/其它的选择:
主要是通过Fmt和Type来决定,Fmt决定是写还是读,Type决定是作用于当前总线上的设备还是总线的下游的其它总线下的设备,二者组合又决定是配置W/R还是IO W/R。
- 接下来则是设备的确认:Bus/Device/Funciton/Register
Field | Description |
---|---|
FMT | TLP格式,配置读/写操作为 001b 或 010b |
Type | 配置读 000001b 或 配置写 000101b |
Length | 数据负载长度,配置读为 000b (无数据),写为 1b |
Requester ID | 发起请求的设备ID(包含Bus、Device、Function) |
Tag | 请求标签,用于追踪该请求 |
Bus Number | 指定要访问的目标设备所属的PCIe总线 |
Device Number | 指定目标设备在该总线下的设备号 |
Function Number | 指定目标设备的功能号 |
Register Number | 指定配置空间的寄存器号,访问相应寄存器的偏移地址 |
至于设备申请的空间范围基地址,应该是在data Payload中, 在配置写入时,Payload部分会包含要写入配置空间寄存器的数据(如配置寄存器的基地址)。对于配置读请求,Payload通常为空,因为只是请求读取某个寄存器的数据。
3.桥设备和非桥设备的配置空间
是位于设备内部的,是传输TPL的目标空间
PCI/PCIe设备、桥,它们的配置寄存器前面若干字节格式是一样的,可以从里面的"Header Type"分辨:
- 它是普通设备,还是桥
- 它是单功能设备,还是多功能设备:所谓功能,就是Function,一个物理设备可以有多个功能,也就有多个逻辑设备
一般的PCI/PCIe设备,它的配置寄存器格式如上上图的"Type 0 Header",在PCIe系统中这类设备被称为Endpoint。
PCI/PCIe桥,它的配置寄存器格式如上上图的"Type 1 Header",
对于PCI/PCIe桥,里面的由三项重要的总线号:
- Pirmary Bus Number:上游总线号
- Secondary Bus Number:自己的总线号
- Subordinate Bus Number:下游总线号的最大数值
这些总线号示例如下:
4.非桥(Type0)
RC本身就是一个桥,要去访问跟桥直接相连的设备,用CfgRd0类型的TLP:
-
Fmt和Type取值为0b00, 0b00100,表示:Configuration Read Type 0
-
TLP中设置有"Bus/Device/Funciton/Register"
-
提问:上图红圈中是设备,怎么知道它自己的Bus号是0,Device分别是0、1、2?
- 红圈中的设备都是在RC内部,它们的Device号是硬件里写死的(hard-coded)
- 当这些设备监测到Bus0上的TLP是CfgRd0后,忽略TLP中的Bus,比对TLP中的Device
- 如果Device吻合,就回应TLP
5.桥(Type1)
如果要配置的设备,不在当前总线上,但是在它下面的总线上,即:
- 目标设备的Bus号大于当前桥的Secondary Bus Number,
- 目标设备的Bus号小于或等于当前桥的Subordinate Bus Number,
那么在当前总线(即Secondary Bus Number)上传输的就是"Type 1 Configuration Request":
- TLP格式如下图所示
- 会穿过桥
- 到达设备时,跟设备直接连接的桥会把它转换为"Type 0 Configuration Request"
6.配置过程示例
:Single Root Enumeration Example
下文中BDF表示Bus,Device,Function,用这三个数值来表示设备。
-
软件设置Host/PCI Bridge的Secondary Bus Number为0,Subordinate Bus Number为255(先设置为最大,后面再改)。
-
从Bus 0开始扫描:先尝试读到BDF(0,0,0)设备的Vendor ID,如果不成功表示没有这个设备,就尝试下一个设备BDF(0,1,0)。一个桥下最多可以直接连接32个设备,所以会尝试32次:Device号从0到31。注意:在Host/PCI Bridge中,这些设备的Device号是硬件写死的。
-
步骤2读取BDF(0,0,0)设备(即使图中的A)时,发现它的Header Type是01h,表示它是一个桥、单功能设备
-
发现了设备A是一个桥,配置它:
- Primary Bus Number Register = 0:它的上游总线是Bus 0
- Secondary Bus Number Register = 1:从它发出的总线是Bus 1
- Subordinate Bus Number Register = 255:先设置为最大,后面再改
-
因为发现了桥A,执行"深度优先"的配置过程:先去枚举A下面的设备,再回来枚举跟A同级的B
-
软件读取BDF(1,0,0)设备(就是设备C)的Vendor ID,成功得到Vendor ID,表示这个设备存在。
-
它的Header Type是01h,表示这是一个桥、单功能设备。
-
配置桥C:
- Primary Bus Number Register = 1:它的上游总线是Bus 1
- Secondary Bus Number Register = 2:从它发出的总线是Bus 2
- Subordinate Bus Number Register = 255:先设置为最大,后面再改
-
继续从桥C执行"深度优先"的配置过程,枚举Bus 2下的设备,从BDF(2,0,0)开始
-
读取BDF(2,0,0)设备(就是设备D)的Vendor ID,成功得到Vendor ID,表示这个设备存在。
-
它的Header Type是01h,表示这是一个桥、单功能设备。
-
配置桥D:
- Primary Bus Number Register = 2:它的上游总线是Bus 2
- Secondary Bus Number Register = 3:从它发出的总线是Bus 3
- Subordinate Bus Number Register = 255:先设置为最大,后面再改
-
继续从桥D执行"深度优先"的配置过程,枚举Bus 2下的设备,从BDF(3,0,0)开始
-
读取BDF(3,0,0)设备的Vendor ID,成功得到Vendor ID,表示这个设备存在。
-
它的Header Type是80h,表示这是一个Endpoing、多功能设备。
-
软件枚举这个设备的所有8个功能,发现它有Function0、1
-
软件继续枚举Bus 3上其他设备(Device号1~31),没发现更多设备
-
现在已经扫描完桥D即Bus 3下的所有设备,它下面没有桥,所以桥D的Subordinate Bus Number等于3。扫描完Bus 3后,回退到上一级Bus 2,继续扫描其他设备,从BDF(2,1,0)开始,就是开始扫描设备E。
-
读取BDF(2,1,0)设备(就是设备E)的Vendor ID,成功得到Vendor ID,表示这个设备存在。
-
它的Header Type是01h,表示这是一个桥、单功能设备。
-
配置桥E:
- Primary Bus Number Register = 2:它的上游总线是Bus 2
- Secondary Bus Number Register = 4:从它发出的总线是Bus 4
- Subordinate Bus Number Register = 255:先设置为最大,后面再改
-
继续从桥D执行"深度优先"的配置过程,枚举Bus 4下的设备,从BDF(4,0,0)开始
-
读取BDF(4,0,0)设备的Vendor ID,成功得到Vendor ID,表示这个设备存在。
-
它的Header Type是00h,表示这是一个Endpoing、单功能设备。
-
软件继续枚举Bus 4上其他设备(Device号1~31),没发现更多设备
-
已经枚举完设备E即Bus 4下的所有设备了,更新设备E的Subordinate Bus Number为4。然后继续扫描设备E的同级设备:Bus=2,Device从2到31,发现Bus 2上没有这些设备。
-
软件更新设备C即Bus 2的桥,把它的Subordinate Bus Number设置为4。然后继续扫描设备C的同级设备:Bus=1,Device从1到31,发现Bus 1上没有这些设备。
-
软件更新设备A即Bus 1的桥,把它的Subordinate Bus Number设置为4。然后继续扫描设备A的同级设备:Bus=0,Device从1到31,发现Bus 0上的设备B。
-
配置桥B:
- Primary Bus Number Register = 0:它的上游总线是Bus 0
- Secondary Bus Number Register = 5:从它发出的总线是Bus 5
- Subordinate Bus Number Register = 255:先设置为最大,后面再改
-
再从桥B开始,执行"深度优先"的配置过程。
当PCIe控制器发出的 TLP(Transaction Layer Packet) 到达外部的PCIe设备时,设备的内部逻辑会通过其寄存器和硬件模块来解析TLP,并对设备的配置空间或内存空间进行访问或更新。
1. 配置TLP如何配置PCIe设备的配置空间:
PCIe设备通常包含 配置寄存器(Configuration
Registers),用于设备初始化、配置和状态监控。设备的配置空间通过PCIe总线进行访问。配置TLP在被PCIe设备接收到后,会通过以下步骤来实现对配置空间的操作:
PCIe设备接收到TLP:
- 当PCIe设备接收到一个包含配置访问命令的 TLP (例如
Configuration Read
或Configuration Write
),设备会解析TLP中的地址和命令字段。解析TLP并映射到配置空间:
- 设备的寄存器或硬件逻辑会将TLP中的PCIe总线地址映射到设备内部的配置寄存器地址。设备内部的地址空间可能有特定的映射方式(通常是通过
BAR
寄存器定义的基地址寄存器,或者专门的配置空间地址)。执行读/写操作:
- 如果是读操作,设备会从指定的配置寄存器读取数据并将其封装成一个返回的TLP,发送回主机。
- 如果是写操作,设备会根据TLP的内容更新指定的配置寄存器的值。
TLP到达设备后,设备通过寄存器或硬件逻辑解析TLP,并根据TLP中携带的命令(读/写)和地址字段来访问其配置空间。
2. 访问IO空间的TLP,携带的是
**addr_pcie**
如何访问:对于访问 IO空间的TLP (例如
Memory Read
或Memory Write
TLP),流程类似,TLP会携带PCIe地址,PCIe设备会通过寄存器解析TLP并找到相应的内部地址。具体过程如下:
设备接收到IO TLP:
- PCIe设备接收到 IO TLP 时,会检查TLP中的 PCIe地址(addr_pcie) 字段。
PCIe地址映射到设备内部地址:
- 设备内部通常有 地址映射寄存器(如BAR寄存器) ,这些寄存器定义了PCIe地址和设备内部地址空间的映射。根据
addr_pcie
,设备的硬件逻辑或寄存器能够确定这个请求是针对设备内部哪个资源(IO寄存器、内存空间等)。执行IO操作:
- 如果是读IO请求,设备会在指定的内部地址空间中查找数据,并封装成TLP返回主机。
- 如果是写IO请求,设备会根据TLP携带的写数据更新指定的内部地址。
BAR寄存器在PCIe地址映射中的作用:
PCIe设备通常通过 基址寄存器(BAR,Base Address Registers)
来定义外部PCIe地址如何映射到设备内部的地址空间。BAR寄存器通常在设备初始化时配置。主机在启动设备时,会读取或写入BAR寄存器来确定设备的地址映射。
BAR映射:
- 主机根据设备配置空间中的 BAR寄存器 设置,将PCIe总线地址与设备内部的内存或IO资源进行映射。
- 例如,当主机发送一个TLP请求访问某个内存或IO地址,设备会通过BAR寄存器来解析TLP中包含的
addr_pcie
,并将其映射到设备的内部地址空间。当TLP到达PCIe设备时,设备通过其 BAR寄存器
或其他地址映射寄存器来解析TLP中的地址信息,从而将PCIe总线上的地址映射到设备的内部地址空间。