什么是驅動:
驅動就是對底層硬件設備的操作進行封裝,并向上層提供函數接口。
設備分類:
linux系統將設備分為3類:字符設備、塊設備、網絡設備。
- 字符設備:指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先后順序。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制臺和LED設備等。
- 塊設備: 指可以從設備的任意位置讀取一定長度數據的設備。塊設備包括硬盤、磁盤、U盤和SD卡等。
- 網絡設備: 網絡設備可以是一個硬件設備,如網卡; 但也可以是一個純粹的軟件設備, 比如回環接口(lo).一個網絡接口負責發送和接收數據報文。
用戶態:
- 是指用戶編寫程序、運行程序的層面,用戶態在開發時需要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函數。驅動插入鏈表的順序由設備號檢索,就是說主設備號和次設備號除了能區分不同種類的設備和不同類型的設備,還能起到將驅動程序加載到鏈表的某個位置,在下面介紹的驅動代碼的開發無非就是添加驅動(添加設備號、設備名和設備驅動函數)和調用驅動。
- 綜上所述:如果想要打開
dev
下面的pin4
引腳,過程是:用戶態調用open(“/de/pin4”,O_RDWR),對于內核來說,上層調用open函數會觸發一個軟中斷(系統調用專用,中斷號是0x80,0x80代表發生了一個系統調用),系統進入內核態,并走到system_call,可以認為這個就是此軟中斷的中斷服務程序入口,然后通過傳遞過來的系統調用號來決定調用相應的系統調用服務程序(在這里是調用VFS中的sys_open)。sys_open會在內核的驅動鏈表里面根據設備名和設備號查找到相關的驅動函數(每一個驅動函數是一個節點),驅動函數里面有通過寄存器操控IO口的代碼,進而可以控制IO口實現相關功能。 - system_call函數是怎么找到詳細的系統調用服務例程的呢? 通過系統調用號查找系統調用表sys_call_table!軟中斷指令INT 0x80運行時,系統調用號會被放入 eax 寄存器中,system_call函數能夠讀取eax寄存器獲取,然后將其乘以4,生成偏移地址,然后以sys_call_table為基址。基址加上偏移地址,就能夠得到詳細的系統調用服務例程的地址了!然后就到了系統調用服務例程了。
補充:
- 每個系統調用都對應一個系統調用號,而系統調用號就對應內核中的相應處理函數。
- 所有系統調用都是通過中斷0x80來觸發的。
- 使用系統調用時,通過eax 寄存器將系統調用號傳遞到內核,系統調用的入參通過ebx、ecx……依次傳遞到內核
- 和函數一樣,系統調用的返回值保存在eax中,所有要從eax中取出
基于框架編寫驅動代碼:
- 最簡單的字符設備驅動框架:
#include <linux/fs.h> //file_operations聲明
#include <linux/module.h> //module_init module_exit聲明
#include <linux/init.h> //__init __exit 宏定義聲明
#include <linux/device.h> //class devise聲明
#include <linux/uaccess.h> //copy_from_user 的頭文件
#include <linux/types.h> //設備號 dev_t 類型聲明
#include <asm/io.h> //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 = {.owner = THIS_MODULE,.open = pin4_open,.write = pin4_write,
};
/*
上面的代碼等同于以下代碼(但是在單片機的編譯環境里面不允許以上寫法):
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); //創建設備號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");
- 上面這個字符設備驅動代碼里面有讓代碼自動的在dev下面生成設備,除此之外我們還可以手動創建設備名。使用指令:
sudo mknod +設備名字 +設備類型(c表示字符設備驅動) +主設備號+次設備號
b : create a block (buffered) pecial file。 c, u: create a character (unbuffered) special file。 p: create a FIFO,刪除手動創建的設備名直接rm
就好。如下圖所示:
驅動模塊代碼編譯(模塊的編譯需要配置過的內核源碼,編譯、連接后生成的內核模塊后綴為.ko,編譯過程首先會到內核源碼目錄下,讀取頂層的Makefile文件,然后再返回模塊源碼所在目錄。):
- 使用下面的的代碼:
#include <linux/fs.h> //file_operations聲明
#include <linux/module.h> //module_init module_exit聲明
#include <linux/init.h> //__init __exit 宏定義聲明
#include <linux/device.h> //class devise聲明
#include <linux/uaccess.h> //copy_from_user 的頭文件
#include <linux/types.h> //設備號 dev_t 類型聲明
#include <asm/io.h> //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這個文件。文件內容如下圖所示:(-y表示編譯進內核,-m表示生成驅動模塊,CONFIG_表示是根據config生成的),所以只需要將obj-m += pin4drive.o
添加到Makefile中即可。
- 然后使用指令:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules
進行編譯生成驅動模塊。然后將生成的.ko
文件發送給樹莓派:scp pin4drive.ko pi@192.168.43.136:/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):
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void main()
{int fd,data;fd = open("/dev/pin4",O_RDWR);if(fd<0){printf("open fail\n");perror("reson:");}else{printf("open successful\n");}fd=write(fd,'1',1);
}
-
將以上代碼進行交叉編譯后發送給樹莓派,就可以看到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內核源代碼),也可以在樹莓派上面編譯,但在樹莓派里編譯,效率會很低,要非常久。這篇文章有講樹莓派驅動的本地編譯。