來源: Ramax 喵董

目前進行中的項目是在 DaVinci 平台上開發 SPI 控制器驅動程式,不過由於手邊缺乏硬體平台可供測試的關係,現在仍然進度緩慢。不過這期間也研究了一下 Linux SPI 子系統,稍微有一些心得,來和各位分享一下。

SPI 是 Serial Peripheral Interface 的縮寫,它是一種串列式的 IO 介面,時脈約 1~ 70MHz,詳細資料可見 [1]。 DaVinci DM6446 上配備了一個 SPI 控制器,支援二個 chip select,時脈可達 33MHz。

Linux SPI 子系統將驅動程式分為三種類型:

  • SPI bus driver:
    主要目的為提供 API 給下層的 master driver 及上層的 protocol driver (稍後會提)來註冊,並傳送封包至下層的 driver。
  • SPI master driver:
    主要工作為初始化控制器,並負 責對暫存器寫入以及中斷處理。
  • SPI protocol driver:
    主要為發送 SPI 訊息封包至所選擇的裝置。

從 這樣的架構來看,bus driver 扮演了一個抽象層的角色,對上隱藏了硬體的細節,對下則隱藏了對特定裝置的通訊協定。是故 master driver 並不需要知道控制器接了什麼周邊,它只專注在如何操作控制器來將訊息封包送出;而 protocol driver 只需專心於和特定週邊的溝通,而不需了解要如何傳送訊息封包。

在子系統中,訊息封包的格式定義在 spi.h 的 spi_message 結構中。protocol driver 要傳送資料,只需填上 spi_message 結構相對應的欄位,並呼叫 spi_sync 以起始傳送。或者也可直接呼叫 spi_read/spi_write,參數則填入你的 buffer pointer 以及長度即可。

一般在嵌入式系統中,SPI 周邊多是直接固定在板子上,且硬體並無特別的方式可供偵測,因此我們需要在平台初始化時預先定義系統包含那些 SPI 周邊,而子系統也定義一個結構 spi_board_info 提供給開發者填入相對應的資訊,例如:

struct spi_board_info devices[] = {
     {
         .modalias = "mydevice",
         .max_speed_hz = 6000000,
         .bus_num = 0,
         .chip_select = 1,
         .mode = SPI_MODE_1,
     },
     {
         ......
     },
};

接著呼叫 spi_register_board_info 來註冊這些裝置。需要注意的是,modalias 欄位的值必須與 protocol driver 註冊的名字相同;bus driver 會以該值作為比對的依據。若需在執行時期註冊新的裝置,則可呼叫 spi_new_device 來加入。

關 於 master driver 的部分留待下一篇來討論。

相關連結:
[1] Serial Peripheral Interface Bus
[2] TMS320DM644x DMSoC Serial Peripheral Interface (SPI) User's Guide

我們在上 一篇文章中提到,master driver 負責初始化硬體控制器,寫入暫存器以及中斷處理。換句話說,它必須要能夠做到低階的硬體控制;在以 ARM 為基礎的 SoC 當中,也就是要實際上對 I/O memory 做讀寫的動作。因此撰寫 master driver 前,必須要先熟讀 SoC 的使用手冊,充分了解如何操作這些暫存器以支援相關的功能。

此外,master driver 還必須要將從上層傳送來的訊息封包加以解析,並將傳送該封包所需要的硬體參數設定事先寫入暫存器,最後才起始資料傳送。如果控制器支援中斷,那麼 master driver 也必須先註冊中斷常式以便處理例外狀況。接下來我們就來看看一些細節部分的資料結構與函式。

一般在 SoC 系統下,SPI 控制器會以 platform device 的形式存在,是故我們會將 master driver 註冊為一種 platform driver,接著在 probe 函式中呼叫 spi_alloc_master,該函式宣告如下:

struct spi_master *spi_alloc_master(struct device *host, unsigned size);

該 函式會分配一塊記憶體並回傳指向 spi_master 結構的指標,參數 size 則可以指定額外的空間大小,該空間可以透過 spi_master_get_devdata 來取得,開發者可用以存放額外的資訊。接著我們需要設定一些關於該控制器的資訊,如:

master->bus_num = 0;
master->num_chipselect = 1;
master->setup = mysetup;
master->transfer = mytransfer;
master->cleanup = mycleanup;

其中 bus_num 是代表控制器的識別號碼,若指定 -1 則表示由系統動態分配。num_chipselect 是這個控制器所能連接的裝置編號的最大值,1 代表最多可連接 2 個裝置 (0, 1)。最後呼叫 spi_register_master 來向系統註冊即可。

當開始要進行傳輸 時,在上面程式片段中的 setup 會在傳送前被呼叫,讓 master driver 根據周邊所能接受的傳輸模式參數來設定控制器;接著 transfer 會被呼叫,其參數包括要傳送的周邊以及指向 spi_message 結構的指標。其中 spi_message 結構還包含了一個或多個要傳輸的 buffer,定義在 spi_transfer 結構中。要注意的是,由於 transfer 函式中不可呼叫任何會進入睡眠的函式,是故實際的傳輸會由另外產生的 worker thread 來達成;並且在傳輸完成後,呼叫 spi_message 結構中的 complete 函式以通知 bus driver。最後則是 cleanup 函式被呼叫以釋放進行傳輸時所使用的資源。

另外當要卸載核心模組時,需要呼叫 spi_unregister_master 以解除註冊。

下一篇我們將會來討論 protocol driver 的部分。

 

在上二篇的文章中我們提到了 Linux SPI 子系統的架構以及如何撰寫 master driver,接下來則是要討論如何撰寫 protocol driver。

如同 master driver,要讓核心能夠使用 protocol driver,必須先註冊我們所要驅動的硬體以及對應的驅動程式。硬體裝置的註冊有兩種方法:

  1. 事先在核心平台相關初始化 程式碼中,填入相關的周邊硬體資訊在 spi_board_info 結構中。該結構的宣告如下:

    struct spi_board_info {
        char modalias[KOBJ_NAME_LEN];
        const void *platform_data;
        void *controller_data;
        int irq;
        u32 max_speed_hz;
        u16 bus_num;
        u16 chip_select;
        u8 mode;
    }

    其中 modalias 必須與 protocol driver 的名稱相同以供核心比對;其他欄位的意義可參照 include/spi/spi.h 檔案中的說明。完成後呼叫 spi_register_board_info 來註冊這些裝置。
  2. 若 是無法確定會接上什麼裝置,則可以在核心模組程式碼中,填入上述的結構,但改為呼叫 spi_new_device 來註冊該裝置。函式中的 master 參數可透過呼叫 spi_busnum_to_master 來取得。

在註冊時,核心會為每個裝置配置一個 spi_device 結構,將上述 spi_board_info 結構中的資訊填入對應的欄位中。

接下來我們要將我們所撰寫的 protocol driver 回呼函式填入 spi_driver 結構中,如下所示:

struct spi_driver mydriver {
    .probe = mydriver_probe,
    .remove = mydriver_remove,
    .driver = {
        .name = "mydriver",
        .owner = THIS_MODULE,
    }
}

接著呼叫 spi_register_driver 來註冊新的 protocol driver。

結語
從這 一系列文章,我們可以了解到,即使是像 SPI 如此簡單的硬體介面,為了讓驅動程式更具可移植性及模組化,核心開發者設計了分層式的架構,讓每個驅動程式能夠各司其職,互不干擾。這樣的架構在其他核心 子系統中也常會見到,也許名稱不同,但基本理念仍是相同的。

kezeodsnx 發表在 痞客邦 PIXNET 留言(0) 人氣()