【Kernel】驱动开发学习之字符设备

1,116 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 @TOC

一、驱动模块传参

当我们在开发调试设备驱动的时候,有时候会遇到需要在驱动被注册时才会赋值的情况我们就需要用到驱动传参,这里涉及到两个函数:

1、module_param(name,type,perm)

第一个参数name表示我们接受该参数的变量名 第二个参数type表示我们接受该参数的类型 第三个参数是传入的参数的读写权限,有以下可选项 在这里插入图片描述使用示例:

static int major_num, minor_num; //定义主设备号和次设备号
//驱动模块传入普通参数 major_num 
module_param(major_num, int, S_IRUSR);    // 主设备号
//驱动模块传入普通参数 minor_num
module_param(minor_num, int, S_IRUSR);  	// 次设备号

加粗样式

2、module_param_array(name,type,perm)

//定义数组 static int arry[5]; //定义实际传入进去参数的个数 static int arry_count; //传递数组的参数
module_param_array(arry, int, &arry_count, S_IRUSR);

使用方式:

insmod param.ko arry=2,3,4,5,6

二、注册字符设备

1、静态设置设备号

这里主要使用到的函数有

register_chrdev_region(设备号, 次设备数量, 设备名); //注册设备号
unregister_chrdev_region(设备号, 次设备数量);	 //注销设备号

这里需要注意的是注册的设备号需要使用MKDEV合并子设备好和主设备号,静态注册示例:


#include <linux/init.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/uaccess.h> 
#include <linux/io.h> // IO操作
#include <linux/kernel.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h> // 对字符设备结构 cdev 以及一系列的操作函数的定义。包含了 cdev 结构及 相关函数的定义。
#include <linux/device.h> // 自动创建设备节点需要使用

static int major_num, minor_num; //定义主设备号和次设备号
//驱动模块传入普通参数 major_num 
module_param(major_num, int, S_IRUSR);    // 主设备号
//驱动模块传入普通参数 minor_num
module_param(minor_num, int, S_IRUSR);  	// 次设备号

