[原創] 【i.MX6ULL】驅動開發2——新字符設備開發模板

DDZZ669 樓主
2021-8-30 23:00

上篇文章介紹了字符設備的開發模板,但那是一種舊版本的驅動開發模式,設備驅動需要手動分配設備號再使用 register_chrdev進行注冊,加載成功以后還需要手動使用mknod命令創建設備節點,比較麻煩

目前Linux內核推薦的新字符設備驅動API函數,使得驅動的使用更加自動化,本篇就來一起研究下。

 

1 舊字符設備驅動的弊端

使用register_chrdev函數注冊字符設備,需要指定一個設備號,這就造成:

  • 需要事先確定好哪些主設備號沒有使用

  • 會將一個主設備號下的所有次設備號都使用掉,比如主設備號為200,那么 0~1048575(2^20-1)這個區間的次設備號就全部都被占用了

回顧上一篇的操作,先是加載驅動:

10.png

加載完,還有手動使用mknod指令來手動創建該設備節點,并且指定驅動程序中寫死的設備號:

11.png

本篇,就要使用一種新的字符驅動編寫方式,實現設備號的自動分配,省去mknod指令操作

2 新字符設備驅動原理

2.1 分配和釋放設備號

使用設備號的時候向Linux內核申請,需要幾個就申請幾個,由Linux內核分配設備可以使用的設備號。

使用如下函數來申請設備號(該函數在上篇提到過):

/*
* dev:保存申請到的設備號
* baseminor:次設備號起始地址,一般baseminor為0 (次設備號以baseminor為起始地址地址開始遞)
* count:要申請的設備號數量
* name:設備名字
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) 

如果給定了設備的主設備號和次設備號就使用如下所示函數來注冊設備號即可:

/*
* from:要申請的起始設備號
* count:要申請的設備號數量
* name:設備名字
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name) 

注銷字符設備之后要釋放設備號,不管是通過alloc_chrdev_region函數的動態分配還是register_chrdev_region函數手動指定的設備號,統一使用(和上篇使用的一樣)的釋放函數:

/*
* from:要釋放的設備號
* count:表示從from開始,要釋放的設備號數量
*/
void unregister_chrdev_region(dev_t from, unsigned count) 

新字符設備驅動下,設備號分配示例代碼如下:

int major;      /*主設備號*/ 
int minor;      /*次設備號*/ 
dev_t devid;    /*設備號*/ 
​
/*定義了主設備號*/
if (major)       
{
   devid = MKDEV(major, 0);    /*大部分驅動次設備號都選擇0*/ 
   register_chrdev_region(devid, 1, "test"); 
} 
/*沒有定義設備號*/ 
else 
{                         
   alloc_chrdev_region(&devid, 0, 1, "test");  /*申請設備號*/ 
   major = MAJOR(devid);       /*獲取分配號的主設備號*/ 
   minor = MINOR(devid);       /*獲取分配號的次設備號*/ 
}

2.2 字符設備注冊

2.2.1 cdev字符設備結構

在Linux中使用cdev結構體表示一個字符設備,其定義在include/linux/cdev.h文件中:

struct cdev { 
    struct kobject               kobj; 
    struct module                *owner; 
    const struct file_operations *ops;  /*文件操作函數集合*/
    struct list_head             list; 
    dev_t                        dev;   /*設備號*/              
    unsigned int                 count; 
};

2.2.2 cdev_init 函數

定義好cdev變量以后就要使用cdev_init函數對其進行初始化:

/*
* cdev:要初始化的cdev結構體變量
* fops:字符設備文件操作函數集合
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 

該函數的使用示例如下:

/*要初始化的cdev結構體*/
struct cdev testcdev; 
​
/* 設備操作函數 */ 
static struct file_operations test_fops = { 
   .owner = THIS_MODULE, 
   /* 其他具體的初始項 */ 
}; 
​
testcdev.owner = THIS_MODULE;
​
/* 初始化cdev*/ 
cdev_init(&testcdev, &test_fops); 

 

2.2.3 cdev_add函數

該函數用于向Linux系統添加字符設備,即cdev結構體變量:

/*
* cdev:要初始化的cdev結構體變量
* dev:字符設備所使用的設備號
* count:要添加的設備數量
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count) 

2.2.4 cdev_del函數

卸載驅動的時候要使用cdev_del函數從Linux內核中刪除字符設備:

/*
* p:要刪除的字符設備
*/
void cdev_del(struct cdev *p)

