PCIe_Host驱动分析_地址映射

往期内容

本文章相关专栏往期内容,PCI/PCIe子系统专栏:

  1. 嵌入式系统的内存访问和总线通信机制解析、PCI/PCIe引入
  2. 深入解析非桥PCI设备的访问和配置方法
  3. PCI桥设备的访问方法、软件角度讲解PCIe设备的硬件结构
  4. 深入解析PCIe设备事务层与配置过程
  5. PCIe的三种路由方式
  6. PCI驱动与AXI总线框架解析(RK3399)
  7. 深入解析PCIe地址空间与寄存器机制:从地址映射到TLP生成的完整流程

Uart子系统专栏:

  1. 专栏地址:Uart子系统
  2. Linux内核早期打印机制与RS485通信技术
    -- 末片,有专栏内容观看顺序

interrupt子系统专栏:

  1. 专栏地址:interrupt子系统
  2. Linux 链式与层级中断控制器讲解:原理与驱动开发
    -- 末片,有专栏内容观看顺序

pinctrl和gpio子系统专栏:

  1. 专栏地址:pinctrl和gpio子系统

  2. 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用

    -- 末片,有专栏内容观看顺序

input子系统专栏:

  1. 专栏地址:input子系统
  2. input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
    -- 末片,有专栏内容观看顺序

I2C子系统专栏:

  1. 专栏地址:IIC子系统
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    -- 末篇,有专栏内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    -- 末篇,有专栏内容观看顺序

目录

资料

开发板资料:

分析的文件:

1.回顾

CPU访问外部的PCIe设备的流程:

  1. 读PCIe设备的配置空间,CPU依靠控制器的Region0
  2. 获取PCIe设备申请空间的大小,CPU分配空间(基地址为addr_cpu)
  3. 控制器将addr_cpu转化为addr_pcie,包装成TPL格式写给设备,进行配置
  4. CPU以后访问外部设备可以发出addr_cpu0,控制器就会自动转化为对应的addr_pcie0地址去访问该外部设备

那么控制器想要发送TPL,将CPU发送来的地址信息(region0)进行解析转化,就得去配置控制器上region0的寄存器:

  1. 配置region1~32的register
  2. 记录地址资源,用来分配给PCIe设备用的(分配给设备的地址资源,上面提到的流程的第2点)

2.设备树文件

