姓名:薛绍宏 学号:19020100016 学院:电子工程学院
【嵌牛导读】本文介绍了嵌入式Linux驱动程序开发的字符设备驱动的概念和方法
【嵌牛鼻子】嵌入式Linux驱动程序开发
【嵌牛提问】嵌入式Linux字符设备驱动如何开发?
【嵌牛正文】
Linux 中的三大类驱动:字符设备驱动、块设备驱动和网络设备驱动。
字符设备驱动:IO口的驱动,比如点灯、 I2C、 SPI、音频等。
块设备驱动:存储器设备的驱动,比如 EMMC、 NAND、 SD 卡和 U 盘等存储设备
网络设备驱动:网络驱动,比如 USB WIFI,其使用 USB 接口,所以属于字符设备,但是其又能上网,所以也属于网络设备驱动。
Linux 下的应用程序是如何调用驱动程序的?
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如, led 灯/dev/led 的驱动文件,应用程序使用open()、close()、write()、read()等函数对文件操作,进而控制硬件。
字符设备驱动简介
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。
如上图,从应用程序到具体驱动程序都有相应的函数与之对应,比如调用open 这个函数。
在内核部分: Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。(听名字就知道是管理驱动文件操作的)
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
......
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
file_operation 结构体中比较重要的、常用的函数:
第 1589 行, owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE。
第 1590 行, llseek 函数用于修改文件当前的读写位置。
第 1591 行, read 函数用于读取设备文件。
第 1592 行, write 函数用于向设备文件写入(发送)数据。
第 1596 行, poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
第 1597 行, unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
第 1598 行, compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。
第 1599 行, mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
第 1601 行, open 函数用于打开设备文件。
第 1603 行, release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
第 1604 行, fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。
第 1605 行, aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
字符设备驱动开发步骤
在 Linux 驱动开发中肯定也是要初始化相应的外设寄存器, Linux 驱动开发中我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架。
驱动模块的加载和卸载
Linux 驱动有两种运行方式:
第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。
第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。
调试时推荐使用将驱动编译成模块,在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。
驱动开发完成可以将驱动编译进Linux 内核中。
模块有加载和卸载两种操作,
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
1
2
module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候, xxx_init 这个函数就会被调用。
module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
字符设备驱动模块加载和卸载模板如下所示:
1 /* 驱动入口函数 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函数具体内容 */
5 return 0;
6 }
7 8
/* 驱动出口函数 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函数具体内容 */
12 }
13
14 /* 将上面两个函数指定为驱动的入口和出口函数 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
驱动编译完成以后扩展名为.ko的模块,终端命令常用insmod、rmmod,另外modprobe命令可以将具有依赖关系的模块一锅端的加载和卸载:
//加载驱动模块
insmod drv.ko
或 modprobe drv.ko
//卸载驱动模块
rmmod drv.ko
或 modprobe -r drv.ko
1
2
3
4
5
6
字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。
(就是你不仅要加载自己的驱动模块,还要向内核注册申请,这样才能是合法公民)
字符设备的注册和注销函数原型:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
major: 主设备号, Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops: 结构体 file_operations 类型指针,指向设备的操作函数集合变量。
1
2
3
4
5
6
注册函数register_chrdev()位于加载函数调用的驱动入口函数中;
注销函数unregister_chrdev()位于卸载函数调用的驱动出口函数中。
37 /* 驱动入口函数 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函数具体内容 */
41 int retvalue = 0;
42
43 /* 注册字符设备驱动 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符设备注册失败,自行处理 */
47 }
48 return 0;
49 }
50
51 /* 驱动出口函数 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注销字符设备驱动 */
55 unregister_chrdev(200, "chrtest");
56 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
设备号不能和其他驱动文件冲突,可使用命令查看当前已经被使用掉的设备号:
cat /proc/devices
1
实现设备的具体操作函数
比如,对chrtest 设备进行驱动,对file_operations结构体类型的变量 test_fops进行初始化,内部存有驱动文件操作函数,用什么操作函数就添加什么操作函数。
打开和关闭操作:最基本的要求;
读写操作:拥有读写一段缓冲区(内存)。
1 /* 打开设备 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用户实现具体功能 */
5 return 0;
6 }
7
8 /* 从设备读取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
10 {
11 /* 用户实现具体功能 */
12 return 0;
13 }
14
15 /* 向设备写数据 */
16 static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
17 {
18 /* 用户实现具体功能 */
19 return 0;
20 }
21
22 /* 关闭/释放设备 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用户实现具体功能 */
26 return 0;
27 }
28//初始化操作函数结构体变量
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
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
添加 LICENSE 和作者信息
LICENSE 是必须添加的,否则的话编译的时候会报错,LICENSE 采用 GPL 协议。
作者信息可以添加也可以不添加。
MODULE_LICENSE("GPL") //添加模块 LICENSE 信息
MODULE_AUTHOR("zxy") //添加模块作者信息
1
2
总结
一个完整的字符设备驱动模板就上以上4条:
/*1、文件操作函数file_operations结构体 变量初始化*/
......
/*2、驱动入口函数注册 驱动出口函数注销*/
......
/*3、加载驱动模块 卸载驱动模块*/
......
/*4、license和作者信息*/
(执行流程:3调用2,2调用1)
1
2
3
4
5
6
7
8
Linux 设备号
设备号的组成
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,
主设备号表示某一个具体的驱动,
次设备号表示使用这个驱动的各个设备。
设备号由dev_t数据类型表示:
typedef __kernel_dev_t dev_t;
......
typedef __u32 __kernel_dev_t;
......
typedef unsigned int __u32;
......
经过来回的定义,设备号就是c语言中unsigned int的32位数据类型;
其中高 12 位为主设备号, 低 20 位为次设备号。
1
2
3
4
5
6
7
8
文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏函数):
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
1
2
3
4
5
6
第 6 行,宏 MINORBITS 表示次设备号位数,一共是 20 位。
第 7 行,宏 MINORMASK 表示次设备号掩码。
第 9 行,宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
设备号的分配
1、静态分配设备号
使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号,人为选择没有使用的设备号。
2、动态分配设备号
Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统自动给你一个没有被使用的设备号,这样就避免了冲突。
设备号的申请函数如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev: 保存申请到的设备号。
baseminor: 次设备号起始地址, alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name: 设备名字。
1
2
3
4
5
6
设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。
1
2
3
4
chrdevbase 字符设备驱动开发实验
为了完整的编写一个字符设备驱动模块,假设一个虚拟设备: chrdevbase
设备描述:chrdevbase 设备有两个缓冲区,一个为读缓冲
区,一个为写缓冲区,这两个缓冲区的大小都为 100 字节。
应用程序描述:在应用程序中可以向 chrdevbase 设备的写缓冲区中写入数据,从读缓冲区中读取数据。
实验程序编写
实现功能描述:
应用程序调用 open 函数打开 chrdevbase 这个设备,
使用 write 函数向chrdevbase 的写缓冲区 writebuf 中写入数据(不超过 100 个字节),
使用 read 函数读取读缓冲区 readbuf 中的数据操作,
应用程序使用 close 函数关闭 chrdevbase 设备。
驱动程序chrdevbase.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 : 左忠凯
版本 : V1.0
描述 : chrdevbase驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#define CHRDEVBASE_MAJOR 200 /* 主设备号 */
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
//printk("chrdevbase open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *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");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __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");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __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");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __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("zuozhongkai");
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
分析:文件架构
头文件调用的都是linux内核文件;
4个驱动文件操作函数;
驱动文件操作函数结构体初始化;
驱动入口函数,注册设备;
驱动出口函数,注销设备;
加载驱动模块
卸载驱动模块
许可证
人名
printk函数 相当于 printf 的孪生兄妹, printf运行在用户态 printk 运行在内核态,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件include/linux/kern_levels.h。
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息*/
比如:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); //0级别
printk("gsmi: Log Shutdown Reason\n"); //MESSAGE_LOGLEVEL_DEFAULT 默认级别为4
1
2
3
4
5
6
7
8
9
10
11
12
13
参数 filp 有个叫做 private_data 的成员变量, private_data 是个 void 指针,一般在驱动中将private_data 指向设备结构体,设备结构体会存放设备的一些属性。
用户程序chrdevbaseApp.c
C 库文件操作基本函数
编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数,比如open、 read、 write 和 close 这四个函数。
①、 open 函数
int open(const char *pathname, int flags)
pathname:要打开的设备或者文件名。
flags: 文件打开模式,以下三种模式必选其一:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
另外,<逻辑或> 来选择多种模式:
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
返回值:如果文件打开成功的话返回文件的文件描述符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在 Ubuntu 中输入“man 2 open” 即可查看 open 函数的详细内容
②、close 函数
int close(int fd);
fd:要关闭的文件描述符。
返回值: 0 表示关闭成功,负值表示关闭失败。
1
2
3
4
③、read 函数
当然是从文件里读内容啦~需要open函数返回的文件描述符。
ssize_t read(int fd, void *buf, size_t count)
fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 数据读取到此 buf 中。
count: 要读取的数据长度,也就是字节数。
返回值: 读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
1
2
3
4
5
6
在 Ubuntu 中输入“man 2 read”命令即可查看 read 函数的详细内容。
④、write 函数
ssize_t write(int fd, const void *buf, size_t count);
fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件, open 函数打开文件成功以后会得到文件描述符。
buf: 要写入的数据。
count: 要写入的数据长度,也就是字节数。
返回值: 写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
1
2
3
4
5
6
编写一个简单的测试 APP,控制驱动文件,测试 APP 运行在用户空间,测试 APP 很简单通过输入相应的指令来对 chrdevbase 设备执行读或者写操作。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbaseApp.c
作者 : 左忠凯
版本 : V1.0
描述 : chrdevbase驱测试APP。
其他 : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
argv[2] 1:读文件
argv[2] 2:写文件
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
static char usrdata[] = {"usr data!"}; //写入的用户数据
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
//要求只能传入3个参数
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;
}
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;
}
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
分析:程序架构
调用C库函数;
打开文件;
读写文件;
关闭文件。
判断 argv[2]参数的值是 1 或 2 ,因为输入命令的时候其参数都是字符串格式的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。
编译驱动程序和测试 APP
1、编译驱动程序chrdevbase.c 这个文件,将其编译为.ko 模块,创建Makefile 文件,路径自己改改:
KERNELDIR := /home/zxy/linux/kernel_lib/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1
2
3
4
5
6
7
8
9
10
KERNELDIR 表示开发板所使用的 Linux 内核源码目录(原子官方linux内核源码,需要编译了好久),使用绝对路径,大家根据自己的实际情况填写即可。
CURRENT_PATH 表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。
obj-m 表示将 chrdevbase.c 这个文件编译为 chrdevbase.ko 模块。
后面的modules 表示编译模块,
-C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。
M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
2、编译 chrdevbaseApp.c生成可执行程序,这里差不多就是单纯的C语言程序,用交叉编译器arm-linux-gnueabihf-gcc编译即可:
//编译
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
//查看文件信息,chrdevbaseAPP 这个可执行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下运行。
file chrdevbaseApp
1
2
3
4
3、传文件到开发板验证
左老师使用的 TFTP 从网络启动,并且使用 NFS 挂载网络根文件系统,说白了就是从uboot层面网络启动linux内核和挂在根文件系统,但是这能保存到DRAM中。
本人这里研究了一下ssh传输协议,实现linux系统client和server之间的文件互传:
1、两者连接在同一网络地址;
2、服务器安装并配置ssh server协议;
3、板子可以直接scp获取文件啦。
就是这么简单,有手就行。
具体把驱动文件chrdevbase.ko放到/lib/modules/4.1.15-gb8ddbbc目录中,就是放到这个一种库里;
应用程序chrdevApp放到home目录下或者其他正常的位置都行。
4、验证操作
insmod chrdevbase.ko //加载驱动模块
cat /proc/devices //查找能用的设备号
mknod /dev/chrdevbase c 200 0 //创建节点 “ 200”是设备的主设备号,“ 0”是设备的次设备号。
//操作这个设备节点
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
rmmod chrdevbase.ko //卸载驱动模块
lsmod //查看某些模块还存不存在,
1
2
3
4
5
6
7
8
9
10
11
总结
这种简单的驱动模块,整体就传到板子上驱动文件.ko和应用程序文件。
驱动文件放到/lib下的库文件中,一直放着就行;
使用insmod命令加载,就是跟内核申请了一块儿空间;
使用mknod命令创建节点再/dev下,就是跟内核申请营业许可证;
APP可以放到任何正常的目录下,使用APP应用程序控制/dev下这个节点,就能实现驱动的控制了;
使用rmmod命令卸载,就是跟内核说一声退还这一块儿空间,APP就控制不了了。