linux設備驅動程序--串行通訊驅動框架分析

linux 串行通訊接口驅動框架

在學習linux內核驅動時,不管是看linux相關的書籍,又或者是直接看linux的源碼,老是能在linux中看到各類各樣的框架,linux內核極其龐雜,linux各類框架理解起來並不容易,若是直接硬着頭皮死記硬背,意義也不大。html

博主學習東西一直秉持着追本溯源的態度,要弄清一個東西是怎麼樣的,若是可以瞭解它的發展,瞭解它爲何會變成這樣,理解起來就很是簡單了。抓住主幹,沿着線頭就能夠將整個框架慢慢梳理清楚。linux

從i2c開始

在嵌入式中,無論是單片機仍是單板機,i2c做爲板級通訊協議是很是受歡迎的,尤爲是在傳感器的使用領域,其主從結構加上對硬件資源的低要求,穩穩地佔據着主導地位。程序員

咱們就以i2c協議爲例,聊一聊linux內核中串行通訊接口框架。 (注:在這篇文章只討論大體框架,並不涉及具體細節,linux內核驅動部分的框架分得很細,沒法所有覆蓋,但求創建一個大致的概念)編程

單片機中的i2c

每一個MCU基本上都會集成硬件i2c控制器,在單片機編程中,對於操做硬件i2c控制器,咱們只須要操做一些相應的寄存器便可實現數據的收發。安全

那若是沒有硬件i2c控制器或者i2c控制器不夠用呢?框架

事情也不麻煩,咱們可使用兩個gpio來軟件模擬i2c協議,代碼不過幾十行,雖然i2c協議自己有必定的複雜性,可是若是僅僅是實現通訊,在單片機上仍是很是簡單的。模塊化

單片機中實現i2c程序

咱們不妨回想一下,在單片機中編寫一個i2c驅動程序的流程:函數

以sht31(這是一個經常使用的i2c接口的溫溼度傳感器)爲例,剛入行的新手程序員可能這樣寫主機程序(僞代碼):性能

int sht31_read_temprature(){                 //讀取溫度值實現函數
    設置i2c寫寄存器,發送i2c器件地址
    設置i2c寫寄存器,發送i2c寄存器地址
    設置i2c讀
    temperature = 讀取目標器件發回的數據
    return temperature;
}   

int sht31_read_humidity(){                   //讀取溼度值實現函數
    設置i2c寫寄存器,發送i2c器件地址
    設置i2c寫寄存器,發送i2c寄存器地址
    設置i2c讀
    humidity = 讀取目標器件發回的數據
    return humidity;
}   
....

程序優化

每次讀寫函數都對硬件i2c的寄存器進行設置,很顯然,這樣的代碼有不少重複的部分,咱們能夠將重複的讀寫部分提取出來做爲公共函數,寫成這樣:學習

array sht31_read_data(sht31數據寄存器地址){
    設置i2c寫寄存器,發送i2c器件地址
    設置i2c寫寄存器,發送i2c寄存器地址
    設置i2c讀
    return 讀取目標器件發回的數據;
}

因此,上例中的讀溫溼度就能夠寫成這樣:

array sht31_read_temprature(){
    return sht31_read_data(sht31溫度數據寄存器地址);
}
array sht31_read_humidity(){
    return sht31_read_data(sht31溼度數據寄存器地址);
}
...

通過這一步優化,這個驅動程序就變成了兩層:

  • i2c硬件操做部分,好比i2c與設備的讀寫,在同一平臺上,硬件讀寫的寄存器操做都是一致的。
  • 設備的操做函數,不一樣的設備有不一樣的寄存器,對於存儲設備而言就是存取數據,對於傳感器而言就是讀寫傳感器數據,須要讀寫設備時,直接調用第一步中的接口,傳入不一樣的參數。

能夠明顯看到的是,第一步中的i2c操做函數部分能夠被抽象出來。

這就是軟件的分層

若是你仔細看了上面的示例,基本上就理解了軟件分層是什麼概念,它其實就是不斷地將公共的部分和重複代碼提取出來,將其做爲一個統一的模塊,向外提供訪問的接口。

從宏觀上來看程序就被分紅了兩部分,因爲是調用與被調用的關係,因此層次結構能夠更明顯地體現他們之間的關係。

分層的好處

