1. 程式人生 > 實用技巧 >【原創】Linux PCI驅動框架分析(三)

【原創】Linux PCI驅動框架分析(三)

背 景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

先回顧一下PCIe的架構圖:

  • 本文將講PCIe Host的驅動,對應為Root Complex部分,相當於PCI的Host Bridge部分;
  • 本文會選擇Xilinx的nwl-pcie來進行分析;
  • 驅動的編寫整體偏簡單,往現有的框架上套就可以了,因此不會花太多筆墨,點到為止;

2. 流程分析

  • 但凡涉及到驅動的分析,都離不開驅動模型的介紹,驅動模型的實現讓具體的驅動開發變得更容易;
  • 所以,還是回顧一下上篇文章提到的驅動模型:Linux核心建立了一個統一的裝置模型,分別採用匯流排、裝置、驅動三者進行抽象,其中裝置與驅動都掛在總線上,當有新的設備註冊或者新的驅動註冊時,匯流排會去進行匹配操作(match函式),當發現驅動與裝置能進行匹配時,就會執行probe函式的操作;

  • 《Linux PCI驅動框架分析(二)》中提到過PCI裝置、PCI匯流排和PCI驅動的建立,PCI裝置和PCI驅動掛接在PCI總線上,這個理解很直觀。針對PCIe的控制器來說,同樣遵循裝置、匯流排、驅動的匹配模型,不過這裡的匯流排是由虛擬匯流排platform
    匯流排來替代,相應的裝置和驅動分別為platform_deviceplatform_driver

那麼問題來了,platform_device是在什麼時候建立的呢?那就不得不提到Device Tree裝置樹了。

2.1 Device Tree

  • 裝置樹用於描述硬體的資訊,包含節點各類屬性,在dts檔案中定義,最終會被編譯成dtb檔案載入到記憶體中;
  • 核心會在啟動過程中去解析dtb檔案,解析成device_node描述的Device Tree
  • 根據device_node節點,建立platform_device結構,並最終註冊進系統,這個也就是PCIe Host裝置的建立過程;

我們看看PCIe Host的裝置樹內容:

pcie: pcie@fd0e0000 {
	compatible = "xlnx,nwl-pcie-2.11";
	status = "disabled";
	#address-cells = <3>;
	#size-cells = <2>;
	#interrupt-cells = <1>;
	msi-controller;
	device_type = "pci";
    
	interrupt-parent = <&gic>;
	interrupts = <0 118 4>,
		     <0 117 4>,
		     <0 116 4>,
		     <0 115 4>,	/* MSI_1 [63...32] */
		     <0 114 4>;	/* MSI_0 [31...0] */
	interrupt-names = "misc", "dummy", "intx", "msi1", "msi0";
	msi-parent = <&pcie>;
    
	reg = <0x0 0xfd0e0000 0x0 0x1000>,
	      <0x0 0xfd480000 0x0 0x1000>,
	      <0x80 0x00000000 0x0 0x1000000>;
	reg-names = "breg", "pcireg", "cfg";
	ranges = <0x02000000 0x00000000 0xe0000000 0x00000000 0xe0000000 0x00000000 0x10000000	/* non-prefetchable memory */
		  0x43000000 0x00000006 0x00000000 0x00000006 0x00000000 0x00000002 0x00000000>;/* prefetchable memory */
	bus-range = <0x00 0xff>;
    
	interrupt-map-mask = <0x0 0x0 0x0 0x7>;
	interrupt-map =     <0x0 0x0 0x0 0x1 &pcie_intc 0x1>,
			    <0x0 0x0 0x0 0x2 &pcie_intc 0x2>,
			    <0x0 0x0 0x0 0x3 &pcie_intc 0x3>,
			    <0x0 0x0 0x0 0x4 &pcie_intc 0x4>;
    
	pcie_intc: legacy-interrupt-controller {
		interrupt-controller;
		#address-cells = <0>;
		#interrupt-cells = <1>;
	};
};

關鍵欄位描述如下:

  • compatible:用於匹配PCIe Host驅動;
  • msi-controller:表示是一個MSI(Message Signaled Interrupt)控制器節點,這裡需要注意的是,有的SoC中斷控制器使用的是GICv2版本,而GICv2並不支援MSI,所以會導致該功能的缺失;
  • device-type:必須是"pci"
  • interrupts:包含NWL PCIe控制器的中斷號;
  • interrupts-namemsi1, msi0用於MSI中斷,intx用於舊式中斷,與interrupts中的中斷號對應;
  • reg:包含用於訪問PCIe控制器操作的暫存器實體地址和大小;
  • reg-name:分別表示Bridge registersPCIe Controller registersConfiguration space region,與reg中的值對應;
  • ranges:PCIe地址空間轉換到CPU的地址空間中的範圍;
  • bus-range:PCIe匯流排的起始範圍;
  • interrupt-map-maskinterrupt-map:標準PCI屬性,用於定義PCI介面到中斷號的對映;
  • legacy-interrupt-controller:舊式的中斷控制器;

