嵌入式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

Kconfig

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

gedit drivers/Kconfig

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

source "drivers/user/Kconfig"

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

gedit drivers/user/Kconfig

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

menu "User drivers"

source "drivers/user/char_dev/Kconfig"

endmenu

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

gedit drivers/user/char_dev/Kconfig

写入下面内容:

config USER_CHAR_DEV
	tristate "char_dev"
	default n
	help
	char_dev driver test.

上面内容中 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

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

obj-y				+= user/

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

gedit drivers/user/Makefile

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

obj-y				+= char_dev/

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

gedit drivers/user/char_dev/Makefile

写入下面内容:

obj-$(CONFIG_USER_CHAR_DEV) += char_dev.o

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

驱动程序

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

gedit drivers/user/char_dev/char_dev.c

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

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/device.h>

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"); // 模块许可

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

编译与测试

模块方式

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

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

在这里插入图片描述


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

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

在这里插入图片描述

编译到内核中

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

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

在这里插入图片描述

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

# 在开发板中挂载启动分区
# mount /dev/mmcblk0p1 /mnt/

# 在虚拟机中拷贝编译生成的内核到开发板
# scp ../image/980uimage root@192.168.31.142:/mnt/

在这里插入图片描述


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

总结

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340