Linux嵌入式驱动开发08——字符设备(步步为营)

本文详细介绍嵌入式Linux环境下字符设备驱动的开发流程,包括设备号申请、cdev结构体使用、设备节点创建等内容,并提供了丰富的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

全系列传送门

Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)

Linux嵌入式驱动开发02——驱动编译到内核

Linux嵌入式驱动开发03——杂项设备驱动(附源码)

Linux嵌入式驱动开发04——应用层和内核层数据传输

Linux嵌入式驱动开发05——物理地址到虚拟地址映射

Linux嵌入式驱动开发06——第一个相对完整的驱动实践编写

Linux嵌入式驱动开发07——GPIO驱动过程记录(飞凌开发板)

Linux嵌入式驱动开发08——字符设备(步步为营)

Linux嵌入式驱动开发09——平台总线详解及实战

Linux嵌入式驱动开发10——设备树开发详解

Linux嵌入式驱动开发11——平台总线模型修改为设备树实例

Linux嵌入式驱动开发12——pinctl和gpio子系统实践操作

Linux嵌入式驱动开发13——ioctl接口(gpio控制使用)

Linux嵌入式驱动开发14——中断的原理以及按键中断的实现(tasklet中断下文)

Linux嵌入式驱动开发15——等待队列和工作队列

Linux嵌入式驱动开发16——按键消抖实验(内核定时器)

Linux嵌入式驱动开发17——输入子系统

Linux嵌入式驱动开发18——I2C通信

驱动模块传参

传递普通的参数 char int

使用的函数

module_param(name,type,perm);

param_test.c 代码

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

static int my_param;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(my_param, int, S_IRUSR);


static int hello_init(void)
{
    printk("my_param = %d\n", my_param);          // 在内核中无法使用c语言库,所以不用printf
    return 0;
}

