• 嵌入式Linux驱动开发 02:将驱动程序添加到内核中


    目的

    在上一篇文章 《嵌入式Linux驱动开发 01:基础开发与使用》 中我们已经实现了最基础的驱动功能。在那篇文章中我们的驱动代码是独立于内核代码存放的,并且我们的驱动编译后也是一个独立的模块。在实际使用中将驱动代码放在内核代码中,并将驱动编译到内核中也是比较常见的选择,这篇文章将此进行介绍。

    这篇文章中内容均在下面的开发板上进行测试:
    《新唐NUC980使用记录:自制开发板(基于NUC980DK61YC)》

    这篇文章主要是在下面文章基础上进行的:
    《新唐NUC980使用记录:访问以太网(LAN8720A) & 启用SSH》

    基础说明

    将驱动程序添加到内核中可以分为两层含义来理解:

    1. 将驱动程序源码等放到 Linux Kernel 源码目录下
      Linux Kernel 源码中通常将驱动放到 drivers/ 下,本文中也将驱动源码放到这个目录下;
    2. 在 Linux Kernel 配置管理工具中统一管理与编译
      这条主要指可以在 menuconfig 中进行配置管理来选择编译(这里先不讨论使用设备树的情况);
      menuconfig 界面中各个菜单和选项都是由 Kconfig 文件定义的,所以我们需要修改和编写相关文件;
      menuconfig 中配置最终改变的是 make 时 Makefile 文件中各个变量,我们的自己的驱动也需要 Makefile 文件来指定编译规则,并结合 Kconfig 文件中定义的变量来控制编译过程;

    添加到内核中

    本文中演示中涉及目录与文件结构组织如下:
    在这里插入图片描述
    其中 char_dev 就是本文中要添加的驱动。 user/ 目录用于统一存放自己编写的驱动,如果没有这个需求这一层可以去掉,这样结构上会更简单些,当然推荐还是留着。各目录下的 Kconfig 是一层层应用的, Makefile 同理。

    进入源码目录并建立相关目录和文件:

    cd ~/nuc980-sdk/NUC980-linux-4.4.y/
    
    mkdir -p drivers/user
    touch drivers/user/Kconfig
    touch drivers/user/Makefile
    
    mkdir -p drivers/user/char_dev
    touch drivers/user/char_dev/char_dev.c
    touch drivers/user/char_dev/Kconfig
    touch drivers/user/char_dev/Makefile
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Kconfig

    首先修改drivers目录下Kconfig文件:

    gedit drivers/Kconfig
    
    • 1

    在其中添加下面一行,用来引用drivers/user目录下的Kconfig文件:

    source "drivers/user/Kconfig"
    
    • 1

    接着编辑drivers/user目录下的Kconfig文件:

    gedit drivers/user/Kconfig
    
    • 1

    写入下面内容,用来引用drivers/user/char_dev目录下的Kconfig文件:

    menu "User drivers"
    
    source "drivers/user/char_dev/Kconfig"
    
    endmenu
    
    • 1
    • 2
    • 3
    • 4
    • 5

    最后编辑drivers/user/char_dev目录下的Kconfig文件:

    gedit drivers/user/char_dev/Kconfig
    
    • 1

    写入下面内容:

    config USER_CHAR_DEV
    	tristate "char_dev"
    	default n
    	help
    	char_dev driver test.
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面内容中 config USER_CHAR_DEV 表示设置一个可配置的变量,名称为 USER_CHAR_DEV (保存后实际的变量名会在头部添加 CONFIG_CONFIG_USER_CHAR_DEV ,这个变量可以在Makefile中使用)。 tristate 表示该变量可取值为 n/y/m ,后面的字符串为该条目在 menuconfig 中显示的文本。 default n 表示该变量默认值。 help 表示其下面的内容是可在 menuconfig 中查看帮助信息。

    经过上面处理后就可以在 menuconfig 中看到相关选项并进行操作了:
    在这里插入图片描述

    Makefile

    首先修改drivers目录下Makefile文件:

    gedit drivers/Makefile
    
    • 1

    在其中添加下面一行,这样编译时会进入drivers/user目录下:

    obj-y				+= user/
    
    • 1

    接着编辑drivers/user目录下的Makefile文件:

    gedit drivers/user/Makefile
    
    • 1

    写入下面内容,这样编译时会进入drivers/user/char_dev目录下:

    obj-y				+= char_dev/
    
    • 1

    最后编辑drivers/user/char_dev目录下的Makefile文件:

    gedit drivers/user/char_dev/Makefile
    
    • 1

    写入下面内容:

    obj-$(CONFIG_USER_CHAR_DEV) += char_dev.o
    
    • 1

    上面就是最终编译驱动程序过程了,这里的 CONFIG_USER_CHAR_DEV 变量就是由前面配置来产生的。根据变量的值,其最终可能产生 obj-nobj-yobj-m 几个结果,这几个是 Linux Kernel 源码总的Makefile中定义的变量,添加到 obj-y 的内容会编译到内核中,添加到 obj-m 的内容会编译成单独的模块。

    驱动程序

    最后编辑下进行测试用的驱动程序:

    gedit drivers/user/char_dev/char_dev.c
    
    • 1

    直接使用上一篇文章的程序即可:

    #include 
    #include 
    #include 
    #include 
    
    static int major = 0;
    static const char *char_dev_name = "char_dev";
    static struct class *char_dev_class;
    static struct device *char_dev_device;
    
    static char dev_buf[4096];
    
    #define MIN(a, b) ((a) < (b) ? (a) : (b))
    
    static int char_dev_open(struct inode *node, struct file *file)
    {
    	return 0;
    }
    
    static int char_dev_close(struct inode *node, struct file *file)
    {
    	return 0;
    }
    
    static ssize_t char_dev_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_to_user(buf, dev_buf, MIN(size, 4096)); // 从内核空间拷贝数据到用户空间
    	return ret;
    }
    
    static ssize_t char_dev_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
    {
    	int ret;
    	ret = copy_from_user(dev_buf, buf, MIN(size, 4096)); // 从用户空间拷贝数据到内核空间
    	return ret;
    }
    
    static const struct file_operations char_dev_fops = {
    	.owner = THIS_MODULE,
    	.open = char_dev_open,
    	.release = char_dev_close,
    	.read = char_dev_read,
    	.write = char_dev_write,
    };
    
    static int __init char_dev_init(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    	major = register_chrdev(0, char_dev_name, &char_dev_fops); // 注册字符设备,第一个参数0表示让内核自动分配主设备号
    
    	char_dev_class = class_create(THIS_MODULE, "char_dev_class"); // 
    	if (IS_ERR(char_dev_class))
    	{
    		unregister_chrdev(major, char_dev_name);
    		return -1;
    	}
    	char_dev_device = device_create(char_dev_class, NULL, MKDEV(major, 0), NULL, char_dev_name); // 创建设备节点创建设备节点,成功后就会出现/dev/char_dev_name的设备文件
    	if (IS_ERR(char_dev_device))
    	{
    		device_destroy(char_dev_class, MKDEV(major, 0));
    		unregister_chrdev(major, char_dev_name);
    		return -1;
    	}
    
    	return 0;
    }
    
    static void __exit char_dev_exit(void)
    {
    	printk("modlog: func %s, line %d.\n", __FUNCTION__, __LINE__);
    
    	device_destroy(char_dev_class, MKDEV(major, 0)); // 销毁设备节点,销毁后/dev/下设备节点文件就会删除
    	class_destroy(char_dev_class);
    
    	unregister_chrdev(major, char_dev_name); // 注销字符设备
    }
    
    module_init(char_dev_init); // 模块入口
    module_exit(char_dev_exit); // 模块出口
    
    MODULE_LICENSE("GPL"); // 模块许可
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    这个驱动程序在安装和卸载时打印了一些消息,可以通过此判断驱动程序是否工作。

    编译与测试

    模块方式

    menuconfig 将控制驱动的选项选择为模块后进行编译:

    # make menuconfig
    export PATH=$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/bin
    # 可以使用make整体编译或使用make modules编译单独模块
    # make 
    make modules
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    最后编译生成的模块默认在模块源码目录下,可以拷贝到开发板中进行测试:

    # scp drivers/user/char_dev/char_dev.ko root@192.168.31.142:/root/
    
    • 1

    在这里插入图片描述

    编译到内核中

    menuconfig 将控制驱动的选项选择为y后进行编译:

    # make menuconfig
    # export PATH=$PATH:/home/nx/nuc980-sdk/arm_linux_4.8/bin
    make uImage
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    编译完成后拷贝内核文件到开发板boot分区:

    # 在开发板中挂载启动分区
    # mount /dev/mmcblk0p1 /mnt/
    
    # 在虚拟机中拷贝编译生成的内核到开发板
    # scp ../image/980uimage root@192.168.31.142:/mnt/
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    开发板重启后可以看到驱动程序在内核启动时自动启动了,可以看到打印的信息以及 /dev/ 下的设备文件。

    总结

    将驱动程序添加到内核中还是比较简单的,按照内核源码本身的组织方式来进行就行了。更多的示例可以参考内核源码 drivers/ 目录下各个驱动。

  • 相关阅读:
    IDEA如何拉取gitee项目?
    nginx 学习笔记
    用Python操作PPT的办公自动化教程
    [课程笔记](图像分割入门到实战)
    【Dart】dart之mixin探究
    TensorFlow基本概念:张量、计算图
    长安链源码学习v2.2.1--ioc机制(九)
    YApi、Swagger
    C# 9.0 record和with的定义及使用
    一种基于目标检测实现黑花屏分类任务的方案
  • 原文地址:https://blog.csdn.net/Naisu_kun/article/details/126362277