編寫字符設備小Demo

Linux內核——字符設備.


設備相關點解.

Linux驅動設備一共分爲三種:字符設備塊設備網絡設備,其中只有網絡設備比較特殊,其在/dev目錄下是沒有設備文件的。

區分文件類型:

符號 文件類型
c 字符設備
b 塊設備
d 目錄
l 符號鏈接

字符設備

       該設備對數據的處理按照字節流的形式進行的,支持順序訪問(是有時間的概念),也可以支持隨機訪問。
典型的字符設備:串口、鍵盤、觸摸屏、攝像頭、I2C、SPI、聲卡、幀緩衝設備….
順序訪問的設備:串口、鍵盤、觸摸屏
隨機訪問的設備:幀緩衝設備

       應用程序,能夠使用系統IO函數來就行訪問:open、write、read、lseek、close…..

比如在根目錄的/dev下我們能看到
/dev

       其中第五第六列分別是主設備號次設備號
如:fb0的主設備號爲29,次設備號爲0

1
crw-rw----  1 root video    29,   0 Feb  6 11:52 fb0

主設備號

主設備號主要用於區分某一類型的設備,比如顯示器、鼠標、鍵盤、硬盤等

次設備號

       次設備號用來區分同一類型設備的不同個體或者不同分區,比如顯示器A.a、顯示器A.b 或者硬盤的分區1~分區7;


簡述字符設備驅動的設計流程.

  1. 首先定義一個字符設備 struct cdev
  2. 申請設備號
    a. 靜態註冊
    MKDEV
    register_chrdev_region
    b. 動態註冊
    alloc_chrdev_region
  3. 定義file_operations初始化打開、關閉、讀、寫等函數接口。
  4. cdev初始化
    .cdev_init
  5. 將cdev加入到內核
    .cdev_add
  6. 創建設備文件
    .手動創建,去/dev目錄進行創建
    .自動創建
    1)class_create
    2)device_create

此處默認都能使用Source Insight來閱讀內核源碼了

定義一個字符設備.

使用到一個結構體,見include/linux/cdev.h –> cdev

1
2
3
4
5
6
7
8
9
10
#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;
};

kobj:內核管理驅動的時候,自動管理的對象,自動鏈接調用(不需要我們管);
*owner:屬於哪一個內核模塊,初始化一般默認編寫值爲THIS_MODULE,指向當前模塊;(重點)
*ops:字符設備文件操作集;(重點)
list:內核自動管理字符設備的鏈表,內核自動調用;
dev:設備號;
count:對當前設備申請次設備號的數目;(重點)

定義並初始化一個文件操作集.

定義一個file_operations結構體,見/include/linux/fs.h –> file_operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <linux/fs.h>

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	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 (*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 **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
};

       當然很多時候我們用不到它結構體定義的這麼多函數接口,只需保留最常用的即可;

       每個字符設備都必須有一個文件操作集,文件操作集是驅動程序給應用程序訪問硬件的一個接口,應用層與驅動程序函數接口的對應關係如下:

應用層 驅動層
open open
close release
write write
read read
lseek llseek
ioctl unlocked_ioctl

用法例子:(摘自源碼bsg.c)

1
2
3
4
5
6
7
8
9
10
static const struct file_operations bsg_fops = {
	.read		=	bsg_read,
	.write		=	bsg_write,
	.poll		=	bsg_poll,
	.open		=	bsg_open,
	.release	=	bsg_release,
	.unlocked_ioctl	=	bsg_ioctl,
	.owner		=	THIS_MODULE,
	.llseek		=	default_llseek,
};

對應的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ssize_t
bsg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	struct bsg_device *bd = file->private_data;
        ...
        ...
        ...
	return bytes_read;
}

static ssize_t
bsg_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	struct bsg_device *bd = file->private_data;
	ssize_t bytes_written;
        ...
        ...
        ...
	return bytes_written;
}
...
...
...

當應用層的 open函數 調用了 xxx.koxxx_open 函數
當應用層的 write函數 調用了 xxx.koxxx_write 函數
當應用層的 close函數 調用了 xxx.koxxx_release 函數

我們需要定義一個file_operations的結構體把自己編寫的接口配置進去,以便後續cdev_init的初始化。


源碼及詳解.

led_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
//1.定義字符設備
static struct cdev led_cdev;

//2.聲明設備號
static dev_t led_dev_num;

static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
    printk("<3>""led_read\n");
    return 0;
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
    printk("<3>""led_write\n");
    return 0;
}

