Linux驱动编程篇(一)—Hello驱动(字符驱动程序)(不涉及硬件)

本文详细介绍了Linux驱动程序的开发过程,包括在应用层调用glibc时系统的处理步骤,以及如何编写字符设备驱动。从确定主设备号、定义file_operations结构体,到实现open、read、write、release等函数,再到注册和卸载驱动的入口和出口函数,最后通过实例演示了一个简单的驱动程序。文章还提到了驱动程序的测试和加载,并强调了内核版本与开发板的一致性重要性。

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

一、在应用层的APP上调用glibc,Linux系统会做哪些事?

如果访问的是普通文件,则会经系统调用后访问。
如果是驱动程序则会通过调用驱动程序对应的代码进行访问(提供drv_open),如下图。
在这里插入图片描述

二、编写驱动程序的几个步骤

① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 实现入口函数:安装驱动程序时,就会去调用这个入口函数,执行工作:1、把 file_operations 结构体告诉内核:register_chrdev. 2、创建类class_create. 3、创建设备device_create.
⑤ 实现出口函数:卸载驱动程序时,就会去调用这个入口函数,执行工作:1、把 file_operations 结构体从内核注销:unregister_chrdev. 2、销毁类class_create. 3、销毁设备结点class_destroy.
⑥ 其他完善:GPL协议、驱动作者、驱动面熟

int open(const char *pathname,int flags,mode_t mode);/pathname:指向文件路径的指针;flags:文件权限;mode:创建模式/
/成功:返回文件描述符/

三、编写第一个驱动程序(字符设备驱动)

实现的功能:

参考 driver/char 中的程序,包含头文件,写框架,传输数据:
1、驱动中实现 open, read, write, release,APP 调用这些函数时,都打印内核信息
2、APP 调用 write 函数时,传入的数据保存在驱动中
3、APP 调用 read 函数时,把驱动中保存的数据返回给 APP

学习目的:

主要是学习别人的框架,理解其原理。

1、打开内核目录下\Linux-5.4\drivers\char中的misc.c,这是一个比较经典的字符驱动程序
第一步、添加需要的头文件

misc.c中它添加了如下的头文件,我们也参照它(后序肯定会把它们都研究透彻,第一次学习先跳过),直接复制进我们的代码中。
在这里插入图片描述

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
第二步、确定主设备号,也可以让内核分配

我们这里自己设个0

static int major = 0;
第三步、定义自己的 file_operations 结构体(存放驱动函数,如drv_open等)

我们可以先看一下 file_operations 结构体 有哪些成员:
在这里插入图片描述

