往期內容
本文章相關專欄往期內容,PCI/PCIe子系統專欄:
- 嵌入式系統的內存訪問和總線通信機制解析、PCI/PCIe引入
- 深入解析非橋PCI設備的訪問和配置方法
- PCI橋設備的訪問方法、軟件角度講解PCIe設備的硬件結構
- 深入解析PCIe設備事務層與配置過程
- PCIe的三種路由方式
- PCI驅動與AXI總線框架解析(RK3399)
- 深入解析PCIe地址空間與寄存器機制:從地址映射到TLP生成的完整流程
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.回顧
- 2.設備樹文件
- 3.PCI驅動程序框架
- 4.驅動程序源碼分析
- 4.1 region和寄存器的配置基地址
- 4.2 確定CPU/PCI地址空間
- 4.3 建立CPU/PCI地址空間的映射
- 4.4 總結
資料
開發板資料:
- https://wiki.t-firefly.com/zh_CN/ROC-RK3399-PC-PLUS/
分析的文件:
linux-4.4_rk3399\drivers\pci\host\pcie-rockchip.c
📎pcie-rockchip.c
1.回顧
CPU訪問外部的PCIe設備的流程:
- 讀PCIe設備的配置空間,CPU依靠控制器的Region0
- 獲取PCIe設備申請空間的大小,CPU分配空間(基地址為addr_cpu)
- 控制器將addr_cpu轉化為addr_pcie,包裝成TPL格式寫給設備,進行配置
- CPU以后訪問外部設備可以發出addr_cpu0,控制器就會自動轉化為對應的addr_pcie0地址去訪問該外部設備
那么控制器想要發送TPL,將CPU發送來的地址信息(region0)進行解析轉化,就得去配置控制器上region0的寄存器:
- 配置region1~32的register
- 記錄地址資源,用來分配給PCIe設備用的(分配給設備的地址資源,上面提到的流程的第2點)
2.設備樹文件
pcie0: pcie@f8000000 {compatible = "rockchip,rk3399-pcie";#address-cells = <3>;#size-cells = <2>;ranges = <0x83000000 0x0 0xfa000000 0x0 0xfa000000 0x0 0x1e000000x81000000 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.驅動程序源碼分析
- 從設備樹中獲取PCIe控制器寄存器和region0配置空間的基地址,然后才能去通過配置對應的region的寄存器,達到想要發送的TPL的數據內容格式,比如TPL中的某些字段(bus、device number、func、reg等)可以由addr_cpu來提供,其余位設置ob_addr寄存器,讓其來補。因此最主要的就是獲取設備樹中的reg中的region0和寄存器的base_addr
- 從設備樹中獲取region1~32的addr_cpu和addr_pcie的基地址,根據flag去分別解析得到資源。------獲取設備樹中ranges有關IO/Memory的配置空間資源
- 既然知道了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,函數調用關系如下:
rockchip_pcie_probeerr = rockchip_pcie_init_port(rockchip); // 初始化,配置讀/寫類型的TPLrockchip_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類型的TPLrockchip_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
,解析過程如下:
rockchip_pcie_proberesource_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_initparser->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 = 0pci_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 addrrockchip->mem_bus_addr = mem->start - win->offset; // 0xfba00000, cpu addrrockchip->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地址空間的映射
配置地址轉轉換單元,調用關系如下:
rockchip_pcie_probeerr = 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字段(如
bus
、device number
、func
、reg
等)。正如你所描述的,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_cpu
和addr_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_cpu
和addr_pcie
是相同的,那么從邏輯上看,TPL可以由addr_cpu
直接構成。然而,無論如何,最終的TPL是與PCIe設備交互的PCIe地址,而不是CPU直接使用的虛擬地址或物理地址。因此,在事務處理中,PCIe控制器會處理地址轉換,并將相應的PCIe地址信息寫入TPL。
- 通過設備樹的
reg
獲取PCIe控制器寄存器和Region 0配置空間的基地址,并通過這些地址配置PCIe控制器的寄存器,設置TPL中字段(如bus
、device number
、func
等)。 - 通過設備樹的
ranges
獲取Region 1~32的addr_cpu
和addr_pcie
基地址,并根據flags
標志解析出不同類型的資源映射。 - 通過PCIe控制器的Region寄存器配置,確定
addr_cpu
與addr_pcie
的映射關系。CPU發出的addr_cpu
可以映射到相應的addr_pcie
,實現對PCIe設備的訪問。 - TPL由PCIe地址構成,但如果
addr_cpu
和addr_pcie
相同,則TPL可以近似由addr_cpu
構成。
這一過程確保了CPU發出的請求能夠通過PCIe控制器映射到正確的PCIe設備地址,并且TPL可以正確地記錄和管理事務。