驅動認知
1. 什么是驅動
驅動就是對底層硬件設備的操作進行封裝,并向上層提供函數接口。
設備分類:linux系統將設備分為3類:字符設備、塊設備、網絡設備。
- 字符設備:指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先后順序。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制臺和LED設備等,字符設備驅動程序通常至少要實現open、close、read和write的系統調用,字符終端(/dev/console)和串口(/dev/ttyS0以及類似設備)就是兩個字符設備,它們能很好的說明“流”這種抽象概念。
- 塊設備:指可以從設備的任意位置讀取一定長度數據的設備。塊設備包括硬盤、磁盤、U盤和SD卡等。
- 網絡設備:網絡設備可以是一個硬件設備,如網卡; 但也可以是一個純粹的軟件設備,比如回環接口(lo).一個網絡接口負責發送和接收數據報文。
我們來舉一個例子來說一下整體的調用過程
-
在上層我們調用 c語言 open函數
open("/dev/pin4",O_RDWR);
調用/dev下的pin4以可讀可寫的方式打開,**==對于上層open調用到內核時會發生一次軟中斷中斷號是0X80,從用戶空間進入到內核空間==** -
open會調用到
system_call
(內核函數),system_call會根據/dev/pin4設備名,去找出你要的設備號。 -
再調到虛擬文件VFS (為了上層調用到確切的硬件統一化),調用VFS里的
sys_open
,sys_open會找到在驅動鏈表里面,根據主設備號和次設備號找到引腳4里的open函數,我們在引腳4里的open是對寄存器操作
我們寫驅動無非就是做添加驅動:添加驅動做哪些事呢?
- 設備名
- 設備號
- 設備驅動函數 (操作寄存器 來驅動 IO口)
==綜上所述==如果想要打開dev
下面的pin4
引腳,過程是:用戶態調用open(“/de/pin4”,O_RDWR
),對于內核來說,上層調用open函數會觸發一個軟中斷(系統調用專用,中斷號是0x80,0x80代表發生了一個系統調用),系統進入內核態,并走到system_call
,可以認為這個就是此軟中斷的中斷服務程序入口,然后通過傳遞過來的系統調用號來決定調用相應的系統調用服務程序(在這里是調用VFS
中的sys_open
)。sys_open
會在內核的驅動鏈表里面根據設備名和設備號查找到相關的驅動函數(每一個驅動函數是一個節點
),**==驅動函數里面有通過寄存器操控IO口的代碼,進而可以控制IO口實現相關功能==**。
2. 各分態的詳解
用戶態:
-
是指用戶編寫程序、運行程序的層面,用戶態在開發時需要C的基礎和C庫,C庫講到文件,進程,進程間通信,線程,網絡,界面(GTk)。C庫(是linux標準庫一定有):就是Clibary,提供了程序支配內核干活的接口,調用的
open,read,write,fork,pthread,socket
由此處封裝實現,由寫的應用程序調用,C庫中的各種API調用的是內核態,支配內核干活。
內核態:
-
用戶要使用某個硬件設備時,需要內核態的設備驅動程序,進而驅動硬件干活,就比如之前文章里面所提到的
wiringPi庫
,就是提供了用戶操控硬件設備的接口,在沒有wiringPi庫時就需要自己實現wiringPi庫的功能,就是自己寫設備驅動程序。這樣當我們拿到另一種類型的板子時,同樣也可以完成開發。 -
在linux中一切皆文件,各種的文件和設備(比如:鼠標、鍵盤、屏幕、flash、內存、網卡、如下圖所示:)都是文件,那既然是文件了,就可以使用文件操作函數來操作這些設備。
-
有一個問題,open、read等這些文件操作函數是如何知道打開的文件是哪一種硬件設備呢?①在open函數里面輸入對應的文件名,進而操控對應的設備。②通過 ==設備號(主設備號和次設備號)== 。除此之外我們還要了解這些驅動程序的位置,和如何實現這些驅動程序,每一種硬件設備對應不同的驅動(這些驅動有我們自己來實現)。
-
Linux的設備管理是和文件系統緊密結合的,各種設備都以文件的形式存放在/dev目錄下,稱為 ==設備文件==。應用程序可以打開、關閉和讀寫這些設備文件,完成對設備的操作,就像操作普通的數據文件一樣。為了管理這些設備,系統為設備編了號,每個設備號又分為==主設備號== 和 ==次設備號==(如下圖所示:)。
主設備號用來區分不同種類的設備,而次設備號用來區分同一類型的多個設備。對于常用設備,Linux有約定俗成的編號,如硬盤的主設備號是3。一個字符設備或者塊設備都有一個主設備號和次設備號。**==主設備號和次設備號統稱為設備號==**。主設備號用來表示一個特定的驅動程序。
次設備號用來表示使用該驅動程序的各設備。例如一個嵌入式系統,有兩個LED指示燈,LED燈需要獨立的打開或者關閉。那么,可以寫一個LED燈的字符設備驅動程序,可以將其主設備號注冊成5號設備,次設備號分別為1和2。這里,次設備號就分別表示兩個LED燈。
==驅動鏈表==
管理所有設備的驅動,添加或查找
添加
是發生在我們編寫完驅動程序,加載到內核。查找
是在調用驅動程序,由應用層用戶空間去查找使用open函數。驅動插入鏈表的順序由設備號檢索,就是說主設備號和次設備號除了能區分不同種類的設備和不同類型的設備,還能起到將驅動程序加載到鏈表的某個位置,在下面介紹的驅動代碼的開發無非就是添加驅動(添加設備號、設備名和設備驅動函數)和調用驅動。
- system_call函數是怎么找到詳細的系統調用服務例程的呢?通過系統調用號查找系統調用表sys_call_table! 軟中斷指令INT 0x80運行時,系統調用號會被放入 eax寄存器中,system_call函數能夠讀取eax寄存器獲取,然后將其乘以4,生成偏移地址,然后以sys_call_table為基址。基址加上偏移地址,就能夠得到詳細的系統調用服務例程的地址了!然后就到了系統調用服務例程了。
補充:
- 每個系統調用都對應一個系統調用號,而系統調用號就對應內核中的相應處理函數。
- 所有系統調用都是通過中斷0x80來觸發的。
- 使用系統調用時,通過eax 寄存器將系統調用號傳遞到內核,系統調用的入參通過ebx、ecx……依次傳遞到內核
- 和函數一樣,系統調用的返回值保存在eax中,所有要從eax中取出
3. 字符設備驅動工作原理
字符設備驅動工作原理在linux的世界里一切皆文件,所有的硬件設備操作到應用層都會被抽象成文件的操作。我們知道如果應用層要訪問硬件設備,它必定要調用到硬件對應的驅動程序。Linux內核有那么多驅動程序,應用怎么才能精確的調用到底層的驅動程序呢?
==必須知道的知識:==
-
在Linux文件系統中,每個文件都用一個
struct inode
結構體來描述,這個結構體記錄了這個文件的所有信息,例如文件類型,訪問權限等。 -
在linux操作系統中,每個驅動程序在應用層的
/dev
目錄或者其他如/sys
目錄下都會有一個文件與之對應。 -
在linux操作系統中, 每個驅動程序都有一個設備號。
-
在linux操作系統中,每打開一次文件,Linux操作系統會在VFS層分配一個
struct file
結構體來描述打開的文件。
(1) 當open函數打開設備文件時,可以根據設備文件對應的struct inode結構體描述的信息,可以知道接下來要操作的設備類型(字符設備還是塊設備),還會分配一個struct file結構體。
(2) 根據struct inode結構體里面記錄的設備號,可以找到對應的驅動程序。這里以字符設備為例。在Linux操作系統中每個字符設備都有一個struct cdev結構體。此結構體描述了字符設備所有信息,其中最重要的一項就是字符設備的操作函數接口。
(3) 找到struct cdev結構體后,linux內核就會將struct cdev結構體所在的內存空間首地址記錄在struct inode結構體i_cdev成員中,將struct cdev結構體中的記錄的函數操作接口地址記錄在struct file結構體的f_ops成員中。
(4) 任務完成,VFS層會給應用返回一個文件描述符(fd)。這個fd是和struct file結構體對應的。接下來上層應用程序就可以通過fd找到struct file,然后在struct file找到操作字符設備的函數接口file_operation了。
其中,cdev_init和cdev_add在驅動程序的入口函數中就已經被調用,分別完成字符設備與file_operation函數操作接口的綁定,和將字符驅動注冊到內核的工作。
基于框架編寫驅動代碼:
- 上層調用代碼:操作驅動的上層代碼(pin4test.c):
#include
#include
#include
#include
voidmain()
{
intfd,data;
fd=open("/dev/pin4",O_RDWR);
if(fd<0){
printf("openfail
");
perror("reson:");
}
else{
printf("opensuccessful
");
}
fd=write(fd,'1',1);
}
-內核驅動**==最簡單的字符設備驅動框架==**:
字符設備驅動框架代碼
#include//file_operations聲明
#include//module_initmodule_exit聲明
#include//__init__exit宏定義聲明
#include//classdevise聲明
#include//copy_from_user的頭文件
#include//設備號dev_t類型聲明
#include//ioremapiounmap的頭文件
staticstructclass*pin4_class;
staticstructdevice*pin4_class_dev;
staticdev_tdevno;//設備號,devno是用來接收創建設備號函數的返回值,銷毀的時候需要傳這個參數
staticintmajor=231;//主設備號
staticintminor=0;//次設備號
staticchar*module_name="pin4";//模塊名
//led_open函數
staticintpin4_open(structinode*inode,structfile*file)
{
printk("pin4_open
");//內核的打印函數和printf類似
return0;
}
//led_write函數
staticssize_tpin4_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos)
{
printk("pin4_write
");//內核的打印函數和printf類似
return0;
}
//將上面的函數賦值給一個結構體中,方便下面加載到到驅動鏈表中去
staticstructfile_operationspin4_fops={
//static防止其他文件也有同名pin4_fops
//static限定這個結構體的作用,僅僅只在這個文件。
.owner=THIS_MODULE,
.open=pin4_open,
.write=pin4_write,
};
/*
上面的代碼等同于以下代碼(但是在單片機keil的編譯環境里面不允許以上寫法):
里面的每個pin4_fops結構體成員單獨賦值
staticstructfile_operationspin4_fops;
pin4_fops.owner=THIS_MODULE;
pin4_fops.open=pin4_open;
pin4_fops.write=pin4_write;
*/
//static限定這個結構體的作用,僅僅只在這個文件。
int__initpin4_drv_init(void)//真實的驅動入口
{
intret;
devno=MKDEV(major,minor);//2.創建設備號
ret=register_chrdev(major,module_name,&pin4_fops);
//3.注冊驅動告訴內核,把這個驅動加入到內核驅動的鏈表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代碼在dev下自動生成設備,創建一個類
pin4_class_dev=device_create(pin4_class,NULL,devno,NULL,module_name);
//創建設備文件,先有上面那一行代碼,創建一個類然后這行代碼,類下面再創建一個設備。
return0;
}
void__exitpin4_drv_exit(void)
{
device_destroy(pin4_class,devno);//先銷毀設備
class_destroy(pin4_class);//再銷毀類
unregister_chrdev(major,module_name);//卸載驅動
}
module_init(pin4_drv_init);//入口,內核加載驅動的時候,這個宏(不是函數)會被調用,去調用pin4_drv_init這個函數
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPLv2");
手動創建設備名
-
上面這個字符設備驅動代碼里面有讓代碼自動的在dev下面生成設備除此之外我們還可以手動創建設備名。使用指令:
sudo mknod +設備名字 +設備類型(c表示字符設備驅動) +主設備號+次設備號
b :create a block(buffered) pecial file。c, u:create a character (unbuffered)special file。 p:create a FIFO, 刪除手動創建的設備名直接rm就好。如下圖所示:
驅動框架執行流程:
-
通過上層程序打開某個設備,如果沒有驅動,執行就會報錯,在內核驅動中,上層系統調用
open,wirte
函數會觸發sys_call
、sys_call會調用sys_open,
和sys_write
、sys_open,和sys_write通過主設備號在內核的驅動鏈表里把設備驅動找出來,執行里面的open和write、我們為了整個流程順利進行,我們要先準備好驅動(設備驅動文件)。 - 設備驅動文件有固定框架:
-
module_init(pin4_drv_init);
//入口 去調用pin4_drv_init
函數 -
int __init pin4_drv_init(void)
//真實的驅動入口 -
驅動入口
devno = MKDEV(major,minor);
// 創建設備號 -
register_chrdev(major, module_name,&pin4_fops);
//注冊驅動 告訴內核,把上面準備好的結構體加入到內核驅動的鏈表中 -
pin4_class=class_create(THIS_MODULE,"myfirstdemo");
//由代碼在dev下自動生成設備,創建一個類 -
pin4_class_dev =device_create(pin4_class,NULL,devno,NULL,module_name);
//創建設備文件。 -
主要是要讓
/dev
下多了個文件供我們上層可以open -
如果沒有,也可以手動
sudo mknod +設備名字 +設備類型(c表示字符設備驅動) +主設備號+次設備號
的去創造設備
驅動模塊代碼編譯
驅動模塊代碼編譯
驅動模塊代碼編譯(模塊的編譯需要配置過的內核源碼,編譯、連接后生成的內核模塊后綴為.ko
,編譯過程首先會到內核源碼目錄下,讀取頂層的Makefile文件,然后再返回模塊源碼所在目錄。):
- 使用下面的的代碼:(就是上面的驅動架構代碼)
#include//file_operations聲明
#include//module_initmodule_exit聲明
#include//__init__exit宏定義聲明
#include//classdevise聲明
#include//copy_from_user的頭文件
#include//設備號dev_t類型聲明
#include//ioremapiounmap的頭文件
staticstructclass*pin4_class;
staticstructdevice*pin4_class_dev;
staticdev_tdevno;//設備號
staticintmajor=231;//主設備號
staticintminor=0;//次設備號
staticchar*module_name="pin4";//模塊名
//led_open函數
staticintpin4_open(structinode*inode,structfile*file)
{
printk("pin4_open
");//內核的打印函數和printf類似
return0;
}
//read函數
staticintpin4_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos)
{
printk("pin4_read
");//內核的打印函數和printf類似
return0;
}
//led_write函數
staticssize_tpin4_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos)
{
printk("pin4_write
");//內核的打印函數和printf類似
return0;
}
staticstructfile_operationspin4_fops={
.owner=THIS_MODULE,
.open=pin4_open,
.write=pin4_write,
.read=pin4_read,
};
//static限定這個結構體的作用,僅僅只在這個文件。
int__initpin4_drv_init(void)//真實的驅動入口
{
intret;
devno=MKDEV(major,minor);//創建設備號
ret=register_chrdev(major,module_name,&pin4_fops);//注冊驅動告訴內核,把這個驅動加入到內核驅動的鏈表中
pin4_class=class_create(THIS_MODULE,"myfirstdemo");//讓代碼在dev下自動>生成設備
pin4_class_dev=device_create(pin4_class,NULL,devno,NULL,module_name);//創建設備文件
return0;
}
void__exitpin4_drv_exit(void)
{
device_destroy(pin4_class,devno);
class_destroy(pin4_class);
unregister_chrdev(major,module_name);//卸載驅動
}
module_init(pin4_drv_init);//入口,內核加載驅動的時候,這個宏會被調用,去調用pin4_drv_init這個函數
module_exit(pin4_drv_exit);
MODULE_LICENSE("GPLv2");
-
在導入虛擬機的內核代碼中找到字符設備驅動的那一個文件夾:
/SYSTEM/linux-rpi-4.19.y/drivers/char
將以上代碼復制到一個文件中,然后下一步要做的是就是:將上面的驅動代碼編譯生成模塊,再修改Makefile。(你放那個文件下,就改哪個文件下的Makefile) -
文件內容如下圖所示:(-y表示編譯進內核,-m表示生成驅動模塊,CONFIG_表示是根據config生成的)所以只需要將
obj-m += pin4drive.o
添加到Makefile中即可。下圖:Makefile文件圖 -
編譯生成驅動模塊,將生成的
.ko
文件發送給樹莓派然后回/SYSTEM/linux-rpi-4.19.y
下使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
進行編譯生成驅動模塊。然后將生成的.ko
文件發送給樹莓派:scp drivers/char/pin4driver.ko pi@192.168.0.104:/home/pi
編譯生成驅動模塊會生成以下幾個文件: -
.o
的文件是object文件,.ko
是kernel object,與.o的區別在于其多了一些sections,比如.modinfo
。.modinfo section
是由kernel source里的modpost工具生成的,包括MODULE_AUTHOR, MODULE_DESCRIPTION, MODULE_LICENSE, device ID table以及模塊依賴關系等等。depmod 工具根據.modinfo section生成modules.dep, modules.*map等文件,以便modprobe更方便的加載模塊。
- 編譯過程中,經歷了這樣的步驟:
- 先進入Linux內核所在的目錄,并編譯出pin4drive.o文件
- 運行MODPOST會生成臨時的pin4drive.mod.c文件, 而后根據此文件編譯出pin4drive.mod.o,
- 之后連接pin4drive.o和pin4drive.mod.o文件得到模塊目標文件pin4drive.ko,
- 最后離開Linux內核所在的目錄。
將pin4test.c (上層調用代碼) 進行 交叉編譯后發送給樹莓派,就可以看到pi目錄下存在發送過來的.ko文件
和pin4test
這兩個文件,如下圖所示:
加載內核驅動
然后使用指令:sudo insmod pin4drive.ko
加載內核驅動(相當于通過insmod調用了module_init這個宏,然后將整個結構體加載到驅動鏈表中)加載完成后就可以在dev
下面看到名字為pin4
的設備驅動(這個和驅動代碼里面static char *module_name="pin4"; //模塊名這行代碼有關),設備號也和代碼里面相關。
lsmod
可以查看驅動已經裝進去了。
-
我們再執行./pin4test 執行上層代碼 執行上層代碼出現以下錯誤:表示沒有權限 使用指令:
sudo chmod 666 /dev/pin4
為pin4賦予權限,讓所有人都可以打開成功。
然后再次執行pin4test
表面上看沒有任何信息輸出,其實內核里面有打印信息只是上層看不到如果想要查看內核打印的信息可以使用指令:dmesg |grep pin4
。如下圖所示:表示驅動調用成功在裝完驅動后可以使用指令:sudo rmmod +驅動名
(不需要寫ko)將驅動卸載。
為什么生成驅動模塊需要在虛擬機上生成
-
為什么生成驅動模塊需要在虛擬機上生成?樹莓派不行嗎?
生成驅動模塊需要編譯環境(linux源碼并且編譯,需要下載和系統版本相同的Linux內核源代碼),也可以在樹莓派上面編譯,但在樹莓派里編譯,效率會很低,要非常久。這篇文章有講樹莓派驅動的本地編譯。
原文標題:Linux中級——“驅動” 控制硬件必須學會的底層知識
文章出處:【微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
-
嵌入式系統
+關注
關注
41文章
3570瀏覽量
129253 -
驅動
+關注
關注
12文章
1827瀏覽量
85186 -
Linux
+關注
關注
87文章
11232瀏覽量
208957
原文標題:Linux中級——“驅動” 控制硬件必須學會的底層知識
文章出處:【微信號:yikoulinux,微信公眾號:一口Linux】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論