Linux SPI 子系統(x86平臺)

Linux SPI 子系統(x86平臺)

前言

寫文在於交流和傳播知識,本人才粗學淺,還請多多指教,板磚輕拍。

網絡上很多 Linux SPI 驅動框架參考資料,但這些資料大部分以講解源碼爲主,雖然 Linux 內核源碼很清晰美妙,但內核版本衆多,細枝末節處差異很大,不利於初學者進行對比學習。另外初學者需要閱讀大量的源碼才能明晰程序流程,這就增加了系統瞭解 SPI 子系統的難度,很難快速完成具體開發工作。因此本文主要以文字描述爲主,源碼爲輔,重點在於理清與 SPI 有關的相關概念和 SPI 子系統的初始化的流程。

另外,本文主要描述 x86 體系下的 SPI 框架,也可作爲 ARM 體系下 SPI 框架的參考,因爲兩種框架下的概念和原理都是相通的,只是有些地方的具體實現不同。

總述

SPI 是一種總線通訊協議,由總線控制器和從設備構成。Linux SPI 驅動包含兩個部分,分別用於驅動 SPI 總線控制器和從設備,內核中的 SPI 子系統爲這兩種驅動提供了開發框架。

在系統探測到設備並掛載相關驅動的過程中,涉及到設備發現(探測或枚舉)和驅動匹配(Match),對於總線上的設備,需要知道該設備掛載在哪條總線上,因此就需要知道該設備的總線號。在 SPI 子系統中,這些動作由 SPI Board Info(ACPI 或 Device Tree 中與 SPI 從設備有關的部分)、SPI 控制器驅動以及SPI 從設備驅動共同完成,它們各自的分工如下:

  1. SPI Board Info(ACPI 或 Device Tree 中與 SPI 從設備有關的部分):用於聲明設備的存在,提供 SPI 從設備所在的總線號、片選號以及用於與 SPI 從設備匹配的 modalias 字段(與 SPI 從設備中的 name 字段匹配)。
  2. SPI 控制器驅動:用於驅動 SPI 總線控制器。
  3. SPI 從設備驅動:用於驅動 SPI 從設備。

接下來將從 SPI 硬件系統與軟件抽象之間的關係,以及 SPI 驅動的探測過程兩個方面展開說明。

SPI 硬件系統與軟件抽象之間的關係

電子系統中有很多外設,有像 GPIO 這樣簡單的設備,也有像 LCD 控制器這樣複雜的。有一類特殊的外設,用於實現總線通信,通常以控制器的角色出現,被稱爲總線控制器,例如 UART 控制器、以太網控制器,以及本文的主角 SPI 控制器。

SPI 控制器用於實現 SPI 總線通訊,該通訊採用主從通訊形式,一個主設備可以掛載多個 SPI 從設備,每個從設備通過片選(CS)信號來選定。核心 CPU 通過 SPI 控制器與從設備進行交互。SPI 控制器常常以某種形式掛載到核心 CPU 上,具體而言,在 x86 平臺上,SPI 總線控制器通過 PCI 總線掛接到主 CPU 上;而 ARM 平臺上往往是以片載外設的形式出現,直接掛接在 SOC 或 MCU 片內的系統總線上。

圖 1 SPI 總線結構

SPI 通訊離不開 SPI 總線控制器和從設備,因此在 Linux 系統中就需要爲這兩種對象開發驅動程序。用於驅動 SPI 總線控制器的驅動稱爲 SPI 控制器驅動,而用於驅動 SPI 從設備的驅動稱爲 SPI 設備驅動(個人覺得叫 SPI 從設備驅動似乎更貼切些,爲便於區分,後文都將 SPI 設備驅動稱作 SPI 從設備驅動)。從另一個角度來說,SPI 從設備驅動的工作就是基於 SPI 通訊來實現對從設備的控制和訪問,相當於實現 SPI 通信協議與具體的設備控制/訪問協議的相互轉換,所以 SPI 從設備驅動又稱爲 SPI 協議驅動。從名稱上可以看出,SPI 控制器驅動屬於總線控制器驅動,然而 SPI 從設備是多種多樣的,可能是 char 設備(如 spidev),也可能是 block 設備(如 SPI Flash),因此 SPI 從設備驅動也是有多種類型的。一條 SPI 總線(對應一個 SPI 總線控制器)上可以掛接多個 SPI 從設備,與之對應的是:一個 SPI 控制器驅動也可以掛接多個 SPI 從設備驅動。下圖,詳細描述了 Linux 系統中 SPI 驅動框架,以及各部分之間的相互關係。

圖 2 Linux SPI 子系統框架

  1. SPI 控制器驅動:相當於 master/controller,由 spi_master 描述,用於驅動 SPI 總線控制器,實現其初始化、中斷回調等功能,在 /sys 目錄下創建節點,不提供 file operation 接口,驅動類型爲 SPI 總線控制器驅動。

  2. SPI 協議驅動(SPI 從設備驅動): 相當於 slave,用於驅動 SPI 總線上所掛接的設備,在 /dev 目錄下創建設備節點,提供 open、read、write、ioctl 等 file operation 接口,驅動類型由設備本身所屬設備驅動類型描述(char/block/…)。

