在學習linux內核驅動時,不管是看linux相關的書籍,又或者是直接看linux的源碼,老是能在linux中看到各類各樣的框架,linux內核極其龐雜,linux各類框架理解起來並不容易,若是直接硬着頭皮死記硬背,意義也不大。html
博主學習東西一直秉持着追本溯源的態度,要弄清一個東西是怎麼樣的,若是可以瞭解它的發展,瞭解它爲何會變成這樣,理解起來就很是簡單了。抓住主幹,沿着線頭就能夠將整個框架慢慢梳理清楚。linux
在嵌入式中,無論是單片機仍是單板機,i2c做爲板級通訊協議是很是受歡迎的,尤爲是在傳感器的使用領域,其主從結構加上對硬件資源的低要求,穩穩地佔據着主導地位。程序員
咱們就以i2c協議爲例,聊一聊linux內核中串行通訊接口框架。 (注:在這篇文章只討論大體框架,並不涉及具體細節,linux內核驅動部分的框架分得很細,沒法所有覆蓋,但求創建一個大致的概念)編程
每一個MCU基本上都會集成硬件i2c控制器,在單片機編程中,對於操做硬件i2c控制器,咱們只須要操做一些相應的寄存器便可實現數據的收發。安全
那若是沒有硬件i2c控制器或者i2c控制器不夠用呢?框架
事情也不麻煩,咱們可使用兩個gpio來軟件模擬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操做函數部分能夠被抽象出來。
若是你仔細看了上面的示例,基本上就理解了軟件分層是什麼概念,它其實就是不斷地將公共的部分和重複代碼提取出來,將其做爲一個統一的模塊,向外提供訪問的接口。
從宏觀上來看程序就被分紅了兩部分,因爲是調用與被調用的關係,因此層次結構能夠更明顯地體現他們之間的關係。
最直觀地看過去,軟件分層第一個好處就是節省代碼空間,代碼量更少,層次更清晰,對於代碼的可讀性和後期的維護都是很是大的好處。
將相關的數據和操做封裝成一個模塊,對外提供接口,屏蔽實現細節,便於移植和協做,由於在移植和修改時,只須要修改當前模塊的代碼,保持對外接口不變便可,減小了移植和維護成本。
舉個例子:在上述的代碼中,若是將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爲例,同系列設備的操做變成這樣:
這樣,對於sht3x而言,用戶在使用這一類設備的時候就只須要簡單地調用諸如sht3x_init(u8 i2c_addr),sht3x_set_resolution(u8 resolution)這一類的函數便可完成設備的操做,經過傳入不一樣的參數執行不一樣的操做。
這裏貼上一個圖來加深理解:
到這裏,對於一個單片機上的i2c設備而言,基本上已經有了比較好的層次結構:
將上述分層的1.2步集成到系統中,用戶只須要調用相應接口直接就能夠操做到設備,對用戶來講簡化了操做流程,對系統來講節省了程序空間。
到這裏,這一份i2c程序就已經比較完善了,當須要添加sht3x設備時,只須要在設備層添加便可。
當須要添加其餘i2c設備時,則須要提供驅動層和設備層的實現而複用硬件讀寫層。
當須要將sht3x驅動程序移植到另外一個平臺時,只須要修改i2c硬件讀寫層,由於平臺之間的i2c讀寫操做可能不一致,驅動層和設備層不須要修改。
整個分層的思想就是複用。大大節省了調試時間,在debug的時候也能很方便地定位是哪一部分出了問題,同時能夠將程序發佈給其餘用戶,其餘用戶根據須要能夠很方便地進行裁剪移植,避免浪費過多時間在同一件事情上。
在單片機上能夠碰到的問題,在linux系統上一樣能碰到,單片機上運行的程序通常而言不會太龐大,驅動部分更是所佔甚微,因此設備驅動程序的好壞並不會太過於影響程序的執行效率。
可是在linux中,有時須要集成大量的驅動設備到系統中,同時內核代碼是多人維護的模式,因此也必須採用驅動分層的模式來提升內存使用效率,下降程序耦合性。
那麼,在linux中是否也是像單片機中同樣分爲i2c硬件讀寫層、驅動層、設備層便可呢?
其實大體的思想是同樣的:將硬件讀寫抽象成一層,將同系列產品驅動抽象成一層,將具體設備的添加抽象成頂層,可是具體實現徹底不同。
與單片機中程序不同的是:linux中內核空間和用戶空間是區分開來的,驅動程序將會被加載到內核中,提供接口給用戶進行操做。
不難想到的解決方案是:分層模型不變,將上述分層中的設備層改成由驅動層直接在用戶空間註冊文件接口,用戶程序經過用戶文件對設備進行操做。
因而,分層模型變成了這樣:
問題就這樣獲得解決。
用戶在操做用戶空間文件接口的時候,依次地經過文件接口傳遞目標設備的資源對設備進行初始化,各類設置,而後讀寫便可,對於sht3x而言,這些資源包括i2c地址、精度、閾值等等。
可是,這樣的作法的缺陷是:
想想,當用戶須要使用sht31時,用戶還得去閱讀sht31的datasheet來查看並設置各類參數,這樣驅動和用戶程序不分離徹底不符合高內聚低耦合的程序思想。
最理想的狀態天然是:
既然驅動部分沒法針對單一設備,而是針對諸如sht3x這一類設備,那咱們爲每一個單一設備添加一份描述設備信息來提供資源(i2c地址等),好比sht3一、sht35分別提供一份設備描述信息,而一個驅動程序能夠對應多份設備描述信息。
當須要使用某個具體設備好比sht31時,再將sht31的設備描述信息和sht3x的驅動程序結合起來,生成一個完整的sht31設備驅動程序,並在用戶空間註冊sht31文件接口,這樣文件接口就能夠針對具體的設備而實現具體的操做功能,用戶能夠經過簡單的參數選擇而直接使用。
同時將設備描述信息同時註冊到內核中,由內核統一管理,提升了驅動和資源管理的獨立性和安全性,同時移植性能較好。
這樣,在分層模型中,咱們將設備描述信息(設備資源)部分和驅動程序放置在同一層,驅動模型就變成了這樣:
這樣的分層有個好處,在第二層驅動層中,直接實現了每一個單一設備的程序,對應用層提供簡便的訪問接口,這樣在用戶層不用再進行復雜的設置,就能夠直接對設備進行讀寫。
它的分層是這樣的:
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.
在上面的分層討論中可知:驅動層由驅動部分和設備部分組成,驅動部分由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不沾身.