
1. 字符設備驅動簡介
字符設備是Linux驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先后順序的。 比如常見的點燈、按鍵、IIC、SPI、LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
Linux驅動基本原理:Linux中一切皆為文件,驅動加載成功后會在/dev目錄下生成一個相應的文件,應用程序通過對這個名為/dev/xxx的文件進行相應的操作即可實現對硬件的操作。
(資料圖)
比如LED驅動,會有/dev/led驅動文件,應用程序使用open函數來打開該文件; 若要點亮或關閉led,就使用write函數寫入開關值; 若要獲取led燈的狀態,就用read函數從驅動文件中讀取相應的狀態; 使用完成后使用close函數關閉該驅動文件。
Linux軟件從上到下可分為4層結構,如下圖左示。 以控制LED為例,具體過程如下圖右示:
每個系統調用,在驅動中都有與之對應的驅動函數,內核include/linux/fs.h文件中有個file_operations結構體,就是Linux內核驅動操作函數集合:
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 *); ...... ......};
Linux驅動運行方式有以下兩種:
將驅動編譯進內核中, 當Linux內核啟動時就會自動運行驅動程序將驅動編譯成模塊, 在內核啟動后使用insmod命令加載驅動模塊在驅動開發階段一般都將其編譯為模塊,不需要編譯整個Linux代碼,方便調試驅動程序。 當驅動開發完成后,根據實際需要,可以選擇是否將驅動編譯進Linux內核中。
2. Linux設備號
2.1 設備號的組成
Linux中每個設備都有一個設備號,由主設備號和次設備號兩部分組成:
主設備號表示某一個具體的驅動次設備號表示使用這個驅動的各個設備Linux 提供了名為dev_t的數據類型表示設備號,其本質是32位的unsigned int數據類型,其中高12位為主設備號,低20位為次設備號,因此Linux中主設備號范圍為0~4095
在文件include/linux/kdev_t.h中提供了幾個關于設備號操作的宏定義:
#define MINORBITS 20#define MINORMASK ((1U << MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
MINORBITS:表示次設備號位數,一共20位MINORMASK:表示次設備號掩碼MAJOR:用于從dev_t中獲取主設備號,將dev_t右移20位即可MINOR:用于從dev_t中獲取次設備號,取dev_t的低20位的值即可MKDEV:用于將給定的主設備號和次設備號的值組合成dev_t類型的設備號2.2 主設備號的分配
主設備號的分配包括靜態分配和動態分配。 靜態分配需要手動指定設備號,并且要注意不能與已有的重復,一些常用的設備號已經被Linux內核開發者給分配掉了,可使用cat /proc/devices命令查看當前系統中所有已經使用了的設備號。
動態分配是在注冊字符設備之前先申請一個設備號,系統會自動分配一個沒有被使用的設備號, 這樣就避免了沖突。 在卸載驅動的時候釋放掉這個設備號即可。
設備號的申請函數
//設備號申請函數int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)// dev:保存申請到的設備號// baseminor:次設備號起始地址// count:要申請的設備號數量// name:設備名字
設備號的釋放函數
//設備號釋放函數void unregister_chrdev_region(dev_t from, unsigned count)// from:要釋放的設備號// count:表示從 from 開始,要釋放的設備號數量
3. 字符設備驅動開發模板
3.1 加載與卸載
在編寫驅動的時候需要注冊模塊加載和卸載這兩種函數:
module_init(xxx_init); //注冊模塊加載函數module_exit(xxx_exit); //注冊模塊卸載函數
module_init():向內核注冊一個模塊加載函數,參數xxx_init就是需要注冊的具體函數,當使用insmod命令加載驅動時,xxx_init函數就會被調用
module_exit():向內核注冊一個模塊卸載函數,參數xxx_exit就是需要注冊的具體函數,當使用rmmod命令卸載驅動時,xxx_exit函數就會被調用
字符設備驅動模塊加載和卸載模板如下所示:
/* 驅動入口函數 */staticint __init xxx_init(void){/*入口函數具體內容*/ return0;}/* 驅動出口函數 */staticvoid __exit xxx_exit(void){/*出口函數具體內容*/}/* 將上面兩個函數指定為驅動的入口和出口函數 */module_init(xxx_init);module_exit(xxx_exit);
驅動編譯完成以后擴展名為.ko,有兩種命令可以加載驅動模塊:
insmod:最簡單的模塊加載命令,但不能解決模塊的依賴關系modprobe:會分析模塊的依賴關系,將所有的依賴模塊都加載到內核中卸載驅動也有兩種命令:
rmmod:最簡單的模塊卸載命令modprobe -r:除了卸載指定的驅動,還卸載其所依賴的其他模塊,若依賴模塊還在被其它模塊使用,就不能使用該命令來卸載驅動模塊3.2 注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,卸載驅動模塊時也要注銷掉字符設備。 字符設備的注冊和注銷函數原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)//major:主設備號//name:設備名字,指向一串字符串//fops:結構體 file_operations 類型指針,指向設備的操作函數集合變量static inline void unregister_chrdev(unsigned int major, const char *name)//major:要注銷的設備對應的主設備號//name:要注銷的設備對應的設備名
一般字符設備的注冊在驅動模塊的入口函數xxx_init中進行,字符設備的注銷在驅動模塊的出口函數xxx_exit中進行
//定義了一個file_operations結構體變量,就是設備的操作函數集合static struct file_operations test_fops;/* 驅動入口函數 */static int __init xxx_init(void){ /* 入口函數具體內容 */ int retvalue = 0; /* 注冊字符設備驅動 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符設備注冊失敗,自行處理 */ } return 0;}/* 驅動出口函數 */static void __exit xxx_exit(void){ /* 注銷字符設備驅動 */ unregister_chrdev(200, "chrtest");}/* 將上面兩個函數指定為驅動的入口和出口函數 */module_init(xxx_init);module_exit(xxx_exit);
3.3 實現設備的具體操作函數
file_operations結構體就是設備的具體操作函數。 假設對chrtest這個設備有如下兩個要求:
能夠實現打開和關閉操作:需要實現open和release這兩個函數能夠實現進行讀寫操作:需要實現read和write這兩個函數實現file_operations中的這四個函數,完成后的內容框架如下所示:
/* 打開設備 */static int chrtest_open(struct inode *inode, struct file *filp){ /* 用戶實現具體功能 */ return 0;}/* 從設備讀取 */static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){ /* 用戶實現具體功能 */ return 0;}/* 向設備寫數據 */static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){ /* 用戶實現具體功能 */ return 0;}/* 關閉/釋放設備 */static int chrtest_release(struct inode *inode, struct file *filp){ /* 用戶實現具體功能 */ return 0;}
然后是驅動的入口(init)和出口(exit) 函數:
//定義了一個file_operations結構體變量test_fops,就是設備的操作函數集合static struct file_operations test_fops = { .owner = THIS_MODULE, .open = chrtest_open, .read = chrtest_read, .write = chrtest_write, .release = chrtest_release,}/* 驅動入口函數 */static int __init xxx_init(void){ /* 入口函數具體內容 */ int retvalue = 0; /* 注冊字符設備驅動 */ retvalue = register_chrdev(200, "chrtest", &test_fops); if(retvalue < 0){ /* 字符設備注冊失敗,自行處理 */ } return 0;}/* 驅動出口函數 */static void __exit xxx_exit(void){ /* 注銷字符設備驅動 */ unregister_chrdev(200, "chrtest");}/* 將上面兩個函數指定為驅動的入口和出口函數 */module_init(xxx_init);module_exit(xxx_exit);
3.4 添加LICENSE和作者信息
LICENSE是必須添加的,否則編譯時會報錯,作者信息可加可不加
MODULE_LICENSE() //添加模塊 LICENSE 信息MODULE_AUTHOR() //添加模塊作者信息
綜上所述,字符設備驅動開發流程如下圖所示:
4. 字符設備驅動開發實驗
下面以正點原子的IMX6ULL開發板為平臺,完整的編寫一個虛擬字符設備驅動模塊。 chrdevbase不是實際存在的一個設備,只是為了學習字符設備的開發的流程
4.1 驅動程序編寫
宏定義及變量定義
#include #include #include #include #include #include #define CHRDEVBASE_MAJOR 200 /* 主設備號 */#define CHRDEVBASE_NAME "chrdevbase" /* 設備名 */static char readbuf[100]; /* 讀緩沖區 */static char writebuf[100]; /* 寫緩沖區 */static char kerneldata[] = {"kernel data!"};
打開、關閉、讀取、寫入函數實現
staticintchrdevbase_open(structinode*inode,structfile*filp){ printk("chrdevbase open!\\r\\n"); return0;}staticssize_tchrdevbase_read(structfile*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"); return0;}staticssize_tchrdevbase_write(structfile*filp,constchar __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"); return0;}staticintchrdevbase_release(structinode*inode,structfile*filp){printk("chrdevbase release!\\r\\n");return0;}
驅動加載與注銷
staticstructfile_operations chrdevbase_fops ={ .owner = THIS_MODULE, .open = chrdevbase_open, .read = chrdevbase_read, .write = chrdevbase_write, .release = chrdevbase_release,};/*驅動入口函數 */staticint __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"); return0;}/* 驅動出口函數 */staticvoid __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("andyxi");
4.2 應用程序編寫
應用程序運行在用戶空間,其通過輸入相應的指令來對chrdevbase設備執行讀或者寫操作。 下面將程序進行分段介紹
頭文件和main函數入口,以及main函數的傳參處理
#include "stdio.h"#include "unistd.h"#include "sys/types.h"#include "sys/stat.h"#include "fcntl.h"#include "stdlib.h"#include "string.h"static char usrdata[] = {"usr data!"};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; }
對 chrdevbase 設備的具體操作
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;}
4.3 程序編譯
程序編譯包括驅動程序編譯和應用程序編譯兩個部分
驅動程序編譯:將驅動程序編譯為.ko模塊
創建Makefile文件
# KERNELDIR:開發板所使用的Linux內核源碼目錄KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi# CURRENT_PATH:當前路徑,通過運行“pwd”命令獲取CURRENT_PATH := $(shell pwd)# obj-m:將 chrdevbase.c 這個文件編譯為chrdevbase.ko模塊obj-m := chrdevbase.obuild: kernel_modules# -C 表示切換工作目錄到KERNERLDIR目錄# M 表示模塊源碼目錄# modules 表示編譯模塊kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
輸入make命令即可編譯,編譯成功以后就會生成一個叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 設備的驅動模塊
注意:若直接make編譯可能會出錯,是因為kernel中沒有指定編譯器和架構,使用了默認的x86平臺編譯報錯。 解決辦法就是在內核頂層Makefile中,直接定義ARCH和CROSS_COMPILE這兩個的變量值為 arm和 arm-linux-gnueabihf- 即可
應用程序編譯:無需內核參與,直接編譯即可
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
使用file命令,查看生成的chrdevbaseApp文件信息,如下圖示,文件是32位LSB格式,ARM版本的,因此只能在ARM芯片下運行
4.4 運行測試
為了方便測試,Linux系統選擇通過TFTP從網絡啟動,并且使用NFS掛載網絡根文件系統。 確保開發板系統移植成功,能正常啟動。 具體的實現方法可參考之前介紹過的系統移植專輯系列文章
加載驅動模塊
在根文件系統創建/lib/modules/4.1.15文件夾,用來存放驅動模塊
/lib/modules是通用的4.1.15根據所使用的內核版本來設置,否則modprobe命令無法加載驅動模塊在Ubuntu中將chrdevbase.ko和chrdevbaseAPP,復制到根文件系統的 rootfs/lib/modules/4.1.15 目錄中
在開發板中使用insmod或modprobe命令來加載驅動文件
輸入lsmod命令即可查看當前系統中存在的模塊,輸入cat /proc/devices命令,查看當前系統中有沒有chrdevbase 這個設備
創建設備節點文件:驅動加載成功后,需要在/dev目錄下創建一個與之對應的設備節點文件,應用程序通過操作這個設備節點文件來完成對具體設備的操作
使用mknod命令創建/dev/chrdevbase設備節點文件
mknod /dev/chrdevbase c 200 0#/dev/chrdevbase 是要創建的節點文件# c 表示這是個字符設備# 200 是設備的主設備號# 0 是設備的次設備號
創建完后可使用ls /dev/chrdevbase -l命令查看是否存在
操作設備測試:使用應用程序讀寫設備,對/dev/chrdevbase文件進行讀寫操作
# 讀操作命令./chrdevbaseApp /dev/chrdevbase 1# 輸出“ kernel senddata ok!”是驅動程序中chrdevbase_read函數輸出的信息# “read data:kernel data!”就是chrdevbaseAPP打印出來的接收到的數據# 寫操作命令./chrdevbaseApp /dev/chrdevbase 2# “kernel recevdata:usr data!”,是驅動程序中的chrdevbase_write函數輸出的
卸載驅動模塊:若不再使用某個設備的話可以將其驅動卸載掉。 輸入rmmod命令卸載驅動后,使用lsmod命令查看chrdevbase這個模塊還存不存在
至此,Linux字符設備驅動開發完成。 本文介紹了驅動開發中的字符驅動開發的基本模式,并使用一個虛擬的字符設備驅動進行測試,了解驅動程序與應用程序之間的調用關系。
標簽: