1主機、外設驅動分離的意義
在Linux設備驅動框架的設計中,除了有分層設計實現以外,還有分隔的思想。舉一個簡單的例子,假設我們要通過SPI總線訪問某外設,在這個訪問過程中,要通過操作CPU XXX上的SPI控制器的寄存器來達到訪問SPI外設YYY的目的,最簡單的方法是:
return_type xxx_write_spi_yyy(...)
{
xxx_write_spi_host_ctrl_reg(ctrl);
xxx_ write_spi_host_data_reg(buf);
while(!(xxx_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
如果按照這種方式來設計驅動,結果是對於任何一個SPI外設來講,它的驅動代碼都是CPU相關的。也就是說,當然用在CPU XXX上的時候,它訪問XXX的SPI主機控制寄存器,當用在XXX1的時候,它訪問XXX1的SPI主機控制寄存器:
return_type xxx1_write_spi_yyy(...)
{
xxx1_write_spi_host_ctrl_reg(ctrl);
xxx1_ write_spi_host_data_reg(buf);
while(!(xxx1_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
這顯然是不能接受的,因為這意味著外設YYY用在不同的CPU XXX和XXX1上的時候需要不同的驅動。那麼,我們可以用如圖12.4的思想對主機控制器驅動和外設驅動進行分離。這樣的結構是,外設a、b、c的驅動與主機控制器A、B、C的驅動不相關,主機控制器驅動不關心外設,而外設驅動也不關心主機,外設只是訪問核心層的通用的API進行數據傳輸,主機和外設之間可以進行任意的組合。
圖12.4 Linux設備驅動的主機、外設驅動分離
如果我們不進行如圖12.4的主機和外設分離,外設a、b、c和主機A、B、C進行組合的時候,需要9個不同的驅動。設想一共有m個主機控制器,n個外設,分離的結果是需要m+n個驅動,不分離則需要m*n個驅動。
Linux SPI、I2C、USB、ASoC(ALSA SoC)等子系統都典型地利用了這種分離的設計思想,在本章我們先以簡單一些的SPI為例,而I2C、USB、ASoC等則在後續章節會進行詳細介紹。
2 Linux SPI主機和設備驅動
SPI(同步外設接口)是由摩托羅拉公司開發的全雙工同步串行總線,其接口由MISO(串行數據輸入),MOSI(串行數據輸出),SCK(串行移位時鐘),SS(從使能信號)四種信號構成,SS決定了唯一的與主設備通信的從設備,主設備通過產生移位時鐘來發起通訊。通訊時,數據由MOSI輸出,MISO輸入,數據在時鐘的上升或下降沿由MOSI輸出,在緊接著的下降或上升沿由MISO讀入,這樣經過8/16次時鐘的改變,完成8/16位數據的傳輸。
SPI模塊為了和外設進行數據交換,根據外設工作要求,其輸出串行同步時鐘極性(CPOL)和相位(CPHA)可以進行配置。如果 CPOL=0,串行同步時鐘的空閒狀態為低電平;如果CPOL=1,串行同步時鐘的空閒狀態為高電平。如果CPHA=0,在串行同步時鐘的第一個跳變沿(上升或下降)數據被采樣;如果CPHA=1,在串行同步時鐘的第二個跳變沿(上升或下降)數據被采樣。SPI接口時序如圖12.5所示。
圖12.5 SPI總線時序
在Linux中,用代碼清單12.12的spi_master結構體來描述一個SPI主機控制器驅動,其主要成員是主機控制器的序號(系統中可能存在多個SPI主機控制器)、片選數量、SPI模式和時鐘設置用到的函數、數據傳輸用到的函數等。
代碼清單12.12 spi_master結構體
1 struct spi_master {
2 struct device dev;
3 s16 bus_num;
4 u16 num_chipselect;
5
6 /* 設置模式和時鐘 */
7 int (*setup)(struct spi_device *spi);
8
9 /* 雙向數據傳輸 */
10 int (*transfer)(struct spi_device *spi,
11 struct spi_message *mesg);
12
13 void (*cleanup)(struct spi_device *spi);
14 };
分配、注冊和注銷SPI主機的API由SPI核心提供:
struct spi_master * spi_alloc_master(struct device *host, unsigned size);
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);
在Linux中,用代碼清單12.13的spi_driver結構體來描述一個SPI外設驅動,可以認為是spi_master的client驅動。
代碼清單12.13 spi_driver結構體
1 struct spi_driver {
2 int (*probe)(struct spi_device *spi);
3 int (*remove)(struct spi_device *spi);
4 void (*shutdown)(struct spi_device *spi);
5 int (*suspend)(struct spi_device *spi, pm_message_t mesg);
6 int (*resume)(struct spi_device *spi);
7 struct device_driver driver;
8 };
可以看出,spi_driver結構體和platform_driver結構體有極大的相似性,都有probe()、remove()、suspend()、resume()這樣的接口。是的,這幾乎是一切client驅動的習慣模板。
在SPI外設驅動中,當透過SPI總線進行數據傳輸的時候,使用了一套與CPU無關的統一的接口。這套接口的第1個關鍵數據結構就是spi_transfer,它用於描述SPI傳輸,如代碼清單12.14。
代碼清單12.14 spi_transfer結構體
1 struct spi_transfer {
2 const void *tx_buf;
3 void *rx_buf;
4 unsigned len;
5
6 dma_addr_t tx_dma;
7 dma_addr_t rx_dma;
8
9 unsigned cs_change:1;
10 u8 bits_per_word;
11 u16 delay_usecs;
12 u32 speed_hz;
13
14 struct list_head transfer_list;
15 };
而一次完整的SPI傳輸流程可能不只包含1次spi_transfer,它可能包含1個或多個spi_transfer,這些spi_transfer最終通過spi_message組織在一起,其定義如代碼清單12.15。
代碼清單12.15 spi_message結構體
1 struct spi_message {
2 struct list_head transfers;
3
4 struct spi_device *spi;
5
6 unsigned is_dma_mapped:1;
7
8 /* 完成被一個callback報告 */
9 void (*complete)(void *context);
10 void *context;
11 unsigned actual_length;
12 int status;
13
14 struct list_head queue;
15 void *state;
16 };
通過spi_message_init()可以初始化spi_message,而將spi_transfer添加到spi_message隊列的方法則是:
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
發起一次spi_message的傳輸有同步和異步兩種方式,使用同步API時,會阻塞等待這個消息被處理完。同步操作時使用的API是:
int spi_sync(struct spi_device *spi, struct spi_message *message);
使用異步API時,不會阻塞等待這個消息被處理完,但是可以在spi_message的complete字段掛接一個回調函數,當消息被處理完成後,該函數會被調用。異步操作時使用的API是:
int spi_async(struct spi_device *spi, struct spi_message *message);
代碼清單12.16是非常典型的初始化spi_transfer、spi_message並進行SPI數據傳輸的例子,同時它們也是SPI核心層的2個通用API,在SPI外設驅動中可以直接調用它們進行寫和讀操作。
代碼清單12.16 SPI傳輸實例spi_write()、spi_read() API
1 static inline int
2 spi_write(struct spi_device *spi, const u8 *buf, size_t len)
3 {
4 struct spi_transfer t = {
5 .tx_buf = buf,
6 .len = len,
7 };
8 struct spi_message m;
9
10 spi_message_init(&m);
11 spi_message_add_tail(&t, &m);
12 return spi_sync(spi, &m);
13 }
14
15 static inline int
16 spi_read(struct spi_device *spi, u8 *buf, size_t len)
17 {
18 struct spi_transfer t = {
19 .rx_buf = buf,
20 .len = len,
21 };
22 struct spi_message m;
23
24 spi_message_init(&m);
25 spi_message_add_tail(&t, &m);
26 return spi_sync(spi, &m);
27 }
LDD6410開發板所使用的S3C6410的SPI主機控制器驅動位於drivers/spi/spi_s3c.h和drivers/spi/spi_s3c.c這2個文件,其主體是實現了spi_master的setup()、transfer()等成員函數。
SPI外設驅動遍布於內核的drivers、sound的各個子目錄之下,SPI只是一種總線,spi_driver的作用只是將SPI外設掛接在該總線上,因此在spi_driver的probe()成員函數中,將注冊SPI外設本身所屬設備驅動的類型。
和platform_driver對應著一個platform_device一樣,spi_driver也對應著一個spi_device;platform_device需要在BSP的板文件中添加板信息數據,而spi_device也同樣需要。spi_device的板信息用spi_board_info結構體描述,該結構體記錄SPI外設使用的主機控制器序號、片選序號、數據比特率、SPI傳輸模式(即CPOL、CPHA)等。如諾基亞770上2個SPI設備的板信息數據如代碼清單12.17,位於板文件arch/arm/mach-omap1/board-nokia770.c。
代碼清單12.17諾基亞770板文件中的spi_board_info
1 static struct spi_board_info nokia770_spi_board_info[] __initdata = {
2 [0] = {
3 .modalias = "lcd_mipid",
4 .bus_num = 2, /* 用到的SPI主機控制器序號 */
5 .chip_select = 3, /* 使用哪一號片選 */
6 .max_speed_hz = 12000000, /* SPI數據傳輸比特率 */
7 .platform_data = &nokia770_mipid_platform_data,
8 },
9 [1] = {
10 .modalias = "ads7846",
11 .bus_num = 2,
12 .chip_select = 0,
13 .max_speed_hz = 2500000,
14 .irq = OMAP_GPIO_IRQ(15),
15 .platform_data = &nokia770_ads7846_platform_data,
16 },
17 };
在Linux啟動過程中,在機器的init_machine()函數中,會通過如下語句注冊這些spi_board_info:
spi_register_board_info(nokia770_spi_board_info,
ARRAY_SIZE(nokia770_spi_board_info));
這一點和啟動時通過platform_add_devices()添加platform_device非常相似。