2.3 自動創建設備節點

上篇的Linux驅動實驗中,在使用modprobe加載驅動程序以后還需要使用“mknod”命令手動創建設備節點,比較麻煩,這里就來研究一下如何實現自動創建設備節點。

2.3.1 mdev機制

在Linux下通過udev來實現設備文件的自動創建與刪除。使用busybox構建根文件系統的時候,busybox會創建一個udev的簡化版本mdev

所以,在嵌入式開發中使用mdev來實現設備節點文件的自動創建與刪除。Linux系統中的熱插拔事件也由mdev 管理,在/etc/init.d/rcS 文件中如下語句:

echo /sbin/mdev > /proc/sys/kernel/hotplug 

2.3.2 創建和刪除類

自動創建設備節點的工作是在驅動程序的入口函數中完成的,一般在cdev_add函數后面添 加自動創建設備節點相關代碼。

首先要創建一個class類,其實是個結構體,定義在include/linux/device.h里面。class_create是類創建函數(宏定義):

#define class_create(owner, name) \ 
({ \ 
    static struct lock_class_key __key; \ 
    __class_create(owner, name, &__key); \ 
}) 
​
struct class *__class_create(struct module *owner, 
                             const char *name, 
                             struct lock_class_key *key) 

卸載驅動程序的時候需要使用函數為class_destroy刪除掉類

/*
* cls:要刪除的類
*/
void class_destroy(struct class *cls); 

 

2.3.3 創建設備

創建好類以后還不能實現自動創建設備節點,還需要在這個類下創建一個設備。使用device_create函數創建設備:

/*
* class:設備要創建哪個類下面
* parent:父設備, 一般為 NULL
* devt:設備號
* drvdata:設備可能會使用的一些數據,一般為 NULL
* fmt:設備名字
*/
struct device *device_create(struct clas *class,  
                             struct device *parent,  
                             dev_t devt,  
                             void *drvdata,  
                             const char *fmt, ...) 

參數最后的...表示這在是一個可變參數的函數。

2.4 設置文件私有數據

每個硬件設備都有一些屬性, 比如主設備號(dev_t),類(class)、設備(device)、開關狀態(state)等等,在編寫驅動的時候你可以將這些屬性全部寫成變量的形式:

dev_t         devid;     /*設備號*/ 
struct cdev   cdev;      /*cdev*/ 
struct class  *class;    /*類*/ 
struct device *device;   /*設備*/ 
int           major;     /*主設備號*/ 
int           minor;     /*次設備號*/ 

可以將所有屬性信封裝到結構體中, 并在編寫驅動open函數的時候將其作為私有數據添加到設備文件中:

/*設備結構體*/ 
struct test_dev{ 
    dev_t         devid;     /*設備號*/ 
    struct cdev   cdev;      /*cdev*/ 
    struct class  *class;    /*類*/ 
    struct device *device;   /*設備*/ 
    int           major;     /*主設備號*/ 
    int           minor;     /*次設備號*/ 
}; 
​
struct test_dev testdev; 
​
/*open函數*/ 
static int test_open(struct inode *inode, struct file *filp) 
{ 
    filp->private_data = &testdev; /*設置私有數據*/ 
    return 0; 
} 

3 驅動程序編寫

在上篇的基礎上進行修改,因為只是更換的驅動程序的編寫方式,與應用程序無關,因此只修改驅動程序即可。

3.1 添加一些定義

因為上篇文章的代碼中使用的是chrdevbase這個名稱,為了減少修改量,這里僅把結構體類型定義為帶有new標志的newchr_dev,變量名仍使用chrdevbase這個名稱。

#define CHRDEVBASE_CNT          1       /* 設備號個數 */
#define CHRDEVBASE_NAME  "chrdevbase"   /* 名字 */
​
/*newchr設備結構體 */
struct newchr_dev{
    dev_t         devid;    /* 設備號   */
    struct cdev   cdev;     /* cdev     */
    struct class  *class;   /* 類       */
    struct device *device;  /* 設備     */
    int           major;    /* 主設備號 */
    int           minor;    /* 次設備號 */
};
​
struct newchr_dev chrdevbase; /* 自定義字符設備 */

3.2 修改open函數

在上篇程序的基礎上增加了一條“設置私有數據”

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	printk("chrdevbase open!\r\n");
    filp->private_data = &chrdevbase; /* 設置私有數據 */
	return 0;
}

3.3 修改init函數

這個修改比較大,因為要在init函數中使用設備號的自動分配