static int led_open(struct inode *inode, struct file *file)
{
    printk("<3>""led_open\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
    printk("<3>""led_release\n");
    return 0;
}

//3.定義file_operations
static const struct file_operations led_fops = {
	.read		=	led_read,
	.write		=	led_write,
	.open		=	led_open,
	.release	=	led_release,
	.owner		=	THIS_MODULE
};

//入口函數
static int __init gec6818_led_init(void)
{
    int ret = 0;

    led_dev_num = MKDEV(239,0);     //2.1靜態申請設備號  主設備號爲239,次設備號爲0

    //2.2註冊設備號  int register_chrdev_region(dev_t from, unsigned count, const char *name)
    ret = register_chrdev_region(led_dev_num, 1, "myled");
    if (ret < 0)
    {
        printk("<3>""register chrdev region error\n");
        return -1;
    }

    //4.字符設備的初始化 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    cdev_init(&led_cdev, &led_fops);
    //5.將設備加入到內核 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    ret = cdev_add(&led_cdev, led_dev_num, 1);
    if(ret < 0)
    {
        printk("<3>""cdev_add error\n");
        goto err_cdev_add;

    }

    printk("<3>""gec6818 led init success\n");
    printk("<3>""device num: %d\n", MAJOR(led_dev_num));
    printk("<3>""device last num: %d\n", MANOR(led_dev_num));
    return 0;

err_cdev_add:
    unregister_chrdev_region(led_dev_num, 1);       //註銷設備號
    return ret;
} 

//出口函數
static void __exit gec6818_led_exit(void)
{
    
    cdev_del(&led_cdev);                            //從系統中刪除字符設備                                     

    unregister_chrdev_region(led_dev_num, 1);       //註銷設備號


	printk("<4>""gec6818 led exit\n");
}



//驅動程序的入口:insmod led_drv.ko調用module_init,module_init又會去調用gec6818_led_init。
module_init(gec6818_led_init);

//驅動程序的出口:rmmod led_drv調用module_exit,module_exit又會去調用gec6818_led_exit。
module_exit(gec6818_led_exit)


//模塊描述
MODULE_AUTHOR("LYQ");			                //作者信息
MODULE_DESCRIPTION("study kernal first code test");	//模塊功能說明
MODULE_LICENSE("GPL");				        //許可證:驅動遵循GPL協議

要點詳解

靜態申請設備號 MKDEV

MKDEV 原型 參數1:主設備號,參數2:次設備號

1
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

註冊設備號

register_chrdev_region 原型 參數1:聲明的設備號,參數2:註冊的數量,參數3:註冊的名字 返回值:成功爲0,失敗返回負數的錯誤碼

1
int `register_chrdev_region(dev_t from, unsigned count, const char *name)

字符設備的初始化

cdev_init 原型 參數1:定義的字符設備,參數2:file_operations

1
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

將設備加入內核

cdev_add 原型 參數1:定義的字符設備地址,參數2:聲明的設備號,參數3:加入的數量,返回值:成功爲0,失敗爲負數錯誤值

1
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

設備號註銷

unregister_chrdev_region 原型 參數1:聲明的設備號,參數2:數量

1
void unregister_chrdev_region(dev_t from, unsigned count)

將字符設備從內核刪除

cdev_del 原型 參數1:定義的字符設備地址

1
void cdev_del(struct cdev *p)

編寫對應Makefile.

1
2
3
4
5
6
7
8
9
10
obj-m += led_dev.o
KERNEL_DIR := /home/bbigq/6818GEC/kernel
CROSS_COMPILE := /home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD := $(shell pwd)

default:
	$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	rm  *.o *.order .*.cmd  *.mod.c *.symvers .tmp_versions -rf

編譯完成後,insmod到Linux系統中後,可以通過lsmod查看相關信息,也可以使用cat命令查看/proc/devices,能夠找到註冊成功的設備名字。

注意:由於沒有這裏只是小demo是需要手動創建設備節點的,如果配合應用程序使用時,需要爲該設備在/dev目錄下創建設備文件;
使用:

mknod /dev/led c 239 0
mknod 設備文件 設備類型 主設備號 次設備號

後續的博客中將會加入自動創建設備節點的代碼,和講解!
注意
1.手動創建設備文件的時候,主設備號和次設備號不能寫錯;
2.若此前的設備文件已經存在,再次創建相同名字的設備文件,會出現錯誤;(解決方法:先使用rm命令刪除原來的設備文件,再使用mknod命令重新創建。

最後用ls -l /dev/xxx檢查是否創建成功!


我的GITHUB

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章