static int char_device_init(void)
{
    int ret;
    /*静态注册设备号*/
    if(0 != major_num)
    {
        dev_num = MKDEV(major_num, minor_num); //MKDEV 将主设备号和次设备号合并为 一个设备号
        // 一个次设备号名称为jchrdev
        ret = register_chrdev_region(dev_num, 1, “jchrdev”); 
        if(ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
    }
    return 0;
}

static void char_device_exit(void)
{
    //释放设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); 
    printk(" char device gooodbye! \n");
}


module_init(char_device_init);
module_exit(char_device_exit);

然后编译我们的驱动代码再通过上面的演示我们就可以看到字符设备注册成功了,但是这种方式存在一个问题就是我们要提前知道空闲的设备号才能进行注册,否则将会发生设备号冲突问题,这里就拿杂项设备驱动号来做申请尝试 在这里插入图片描述 所以正常注册字符设备更推荐使用动态注册的方式。

2、动态申请设备号

动态注册和静态注册的方式基本相同,只是改变注册函数alloc_chrdev_region:

dev :alloc_chrdev_region函数向内核申请下来的设备号 baseminor :次设备号的起始 count: 申请次设备号的个数 name :执行 cat /proc/devices显示的名称

ret = alloc_chrdev_region(&dev_num, 0, 1, “dchrdev”); 
if (ret < 0) {
     printk("alloc_chrdev_region error\n"); 
}
//动态注册设备号成功,则打印 
major_num = MAJOR(dev_num);  //将主设备号取出来 
minor_num = MINOR(dev_num); 

这里需要注意的是我们动态申请的设备号是主设备号和次设备号已经合并了的,如果后续会使用到次设备号我们需要使用MAJOR、MINOR来单独提取申请到的主设备号和次设备号。 在这里插入图片描述 不管是杂项设备也好字符设备也罢,我们最主要的目的肯定是为应用层提供硬件操作接口,

3、注册设备

在 Linux 内核中,使用 cdev 结构体描述一个字符设备,cdev 结构体的定义如下:

 struct cdev { //描述字符设备的一个结构体 
 	struct kobject kobj; 
 	struct module *owner; 
 	const struct file_operations *ops; 
	struct list_head list; dev_t dev; 
	unsigned int count; 
 };

操作该结构体的函数有

void cdev_init(struct cdev *, const struct file_operations *); 

第一个参数 要初始化的 cdev 第二个参数 文件操作集 cdev->ops = fops; //实际就是把文件操作集写给 ops 功能 cdev_init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。

int cdev_add(struct cdev *, dev_t, unsigned); 

第一个参数 cdev 的结构体指针 第二个参数 设备号 第三个参数 次设备号的数量 功能 cdev_add()函数用于动态申请一个 cdev 内存。

void cdev_del(struct cdev *);

传入cdev设备即可注销释放设备,使用示例

//文件操作集
struct file_operations char_device_fops=
{
    .owner = THIS_MODULE,
    .open = char_device_open,
    .write = char_device_write,
};

//miscdevice 结构体
struct cdev char_device =
 {
    // .name = "char_led",
    .owner = THIS_MODULE,
    .ops = &char_device_fops,
};

// 在modbul_init的函数中调用初始化字符设备和注册到内核中
cdev_init(&char_device, &char_device_fops); //完成字符设备注册到内核 
cdev_add(&char_device, dev_num, DEVICE_NUMBER);

// 在modbule_exit函数中调用释放函数
// 卸载字符设备
cdev_del(&char_device);

到这里对于字符设备的注册已经完成了,但是如果我们装载完成这个驱动过后查看/dev下没有生成新的节点,那我们就无法操作该驱动设备,我们需要使用mknod命令创建一个设备节点格式: mknod 名称类型 主设备号 次设备号 在这里插入图片描述

三、自动创建设备节点

自动创建设备节点分为两个步骤: 步骤一:使用 class_create 函数创建一个类。 步骤二:使用 device_create 函数在我们创建的类下面创建一个设备。

1、类的添加

在 Linux 驱动程序中一般通过 class_create 和 class_destroy 来完成设备类的创建和删除。首先要创建 一个 class 类结构体,class 结构体定义在 include/linux/device.h 里面。class_create 是个宏,宏定义如下:

 #define class_create(owner, name) \ 
 ({ \ 
 	static struct lock_class_key __key; \
  	__class_create(owner, name, &__key); \ 
  })
  
  struct class *__class_create(struct module *owner, const char *name, \
  									struct lock_class_key *key) 

class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个 指向结构体 class 的指针,也就是创建的类。

卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy他只有一个参数就是类结构体;

struct class *char_device_class; // 生成在sys/class中的一个类
char_device_class = class_create(THIS_MODULE, “char_device_class”); // init
class_destroy(char_device_class);// 释放类 exit

在这里插入图片描述

2、设备节点添加

当我们创建完成一个类后就可以开始添加我们的设备节点了,创建设备的函数有:

struct device *device_create(struct class *class, \
				struct device *parent, dev_t devt, void *drvdata, \
				const char *fmt, ...)

void device_destroy(struct class *class, dev_t devt)

device_create参数: class:在该类中创建节点 parent:指定父设备,这里一般为NULL devt:设备号 drvdata :设备可能会使用的一些数据,一般 为 NULL fmt :是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件

示例代码

struct device *char_device_node; // 设备节点

// 添加节点
char_device_node = device_create(char_device_class,NULL, dev_num, NULL, “char_device_node”);
// 删除节点
device_destroy(char_device_class, dev_num);

在这里插入图片描述 这里需要注意的是释放节点一定是在释放设备类之前

    //释放设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); 
    // 卸载字符设备
    cdev_del(&char_device);
    // 删除节点
    device_destroy(char_device_class, dev_num);
    // 释放类
    class_destroy(char_device_class);

完整代码示例

驱动部分

#include <linux/init.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/uaccess.h> 
#include <linux/io.h> // IO操作
#include <linux/kernel.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h> // 对字符设备结构 cdev 以及一系列的操作函数的定义。包含了 cdev 结构及 相关函数的定义。
#include <linux/device.h> // 自动创建设备节点需要使用

static int major_num, minor_num; //定义主设备号和次设备号
//驱动模块传入普通参数 major_num 
module_param(major_num, int, S_IRUSR);    // 主设备号
//驱动模块传入普通参数 minor_num
module_param(minor_num, int, S_IRUSR);  // 次设备号


#define DEVICE_NUMBER 1 //定义次设备号的个数 
#define DEVICE_SNAME "jchardev" //定义静态注册设备的名称 
#define DEVICE_ANAME "dchardev" //定义动态注册设备的名称 
#define DEVICE_MINOR_NUMBER 0 //定义次设备号的起始地址
#define DEVICE_CLASS_NAME "char_device_class"
#define DEVICE_NODE_NAME "char_device_node"

#define GPIO_DR 0xfdd60000 //LED 物理地址,通过查看原理图得知
unsigned int *vir_gpio_dr; //存放映射完的虚拟地址的首地址

int dev_num; // 设备号

struct class *char_device_class; // 生成在sys/class中的一个类
struct device *char_device_node; // 设备节点


ssize_t char_device_write (struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
    char kbuf[64] = {0};
    if(copy_from_user(kbuf,ubuf,size)!= 0)
    {
        printk("copy_from_user error \n ");
        return -1;
    }

    if(kbuf[0]==1) //传入数据为 1 ,LED 亮
    {
        *vir_gpio_dr = 0x80008000;
    }
    else if(kbuf[0]==0) //传入数据为 0,LED 灭
        *vir_gpio_dr = 0x80000000;

    return 0;
}

int char_device_open(struct inode *inode,struct file *file)
{
    printk("char device open\n ");
    return 0;
}

////////////////////////////////////////////////////////////////////////
//文件操作集
struct file_operations char_device_fops=
{
    .owner = THIS_MODULE,
    .open = char_device_open,
    .write = char_device_write,
};

//miscdevice 结构体
struct cdev char_device =
 {
    // .name = "char_led",
    .owner = THIS_MODULE,
    .ops = &char_device_fops,
};
////////////////////////////////////////////////////////////////////////


static int char_device_init(void)
{
    int ret;
    /*静态注册设备号*/
    if(0 != major_num)
    {
        dev_num = MKDEV(major_num, minor_num); //MKDEV 将主设备号和次设备号合并为 一个设备号
        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
        if(ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
    }/*动态注册设备号*/
    else
    {
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME); 
        if (ret < 0) {
             printk("alloc_chrdev_region error\n"); 
        }
        //动态注册设备号成功,则打印 
        major_num = MAJOR(dev_num);  //将主设备号取出来 
        minor_num = MINOR(dev_num); 
        
        printk("major_num = %d\n", major_num);  //打印申请的主设备号 
        printk("minor_num = %d\n", minor_num); //打印申请的次设备号
    }

   
    cdev_init(&char_device, &char_device_fops); //完成字符设备注册到内核 
    cdev_add(&char_device, dev_num, DEVICE_NUMBER);

    char_device_class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
    char_device_node = device_create(char_device_class,NULL, dev_num, NULL, DEVICE_NODE_NAME);

    //将物理地址转化为虚拟地址
    vir_gpio_dr = ioremap(GPIO_DR,4);
    if(vir_gpio_dr == NULL)
    {
        printk("GPIO_DR ioremap is error \n");
        return EBUSY;
    }
    printk("GPIO_DR ioremap is ok \n");
    return 0;
}

static void char_device_exit(void)
{
    //释放设备号
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); 

    // 卸载字符设备
    cdev_del(&char_device);

   // 删除节点
    device_destroy(char_device_class, dev_num);

    // 释放类
    class_destroy(char_device_class);

    iounmap(vir_gpio_dr);
    printk(" char device gooodbye! \n");
}


module_init(char_device_init);
module_exit(char_device_exit);
MODULE_LICENSE("GPL");

Makefile

obj-m += char_device.o
KDIR =/home/topeet/Linux/02.sdk/rk356x_linux/kernel
PWD ?= $(shell pwd) 
all: 
	make -C $(KDIR) M=$(PWD) modules modules ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

clean: 
	rm -rf modules.order *.o workqueue.o Module.symvers *.mod.c *.ko

应用程序

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// #include <delay.h>
int main(int argc,char *argv[])
{
    int fd;
    char buf[2] = {0};//定义 buf 缓存
    //打开设备节点
    fd = open("/dev/char_device_node",O_RDWR);
    if(fd < 0)
    {
        //打开设备节点失败
        perror("open error \n");
        return fd;
    }
    //把缓冲区数据写入文件中
    while(1)
    {
        buf[0] = 0;
        write(fd,buf,sizeof(buf));
        sleep(1);
        buf[0] = 1;
        write(fd,buf,sizeof(buf));
        sleep(1);
    }
    close(fd);
}