[課程筆記]Linux Driver正點原子課程筆記5 - 新字元設備驅動實驗

Posted by John on 2021-03-29
Words 2.4k and Reading Time 11 Minutes
Viewed Times

〖想觀看更多課程筆記,至[課程筆記]課程筆記系列總覽可以看到目前已發布的所有文章!〗

Course 5 - 新字元設備驅動實驗

新字元設備驅動原理

在之前的課程中了解到了怎麼註冊一個chrdev,但之前使用的api是舊版的存在著一些問題,例如register_chrdev()這個function

  1. 會佔用主設備號下的所有次設備號,浪費了很多次設備號
  2. 必須手動指定主設備號,也就是說你得先手動查詢哪個主設備號還沒被佔用,然後再去指定

所以這節課程會使用新的api來撰寫字元驅動,新版的api有兩種方式:

  • alloc_chrdev_region(): 向linux kernel申請設備號
    • 給定次設備號和名字,告訴linux要申請幾個設備號,讓linux直接幫你尋找主設備號
  • register_chrdev_region(): 給定主設備號去向linux申請
    • 給定次設備號為0的devid,讓linux自己去找該主設備下可使用的次設備號
0
1
2
3
/* fs/char_dev.c */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
int register_chrdev_region(dev_t from, unsigned count, const char *name);

不論是哪種申請方式,釋放時都是call unregister_chrdev_region()

0
void unregister_chrdev_region(dev_t from, unsigned count);

一般來說在設計時兩種情況都會考慮到,一個範例code如下:

0
1
2
3
4
5
6
7
8
9
10
int major, minor;
dev_t devid;

if (major) {
devid = MKDEV(major, 0);
register_chrdev_region(devid, 1, "test");
} else {
alloc_chrdev_region(&devid, 0, 1, "test");
major = MAJOR(devid);
minor = MINOR(devid);
}

新字元設備註冊方式

介紹一個新的char structure: cdev

0
1
2
3
4
5
6
7
8
/* 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;
} __randomize_layout;

使用cdev表示char device,然後使用cdev_init()初始化結構體,然後透過cdev_add()進行添加到linux kernel中。最後,註銷時使用cdev_del()

0
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
/* fs/char_dev.c */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;

p->dev = dev;
p->count = count;

error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;

kobject_get(p->kobj.parent);

return 0;
}

void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}

如果回頭去trace register_chrdev()的code,其實他背後的本質就是上述這些flow,既然這樣為何不用register_chrdev()就好?

  • 還記得一開始提到register_chrdev()的缺點嗎?他會佔用主設備號下的所有次設備號

自動創建設備節點

到這裡還有另一個需要探討的議題:

driver儘管寫完了,但目前為止每次都仍要透過mknod來創建設備節點,例如

0
mknod /dev/newchrled c 249 0

接下來將介紹如何自動創建設備節點

udev

udev是Linux kernel 2.6系列引入的設備管理器,主要功能是管理/dev目錄下的設備節點,在熱插拔機制下,添加/刪除硬體時需要負責處理/dev目錄以及所有user space的行為。

傳統的建立設備節點需要手動添加,透過使用mknod方法,但這樣並不易管理,比方說可能有設備根本沒掛載,但他仍然再/dev下存在著設備節點。

採用udev的方法的話,只有備kernel檢測到的設備才會去添加設備節點。因為這些設備節點在每次系統啟動時被創建,他們會備存在ramfs(一個內存中的文件系統,不占用任何空間),設備節點不會占用大量的磁碟空間,因此使用的memory可以忽略。

mdev

在使用busybox建構rootfs的時候,busybox會創建一個udev的簡化版本-mdev的簡化版本 - mdev,在嵌入式系統一般都是用mdev來管理設備節點的自動插入和刪除。

在/etc/init.d/rcS中可以看到如下的設置

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

上述命令設置熱插拔事件由mdev來管理,所以接下來來看一下如何用mdev來實現設備節點的新增和刪除。

自動創建設備節點是在驅動的entry function完成的,一般會在cdev_add後面加入相關代碼。

首先需要創建一個class,class被定義在include/linux/device.h內

創建/註銷class

/* include/linux/device.h */

struct class {
const char *name;
struct module *owner;

const struct attribute_group **class_groups;
const struct attribute_group **dev_groups;
struct kobject *dev_kobj;

int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
char *(*devnode)(struct device *dev, umode_t *mode);

void (*class_release)(struct class *class);
void (*dev_release)(struct device *dev);

int (*shutdown_pre)(struct device *dev);

const struct kobj_ns_type_operations *ns_type;
const void *(*namespace)(struct device *dev);

void (*get_ownership)(struct device *dev, kuid_t *uid, kgid_t *gid);

const struct dev_pm_ops *pm;

struct subsys_private *p;
};

使用時需要#include <linux/device.h>透過class_create來自動創建class

0
1
2
3
4
5
6
7
8
/* include/linux/device.h */

