虫虫首页|资源下载|资源专辑|精品软件
登录|注册

您现在的位置是:首页 > 技术阅读 >  linux驱动开发(2、第一个字符设备驱动)

linux驱动开发(2、第一个字符设备驱动)

时间:2024-06-01

文章目录

  • 设备号

  • 常用接口

    • 设备号申请/释放

      • register_chrdev_region函数

      • alloc_chrdev_region函数

      • unregister_chrdev_region函数

      • 其它宏定义

    • 字符设备的定义

      • cdev_alloc函数

    • 字符设备的绑定

      • cdev_init函数

    • 字符设备注册/注销

      • cdev_add函数

      • cdev_del函数

    • file_operations结构

  • 实现一个简单的字符设备驱动

  • 以前的接口

    • register_chrdev函数

    • unregister_chrdev函数

  • 测试流程及结果

  • 总结


点击下方阅读原文可访问文中超链接

设备号

设备号(统称,由主设备号和次设备号组成)是一种资源,更是应用层与驱动层之间的纽带,设备号由主设备号和次设备号组成,主设备号用于区分是哪类设备,次设备号用于区分某一类设备中的各个子设备;设备号的分配规则是从大往小分配,其中某些设备号是系统已经约定好了的预分配给指定设备;
当向内核申请了设备号后,就可以在/proc/devices文件中看到对应的主设备号及设备名,此时使用mknod也可以在/dev目录生成设备节点,不过这个节点是个空节点,没有任何用;设备号被注销后,/proc/devices中也会自动删除,如下:

常用接口

设备号申请/释放

位于头文件:include/linux/fs.h

register_chrdev_region函数

向内核申请指定的设备号,如果指定的设备号已存在则会申请失败。

int register_chrdev_region(dev_t, unsigned, const char *);

示例用法:

//MKDEV是一个宏,用于将主次设备号合并成设备号
//第二个参数表示申请多少个设备号
//第三个参数是设备名
register_chrdev_region(MKDEV(247,0),1,"test_char_dev");

alloc_chrdev_region函数

向内核申请设备号,并且由内核自动分配可用的设备号。

int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

示例用法:

//devno存储申请的设备号
//第二个参数表示次设备号的起始值
//第三个参数表示申请多少个设备号
//第四个参数是设备名
alloc_chrdev_region(&devno,0,1,"test_char_dev");

unregister_chrdev_region函数

用于注销由register_chrdev_regionalloc_chrdev_region申请的设备号,前面也说了设备号是一种资源,所以要养成良好的习惯,不用就要归还。

void unregister_chrdev_region(dev_t, unsigned);

示例用法:

//devno申请的设备号
//第二个参数表示申请的个数,申请了多少就要注销多少
unregister_chrdev_region(devno,1);

其它宏定义

位于头文件:include/linux/kdev_t.h
用于提取设备号的主设备号。

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))

用于提取设备号的次设备号。

#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

字符设备的定义

cdev_alloc函数

位于头文件:include/linux/cdev.h
使用动态内存的方式定义一个字符设备。

struct cdev *cdev_alloc(void);

或者直接定义为静态全局变量

static struct cdev chr_dev;

字符设备的绑定

cdev_init函数

将一个字符设备与file_operations结构绑定起来。

void cdev_init(struct cdev *, const struct file_operations *);

字符设备注册/注销

cdev_add函数

向内核注册字符设备。

int cdev_add(struct cdev *, dev_t, unsigned);

cdev_del函数

注销一个字符设备。

void cdev_del(struct cdev *);

file_operations结构

位于头文件:include/linux/fs.h
这个结构体包含了一系列的函数指针,基本上每一个函数指针都对应了应用层一个接口函数,比如调用应用层的open函数时,最终就会调用到这个结构体里面的open函数指针。我们就可以通过实现一些需要的函数来操作硬件。
示例代码:

static int test_open (struct inode *inode, struct file *file)
{
return 0;
}

static int test_close (struct inode *inode, struct file *file)
{
return 0;
}

static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};

实现一个简单的字符设备驱动

一个简单的字符设备驱动大致分为以下几个步骤:

  • 向内核申请设备号

  • 定义一个字符设备对象

  • 绑定字符设备对象与file_operations结构

  • 向内核注册字符设备

  • 实现file_operations结构体内的部分接口

简单源码如下(注意未实现各种错误及边界检查,实际应用时应完善,以保证程序的健壮性):

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define DEBUG(fmt, ...) \
do{ \
if(if_debug) \
printk(KERN_INFO "DEBUG > " fmt, ##__VA_ARGS__); \
}while(0)


bool if_debug = false;
/*定义一个bool类型的变量,作为一个模块入参,在装载模块时可赋值*/
module_param(if_debug, bool, S_IRUSR);

#define DEV_COUNT 1

/*设备号*/
static dev_t devno;
/*定义一个字符设备*/
static struct cdev *char_dev;

static int test_open (struct inode *inode, struct file *file)
{
DEBUG("open test..\r\n");

return 0;
}

static int test_close (struct inode *inode, struct file *file)
{
DEBUG("close test..\r\n");

return 0;
}

static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};