static int __init chrdevbase_init(void)
{
    /* 注冊字符設備驅動 */
	/* 1、創建設備號 */
	if (chrdevbase.major) /* 定義了設備號 */
    {
		chrdevbase.devid = MKDEV(chrdevbase.major, 0);
		register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME);
	} 
    else /* 沒有定義設備號 */
    {
		alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME);	/* 申請設備號 */
		chrdevbase.major = MAJOR(chrdevbase.devid);	/* 獲取分配號的主設備號 */
		chrdevbase.minor = MINOR(chrdevbase.devid);	/* 獲取分配號的次設備號 */
	}
	printk("chrdevbase major=%d,minor=%d\r\n",chrdevbase.major, chrdevbase.minor);	
	
	/* 2、初始化cdev */
	chrdevbase.cdev.owner = THIS_MODULE;
	cdev_init(&chrdevbase.cdev, &chrdevbase_fops);
	
	/* 3、添加一個cdev */
	cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT);

	/* 4、創建類 */
	chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
	if (IS_ERR(chrdevbase.class)) 
    {
		return PTR_ERR(chrdevbase.class);
	}

	/* 5、創建設備 */
	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!\r\n");
	return 0;
}

3.4 修改exit函數

因為init修改較大,對應的exit也要進行大的修改:

static void __exit chrdevbase_exit(void)
{
    /* 注銷字符設備驅動 */
	cdev_del(&chrdevbase.cdev);/*  刪除cdev */
	unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /* 注銷設備號 */

	device_destroy(chrdevbase.class, chrdevbase.devid);
	class_destroy(chrdevbase.class);
    
    printk("chrdevbase exit done!\r\n");
}

至此,修改完畢,其它的與之前的一樣。

3.5 新舊驅動方式對比

通過一張圖來對比新舊兩種驅動編寫方式的區別

  • 舊方式編寫驅動的流程

1.png

  • 新方式編寫驅動的流程

2.png

可以看出主要區別在驅動的加載和卸載。

4 編譯驅動

和上次編譯驅動的方式一樣,使用makefile,因為驅動的c文件名由chrdevbase.c改為了newchrdevbase.c,因此makefile文件中也要把名字改掉。

編譯完之后,將編譯出的ko文件先復制到ubuntu虛擬機的tftpboot目錄中,為后面的測序做準備。

3.png

復制后,看一下tftpboot目錄:

4.png

5 程序測試

5.1 文件發送到板子

和上篇一樣,使用tftp傳輸,將ubuntu虛擬機編譯出的ko文件發送到linux板子中

再來看下tftp傳輸的硬件環境示意圖:

8.png

然后是傳輸指令以及傳輸結果,可以看到newchrdevbase.ko已經從ubuntu虛擬機的tftpboot目錄傳輸到了linux板子的/lib/modules/4.1.15目錄中了。

5.png

5.2 測試

輸入如下兩條指令加載 newchrdevbase.ko 驅動模塊:

depmod   //第一次加載驅動的時候需要運行此命令 
modprobe newchrdevbase.ko   //加載驅動 

驅動加載成功后,可以看到自動申請到的主設備號和次設備號,如下圖,主設備號為249。

再輸入ls /dev/chrdevbase -l指令驗證/dev/chrdevbase 這個設備節點文件是否存在,如下圖,可以看到設備存在,注意和上篇舊驅動方式操作上的不同之處,舊的驅動方式需要額外使用mknod指令來手動創建該設備節點

6.png

驅動已經加載成功,再來測試APP程序,理論上和上篇的效果一樣,實測也是:

7.png

OK,測試完畢,測試完使用rmmod指令卸載驅動。

6 總結

此篇文章針對上篇文章使用舊字符驅動編寫方式存在的不足,介紹了一種新的字符驅動編寫方式,對比兩種方式編寫的主要區別,在上篇驅動代碼的基礎上進行修改,并測試通過,和上篇實現一樣的效果,但驅動的加載更加方便,不再需要人為指定設備號。

 

回復評論 (3)

板凳 青陽澤

2021-9-1 02:38

4樓 freebsder

2021-9-1 17:33

謝謝分享,很多網上的帖子都是說的老驅動,期待樓主多上點新內核驅動的新用法。

默認摸魚

電子工程世界版權所有 京B2-20211791 京ICP備10001474號-1 京公網安備 11010802033920號
    我也要說兩句
    發送
    評論
    萝卜大香蕉