在進行linux驅動開發之前,我們先來思考一下什么是linux驅動?我們在前面的文章中介紹過Mini2440裸機程序的開發,比如如何點亮LED、如何通過LCD顯示圖片。
但是如果我們開發板移植了linux內核之后,我還想點亮LED,那該怎么辦呢?
由於我們編寫的應用程序是無法和硬件直接打交道的,為此衍生了驅動程序,驅動程序充當了硬件和應用程序之間的樞紐。
因此驅動程序的表現形式可能就是一些標准的、事先協定好的API函數,編寫一個驅動只需要去完成相應函數的填充就可以了。
一、linux驅動介紹
1.1 linux內核架構
linux內核作為操作系統內核,向下承接最底層的硬件驅動,向上提供應用層的接口實現,適用於各類軟硬件結合系統。
其擁有五大核心部分:進程管理、內存管理、文件系統、設備驅動與網絡模塊。
下圖是從網上找來的linux內核的架構圖,下圖反映了應用程序、linux內核、驅動程序、硬件的關系:

這里我們只介紹設備驅動,其它部分內容不是我們這一節的重點。
1.2 linux設備驅動
linux 將所有的外設分為 3 類:
- 字符設備:字符設備是能夠像字節流(比如文件)一樣被訪問的設備,就是說對它的讀寫是以字節為單位的。 比如串口在進行收發數據時就是一個字節一個字節的進行的,我們可以在驅動程序內部使用緩沖區來存放數據以提高效率,但是串口本身對這並沒有要求。字符設備的驅動程序中實現了 open、close、read、write 等系統調用,應用程序可以通過設備文件(比如/dev/ttySAC0 等)來訪問字符設備。
- 塊設備:塊設備上的數據以塊的形式存放,比如 NAND FLASH上的數據就是以頁為單位存放的。塊設備驅動程序向用戶層提供的接口與字符設備一樣, 應用程序也可以通過相應的設備文件(比如/dev/mtdblock0、/dev/hda1 等)來調用 open、close、read、write 等系統調用,與塊設備傳送任意字節的數據。對用戶而言,字符設備和塊設備的訪問方式沒有差別。塊設備驅動程序的特別之處如下。
1). 操作硬件的接口實現方式不一樣。
塊設備驅動程序先將用戶發來的數據組織成塊,再寫入設備;或從設備中讀出若干塊數據,再從中挑出用戶需要的。
2). 數據塊上的數據可以有一定的格式。
通常在塊設備中按照一定的格式存放數據,不同的文件系統類型就是用來定義這些格式的。內核中,文件系統的層次位於塊設備驅動程序上面,這意味着塊設備驅動程序除了向用戶層提供與字符設備一樣的接口外,還要向內核其他部件提供一些接口,這些接口用戶是看不到的。這些接口使得可以在塊設備上存放文件系統,掛載塊設備。
- 網絡設備:網絡設備同時具有字符設備、塊設備的部分特點,無法將它歸入這兩類中:如果說它是字符設備,他的輸入/輸出卻是有結構的、成塊的(報文、包、幀);如果說它是塊設備,它的“塊”又不是固定大小的,大到數百甚至數千字節,小到幾字節。UNIX 式的操作系統訪問網絡接口的方法是給它們分配一個唯一的名字(比如 eth0),但這個名字在文件系統中(比如/dev 目錄下)不存在對應的節點項。應用程序、內核和網絡驅動程序間的通信完全不同於字符設備、塊設備,庫、內核提供了一套和數據包傳輸相關的函數,而不是 open、read、write 等。
在linux系統中,有一個約定俗成的說法”一切皆是文件“。應用程序使用設備文件節點訪問對應設備。
linux下的各種硬件設備以文件的形式存放在/dev目錄下,通過ls /dev查看:
root@zhengyang:/work/sambashare/linux-5.2.8-drivers# ls /dev agpgart full mapper sda1 tty15 tty33 tty51 ttyS10 ttyS29 vcs6 autofs fuse mcelog sda2 tty16 tty34 tty52 ttyS11 ttyS3 vcs7 block hidraw0 mem sda5 tty17 tty35 tty53 ttyS12 ttyS30 vcsa bsg hpet memory_bandwidth sg0 tty18 tty36 tty54 ttyS13 ttyS31 vcsa1 btrfs-control hugepages midi sg1 tty19 tty37 tty55 ttyS14 ttyS4 vcsa2 bus hwrng mqueue shm tty2 tty38 tty56 ttyS15 ttyS5 vcsa3 cdrom initctl net snapshot tty20 tty39 tty57 ttyS16 ttyS6 vcsa4 cdrw input network_latency snd tty21 tty4 tty58 ttyS17 ttyS7 vcsa5 char kmsg network_throughput sr0 tty22 tty40 tty59 ttyS18 ttyS8 vcsa6 console lightnvm null stderr tty23 tty41 tty6 ttyS19 ttyS9 vcsa7 core log port stdin tty24 tty42 tty60 ttyS2 uhid vfio cpu_dma_latency loop0 ppp stdout tty25 tty43 tty61 ttyS20 uinput vga_arbiter cuse loop1 psaux tty tty26 tty44 tty62 ttyS21 urandom vhci disk loop2 ptmx tty0 tty27 tty45 tty63 ttyS22 userio vhost-net dmmidi loop3 pts tty1 tty28 tty46 tty7 ttyS23 vcs vhost-vsock dri loop4 random tty10 tty29 tty47 tty8 ttyS24 vcs1 vmci dvd loop5 rfkill tty11 tty3 tty48 tty9 ttyS25 vcs2 vsock ecryptfs loop6 rtc tty12 tty30 tty49 ttyprintk ttyS26 vcs3 zero fb0 loop7 rtc0 tty13 tty31 tty5 ttyS0 ttyS27 vcs4 fd loop-control sda tty14 tty32 tty50 ttyS1 ttyS28 vcs5
linux把對硬件的操作全部抽象成對文件的操作,比如open、read、write、close等。
每個設備文件都有其文件屬性(c或者b),使用ll /dev 的命令查看, 表明其是字符設備或者塊設備,網絡設備沒有在這個文件夾下。
二、linux驅動開發步驟
Linux 內核就是由各種驅動組成的,內核源碼中有大約 85%是各種驅動程序的代碼。內核中驅動程序種類齊全,可以在同類驅動的基礎上進行修改以符合具體單板。
編寫驅動程序的難點並不是硬件的具體操作,而是弄清楚現有驅動程序的框架,在這個框架中加入這個硬件。
比如,x86 架構的內核對 IDE 硬盤的支持非常完善:首先通過 BIOS 得到硬盤的信息,或者使用默認 I/O 地址去枚舉硬盤,然后識別分區、掛載文件系統。對於其他架構的內核,只是要指定了硬盤的訪問地址和中斷號,后面的枚舉、識別和掛接的過程完全是一樣的。也許修改的代碼不超過 10 行,花費精力的地方在於:了解硬盤驅動的框架, 找到修改的位置。
編寫驅動程序還有很多需要注意的地方,比如:驅動程序可能同時被多個進程使用,這需要考慮並發的問題;盡可能發揮硬件的作用以提高性能。比如在硬盤驅動程序中既可以使用 DMA 也可以不用,使用 DMA 時程序比較復雜,但是可以提高效率;處理硬件的各種異常情況,否則出錯時可能導致整個系統崩潰。
2.1 驅動程序開發步驟
一般來說,編寫一個 linux 設備驅動程序的大致流程如下:
- 查看原理圖、數據手冊,了解設備的操作方法;
- 在內核中找到相近的驅動程序,以它為模板進行開發,有時候需要從零開始;
- 實現驅動程序的初始化:比如向內核注冊這個驅動程序,這樣應用程序傳入文件名時,內核才能找到相應的驅動程序;
- 設計所要實現的操作,比如 open、close、read、write 等函數;
- 實現中斷服務(中斷並不是每個設備驅動所必須的);
- 編譯該驅動程序到內核中,或者用 insmod 命令加載;
- 測試驅動程序;
2.2 驅動程序的加載和卸載
linux設備驅動屬於內核的一部分,設備驅動可以以一下兩種方式加載到內核中:
- 直接編譯進linux內核,隨同linux啟動時加載;
- 設備驅動可以將它作為模塊在使用時再加載,模塊的擴展名為.ko,使用 insmod 命令加載,使用 rmmod 命令卸載;
2.3 驅動程序執行流程
假設,我們編寫好了LED設備對應的字符驅動程序,並且加載到了linux內核,那么我們的應用程序點亮LED的流程是怎樣的呢?
- 應用程序使用庫提供的open函數打開代表 LED的設備文件;
- 庫根據open函數傳入的參數執行“swi”指令,這條指令會引起 CPU 異常,進入內核;
- 內核的異常處理函數根據這些參數找到相應的驅動程序,返回一個文件句柄給庫,進而返回給應用程序;
- 應用程序得到文件句柄后,使用庫提供的 write 或 ioclt 函數發出控制命令;
- 庫根據 write 和 ioclt 函數傳人的參數執行 “swi” 指令, 這條指令會引起 CPU 異常,進入內核;
- 內核的異常處理函數根據這些參數調用驅動程序的相關函數。
庫(比如 glibc)給應用程序提供的 open、read、write、ioctl、mmap 等接口函數被稱為系統調用,它們都是設置好相關寄存器后,執行某條指令引發異常進入內核。
除系統調用接口外, 庫還提供其他函數, 比如字符串處理函數(strcpy、 strcmp 等)、 輸入/輸出函數(scanf、printf 等)、數學庫,還有應用程序的啟動代碼等。
在異常處理函數中,內核會根據傳入的參數執行各種操作,比如根據設備文件名找到對應的驅動程序,調用驅動程序的相關函數等。