static int __init test_init(void)
{
/*申请设备号*/
alloc_chrdev_region(&devno,0,DEV_COUNT,"test_char_dev");
/*使用动态内存定义一个字符设备*/
char_dev = cdev_alloc();
/*绑定字符设备和file_operations结构*/
cdev_init(char_dev,&fops);
/*注册字符设备*/
cdev_add(char_dev,devno,DEV_COUNT);

return 0;
}

static void __exit test_exit(void)
{
/*注销一个字符设备*/
cdev_del(char_dev);
/*注销申请的设备号*/
unregister_chrdev_region(devno,DEV_COUNT);
}

/*此宏声明内核模块的初始化入口点*/
module_init(test_init);
/*此宏声明内核模块的退出入口点*/
module_exit(test_exit);

/*声明开源协议*/
MODULE_LICENSE("GPL");
/*声明作者*/

MODULE_AUTHOR("wei");
/*声明模块的描述*/
MODULE_DESCRIPTION("this is a test driver");

以前的接口

同样是创建字符设备,但是内核还有两个接口,可以大大简化上面的代码。

register_chrdev函数

此函数可一步到位,完成了设备号的申请,字符设备与file_operations结构的绑定以及字符设备的注册全部操作,只不过这个函数会自动申请256个主设备号相同的设备号(次设备号为0到255),如果没有这么多同类子设备的话会造成空间的浪费

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)

示例代码:

//第一个参数为主设备号,如果为0则内核自动分配
//第二个参数为设备名
//第三个参数为file_operations结构体
register_chrdev(247,"test_char_dev",fops);

unregister_chrdev函数

用于注销由register_chrdev申请的资源。

void unregister_chrdev(unsigned int major, const char *name)

示例用法:

//第一个参数为主设备号
//第二个参数为设备名
unregister_chrdev(247,"test_char_dev");

第二种更简洁的写法:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

#define DEBUG(fmt, ...) \
do{ \
if(if_debug) \
printk(KERN_INFO "DEBUG > " fmt, ##__VA_ARGS__); \
}while(0)


bool if_debug = false;
/*定义一个bool类型的变量,作为一个模块入参,在装载模块时可赋值*/
module_param(if_debug, bool, S_IRUSR);

#define CHRDEV_NAME "test_char_dev"
/*主设备号*/
static dev_t major;

static int test_open (struct inode *inode, struct file *file)
{
DEBUG("open test..\r\n");

return 0;
}

static int test_close (struct inode *inode, struct file *file)
{
DEBUG("close test..\r\n");

return 0;
}

static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_close,
};

static int __init test_init(void)
{
major = register_chrdev(0,CHRDEV_NAME,&fops);

return 0;
}

static void __exit test_exit(void)
{
unregister_chrdev(major,CHRDEV_NAME);
}

/*此宏声明内核模块的初始化入口点*/
module_init(test_init);
/*此宏声明内核模块的退出入口点*/
module_exit(test_exit);

/*声明开源协议*/
MODULE_LICENSE("GPL");
/*声明作者*/
MODULE_AUTHOR("wei");
/*声明模块的描述*/
MODULE_DESCRIPTION("this is a test driver");

驱动写好后,还需要让应用程序能够使用,而应用程序是通过设备节点来访问的,所以接下来创建设备节点,以让应用程序可以访问写好的驱动。使用mknod命令创建设备节点:

mknod 设备名 设备类型 主设备号 次设备号

示例用法:

mknod /dev/test_char_dev c 247 0

此程序没有实现任何的硬件操作,下面写个简单的应用程序测试一下驱动是否能正常工作,因为只实现了openrelease接口,所以在应用程序中也只能使用这两个接口。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <stdio.h>

int main(int argc,char *argv[])
{
int fd;

fd = open("/dev/test_char_dev",O_RDONLY);
if(fd < 0)
{
perror("open");

return -1;
}

close(fd);

return 0;
}

测试流程及结果

# 装载驱动
insmod test.ko if_debug=1
# 查看申请的设备号
cat /proc/devices
# 创建设备节点(主设备号由上条命令查看,次设备号与驱动程序一致)
mknod /dev/test_char_dev c 247 0
# 执行应用程序
./app_test
# 运行结果会打印出驱动程序中的两个调试信息
DEBUG > open test..
DEBUG > close test..
# 卸载驱动
rmmod test.ko
# 删除设备节点
rm /dev/test_char_dev

总结

我根据自己的理解,画了一个关系图。

测试源码获取:点我