Linux驱动开发:字符设备驱动开发理论

嵌入式技术

1332人已加入

描述

大部分学习者的最终目的就是学习 Linux驱动开发,Linux中的外设驱动可以分为:字符设备驱动、块设备驱动和网络设备驱动。

Linux

| 字符设备驱动简介

字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

Linux 应用程序对驱动程序的调用流程

Linux

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。 比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态。

应用程序运行在用户空间,而Linux驱动属于内核的一部分,因此驱动运行于内核空间。当应用层通过open函数打开/dev/led 这个驱动时,因用户空间不能直接操作内核,因此会使用“系统调用”的方法来从用户空间“陷入”到内核空间,实现对底层驱动的操作。

Linux

比如应用程序调用了open这个函数,则在驱动程序中也应有一个对应的open的函数。

Linux内核驱动操作函数

每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合:

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 *, constchar __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 *);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  long (*unlocked_ioctl) (struct file *, unsignedint, unsignedlong);
  long (*compat_ioctl) (struct file *, unsignedint, unsignedlong);
  int (*mmap) (struct file *, struct vm_area_struct *);
  int (*mremap)(struct file *, struct vm_area_struct *);
  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 (*aio_fsync) (struct kiocb *, int datasync);
  int (*fasync) (int, struct file *, int);
  /*省略若干行...*/
};
其中常用的函数有以下这几种:

 

owner:拥有该结构体的模块的指针,一般设置为THIS_MODULE。

llseek函数:用于修改文件当前的读写位置。

read函数:用于读取设备文件。

write函数:用于向设备文件写入(发送)数据。

poll函数:是个轮询函数,用于查询设备是否可以进行非阻塞的读写。

unlocked_ioctl函数:提供对于设备的控制功能, 与应用程序中的 ioctl 函数对应。

compat_ioctl函数:与 unlocked_ioctl功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl。

mmap函数:用于将将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数, 比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。

open函数:用于打开设备文件。

release函数:用于释放(关闭)设备文件,与应用程序中的 close 函数对应。

fasync函数:用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。

aio_fsync函数:与fasync功能类似,只是 aio_fsync 是异步刷新待处理的

Linux 驱动有两种运行方式

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在驱动开发阶段一般都将其编译为模块,不需要编译整个Linux代码,方便调试驱动程序。当驱动开发完成后,根据实际需要,可以选择是否将驱动编译进Linux内核中。 Linux 设备号 为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:

typedef __u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;
dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。 在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1) 8 


#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

 

MINORBITS:表示次设备号位数,一共20位

MINORMASK:表示次设备号掩码

MAJOR:用于从dev_t中获取主设备号,将dev_t右移20位即可

MINOR:用于从dev_t中获取次设备号,取dev_t的低20位的值即可

MKDEV:用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号

设备号的分配有两种方式,第一种是静态分配设备号,需要开发者手动指定设备号,并且要注意不能与已有的重复,一些常用的设备号已经被Linux内核开发者给分配掉了,使用“cat /proc/devices”命令可查看当前系统中所有已经使用了的设备号。;第二种是动态分配设备号,静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。 设备号的申请函数

/*
dev:保存申请到的设备号。
baseminor:次设备号起始地址,一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  设备号释放函数
/*
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。
*/
void unregister_chrdev_region(dev_t from, unsigned count)

 

| 字符设备驱动开发模板

加载与卸载

在编写驱动的时候需要注册模块加载和卸载这两种函数:

 

module_init(xxx_init);   //注册模块加载函数 
module_exit(xxx_exit);   //注册模块卸载函数

 

module_init()用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用 “insmod” 命令加载驱动的时候,xxx_init这个函数就会被调用。

module_exit()用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候 xxx_exit函数就会被调用。

字符设备驱动模块加载和卸载模板如下所示:

 

/* 驱动入口函数 */
static int __init xxx_init(void) 
{ 
  /*入口函数内容 */
  return0; 
} 


/* 驱动出口函数 */
static void __exit xxx_exit(void) 
{ 
  /*出口函数内容*/
} 


