注:本文的大部内容来源于宋宝华的《Linux设备驱动开发详解》、《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf》和 韦东山的《嵌入式linux应用开发》,只用于学习记录。
1. Linux字符设备驱动的概念
字符设备是 Linux 驱动中最基本的一类设备,它是能够像字节流一样被访问的设备,也就是说对它的读写是以字节为单位的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等都是字符设备,这些设备的驱动就叫做字符设备驱动。字符设备的驱动程序实现了open、close、read、write等系统调用,应用程序可以通过设备文件(例如/dev/led)来访问字符设备。
2. Linux 应用程序对驱动程序的调用流程
在 Linux 中一切皆为文件,驱动加载成功以后会在 “/dev” 目录下生成一个相应的文件,应用程序通过对这个名为 “/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。以一个LED驱动程序为例,应用程序、库、内核、驱动程序之间的协作关系如下:
(1) 应用程序使用库提供的 open 函数打开代表LED的设备文件,如 /dev/led ;
(2) 库根据 open 函数传入的参数执行swi指令,该指令会触发CPU异常,进入内核;此时CPU由用户模式进入特权模式;
(3) 内核的异常处理函数根据这些参数找到相应的驱动程序,返回一个文件句柄给库,进而返回给应用程序;
(4) 应用程序得到文件句柄后,使用库提供的write或者ioctl函数发出控制命令;
(5) 库根据write或者ioctl函数传入的参数执行swi指令,该指令会引发CPU异常,进入内核;
(6) 内核的异常处理函数根据write或ioctl传入的参数调用驱动程序的相关函数,操作LED。
例如,调用open函数时的执行流程如下:
3. 字符设备驱动结构
3.1 cdev结构体
在Linux内核中,使用cdev结构体描述一个字符设备, cdev结构体的定义如代码如下:
struct cdev {
struct kobject kobj; /*内嵌的kobject对象*/
struct module *owner; /*所属模块*/
const struct file_operations *ops; /*file_operations 文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count; /*隶属于同一主设备号的次设备号的个数.*/
};
(1) cdev结构体的dev_t.成员定义了设备号,为32位,其中高12位为主设备号, 低20位为次设备号。
(2) 使用下列宏可以根据 dev_t 获得主设备号和次设备号:
MAJOR(dev_t dev) /*根据设备号dev_t获取主设备号*/
MINOR(dev_t dev) /*根据设备号dev_t获取次设备号*/
它们的定义如下:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) /* MINORMASK = 0xFFFFF */
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) /*高12位清零*/
(3) 使用下列宏则可以通过主设备号和次设备号生成dev_t:
MKDEV(ma,mi)
它的定义如下:
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
(4) cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。
3.1.1 操作cdev结构体的函数
Linux内核提供了一组函数以用于操作cdev结构体,这些函数如下:
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
3.1.1.1 cdev_init 函数
cdev_init 函数用于初始化 cdev 的成员,并建立 cdev和 file_operations之间的连接,它的源代码如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list); /*初始化cdev->list链表*/
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; /*将传入的file_operations结构体指针赋值给 cdev 的 file_operations 结构体成员*/
}
3.1.1.2 cdev_alloc 函数
cdev_alloc 函数用于动态申请一个cdev内存,其源代码如下:
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
3.1.1.3 cdev_add 和 cdev_del 函数
cdev_add 函数和cdev_del 函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。
(1) cdev_add 通常发生在字符设备驱动模块加载函数;
(2) cdev_del通常发生在字符设备驱动模块卸载函数;
3.2 分配和释放设备号
3.2.1 分配设备号
在调用cdev_add函数向系统注册字符设备之前,应首先调用register_chrdev_region 或 alloc_chrdev_region 函数向系统申请设备号,它们的原型如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
(1) register_chrdev_region函数用于已知起始设备的设备号的情况;
(2) alloc_chrdev_region 函数用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。
(3) alloc_chrdev_region 相比于register_chrdev_region 的优点在于它会自动避开设备号重复的冲突。
3.2.2 释放设备号
在调用cdev_del函数从系统注销字符设备之后,unregister_chrdev_region 应该被调用以释放原先申请的设备号,这个函数的原型为:
void unregister_chrdev_region(dev_t from, unsigned count);
注:①region(区域) : 是指从(某个主设备号、某个次设备号) ~(某主设备号,某次设备号+n)都对应同一个 file_operations 结构体;例如:
register_chrdev_region(MKDEV(major,0),2,"hello");/*(major,0~1)都对应同一个file_operations 结构体,(major,2~255)都不对应这个file_operations结构体*/
(major,0~1)都对应同一个file_operations 结构体,(major,2~255)都不对应这个file_operations结构体。
② open 一个字符设备时,虚拟文件系统(VFS)层,sys_open 会以 “主设备号”和“次设备号”两个作为一个整体来找到 file_operations 结构体。
3.3 file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close() 等系统调用时最终被内核调用。file_operations 结构体代码如下:
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
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 *);
unsigned long mmap_supported_flags;
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 (*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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
}; /*这是linux-4.15内核file_operations的源码*/
(1) owner 拥有该结构体的模块的指针,一般设置为 THIS_MODULE;
(2) llseek 函数用于修改文件当前的读写位置;
(3) read 函数用于读取设备文件;
(4) write 函数用于向设备文件写入(发送)数据;
(5) poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写;
(6) unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应;
(7) compat_ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl;
(8) mmap 函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制;
(9) open 函数用于打开设备文件;
(10) release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应;
(11) fasync 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中;
(12) aio_fsync 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据。
在字符设备驱动开发中最常用的就是上面这些函数,在字符设备驱动开发中最主要的工作就是根据具体的硬件实现上面这些函数。
3.4 字符设备驱动的组成
在Linux中,字符设备驱动的组成部分:模块加载与卸载函数、file_operations结构体中的成员函数。
3.4.1 模块加载与卸载函数
字符设备驱动模块加载与卸载函数的功能:
加载函数: 设备号的申请和cdev的注册;
卸载函数: 实现设备号的释放和cdev的注销;
字符设备驱动模块加载与卸载函数模板,代码如下:
/*定义设备结构体*/
struct xxx_dev_t {
struct cdev cdev;
...
}xxx_dev;
/*设备驱动模块加载函数*/
static int _ _init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops);/*初始化cdev*/
xxx_dev.cdev.owner = THIS_MODULE; /*所述本模块*/
/* 获取字符设备号*/
if (xxx_major)
{
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
}else{
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备*/
...
}
/* 设备驱动模块卸载函数*/
static void _ _exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号*/
cdev_del(&xxx_dev.cdev); /* 注销设备*/
...
}
3.4.2 file_operations结构体中的成员函数
file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。
字符设备驱动读、写、 I/O控制函数模板代码如下:
/* 读设备*/
/* filp:文件结构体指针
* buf:用户空间内存的地址,该地址在内核空间不宜直接读写
* count:要读的字节数
* f_pos:读的位置相对于文件开头的偏移
*/
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...);
...
}
/*写设备*/
ssize_t xxx_write(struct file *filp, const char __user *buf,size_t count,loff_t *f_pos)
{
...
copy_from_user(..., buf, ...);
...
}
/* ioctl函数 */
long xxx_ioctl(struct file *filp, unsigned int cmd,unsigned long arg)
{
...
switch (cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
/* 不能支持的命令 */
return - ENOTTY;
}
return 0;
}
注:由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user() 完成用户空间缓冲区到内核空间的复制,以及copy_to_user() 完成内核空间到用户空间缓冲区的复制。
完成内核空间和用户空间内存复制的 copy_from_user() 和 **copy_to_user()**的原型分别为:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。如果复制失败,则返回负值。
注:读和写函数中的_user是一个宏,表明其后的指针指向用户空间。
3.4.3 字符设备驱动结构图
从上图可知:
(1) 一个字符设备对应一个cdev结构体;
(2) cdev结构体主要包含的信息有:设备号dev_t、file_operations 结构体;
(3) 模块的加载函数 会 初始化cdev结构体,同时把驱动的file_operations 绑定到 cdev结构体的 fops,并把cdev结构体添加进内核;
(4) 模块卸载函数会把 cdev结构体从内核删除;
(5) 当字符设备驱动被添加到内核后,用户空间的应用程序通过系统调用可以间接访问到字符设备驱动的read、write、ioctl等底层驱动函数。
4. 创建设备节点
驱动加载成功需要在 /dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。设备节点的创建方式分有两种:手动创建设备节点、自动创建设备节点。
4.1 手动创建设备节点
在Linux中,可以通过 “mknod” 命令来手动创建设备节点,该命令的格式为:mknod /dev/xxx 设备类型 主设备号 次设备号
例如:在/dev 目录下创建led设备节点
mknod /dev/led c 200 0
其中,/dev/led 是要创建的设备节点文件;“c” 表示字符设备;“200” 是设备的主设备号;“0” 是设备的次设备号。
4.2 自动创建设备节点
在驱动中实现自动创建设备节点的功能以后,加载驱动模块成功的后会自动在 /dev 目录下创建对应的设备文件。自动创建设备节点涉及到一个机制,这个机制就是udev机制。
4.2.1 udev机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除, udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。使用 busybox 构建根文件系统的时候,busybox 会创建一个 udev 的简化版本—mdev,所以在嵌入式 Linux 中我们使用 mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理,在 /etc/init.d/rcS 文件中如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
上述命令设置热插拔事件由 mdev 来管理,内核有设备加载或者卸载时,就会通过 /proc/sys/kernel/hotplug 来指示应用程序(/sbin/mdev)自动创建或者删除设备节点。
4.2.2 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件 include/linux/device.h 里面,class_create 是类创建函数, 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_create 展开以后内容如下:
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
4.2.3 创建和删除设备
创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备, device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...);
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成 /dev/xxx 这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原:
void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
5. 字符设备驱动开发步骤
在 Linux 驱动开发中我们需要按照其规定的框架来编写驱动,所以说学 Linux 驱动开发重点是学习其驱动框架,字符设备驱动模型如下:
我们可以根据字符设备驱动的框架去开发具体的字符设备驱动,以下是字符设备驱动程序的编写步骤:
(1) 编写驱动程序的初始化函数:进行一些必要的初始化,比如初始化cdev结构体、硬件初始化、向内核注册驱动程序等;
(2) 构造file_operations 结构体中需要用到的各个成员函数;
(3) 修饰入口函数、出口函数;
5. 字符设备驱动程序代码框架
通过前面的学习,我们已经对字符设备驱动程序框架有了基本的了解,下面我们以 chrdevbase 这个虚拟设备为例,完整的编写一个字符设备驱动模块,chrdevbase.c 代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static char readbuf[100]; /* 读缓冲区 */
static char writebuf[100]; /* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};
/*1.定义设备结构体*/
struct chrdevbase_dev_t {
struct cdev cdev; /*1.1 定义字符设备结构体*/
int major; /*1.2 定义主设备号*/
struct class *class; /*1.3 定义类 */
struct device *device; /*1.4 定义设备 */
};
static struct chrdevbase_dev_t chrdevbase_dev;
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!\r\n");
return 0;
}
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;
}
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;
}
static int chrdevbase_release(struct inode *inode,struct file *filp)
{
printk("chrdevbase release! \r\n");
return 0;
}
/*2.构造file_operations结构*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
static int __init chrdevbase_init(void)
{
/* 注册字符设备驱动 */
/* 1、创建设备号 */
dev_t devid;
if(chrdevbase_dev.major)
{
devid = MKDEV(chrdevbase_dev.major,0);
register_chrdev_region(devid,1,CHRDEVBASE_NAME);
}else
{
alloc_chrdev_region(&devid,0,1,CHRDEVBASE_NAME);
chrdevbase_dev.major = MAJOR(devid);
}
/* 2、初始化cdev */
cdev_init(&chrdevbase_dev.cdev,&chrdevbase_fops);
/* 3、添加一个cdev */
cdev_add(&chrdevbase_dev.cdev,devid,1);
/* 4、创建类 */
chrdevbase_dev.class = class_create(THIS_MODULE,CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase_dev.class)) {
return PTR_ERR(chrdevbase_dev.class);
}
/* 5、创建设备 */
chrdevbase_dev.device = device_create(chrdevbase_dev.class,NULL,devid,NULL,CHRDEVBASE_NAME); /*创建设备节点*/
if (IS_ERR(chrdevbase_dev.device)) {
return PTR_ERR(chrdevbase_dev.device);
}
return 0;
}
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
device_destroy(chrdevbase_dev.class,MKDEV(chrdevbase_dev.major,0)); /*删除设备*/
class_destroy(chrdevbase_dev.class); /*删除类*/
cdev_del(&chrdevbase_dev.cdev);/* 删除cdev */
unregister_chrdev_region(MKDEV(chrdevbase_dev.major,0),1);/* 注销设备号 */
}
module_init(chrdevbase_init); /*修饰入口函数*/
module_exit(chrdevbase_exit); /*修饰出口函数*/
MODULE_LICENSE("GPL");
Makefile 代码如下:
KERN_DIR = /home/book/works/linux-4.15
# -C是转到后面的目录$(KERN_DIR),使用该目录下的Makefile来编译
# M是指当前目录是什么
# modules是目标
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
#obj-m 表示编译成.ko 模块
obj-m += chrdevbase.o
测试app 代码如下: (chrdevbase_app.c)
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
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) 在驱动程序所在的目录下输入 make 命令会执行Makeflie编译chrdevbase.c驱动程序,生成chrdevbase.ko文件;
(2) 在chrdevbase_app.c所在目录输入命令编译应用程序:arm-linux-gcc -o chrdevbase_app chrdevbase_app.c
(3) 挂载服务器文件:mount -t nfs -o nolock,vers=2 192.168.0.105:/home/book/works/first_fs /mnt
该命令的功能:把192.168.0.105服务器的目录**/home/book/works/first_fs**的文件挂载到linux开发板的 /mnt目录下;这样我们就可以直接把编译好的 .ko 文件拷贝到 /home/book/works/first_fs 目录,通过网络挂载的方式可以直接下载到开发板,方便我们调试。
(4) 驱动加载的命令:insmod;驱动卸载命令:rmmod;查看已加载驱动的命令:lsmod;
注:新烧写的文件系统由于没有 /lib/modules/内核版本号/ 这个目录,会出现错误:rmmod: chdir(/lib/modules): No such file or directory
解决办法:在开发板执行命令:mkdir -p /lib/modules/$(uname -r)
5.1 测试
(1) 在开发板输入如下命令装载chrdevbase.ko驱动程序:
insmod chrdevbase.ko
重启系统第一次装载驱动程序是会输出:loading out-of-tree module taints kernel.,如下图所示:
据说因为内核在编译的时候选择支持内核签名机制,这是为了Kernel安全加上的。
(2) 成功加载驱动程序后,可以输入lsmod 命令查看,如下图所示:
从上图可以知,当前系统只有“chrdevbase”这一个模块。
(3) 输入如下命令查看当前系统中有没有 chrdevbase 这个设备:
cat /proc/devices
(4) 使用 chrdevbase_app 软件操作 chrdevbase 这个设备,看看读写是否正常,首先进行读操作,输入如下命令:
./chrdevbase_app /dev/chrdevbase 1
输出信息如下:
上图说明,说明对 chrdevbase 的读操作正常。
(5) 接下来测试对 chrdevbase 设备的写操作,输入如下命令:
./chrdevbase_app /dev/chrdevbase 2
输出信息如下:
只有一行“kernel recevdata:usr data!”,这个是驱动程序中的 chrdevbase_write 函数输出的,说明对 chrdevbase 的写操作正常,既然读写都没问题,说明我们编写的 chrdevbase 驱动是没有问题的。
(6) 当驱动不需要使用时,可以使用以下命令卸载该驱动程序:
rmmod chrdevbase