目录
一、字符设备
在 Linux 中一切皆为文件,驱动加载成功以后会在 “/dev”
目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”
(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。
1.1 驱动模块的加载和卸载
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)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
// __init、__exit 定义在 inlucde/linux/init.h
// 声明在 inlucde/linux/init.h
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
驱动编译完成以后扩展名为 .ko
,有两种命令可以加载驱动模块:insmod
和 modprobe
,insmod
是最简单的模块加载命令,此命令用于加载指定的 .ko
模块,比如加载 drv.ko
这个驱动模块,命 令如下:
insmod drv.ko
insmod
命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod
命令加载 first.ko
这个模块,然后再加载 drv.ko
这个模块。
但是 modprobe
就不会存在这个问题,modprobe
会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe
命令相比 insmod
要智能一些。modprobe
命令主要智能在提供了模块的依赖性分析、 错误检查、错误报告等功能,推荐使用modprobe 命令来加载驱动。modprobe
命令默认会去 /lib/modules/< kernel-version> 目录中查找模块,比如本书使用的 Linux kernel 的版本号为 4.1.15, 因此modprobe
命令默认会到 /lib/modules/4.1.15 这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
驱动模块的卸载使用命令 “rmmod
” 即可,比如要卸载 drv.ko
,使用如下命令即可:
rmmod drv.ko
也可以使用 “modprobe -r
” 命令卸载驱动,比如要卸载 drv.ko
,命令如下:
modprobe -r drv.ko
使用 modprobe
命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe
来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod
命令。
1.2 字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
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
:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。
name
:设备名字,指向一串字符串。
fops
:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev
函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major
:要注销的设备对应的主设备号。
name
:要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init( )
中进行,字符设备的注销在驱动模块的出口函数 xxx_exit( )
中进行。模板如下:
static struct file_operations test_fops;
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(200, "chartest", &test_fops);
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
/* 注销字符设备驱动 */
unregister_chrdev(200, "chartest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
1.3 实现设备的具体操作函数
file_operations
结构体就是设备的具体操作函数,我们定义了 file_operations
结构体类型的变量 test_fops
,但是还没对其进行初始化,也就是初始化其中的 open、 release、read
和 write
等具体的设备操作函数。
添加基本操作,添加后内容如下:
/* 打开设备 */
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, "chartest", &test_fops);
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
/* 注销字符设备驱动 */
unregister_chrdev(200, "chartest");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
1.4 添加LICENSE 和作者信息
我们需要在驱动中加入 LICENSE
信息和作者
信息,其中 LICENSE
是必须添加的,否则的话编译的时候会报错,作者
信息可以添加也可以不添加。LICENSE
和作者信息的添加使用 如下两个函数:
MODULE_LICENSE() //添加模块LICENSE信息
MODULE_AUTHOR() //添加模块作者信息
添加后内容如下:
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
// 声明在 inlucde/linux/module.h
/* LICENSE 采用GPL 协议 */
MODULE_LICENSE("GPL");
1.5 创建设备节点文件
驱动加载成功需要在 /dev
目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建 /dev/chrdevbase
这个设备节 点文件:
mknod /dev/chrdevbase c 200 0
其中 “mknod
” 是创建节点命令,“/dev/chrdevbase
” 是要创建的节点文件,“c” 表示这是个字符设备,“200” 是设备的主设备号,“0” 是设备的次设备号。创建完成以后就会存在 /dev/chrdevbase
这个文件,可以使用 “ls /dev/chrdevbase -l
” 命令查看,结果下所示:
/dev # ls /dev/chrdevbase -l
crw-r--r-- 1 0 0 200, 0 Jan 1 01:38 /dev/chrdevbase
如果想要读写 chrdevbase
设备,直接对 /dev/chrdevbase
进行读写操作即可。相当于 /dev/chrdevbase
这个文件是 chrdevbase
设备在用户空间中的实现。
如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase
这个设备:
rmmod chrdevbase.ko
卸载以后使用 lsmod
命令查看 chrdevbase
这个模块还存不存在。
/ # lsmod
Module Size Used by Tainted: G
chrdevbase 1282 0
/ # rmmod chrdevbase.ko
/ # lsmod
Module Size Used by Tainted: G
/ # ls /dev/chrdevbase -l
ls: /dev/chrdevbase: No such file or directory
/ #
二、杂项设备
linux里面的misc杂项设备是主设备号为10的驱动设备
定义头文件<linux/miscdevice.h>
2.1 杂项设备的结构体
struct miscdevice{
int minor; //杂项设备的此设备号(如果设置为MISC_DYNAMIC_MINOR,表示系统自动分配未使用的minor)
const char *name;
const stuct file_operations *fops;//驱动主题函数入口指针
struct list_head list;
struct device *parent;
struct device *this device;
const char *nodename;(在/dev下面创建的设备驱动节点)
mode_t mode;
};
2.2 注册和释放
注册:int misc_register(struct miscdevice *misc)
释放:int misc_deregister(struct miscdevice *misc)
misc_device
是特殊字符设备。注册驱动程序时采用 misc_register
函数注册,此函数中会自动创建设备节点,即设备文件。无需 mknod
指令创建设备文件。因为 misc_register()
会调用 class_device_creat
或者 device_creat()
.
2.3 杂项字符设备和一般字符设备的区别
- 1.一般字符设备首先申请设备号。 但是杂项字符设备的主设备号为10次设备号通过结构体
struct miscdevice
中的minor来设置。 - 2.一般字符设备要创建设备文件。 但是杂项字符设备在注册时会自动创建。
- 3.一般字符设备要分配一个
cdev
(字符设备)。 但是杂项字符设备只要创建struct miscdevice
结构即可。 - 4.一般字符设备需要初始化
cdev
(即给字符设备设置对应的操作函数集struct file_operation
). 但是杂项字符设备在结构体struct miscdevice
中定义。 - 5.一般字符设备使用注册函数
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
(第一个参数为之前初始化的字符设备,第二个参数为设备号,第三个参数为要添加设备的个数) 而杂项字符设备使用int misc_register(struct miscdevice *misc)
来注册
驱动调用的实质:
就是通过 设备文件
找到与之 对应设备号的设备
,再通过设备初始化时绑定的操作函数对硬件进行控制的。
2.4 杂项设备驱动示例
2.4.1 驱动层注册
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "led_ctrl"
#define LED_NUM 4
// IOCTL命令定义
#define LED_ON _IO('L', 1)
#define LED_OFF _IO('L', 0)
static unsigned long led_table[] =
{
S3C2410_GPB(5),
S3C2410_GPB(6),
S3C2410_GPB(7),
S3C2410_GPB(8),
};
static unsigned int led_cfg_table[] =
{
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
S3C2410_GPIO_OUTPUT,
};
//定义4个GPIO引脚(GPB5-GPB8)及其配置模式(输出模式)
static long led_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int index = (int)arg;
if (index < 0 || index >= LED_NUM) {
return -EINVAL;
}
switch (cmd) {
case LED_ON:
s3c2410_gpio_setpin(led_table[index], 1);
break;
case LED_OFF:
s3c2410_gpio_setpin(led_table[index], 0);
break;
default:
return -ENOTTY;
}
return 0;
}
static struct file_operations dev_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = phy_ioctl,
};
//unlocked_ioctl用于处理用户空间的ioctl请求
static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,// 动态分配次设备号
.name = "misc_phy", // 设备名(/dev/misc_phy)
.fops = &dev_fops, // 关联 file_operations
};
static int __init dev_init(void)
{
int ret;
int i;
for (i = 0; i < 4; i++)
{
//初始化GPIO为输出模式,并关闭LED(置0)
s3c2410_gpio_cfgpin(led_table[i], led_cfg_table[i]);
s3c2410_gpio_setpin(led_table[i], 0);
}
ret = misc_register(&misc);
printk(DEVICE_NAME " initialized\n");
return ret;
}
static void __exit dev_exit(void)
{
//注销设备
misc_deregister(&misc);
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("www.e-online.cc");
MODULE_DESCRIPTION("LEDS control for GT2440 Board");
该代码实现了一个混杂设备驱动(miscdevice),主要功能包括:
- GPIO初始化:配置开发板(如S3C2410)上的4个GPIO引脚为输出模式,用于控制LED。
- 用户空间交互:通过ioctl接口实现与用户空间的通信,支持读取PHY寄存器(代码中功能不完整)。
- 模块管理:支持动态加载和卸载内核模块。
2.4.2 用户空间测试
整体流程简单如下
int fd = open("/dev/misc_led", O_RDWR);
ioctl(fd, LED_ON, 0); // 打开第0号LED
ioctl(fd, LED_OFF, 0); // 关闭第0号LED
三、参考链接
字符设备:
Linux 驱动开发 三:字符设备驱动框架
杂项设备:
misc_register 杂项设备