/*指定为驱动的入口和出口函数 */
module_init(xxx_init); 
module_exit(xxx_exit);

 

驱动编译完成以后扩展名为.ko, 有两种命令可以加载驱动模块:

insmod:最简单的模块加载命令,用于加载指定的.ko模块,此命令不能解决模块的依赖关系

modprobe:该命令会分析模块的依赖关系,将所有的依赖模块都加载到内核中,因此更智能

modprobe 命令默认会去/lib/modules/目录中查找模块(自制的根文件系统没有这个目录,需要手动创建)

卸载驱动也有两种命令:

rmmod:例如使用rmmod drv.ko来卸载 drv.ko这一个模块

modprobe -r:该命令除了卸载指定的驱动,还卸载其所依赖的其他模块,若这些依赖模块还在被其它模块使用,就不能使用 modprobe来卸载驱动模块!  

注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。

字符设备的注册函数原型如下所示:

 

/*
major:主设备号
name:设备名字,指向一串字符串
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量
*/
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)

 


字符设备的注销函数原型如下所示:

 

/*
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
*/
static inline void unregister_chrdev(unsigned int major, const char *name)

 

一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

 

staticstruct file_operations test_fops;


/* 驱动入口函数 */
static int __init xxx_init(void) 
{ 
  /* 入口函数具体内容 */
  int retvalue = 0; 
  /* 注册字符设备驱动 */
  retvalue = register_chrdev(200, "chrtest", &test_fops); 
  if(retvalue < 0)
    { 
    /*  字符设备注册失败, 自行处理 */
  } 
  return0; 
} 


/* 驱动出口函数 */
static void __exit xxx_exit(void) 
{ 
  /* 注销字符设备驱动 */
  unregister_chrdev(200, "chrtest"); 
} 


/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init); 
module_exit(xxx_exit);
要注意的一点就是,选择没有被使用的主设备号;
// 查看当前已经被使用掉的设备号
cat /proc/devices

 

实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,在上图代码中定义了file_operations结构体类型的变量test_fops,但是还没对其进行初始化,也就是初始化其中的 open、release、read 和 write 等具体的设备操作函数。

假设对chrtest这个设备有如下两个要求:

能够实现打开和关闭操作:需要实现 file_operations 中的open和release 这两个函数

能够实现进行读写操作:需要实现 file_operations 中的read和write这两个函数

 

/*打开设备*/
static int chrtest_open(struct inode *inode, struct file *filp) 
{ 
  /*用户实现具体功能*/
  return0; 
}
/*从设备读取*/
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) 
{ 
  /*用户实现具体功能*/
  return0; 
}
/*向设备写数据*/
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) 
{ 
  /*用户实现具体功能*/
  return0; 
}


/*关闭释放设备*/
static int chrtest_release(struct inode *inode, struct file *filp) 
{ 
  /*用户实现具体功能*/
  return0; 
}


/*文件操作结构体*/
staticstruct 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)
  { 
    /*字符设备注册失败*/
  } 
  return0; 
}


/*驱动出口函数*/
static void __exit xxx_exit(void) 
{
  /*注销字符设备驱动*/
  unregister_chrdev(200, "chrtest"); 
}


/*指定为驱动的入口和出口函数*/
module_init(xxx_init); 
module_exit(xxx_exit);

 

一开始编写了四个函数:chrtest_open、chrtest_read、chrtest_write和 chrtest_release。这四个函数就是 chrtest 设备的 open、read、write 和 release 操作函数。结构体配置就是初始化 test_fops 的 open、read、write 和 release 这四个成员变量。

  添加 LICENSE 和作者信息 最后需要在驱动中加入 LICENSE(许可) 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。 LICENSE 和作者信息的添加使用如下两个函数:

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
 
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
  /* 用户实现具体功能 */
  return 0;
}
......


/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);


MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
  LICENSE 采用 GPL 协议,有时候协议是很有必要的,特别是开源的项目,对于常用的协议还是要有一定的了解。 

 

  审核编辑:汤梓红
 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分