驅動認知
驅動就是將底層硬體設備的操作進行封裝,並向上層提供函數介面。
設備分類:linux系統將設備分為3類:字元設備、區塊設備、網路設備。
#我們來舉一個例子來說一下整體的呼叫過程
open("/dev/pin4",O_RDWR);
呼叫/dev下的pin4以可讀可寫的方式打開,**==對於上層open呼叫到核心時會發生一次軟中斷中斷號是0X80,從用戶空間進入到核心空間==**system_call
(核心函數),system_call會根據/dev/pin4裝置名,去找出你要的裝置號碼。 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口實現相關功能==**。
「
#使用者狀態:
#」
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函數。驅動插入鍊錶的順序由設備號檢索,就是說主設備號和次設備號除了能區分不同種類的設備和不同類型的設備,還能起到將驅動程式載入到鍊錶的某個位置,在下面介紹的驅動程式碼的開發無非就是新增驅動(新增裝置號、裝置名稱和裝置驅動函數)和呼叫驅動 。
」
補充:
#字元裝置驅動程式運作原理在linux的世界裡一切皆文件,所有的硬體裝置操作到應用層都會被抽象化成文件的操作。我們知道如果應用層要存取硬體設備,它必定要呼叫到硬體對應的驅動程式。 Linux核心有這麼多驅動程序,應用怎麼才能精確的呼叫到底層的驅動程式呢?
==必須知道的知識:==
struct inode
結構體來描述,這個結構體記錄了這個檔案的所有信息,例如檔案類型,訪問權限等。 /dev
目錄或其他如/sys
目錄下都會有一個檔案與之對應。 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函數操作介面的綁定,和將字元驅動註冊到核心的工作。
#include #include #include #include void main() { int fd,data; fd = open("/dev/pin4",O_RDWR); if(fdprintf("open fail\n"); perror("reson:"); } else{ printf("open successful\n"); } fd=write(fd,'1',1); }
-核心驅動 **==最簡單的字元裝置驅動框架==**:
#include //file_operations声明 #include //module_init module_exit声明 #include //__init __exit 宏定义声明 #include //class devise声明 #include //copy_from_user 的头文件 #include //设备号 dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno; //设备号,devno是用来接收创建设备号函数的返回值,销毁的时候需要传这个参数 static int major =231; //主设备号 static int minor =0; //次设备号 static char *module_name="pin4"; //模块名 //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_open\n"); //内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_write\n"); //内核的打印函数和printf类似 return 0; } //将上面的函数赋值给一个结构体中,方便下面加载到到驱动链表中去 static struct file_operations pin4_fops = { //static防止其他文件也有同名pin4_fops //static限定这个结构体的作用,仅仅只在这个文件。 .owner = THIS_MODULE, .open = pin4_open, .write = pin4_write, }; /* 上面的代码等同于以下代码(但是在单片机keil的编译环境里面不允许以上写法): 里面的每个pin4_fops结构体成员单独赋值 static struct file_operations pin4_fops; pin4_fops.owner = THIS_MODULE; pin4_fops.open = pin4_open; pin4_fops.write = pin4_write; */ //static限定这个结构体的作用,仅仅只在这个文件。 int __init pin4_drv_init(void) //真实的驱动入口 { int ret; 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); //创建设备文件,先有上面那一行代码,创建一个类然后这行代码,类下面再创建一个设备。 return 0; } void __exit pin4_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("GPL v2");
手動建立裝置名稱
#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
下多了個檔案供我們上層可以opensudo mknod 設備名字 設備類型(c表示字元設備驅動) 主設備號碼 次設備號碼
的去創造設備#驅動模組程式碼編譯(模組的編譯需要配置過的核心原始碼,編譯、連線後產生的核心模組後綴為**.ko
,編譯過程會先到核心原始碼目錄下,讀取頂層的Makefile文件,然後再返回模組原始碼所在目錄。):**
#include //file_operations声明 #include //module_init module_exit声明 #include //__init __exit 宏定义声明 #include //class devise声明 #include //copy_from_user 的头文件 #include //设备号 dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno; //设备号 static int major =231; //主设备号 static int minor =0; //次设备号 static char *module_name="pin4"; //模块名 //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_open\n"); //内核的打印函数和printf类似 return 0; } //read函数 static int pin4_read(struct file *file,char __user *buf,size_t count,loff_t *ppos) { printk("pin4_read\n"); //内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_write\n"); //内核的打印函数和printf类似 return 0; } static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open = pin4_open, .write = pin4_write, .read = pin4_read, }; //static限定这个结构体的作用,仅仅只在这个文件。 int __init pin4_drv_init(void) //真实的驱动入口 { int ret; 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); //创建设备文件 return 0; } void __exit pin4_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("GPL v2");
/SYSTEM/linux-rpi-4.19.y/drivers/char
将以上代码复制到一个文件中,然后下一步要做的是就是:将上面的驱动代码编译生成模块,再修改Makefile。(你放那个文件下,就改哪个文件下的Makefile)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
可以查看驅動程式已經裝進去了。
sudo chmod 666 /dev/pin4
為pin4賦予權限,讓所有人都可以打開成功。 然後再執行pin4test
表面上看沒有任何資訊輸出,其實核心裡面有列印訊息只是上層看不到如果想要查看核心列印的資訊可以使用指令:dmesg |grep pin4
。如下圖所示:表示驅動呼叫成功
在安裝驅動程式後可以使用指令:sudo rmmod 驅動名稱
(不需要寫入ko)將驅動卸載。
為什麼產生驅動模組需要在虛擬機器上產生?樹莓派不行嗎?
產生驅動模組需要編譯環境(linux原始碼並且編譯,需要下載和系統版本相同的Linux核心原始碼),也可以在樹莓派上面編譯,但在樹莓派裡編譯,效率會很低,要非常久。這篇文章有一個講樹莓派驅動的本地編譯。
以上是Linux中級-「驅動」 控制硬體必須學會的底層知識的詳細內容。更多資訊請關注PHP中文網其他相關文章!