字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。常见的点灯、按键、IIC、SPI、LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
Linux应用程序对驱动程序的调用流程如下。
①应用程序调用库函数提供的open()函数打开某个设备文件;
②库根据open()函数的输入参数引起CPU异常,进入内核;
③内核的异常处理函数根据输入参数找到相应的驱动程序,返回文件句柄给库,库函数再返回给应用程序;
④应用程序再使用得到的文件句柄调用write()、read()等函数发出控制指令;
⑤库根据write()、read()等函数的输入参数引起CPU异常,进入内核;
⑥内核的异常处理函数根据输入参数调用相应的驱动程序执行相应的操作。
在Linux中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个文件进行相应的操作即可实现对硬件的操作。
例如/dev/led的驱动文件,此文件是led灯的驱动文件,应用程序使用open函数来打开文件/dev/led,使用完成以后使用close函数关闭/dev/led这个文件。open和close就是打开和关闭led驱动的函数,如果要点亮或关闭led,那么就使用write函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开led的控制参数,如果要获取led灯的状态,就用read函数从驱动中读取相应的状态。
应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open函数打开/dev/led这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用系统调用来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write和read等这些函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。
open函数调用流程如下。
应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了open这个函数,那么在驱动程序中也得有一个名为open的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数。
在Linux内核文件include/linux/fs.h中有个名为file_operations(第1588行)的结构体,此结构体就是Linux内核驱动操作函数集合。
struct file_operations {
struct module *owner; //owner拥有该结构体的模块的指针,一般设置为THIS_MODULE
loff_t (*llseek) (struct file *, loff_t, int); //llseek函数用于修改文件当前的读写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //read函数用于读取设备文件
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //write函数用于向设备文件写入(发送)数据
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 *);
unsigned int (*poll) (struct file *, struct poll_table_struct *); //poll是个轮询函数,用于查询设备是否可以进行非阻塞的读写
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //unlocked_ioctl函数提供对于设备的控制功能,与应用程序中的ioctl函数对应
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); //compat_ioctl函数与unlocked_ioctl函数功能一样,区别在于在64位系统上,32位的应用程序调用将会使用此函数,在32位的系统上运行32位的应用程序调用的是unlocked_ioctl
int (*mmap) (struct file *, struct vm_area_struct *); //mmap函数用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如LCD驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *); //open函数用于打开设备文件
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); //release 函数用于释放(关闭)设备文件,与应用程序中的close函数对应
int (*fsync) (struct file *, loff_t, loff_t, int datasync); //fsync函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
int (*aio_fsync) (struct kiocb *, int datasync); //aio_fsync函数与fasync函数的功能类似,只是aio_fsync是异步刷新待处理的数据
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
};
该结构体中包含的是一些函数的原型,在使用到的时候直接复制到代码里面修改即可。
Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用insmod命令加载驱动模块。 在调试驱动的时候一般都选择将其编译为模块,这样修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码,而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux内核中,当然也可以不编译进Linux内核。
模块有加载和卸载两种操作,模块的加载和卸载注册函数如下。
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候,xxx_init这个函数就会被调用。module_exit()函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit函数就会被调用。字符设备驱动模块加载和卸载模板如下。
#include //包含宏定义的头文件
#include //包含初始化加载模块的头文件
//驱动入口函数
static int __init xxx_init(void)
{
//入口函数具体内容
return 0;
}
//驱动出口函数
static void __exit xxx_exit(void)
{
//出口函数具体内容
}
//将上面两个函数指定为驱动的入口和出口函数
module_init(xxx_init);
module_exit(xxx_exit);
加载驱动模块有两种命令,insmod和modprobe,insmod命令不能解决模块的依赖关系,如果一个模块依赖于另一个模块,就需要先加载被依赖的模块,再加载另一个模块,而modprobe会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此modprobe命令相比insmod要智能一些。modprobe命令主要智能在提供了模块的依赖性分析、错误检查、错误报告等功能,推荐使用modprobe命令来加载驱动。
卸载驱动可以使用rmmod和modprobe -r命令,使用modprobe命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe来卸载驱动模块,所以对于模块的卸载,还是推荐使用rmmod命令。
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备,字符设备的注册和注销函数原型如下。
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)
register_chrdev函数用于注册字符设备,major是主设备号,name是设备名字,指向一串字符串,fops指向设备操作函数集合的变量。
unregister_chrdev函数用于注销字符设备,major是主设备号,name是要注销设备的设备名称。
一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。
#include //包含宏定义的头文件
#include //包含初始化加载模块的头文件
static struct file_operations test_fops;
//驱动入口函数
static int __init xxx_init(void)
{
//入口函数具体内容
int retvalue = 0;
retvalue = register_chrdev(200,"chrtest",&test_fops); //注册字符设备驱动
if(retvalue < 0)
{
//打印或相应处理
}
return 0;
}
//驱动出口函数
static void __exit xxx_exit(void)
{
//出口函数具体内容
unregister_chrdev(200, "chrtest"); //注销字符设备驱动
}
//将上面两个函数指定为驱动的入口和出口函数
module_init(xxx_init);
module_exit(xxx_exit);
file_operations结构体就是设备的具体操作函数,上面定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的open、release、read和write等具体的设备操作函数。下面代码中主要就是打开、关闭、读写操作。
#include //包含宏定义的头文件
#include //包含初始化加载模块的头文件
static int chrtest_open (struct inode *inode, struct file *filp) //打开设备
{
//具体功能实现
return 0;
}
static ssize_t chrtest_read (struct file *filp, char __user *buf, size_t cnt, loff_t *offt) //从设备读取
{
//具体功能实现
return 0;
}
static ssize_t chrtest_write (struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) //向设备写数据
{
//具体功能实现
return 0;
}
static int chrtest_release (struct inode *inode, struct file *filp) //关闭设备
{
//具体功能实现
return 0;
}
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release
};
//驱动入口函数
static int __init xxx_init(void)
{
//入口函数具体内容
int retvalue = 0;
retvalue = register_chrdev(200,"chrtest",&test_fops); //注册字符设备驱动
if(retvalue < 0)
{
//打印或相应处理
}
return 0;
}
//驱动出口函数
static void __exit xxx_exit(void)
{
//出口函数具体内容
unregister_chrdev(200, "chrtest"); //注销字符设备驱动
}
//将上面两个函数指定为驱动的入口和出口函数
module_init(xxx_init);
module_exit(xxx_exit);
最后在驱动程序中需要添加LICENSE信息和作者信息,其中LICENSE信息必不可少,作者信息可有可无。LICENSE和作者信息的添加使用如下两个函数。
MODULE_LICENSE("GPL");
MODULE_AUTHOR("forlinx");
为了方便管理,Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。dev_t的数据类型表示设备号,共32位,高12位为主设备号,低20位为次设备号,所以Linux系统中的主设备号范围是0-4095,在选择的时候不要超过。
include/linux/kdev_t.h 文件中中提供了几个关于设备号的操作函数,其本质是宏。
#define MINORBITS 20 //表示次设备号位数
#define MINORMASK ((1U << MINORBITS) - 1) //表示次设备号掩码,1U代表该无符号整型的值为1
//1U << MINORBITS : 0000_0000_0001_0000_0000_0000_0000_0000
//(1U << MINORBITS) - 1 : 0000_0000_0000_1111_1111_1111_1111_1111
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //从dev_t中获取主设备号,将dev_t右移20位即可
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //从dev_t中获取次设备号,取dev_t的低20位的值即可
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //将给定的主设备号和次设备号的值组合成dev_t类型的设备号
静态分配设备号就像前面的一样,在注册字符设备的时候指定一个设备号,动态分配设备号在注册字符设备之前先申请一个设备号,系统会自动给一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev用于保存申请到的设备号,baseminor是次设备号的起始地址,count是要申请的设备号数量,name是设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下。
void unregister_chrdev_region(dev_t from, unsigned count)
from是要释放的设备号,count表示从from开始,要释放的设备号数量。
新字符设备驱动下,设备号分配示例代码如下。
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);
}
注销设备号的代码如下。
unregister_chrdev_region(devid, 1);
在Linux中使用cdev结构体表示一个字符设备,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;
};
初始化函数原型。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
添加字符设备函数原型。
int cdev_add(struct cdev *p, dev_t dev, unsigned count) //dev是设备所使用的设备号,count是要添加的设备数量
删除字符设备函数原型。
void cdev_del(struct cdev *p)
文件操作有关的open、close、read、write函数介绍如下。
open函数原型如下。
int open(const char *pathname, int flags)
open函数的参数pathname表示要打开的设备或者文件名称,flags是文件打开模式,有只读(O_RDONLY)、只写(O_WRONLY)、读写(O_RDWR)三种。执行open函数,如果文件打开成功的话返回文件的文件描述符。
close函数原型如下。
int close(int fd)
close函数只携带一个参数fd,表示的是要关闭的文件描述符。返回值为0表示关闭成功,复制表示关闭失败。
read函数原型如下。
ssize_t read(int fd, void *buf, size_t count)
read函数中, fd表示要读取的文件描述符,读取文件之前要先用open函数打开文件,open函数打开文件成功以后会得到文件描述符。buf用来存放读取到的数据,count表示要读取的数据长度,即字节数。读取成功的话返回读取到的字节数,如果返回0表示读取到了文件末尾,如果返回负值,表示读取失败。
write函数原型如下。
ssize_t write(int fd, const void *buf, size_t count)
write函数中, fd表示要进行写操作的文件描述符,写文件之前要先用open函数打开文件,open函数打开文件成功以后会得到文件描述符。buf用来存放要写入的数据,count表示要写入的数据长度,即字节数。写入成功的话返回写入的字节数,如果返回0表示没有写入任何数据,如果返回负值,表示写入失败。
内核空间不能直接操作用户空间的内存,因此需要借助copy_to_user函数来完成内核空间的数据到用户空间的复制。
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
参数to表示目的,参数from表示源,参数n表示要复制的数据长度,如果复制成功,返回值为0,如果复制失败则返回负数。
copy_from_user函数的原型如下。
copy_from_user(void *to, const void __user *from, unsigned long n)
这里用的源文件和测试代码都是正点原子的,我只做了一点修改!
chrdev.c源代码如下。
#include
#include
#include
#include
#include
#include
#define CHRDEV_MAJOR 200 // 主设备号
#define CHRDEV_NAME "chrdev" // 设备名
static char readbuf[100]; // 读缓冲区
static char writebuf[100]; // 写缓冲区
static char kerneldata[] = {"abcdefghi"};
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量,一般在open的时候将private_data指向设备结构体
* @return : 0 成功;其他 失败
*/
static int chrdev_open(struct inode *inode, struct file *filp)
{
printk("chrdev open!\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
//向用户空间发送数据
memcpy(readbuf, kerneldata, sizeof(kerneldata)); //将数组kerneldata中的数据复制到读缓冲区readbuf
retvalue = copy_to_user(buf, readbuf, cnt); //内核空间不能直接操作用户空间的内存,需要借助该函数完成内核空间复制数据到用户空间
if(retvalue == 0)
{
printk("kernel send data : %s\r\n",readbuf);
}
else
{
printk("kernel send data failed!\r\n");
}
//printk("chrdev read!\r\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
//接收用户空间传递给内核的数据并且打印出来
retvalue = copy_from_user(writebuf, buf, cnt); //将buf中的数据复制到写缓冲区writebuf中,同样地,用户空间内存不能直接访问内核空间内存
if(retvalue == 0)
{
msleep(1000);
printk("kernel receive data : %s\r\n", writebuf);
}
else
{
printk("kernel receive data failed!\r\n");
}
//printk("chrdev write!\r\n");
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdev_release(struct inode *inode, struct file *filp)
{
printk("chrdev release!\r\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdev_init(void)
{
int retvalue = 0;
//注册字符设备驱动
retvalue = register_chrdev(CHRDEV_MAJOR, CHRDEV_NAME, &chrdev_fops);
if(retvalue < 0)
{
printk("chrdev driver register failed!\r\n");
}
printk("chrdev init!\r\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdev_exit(void)
{
//注销字符设备驱动
unregister_chrdev(CHRDEV_MAJOR, CHRDEV_NAME);
printk("chrdev exit!\r\n");
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdev_init);
module_exit(chrdev_exit);
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("forlinx");
需要注意的是,Linux内核中没有printf这个函数,printk相当于printf的孪生兄妹,printf运行在用户态,printk运行在内核态。
本例中Makefile文件的代码如下。
obj-m := chrdev.o #将chrdev.c源文件编译为chrdev.ko模块
KERNEL_DIR := /home/lyx/linux_kernel #Linux内核源码路径,解压Linux内核后的存放路径
CURRENT_PATH := $(shell pwd) #当前路径
all:
#-C表示将当前工作目录切换到指定目录中,M表示源码的目录,modules表示编译模块
make -C $(KERNEL_DIR) M=$(CURRENT_PATH) modules
clean:
#删除编译过程中生成的文件
make -C $(KERNEL_DIR) M=$(CURRENT_PATH) clean
#rm *.o *.ko *.symvers *.mod.c *.order
obj-m表示把文件chrdev.o作为模块进行编译,不会编译到内核,但是会生成一个独立的 “chrdev.ko” 文件;obj-y表示把chrdev.o文件编译进内核。
测试代码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[] = {"123456789"};
/*
* @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];
if(argc != 3) //用户需要输入三参数,输入示例: ./app /dev/chrdev 1
{
printf("Error input!\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) //第三个参数为1,从驱动文件读取数据,atoi函数将字符串格式的数字转换为数字格式
{
retvalue = read(fd,readbuf,10); //读取10字节的数据,读取到的数据存放在readbuf中
if(retvalue < 0)
{
printf("read file %s failed!\r\n", filename);
}
else
{
printf("user receive data : %s\r\n",readbuf); //打印出读取成功的数据
}
}
else if(atoi(argv[2]) == 2) //第三个参数为2,向设备驱动写数据
{
memcpy(writebuf,usrdata,sizeof(usrdata)); //先读取要写的内容到writebuf中
retvalue = write(fd,writebuf,10);
if(retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
}
else
{
printf("user send data : %s \r\n", writebuf);
}
}
else
{
printf("Nothing to do!\r\n");
}
sleep(1); //确保app的测试信息打印完再关闭设备
retvalue = close(fd); //关闭字符设备
if(retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
有了上面的源文件和Makefile,将这两个文件放在同一个文件夹下,直接使用make命令生成chrdev.ko驱动文件,这里ko表示kernel object。
app.c代码直接使用交叉编译器编译即可,编译命令如下。
arm-linux-gnueabihf-gcc app.c -o app
编译完成后文件夹下包含的所有文件如下。
将chrdev.ko文件和app文件发送到开发板进行验证。
使用下面的命令先看一下系统中现有的字符设备。
cat /proc/devices
可以看到,我们在代码中使用的主设备号200在这里是没有的,所以提前在这里查看一下,选择一个没有被使用的主设备号使用。
开发板上收到驱动文件后,使用下面的命令加载驱动文件。
insmod chrdev.ko
驱动加载成功以后再看一下/proc/devices下存在的设备,我们主设备号为200的字符设备就出现了,如下图所示。
接下来要创建设备节点文件,在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。创建设备节点文件的命令如下。
mknod /dev/chrdev c 200 0
mknod是创建节点命令,/dev/chrdev是要创建的节点文件,c表示这是个字符设备,200是主设备号,0是该设备的次设备号。使用该命令以后就会在/dev目录下创建一个名为chrdev的文件,这时候就可以通过测试文件app对/dev/chrdev进行读写操作了。
创建设备节点chrdev文件之前/dev目录下的文件。
创建设备节点chrdev文件之后/dev目录下的文件,可以看到chrdev文件已经存在了。
到这里,我们的字符设备就创建完成了,接下来使用测试程序app进行读写验证。
这是我刚开始测试时打印的信息,发现测试程序app中的信息没有打印,如下图所示。
一开始我以为是代码哪里出了问题,所以才会有上面的问题,后来发现可能是字符设备关闭发生在测试程序app打印信息之前,所以在代码中通过延时一定的时间再关闭字符设备以确保信息打印,通过测试,这样操作可以得到自己想要的输出。
开发板上驱动的加载、测试过程和卸载的执行过程如下图所示。
通过上面的测试结果,字符设备驱动的加载卸载以及读写测试都是正确的。
本文参考文档:
I.MX6U嵌入式Linux驱动开发指南V1.5——正点原子