最直觀地看過去,軟件分層第一個好處就是節省代碼空間,代碼量更少,層次更清晰,對於代碼的可讀性和後期的維護都是很是大的好處。

將相關的數據和操做封裝成一個模塊,對外提供接口,屏蔽實現細節,便於移植和協做,由於在移植和修改時,只須要修改當前模塊的代碼,保持對外接口不變便可,減小了移植和維護成本。

舉個例子:在上述的代碼中,若是將sht31換成其餘i2c設備,我只須要修改設備的操做函數部分,而i2c的讀寫部分能夠複用.又或者一樣的設備,切換成了spi協議通訊,那麼,設備的操做函數部分能夠不用修改,只須要將i2c硬件讀寫換成spi硬件讀寫。

程序的再次優化

在程序的第一次優化中,i2c被抽象出兩層:i2c硬件讀寫層和i2c的應用層(暫且這麼命名吧),讀寫層只負責讀寫數據,而應用層則根據不一樣設備進行不一樣的讀寫操做,調用讀寫層接口。

劃分紅讀寫層和驅動層以後,在空間上和程序複用性上已經走了一大步。

可是在以後的開發中又發現一個問題:對於單個的廠商而言,生產的設備每每具備高度類似的寄存器操做方式,好比對於咱們經常使用的sht3x(溫溼度傳感器)而言,這些系列的傳感器設備之間的不一樣僅僅是測量範圍、測量精度的不一樣,其餘的寄存器配置實際上是同樣的,有經驗的程序員就想到能夠抽象出這樣一個統一接口來針對全部這些同系列設備:

sht3x_init(int sht3x_type,int measurement_range,int resolution){
    switch(sht3x_type){
        case sht31:
        set_measurement_range(sht31,measurement_range);
        set_resolution(sht31,resolution);
        break;
        case sht35:
        set_measurement_range(sht35,measurement_range);
        set_resolution(sht35,resolution);
        break;
        case ...
    }
}

僅僅是在設置的時候設置不一樣的測量範圍和精度,而其餘的設置,數據讀寫部分都是徹底相同的。

對於這些同系列的設備,咱們一樣能夠抽象出一個驅動層,以sht3x爲例,同系列設備的操做變成這樣:

  • i2c硬件讀寫層。
  • sht3x的公共操做函數,經過調用上層接口實現初始化、設置溫度閾值等函數。
  • 具體設備的操做函數,對於sht31而言,經過傳入sht31的參數,調用上層接口來讀寫sht31中的數據,須要傳入的參數主要是i2c地址,設置精度等sht3x系列之間的差別化部分。

這樣,對於sht3x而言,用戶在使用這一類設備的時候就只須要簡單地調用諸如sht3x_init(u8 i2c_addr),sht3x_set_resolution(u8 resolution)這一類的函數便可完成設備的操做,經過傳入不一樣的參數執行不一樣的操做。

這裏貼上一個圖來加深理解:

到這裏,對於一個單片機上的i2c設備而言,基本上已經有了比較好的層次結構:

  • i2c硬件讀寫層
  • 驅動層(主要是解決同系列設備驅動重複的寄存器操做問題)
  • 設備層(應用程序調用上一層接口實現具體的設備讀寫)

將上述分層的1.2步集成到系統中,用戶只須要調用相應接口直接就能夠操做到設備,對用戶來講簡化了操做流程,對系統來講節省了程序空間。

到這裏,這一份i2c程序就已經比較完善了,當須要添加sht3x設備時,只須要在設備層添加便可。

當須要添加其餘i2c設備時,則須要提供驅動層和設備層的實現而複用硬件讀寫層。

當須要將sht3x驅動程序移植到另外一個平臺時,只須要修改i2c硬件讀寫層,由於平臺之間的i2c讀寫操做可能不一致,驅動層和設備層不須要修改。

整個分層的思想就是複用。大大節省了調試時間,在debug的時候也能很方便地定位是哪一部分出了問題,同時能夠將程序發佈給其餘用戶,其餘用戶根據須要能夠很方便地進行裁剪移植,避免浪費過多時間在同一件事情上。

再看看linux中的i2c

在單片機上能夠碰到的問題,在linux系統上一樣能碰到,單片機上運行的程序通常而言不會太龐大,驅動部分更是所佔甚微,因此設備驅動程序的好壞並不會太過於影響程序的執行效率。

