一、字符設備驅動框架
字符設備驅動的編寫主要就是驅動對應的open、close、read。。。其實就是
file_operations結構體的成員變量的實現。
其中關于 C 庫以及如何通過系統調用“陷入” 到內核空間這個我們不用去管,我們重點關注的是應用程序和具體的驅動,應用程序使用到的函數在具體驅動程序中都有與之對應的函數,比如應用程序中調用了 open 這個函數,那么在驅動程序中也得有一個名為 open 的函數。每一個系統調用,在驅動中都有與之對應的一個驅動函數,在 Linux 內核文件 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函數集合,內容如下所示
二、驅動模塊的加載與卸載
Linux 驅動有兩種運行方式,第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”命令加載驅動模塊。在調試驅動的時候一般都選擇將其編譯為模塊,這樣我們修改驅動以后只需要編譯一下驅動代碼即可,不需要編譯整個 Linux 代碼。而且在調試的時候只需要加載或者卸載驅動模塊即可,不需要重啟整個系統。總之,將驅動編譯為模塊最大的好處就是方便開發,當驅動開發完成,確定沒有問題以后就可以將驅動編譯進Linux 內核中,當然也可以不編譯進 Linux 內核中,具體看自己的需求。
模塊有加載和卸載兩種操作,我們在編寫驅動的時候需要注冊這兩種操作函數,模塊的加載和卸載注冊函數如下:
module_init 函數用來向 Linux 內核注冊一個模塊加載函數,參數 xxx_init 就是需要注冊的具體函數,當使用“insmod”命令加載驅動的時候, xxx_init 這個函數就會被調用。 module_exit()函數用來向 Linux 內核注冊一個模塊卸載函數,參數 xxx_exit 就是需要注冊的具體函數,當使用“rmmod”命令卸載具體驅動的時候 xxx_exit 函數就會被調用。字符設備驅動模塊加載和卸載模板如下所示:
Linux驅動程序可以編譯到kernel里面,也就是zImage,也可以編譯為模塊,.ko。測試的時候只需要加載.ko模塊就可以。
編寫驅動的時候注意事項!
1、編譯驅動的時候需要用到linux內核源碼!因此要解壓縮linux內核源碼,編譯linux內核源碼。得到zImage和.dtb。需要使用編譯后的到的zImage和dtb啟動系統。
2、從SD卡啟動,SD卡燒寫了uboot。uboot通過tftp從ubuntu里面獲取zimage和dtb,rootfs也是通過nfs掛在。
3、設置bootcmd和bootargs
bootargs=console=ttymxc0,115200 rw root=/dev/nfs nfsroot=192.168.1.66:/home/zzk/linux/nfs/rootfs ip=192.168.1.50:192.168.1.66:192.168.1.1:255.255.255.0::eth0:off
bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000;
4、將編譯出來的.ko文件放到根文件系統里面。加載驅動會用到加載命令:insmod,modprobe。移除驅動使用命令rmmod。對于一個新的模塊使用modprobe加載的時候需要先調用一下depmod命令。
驅動編譯完成以后擴展名為.ko,有兩種命令可以加載驅動模塊: insmod和 modprobe, insmod是最簡單的模塊加載命令,此命令用于加載指定的.ko 模塊,比如加載 drv.ko 這個驅動模塊,命令如下:
insmod drv.ko
insmod 命令不能解決模塊的依賴關系,比如 drv.ko 依賴 first.ko 這個模塊,就必須先使用insmod 命令加載 first.ko 這個模塊,然后再加載 drv.ko 這個模塊。 但是 modprobe 就不會存在這個問題, modprobe 會分析模塊的依賴關系,然后會將所有的依賴模塊都加載到內核中,因此modprobe 命令相比 insmod 要智能一些。 modprobe 命令主要智能在提供了模塊的依賴性分析、錯誤檢查、錯誤報告等功能,推薦使用 modprobe 命令來加載驅動。 modprobe 命令默認會去/lib/modules/目錄中查找模塊,比如本書使用的 Linux kernel 的版本號為 4.1.15,因此 modprobe 命令默認會到/lib/modules/4.1.15 這個目錄中查找相應的驅動模塊,一般自己制作的根文件系統中是不會有這個目錄的,所以需要自己手動創建。
/lib/modules/4.1.15
/lib/modules/4.1.15 # modprobe chrdevbase
chrdevbase init!
驅動模塊的卸載使用命令“rmmod”即可,比如要卸載 drv.ko,使用如下命令即可: rmmod drv.ko
也可以使用“modprobe -r”命令卸載驅動,比如要卸載 drv.ko,命令如下: modprobe -r drv.ko
使用 modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊。所以對于模塊的卸載,還是推薦使用 rmmod 命令。
5,驅動模塊加載成功以后可以使用lsmod查看一下。
6,卸載模塊使用rmmod命令
/lib/modules/4.1.15 # rmmod chrdevbase.ko
chrdevbase exit!
/lib/modules/4.1.15 # lsmod
chrdevbase 1884 0 - Live 0x7f00c000 (O)
當應用程序調用 open 函數的時候此函數就會調用,本例程中我們沒有做任何工作,只是輸出一串字符,用于調試。這里使用了 printk 來輸出信息,而不是 printf!因為在 Linux 內核中沒有 printf 這個函數。 printk 相當于 printf 的孿生兄妹, printf運行在用戶態, printk 運行在內核態。在內核中想要向控制臺輸出或顯示一些內容,必須使用printk 這個函數。不同之處在于, printk 可以根據日志級別對消息進行分類,一共有 8 個消息級別,這 8 個消息級別定義在文件 include/linux/kern_levels.h 里面,定義如下:
三、字符設備的注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備。字符設備的注冊和注銷函數原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函數用于注冊字符設備,此函數一共有三個參數,這三個參數的含義如下: major: 主設備號, Linux 下每個設備都有一個設備號,設備號分為主設備號和次設備號兩部分,關于設備號后面會詳細講解。name:設備名字,指向一串字符串。
fops: 結構體 file_operations 類型指針,指向設備的操作函數集合變量。unregister_chrdev 函數用戶注銷字符設備,此函數有兩個參數,這兩個參數含義如下: major: 要注銷的設備對應的主設備號。
name: 要注銷的設備對應的設備名。
一般字符設備的注冊在驅動模塊的入口函數 xxx_init 中進行,字符設備的注銷在驅動模塊的出口函數 xxx_exit 中進行。在示例代碼 40.2.1.1 中字符設備的注冊和注銷,內容如下所示
1、我們需要向系統注冊一個字符設備,使用函數register_chrdev。
2、卸載驅動的時候需要注銷掉前面注冊的字符設備,使用函數unregister_chrdev,注銷字符設備。
四、設備號
1,Linux內核使用dev_t。
typedef __kernel_dev_t dev_t;
typedef __u32 __kernel_dev_t;
typedef unsigned int __u32;
2、Linux內核將設備號分為兩部分:主設備號和次設備號。主設備號占用前12位,次設備號占用低20位。
因此 Linux系統中主設備號范圍為 0~4095,所以大家在選擇主設備號的時候一定不要超過這個范圍。
在文件 include/linux/kdev_t.h 中提供了幾個關于設備號的操作函數(本質是宏),如下所示
3、設備號的操作函數,或宏
從dev_t獲取主設備號和次設備號,MAJOR(dev_t),MINOR(dev_t)。也可以使用主設備號和次設備號構成dev_t,通過MKDEV(major,minor)
示例代碼 40.3.3 設備號操作函數
第 6 行,宏 MINORBITS 表示次設備號位數,一共是 20 位。第 7 行,宏 MINORMASK 表示次設備號掩碼。
第 9 行,宏 MAJOR 用于從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于將給定的主設備號和次設備號的值組合成 dev_t 類型的設備號。
/lib/modules/4.1.15 # cat /proc/devices
Character devices:1 mem4 /dev/vc/04 tty5 /dev/tty5 /dev/console5 /dev/ptmx7 vcs10 misc13 input29 fb81 video4linux89 i2c90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 chrdevbase
207 ttymxc
226 drm
250 ttyLP
251 watchdog
252 ptp
253 pps
254 rtcBlock devices:1 ramdisk
259 blkext7 loop8 sd31 mtdblock65 sd66 sd67 sd68 sd69 sd70 sd71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/lib/modules/4.1.15 #
五、file_operations的具體實現
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
六、字符設備驅動框架的搭建
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
文件名 : chrdevbase.c
描述 : chrdevbase驅動文件。
***************************************************************/#define CHRDEVBASE_MAJOR 200 /* 主設備號 */
#define CHRDEVBASE_NAME "chrdevbase" /* 設備名 */static char readbuf[100]; /* 讀緩沖區 */
static char writebuf[100]; /* 寫緩沖區 */
static char kerneldata[] = {"kernel data!"};/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量* 一般在open的時候將private_data指向設備結構體。* @return : 0 成功;其他 失敗*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{//printk("chrdevbase open!\r\n");return 0;
}/** @description : 從設備讀取數據 * @param - filp : 要打開的設備文件(文件描述符)* @param - buf : 返回給用戶空間的數據緩沖區* @param - cnt : 要讀取的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 讀取的字節數,如果為負值,表示讀取失敗*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 向用戶空間發送數據 */memcpy(readbuf, kerneldata, sizeof(kerneldata));retvalue = copy_to_user(buf, readbuf, cnt);if(retvalue == 0){printk("kernel senddata ok!\r\n");}else{printk("kernel senddata failed!\r\n");}//printk("chrdevbase read!\r\n");return 0;
}/** @description : 向設備寫數據 * @param - filp : 設備文件,表示打開的文件描述符* @param - buf : 要寫給設備寫入的數據* @param - cnt : 要寫入的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 寫入的字節數,如果為負值,表示寫入失敗*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 接收用戶空間傳遞給內核的數據并且打印出來 */retvalue = copy_from_user(writebuf, buf, cnt);if(retvalue == 0){printk("kernel recevdata:%s\r\n", writebuf);}else{printk("kernel recevdata failed!\r\n");}//printk("chrdevbase write!\r\n");return 0;
}/** @description : 關閉/釋放設備* @param - filp : 要關閉的設備文件(文件描述符)* @return : 0 成功;其他 失敗*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{//printk("chrdevbase release!\r\n");return 0;
}/** 設備操作函數結構體*/
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE, .open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};/** @description : 驅動入口函數 * @param : 無* @return : 0 成功;其他 失敗*/
static int __init chrdevbase_init(void)
{int retvalue = 0;/* 注冊字符設備驅動 */retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);if(retvalue < 0){printk("chrdevbase driver register failed\r\n");}printk("chrdevbase init!\r\n");return 0;
}/** @description : 驅動出口函數* @param : 無* @return : 無*/
static void __exit chrdevbase_exit(void)
{/* 注銷字符設備驅動 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\r\n");
}/* * 將上面兩個函數指定為驅動的入口和出口函數 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);/* * LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("mmk");
七、應用程序編寫
Linux下一切皆文件,首先要open
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
文件名 : chrdevbaseApp.c
版本 : V1.0
描述 : chrdevbase驅測試APP。
其他 : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>argv[2] 1:讀文件argv[2] 2:寫文件
***************************************************************/static char usrdata[] = {"usr data!"};/** @description : main主程序* @param - argc : argv數組元素個數* @param - argv : 具體參數* @return : 0 成功;其他 失敗*/
int main(int argc, char *argv[])
{int fd, retvalue;char *filename;char readbuf[100], writebuf[100];if(argc != 3){printf("Error Usage!\r\n");return -1;}filename = argv[1];/* 打開驅動文件 */fd = open(filename, O_RDWR);if(fd < 0){printf("Can't open file %s\r\n", filename);return -1;}if(atoi(argv[2]) == 1){ /* 從驅動文件讀取數據 */retvalue = read(fd, readbuf, 50);if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{/* 讀取成功,打印出讀取成功的數據 */printf("read data:%s\r\n",readbuf);}}if(atoi(argv[2]) == 2){/* 向設備驅動寫數據 */memcpy(writebuf, usrdata, sizeof(usrdata));retvalue = write(fd, writebuf, 50);if(retvalue < 0){printf("write file %s failed!\r\n", filename);}}/* 關閉設備 */retvalue = close(fd);if(retvalue < 0){printf("Can't close file %s\r\n", filename);return -1;}return 0;
}
測試 APP 比較簡單,只有一個文件,因此就不需要編寫 Makefile 了,直接輸入命令編譯。因為測試 APP 是要在 ARM 開發板上運行的,所以需要使用 arm-linux-gnueabihf-gcc 來編譯,輸入如下命令:
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
編譯完成以后會生成一個叫做 chrdevbaseApp 的可執行程序,輸入如下命令查看chrdevbaseAPP 這個程序的文件信息
file chrdevbaseApp
chrdevbaseApp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.31, BuildID[sha1]=5d017375992cf6c40e8fccb19a238dfd552c
chrdevbaseAPP 這個可執行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下運行。
拷 貝 完 成 以 后 就 會 在 開 發 板 的 /lib/modules/4.1.15 目 錄 下 存 在 chrdevbase.ko 和chrdevbaseAPP 這兩個文件
八、測 試
1、加載驅動。
modprobe chrdevbase.ko
或者
modprobe chrdevbase
2,設備驅動在dev里,進入/dev查看設備文件,chrdevbase。/dev/chrdevbase。但是實際沒有,因為我們沒有創建設備節點。
mknod /dev/chrdevbase c 200 0
/dev # ls
autofs ram14 tty37
bus ram15 tty38
console ram2 tty39
cpu_dma_latency ram3 tty4
dri ram4 tty40
fb0 ram5 tty41
full ram6 tty42
fuse ram7 tty43
hwrng ram8 tty44
i2c-0 ram9 tty45
i2c-1 random tty46
input rfkill tty47
kmsg rtc0 tty48
loop-control snd tty49
loop0 tty tty5
loop1 tty0 tty50
loop2 tty1 tty51
loop3 tty10 tty52
loop4 tty11 tty53
loop5 tty12 tty54
loop6 tty13 tty55
loop7 tty14 tty56
mem tty15 tty57
memory_bandwidth tty16 tty58
mmcblk0 tty17 tty59
mmcblk1 tty18 tty6
mmcblk1boot0 tty19 tty60
mmcblk1boot1 tty2 tty61
mmcblk1p1 tty20 tty62
mmcblk1p2 tty21 tty63
mmcblk1rpmb tty22 tty7
network_latency tty23 tty8
network_throughput tty24 tty9
null tty25 ttymxc0
pps0 tty26 ttymxc1
pps1 tty27 ubi_ctrl
ptmx tty28 urandom
ptp0 tty29 vcs
ptp1 tty3 vcs1
pts tty30 vcsa
ram0 tty31 vcsa1
ram1 tty32 video0
ram10 tty33 watchdog
ram11 tty34 watchdog0
ram12 tty35 zero
ram13 tty36
創建設備節點文件
驅動加載成功需要在/dev 目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作。輸入如下命令創建/dev/chrdevbase 這個設備節點文件:
mknod /dev/chrdevbase c 200 0
其中“mknod”是創建節點命令,“/dev/chrdevbase”是要創建的節點文件,“c”表示這是個字符設備,“ 200”是設備的主設備號,“ 0”是設備的次設備號。創建完成以后就會存在/dev/chrdevbase 這個文件,可以使用“ls /dev/chrdevbase -l”命令查看,
/dev # ls /dev/chrdevbase -l
crw-r–r-- 1 0 0 200, 0 Jan 1 01:34 /dev/chrdevbase
如果 chrdevbaseAPP 想要讀寫 chrdevbase 設備,直接對/dev/chrdevbase 進行讀寫操作即可。相當于/dev/chrdevbase 這個文件是 chrdevbase 設備在用戶空間中的實現。前面一直說 Linux 下一切皆文件,包括設備也是文件,現在大家應該是有這個概念了吧?
3、測試
進入/lib/modules/4.1.15 #
./chrdevbaseApp /dev/chrdevbase
使用 chrdevbaseApp 軟件操作 chrdevbase 這個設備,看看讀寫是否正常,首先進行讀操作,輸入如下命令
./chrdevbaseApp /dev/chrdevbase
/lib/modules/4.1.15 # cat /proc/devices
Character devices:1 mem4 /dev/vc/04 tty5 /dev/tty5 /dev/console5 /dev/ptmx7 vcs10 misc13 input29 fb81 video4linux89 i2c90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 chrdevbase
207 ttymxc
226 drm
250 ttyLP
251 watchdog
252 ptp
253 pps
254 rtcBlock devices:1 ramdisk
259 blkext7 loop8 sd31 mtdblock65 sd66 sd67 sd68 sd69 sd70 sd71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/lib/modules/4.1.15 #
沒有200
rmmod chrdevbase
cat /proc/devices
modprobe chrdevbase
有200 了
/lib/modules/4.1.15 # mknod /dev/chrdevbase c 200 0
/lib/modules/4.1.15 # mknod /dev/chrdevbase c 200 0
mknod: /dev/chrdevbase: File exists
./chrdevbaseApp /dev/chrdevbase
/lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase
Error Usage!
/
lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase 1
kernel senddata ok!
read data:kernel data!
/lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase 2
kernel recevdata:usr data!
/lib/modules/4.1.15 # lsmod
chrdevbase 1884 0 - Live 0x7f004000 (O)
八、chrdevbase虛擬設備驅動的完善
要求:應用程序可以對驅動讀寫操作,讀的話就是從驅動里面讀取字符串,寫的話就是應用向驅動寫字符串。
1、chrdevbase_read驅動函數編寫
驅動給應用傳遞數據的時候需要用到copy_to_user函數。
if(atoi(argv[2]) == 1){ /* 從驅動文件讀取數據 */retvalue = read(fd, readbuf, 50);if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{/* 讀取成功,打印出讀取成功的數據 */printf("App read data:%s\r\n",readbuf);}
}
//編譯驅動
make
//編譯App
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
/lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase 1
kernel senddata ok!
App read data:kernel data!