我们当然需要填写那么多成员,因此我们参照misc.c中定义的file_operations结构体,发现除了驱动函数函数外,还需要一个.owner,那我们也在自己的程序中添加。
在这里插入图片描述
除了添加.owner之外,根据我们刚才的项目要实现的功能,我们还需要在结构体中添加四个函数,分别实现open, read, write, release功能。我们继续借鉴misc.c文件中的命名方式和函数的写法(模板很重要
我们misc.c文件内的模板如下:
在这里插入图片描述
同时参照file_operations结构体中的四个函数的定义:
在这里插入图片描述

第四步、实现file_operations 结构体中的函数
//③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
static ssize_t (*hello_drv_read) (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	err = _copy_from_user(buf,kernel_buf, MIN(1024, size));
	return MIN(1024, size);
}
static ssize_t (*hello_drv_write) (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	//_copy_from_user(void * to, const void __user * from, unsigned long n)
	err = _copy_from_user(kernel_buf,buf, MIN(1024, size));
	return MIN(1024,size);
	
}
static int (*hello_drv_open) (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	return 0;
}
static int (*hello_drv_close) (struct inode *node, struct file *file);
{
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	return 0;
}

注:
1、printk是内核里进行打印的函数,printf是应用层打印的函数

2、_copy_from_user()

第五步、实现入口函数,注册file_operations 结构体

首先我们先参考misc.c里的入口函数编写
在这里插入图片描述
(1)注册file_operations 结构体
(2)创建类
(3)创建设备节点(应用程序若想要访问某个驱动程序,需要依赖与某个设备结点
实现我们自己的入口函数:

static int __init hello_init(void)
{
	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	major=register_chrdev(0,"hello", &hello_drv);//注册file_operations 结构体
	hello_class = class_create(THIS_MODULE, "hello_class");//创建类
	err = PTR_ERR(hello_class);
	if (IS_ERR(hello_class)){
		printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
		return -1;
		unregister_chrdev(major,"hello");
	}
	device_create(hello_class, NULL, MKDEV(major,0), NULL, "hello" );/*创建设备节点 以后可以通过/dev/hello访问该结点 */
	return 0;
}
第六步、实现出口函数

但是在misc.c并未有相应例子,我们再去找一个经典案例参考内核目录下的\Linux-5.4\drivers\char中的pc8736x_gpio.c,我们实现我们的出口函数():
在这里插入图片描述
(1)注销file_operations 结构体
(2)销毁类
(3)销毁设备节点
实现我们自己的出口函数:

	printk("%s %s line %d\n",__FILE__,__FUNCTION__,__LINE);
	device_destroy(hello_class,MKDEV(major,0));//销毁设备节点
	class_destroy(hello_class);//销毁类
	unregister_chrdev(major,"hello");//注销file_operations 结构体
	return 0;
第七步、对入口、出口函数进行宏修饰

为什么需要宏修饰?因为我们需要告诉内核我们的入口和出口函数是哪一个?
但是在misc.c并未有相应例子,我们再去找一个经典案例参考内核目录下的\Linux-5.4\drivers\char中的pc8736x_gpio.c
在这里插入图片描述
我们也是嫖到自己的程序里,生成自己的宏修饰。

module_init(hello_init);
module_exit(hello_exit);
第八步、添加一些协议(GPL)、作者、描述

在这里插入图片描述
GPL协议:

GNU通用公共许可证简称为GPL,是由自由软件基金会发行的用于计算机软件的协议证书,使用该证书的软件被称为自由软件。大多数的GNU程序和超过半数的自由软件使用它。 下面的正文是自由软件基金会GNU通用公共许可证原始文档的副本。Linux操作系统以及与它有关的大量软件是在GPL的推动下开发和发布的。你将看到:如果你打算为了发布的目的修改,更新或改进任何受通用公共许可证约束的软件,你所修改的软件同样必须受到GNU通用许可证条款的约束。

若未遵守该协议内核的函数将无法使用,因为整个linux内核都遵循该协议。
我们这步的代码:

MODULE_AUTHOR("aipolo <xxx@qq.com>");
MODULE_DESCRIPTION("hello Driver");
MODULE_LICENSE("GPL");
第九步、编译驱动文件

(1)修改Makefile文件内的内核地址
在这里插入图片描述
Makefile内文件内容:

/*以下为韦东山老师编写*/
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4

all:
        make -C $(KERN_DIR) M=`pwd` modules
        $(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c

clean:
        make -C $(KERN_DIR) M=`pwd` modules clean
        rm -rf modules.order
        rm -f hello_drv_test

obj-m   += hello_drv.o

(2)生成hello_drv.ko驱动文件和hello_drv_test可执行文件
在这里插入图片描述

第十步、在开发板上安装驱动文件

将第十一步的hello_drv.ko驱动文件和hello_drv_test可执行文件移到开发板上(具体操作省略)
在这里插入图片描述
(1)安装驱动程序

insmod hello_drv.ko

(2)查看安装是否成功

lsmod

在这里插入图片描述
(3)查看设备节点

cat /proc/devices

在这里插入图片描述
(4)查看节点的主次设备号
在这里插入图片描述
根据图示,主设备号为241,次设备号为0
我这边是241,不同机型值都可能不同

第十一步、执行测试程序

测试程序内容:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
 * ./hello_drv_test -w abc
 * ./hello_drv_test -r
 */
int main(int argc, char **argv)
{
        int fd;
        char buf[1024];
        int len;
        /* 1. 判断参数 */
        if (argc < 2)
        {
                printf("Usage: %s -w <string>\n", argv[0]);
                printf("       %s -r\n", argv[0]);
                return -1;
        }
        /* 2. 打开文件 */
        fd = open("/dev/hello", O_RDWR);
        if (fd == -1)
        {
                printf("can not open file /dev/hello\n");
                return -1;
        }
        /* 3. 写文件或读文件 */
        if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
        {
                len = strlen(argv[2]) + 1;
                len = len < 1024 ? len : 1024;
                write(fd, argv[2], len);
        }else
        {
                len = read(fd, buf, 1024);
                buf[1023] = '\0';
                printf("APP read : %s\n", buf);
        }
        close(fd);
        return 0;
}
./hello_drv_test -w 
./hello_drv_test -r

在这里插入图片描述

大功告成啦

总结时刻

驱动的开发流程:

有个重要补充就是虚拟机上的Linux内核版本需要和开发板的内核版本一致
在这里插入图片描述

单个驱动的编写流程:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值