目录
一、驱动模块的加载和卸载
Linux驱动有两种运行方式:
第一种即是将驱动编译进 Linux内核中,Linux内核启动时自动运行驱动程序。
第二种即是将驱动编译成模块 (Linux下模块扩展名为 .ko),Linux内核启动以后使用“ insmod xxx.ko”命令加载驱动模块。
调试驱动一般选用第二种。这样更改完驱动时只用编译模块而不用编译整个linux导致费时。
模块有加载和卸载两种操作:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
module_init()函数被调用时,会向Linux内核注册一个模块加载函数,参数xxx_init即是注册的具体函数,当使用“ insmod xxx.ko”命令加载驱动的时候xxx_init这个函数就会被调用。
module_exit()函数被调用时,会向Linux内核注册一个模块卸载函数,参数xxx_exit即是注册的具体函数,当使用“ rmmod xxx.ko”命令卸载具体驱动的时候xxx_exit函数就会被调用。
二、分配和释放设备号
Linux中每个设备都有一个设备号,类似于身份ID。设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux提供了
一个名为 dev_t的数据类型表示设备号, dev_t定义在文件 include/linux/types.h里面。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数dev是保存申请到的设备号;参数baseminor是次设备号起始地址,一般为 0,也就是说次设备号从0开始;参数from是要申请的已经给定的起始设备号;参数count是要申请的数量,一般都是一个;参数name是设备名字。
注销字符设备之后要释放掉设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
三、字符设备注册与注销
在 Linux中使用cdev结构体表示一个字符设备, cdev结构体的定义在include/linux/cdev.h里面。
在cdev中有两个重要的成员变量:ops和 dev,这两个就是字符设备文件操作函数集合file_operations以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
定义好cdev变量以后就要使用cdev_init函数对其进行初始化, cdev_init函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数cdev即是要初始化的cdev结构体变量,参数fops即是字符设备文件操作函数集合。
然后使用cdev_add函数向Linux系统添加这个字符设备,cdev_add函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数p指向要添加的字符设备 (cdev结构体变量),参数dev就是设备所使用的设备号,参数count是要添加的设备数量。
最后卸载驱动时使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del函数原型如下:
void cdev_del(struct cdev *p)
四、实现设备的具体操作函数
file_operations结构体即是设备的具体操作函数,我们需要初始化其中的open、release、 read和 write等具体的设备操作函数。
在初始化fops之前我们要分析一下需求,也就是要对这个设备进行哪些操作。由于是入门实验,这里不多做要求。
struct file_operations file_operations = {
.owner = THIS_MODULE,
};
五、添加 LICENSE和作者信息
我们需要在驱动中加入LICENSE信息和作者信息,其中LICENSE是必须添加的,否则编译会报错。
MODULE_LICENSE();
MODULE_AUTHOR();
六、驱动程序完整代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
static dev_t dev_id;
static struct cdev mydev;
/* 文件操作集合 */
static struct file_operations mydev_fops = {
.owner = THIS_MODULE,
};
static __init int mydev_init(void)
{
/* 申请设备号 */
alloc_chrdev_region(&dev_id, 1, 1, "mydev");
/* 设置字符设备 */
cdev_init(&mydev, &mydev_fops);
/* 注册字符设备 */
cdev_add(&mydev, dev_id, 1);
/* 打印申请到的主次设备号 */
printk("mydev_init!\n");
return 0;
}
static __exit void mydev_exit(void)
{
cdev_del(&mydev);
unregister_chrdev_region(dev_id, 1);
}
module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wJen");
Linux内核里没有printf,printf运行在用户态,printk运行在内核态。
七、编译驱动程序
编写Makefile文件:
KERNELDIR := /home/karudo/linux/IMX6ULL/linux/alientek_linux
CURRENT_PATH := $(shell pwd)
obj-m := mydev.o
build : kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
KERNELDIR表示开发平台所使用的Linux内核源码的绝对路径。这里我已经修改过Linux顶层Makefile的ARCH和CROSS_COMPILE。
modules表示编译模块 。
-C表示将当前的工作目录切换到指定目录中,这里即是KERNERLDIR目录。
M表示模块源码目录,这里即是CURRENT_PATH目录。程序会自动到指定的目录中读取模块的源码并将其编译为 .ko文件。
编写完成后,make编译出mydev.ko。
八、加载驱动模块
将mydev.ko cp到你的开发平台下,用lsmod查看已挂载模块:
可以看到目前没有任何模块挂载,用insmod将我们的模块挂载:
可以看到提示消息mydev_init。再用lsmod查看已挂载模块:
可以看到mydev模块挂载成功。再用cat /proc/devices查看当前设备:
可以看到mydev被分配到的主设备号为248。最后记得不使用该设备时可以用rmmod卸载该设备。
标签:入门,mydev,cdev,dev,模块,Linux,驱动,设备 From: https://blog.csdn.net/weixin_61979510/article/details/137563263