Linux PCI设备驱动
我们在写程序时,对变量的赋值,取值操作,实际上做了对内存进行读写。通过地址就可以对内存进行读写操作。实际上对芯片上的其它外设IO操作也是一样。
int* a=0x100000;
int* b=0x200000;
*a = 1000;
*b= *a;
CPU 与外设的连接结构图如下所示,通过地址线传输想要访问的地址,数据线传输数据,读写信号线确定你的操作是读/写。
可以看到总线上所挂的设备(ram、flash、gpio),它们对信号线都是共享的,那么怎么选中想要访问的设备呢? 它们每一个设备都有各自的地址段,当地址信号线上的地址属于自己的范围时,cs(片选信号)就会选中芯片,然后进行访问。
关键在于:内存控制器,它会根据地址范围发出对应的片选信号,选中对应的设备。
类似于这样的接口 叫ram-like接口,所有的ram-like 接口设备,cpu 都可以直接通过地址读写。
下图是imx6ull 的片上外设控制器地址表(从0x0000_0000~ 0x8000_0000),可以看到不同的外设控制器各自有自己的地址段,0x8000_0000 是内存控制器。
上图中的外设控制器都可以通过地址直接访问,但是它们管理的设备或其它设备是不能的。
例如我们在使用一个i2c设备时,需要读写i2c控制器,通过一定的协议才能完成对i2c设备的访问。i2c 协议
如emmc ,要像访问emmc 设备必须通过emmc 控制器,必须编写emmc控制器驱动。
这样的访问方式就会比较复杂,无法像内存一样轻松读写,因此pci 总线诞生。
PCI总线(Peripheral Component Interconnect,外部设备互联),挂在pci 总线下的设备,cpu可以通过地址直接访问,让访问外设变得像内存一样简单,那么它是怎么做到的呢?
如图pci 控制器(host birdge:主桥又称北桥)跟其它片上设备一样挂在内存控制器下,可以通过地址直接访问,pci/pcie 则由它来管理。
当cpu 想要访问某个pci 设备时,cpu 发出的地址到达pci控制器后,它会将地址做偏移处理,转换成pci_addr。同样每一个pci设备都拥有属于自己的一段pci_addr,它们确认控制器发出的pci_addr属于自己的范围内,就会与其进行交互。
pci_addr = cpu_addr + offset
pci 接口定义如图所示:红色为必需信号线,蓝色为可选信号线。
必需的引脚中分为以下几个部分
在地址周期时,C/BE[3:0]# command 定义如下。
pci 总线 与设备的连接结构如图,与前面一样许多设备都挂在同一条pci 总线上,它们对信号线是共享的,通过地址来对指定的设备访问。
问题1: 那这些设备怎么确认自己的地址段呢?在用地址访问之前需要对pci 设备做配置,也就是读写配置寄存器。
先看以下配置空间的结构:
每一条pci支总线最多可以拥有32个设备(实际上负载不了这么多),每一个设备都有8个功能,每一个功能有一块 256 Byte 的配置空间(所谓的配置空间即许多寄存器的集合)。
PCI设备可以简单地分为PCI Agent、PCI Bridge、Cardbus Bridge:
根据pci 设备类型的不同,pci 配置空间也有不同。pci配置空间一共有256 字节,其中64 字节为头部空间,配置过程就是头部寄存器的读写。
问题2: pci 总线上有那么多设备,如何能访问到其中某一个设备?
0x00~0x0c 这一段寄存器,除了header type值不同外其它都与type 0相同,0x10 开始都不同。
BAR: 桥设备不需要多大的BAR空间,因为它只需要做转发工作。
primary bus: 上一级总线的总线号。
secondary bus: 自己的总线号。
subordlinate bus: 子总线中最大的总线号。
问题4:如何配置桥设备?
问题5:前面描述了如何配置 bus0 下的pci设备,那么如何配置bus1 下的pci设备呢?怎么通过桥转发到它的子设备。
这个过程类似于路由,需要到达的设备属于自己时,就转发给设备,不属于自己,就转发给下一级总线。
Configuration Command type 1
后,有2个选择
Configuration Command type 1
转换为Configuration Command type 0
Configuration Command type 1
PCIE 总线即PCI Express(快速的pci)。
在我们的映像中并行的数据线(例如pci 的AD[31:0])相对传输速率比较高,但是当真正高速后,信号线之间会产生干扰,这种情况下反而使用串行信号线速度更快。
所以PCIE 的数据线改为串行的差分信号线(一个方向上有两根信号线tx+、tx-)。
PCIE 的接口引脚如图:
PCIE_TXP/N、PCIE_RXP/N,构成了一组发送与接收的信号线,被称为一个 lane ,P为+、N为-。
PCIE_REFCLKP/N 时钟也是差分信号。
PCI接口的引脚时并行的,PCEe的是串行的,每个方向的数据使用2条差分信号线来传输,发送/接收两个方向就需要4条线,这被称为1个Lane:
PCIE 设备之间可以有多个lane:
PCIE总线 与PCI 总线的相同点:虽然PCIE 信号线发生改变,但是对CPU 来讲它与PCI 总线是一样的,CPU仍让是使用地址即可以访问到PCIE设备,所以软件驱动上PCI 与PCIE 设备驱动是兼容的。
PCIE 仍然需要像PCI 一样配置设备,分配设备所需要的地址段。
对比PCI 与PCIE 结构图,PCI 总线 CPU 对接的是Host Bridge(主桥)、PCIE 总线CPU 对接的是Root Complex,实际上Root Complex已经将桥封装在内部,所以它们是对于CPU 来说是相同的。只是在桥后的部分不同。
** PCIe 与PCI的不同:**
这是PCIE 总线详细的结构图,标注出了Root Complex、switch的内部结构。
Root Complex 由Host Bridge 和多个 PCI-to-PCI Bridge 组成。
Switch 由多个 PCI-to-PCI Bridge 组成,功能主要是扩展PCIE 能接更多的设备。
前面说到CPU 对于PCIE 设备仍然是使用PCI地址就可以访问到设备,那么它们需要像PCI 设备一样配置吗?
答案:是的
CPU 将如何配置它们?PCIE 的数据信号线变化,并且没有地址线,桥与PCIE 设备之间该以怎样的形式通信呢?
问题1:PCIE 设备之间如何通信(主要就是桥设备与普通设备),怎么传输地址、数据?
PCIE 总线数据线为串行信号线,属于字节流,只能一个一个Byte 发送,因此采用 数据包 方式传送。
PCIE 设备之间的数据通信,与网络协议栈非常相似,如图PCIE 协议栈一共有三层。
分别是Transaction层(事务层)、Data Link层(数据链路层)和 Physical层(物理层)。
事务层: 传输的是Transaction Layer Packet(TLP),其中包含 header 头部、data payload 要传输的数据内容、ECRC crc校验码,验证header 和data payload 传输正确性。头部中又包含了命令类型(什么方式的读/写)、地址,保证数据能发送到目的地
数据链路层: (发送回路)数据链路层在拿到 TLP 包后在两端加上序列号和LCRC,在发生传输错误时,拥有重传机制。(接收回路)接收到一个link 包,验证seq 与crc并传递给事务层。
物理层: (发送回路)物理层 拿到link 包,在两端加上开始信号和结束信号。(接收回路)接收到一个物理层的包,将两端的开始与结束信号拆分,传递给上层的数据链路层。
TLP 的通用格式如下:
PCIe TLP头部决定 数据包的目标设备、目录地址、读写命令等等。
在 PCIE 的不同的读写情况下TLP 包的格式也有不同:
PCIE TLP头部完整格式:
Fmt 与type: 决定命令的类型,内存读/写、IO读/写、配置0读/写 或配置1读/写。参照表格
Bus Number: 要访问的总线号。(以下几个号都只适用于配置设备时的TLP头部)
Device Number: 要访问的设备号。
Function Number: 要访问的功能号。
Register Number: 要访问的寄存器号。
下图是PCIE 配置TLP头部:
PCIE总线 与PCI 一样,需要读取设备配置空间,获取到设备信息,分配地址并写入配置空间。
根据pcie设备类型不同,PCIE 配置空间分为两类:type0(非桥设备)和type1(桥设备)。
基本与PCI 设备配置空间相同,主要关注以下几个:
header type: 区分设备类型,非桥设备为0x00、桥设备为0x01。bit7 位决定PCIe 是多功能设备(一般endpoint才有多功能,桥设备一般为单功能),还是单功能设备。bit7=1 多功能,bit7=0 单功能。
Base Address: 需要分配的地址长度存放在BAR中,并且配置完会向BAR写入首地址。
Pirmary Bus Number: 上游总线号。
Secondary Bus Number: 自己的总线号。
配置PCIe 设备首先需要配置桥设备,因为需要通过桥的转发才能到达endpoint。
例如配置与主桥相连的A桥:
根据Fmt 与type选择有两种类型的TLP 配置包:分别为type0和type1。它们的区别是当需要到达的设备与桥直连时(比如host 与A、A与C),该桥会发出type0的包,直接到达设备;当需要到达的设备在它的下游,并且不与他直连时(比如A 与最下端设备),该桥会发出type1的包,type1经过一层层桥的转发,到达与该设备直连的桥时,该桥会发出type0 的包,到达设备。
① A与主桥相连,所以主桥发出type0 的包,并且标明Device 、Function 和Register Number。
② 根据设备号(Root Complex 为封装好的硬件,所以设备号已经在硬件上固定了,switch 也是一样)、功能号和寄存器号,就可以找到想要读写的寄存器。
③ 读取配置空间header type,发现A 设备为桥,将其的总线号分别配置为 Pirmary Bus Number=0、Secondary Bus Number=1、Subordinate Bus Number=255(暂定为255,待扫描完下游所有的桥设备后,修改为下游最大桥设备号)。
这样一个桥设备就配置完成了。
注意:
非桥设备如上图中的 Bus3 Dev0设备,这种设备一般被叫做endpoint。
配置Bus3 Dev0:
① Host Bridge 发出type1 的TLP 数据包,Fmt=010、type=00101,Bus Number=3,Device Number=0,Function Number=0,Register Number=xxx。
② type1 TLP 包经过桥A->C->D桥,最终桥D发出type0 数据包,到达Bus3 Dev0。
③ 读取Bus3 Dev0 Function0 的配置寄存器,发现为普通设备
④ 读取BAR,获得设备需要的内存大小。
⑤ 分配指定大小内存,并将首地址写入设备0 BAR。配置完成。
下文中BDF表示Bus,Device,Function,用这三个数值来表示设备。
数据传输时,最先要确定的是:怎么找到对方?
所谓"路由",就是怎么找到对方,PCIe协议中有三种路由方式:
不管是什么路由方式,我们最终关心的是TLP的格式。
内存读、内存写或者IO读、IO写属于地址路由。
下图是内存读写与IO读写的TLP 头部格式:内存读写数据格式有64bit和32bit两种,IO读写32位bit。
Address:代表要读写设备的地址(pci地址)。
requester ID:请求读写的设备id,其中
tag:表示一次pcie的通信。
pcie的每一次发送或读取信息都是通过TLP包,tag就表示TLP包的唯一序号。在设备发出包后会有一段缓存专门存放TLP包,如果对方回应这个TLP 那么设备会释放这段缓存,假如等待一段时间没有回应,设备将会重传这个TLP。就是通过tag判断。
想要了解PCIe 地址的路由方式首先要了解桥设备的配置空间(type1):
Memory Base 和Memory Limit:Base表示一段内存的首地址,Limit表示大小,合起来就描述了一段地址空间。描述下游设备所分配的所有地址段(一个桥下游设备所分配的地址应该是连续的,所以Base表示这些设备地址中最首的地址,Limit 表示它们的总长 )。
Prefetchable Memory Base、Limit:表示下游设备可预取的内存空间。
IO Base 和IO Limit:表示所有子设备所分配的IO 地址空间。
下图里面的Requester ID、TAG,被称为"Transaction ID"。
主设备要给EndPoint的内存写数据,它发出"内存写报文",不需要对方回应。
主设备要读EndPoint的内存数据,它发出"内存读报文",需要对方回应。
主设备要给EndPoint的IO写数据,它发出"IO写报文",需要对方回应。
主设备要读EndPoint的IO数据,它发出"IO读报文",需要对方回应。
场景①:最常见的场景。cpu 想要访问nedpoint1,cpu给他分配的空间是A-B,那么桥P-P2的Memory base和Limit描述的大小就是A-B(因为它的下游只有一个设备。如桥p-p1他的下游有多个设备分配了空间,分别是A-B和C-D,那么它的base和Limit 描述的这段空间就是A-D )。
场景②:EP1 与EP2之间的通信
场景③:EP2 通过DMA 直接访问内存(主存储器)
消息报文的头部格式如下:
消息报文中头部的Type字段里低3位表示隐式路由方式: