公眾號:嵌入式攻城獅(ID:andyxi_linux)
作者:安迪西
Linux字符設(shè)備驅(qū)動(dòng)開發(fā)模板中介紹了舊版本的驅(qū)動(dòng)開發(fā)模板,其需要手動(dòng)分配設(shè)備號后,再進(jìn)行注冊,驅(qū)動(dòng)加載成功后還需要手動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn),比較麻煩。目前Linux內(nèi)核推薦的新字符設(shè)備驅(qū)動(dòng)API函數(shù),可以自動(dòng)分配設(shè)備號、創(chuàng)建設(shè)備節(jié)點(diǎn),使得驅(qū)動(dòng)的使用更加方便
1. 新字符設(shè)備驅(qū)動(dòng)原理
1.1 分配和釋放設(shè)備號
舊字符設(shè)備驅(qū)動(dòng)開發(fā)中使用register_chrdev函數(shù)注冊字符設(shè)備時(shí),需要事先確定好主設(shè)備號,并且注冊成功后,會將該設(shè)備號下的所有次設(shè)備號都使用掉而新字符設(shè)備驅(qū)動(dòng)API函數(shù)很好的解決了這個(gè)問題,使用設(shè)備號時(shí)再向內(nèi)核申請,需要幾個(gè)就申請幾個(gè),由內(nèi)核分配設(shè)備可以使用的設(shè)備號
? 設(shè)備號申請函數(shù):沒有指定設(shè)備號
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev:保存申請到的設(shè)備號
//baseminor:次設(shè)備號起始地址,一般為0
//count:要申請的設(shè)備號數(shù)量
//name:設(shè)備名字
? 設(shè)備號申請函數(shù):指定了主次設(shè)備號
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//from:要申請的起始設(shè)備號
//count:要申請的設(shè)備號數(shù)量
//name:設(shè)備名字
? 設(shè)備號釋放函數(shù):統(tǒng)一使用下面函數(shù)釋放
void unregister_chrdev_region(dev_t from, unsigned count)
//from:要釋放的設(shè)備號
//count:表示從from開始,要釋放的設(shè)備號數(shù)量
因此新字符設(shè)備驅(qū)動(dòng)中,設(shè)備號分配代碼通常按如下示例編寫:
int major; /* 主設(shè)備號 */
int minor; /* 次設(shè)備號 */
dev_t devid; /* 設(shè)備號 */
if (major) { /* 定義了主設(shè)備號 */
devid = MKDEV(major, 0); /*大部分驅(qū)動(dòng)次設(shè)備號都選擇0*/
register_chrdev_region(devid, 1, "test");
} else { /* 沒有定義設(shè)備號 */
alloc_chrdev_region(&devid, 0, 1, "test"); /*申請?jiān)O(shè)備號*/
major = MAJOR(devid); /* 獲取分配號的主設(shè)備號 */
minor = MINOR(devid); /* 獲取分配號的次設(shè)備號 */
}
1.2 注冊字符設(shè)備
Linux中使用cdev表示字符設(shè)備,在include/linux/cdev.h中定義:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //文件操作函數(shù)集合
struct list_head list;
dev_t dev; //設(shè)備號
unsigned int count;
};
//編寫字符設(shè)備驅(qū)動(dòng)之前需要定義一個(gè)cdev結(jié)構(gòu)體變量
? cdev_init函數(shù):定義好cdev變量后,用該函數(shù)進(jìn)行初始化
? cdev_add函數(shù):向系統(tǒng)添加字符設(shè)備(cdev結(jié)構(gòu)體變量)
struct cdev testcdev;
/* 設(shè)備操作函數(shù) */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具體的初始項(xiàng) */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化cdev結(jié)構(gòu)體變量 */
cdev_add(&testcdev, devid, 1); /* 添加字符設(shè)備 */
? cdev_del函數(shù):卸載驅(qū)動(dòng)時(shí)從內(nèi)核中刪除相應(yīng)的字符設(shè)備
void cdev_del(struct cdev *p)
//p:要?jiǎng)h除的字符設(shè)備
1.3 自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)
舊字符設(shè)備驅(qū)動(dòng)開發(fā)中,驅(qū)動(dòng)程序加載成功后還需要使用mknod命令手動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn),十分麻煩。而新字符設(shè)備驅(qū)動(dòng)開發(fā)中,Linux通過udev用戶程序來實(shí)現(xiàn)設(shè)備文件的自動(dòng)創(chuàng)建與刪除。udev會檢測系統(tǒng)中硬件設(shè)備狀態(tài),并根據(jù)硬件設(shè)備狀態(tài)來創(chuàng)建或者刪除設(shè)備文件。
使用busybox構(gòu)建根文件系統(tǒng)時(shí),busybox會創(chuàng)建一個(gè)udev的簡化版本mdev。因此,在嵌入式開發(fā)中使用mdev來實(shí)現(xiàn)設(shè)備節(jié)點(diǎn)文件的自動(dòng)創(chuàng)建與刪除。Linux系統(tǒng)中的熱插拔事件也由mdev管理,在/etc/init.d/rcS文件中有如下語句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
? 創(chuàng)建類:自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn)的工作是在驅(qū)動(dòng)程序入口函數(shù)中完成的,一般在cdev_add之后添加相關(guān)代碼
struct class *class_create (struct module *owner, const char *name)
//owner 一般為 THIS_MODULE
//name 是類名字
//返回值是個(gè)指向結(jié)構(gòu)體class的指針,也就是創(chuàng)建的類
刪除類:卸載驅(qū)動(dòng)程序時(shí)需要?jiǎng)h除類
void class_destroy(struct class *cls)
//cls 就是要?jiǎng)h除的類
? 創(chuàng)建設(shè)備:類創(chuàng)建好后還不能實(shí)現(xiàn)自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn),還需要在該類下創(chuàng)建一個(gè)設(shè)備
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
//class 設(shè)備創(chuàng)建在哪個(gè)類下
//parent 父設(shè)備,一般為NULL
//devt 設(shè)備號
//drvdata 設(shè)備可能會使用的數(shù)據(jù),一般NULL
//fmt 設(shè)備名字
//返回值是創(chuàng)建好的設(shè)備
刪除設(shè)備:卸載驅(qū)動(dòng)時(shí)需要?jiǎng)h除創(chuàng)建的設(shè)備
void device_destroy(struct class *class, dev_t devt)
//class 是要?jiǎng)h除的設(shè)備所處的類
//devt 是要?jiǎng)h除的設(shè)備號
1.4 設(shè)置文件私有數(shù)據(jù)
每個(gè)硬件設(shè)備都有一些屬性,比如主設(shè)備號、類、設(shè)備、開關(guān)狀態(tài)等等,在編寫驅(qū)動(dòng)時(shí)可將這些屬性封裝成一個(gè)結(jié)構(gòu)體。并在編寫驅(qū)動(dòng)open函數(shù)時(shí)將設(shè)備結(jié)構(gòu)體作為私有數(shù)據(jù)添加到設(shè)備文件中:
/*設(shè)備結(jié)構(gòu)體*/
struct test_dev{
dev_t devid; /*設(shè)備號*/
struct cdev cdev; /*cdev*/
struct class *class; /*類*/
struct device *device; /*設(shè)備*/
int major; /*主設(shè)備號*/
int minor; /*次設(shè)備號*/
};
struct test_dev testdev;
/*open函數(shù)*/
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /*設(shè)置私有數(shù)據(jù)*/
return 0;
}
綜上所述,新字符設(shè)備驅(qū)動(dòng)開發(fā)流程如下圖所示:
2. 新字符設(shè)備驅(qū)動(dòng)開發(fā)實(shí)驗(yàn)
新字符設(shè)備驅(qū)動(dòng)開發(fā)實(shí)驗(yàn)是在Linux字符設(shè)備驅(qū)動(dòng)開發(fā)模板一文的基礎(chǔ)上進(jìn)行修改,只更改了驅(qū)動(dòng)的編寫方式,與應(yīng)用程序無關(guān),因此只修改驅(qū)動(dòng)程序即可
2.1 驅(qū)動(dòng)程序編寫
? 添加定義:宏及字符設(shè)備定義
#define CHRDEVBASE_CNT 1 //設(shè)備號個(gè)數(shù)
#define CHRDEVBASE_NAME "chrdevbase" //名字
/*newchr設(shè)備結(jié)構(gòu)體 */
struct newchr_dev{
dev_t devid; //設(shè)備號
struct cdev cdev; //cdev
struct class *class; //類
struct device *device; //設(shè)備
int major; //主設(shè)備號
int minor; //次設(shè)備號
};
struct newchr_dev chrdevbase; //自定義字符設(shè)備
? 修改open函數(shù):設(shè)置私有數(shù)據(jù)
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!rn");
filp->private_data = &chrdevbase; //設(shè)置私有數(shù)據(jù)
return 0;
}
? 修改init函數(shù)
static int __init chrdevbase_init(void)
{
/* 注冊字符設(shè)備驅(qū)動(dòng) */
//1、創(chuàng)建設(shè)備號
if (chrdevbase.major) /* 定義了設(shè)備號 */
{
chrdevbase.devid = MKDEV(chrdevbase.major, 0);
register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME);
}
else /* 沒有定義設(shè)備號 */
{
alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME); /* 申請?jiān)O(shè)備號 */
chrdevbase.major = MAJOR(chrdevbase.devid); /* 獲取分配號的主設(shè)備號 */
chrdevbase.minor = MINOR(chrdevbase.devid); /* 獲取分配號的次設(shè)備號 */
}
printk("chrdevbase major=%d,minor=%drn",chrdevbase.major, chrdevbase.minor);
//2、初始化cdev
chrdevbase.cdev.owner = THIS_MODULE;
cdev_init(&chrdevbase.cdev, &chrdevbase_fops);
//3、添加一個(gè)cdev
cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT);
//4、創(chuàng)建類
chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.class))
{
return PTR_ERR(chrdevbase.class);
}
//5、創(chuàng)建設(shè)備
chrdevbase.device = device_create(chrdevbase.class, NULL, chrdevbase.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.device))
{
return PTR_ERR(chrdevbase.device);
}
printk("chrdevbase init done!rn");
return 0;
}
? 修改exit函數(shù)
static void __exit chrdevbase_exit(void)
{
/* 注銷字符設(shè)備驅(qū)動(dòng) */
cdev_del(&chrdevbase.cdev);/* 刪除cdev */
unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /* 注銷設(shè)備號 */
device_destroy(chrdevbase.class, chrdevbase.devid);
class_destroy(chrdevbase.class);
printk("chrdevbase exit done!rn");
}
2.2 程序編譯
程序編譯包括驅(qū)動(dòng)程序和應(yīng)用程序編譯兩個(gè)部分:
? 驅(qū)動(dòng)程序編譯:創(chuàng)建Makefile文件,使用make命令,編譯驅(qū)動(dòng)程序
KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
CURRENT_PATH := $(shell pwd)
obj-m := newchrdev.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
? 應(yīng)用程序編譯:無需內(nèi)核參與,直接編譯即可
arm-linux-gnueabihf-gcc newchrdevApp.c -o newchrdevApp
2.3 運(yùn)行測試
為了方便,選擇通過TFTP從網(wǎng)絡(luò)啟動(dòng),并使用NFS掛載網(wǎng)絡(luò)根文件系統(tǒng)。確保開發(fā)板能正常啟動(dòng),在Ubuntu中將驅(qū)動(dòng)和測試文件復(fù)制到modules/4.1.15目錄中
? 在開發(fā)板中輸入如下指令加載驅(qū)動(dòng)模塊
depmod //第一次加載驅(qū)動(dòng)的時(shí)候需要運(yùn)行此命令
modprobe newchrdev.ko //加載驅(qū)動(dòng)
? 驅(qū)動(dòng)加載成功后,可以看到自動(dòng)申請到的主設(shè)備號和次設(shè)備號
? 使用ls /dev/chrdevbase -l命令驗(yàn)證該設(shè)備節(jié)點(diǎn)文件是否存在,而舊驅(qū)動(dòng)方式需要額外使用mknod指令來手動(dòng)創(chuàng)建該設(shè)備節(jié)點(diǎn)文件
? 驅(qū)動(dòng)加載成功后,測試APP程序,如下
? 測試完使用rmmod指令卸載驅(qū)動(dòng)
以上可見Linux新字符設(shè)備驅(qū)動(dòng)開發(fā)方式可以自動(dòng)分配設(shè)備號、創(chuàng)建設(shè)備節(jié)點(diǎn),使得驅(qū)動(dòng)的使用更加方便、便捷。