有一些 SPI 從設備並不是由內核來爲其提供驅動的,而是交給用戶態去處理,此時需要在內核爲這些從設備導出用戶態操作的接口,Linux 內核爲此提供了統一的內核程序——spidev,來完成此項工作。被 spidev 導出的接口以 spidev<總線號>.<子設備號/片選序號> 的文件形式出現在 /dev 系統目錄下,如:spidev1.2 便是第二個 SPI 總線上的第三個從設備。之後就可以在用戶態通過 open、read、write、ioctl、close 等標準接口操作 spidev 了。spidev 相當於一種特殊的 SPI 從設備驅動,即在 SPI 子系統框架下實現的字符設備,該設備能夠從用戶態進行直接 SPI 通訊。

瞭解 Linux SPI 子系統中兩個主要角色(SPI 控制器驅動和 SPI 從設備驅動)後,就需要知道他們是如何被初始化的,以及如何與具體設備匹配上的,這涉及到 SPI 總線控制器、SPI 從設備的枚舉和發現,以及驅動匹配等。

SPI 驅動的 Probe 和 Match 過程

有些總線設備是可以自動枚舉到的,如 PCI 總線可以通過 BDF(總線號 Bus、設備號 Device 和功能號 Function) 來枚舉設備並通過 Device ID 和 Vendor ID 來匹配驅動程序。然而有很多總線不能自動枚舉設備並匹配驅動程序。因此內核提供了幾種配置表用於聲明某些設備的存在,對於 ARM 平臺目前使用 Device Tree,而 x86 平臺有 ACPI 表,或者乾脆以平臺設備的形式註冊 Board Info。Linux 內核會掃描這些表或已註冊的 Board Info,並根據其中的信息觸發對應驅動程序的 Probe 流程。這個過程由系統內核框架實現的,不需要設備驅動開發人員關心,只需要寫好 Device Tree,提供好 ACPI 表,或註冊好 Board Info 即可,而這一般會有 Demo 可以參考。

在 x86 平臺下,SPI 總線控制器作爲 PCI 設備接入,由 PCI 枚舉來探測到設備,並通過 Device ID 和 Vendor ID 來匹配 SPI 控制器驅動(Linux 內核程序結合硬件機制實現了該功能,具體 PCI 設備枚舉和匹配過程可以參考 PCI 方面的專業資料),觸發其 Probe 程序;然而 SPI 從設備是無法自動探測到的,需要在 ACPI 表中聲明這些從設備(x86 平臺),或在平臺設備中註冊相關的 SPI Board Info,以便內核能夠匹配到正確的 SPI 設備驅動並觸發其 Probe 程序。

以平臺設備爲例,看下 SPI 從設備的探測流程。由於 SPI 平臺設備是通過 SPI Board Info 來聲明設備存在的,所以先介紹 spi_board_info 的具體結構。然後再根據 SPI Board Info 一步步分析 SPI 從設備的 Probe 過程。

注:x86 系統的平臺設備聲明在 arch->x86->platform 下。

SPI Board Info

對於 spi_board_info,需要重點關注 bus_num 和 modalias,bus_num 將設備與對應的總線控制器驅動匹配,modalias 用於與 SPI 從設備匹配。以下是 spi_board_info 的具體結構,注意每個成員後面的註釋:

struct spi_board_info {
    char    modalias[SPI_NAME_SIZE];        // 用於與 SPI 從設備驅動匹配,觸發其 Probe 過程.
    const void   *platform_data;
    const struct property_entry *properties;
    void    *controller_data;
    int     irq;
    u32     max_speed_hz;
    u16     bus_num;        // 用於與 SPI 控制器中的 bus num 匹配,對應 spidev<總線號>.<子設備號/片選序號> 中的 總線號.
    u16     chip_select;    // 指定片選序號,將作爲 spidev<總線號>.<子設備號/片選序號> 中的 子設備號/片選序號.
    u16     mode;           // SPI 有四種模式,見下圖,具體參考 SPI 協議相關資料.
};

圖 3 SPI 四種模式
接下來,根據 bus_num 和 modalias 分析下從設備驅動的 Match 和 Probe 流程。

從設備驅動的 Match 和 Probe 過程

系統啓動時,先執行 arch_initcall 中的定義的板級初始化程序,由該程序完成 spi_board_info 的註冊,並由此形成一張 SPI 從設備列表。之後,在 x86 平臺下,當系統進行 PCI 設備枚舉時,將發現 SPI 總線控制器,並調用與之對應的 SPI 總線控制器驅動中的 Probe 程序,該 Probe 程序繼續調用 regist 函數來註冊 SPI master/controller,在這個註冊函數中比較當前 SPI 控制器的總線號,如果與 SPI 從設備列表中的總線號對應,則將該從設備掛接到這個控制器上。在 4.19.23 版本內核中,以 pxa2xx 爲例,可用以下函數調用關係來描述:

spi_register_board_info(){ /*Provide bus_num*/}  <--+
                                                    |
pxa2xx_spi_probe()                                  |
{                                                   |
    devm_spi_register_controller()                  |
    {                                               |
        spi_register_controller()                   |
        {                                           |
            spi_match_controller_to_boardinfo()     |
            {                                   <---+
                /**
                 * controller 的 bus_num 與
                 * regist 的 spi_board_info
                 * 中的 bus_num 一致則執行
                 * spi_new_device()
                 * 並在 /sys 目錄下創建節點。
                 */
            }
        }
    }
}

一旦 SPI 從設備掛載到了對應的總線上,系統就會查找有無匹配的 SPI 從設備驅動,並觸發其 Probe 過程。判斷 SPI 從設備驅動是否與聲明的平臺設備相匹配,是通過比較 spi_driver 結構體中的 name 字段與 spi_board_info 結構體中的 modalias 字段是否一致來完成的,如果一致,則調用 SPI 從設備的 Probe 程序。SPI 從設備不但要繼續完成 Match 和 Probe 過程,創建具體的設備對象,還要實現從設備的控制和訪問,並向上爲用戶態提供設備訪問接口,創建 /dev 目錄下的設備節點等。下面以 spidev 爲例,看看 spi_driver 的基本結構,以及 SPI 從設備驅動的接口和主要工作:

/**
 * File operation.
 */
static ssize_t spidev_write(...) {...}
static ssize_t spidev_read(...) {...}
static long spidev_ioctl(...) {...}
static int spidev_open(...) {...}
static int spidev_release(...) {...}

static const struct file_operations spidev_fops = {
    .owner = THIS_MODULE,
    .write = spidev_write,
    .read =  spidev_read,
    .unlocked_ioctl = spidev_ioctl,
    .open = spidev_open,
    .release = spidev_release,
    ...
};

/**
 * Probe and remove.
 */
static int spidev_probe(struct spi_device *spi)
{
    ...
    // 形成上文說的 /dev/spidev<總線號>.<子設備號/片選序號> 文件.
    dev = device_create(spidev_class, &spi->dev, spidev->devt,
                spidev, "spidev%d.%d",
                spi->master->bus_num, spi->chip_select);
    ...
}

static int spidev_remove(...) {...}

static struct spi_driver spidev_spi_driver = {
    .driver = {
        // 通過 name 來匹配.
        .name = "spidev",
        // 通過 Device Tree 來匹配.
        .of_match_table = of_match_ptr(spidev_dt_ids),
        // 通過 ACPI 表來匹配.
        .acpi_match_table = ACPI_PTR(spidev_acpi_ids),
    },
    .probe = spidev_probe,
    .remove = spidev_remove,
};


/**
 * Init and deinit.
 */
static int __init spidev_init(void)
{
    ...
    // 註冊字符設備
    register_chrdev(SPIDEV_MAJOR, "spi", &spidev_fops);
    class_create(THIS_MODULE, "spidev");
    spi_register_driver(&spidev_spi_driver);
    ...
}
module_init(spidev_init);

static void __exit spidev_exit(void)
{
    ...
    spi_unregister_driver(&spidev_spi_driver);
    class_destroy(spidev_class);
    unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
    ...
}
module_exit(spidev_exit);

可以看出,代碼首先註冊了 File Operation 方法用於實現用戶態訪問接口,另一方面則通過其 SPI 控制器來訪問和控制實際設備。而從 spi_driver 結構體不難看出,spidev 不但可以驅動已註冊的 SPI 平臺設備,還可以驅動以 Device Tree 或 ACPI 表聲明的 SPI 從設備(匹配 Device Tree 或 ACPI 表)。

SPI 核心層

有了 SPI 總線控制器驅動和 SPI 從設備驅動,SPI 子系統就可以工作了。但是我們發現,對於 SPI 子系統,有很多核心的代碼是完全通用的,把這些共通代碼抽取出來,便構建成了 SPI 核心層。

對於開發的一些簡單指導

基於當前的 SPI 子系統框架,一般有兩種類型的設備驅動需要開發——SPI 控制器驅動和 SPI 從設備驅動。SPI 控制器驅動一般由芯片供應商或開源社區會提供,下游的開發者只需要實現 SPI 從設備驅動即可。對於 SPI 控制器驅動,可以參考 pxa2xx 這個驅動程序;對於 SPI 從設備驅動可以參考 spidev 這個驅動程序。

總結

最後曬一張來自網友的大圖(來源見圖中水印),系統總結了 SPI 子系統的 Probe 過程和各部分的功能:

圖 4 SPI 子系統總結

參考資料

  1. linux設備模型之spi子系統
  2. PXA2xx SPI on SSP driver HOWTO
  3. Linux設備驅動剖析之SPI(一)
  4. Linux設備驅動剖析之SPI(二)
  5. Linux設備驅動剖析之SPI(三)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章