2.2 probe流程

  • 系統會根據dtb檔案建立對應的platform_device並進行註冊;
  • 當驅動與裝置通過compatible欄位匹配上後,會呼叫probe函式,也就是nwl_pcie_probe

看一下nwl_pcie_probe函式:

  • 通常probe函式都是進行一些初始化操作和註冊操作:
    1. 初始化包括:資料結構的初始化以及裝置的初始化等,裝置的初始化則需要獲取硬體的資訊(比如暫存器基地址,長度,中斷號等),這些資訊都從DTS而來;
    2. 註冊操作主要是包含中斷處理函式的註冊,以及通常的裝置檔案註冊等;

  • 針對PCI控制器的驅動,核心的流程是需要分配並初始化一個pci_host_bridge結構,最終通過這個bridge去列舉PCI總線上的所有裝置;
  • devm_pci_alloc_host_bridge:分配並初始化一個基礎的pci_hsot_bridge結構;
  • nwl_pcie_parse_dt:獲取DTS中的暫存器資訊及中斷資訊,並通過irq_set_chained_handler_and_data設定intx中斷號對應的中斷處理函式,該處理函式用於中斷的級聯;
  • nwl_pcie_bridge_init:硬體的Controller一堆設定,這部分需要去查閱Spec,瞭解硬體工作的細節。此外,通過devm_request_irq註冊misc中斷號對應的中斷處理函式,該處理函式用於控制器自身狀態的處理;
  • pci_parse_request_of_pci_ranges:用於解析PCI匯流排的匯流排範圍和總線上的地址範圍,也就是CPU能看到的地址區域;
  • nwl_pcie_init_irq_domainmwl_pcie_enable_msi與中斷級聯相關,下個小節介紹;
  • pci_scan_root_bus_bridge:對總線上的裝置進行掃描列舉,這個流程在Linux PCI驅動框架分析(二)中分析過。brdige結構體中的pci_ops欄位,用於指向PCI的讀寫操作函式集,當具體掃描到裝置要讀寫配置空間時,呼叫的就是這個函式,由具體的Controller驅動實現;

2.3 中斷處理

PCIe控制器,通過PCIe匯流排連線各種裝置,因此它本身充當一箇中斷控制器,級聯到上一層的中斷控制器(比如GIC),如下圖:

  • PCIe匯流排支援兩種中斷的處理方式:
    1. Legacy Interrupt:匯流排提供INTA#, INTB#, INTC#, INTD#四根中斷訊號,PCI裝置藉助這四根訊號使用電平觸發方式提交中斷請求;
    2. MSI(Message Signaled Interrupt) Interrupt:基於訊息機制的中斷,也就是往一個指定地址寫入特定訊息,從而觸發一箇中斷;

針對兩種處理方式,NWL PCIe驅動中,實現了兩個irq_chip,也就是兩種方式的中斷控制器:

  • irq_domain對應一箇中斷控制器(irq_chip),irq_domain負責將硬體中斷號對映到虛擬中斷號上;
  • 來一張舊圖吧,具體文章可以去參考中斷子系統相關文章;

再來看一下nwl_pcie_enable_msi函式:

  • 在該函式中主要完成的工作就是設定級聯的中斷處理函式,級聯的中斷處理函式中最終會去呼叫具體的裝置的中斷處理函式;

所以,稍微彙總一下,作為兩種不同的中斷處理方式,套路都是一樣的,都是建立irq_chip中斷控制器,為該中斷控制器新增irq_domain,具體裝置的中斷響應流程如下:

  1. 裝置連線在PCI總線上,觸發中斷時,通過PCIe控制器充當的中斷控制器路由到上一級控制器,最終路由到CPU;
  2. CPU在處理PCIe控制器的中斷時,呼叫它的中斷處理函式,也就是上文中提到過的nwl_pcie_leg_handlernwl_pcie_msi_handler_high,和nwl_pcie_leg_handler_low
  3. 在級聯的中斷處理函式中,呼叫chained_irq_enter進入中斷級聯處理;
  4. 呼叫irq_find_mapping找到具體的PCIe裝置的中斷號;
  5. 呼叫generic_handle_irq觸發具體的PCIe裝置的中斷處理函式執行;
  6. 呼叫chained_irq_exit退出中斷級聯的處理;

2.4 總結

  • PCIe控制器驅動,各家的IP實現不一樣,驅動的差異可能會很大,單獨分析一個驅動畢竟只是個例,應該去掌握背後的通用框架;
  • 各類驅動,大體都是硬體初始化配置,資源申請註冊,核心是處理與硬體的互動(一般就是中斷的處理),如果需要使用者來互動的,則還需要註冊裝置檔案,實現一堆file_operation操作函式集;
  • 好吧,我個人不太喜歡分析某個驅動,草草收場了;

下篇開始,繼續迴歸到虛擬化,期待一下吧。

參考

Documentation/devicetree/bindings/pci/xlinx-nwl-pcie.txt

歡迎關注個人公眾號,不定期分享技術文章: