Linux字符设备驱动开发

news2024/12/22 20:33:13

文章目录

  • 字符设备简单介绍
  • file_operations结构体
  • 驱动编译为模块
  • 字符设备注册与注销
  • 设备的操作函数初始化
  • 添加LICENSE和作者信息
  • 设备号的分配
  • 文件操作函数
  • 字符设备驱动示例
    • 源文件chrdev.c
    • Makefile文件
    • 测试代码app.c
    • 编译
    • 开发板上验证

字符设备简单介绍

字符设备是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的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数。


file_operations结构体

在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 <linux/init.h>   //包含宏定义的头文件
#include <linux/module.h> //包含初始化加载模块的头文件

//驱动入口函数
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 <linux/init.h>   //包含宏定义的头文件
#include <linux/module.h> //包含初始化加载模块的头文件

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 <linux/init.h>   //包含宏定义的头文件
#include <linux/module.h> //包含初始化加载模块的头文件
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信息必不可少,作者信息可有可无。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

这里用的源文件和测试代码都是正点原子的,我只做了一点修改!
chrdev.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>

#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文件

本例中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

测试代码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——正点原子

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1039410.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lua函数

--函数--无参无返回值 function F1()print("F1函数") end F1() print("*****************")--有参 function F2(a)print("F2函数"..a) end F2(2) --如果传入参数和函数数量不一致 --不会报错只是补空 F2(1,2) print("*****************&quo…

iOS应用程序的签名、重签名和安装测试

目录 前言 打开要处理的IPA文件 设置签名使用的证书和描述文件 开始ios ipa重签名 前言 ipa编译出来后&#xff0c;或者ipa进行修改后&#xff0c;需要进行重新签名才能安装到测试手机&#xff0c;或者提交app store供apple 商店审核上架。ipaguard有签名和重签名功能&…

9.21广读最新arxiv论文 思路学习汇总

Towards Generative Modeling of Urban Flow through Knowledge-enhanced Denoising Diffusion 摘要&#xff1a;尽管生成式人工智能在许多领域取得了成功&#xff0c;但在建模地理空间数据方面的潜力仍尚未充分发掘。城市流动&#xff0c;是一种典型的地理空间数据&#xff0c…

单列集合顶层接口Collection

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 集合体系结构 一、单列集合顶层接口Collect…

机器学习小白理解之一元线性回归

关于机器学习&#xff0c;百度上一搜一大摞&#xff0c;总之各有各的优劣&#xff0c;有的非常专业&#xff0c;有的看的似懂非懂。我作为一名机器学习的门外汉&#xff0c;为了看懂这些公式和名词真的花了不少时间&#xff0c;还因此去着重学了高数。 不过如果不去看公式&…

软件推荐:wiztree

简要介绍 近期C盘占满&#xff0c;找到了这款优秀的软件wiztree。 wiztree称得上最快的磁盘空间分析器&#xff0c;界面简洁明晰&#xff0c;操作简单快捷&#xff0c;无广告。 下载地址&#xff1a;https://www.diskanalyzer.com/download 软件截图

Opencv cuda版本在ubuntu22.04中安装办法,解决Could NOT find CUDNN的办法

文章目录 概要下载cuda的runfile版本配置环境变量官网下载cudann安装Opencv依赖包下载opencv和opencv_contrib并解压准备编译安装anaconda环境执行编译命令安装OpenCV并检查是否安装成功 概要 解决以下安装问题&#xff1a; -- Could NOT find CUDNN: Found unsuitable versi…

OpenCascade绘制贝塞尔曲线

贝塞尔曲线有着很多特殊的性质, 在图形设计和路径规划中应用都非常广泛。 贝塞尔曲线完全由其控制点决定其形状, &#xff4e;个控制点对应着&#xff4e;&#xff0d;&#xff11;阶的贝塞尔曲线&#xff0c;并且可以通过递归的方式来绘制。 一阶: 二阶: 高阶&#xff1a; …

雷士、书客、小米的护眼台灯谁的性价比最高?三款护眼台灯真实测评

护眼台灯怎么选一直是许多家长为孩子选台灯时的一个大难题&#xff0c;主要因为市场上的台灯种类太多&#xff0c;而且这些产品中混杂了许多不专业品牌&#xff0c;甚至包括许多劣质台灯和网红品牌&#xff01;同时也经常能够看到报道很多“抽检不合格”的情况发生&#xff0c;…

S08-如何冻结表格行与列

通常表格第一行或第一列的数据都是数据归类的标题 所以比较常用到的是冻结首行首列 具体操作是点击菜单栏的“开始”-“冻结窗格”“冻结首行”

图像语义分割 FCN图像分割网络网络详解

图像语义分割 FCN图像分割网络网络详解 0、介绍1、VGG16网络结构2、转置卷积3、FCN-32S、FCN-16S&#xff0c;FCN-8S网络结构4、损失函数5、膨胀卷积6、FCN(Backbone-ResNet-50)6.1 项目框架6.2 ResNet50网络结构6.3 FCN(Backbone-ResNet-50)网络结构6.4 FCN(Backbone-ResNet-5…

关于接口测试——自动化框架的设计与实现

一、自动化测试框架 在大部分测试人员眼中只要沾上“框架”&#xff0c;就感觉非常神秘&#xff0c;非常遥远。大家之所以觉得复杂&#xff0c;是因为落地运用起来很复杂&#xff1b;每个公司&#xff0c;每个业务及产品线的业务流程都不一样&#xff0c;所以就导致了“自动化…

LVS和keepalived

Keepalived及其工作原理 Keepalived 是一个基于VRRP协议来实现的LVS服务高可用方案&#xff0c;可以解决静态路由出现的单点故障问题。 在一个LVS服务集群中通常有主服务器&#xff08;MASTER&#xff09;和备份服务器&#xff08;BACKUP&#xff09;两种角色的服务器&#x…

怎么自制gif动画?简单一招快速搞定

众所周知gif动图的画面非常的丰富生动&#xff0c;并且体积小传播方便&#xff0c;在当下的网络中是非常的受欢迎。那么&#xff0c;这种gif格式的图片是怎么制作的呢&#xff1f;下面&#xff0c;给大家分享一款专业的gif动态图片制作&#xff08;https://www.gif.cn/&#xf…

Ubuntu 安装PostgreSQL

网上有各种版本的&#xff0c;也可以去官网看官方的文档。我是下载的PostgreSQL-11.4版本的。找到以后直接复制网上的压缩包链接就可以。 $ mkdir /opt/postgresql && cd /opt/postgresql $ wget https://ftp.postgresql.org/pub/source/v11.4/postgresql-11.4.tar.gz…

如何计算3种卷积之后的尺寸(普通卷积,转置卷积,空洞卷积)

文章目录 前言一、普通卷积二、转置卷积三、空洞卷积 前言 三种卷积之后的feature map的尺寸如何计算。包括普通卷积&#xff0c;转置卷积&#xff0c;空洞卷积。可以在下面这个链接看到三种卷积的动态图。 卷积动态图 一、普通卷积 普通卷积比较简单了&#xff0c;其计算方式…

由于找不到msvcr110.dll 无法继续执行的解决方法分享(最新)

msvcp110.dll 是 Microsoft Visual C 2010 Redistributable Package 中的一个组件&#xff0c;它包含了一些运行时库文件。当计算机缺少这个文件时&#xff0c;可能会出现一些问题&#xff0c;如程序无法正常运行、系统不稳定等。下面是 6 种修复方法&#xff1a; 第1种方法&am…

内网穿透的应用-结合内网穿透实现在线远程Linux DataEase,数据可实时进行可视化分析

文章目录 前言1. 安装DataEase2. 本地访问测试3. 安装 cpolar内网穿透软件4. 配置DataEase公网访问地址5. 公网远程访问Data Ease6. 固定Data Ease公网地址 前言 DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务…

华为小型智能园区网络解决方案

云时代来袭&#xff0c;数字化正在从园区办公延伸到生产和运营的方方面面&#xff0c;智慧校园&#xff0c;柔性制造&#xff0c;掌上金融和电子政务等&#xff0c;面对各种各样的新兴业态的涌现&#xff0c;企业需要构建一张无所不联、随心体验、业务永续的全无线网络&#xf…

数据采集技术在MES管理系统中的应用及效果

在现代制造业中&#xff0c;MES生产管理系统已成为生产过程中不可或缺的一部分。MES管理系统能够有效地将生产计划、生产执行、质量管理等各个生产环节有机地衔接起来&#xff0c;从而实现生产过程的全面优化。本文将以某车间为例&#xff0c;探讨结合MES系统的数据采集技术的应…