/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
  • 第一個參數是owner, 通常都帶THIS_MODULE
  • 第二個參數帶入driver name

而註銷class的時候則是透過class_destroy

  • 帶入class參數

創建/註銷設備

有了創建/註銷class後,需要在該class下創建一個設備,使用device_create來創立

0
1
2
3
/* include/linux/device.h */
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);
  • device_create是一個不定參數的function
  • class代表設備要建立在哪個class下面
  • parent代表父設備, 一般為NULL
  • devt是設備號
  • drvdata是設備可能會使用到的一些數據, 一般為NULL
  • fmt是設備名稱, 如果fmt=xxx的話就會生成/dev/xxx文件

同樣的,卸載驅動的時候也要註銷設備,透過device_destroy來註銷

文件私有數據和goto對錯誤的處理

driver常常會用到一些私有的數據,好的設計方式是透過structure把他們都包起來

struct file中其實有個private_data的變數可以用來將需要的data struct存起來,如此就不用在code中另外宣告需要的變數

  • 通常會在open()內設置filp->private_data,然後在其他function來存取私有數據
  • release()的時候會做對應的釋放
0
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
/* include/linux/fs.h */
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

最後來講講linux driver在init過程中如果發生錯誤時,我們需要將我們init的東西都釋放掉,這時我們常常會透過goto來做流程的控制,透過好的goto流程設計可以讓我們的driver code有更好的可讀性以及程式架構。

了解了上述的所有內容後,最後來重寫之前寫過的led driver code

  • 下方的code還不包含init錯誤時goto的處理

driver

0
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define LED_MAJOR 200
#define LED_NAME "led"

/* register PA */
#define CCM_CCGR1_BASE (0x020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0x020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0x020E02F4)
#define GPIO1_DR_BASE (0x0209C000)
#define GPIO1_GDIR_BASE (0x0209C004)

/* pointer of VA */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

#define LEDOFF 0
#define LEDON 1

#define NEWCHRLED_NAME "newchrled"
#define NEWCHRLED_CNT 1

struct newchrled_dev{
struct cdev cdev;
dev_t devid; // device num
struct class *class; // class
struct device *device; // device
int major;
int minor;
};
void led_switch(u8 sta)
{
u32 val = 0;
if (databuf[0] == LEDOFF) {
val = readl(GPIO1_DR);
val |= ~(1 << 3);
writel(val, GPIO1_DR); // close led
} else {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR); // open led
}
}

static int newchrled_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled;
return 0;
}

static int newchrled_release(struct inode *inode, struct file *filp)
{
struct newchrled_dev *dev = (struct newchrled_dev)filp->private_data;

return 0;
}

static ssize_t newchrled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
int ret;
int val = 0;
unsigned char databuf[1];
ret = copy_from_user(databuf, buf, count);
if (ret < 0){
printk("kernel write fail\n");
return -EFAULT;
}

led_switch(databuf[0]);

return 0;
}

struct newchrled_dev newchrled;
static const struct file_operations newchrled_fops = {
.owner = THIS_MODULE,
.write = newchrled_write,
.open = newchrled_open,
.release = newchrled_release,
};

static void __init newchrled_init(void)
{
int ret = 0;
int val = 0;

/* 1. init led */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); // clr IMX6U_CCM_CCGR1[26:27]
val |= 3 << 26; // set IMX6U_CCM_CCGR1[26:27] to 1
writel(val, IMX6U_CCM_CCGR1);

writel(0x5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);

val = readl(GPIO1_GDIR);
val |= 1 << 3;
writel(val, GPIO1_GDIR);

val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR); // open led

/* 2. register dev number */
if (newchrled.major) {
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME);
} else {
ret = alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME);
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
if (ret < 0) {
printk("newchrled chr_dev region err\n");
return -1;
}
printk("major:%d, minor:%d\n", newchrled.majorm newchrled.minor);

/* 3. register chardev */
newchrled.cdev.owner = THIS_MODULE;
cdev_init(&newchrled.cdev, &newchrled_fops);
ret = cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

/* 4. auto create device node */
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(newchrled.class))
reutrn PTR_ERR(newchrled.class);

newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device))
return PTR_ERR(newchrled.device);
return 0;
}

static void __exit newchrled_exit(void)
{
int val = 0;
val = readl(GPIO1_DR);
val |= ~(1 << 3);
writel(val, GPIO1_DR); // close led

/* 1. unmap va */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);

/* 2. unregister */
cdev_del(&newchrled.cdev);
printk("chrdev unregister\n");

unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);

/* 3. destroy device */
device_destroy(newchrled.class, newchrled.devid);

/* 4. dectroy class */
class_destroy(newchrled.class);

reutrn 0;
}

module_init(newchrled_init);
module_exit(newchrled_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("john");

app

app的部分沒有什麼變動,所以就不重寫了,要看的可以看前幾次的課程筆記


>