static void hello_exit(void)
{
    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");              //声明模块拥有开源许可

核心就是这个module_param函数的使用

static int my_param;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(my_param, int, S_IRUSR);

编译之后,发送到开发板,然后安装,并且传递参数my_param到驱动模块中
在这里插入图片描述
模块安装在位置我们可以通过

/sys/module

可以看到我们的param_test模块

然后查看里面的文件权限,就可以和我们之前的S_IRUSR 400对应上了
在这里插入图片描述

传递数组

函数

module_param_array(name, type, &nump, perm);

param_test.c 代码

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

static int my_param;
static int my_list[5];
static int count;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(my_param, int, S_IRUSR);

/* module_param_array(name, type, &nump, perm);
 * name:要传递进去的参数的名称
 * type:类型
 * nump:实际传入的个数
 * perm:参数读写的权限
 */
module_param_array(my_list, int, &count, S_IRUSR);

static int hello_init(void)
{
    int i;

    for(i=0; i<count; i++){
        printk("my_list[ %d ] = %d\n", i, my_list[i]);
    }
    printk("count = %d\n", count);
    printk("my_param = %d\n", my_param);          // 在内核中无法使用c语言库,所以不用printf
    
    return 0;
}

static void hello_exit(void)
{
    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");              //声明模块拥有开源许可

可以看到运行结果,符合我们的设计

在开发板上运行

insmod param_test.ko my_list=1,2,3,4,5

在这里插入图片描述
在这里插入图片描述
如果传入数据多出5个,那么报错
在这里插入图片描述

字符设备和杂项设备的区别

杂项设备的主设备号是固定的,固定为10,那么我们要学习的字符类设备就需要自己或者系统来给我们分配了。

杂项设备可以自动生成设备节点,字符设备需要我们自己生成设备节点

申请字符类设备号

第一种 静态分配设备号

linux-4.1.15/include/linux

打开fs.h文件
在这里插入图片描述
使用的就是这个函数

register_chrdev_region(dev_t, unsigned, const char *);

在使用函数中,一定要注意使用的设备号的没有被使用过的

参数:

第一个:设备号的起始值

第二个:次设备号的个数

第三个:设备的名称

返回值:成功返回0,失败返回负数

cat /proc/devices

在这里插入图片描述

dev_t类型

dev_t是用来保存设备号的,是一个32位的数

typeded _u32_kernel_dev_t;

typeded _kernel_dev_t dev_t;

其中

高 12位 保存主设备号

低 12位 保存次设备号

linux提供了一些宏定义来操作设备号

在这里插入图片描述
第一个MINORBITS是次设备号的位数,一共二十位

第二个MINORMASK是次设备号的掩码

第三个MAJOR(dev)是在dev_t里面获取我们的主设备号

第四个MINOR(dev)是在dev_t里面获取我们的次设备号

第五个MKDEV(ma, mi)是将我们的主设备号和次设备号组成一个dev_t类型,第一个参数是主设备号,第二个参数是次设备号

第二种 动态分配

在这里插入图片描述

alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

参数

第一个dev_t *:保存生成的设备号

第二个unsigned:我们请求的第一个此设备号,通常是0

第三个unsigned:连续申请的设备号个数

第四个const char *:设备名称

返回值:成功返回0,失败返回负数

使用动态分配会优先使用255到234

注销设备号

使用的是

unregister_chrdev_region(dev_t from, unsigned count)

参数

第一个dev_t from:分配设备号的起始地址

第二个unsigned count:申请的连续设备号数量

代码

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

#define DEVICE_NUMBER 1                                             // 申请设备号的数量
#define DEVICE_SNAME "schrdev"                                      // 静态注册 名称
#define DEVICE_ANAME "achrdev"                                      // 动态注册 名称

#define DEVICE_MINOR_NUMBER 0                                       // 次设备号起始地址,通常是0

static int major_num,minor_num;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(major_num, int, S_IRUSR);
module_param(minor_num, int, S_IRUSR);

static int hello_init(void)
{
    dev_t dev_num;                                                      // 保存设备号

    int ret;

    if(major_num){                                                      // 如果major_num被手动设置,就使用静态注册设备号
        
        printk("major_num = %d\n", major_num);
        printk("minor_num = %d\n", minor_num);

        dev_num = MKDEV(major_num, minor_num);

        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
        
        if(ret < 0){                                                    // ret<0 注册失败
            printk("register_chrdev_region ERROR!!!\n");
        }else{
            printk("register_chrdev_region OK!!!\n");
        }

    }else{                                                              // major_num没有传入,所以动态申请设备号
                                                                  
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

        if(ret < 0){
            printk("alloc_chrdev_region ERROR!!!\n");
        }else{
            printk("alloc_chrdev_region OK!!!\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);
    }

    // printk("my_param = %d\n", my_param);                                // 在内核中无法使用c语言库,所以不用printf
    
    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);

    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");                                                  //声明模块拥有开源许可

验证静态分配代码

需要手动分配设备号,所以我们要先查看哪些使用过,哪些没用过

 cat /proc/devices

在这里插入图片描述
现在我们假定使用9作为我们自己程序的主设备号
在这里插入图片描述
根据图片结果,可以看到主设备号是9,次设备号因为没有设置,默认是0

然后,我们程序中这样的设置,所以设备名称应该是schrdev
在这里插入图片描述
我们可以看到这里生成了我们定义的设备号,以及对应的名字schrdev

在这里插入图片描述

验证动态分配代码

直接加载模块
在这里插入图片描述
查看
在这里插入图片描述

小结

建议使用动态分配

注册字符类设备

注册杂项设备

注册杂项设备

misc_register(&misc_dev)

注销杂项设备

misc_deregister(&misc_dev)

cdev结构体

cdev结构体,是一个描述字符设备的结构体

include/linux/cdev.h

在这里插入图片描述


struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
};

步骤一:定义一个cdev结构体

步骤二:使用cdev_init函数初始化cdev结构体成员变量

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

参数

struct cdev *:要初始化的cdev

const struct file_operations *:文件操作集

cdev->ops = fops; // 把文件集写给成员变量ops

步骤三:使用cdev_add函数注册到内核

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

参数
struct cdev *:cdev的结构体指针

dev_t:设备号

unsigned:次设备号的数量

步骤四:使用cdev_del函数注销

void cdev_del(struct cdev *);

代码

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

#include <linux/cdev.h>

#define DEVICE_NUMBER 1                                             // 申请设备号的数量
#define DEVICE_SNAME "schrdev"                                      // 静态注册 名称
#define DEVICE_ANAME "achrdev"                                      // 动态注册 名称

#define DEVICE_MINOR_NUMBER 0                                       // 次设备号起始地址,通常是0

static int major_num,minor_num;

struct cdev cdev;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(major_num, int, S_IRUSR);
module_param(minor_num, int, S_IRUSR);


int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open OK\n");                                     //chrdev_open正常
    return 0;
}

struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open,                                             // file_operations文件集操作,要定义函数chrdev_open
};

static int hello_init(void)
{
    dev_t dev_num;                                                      // 保存设备号

    int ret;

    if(major_num){                                                      // 如果major_num被手动设置,就使用静态注册设备号
        
        printk("major_num = %d\n", major_num);
        printk("minor_num = %d\n", minor_num);

        dev_num = MKDEV(major_num, minor_num);

        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
        
        if(ret < 0){                                                    // ret<0 注册失败
            printk("register_chrdev_region ERROR!!!\n");
        }else{
            printk("register_chrdev_region OK!!!\n");
        }

    }else{                                                              // major_num没有传入,所以动态申请设备号
                                                                  
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

        if(ret < 0){
            printk("alloc_chrdev_region ERROR!!!\n");
        }else{
            printk("alloc_chrdev_region OK!!!\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.owner = THIS_MODULE;

    cdev_init(&cdev, &chrdev_ops);

    cdev_add(&cdev, dev_num, DEVICE_NUMBER);
    
    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
    
    cdev_del(&cdev);

    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");                                                  //声明模块拥有开源许可

在这里插入图片描述

设备节点

没有设备节点的名字,只有设备的名字,也就是cat看到的

要先创建一个设备节点

字符设备注册完之后,不会自动生成设备节点

需要使用mknod命令创建一个节点

格式

mknod 名称 类型 主设备号 次设备号

举例

mknod /dev/test c 242 0

在这里插入图片描述
运行app

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;

    char buff[64] = {0};

    fd = open("/dev/test", O_RDWR);

    if(fd < 0){

        perror("open error\n");               // 在应用中打印
        return fd;
    }
    
    return 0;
}

在这里插入图片描述

自动创建设备节点

怎么自动创建一个设备节点?

在嵌入式Linux中使用mdev来实现设备节点文件的自动创建和删除

什么是mdev?

mdev 是 udev 的简化版本,是 busybox 中所带的程序,最适合用在嵌入式系统

步骤一:使用class_create函数创建一个class的类

class结构体在

/include/linux/device.h

在这里插入图片描述
class类

struct class {
        const char              *name;
        struct module           *owner;

        struct class_attribute          *class_attrs;
        const struct attribute_group    **dev_groups;
        struct kobject                  *dev_kobj;

        int (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
        char *(*devnode)(struct device *dev, umode_t *mode);

        void (*class_release)(struct class *class);
        void (*dev_release)(struct device *dev);

        int (*suspend)(struct device *dev, pm_message_t state);
        int (*resume)(struct device *dev);

        const struct kobj_ns_type_operations *ns_type;
        const void *(*namespace)(struct device *dev);

        const struct dev_pm_ops *pm;

        struct subsys_private *p;
};

步骤二:使用devic_create函数在我们创建的类下面创建一个设备

创建


#define class_create(owner, name)               \
({                                              \
        static struct lock_class_key __key;     \
        __class_create(owner, name, &__key);    \
})

删除


extern void class_destroy(struct class *cls);

代码

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

#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NUMBER 1                                             // 申请设备号的数量
#define DEVICE_SNAME "schrdev"                                      // 静态注册 名称
#define DEVICE_ANAME "achrdev"                                      // 动态注册 名称

#define DEVICE_MINOR_NUMBER 0                                       // 次设备号起始地址,通常是0

#define DEVICE_CLASS_NAME "chrdev_class"

static int major_num,minor_num;

struct cdev cdev;

struct class *class;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(major_num, int, S_IRUSR);
module_param(minor_num, int, S_IRUSR);


int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open OK\n");                                     //chrdev_open正常
    return 0;
}

struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open,                                             // file_operations文件集操作,要定义函数chrdev_open
};

static int hello_init(void)
{
    dev_t dev_num;                                                      // 保存设备号

    int ret;

    if(major_num){                                                      // 如果major_num被手动设置,就使用静态注册设备号
        
        printk("major_num = %d\n", major_num);
        printk("minor_num = %d\n", minor_num);

        dev_num = MKDEV(major_num, minor_num);

        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
        
        if(ret < 0){                                                    // ret<0 注册失败
            printk("register_chrdev_region ERROR!!!\n");
        }else{
            printk("register_chrdev_region OK!!!\n");
        }

    }else{                                                              // major_num没有传入,所以动态申请设备号
                                                                  
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

        if(ret < 0){
            printk("alloc_chrdev_region ERROR!!!\n");
        }else{
            printk("alloc_chrdev_region OK!!!\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.owner = THIS_MODULE;

    cdev_init(&cdev, &chrdev_ops);

    cdev_add(&cdev, dev_num, DEVICE_NUMBER);

    class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
    
    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
    
    cdev_del(&cdev);

    class_destroy(class);

    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");                                                  //声明模块拥有开源许可

验证

代码运行前,可以看到class中的注册信息
在这里插入图片描述
加载模块

insmod chrdev.ko

在这里插入图片描述
查看class

 ls /sys/class/

看到了我们注册的class类
在这里插入图片描述

在设备节点下创建设备函数

当使用上面的函数创建完成一个类后,使用device_create函数在这个类下创建一个设备

/include/linux/device.h
struct device *device_create(struct class *cls, struct device *parent,
                             dev_t devt, void *drvdata,
                             const char *fmt, ...);

参数class:设备要创建哪个类下面

参数parent:父设备

代码

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

#include <linux/cdev.h>
#include <linux/device.h>

#define DEVICE_NUMBER 1                                                 // 申请设备号的数量
#define DEVICE_SNAME "schrdev"                                          // 静态注册 名称
#define DEVICE_ANAME "achrdev"                                          // 动态注册 名称

#define DEVICE_MINOR_NUMBER 0                                           // 次设备号起始地址,通常是0

#define DEVICE_CLASS_NAME "chrdev_class"
#define DEVICE_NODE_NAME "chrdev_inode_test"

static int major_num,minor_num;
dev_t dev_num;                                                          // 保存设备号

struct cdev cdev;

struct class *class;
struct device *device;

/* module_param(name,type,perm);
 * name:要传递进去的参数的名称
 * type:类型
 * perm:参数读写的权限 S_IRUSR 400
 */
module_param(major_num, int, S_IRUSR);
module_param(minor_num, int, S_IRUSR);


int chrdev_open(struct inode *inode, struct file *file)
{
    printk("chrdev_open OK\n");                                         //chrdev_open正常
    return 0;
}

struct file_operations chrdev_ops = {
    .owner = THIS_MODULE,
    .open = chrdev_open,                                                // file_operations文件集操作,要定义函数chrdev_open
};

static int hello_init(void)
{
    int ret;

    if(major_num){                                                      // 如果major_num被手动设置,就使用静态注册设备号
        
        printk("major_num = %d\n", major_num);
        printk("minor_num = %d\n", minor_num);

        dev_num = MKDEV(major_num, minor_num);

        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
        
        if(ret < 0){                                                    // ret<0 注册失败
            printk("register_chrdev_region ERROR!!!\n");
        }else{
            printk("register_chrdev_region OK!!!\n");
        }

    }else{                                                              // major_num没有传入,所以动态申请设备号
                                                                  
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, DEVICE_NUMBER, DEVICE_ANAME);

        if(ret < 0){
            printk("alloc_chrdev_region ERROR!!!\n");
        }else{
            printk("alloc_chrdev_region OK!!!\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.owner = THIS_MODULE;

    cdev_init(&cdev, &chrdev_ops);

    cdev_add(&cdev, dev_num, DEVICE_NUMBER);

    class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);

    device = device_create(class, NULL, dev_num, NULL, DEVICE_NODE_NAME);
    
    return 0;
}

static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
    
    cdev_del(&cdev);

    device_destroy(class, dev_num);

    class_destroy(class);

    printk("see you again!!!\n");
}

module_init(hello_init);
module_exit(hello_exit);


MODULE_LICENSE("GPL");                                                  //声明模块拥有开源许可

结果

insmod chrdev.ko

在这里插入图片描述

ls /sys/class/

在这里插入图片描述

 ls /dev/

在这里插入图片描述
现在设备节点创建成功

rmmod chrdev.ko

测试卸载功能
在这里插入图片描述

使用

app代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;

    char buff[64] = {0};

    fd = open("/dev/chrdev_inode_test", O_RDWR);

    if(fd < 0){

        perror("open error\n");               // 在应用中打印
        return fd;
    }

    // read(fd, buff, sizeof(buff));

    // write(fd, buff,sizeof(buff));
    
    // close(fd);

    return 0;
}

编译

$CC app.c -o app

发送

scp app root@192.168.0.232:/tmp

在这里插入图片描述

运行

运行app可执行文件,可以执行,触发驱动open中的printk内容
在这里插入图片描述

源码

Linux嵌入式字符设备驱动模块(自动创建设备节点)modules_chrdev_auto

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值