1、dts pcie设备树
1.1、pcie设备树
pcie1: pcie@3400000 {
compatible = "fsl,ls1012a-pcie";
reg = <0x00 0x03400000 0x0 0x00100000 /* controller registers */
0x40 0x00000000 0x0 0x00002000>; /* configuration space */
reg-names = "regs", "config";
interrupts = <0 118 0x4>, /* controller interrupt */
<0 117 0x4>; /* PME interrupt */
interrupt-names = "aer", "pme";
#address-cells = <3>;
#size-cells = <2>;
device_type = "pci";
num-viewport = <2>;
bus-range = <0x0 0xff>;
ranges = <0x81000000 0x0 0x00000000 0x40 0x00010000 0x0 0x00010000 /* downstream I/O */
0x82000000 0x0 0x40000000 0x40 0x40000000 0x0 0x40000000>; /* non-prefetchable memory */
msi-parent = <&msi>;
#interrupt-cells = <1>;
interrupt-map-mask = <0 0 0 7>;
interrupt-map = <0000 0 0 1 &gic 0 110 IRQ_TYPE_LEVEL_HIGH>,
<0000 0 0 2 &gic 0 111 IRQ_TYPE_LEVEL_HIGH>,
<0000 0 0 3 &gic 0 112 IRQ_TYPE_LEVEL_HIGH>,
<0000 0 0 4 &gic 0 113 IRQ_TYPE_LEVEL_HIGH>;
status = "disabled";
};
其中reg里面的configuration space [0x4000000000, 0x4000000000+0x2000)为配置空间的cpu地址,所有ep的配置空间的cpu地址都在这段地址空间,都写这段地址具体是操作总线上的那个设备是由pcie控制器的额外的寄存器也区分,也就是访问具体pcie配置空间的时候,需要把bus、device、function信息写入pcie控制器。后面具体代码后看到读写配置设备配置空间的操作。
1.2、pcie memory地址空间
这段空间也就是cpu域地址空间,包含上面小节所介绍的configuration space以及I/O、memory空间,往这段地址空间读写数据会被pcie控制器转换为对pcie配置空间、I/O或者memory进行读写。具体的pcie协议在软件层面看不到,至于pcie协议里面的事务,完全由pcie控制器实现。读写这段地址空间具体读写哪个设备,后面会有更详细介绍。
2、RC配置空间读写
2.1、rc配置空间
rc的配置空间为pcie控制器的寄存器,各寄存器参考芯片手册的PCI_Express_Configuration_Registers memory map。
2.2、rc配置空间映射
ls1012a pcie驱动调用devm_pci_remap_cfg_resource建立配置空间物理地址到虚拟地址的映射,这里都是cpu域的地址,不是cpu域到pcie域,函数栈如下:
如上调用栈的__ioremap函数所示,phys_addr也就是0x3400000,最终虚拟地址会保存到pci->dbi_base变量中,后续通过pci->dbi_base这个虚拟地址来访问0x3400000 rc配置空间。
2.3、rc地址转换
ls1012a使用DesignWare Cores PCI Express的ip,调用dw_pcie_own_conf_map_bus来获取cpu域地址;rc跟ep地址映射不一样,rc有自己的映射函数,从芯片手册看,rc的配置空间实际是以0x3400000为起始地址的一组pcie host寄存器,不需要cpu域地址到pcie域地址的转换,本质上获取该cpu域物理地址的虚拟地址即可,映射函数实现如下:
void __iomem *dw_pcie_own_conf_map_bus(struct pci_bus *bus, unsigned int devfn, int where)
{
struct pcie_port *pp = bus->sysdata;
struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
if (PCI_SLOT(devfn) > 0)
return NULL;
return pci->dbi_base + where;
}
devfn只能是rc,非rc的映射不走该函数,所以,"if (PCI_SLOT(devfn) > 0)"用于判断devfn是否是rc,非rc返回NULL;最后一行返回的映射地址很明显是上一小节0x3400000虚拟地址加上一个偏移,这个偏移也就是配置空间具体的某个寄存器。获取到cpu域地址之后就可以用这个地址像读写内存一样访问rc的配置空间了。
2.4、rc配置空间读写
ls1012a pcie host配置空间寄存器如下:
如下为ls1012a获取pcie host配置空间vendor id cpu域地址的调用栈:
根据上面芯片手册显示的verdor id寄存器的偏移为0,也就是调用栈中的where,pci->dbi_base虚拟地址为0xffff800013200000(前面小节介绍的0x3400000的虚拟地址),那么函数返回的vendor id的虚拟地址即为0xffff800013200000+0;获取到cpu域地址的虚拟地址之后,读该虚拟地址即可获取配置空间的vendor id寄存器的值,调用栈如下:
(__raw_readl参数addr即为vendor id寄存器cpu域的虚拟地址)
3、ep配置空间读写
3.1、resource offset
ep配置空间的cpu域地址与pcie域地址并不相同,而是有一个线性映射关系,就像物理地址映射到虚拟地址一样,总线发送一个地址到pcie host控制器后,pcie host控制器需要对这个总线地址经过转换之后才是pcie域地址;在pcie dts设备树里面有描述cpu域到pcie域的地址映射,如下:
如上红色方框所示的pcie地址和cpu地址,0x4040000000为cpu域地址,0x40000000为pcie域地址,也就是cpu发送0x4040000000的地址到pcie host控制器,pcie host控制器会把该地址转换为0x40000000,然后去访问对应的pcie设备,映射关系也就是cpu域地址加上一个固定的偏移即为pcie域地址,反过来,知道pcie域地址,用pcie域地址减去一个偏移就是cpu域地址。
3.2、pcie resource
pcie的resource可以理解为dts中的ranges,也就是I/O、内存地址空间,内核通过pci_add_resource_offset函数把pcie的资源添加到pcie bridge结构体windows链表上,一个window为一段连续的地址空间,从代码上看,这段地址空间是cpu域地址空间,当然,里面有一个偏移,记录cpu域地址到pcie域地址的偏移,通过这个偏移可以获取到这段地址空间对应的pcie域地址空间。
如下是non-prefetchable memory添加时resource参数的值:
start为0x4040000000,也就是dts里面描述的cpu域地址,另外pci_add_resource_offset的offset的值为0x4000000000,也就是pcie域地址到cpu域地址的偏移,dts中的0x4040000000-0x40000000;添加non-prefetchable memory资源是函数调用栈如下:
遍历dts ranges,添加资源到bridge代码如下:
如上图黄色高亮行,res->start也就是dts里面的cpu域地址0x4040000000,range.pci_addr也就是dts里面的pcie域地址0x40000000,两个地址相减即为offset;resources为bridge->windows链表,在知道cpu域地址的时候,通过该链表找到cpu域地址的window,再获取该window的偏移,然后就能获取到该cpu域地址的pcie域地址。
3.3、ep配置空间映射(虚拟地址)
在dts中,描述的pcie设备配置空间为[0x4000000000, 0x4000000000+0x00002000):
pcie配置空间物理地址映射调用栈如下:
__ioremap的phys_addr即为0x4000000000,也就是配置空间起始地址,size即为0x2000,也就是配置空间大小,ls1012a所有ep都是使用这一地址空间,之前有看到过其他cpu,在一段连续的cpu地址空间为每个ep分配一个配置空间,也就是起始地址加上一个n倍size的偏移即可获取每个设备的配置空间,ls1012a不是采用这种偏移的方式,而是需要先往pcie host控制器的寄存器写入当前要访问的ep的信息,然后由pcie host控制器根据寄存器信息将相同的cpu域地址转换成不同的ep的读写。
__ioremap之后获取到虚拟地址将保存到pp->va_cfg0_base,之后通过访问这个虚拟地址来访问ep的配置空间。
3.4、ep配置空间映射
ep配置空间映射通过调用dw_pcie_other_conf_map_bus函数实现,该函数调用dw_pcie_prog_outbound_atu写pcie host控制器,告诉pcie host控制器,接下来访问配置空间是访问哪个总线、设备、功能的配置空间,最终写pcie host控制器的函数代码如下:
static void __dw_pcie_prog_outbound_atu(struct dw_pcie *pci, u8 func_no,
int index, int type, u64 cpu_addr,
u64 pci_addr, u32 size)
{
u32 retries, val;
if (pci->ops->cpu_addr_fixup)
cpu_addr = pci->ops->cpu_addr_fixup(pci, cpu_addr);
if (pci->iatu_unroll_enabled) {
dw_pcie_prog_outbound_atu_unroll(pci, func_no, index, type,
cpu_addr, pci_addr, size);
return;
}
dw_pcie_writel_dbi(pci, PCIE_ATU_VIEWPORT,
PCIE_ATU_REGION_OUTBOUND | index);
dw_pcie_writel_dbi(pci, PCIE_ATU_LOWER_BASE,
lower_32_bits(cpu_addr));
dw_pcie_writel_dbi(pci, PCIE_ATU_UPPER_BASE,
upper_32_bits(cpu_addr));
dw_pcie_writel_dbi(pci, PCIE_ATU_LIMIT,
lower_32_bits(cpu_addr + size - 1));
dw_pcie_writel_dbi(pci, PCIE_ATU_LOWER_TARGET,
lower_32_bits(pci_addr));
dw_pcie_writel_dbi(pci, PCIE_ATU_UPPER_TARGET,
upper_32_bits(pci_addr));
dw_pcie_writel_dbi(pci, PCIE_ATU_CR1, type |
PCIE_ATU_FUNC_NUM(func_no));
dw_pcie_writel_dbi(pci, PCIE_ATU_CR2, PCIE_ATU_ENABLE);
/*
* Make sure ATU enable takes effect before any subsequent config
* and I/O accesses.
*/
for (retries = 0; retries < LINK_WAIT_MAX_IATU_RETRIES; retries++) {
val = dw_pcie_readl_dbi(pci, PCIE_ATU_CR2);
if (val & PCIE_ATU_ENABLE)
return;
mdelay(LINK_WAIT_IATU);
}
dev_err(pci->dev, "Outbound iATU is not being enabled\n");
}
参数cpu_addr的值为0x4000000000,也就是配置空间的cpu域地址,这里是配置空间的起始地址,不是需要访问的寄存器的地址,这里映射的一段空间;参数pci_addr的值为0x1000000,这里不是pci域地址,这里是bus、dev、func的一个组合,也就是告诉pcie host,写0x4000000000地址空间将要操作的是哪个总线的哪个设备的哪个功能,前面介绍了,所有ep的配置空间的cpu域地址都是一个地址空间,所以写pcie host寄存器,告诉pcie host当前这段配置地址空间要操作具体的哪个总线设备。
写配置空间cpu域的起始地址:
写配置空间cpu域的结束地址:
写bus、dev、func:
使能等待ATU就绪:
ATU就绪之后,dw_pcie_other_conf_map_bus返回配置空间的虚拟地址即可:
如下是读取pcie网卡vendor id地址映射的函数调用:
4、BAR地址空间分配
4.1、获取BAR空间大小
配置空间映射前面已经介绍;BAR0配置空间的偏移为0x10h,读BAR0寄存器的函数调用栈如下:
读写BAR0,获取BAR0大小的代码如下(读写BAR,获取地址位):
计算BAR空间大小相关代码如下:
最后会调用pcibios_bus_to_resource将pcie域地址转换为cpu域地址,就是前面介绍的pcie域地址加上到cpu域地址的偏移,最后保存到pcie设备的resource数组里面,此时resource里面的资源还不是最终的,只是根据pcie设备BAR寄存器默认值计算得来的。最后会有assign操作给BAR分配资源。
4.2、pcie资源分配
资源分配简单的说就是分配地址空间,前面知道cpu域地址到pcie域地址有一个映射关系,知道pcie域地址的话,可以计算到cpu域地址,但是pcie设备是可热插拔并且pcie域地址是可配的,不同pcie设备默认pcie域地址可能会有冲突等情况,需要对他们的pcie域地址重新分配,分配之后写入BAR寄存器,具体可以参考《存储技术原理分析 - 基于Linux2.6内核源代码》,虽然内核版本比较老,但是还是具有参考意义。
重新分配资源的调用栈如下:
重新分配的代码如下:
其中alloc.start为新分配的cpu域起始地址,此处值为0x4040000000,也就是cpu域的地址,根据前面的调用栈的resno的值,可以看到这是分配的BAR 2的资源,从内核启动打印的日志也可以印证这一点。
分配完资源之后,还得写BAR寄存器;内核调用调用pci_std_update_resource写BAR寄存器,函数调用栈如下:
首先调用pcibios_resource_to_bus函数将cpu域地址转换为pcie域地址,主要工作就是找到该资源在哪个windows,在哪段地址空间,也就是哪个cpu域映射到哪个pcie域地址,现在已经知道cpu域地址,再获取所在区间到pcie域地址的偏移,加上偏移即可获取pcie域地址,代码如下:
获取到pcie域地址之后,将pcie域地址写入到BAR寄存器,调用栈如下:
where的值为24,val的值为0x40000004,24即为BAR 2寄存器在配置空间的偏移,0x40000004即为pcie域地址(这里需要忽略低位,最终地址就是0x40000000),前面已经分析出cpu域地址为0x4040000000,cpu域地址与pcie域地址偏移以及地址正好与dts描述的一样。
配置好BAR寄存器之后,就可以像对内存一样对BAR空间进行访问。读写0x4040000000地址的时候,pcie host控制器就会转换成对0x40000000地址的访问。
5、BAR空间读写
5.1、BAR空间映射
BAR寄存器配置好之后,此时,pcie设备资源里面的地址还是物理地址,也就是前面的0x4040000000还是物理地址,需要建立页表映射才能访问,如下是rtl8111f BAR 2建立映射的调用栈:
phys_addr即为0x4040000000,相关代码如下:
没有找到rtl8111f芯片手册,根据代码分析,BAR 2应该是MAC Registers,获取到BAR 2映射的虚拟地址,就可以使用gdb像打印内存一样去打印这些寄存器的值,如下是在开发板打印的BAR 2相关寄存器的值:
通过ifconfig查看rtl8111f网卡的mac地址如下:
可以看到MAC地址正好与BAR 2前面几个bytes的内容相同。