可是在linux中,有時須要集成大量的驅動設備到系統中,同時內核代碼是多人維護的模式,因此也必須採用驅動分層的模式來提升內存使用效率,下降程序耦合性。

那麼,在linux中是否也是像單片機中同樣分爲i2c硬件讀寫層、驅動層、設備層便可呢?

其實大體的思想是同樣的:將硬件讀寫抽象成一層,將同系列產品驅動抽象成一層,將具體設備的添加抽象成頂層,可是具體實現徹底不同。

與單片機中程序不同的是:linux中內核空間和用戶空間是區分開來的,驅動程序將會被加載到內核中,提供接口給用戶進行操做。

從單片機切換到linux的第一種解決方案

不難想到的解決方案是:分層模型不變,將上述分層中的設備層改成由驅動層直接在用戶空間註冊文件接口,用戶程序經過用戶文件對設備進行操做。

因而,分層模型變成了這樣:

  • i2c硬件讀寫層
  • 驅動層(主要是解決同系列設備驅動重複問題,同時在用戶空間註冊用戶接口以供訪問)
  • 應用層(相對於內核而言,等於單片機分層中的設備層,應用程序經過操做文件調用上一層接口實現具體的設備讀寫)

問題就這樣獲得解決。

用戶在操做用戶空間文件接口的時候,依次地經過文件接口傳遞目標設備的資源對設備進行初始化,各類設置,而後讀寫便可,對於sht3x而言,這些資源包括i2c地址、精度、閾值等等。

可是,這樣的作法的缺陷是:

  • 提升了驅動程序和應用開發的耦合性,同時驅動程序不具備獨立性和安全性,程序的可移植性也不好。
  • 同時,此時的驅動層對應的是同系列設備,註冊到用戶空間的接口會更加抽象,用戶須要對驅動程序進行二次開發.

想想,當用戶須要使用sht31時,用戶還得去閱讀sht31的datasheet來查看並設置各類參數,這樣驅動和用戶程序不分離徹底不符合高內聚低耦合的程序思想。

最理想的狀態天然是:

  • 驅動程序自己和驅動程序須要的資源由內核統一管理,這樣才能提升驅動和管理資源的獨立性,而且擁有較好的額可移植性和安全性,提供儘可能簡單的操做接口給用戶空間。
  • 用戶空間只須要直接使用驅動,而不須要對驅動程序進行二次開發。

第二種解決方案

既然驅動部分沒法針對單一設備,而是針對諸如sht3x這一類設備,那咱們爲每一個單一設備添加一份描述設備信息來提供資源(i2c地址等),好比sht3一、sht35分別提供一份設備描述信息,而一個驅動程序能夠對應多份設備描述信息。

當須要使用某個具體設備好比sht31時,再將sht31的設備描述信息和sht3x的驅動程序結合起來,生成一個完整的sht31設備驅動程序,並在用戶空間註冊sht31文件接口,這樣文件接口就能夠針對具體的設備而實現具體的操做功能,用戶能夠經過簡單的參數選擇而直接使用。

同時將設備描述信息同時註冊到內核中,由內核統一管理,提升了驅動和資源管理的獨立性和安全性,同時移植性能較好。

這樣,在分層模型中,咱們將設備描述信息(設備資源)部分和驅動程序放置在同一層,驅動模型就變成了這樣:

  1. i2c硬件讀寫層
  2. 驅動部分(提供系列設備的公共驅動部分)-------設備部分(提供單一設備的資源,驅動程序獲取資源進行相應配置,與驅動層爲一對多的關係),統稱爲驅動層
  3. 應用層(經過上層提供的接口對設備進行訪問)

這樣的分層有個好處,在第二層驅動層中,直接實現了每一個單一設備的程序,對應用層提供簡便的訪問接口,這樣在用戶層不用再進行復雜的設置,就能夠直接對設備進行讀寫。

它的分層是這樣的:

i2c硬件讀寫層

linux i2c設備驅動程序中的硬件讀寫層由struct i2c_adapter來描述,它的內容是這樣的: struct i2c_adapter { ... const struct i2c_algorithm algo; / the algorithm to access the bus */ void *algo_data; int nr;
char name[48]; ... }; 其中struct i2c_algorithm *algo結構體中master_xfer函數指針指向i2c的硬件讀寫函數,咱們能夠簡單地認爲一個struct i2c_adapter結構體描述一個硬件i2c控制器。在驅動編寫的過程當中,這一層的實現由系統提供。驅動編寫者只須要調用i2c_get_adapter()接口來獲取相應的adapter.

