例程代码路径:ELF 1开发板资料包3-例程源码3-2 驱动例程源码2_字符驱动mydevice
mydevice.c源码
(一)首先包含头文件
#include <linux/module.h> // 包含模块相关函数的头文件
#include <linux/fs.h> // 包含文件系统相关函数的头文件 #include <linux/uaccess.h> // 包含用户空间数据访问函数的头文件 #include <linux/cdev.h> //包含字符设备头文件 |
(二)声明变量
#define DEVICE_NAME "mydevice" // 设备名称
static dev_t dev_num; //分配的设备号 struct cdev my_cdev; //字符设备指针 int major; //主设备号 int minor; //次设备号 |
(三)驱动模块入口和出口函数
module_init(mydevice_init); // 指定驱动程序的初始化函数
module_exit(mydevice_exit); // 指定驱动程序的清理函数 |
(四)入口函数和出口函数实现
static int __init mydevice_init(void)
{ int ret; // 在这里执行驱动程序的初始化操作 // 注册字符设备驱动程序 ret = alloc_chrdev_region(&dev_num,0,1,DEVICE_NAME); if (ret < 0) { printk(KERN_ALERT "Failed to allocate device number: %dn", ret); return ret; } major=MAJOR(dev_num); //获取主设备号 minor=MINOR(dev_num); //获取次设备号 printk(KERN_INFO "major number: %dn",major); printk(KERN_INFO "minor number: %dn",minor); my_cdev.owner = THIS_MODULE; cdev_init(&my_cdev,&fops); //初始化字符设备结构体 cdev_add(&my_cdev,dev_num,1); //将字符设备添加到内核中 printk(KERN_INFO "Device registered successfully.n"); return 0; } static void __exit mydevice_exit(void) { // 在这里执行驱动程序的清理操作 // 删除字符设备 cdev_del(&my_cdev); // 注销字符设备驱动程序 unregister_chrdev(0, DEVICE_NAME); printk(KERN_INFO "Device unregistered.n"); } |
入口函数mydevice_init(void)中主要有以下几个函数的调用:
(1)调用int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);函数进行设备号分配。
dev:指向dev_t类型的指针,用于接收分配的设备号范围的起始设备号;
firstminor:指定次设备号的起始值。次设备号用于区分同一主设备号下的不同设备实例;
count:指定要分配的设备号数量;
name:指定字符设备的名称,这是一个用于标识设备的字符串,可以在/proc/devices中找到;
(2)调用MAJOR(dev)获取主设备号:
MAJOR(dev)宏接受一个dev_t类型的设备号作为参数,并返回该设备号的主设备号。
(3)调用MINOR(dev)获取次设备号:
MINOR(dev)宏接受一个dev_t类型的设备号作为参数,并返回该设备号的次设备号。
(4)调用void cdev_init(struct cdev *cdev, const struct file_operations *fops);函数初始化字符设备结构体。
cdev:指向要初始化的struct cdev结构体的指针;
fops:指向包含设备操作函数的struct file_operations结构体的指针;
(5)调用int cdev_add(struct cdev *p, dev_t dev, unsigned int count);函数将字符设备添加到内核中。
p:指向要添加的struct cdev结构体的指针;
dev:指定要添加的设备号。通常是由alloc_chrdev_region或register_chrdev_region函数分配得到的设备号;
count:指定要添加的设备号数量;
cdev_add函数将字符设备添加到内核中,并与指定的设备号相关联。当成功添加字符设备后,它将被分配一个主设备号和次设备号,并成为系统中的一个可用设备;
注意:在调用cdev_add之前,应先使用cdev_init函数对struct cdev进行初始化,并设置正确的文件操作函数。
出口函数mydevice_exit(void)中主要有以下两个函数的调用:
(1)调用void unregister_chrdev(unsigned int major, const char *name);函数注销已注册的字符设备。
major:要注销的字符设备的主设备号;
name:字符设备的名称;
unregister_chrdev函数将指定的字符设备从内核中注销,并释放相关的资源。注销后,该字符设备将不再可用;
(2)调用void cdev_del(struct cdev *p);函数删除字符设备。
p:指向要删除的struct cdev结构体的指针;
cdev_del函数从内核中删除指定的字符设备,并释放相关的资源。删除后,该字符设备将不再可用;
(五)定义fops结构体
static struct file_operations fops = {
.owner = THIS_MODULE, .open = device_open, .release = device_release, .read = device_read, .write = device_write, }; |
file_operations结构体定义如下:
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 *, const char __user *, size_t, loff_t *); int (*open) (struct inode *, struct file *); int (*release) (struct inode *, struct file *); // 其他成员函数... }; |
file_oprations结构中定义了非常多的成员,但是对于大多数字符驱动而言,只需要实现,其中很少的几个方法即可。下面将忽略不常用的成员,对常用成员进行介绍:
(1)owner:指向持有该结构体的模块的指针;
(2)llseek:用于实现文件的定位(seek)操作;
(3)read:用于实现从文件读取数据的操作;
(4)write:用于实现向文件写入数据的操作;
(5)open:用于打开文件时的处理操作;
(6)release:用于关闭文件时的处理操作;
(六)设备操作函数的实现
static int device_open(struct inode *inode, struct file *file)
{ // 在这里处理设备打开的操作 printk(KERN_INFO "This is device_open.n"); return 0; } static int device_release(struct inode *inode, struct file *file) { // 在这里处理设备关闭的操作 printk(KERN_INFO "This is device_release.n"); return 0; } static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset) { // 在这里处理设备读取的操作 printk(KERN_INFO "This is device_read.n"); return 0; } static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset) { // 在这里处理设备写入的操作 printk(KERN_INFO "This is device_write.n"); return 0; } |
(七)声明模块信息
MODULE_LICENSE("GPL"); // 指定模块的许可证信息
MODULE_AUTHOR("Your Name"); // 指定模块的作者信息 MODULE_DESCRIPTION("A simple Hello World driver"); // 指定模块的描述信息 |
完整mydevice.c示例源码
#include <linux/module.h> // 包含模块相关函数的头文件
#include <linux/fs.h> // 包含文件系统相关函数的头文件 #include <linux/uaccess.h> // 包含用户空间数据访问函数的头文件 #include <linux/cdev.h> //包含字符设备头文件 #define DEVICE_NAME "mydevice" // 设备名称 static dev_t dev_num; //分配的设备号 struct cdev my_cdev; //字符设备指针 int major; //主设备号 int minor; //次设备号 static int device_open(struct inode *inode, struct file *file) { // 在这里处理设备打开的操作 printk(KERN_INFO "This is device_open.n"); return 0; } static int device_release(struct inode *inode, struct file *file) { // 在这里处理设备关闭的操作 printk(KERN_INFO "This is device_release.n"); return 0; } static ssize_t device_read(struct file *file, char __user *buffer, size_t length, loff_t *offset) { // 在这里处理设备读取的操作 printk(KERN_INFO "This is device_read.n"); return 0; } static ssize_t device_write(struct file *file, const char __user *buffer, size_t length, loff_t *offset) { // 在这里处理设备写入的操作 printk(KERN_INFO "This is device_write.n"); return 0; } static struct file_operations fops = { .owner = THIS_MODULE, .open = device_open, .release = device_release, .read = device_read, .write = device_write, }; static int __init mydevice_init(void) { int ret; // 在这里执行驱动程序的初始化操作 // 注册字符设备驱动程序 ret = alloc_chrdev_region(&dev_num,0,1,DEVICE_NAME); if (ret < 0) { printk(KERN_ALERT "Failed to allocate device number: %dn", ret); return ret; } major=MAJOR(dev_num); //获取主设备号 minor=MINOR(dev_num); //获取次设备号 printk(KERN_INFO "major number: %dn",major); printk(KERN_INFO "minor number: %dn",minor); my_cdev.owner = THIS_MODULE; cdev_init(&my_cdev,&fops); //初始化字符设备结构体 cdev_add(&my_cdev,dev_num,1); //将字符设备添加到内核中 printk(KERN_INFO "Device registered successfully.n"); return 0; } static void __exit mydevice_exit(void) { // 在这里执行驱动程序的清理操作 // 删除字符设备 cdev_del(&my_cdev); // 注销字符设备驱动程序 unregister_chrdev(0, DEVICE_NAME); printk(KERN_INFO "Device unregistered.n"); } module_init(mydevice_init); module_exit(mydevice_exit); MODULE_LICENSE("GPL"); // 指定模块的许可证信息 MODULE_AUTHOR("Your Name"); // 指定模块的作者信息 MODULE_DESCRIPTION("A simple character device driver"); // 指定模块的描述信息 |
编译
复制helloworld驱动中的Makefile文件,将其中的hello.o修改为mydevice.o,效果如下:
设置环境变量,编译:
. /opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
elf@ubuntu:~/work/test/02_字符驱动/mydevice$ make |
将生成的mydevice.ko文件拷贝到开发板中。
编写测试应用源码device_app.c
驱动中已经给应用层提供了open、read、write的接口,接下来应用层就可以进行相应的系统调用。
#include <stdio.h>
#include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <errno.h> #include <fcntl.h> #define DEV_NAME "/dev/mydevice" int main(int argc, char *argv[]) { int reg; int fd = 0; int dat = 0; fd = open (DEV_NAME, O_RDWR); if (fd < 0) { perror("Open "DEV_NAME" Failed!n"); exit(1); } reg = read(fd, &dat, 1); if (reg<0) { perror("read "DEV_NAME" Failed!n"); exit(1); } dat = 0; reg = write(fd, &dat, 1); if (reg<0) { perror("write "DEV_NAME" Failed!n"); exit(1); } close(fd); return 0; } |
设置环境变量:
. /opt/fsl-imx-x11/4.1.15-2.0.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi |
编译,将生成的device_app文件拷贝到开发板中:
elf@ubuntu:~/work/test/02_字符驱动/device_app$ $CC device_app.c -o device_app |
测试
(一)加载驱动
root@ELF1:~# insmod mydevice.ko
major number: 246 minor number: 0 Device registered successfully. |
(二)可以看到生成的主设备号为246,次设备号为0。我们需要使用主设备号和次设备号手动创建/dev/下的设备节点。
root@ELF1:~# mknod /dev/mydevice c 246 0 |
mknod的基本语法如下:
mknod <device_file> <file_type> <major> <minor> |
<device_file>:指定要创建的设备文件节点的路径和名称;
<file_type>:指定设备文件的类型,可以是b(块设备)或c(字符设备);
<major>:指定设备文件的主设备号;
<minor>:指定设备文件的次设备号;
(三)运行测试应用程序
root@ELF1:~# ./device_app
This is device_open. This is device_read. This is device_write. This is device_release. |
(四)卸载驱动
root@ELF1:~# rmmod mydevice.ko
Device unregistered. |