字符设备就是按字节流进行读写的设备,读写数据分先后顺序,如点灯,IIC,SPI,LCD等都是字符设备,这些设备的驱动就叫字符设备驱动。
include/linux/fs.h中 file_operations 结构体为内核驱动操作函数集合,C库中调用open,read,write等函数时,具体在驱动层执行的函数指针会关联记录在此结构体中。
流程简述
内核起来之后,使用模块加载命令加载.ko文件时,在驱动层便会开始执行宏 module_init 载入的函数,一个基本的字符设备驱动,加载流程分以下几步:
- 确定设备号,可以是动态分配,也可以是静态指定;
- 关联 file_operations 结构体变量,因为里面存放着具体执行动作的函数指针;
- 关联设备号;
- 将字符设备添加到内核。
卸载流程会调用宏 module_exit 载入的函数,简单来说需要实现注销设备号,调用相关函数删除字符设备结构体,并释放相关的资源。
对于开发着而言,最主要的是 file_operations 结构体对象的实现,然后是正确的框架搭建。
内核设备号
内核中每个设备都有一个设备号,设备号由主,次两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备,内核中提供了一个 dev_t 的数据类型表示设备号,定义在 include/linux/types.h 中,dev_t 实际上是 unsigned int 类型,是32位的数据类型。其中高 12 位为主设备号,低 20 位为次设备号。
在 include/linux/kdev_t.h 中提供了几个关于设备号的操作宏,见下图
设备号的分配分静态和动态分配。
静态分配
有一些常用的设备号已经被内核开发者分配掉了,具体的分配情况可以查看 Documentation/devices.txt 文档,具体能不能用还得看我们硬件平台运行的过程中有没有使用这个主设备号,使用 cat /proc/devices 即可查看当前系统中正在使用的设备号。
动态分配
因为静态分配会带来设备号冲突的问题,所以推荐使用动态分配的方式,在注册字符设备之前先申请一个设备号,注销设备时释放这个设备号即可。设备号的申请,释放函数如下,在文件 fs/char_dev.c 中:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)
alloc_chrdev_region 用于申请设备号,参数解释如下:
dev 用于存放申请到的设备号;
baseminor 次设备号的起始地址,alloc_chrdev_region函数可以申请一段连续的设备号,主设备号相同,但次设备号不同,次设备号以 baseminor 为起始地址开始递增;
count 要申请的设备号数量;
name 设备名字
unregister_chrdev_region 用于释放掉设备号,参数解释如下:
form 要释放的设备号;
count 表示从 from 开始,要释放的设备号数量。
字符设备的注册与注销
老版本内核
2.4版本之前的内核,注册是需要预先确定主设备号的,所以容易造成设备号冲突,而且会将一个主设备号下的所有次设备号都使用掉,浪费次设备号。
老版本字符设备的注册和注销函数原型如下:
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 指向 file_operations 类型指针,与该设备相关的文件操作集合。
unregister_chrdev 函数有两个参数,参数解释如下:
major 主设备号;
name 设备名字。
新版本内核
确定设备号
为了规避老版本内核在设备号冲突和浪费的问题,新内核的解决方法是在使用设备号的时候向内核申请,由内核来分配可以使用的设备号。也就是使用上文提到的 alloc_chrdev_region 函数动态分配设备号。
新内核也支持静态设备号,如果给定了设备的主次设备号可以使用如下函数来注册给定的设备号。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数解释如下:
from 要申请的起始设备号
count 要申请的连续设备号个数
name 设备名称
不管是通过 alloc_chrdev_region 函数分配的设备号,还是通过 register_chrdev_region 函数注册的指定设备号,在注销字符设备之后都要释放掉设备号,使用上文提到的 unregister_chrdev_region 函数释放设备号。
注册设备
在确定了设备号之后,新版本内核就需要使用到 cdev 结构体来注册字符设备,cdev 定义在 include/linux/cdev.h 中,见下图。
其中有两个重要的成员变量,ops 和 dev,ops 是字符设备文件操作函数集合,dev 是设备号。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备。
定义好 cdev 变量之后需要使用 cdev_init 函数对其进行初始化,cdev_init 原型如下:
void cdev_init(struct cdev *, const struct file_operations *);
调用 cdev_init 函数将cdev 变量与字符设备文件操作函数关联之后,需要使用 cdev_add 函数向内核添加字符设备,完成 cdev 变量与设备号的绑定,cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 count为要添加的设备数量。
从内核中删除字符设备需要使用 cdev_del函数,函数原型如下
void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。
添加LICENSE 和作者信息
在编写的驱动中必须要添加 LICENSE 信息,作者可以选择性添加,使用如下两个函数添加:
MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR() //添加模块作者信息
LICENSE 一般写GPL协议,MODULE_LICENSE(“GPL”)
模块的加载和卸载
驱模块的加载有两种方式,
第一种是直接将驱动模块编译进内核中,这样当内核启动的时候就会自动运行驱动程序;
第二种是将驱动模块编译成模块 .ko 文件,在内核启动之后使用相应命令加载驱动模块,这种方式的好处是不用重启内核就可以实现驱动模块的加载和卸载。
有两种命令可以加载 .ko 文件,insmode 和 modprobe;
insmode 命令不能解决模块的依赖问题,
modprobe 命令会分析模块的依赖关系,然后会将所依赖的模块都加载到内核中,modprobe 命令默认会去 /lib/modules/<kernel-version>目录中查找模块,一般自己制作的 rootfs 是不会有这个目录的,所以需手动创建。
模块的卸载使用 rmmod 命令,也可以使用 modprobe -r ***.ko 命令。区别在于,使用 modprobe -r 去卸载模块时也会卸载掉所依赖的其他模块。
驱动程序中,模块的加载和卸载函数如下
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
这里需要注意,驱动程序中,模块的加载函数要使用 __init 来修饰,模块的卸载函数要使用 __exit 来修饰 。
代码举例
#include "linux/types.h"
#include "linux/init.h"
#include "linux/module.h"
#include "linux/ide.h"
#include "linux/fs.h"
#include "linux/kdev_t.h"
#include "linux/cdev.h"
static dev_t g_DeviceID;
static struct cdev g_Cdev;
static int chrdevTest_open(struct inode *pInode, struct file *pFile)
{
return 0;
}
static ssize_t chrdevTest_read(struct file *pFile, char __user *pBuf, size_t cnt, loff_t *tLoff)
{
return 0;
}
static ssize_t chrdevTest_write(struct file *pFile, const char __user *pData, size_t cnt, loff_t *tLoff)
{
return 0;
}
static int chrdevTest_release(struct inode *pInode, struct file *pFile)
{
return 0;
}
static struct file_operations chrdevTest_fops =
{
.owner = THIS_MODULE,
.open = chrdevTest_open,
.read = chrdevTest_read,
.write = chrdevTest_write,
.release = chrdevTest_release,
};
static int __init chrdevTest_init(void)
{
int retVal = 0;
retVal = alloc_chrdev_region(&g_DeviceID, 0, 1, "chrdevTest");
if(retVal < 0)
{
printk("alloc Device ID failed, value:%d\r\n", retVal);
return 1;
}
else
{
printk("alloc Device ID success, major:%d minor:%d\r\n", MAJOR(g_DeviceID), MINOR(g_DeviceID));
}
cdev_init(&g_Cdev, &chrdevTest_fops);
retVal = cdev_add(&g_Cdev, g_DeviceID, 1);
if(retVal < 0)
{
printk("cdev add failed, retval:%d\r\n", retVal);
}
else
{
printk("cdev add success\r\n");
}
return 0;
}
static void __exit chrdevTest_exit(void)
{
cdev_del(&g_Cdev);
unregister_chrdev_region(g_DeviceID, 1);
printk("unregister\r\n");
}
module_init(chrdevTest_init);
module_exit(chrdevTest_exit);
MODULE_LICENSE("GPL");