驅動層(中間層,包含設備部分和驅動部分)

linux總線機制

在上面的分層討論中可知:驅動層由驅動部分和設備部分組成,驅動部分由struct i2c_driver描述,而設備部分由struct i2c_device部分組成。

這兩部分雖然被咱們分在同一層,可是這是相互獨立的,當添加進一個device或者driver,會根據某些條件尋找匹配的driver或者device,那這一部分匹配誰來作呢?

這就不得不提到linux中的總線機制,i2c總線擔任了這個銜接的角色,誠然,i2c總線也屬於總線的一種,i2c總線在系統啓動時被註冊到系統中,管理i2c設備。

咱們先來看看描述總線的結構體:

struct bus_type {
    ....
    const char		*name;
    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);
    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    struct subsys_private *p;
};

struct subsys_private {
    ...
    struct klist klist_devices;
    struct klist klist_drivers;
    unsigned int drivers_autoprobe:1;
    ...
};

這個結構體描述了linux中各類各樣的sub bus,好比spi,i2c,platform bus,能夠看到,這個結構體中有一系列的函數,在struct subsys_private結構體定義的指針p中,有struct klist klist_devices和struct klist klist_drivers這兩項。

當咱們向i2c bus註冊一個driver時,這個driver被添加到klist_drivers這個鏈表中,當向i2c bus註冊一個device時,這個device被添加到klist_devices這個鏈表中。

每有一個添加行爲,都將調用總線的match函數,遍歷兩個鏈表,爲新添加的device或者driver尋找對應的匹配,一旦匹配上,就調用probe函數,在probe函數中執行設備的初始化和建立用戶操做接口。

應用層

在總線的(或者驅動部分的)probe函數被執行時,在/dev目錄下建立對應的文件,例如/dev/sht3x,用戶程序經過讀寫/dev/sht3x文件來操做設備。

同時,也能夠在/sys目錄下生成相應的操做文件來操做設備,應用層直接面對用戶,因此接口理應是簡單易用的。

驅動開發者的工做

通常來講,若是隻是開發i2c驅動,而不須要爲新的芯片移植驅動的話,咱們只須要在驅動層作相應的工做,並嚮應用層提供接口。i2c硬件讀寫層已經集成在系統中。

抽象化仍在進行中

上文中提到,當驅動開發者想要開發驅動時,在驅動層編寫一個driver部分,添加到i2c總線中,同時編寫一個device部分,添加到i2c總線中。

driver部分主要包含了全部的操做接口,而device部分提供相應的資源。

可是,隨着設備的增加,同時因爲device部分老是靜態定義在文件中,致使這一部分佔用的內核空間愈來愈大,並且,最主要的問題時,對於資源來講,大多都是一些重複的定義:好比時鐘、定時器、引腳中斷、i2c地址等同類型資源。

按照一向的風格,對於大量的重複定義,咱們必須對其進行抽象化,因而linus一怒之下對其進行大刀闊斧的整改,因而設備樹橫空出世,是的,設備樹就是針對各類總線(i2c bus,platform bus等等)的device資源部分進行整合。

將設備部分的靜態描述轉換成設備樹的形式,由內核在加載時進行解析,這樣節省了大量的空間。

若是有興趣能夠看看博主的設備樹解析篇:linux設備樹解析--從dtb格式開始

小結

總的來講,分層便是一種抽象,將重複部分的代碼不斷地提取出來做爲一個獨立的模塊,由於模塊之間是調用與被調用的關係,因此看起來就是一種層次結構。

高內聚,低耦合,這是程序設計界的六字箴言。

無論是linux仍是其餘操做系統,又或者是其餘應用程序,將程序模塊化都是一種很好的編程習慣,linux內核的層次結構也是這種思想的產物。

這篇文章只是簡單地分析了linux分層機制的由來,從原理上理解爲何會有這樣的層次結構,事實上,因爲linux系統的複雜性,linux的驅動框架並不是這麼簡單地分紅三個部分,博主只是抽去了大部分細節,展示了最粗獷的框架。

接下來,博主還會帶你走進linux內核代碼,剖析整個i2c框架的函數調用流程。

好了,關於linux驅動框架的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索