與應用程序不同,驅動程序從不主動運行,它是被動的:根據應用程序的要求進行初始化,根據應用程序的要求進行讀寫。
驅動程序加載進內核時,只是告訴內核我在這里,我能做這些工作,至於這些工作何時開始,取決於應用程序。當然,這不是絕對的,比如用戶完全可以寫一個系統時鍾觸發的驅動程序,讓它自動點亮 LED。
在 linux 系統中,應用程序運行於用戶空間,擁有 MMU 的系統能夠限制應用程序的權限(比如將它限制於某個內存塊中),這可以避免應用程序的錯誤使整個系統崩潰。而驅動程序運行於內核空間,它是系統信任的一部分,驅動程序的錯誤有可能導致整個系統崩潰。
三、設備驅動程序框架(hello_dev案例)
按照前面的介紹,我們大致對設備驅動有了一個粗略的了解,本小節我們將會搭建一個簡單的字符設備驅動程序的框架。這里以hello_dev驅動為例。
在/work/sambashare下創建一個drivers文件夾,然后在drivers目錄下創建一個文件夾命名為1.hello_dev,用來保存我們第一個驅動的源代碼。
3.1 hello_open、hello_read等方法實現
int hello_open(struct inode *p, struct file *f) { printk("hello_open\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk("hello_write\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk("hello_read\n"); return 0; }
3.2 注冊驅動程序
定義一個cdev結構成員:
struct cdev *gDev;
通過cdev名字不難猜出,這代表一個字符設備。
定義一個file_operations結構成員:
struct file_operations *gFile;
通過file_operations名字不難猜測出,這個結構成員定義了對設備文件操作的各個回調函數。
int hello_init(void) { int ret = 0; printk("%s enter.\n", __func__); devNum = MKDEV(reg_major, reg_minor); /* 靜態注冊一組字符設備編號 */ ret = register_chrdev_region(devNum, subDevNum, "hello_dev"); /* 返回值為負數,表示操作失敗 */ if(ret < 0){ printk("register char dev region error\n"); goto fail_devid; } gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); if (!gDev) { printk("kzalloc for cdev error\n"); goto fail_gdev; } gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); if (!gFile) { printk("kzalloc for file_operations error\n"); goto fail_gfile; } gFile->open = hello_open; gFile->read = hello_read; gFile->write = hello_write; gFile->owner = THIS_MODULE; /* 初始化字符設備,添加字符設備 */ cdev_init(gDev, gFile); ret = cdev_add(gDev, devNum, 1); /* 返回值為負數,表示操作失敗 */ if (ret < 0) { printk("char device add failed\n"); goto fail_cdev; }else{ printk("char device add success\n"); } return 0; fail_cdev: kfree(gFile); fail_gfile: kfree(gDev); fail_gdev: unregister_chrdev_region(devNum, subDevNum); fail_devid: return ret; }
大概介紹一下這里面幾個主要函數:
(1) 使用register_chrdev_region()來靜態注冊一組字符設備編號,當返回值小於0,表示注冊失敗。
/*靜態注冊一組字符設備編號*/ int register_chrdev_region(dev_t from, unsigned count, const char *name);
參數如下:
- from:注冊的指定起始設備編號,比如:MKDEV(100, 0),表示起始主設備號100,起始次設備號為0;
- count:需要連續注冊的次設備編號個數,比如: 起始次設備號為0,count=100,表示0~99的次設備號都要綁定在同一個file_operations操作方法結構體上;
- *name:字符設備名稱,卸載驅動的時候指定的就是這個名字;
(2) 使用cdev_init初始化字符設備結構體cdev,file_operations結構體放入cdev-> ops 里。
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
其中cdev結構體的成員,如下所示:
struct cdev { struct kobject kobj; //內嵌的kobject對象 struct module *owner; //所屬模塊 const struct file_operations *ops; //操作方法結構體 struct list_head list; //與 cdev 對應的字符設備文件的 inode->i_devices 的鏈表頭 dev_t dev; //起始設備編號,可以通過MAJOR(),MINOR()來提取主次設備號 unsigned int count; //連續注冊的次設備號個數 };
(3) 使用cdev_add將字符設備gDev添加到系統,並將dev(起始設備編號)放入cdev-> dev里, count(次設備編號個數)放入cdev->count里:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
3.3 卸載驅動程序
void __exit hello_exit(void) { printk("hello driver exit\n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; }
首先使用cdev_del從系統中移除字符設備gDev,然后使用unregister_chrdev_region注銷字符設備:
void unregister_chrdev_region(dev_t from, unsigned count);
參數如下:
- from::注銷的指定起始設備編號,比如:MKDEV(100, 0),表示起始主設備號100, 起始次設備號為0;
- count:需要連續注銷的次設備編號個數,比如:起始次設備號為0,baseminor=100,表示注銷掉0~99的次設備號;
3.4 驅動入口函數
module_init(hello_init);
module_exit(hello_exit);
3.5 Makefile文件
KERN_DIR :=/work/sambashare/linux-5.2.8 all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += hello_dev.o
這里實際執行的命令是:
make -C /work/sambashare/linux-5.2.8 M=`pwd` modules
其中`pwd`會返回shell命令pwd執行的結果。這里-C表示切換到內核工作路徑下。然后執行:
make M=`pwd` modules
我們切換到內核Makefile路徑下,可以發現目標modules定義如下:
# Build modules # # A module can be listed more than once in obj-m resulting in # duplicate lines in modules.order files. Those are removed # using awk while concatenating to the final file. PHONY += modules modules: $(vmlinux-dirs) $(if $(KBUILD_BUILTIN),vmlinux) modules.builtin $(Q)$(AWK) '!x[$$0]++' $(vmlinux-dirs:%=$(objtree)/%/modules.order) > $(objtree)/modules.order @$(kecho) ' Building modules, stage 2.'; $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.modpost $(Q)$(CONFIG_SHELL) $(srctree)/scripts/modules-check.sh
我們輸出make運行日志信息:

大致可以看出來應該就是執行了這些命令。
“M=”選項的作用是,當用戶需要以某個內核為基礎編譯一個外部模塊的話,需要在make modules 命令中加入“M=dir”,程序會自動到你所指定的dir目錄中查找模塊源碼,將其編譯,生成ko文件。
3.6 完整代碼hello_dev.c
#include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/slab.h> #define OK (0) #define ERROR (-1) // 字符設備 struct cdev *gDev; struct file_operations *gFile; dev_t devNum; // 起始設備編號 unsigned int subDevNum = 1; // 次設備個數 int reg_major = 232; // 主設備編號 int reg_minor = 0; // 起始次設備編號 int hello_open(struct inode *p, struct file *f) { printk("hello_open\n"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk("hello_write\n"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk("hello_read\n"); return 0; } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); if(OK == register_chrdev_region(devNum, subDevNum, "hello_dev")){ printk("register_chrdev_region ok\n"); }else { printk("register_chrdev_region error\n"); return ERROR; } printk("hello driver init\n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); gFile->open = hello_open; gFile->read = hello_read; gFile->write = hello_write; gFile->owner = THIS_MODULE; cdev_init(gDev, gFile); cdev_add(gDev, devNum, 1); return 0; } void __exit hello_exit(void) { printk("hello driver exit\n"); cdev_del(gDev); kfree(gFile); kfree(gDev); unregister_chrdev_region(devNum, subDevNum); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
代碼中引入的linux/xxx.h頭文件位於linux-5.2.8源代碼include/linux路徑下。這里有很多頭文件,初學linux編程,可能對各個頭文件不熟悉,后面單獨一小節介紹一下這些頭文件。

3.7 編譯驅動
在驅動代碼所在路徑運行如下命令,注意:需要配置編譯器為arm-linux-gcc 4.8.3,和編譯內核使用的編譯器版本一致:
root@zhengyang:/work/sambashare/drivers/1.hello_dev# cd /work/sambashare/drivers/1.hello_dev root@zhengyang:/work/sambashare/drivers/1.hello_dev# make
可以看到:
root@zhengyang:/work/sambashare/drivers/1.hello_dev# ll 總用量 292 drwxr-xr-x 4 root root 4096 2月 11 21:47 ./ drwxr-xr-x 3 root root 4096 2月 11 18:06 ../ -rwxrw-rw- 1 root root 1750 2月 11 21:30 hello_dev.c* -rw-r--r-- 1 root root 99798 2月 11 21:30 hello_dev.ko -rw-r--r-- 1 root root 317 2月 11 21:30 .hello_dev.ko.cmd -rw-r--r-- 1 root root 593 2月 11 21:30 hello_dev.mod.c -rw-r--r-- 1 root root 28516 2月 11 21:30 hello_dev.mod.o -rw-r--r-- 1 root root 22295 2月 11 21:30 .hello_dev.mod.o.cmd -rw-r--r-- 1 root root 72172 2月 11 21:30 hello_dev.o -rw-r--r-- 1 root root 30565 2月 11 21:30 .hello_dev.o.cmd -rwxrw-rw- 1 root root 191 2月 11 18:12 Makefile* -rw-r--r-- 1 root root 57 2月 11 21:30 modules.order -rw-r--r-- 1 root root 0 2月 11 21:30 Module.symvers
四、測試hello_dev驅動
如果需要測試驅動,我們需要將該驅動文件復制到根文件系統中, 並加載驅動到內核。然后在運行對應的應用程序。
4.1 加載驅動到內核
我們首先將驅動拷貝到根文件系統rootfs中:
cp /work/sambashare/drivers/1.hello_dev/hello_dev.ko /work/nfs_root/rootfs/
燒錄根文件系統,啟動內核,加載hello_dev驅動:
[root@zy:/]# insmod hello_dev.ko hello_dev: loading out-of-tree module taints kernel. hello_init enter. char device add success
可見,執行insmod的時候,驅動文件里的hello_init被調用了。
4.2 編寫測試應用程序
在1.hello_dev路徑下,創建app文件夾:
mkdir test
新建main.c文件:
#include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main(int argc,char **argv) { int fd; int val = 1; fd = open("/dev/hello_dev",O_RDWR); if(fd == -1){ printf("can't open!\n"); return -1; }else{ printf("open success!\n"); } write(fd,&val,4); return 0; }
在test路徑下,編譯:
arm-linux-gcc -march=armv4t -o main main.c
將測試程序拷貝到根文件系統rootfs中:
cp /work/sambashare/drivers/1.hello_dev/test/main /work/nfs_root/rootfs/
然后在開發板中運行該程序:
[root@zy:/]# ./main can't open!
這是因為還沒有創建hello_dev驅動的設備文件,我們為hello_dev驅動手動創建設備文件:
[root@zy:/]# mknod /dev/hello_dev c 232 0
備注:這里的232和0要跟驅動文件里定義的主次設備號對應起來!
然后再次執行測試程序,發現成功了:
[root@zy:/]# ./main hello_open open success! hello_write [root@zy:/]#
此外我們可以通過dmesg命令查看驅動輸出信息。
4.3 卸載驅動
卸載hello_dev驅動:
rmmod hello_dev
出現如下錯誤:
rmmod: can't change directory to '/lib/modules': No such file or directory
報錯,執行 “mkdir /lib/modules/xxx” 指令,xxx 是執行 uname -r 指令后查詢的內核版本號。
mkdir -p /lib/modules/5.2.8
再次卸載驅動:
[root@zy:/]# rmmod hello_dev hello driver exit
執行rmmod的時候,hello_exit被調用了。
如果想查看當前系統有哪些驅動,運行:
cat /proc/devices
五、編譯驅動到內核
之前介紹的驅動我們是單獨編譯,然后使用insmod命令安裝到內核。這里介紹一下另一種方式,將驅動直接編譯到內核。
5.1 復制驅動到內核
將驅動程序hello_dev.c復制到linux-5.2.8/drivers/char路徑下:
cp /work/sambashare/drivers/1.hello_dev/hello_dev.c /work/sambashare/linux-5.2.8/drivers/char
5.2 修改Kconfig
cd /work/sambashare/linux-5.2.8/drivers/char
vim Kconfig
新增如下內容:
config HELLO bool "hello_dev" default y help hello driver
在源碼頂層運行make menuconfig,在Device Drivers -> Character devices可以看到:

5.3 修改Makefile
cd /work/sambashare/linux-5.2.8/drivers/char vim Makefile
新增如下內容:
obj-$(CONFIG_HELLO) += hello_dev.o
5.4 編譯內核
在源碼頂層運行:
make s3c2440_defconfig make uImage
可以看到編譯信息:

這樣hello_dev驅動就被編譯進內核,就可以直接運行測試應用程序了。
六、設備文件
6.1 設備文件的作用
之前我們說過在/dev目錄下有很多設備文件,比如:

那設備文件有什么作用呢?實際上,當我們應用程序通過open去打開一個設備的時候,首先就是從文件屬性中獲取到這個設備文件的類型、主設備號、以及次設備號,比如上圖中的sda設備主設備號都是8,次設備號有0、1、2、5。
通過 設備類型 + 設備號(主、次設備號)我們就可以獲取到這個設備的file_operations結構。通過file_operations結構我們就可以找到驅動程序中的讀寫、等方法。
6.2 主設備號設置
主設備號的設置有兩種方法:
其一:通過cat /proc/devices可以找到有哪些已經使用的主設備號,我們手動指定一個沒有使用的主設備號即可。
[root@zy:/]# cat /proc/devices Character devices: 1 mem 2 pty 3 ttyp 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 6 lp 7 vcs 10 misc 13 input 21 sg 29 fb 90 mtd 99 ppdev 116 alsa 128 ptm 136 pts 180 usb 188 ttyUSB 189 usb_device 204 ttySAC 232 hello 250 rpmb 251 usbmon 252 watchdog 253 rtc 254 gpiochip ...
其二:使用alloc_chrdev_region動態分配一組字符設備編號,由系統自動分配主設備號,注冊成功將分配到的起始設備編號存放在dev指針中;
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
其中:
-
dev:存放起始設備編號的指針,可以用MAJOR宏和MINOR宏,將主設備號和次設備號提取出來;
-
baseminor:次設備號的基准,從第幾個次設備號開始分配;
-
count:需要連續注冊的次設備編號個數,比如: 起始次設備號(baseminor)為0,baseminor=2,表示0~1的此設備號都要綁定在同一個file_operations操作方法結構體上;
-
name:字符設備的名字;
6.3 /dev/xxx的創建
在之前的測試發現,應用程序調用open要打開一個設備文件,必須要有設備節點才行,不然會打開失敗。
我們每次可以通過手動創建設備節點:
mknod /dev/xxx c 主 次
不過這個方式相對來說比較麻煩。
我們可以利用mdev機制自動創建設備節點:
(1) 驅動需要調用class_create動態創建設備類,此函數的執行效果就是在/sys/class/目錄下創建一個新的文件夾,比如/sys/class/led;
(2) 調用device_create在/sys/class/xxx類文件下創建dev文件,供mdev程序掃描生成/dev下的節點;
(3) 執行/sbin/mdev -s 去掃描上一步中創建的節點,其中/sbin/mdev由busybox生成;
(4) 編譯內核需要配置make menuconfig;
Device Drivers ---> Generic Driver Options ---> [*] Support for uevent helper
該選項的作用是啟用uevent helper程序的支持。uevent是內核與用戶空間之間通信的一種方式,當內核檢測到新的設備時,會生成一個uevent來通知用戶空間,使得用戶空間能夠及時響應設備插拔事件,並做出相應的處理。其中, uevent helper程序就是在接收到uevent后執行的用戶空間程序,用來完成設備的熱插拔處理。
在內核中,CONFIG_UEVENT_HELPER=y 的設定可以確保uevent helper程序能夠被編譯到內核中,從而能夠正常地接收並響應uevent事件。
(4) 而/sbin/mdev何時執行,有兩種方式,
方式一:可以通過配置文件系統/etc/init.d/rcS;
# 使用mdev動態管理u盤和鼠標等熱插拔設備
/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug
方式二:編譯內核配置make menuconfig,path to uevent helper 配置為/sbin.mdev;
Device Drivers ---> Generic Driver Options ---> [*] Support for uevent helper () path to uevent helper (NEW)
配置后,會在.config生成配置項CONFIG_UEVENT_HELPER_PATH=/sbin/mdev,即指定uevent helper程序為/sbin.mdev。
這樣加載內核module之后才會自動在/dev下創建設備文件。具體可以參考這篇博客:dev下無法生成節點的分析思路和解決方法及原理。
注:cdev_add和device_create區別:
- cdev_init和cdev_add函數執行字符設備注冊。 cdev_add將字符設備添加到系統中。當cdev_add函數成功完成時,設備處於活動狀態,內核可以調用其操作;
- 要從用戶空間訪問此設備,必須在/dev中創建設備節點;通過使用class_create創建設備類,然后使用device_create來完成/dev下設備文件的創建;
6.4 注意
編譯linux內核的編譯器、編譯根文件系統的編譯器、以及編譯驅動和應用程序的工具鏈版本要保持一致,不然應用程序可能會缺少某些庫而無法運行。
七、編寫linux驅動所用到的頭文件
由於編寫linux驅動可能用到許多同文件,而這些頭文件很難記住,因此,這里從網上找到一份,這些頭文件可以在/usr/include路徑下找到:
- <linux/module.h> 最基本的文件,支持動態添加和卸載模塊
- <linux/fs.h> 包含了文件操作相關struct的定義,例如大名鼎鼎的struct file_operations、包含了struct inode 的定義,MINOR、MAJOR的頭文件。
- <linux/errno.h> 包含了對返回值的宏定義,這樣用戶程序可以用perror輸出錯誤信息。
- <linux/types.h> 對一些特殊類型的定義,例如dev_t, off_t, pid_t.其實這些類型大部分都是unsigned int型通過一連串的typedef變過來的,只是為了方便閱讀。
- <linux/cdev.h> 對字符設備結構cdev以及一系列的操作函數的定義。
- <linux/wait.h> 等待隊列相關頭文件//內核等待隊列,它包含了自旋鎖的頭文件
- <linux/slab.h> 包含了kcalloc、kzalloc內存分配函數的定義。
- <linux/uaccess.h> 包含了copy_to_user、copy_from_user等內核訪問用戶進程內存地址的函數定義。
- <linux/device.h> 包含了device、class 等結構的定義
- <linux/io.h> 包含了ioremap、iowrite等內核訪問IO內存等函數的定義。
- <linux/miscdevice.h> 包含了miscdevice結構的定義及相關的操作函數。
- <linux/interrupt.h> 使用中斷必須的頭文件
- <linux/semaphore.h> 使用信號量必須的頭文件
- <linux/spinlock.h> 自旋鎖
- <linux/kfifo.h> fifo環形隊列
- <linux/timer.h> 內核定時器
- <linux/fdreg.h> 軟驅頭文件,含有軟盤控制器參數的一些定義。
- <linux/hdreg.h> 硬盤參數頭文件,定義訪問硬盤寄存器端口、狀態碼和分區表等信息。
- <linux/kernel.h> 內核頭文件,含有一些內核常用函數的原形定義。
- <linux/sched.h> 調度程序頭文件,定義了任務結構task_struct、初始任務0的數據,以及一些有關描述符參數設置和獲取的嵌入式匯編函數宏語句。
- <linux/tty.h> tty頭文件,定義了有關tty_io,串行通信方面的參數、常數。
- <const.h> 常數符號頭文件,目前僅定義了i節點中i_mode字段的各標志位。
- <ctype.h> 字符類型頭文件,定義了一些有關字符類型判斷和轉換的宏。
- <errno.h> 錯誤號頭文件,包含系統中各種出錯號。(Linus從minix中引進的)。
- <fcntl.h> 文件控制頭文件,用於文件及其描述符的操作控制常數符號的定義。
- <signal.h> 信號頭文件,定義信號符號常量,信號結構以及信號操作函數原型。
- <string.h> 字符串頭文件,主要定義了一些有關字符串操作的嵌入函數。
- <termios.h> 終端輸入輸出函數頭文件,主要定義控制異步通信口的終端接口。
- <time.h> 時間類型頭文件,主要定義了tm結構和一些有關時間的函數原形。
- <unistd.h> Linux標准頭文件,定義了各種符號常數和類型,並聲明了各種函數。如,定義了__LIBRARY__,則還包括系統調用號和內嵌匯編_syscall0()等
- <utime.h> 用戶時間頭文件,定義了訪問和修改時間結構以及utime()原型。
- <sys/stat.h> 文件狀態頭文件,含有文件或文件系統狀態結構stat{}和常量。
- <sys/times.h> 定義了進程中運行時間結構tms以及times()函數原型。
- <sys/types.h> 類型頭文件,定義了基本的系統數據類型。
- <sys/utsname.h> 系統名稱結構頭文件。
- <sys/wait.h> 等待調用頭文件,定義系統調用wait()和waitpid()及相關常數符號。
八、代碼下載
Young / s3c2440_project[drivers]
參考文章
[1]linux驅動
[2]Linux驅動開發
[4]一、Linux驅動之基礎概念介紹(部分轉載)
[5]Linux驅動基礎開發(推薦)
[6]hello world!帶你編寫一個最簡單的linux下的字符設備驅動
[7]29.使用register_chrdev_region()系列來注冊字符設備
[9]The Linux Kernel documentation(官方文檔)
[10]Linux 內部
[11]內核研究參考站點
