一、驅(qū)動概念
驅(qū)動與底層硬件直接打交道,充當了硬件與應用軟件中間的橋梁。
具體任務
讀寫設(shè)備寄存器(實現(xiàn)控制的方式) 完成設(shè)備的輪詢、中斷處理、DMA通信(CPU與外設(shè)通信的方式) 進行物理內(nèi)存向虛擬內(nèi)存的映射(在開啟硬件MMU的情況下) 說明:設(shè)備驅(qū)動的兩個任務方向 操作硬件(向下)
將驅(qū)動程序通入內(nèi)核,實現(xiàn)面向操作系統(tǒng)內(nèi)核的接口內(nèi)容,接口由操作系統(tǒng)實現(xiàn)(向上) (驅(qū)動程序按照操作系統(tǒng)給出的獨立于設(shè)備的接口設(shè)計,應用程序使用操作系統(tǒng)統(tǒng)一的系統(tǒng)調(diào)用接口來訪問設(shè)備) Linux系統(tǒng)主要部分:內(nèi)核、shell、文件系統(tǒng)、應用程序 內(nèi)核、shell和文件系統(tǒng)一起形成了基本的操作系統(tǒng)結(jié)構(gòu),它們使得用戶可以運行程序、管理文件并使用系統(tǒng) 分層設(shè)計的思想讓程序間松耦合,有助于適配各種平臺 驅(qū)動的上面是系統(tǒng)調(diào)用,下面是硬件 二、驅(qū)動分類
Linux驅(qū)動分為三個基礎(chǔ)大類:字符設(shè)備驅(qū)動,塊設(shè)備驅(qū)動,網(wǎng)絡設(shè)備驅(qū)動。 字符設(shè)備(Char Device)
字符(char)設(shè)備是個能夠像字節(jié)流(類似文件)一樣被訪問的設(shè)備。 對字符設(shè)備發(fā)出讀/寫請求時,實際的硬件I/O操作一般緊接著發(fā)生。 字符設(shè)備驅(qū)動程序通常至少要實現(xiàn)open、close、read和write系統(tǒng)調(diào)用。 比如我們常見的lcd、觸摸屏、鍵盤、led、串口等等,他們一般對應具體的硬件都是進行出具的采集、處理、傳輸。 塊設(shè)備(Block Device)
一個塊設(shè)備驅(qū)動程序主要通過傳輸固定大小的數(shù)據(jù)(一般為512或1k)來訪問設(shè)備。 塊設(shè)備通過buffer cache(內(nèi)存緩沖區(qū))訪問,可以隨機存取,即:任何塊都可以讀寫,不必考慮它在設(shè)備的什么地方。 塊設(shè)備可以通過它們的設(shè)備特殊文件訪問,但是更常見的是通過文件系統(tǒng)進行訪問。 只有一個塊設(shè)備可以支持一個安裝的文件系統(tǒng)。 比如我們常見的電腦硬盤、SD卡、U盤、光盤等。 網(wǎng)絡設(shè)備(Net Device)
任何網(wǎng)絡事務都經(jīng)過一個網(wǎng)絡接口形成,即一個能夠和其他主機交換數(shù)據(jù)的設(shè)備。 訪問網(wǎng)絡接口的方法仍然是給它們分配一個唯一的名字(比如eth0),但這個名字在文件系統(tǒng)中不存在對應的節(jié)點。 內(nèi)核和網(wǎng)絡設(shè)備驅(qū)動程序間的通信,完全不同于內(nèi)核和字符以及塊驅(qū)動程序之間的通信,內(nèi)核調(diào)用一套和數(shù)據(jù)包傳輸相關(guān)的函(socket函數(shù))而不是read、write等。 比如我們常見的網(wǎng)卡設(shè)備、藍牙設(shè)備。
三、驅(qū)動程序的功能
對設(shè)備初始化和釋放 把數(shù)據(jù)從內(nèi)核傳送到硬件和從硬件讀取數(shù)據(jù) 讀取應用程序傳送給設(shè)備文件的數(shù)據(jù)和回送應用程序請求的數(shù)據(jù) 檢測和處理設(shè)備出現(xiàn)的錯誤
四、驅(qū)動開發(fā)前提知識
4.1 內(nèi)核態(tài)和用戶態(tài)
Kernel Mode(內(nèi)核態(tài))
內(nèi)核模式下(執(zhí)行內(nèi)核空間的代碼),代碼具有對硬件的所有控制權(quán)限。可以執(zhí)行所有CPU指令,可以訪問任意地址的內(nèi)存 User Mode(用戶態(tài)) 在用戶模式下(執(zhí)行用戶空間的代碼),代碼沒有對硬件的直接控制權(quán)限,也不能直接訪問地址的內(nèi)存。 只能訪問映射其地址空間的頁表項中規(guī)定的在用戶態(tài)下可訪問頁面的虛擬地址。 程序是通過調(diào)用系統(tǒng)接口(System Call APIs)來達到訪問硬件和內(nèi)存 Linux利用CPU實現(xiàn)內(nèi)核態(tài)和用戶態(tài)
ARM:內(nèi)核態(tài)(svc模式),用戶態(tài)(usr模式)
x86 : 內(nèi)核態(tài)(ring 0 ),用戶態(tài)(ring 3)// x86有ring 0 - ring3四種特權(quán)等級 Linux實現(xiàn)內(nèi)核態(tài)和用戶態(tài)切換
ARM Linux的系統(tǒng)調(diào)用實現(xiàn)原理是采用swi軟中斷從用戶態(tài)切換至內(nèi)核態(tài)
X86是通過int 0x80中斷進入內(nèi)核態(tài)
Linux只能通過系統(tǒng)調(diào)用和硬件中斷從用戶空間進入內(nèi)核空間
執(zhí)行系統(tǒng)調(diào)用的內(nèi)核代碼運行在進程上下文中,他代表調(diào)用進程執(zhí)行操作,因此能夠訪問進程地址空間的所有數(shù)據(jù) 處理硬件中斷的內(nèi)核代碼運行在中斷上下文中,他和進程是異步的,與任何一個特定進程無關(guān)通常,一個驅(qū)動程序模塊中的某些函數(shù)作為系統(tǒng)調(diào)用的一部分,而其他函數(shù)負責中斷處理 4.2 Linux下應用程序調(diào)用驅(qū)動程序流程
在這里插入圖片描述
Linux下進行驅(qū)動開發(fā),完全將驅(qū)動程序與應用程序隔開,中間通過C標準庫函數(shù)以及系統(tǒng)調(diào)用完成驅(qū)動層和應用層的數(shù)據(jù)交換。
驅(qū)動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對“/dev/xxx” (xxx 是具體的驅(qū)動文件名字) 的文件進行相應的操作即可實現(xiàn)對硬件的操作。 用戶空間不能直接對內(nèi)核進行操作,因此必須使用一個叫做 “系統(tǒng)調(diào)用”的方法 來實現(xiàn)從用戶空間“陷入” 到內(nèi)核空間,這樣才能實現(xiàn)對底層驅(qū)動的操作 應用程序使用到的函數(shù)在具體驅(qū)動程序中都有與之對應的函數(shù),比如應用程序中調(diào)用了 open 函數(shù),那么在驅(qū)動程序中也得有一個名為 open 的函數(shù)。 每一個系統(tǒng)調(diào)用,在驅(qū)動中都有與之對應的一個驅(qū)動函數(shù),在 Linux 內(nèi)核文件 include/linux/fs.h 中有個叫做 file_operations 的結(jié)構(gòu)體,此結(jié)構(gòu)體就是 Linux 內(nèi)核驅(qū)動操作函數(shù)集合。 在這里插入圖片描述
4.3 內(nèi)核模塊
Linux 驅(qū)動有兩種運行方式
將驅(qū)動編譯進 Linux 內(nèi)核中,當 Linux 內(nèi)核啟動的時就會自動運行驅(qū)動程序。
將驅(qū)動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內(nèi)核啟動以后使用相應命令加載驅(qū)動模塊。
內(nèi)核模塊是Linux內(nèi)核向外部提供的一個插口 內(nèi)核模塊是具有獨立功能的程序,他可以被單獨編譯,但不能單獨運行。他在運行時被鏈接到內(nèi)核作為內(nèi)核的一部分在內(nèi)核空間運行 內(nèi)核模塊便于驅(qū)動、文件系統(tǒng)等的二次開發(fā) 內(nèi)核模塊組成
模塊加載函數(shù)
module_init(xxx_init);
module_init 函數(shù)用來向 Linux 內(nèi)核注冊一個模塊加載函數(shù),
參數(shù) xxx_init 就是需要注冊的具體函數(shù)(理解是模塊的構(gòu)造函數(shù))
當加載驅(qū)動的時, xxx_init 這個函數(shù)就會被調(diào)用
模塊卸載函數(shù)
module_exit(xxx_exit);
module_exit函數(shù)用來向 Linux 內(nèi)核注冊一個模塊卸載函數(shù),
參數(shù) xxx_exit 就是需要注冊的具體函數(shù)(理解是模塊的析構(gòu)函數(shù))
當使用“rmmod”命令卸載具體驅(qū)動的時候 xxx_exit 函數(shù)就會被調(diào)用
模塊許可證明
MODULE_LICENSE("GPL") //添加模塊 LICENSE 信息 ,LICENSE 采用 GPL 協(xié)議
模塊參數(shù)是一種內(nèi)核空間與用戶空間的交互方式,只不過是用戶空間 --> 內(nèi)核空間單向的,他對應模塊內(nèi)部的全局變量
MODULE_AUTHOR("songwei") //添加模塊作者信息
模塊操作命令
加載模塊
insmod XXX.ko
為模塊分配內(nèi)核內(nèi)存、將模塊代碼和數(shù)據(jù)裝入內(nèi)存、通過內(nèi)核符號表解析模塊中的內(nèi)核引用、調(diào)用模塊初始化函數(shù)(module_init)
insmod要加載的模塊有依賴模塊,且其依賴的模塊尚未加載,那么該insmod操作將失敗
modprobe XXX.ko
加載模塊時會同時加載該模塊所依賴的其他模塊
卸載模塊
rmmod XXX.ko
查看模塊信息
lsmod
查看系統(tǒng)中加載的所有模塊及模塊間的依賴關(guān)系
modinfo (模塊路徑)
查看詳細信息,內(nèi)核模塊描述信息,編譯系統(tǒng)信息
4.4 設(shè)備號
Linux 中每個設(shè)備都有一個設(shè)備號,設(shè)備號由主設(shè)備號和次設(shè)備號兩部分組成
主設(shè)備號表示某一個具體的驅(qū)動,次設(shè)備號表示使用這個驅(qū)動的各個設(shè)備。
Linux 提供了一個名為 dev_t 的數(shù)據(jù)類型表示設(shè)備號其中高 12 位為主設(shè)備號, 低 20 位為次設(shè)備
使用"cat /proc/devices"命令即可查看當前系統(tǒng)中所有已經(jīng)使用了的設(shè)備號(主)
MAJOR // 用于從 dev_t 中獲取主設(shè)備號,將 dev_t 右移 20 位即可。
MINOR //用于從 dev_t 中獲取次設(shè)備號,取 dev_t 的低 20 位的值即可。
MKDEV //用于將給定的主設(shè)備號和次設(shè)備號的值組合成 dev_t 類型的設(shè)備號。
4.5 地址映射
MMU(Memory Manage Unit)內(nèi)存管理單元
完成虛擬空間到物理空間的映射
內(nèi)存保護,設(shè)置存儲器的訪問權(quán)限,設(shè)置虛擬存儲空間的緩沖特性
對于 32 位的處理器來說,虛擬地址(VA,Virtual Address)范圍是 2^32=4GB
在這里插入圖片描述
內(nèi)存映射函數(shù)
CPU只能訪問虛擬地址,不能直接向寄存器地址寫入數(shù)據(jù),必須得到寄存器物理地址在Linux系統(tǒng)中對應的虛擬地址。
物理內(nèi)存和虛擬內(nèi)存之間的轉(zhuǎn)換,需要用到: ioremap 和 iounmap兩個函數(shù) ioremap,用于獲取指定物理地址空間對應的虛擬地址空間
/*
phys_addr:要映射給的物理起始地址(cookie)
size:要映射的內(nèi)存空間大小
mtype: ioremap 的類型,可以選擇 MT_DEVICE、 MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,
ioremap 函數(shù)選擇 MT_DEVICE
返回值: __iomem 類型的指針,指向映射后的虛擬空間首地址
*/
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}
例:獲取某個寄存器對應的虛擬地址
#define addr (0X020E0068) // 物理地址
static void __iomem* va; //指向映射后的虛擬空間首地址的指針
va=ioremap(addr, 4); // 得到虛擬地址首地址
iounmap,卸載驅(qū)動使用 iounmap 函數(shù)釋放掉 ioremap 函數(shù)所做的映射。
參數(shù) addr:要取消映射的虛擬地址空間首地址
iounmap(va);
I/O內(nèi)存訪問函數(shù)
當外部寄存器或外部內(nèi)存映射到內(nèi)存空間時,稱為 I/O 內(nèi)存。但是對于 ARM 來說沒有 I/O 空間,因此 ARM 體系下只有 I/O 內(nèi)存(可以直接理解為內(nèi)存)。
使用 ioremap 函數(shù)將寄存器的物理地址映射到虛擬地址后,可以直接通過指針訪問這些地址,但是 Linux 內(nèi)核不建議這么做,而是推薦使用一組操作函數(shù)來對映射后的內(nèi)存進行讀寫操作。
讀操作函數(shù)
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、 readw 和 readl 分別對應 8bit、 16bit 和 32bit 讀操作,參數(shù) addr 就是要讀取寫內(nèi)存地址,返回值是讀取到的數(shù)據(jù)
寫操作函數(shù)
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、 writew 和 writel分別對應 8bit、 16bit 和 32bit 寫操作,參數(shù) value 是要寫入的數(shù)值, addr 是要寫入的地址。
4.6 設(shè)備樹
Device Tree是一種描述硬件的數(shù)據(jù)結(jié)構(gòu),以便于操作系統(tǒng)的內(nèi)核可以管理和使用這些硬件,包括CPU或CPU,內(nèi)存,總線和其他一些外設(shè)。
Linux內(nèi)核從3.x版本之后開始支持使用設(shè)備樹,可以實現(xiàn)驅(qū)動代碼與設(shè)備的硬件信息相互的隔離,減少了代碼中的耦合性
引入設(shè)備樹之前:一些與硬件設(shè)備相關(guān)的具體信息都要寫在驅(qū)動代碼中,如果外設(shè)發(fā)生相應的變化,那么驅(qū)動代碼就需要改動。
引入設(shè)備樹之后:通過設(shè)備樹對硬件信息的抽象,驅(qū)動代碼只要負責處理邏輯,而關(guān)于設(shè)備的具體信息存放到設(shè)備樹文件中。如果只是硬件接口信息的變化而沒有驅(qū)動邏輯的變化,開發(fā)者只需要修改設(shè)備樹文件信息,不需要改寫驅(qū)動代碼。
DTS、DTB和DTC
在這里插入圖片描述
DTS
設(shè)備樹源碼文件,硬件的相應信息都會寫在.dts為后綴的文件中,每一款硬件可以單獨寫一份xxxx.dts
DTSI
對于一些相同的dts配置可以抽象到dtsi文件中,然后可以用include的方式到dts文件中
同一芯片可以做一個dtsi,不同的板子不同的dts,然后include同一dtsi
對于同一個節(jié)點的設(shè)置情況,dts中的配置會覆蓋dtsi中的配置
DTC
dtc是編譯dts的工具
DTB dts經(jīng)過dtc編譯之后會得到dtb文件,設(shè)備樹的二進制執(zhí)行文件 dtb通過Bootloader引導程序加載到內(nèi)核。 設(shè)備樹框架
1.根節(jié)點:\2.設(shè)備節(jié)點:nodex①節(jié)點名稱:node②節(jié)點地址:node@0, @后面即為地址3.屬性:屬性名稱(Property name)和屬性值(Property value)4.標簽 “/”是根節(jié)點,每個設(shè)備樹文件只有一個根節(jié)點。在設(shè)備樹文件中會發(fā)現(xiàn)有的文件下也有“/”根節(jié)點,這兩個**“/”根節(jié)點的內(nèi)容會合并成一個根節(jié)點。** Linux 內(nèi)核啟動的時會解析設(shè)備樹中各個節(jié)點的信息,并且在根文件系統(tǒng)的/proc/devicetree 目錄下根據(jù)節(jié)點名字創(chuàng)建不同文件夾 DTS語法
.dtsi頭文件
#include
#include "imx6ull.dtsi"
設(shè)備樹也支持頭文件,設(shè)備樹的頭文件擴展名為.dtsi。在.dts 設(shè)備樹文件中,還可以通過“#include”來引用.h、 .dtsi 和.dts 文件。
設(shè)備節(jié)點
設(shè)備樹是采用樹形結(jié)構(gòu)來描述板子上的設(shè)備信息的文件,每個設(shè)備都是一個節(jié)點,叫做設(shè)備節(jié)點,
每個節(jié)點都通過一些屬性信息來描述節(jié)點信息,屬性就是鍵—值對。
label: node-name@unit-address
label:節(jié)點標簽,方便訪問節(jié)點:通過&label訪問節(jié)點,追加節(jié)點信息
node-name:節(jié)點名字,為字符串,描述節(jié)點功能
unit-address:設(shè)備的地址或寄存器首地址,若某個節(jié)點沒有地址或者寄存器,可以省略
設(shè)備樹源碼中常用的幾種數(shù)據(jù)形式
1.字符串: compatible = "arm,cortex-a7";設(shè)置 compatible 屬性的值為字符串“arm,cortex-a7”
2.32位無符號整數(shù):reg = <0>; 設(shè)置reg屬性的值為0
3.字符串列表:字符串和字符串之間采用“,”隔開 compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
設(shè)置屬性 compatible 的值為“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
屬性
compatible屬性(兼容屬性)
"manufacturer,model"
manufacturer:廠商名稱
model:模塊對應的驅(qū)動名字
例:
imx6ull-alientekemmc.dts 中 sound 節(jié)點是 音頻設(shè)備節(jié)點,采用的歐勝(WOLFSON)出品的 WM8960, sound 節(jié)點的 compatible 屬性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
屬性值有兩個,分別為“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,
其中“fsl”表示廠商是飛思卡爾,
“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驅(qū)動模塊名字。
sound這個設(shè)備首先使用第一個兼容值在 Linux 內(nèi)核里面查找,看看能不能找到與之匹配的驅(qū)動文件,如果沒有找到的話就使用第二個兼容值查。
一般驅(qū)動程序文件會有一個 OF 匹配表,此 OF 匹配表保存著一些 compatible 值,如果設(shè)備節(jié)點的 compatible 屬性值和 OF 匹配表中的任何一個值相等,那么就表示設(shè)備可以使用這個驅(qū)動。