shell 复制代码
pcie0: pcie@f8000000 {
        compatible = "rockchip,rk3399-pcie";
        #address-cells = <3>;
        #size-cells = <2>;
        ranges = <0x83000000 0x0 0xfa000000 0x0 0xfa000000 0x0 0x1e00000
                  0x81000000 0x0 0xfbe00000 0x0 0xfbe00000 0x0 0x100000>;
        reg = <0x0 0xf8000000 0x0 0x2000000>,
              <0x0 0xfd000000 0x0 0x1000000>;
        reg-s = "axi-base", "apb-base";
;

RK3399访问PCIe控制器时,CPU地址空间可以分为:

  • Client Register Set:地址范围 0xFD000000~0xFD7FFFFF,比如选择PCIe协议的版本(Gen1/Gen2)、电源控制等
  • Core Register Set :地址范围 0xFD800000~0xFDFFFFFF,所谓核心寄存器就是用来进行设置地址映射的寄存器等
  • Region 0:0xF8000000~0xF9FFFFFF , 32MB,用于访问外接的PCIe设备的配置空间
  • Region 1:0xFA000000~0xFA0FFFFF,1MB,用于地址转换
  • Region 2:0xFA100000~0xFA1FFFFF,1MB,用于地址转换
  • ......
  • Region 32:0xFBF00000~0xFBFFFFFF,1MB,用于地址转换

其中Region 0大小为32MB,Region1~31大小分别为1MB。

在设备树里都有体现(下列代码中,其他信息省略了):

  • reg属性里的0xf8000000:Region 0的地址

  • reg属性里的0xfd000000:PCIe控制器内部寄存器的地址

    • Client Register Set:地址范围 0xFD000000~0xFD7FFFFF
    • Core Register Set :地址范围 0xFD800000~0xFDFFFFFF
  • ranges属性里

    • 第1个0xfa000000:Region1~30的CPU地址空间首地址,用于内存读写
    • 第2个0xfa000000:Region1~30的PCI地址空间首地址,用于内存读写
    • 第1个0xfbe00000:Region31的CPU地址空间首地址,用于IO读写
    • 第2个0xfbe00000:Region31的PCI地址空间首地址,用于IO读写
  • Region32呢?在.c文件里用作"消息TLP"

对于memory内存读写:之前写入什么值,再去读的时候就是什么值

对于IO内存读写:之前写入什么值,去读的时候不一定就是原来的那个值

3.PCI驱动程序框架

4.驱动程序源码分析

  1. 从设备树中获取PCIe控制器寄存器和region0配置空间的基地址,然后才能去通过配置对应的region的寄存器,达到想要发送的TPL的数据内容格式,比如TPL中的某些字段(bus、device number、func、reg等)可以由addr_cpu来提供,其余位设置ob_addr寄存器,让其来补。因此最主要的就是获取设备树中的reg中的region0和寄存器的base_addr
  2. 从设备树中获取region1~32的addr_cpu和addr_pcie的基地址,根据flag去分别解析得到资源。------获取设备树中ranges有关IO/Memory的配置空间资源
  3. 既然知道了region1~32的addr_cpu_base和addr_pcie_base,那么肯定就是要去确定他们之间的隐射关系。这样后续配置好的pcie设备,CPU就可以发出相对应的addr_cpu映射得到addr_pcie,去直接操作设备。(一般情况下addr_cpu和addr_pcie会设置成一样的,TPL由addr_pcie构成,也就由addr_cpu构成的差不多)

4.1 region和寄存器的配置基地址

从设备树中获取控制器的region和寄存器的地址,去初始化,后续才能发出配置读/写的TPL

0xF8000000就是RK3399的Region0地址,用于 ECAM:ECAM是访问PCIe配置空间一种机制,PCIe配置空间大小是4k。即:只写读写0xF8000000这段空间,就可以只写读写PCIe设备的配置空间。

0xFD000000是RK3399 PCIe控制器本身的寄存器基地址。

Region0用与读写配置空间,它对应的寄存器要设置用于产生对应的TLP,函数调用关系如下:

plain 复制代码
rockchip_pcie_probe
    err = rockchip_pcie_init_port(rockchip);  // 初始化,配置读/写类型的TPL
      	rockchip_pcie_write(rockchip,
      			    (RC_REGION_0_ADDR_TRANS_L + RC_REGION_0_PASS_BITS),  //RC_REGION_0_PASS_BITS:25-1
                //CPU发出的地址为[24:0],但是TPL发出需要的(bus<<20) | (dev<<15) | (fun<<12) | (reg)就要28位了
                //其中24:0就是有cpu_addr填充上,而高三位则由控制器的寄存器ob_addr0的27:25来填充上,这样配置的TPL最基础的
      			    PCIE_CORE_OB_REGION_ADDR0);
      	rockchip_pcie_write(rockchip, RC_REGION_0_ADDR_TRANS_H,
      			    PCIE_CORE_OB_REGION_ADDR1);
      	rockchip_pcie_write(rockchip, 0x0080000a, PCIE_CORE_OB_REGION_DESC0);   
        //设置ob_desc0寄存器,默认发出配置 Type1类型的TPL
        
      	rockchip_pcie_write(rockchip, 0x0, PCIE_CORE_OB_REGION_DESC1);   

以配置写为例:

可以看到,对于对配置空间的写,通过bus、dev、func、reg来构造一个偏移地址busdev,busdev+region的基地址就能实现对PCI设备的配置写

4.2 确定CPU/PCI地址空间

对于memory内存读写:之前写入什么值,再去读的时候就是什么值

对于IO内存读写:之前写入什么值,去读的时候不一定就是原来的那个值

在PCIe设备树里有一个属性ranges,它里面含有多个range,每个range描述了:

  • flags:是内存还是IO
  • PCIe地址
  • CPU地址
  • 长度

先提前说一下怎么解析这些range,函数为for_each_of_pci_range,解析过程如下:


c 复制代码
rockchip_pcie_probe
	resource_size_t io_base;
    LIST_HEAD(res); // 资源链表

	// 解析设备树获得PCI host bridge的资源(CPU地址空间、PCI地址空间、大小)
	err = of_pci_get_host_bridge_resources(dev->of_node, 0, 0xff, &res, &io_base);
		// 解析 bus-range
		// 设备树里:  bus-range = <0x0 0x1f>;
		// 解析得到: bus_range->start= 0 , 
		//          bus_range->end = 0x1f, 
		//          bus_range->flags = IORESOURCE_BUS;
		// 放入前面的链表"LIST_HEAD(res)"
		err = of_pci_parse_bus_range(dev, bus_range);  
			pci_add_resource(resources, bus_range);

		// 解析 ranges
		// 设备树里: 
        //        ranges = <0x83000000 0x0 0xfa000000 0x0 0xfa000000 0x0 0x1e00000
        //                  0x81000000 0x0 0xfbe00000 0x0 0xfbe00000 0x0 0x100000>;
    	of_pci_range_parser_init
    		parser->range = of_get_property(node, "ranges", &rlen);
		for_each_of_pci_range(&parser, &range) {// 解析range            
            // 把range转换为resource
            // 第0个range
            // 		range->pci_space = 0x83000000,
            //		range->flags     = IORESOURCE_MEM,
            //		range->pci_addr  = 0xfa000000,
            //		range->cpu_addr  = 0xfa000000,
            //		range->size      = 0x1e00000,
            // 转换得到第0个res:
            // 		res->flags = range->flags = IORESOURCE_MEM;
            // 		res->start = range->cpu_addr = 0xfa000000;
            // 		res->end = res->start + range->size - 1 = (0xfa000000+0x1e00000-1);
            // ---------------------------------------------------------------
            // 第1个range
            // 		range->pci_space = 0x81000000,
            //		range->flags     = IORESOURCE_IO,
            //		range->pci_addr  = 0xfbe00000,
            //		range->cpu_addr  = 0xfbe00000,
            //		range->size      = 0x100000,
            // 转换得到第1个res:
            // 		res->flags = range->flags = IORESOURCE_MEM;
            // 		res->start = range->cpu_addr = 0xfbe00000;
            // 		res->end = res->start + range->size - 1 = (0xfbe00000+0x100000-1);
            err = of_pci_range_to_resource(&range, dev, res); 

            // 在链表中增加resource
            // 第0个resource:
            //		注意第3个参数: offset = cpu_addr - pci_addr = 0xfa000000 - 0xfa000000 = 0
            // 第1个resouce
            //		注意第3个参数: offset = cpu_addr - pci_addr = 0xfbe00000 - 0xfbe00000 = 0
            pci_add_resource_offset(resources, res,	res->start - range.pci_addr);

        }

    /* Get the I/O and memory ranges from DT */
    resource_list_for_each_entry(win, &res) {
        rockchip->io_bus_addr = io->start - win->offset;   // 0xfbe00000, cpu addr
        rockchip->mem_bus_addr = mem->start - win->offset; // 0xfba00000, cpu addr
        rockchip->root_bus_nr = win->res->start; // 0
    }

	bus = pci_scan_root_bus(&pdev->dev, 0, &rockchip_pcie_ops, rockchip, &res);

	pci_bus_add_devices(bus);

4.3 建立CPU/PCI地址空间的映射

配置地址转转换单元,调用关系如下:

plain 复制代码
rockchip_pcie_probe
    err = rockchip_cfg_atu(rockchip);
                /* MEM映射: Region1~30 */
                // rockchip->mem_bus_addr = 0xfa000000
              	// rockchip->mem_size     = 0x1e00000
              	// 设置Region1、2、......30的映射关系
                for (reg_no = 0; reg_no < (rockchip->mem_size >> 20); reg_no++) {
                    err = rockchip_pcie_prog_ob_atu(rockchip, reg_no + 1,
                                    AXI_WRAPPER_MEM_WRITE,
                                    20 - 1,
                                    rockchip->mem_bus_addr +
                                    (reg_no << 20),
                                    0);
                    if (err) {
                        dev_err(dev, "program RC mem outbound ATU failed\n");
                        return err;
                    }
                }
                
                /* IO映射: Region31 */
                // rockchip->io_bus_addr = 0xfbe00000
              	// rockchip->io_size     = 0x100000
              	// 设置Region31的映射关系
                offset = rockchip->mem_size >> 20;
                for (reg_no = 0; reg_no < (rockchip->io_size >> 20); reg_no++) {
                    err = rockchip_pcie_prog_ob_atu(rockchip,
                                    reg_no + 1 + offset,
                                    AXI_WRAPPER_IO_WRITE,
                                    20 - 1,
                                    rockchip->io_bus_addr +
                                    (reg_no << 20),
                                    0);
                    if (err) {
                        dev_err(dev, "program RC io outbound ATU failed\n");
                        return err;
                    }
                }

                /* 用于消息传输: Region32 */
                rockchip_pcie_prog_ob_atu(rockchip, reg_no + 1 + offset,
                              AXI_WRAPPER_NOR_MSG,
                              20 - 1, 0, 0);

                rockchip->msg_bus_addr = rockchip->mem_bus_addr +
                                ((reg_no + offset) << 20);

何一个Region,都有对应的寄存器:

所谓建立CPU和PCI地址空间的映射,就是设置Region对应的寄存器,都是使用函数rockchip_pcie_prog_ob_atu

4.4 总结

1. 从设备树中获取PCIe控制器寄存器和Region 0配置空间的基地址

设备树(Device

Tree)中的reg属性定义了硬件资源的地址映射。对于PCIe控制器来说,通常在reg属性中包含控制器的寄存器基地址和对应配置空间的地址区域。Region

0通常用于PCIe配置空间(Configuration Space)的访问,也即控制器本身的寄存器设置。

步骤:

  • 通过读取设备树中对应PCIe节点的reg属性,获取PCIe控制器寄存器的基地址Region 0的基地址
  • PCIe控制器的寄存器可以配置多个Region(如region0对应配置空间,region1~32用于PCIe映射)。你需要首先通过寄存器配置来初始化并启用对应的Region(如Region
    0)。

在配置PCIe控制器的寄存器时,你需要设置TPL字段(如busdevice numberfuncreg等)。正如你所描述的,CPU地址(addr_cpu可提供这些信息,同时 Outbound(OB)地址寄存器(ob_addr)负责映射CPU地址到PCIe地址空间。

2. 从设备树中获取Region 1~32的 **addr_cpu** **addr_pcie**的基地址

PCIe控制器的多个Region(如Region1~32)通常用于内存或I/O空间的映射,映射的是CPU地址和PCIe地址之间的对应关系。设备树中的ranges属性指定了这些映射关系,包括addr_cpuaddr_pcie的基地址。

步骤:

  • 解析设备树中对应PCIe控制器节点的ranges属性,提取Region 1~32的 addr_cpu addr_pcie的基地址
  • ranges属性中可能包含标志位(flags),用于区分不同类型的资源(如I/O空间或内存空间)。你需要根据这些标志位去判断每个区域是用于I/O、内存或其他类型的访问。

3. 确定 **addr_cpu_base** **addr_pcie_base**的映射关系

通过设置多个Region(如Region1~32)之间的映射关系,CPU可以通过**addr_cpu**访问PCIe设备的寄存器或内存空间。在配置PCIe设备时,CPU发出的addr_cpu会通过Region的映射机制被转换为addr_pcie,从而在PCIe设备上实现操作。

一般来说,addr_cpu addr_pcie可以设置成相同,以简化访问映射关系,但这并不是必须的。重要的是,PCIe控制器中的Region寄存器需要正确配置,以确保CPU访问时,能正确地转换成对应的PCIe地址。

TPL构成

TPL(Transaction Pending

List)中的字段会根据PCIe地址 (通常是addr_pcie)来构成。如果addr_cpuaddr_pcie是相同的,那么从逻辑上看,TPL可以由addr_cpu直接构成。然而,无论如何,最终的TPL是与PCIe设备交互的PCIe地址,而不是CPU直接使用的虚拟地址或物理地址。因此,在事务处理中,PCIe控制器会处理地址转换,并将相应的PCIe地址信息写入TPL。

  1. 通过设备树的reg获取PCIe控制器寄存器和Region 0配置空间的基地址,并通过这些地址配置PCIe控制器的寄存器,设置TPL中字段(如busdevice numberfunc等)。
  2. 通过设备树的ranges获取Region 1~32的addr_cpuaddr_pcie基地址,并根据flags标志解析出不同类型的资源映射。
  3. 通过PCIe控制器的Region寄存器配置,确定addr_cpuaddr_pcie的映射关系。CPU发出的addr_cpu可以映射到相应的addr_pcie,实现对PCIe设备的访问。
  4. TPL由PCIe地址构成,但如果addr_cpuaddr_pcie相同,则TPL可以近似由addr_cpu构成。

这一过程确保了CPU发出的请求能够通过PCIe控制器映射到正确的PCIe设备地址,并且TPL可以正确地记录和管理事务。

相关推荐
嵌入小生0075 小时前
基于Linux系统下的C语言程序错误及常见内存问题调试方法教程(嵌入式-Linux-C语言)
linux·c语言·开发语言·嵌入式·小白·内存管理调试·程序错误调试
松涛和鸣5 小时前
DAY63 IMX6ULL ADC Driver Development
linux·运维·arm开发·单片机·嵌入式硬件·ubuntu
想放学的刺客8 小时前
单片机嵌入式试题(第23期)嵌入式系统电源管理策略设计、嵌入式系统通信协议栈实现要点两个全新主题。
c语言·stm32·单片机·嵌入式硬件·物联网
猫猫的小茶馆9 小时前
【Linux 驱动开发】五. 设备树
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu·硬件工程
jghhh0110 小时前
基于上海钜泉科技HT7017单相计量芯片的参考例程实现
科技·单片机·嵌入式硬件
恶魔泡泡糖10 小时前
51单片机外部中断
c语言·单片机·嵌入式硬件·51单片机
意法半导体STM3210 小时前
【官方原创】如何基于DevelopPackage开启安全启动(MP15x) LAT6036
javascript·stm32·单片机·嵌入式硬件·mcu·安全·stm32开发
v_for_van11 小时前
STM32低频函数信号发生器(四通道纯软件生成)
驱动开发·vscode·stm32·单片机·嵌入式硬件·mcu·硬件工程
电化学仪器白超11 小时前
③YT讨论
开发语言·python·单片机·嵌入式硬件
乡野码圣11 小时前
【RK3588 Android12】硬件中断IRQ
单片机·嵌入式硬件