如何理解Linux字符设备驱动?

news2024/11/16 6:33:33

我们学习编程的时候,一般都会从hello程序开始。同样的,学习Linux驱动,我们也是从最简单的hello驱动学起。

一、驱动层和应用层

看一下STM32裸机开发与嵌入式Linux开发的一些区别:

图片

图片

图片

嵌入式Linux的开发方式与STM32裸机开发的方式有点不一样。在STM32的裸机开发中,驱动层与应用层的区分可能没有那么明显,常常都杂揉在一起。

当然,有些很有水平的裸机程序分层分得还是很明显的。但是,在嵌入式Linux中,驱动和应用的分层是特别明显的,最直观的感受就是驱动程序是一个.c文件里,应用程序是另一个.c文件。

比如我们这个hello驱动实验中,我们的驱动程序为hello_drv.c、应用程序为hello_app.c。

驱动模块的加载有两种方式:第一种方式是动态加载的方式,即驱动程序与内核分开编译,在内核运行的过程中加载;第二种方式是静态加载的方式,即驱动程序与内核一同编译,在内核启动过程中加载驱动。

在调试驱动阶段常常选用第一种方式,因为较为方便;在调试完成之后才采用第二种方式与内核一同编译。

STM32裸机开发与嵌入式Linux开发还有一点不同的就是:STM32裸机开发最终要烧到板子的常常只有一个文件(除开含有IAP程序的情况或者其它情况),嵌入式Linux就需要分开编译、烧写。

二、Linux字符设备驱动框架

我们先看一张图:

图片

当我们的应用在调用open、close、write、read等函数时,为什么就能操控硬件设备。那是因为有驱动层在支撑着与硬件相关的操作,应用程序在调用打开、关闭、读、写等操作会触发相应的驱动层函数。

本篇笔记我们以hello驱动做分享,hello驱动属于字符设备。实现的驱动函数大概是怎么样的是有套路可寻的,这个套路在内核文件include/linux/fs.h中,这个文件中有如下结构体:

图片

这个结构体里的成员都是些函数指针变量,我们需要根据实际的设备确定我们需要创建哪些驱动函数实体。比如我们的hello驱动的几个基本的函数(打开/关闭/读/写)可创建为(以下代码来自:百问网):

(1)打开操作

左右滑动查看全部代码>>>

static int hello_drv_open (struct inode *node, struct file *file)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  return0;
}

打开函数的两个形参的类型要与struct file_operations结构体里open成员的形参类型一致,里面有一句打印语句,方便直观地看到驱动的运行过程。

(2)关闭操作

左右滑动查看全部代码>>>

static int hello_drv_close (struct inode *node, struct file *file)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  return0;
}

(3)读操作

左右滑动查看全部代码>>>

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_to_user(buf, kernel_buf, MIN(1024, size));
  return MIN(1024, size);
}

copy_to_user函数的原型为:

左右滑动查看全部代码>>>

static inline int copy_to_user(void __user *to, const void *from, unsigned long n);

用该函数来读取内核空间(kernel_buf)的数据给到用户空间(buf)。另外,kernel_buf的定义如下:

staticchar kernel_buf[1024];

MIN为宏:

#define MIN(a, b) (a < b ? a : b)

MIN(1024, size)作为copy_to_user的实参意在对拷贝的数据长度做限制(不能超出kernel_buf的大小)。

(4)写操作

左右滑动查看全部代码>>>

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__);
  err = copy_from_user(kernel_buf, buf, MIN(1024, size));
  return MIN(1024, size);
}

copy_from_user函数的原型为:

左右滑动查看全部代码>>>

static inline int copy_from_user(void *to,const void __user volatile *from,unsigned long n)

用该函数来将用户空间(buf)的数据传送到内核空间(kernel_buf)。

有了这些驱动函数,就可以给到一个struct file_operations类型的结构体变量hello_drv,如:

staticstruct file_operations hello_drv =
{
    .owner   = THIS_MODULE,
    .open    = hello_drv_open,
    .read    = hello_drv_read,
    .write   = hello_drv_write,
    .release = hello_drv_close,
};

有些朋友可能没见过这种结构体初始化的形式(结构体成员前面加个.号),这是C99及C11标准提出的指定初始化器。

上面这个结构体变量hello_drv容纳了我们hello设备的驱动接口,最终我们要把这个hello_drv注册给Linux内核。

套路就是这样的:把驱动程序注册给内核,之后我们的应用程序就可以使用open/close/write/read等函数来操控我们的设备,Linux内核在这里起到一个中间人的作用,把两头的驱动与应用协调得很好。

我们前面说了驱动的装载方式之一的动态装载:把驱动程序编译成模块,再动态装载。

动态装载的体现就是开发板已经启动运行了Linux内核,我们通过开发板串口终端使用命令来装载驱动。装载驱动有两个命令,比如装载我们的hello驱动:

  • 方法一:insmod hello_drv.ko

  • 方法二:modprobe hello_drv.ko

其中modprobe命令不仅能装载当前驱动,而且还会同时装载与当前驱动相关的依赖驱动。有了转载就有卸载,也有两种方式:

  • 方法一:rmmod hello_drv.ko

  • 方法二:modprobe -r hello_drv.ko

其中modprobe命令不仅卸载当前驱动,也会同时卸载依赖驱动。

我们在串口终端调用装载与卸载驱动的命令,怎么就会执行装载与卸载操作。对应到驱动程序里我们有如下两个函数:

module_init(hello_init); //注册模块加载函数
module_exit(hello_exit); //注册模块卸载函数

这里加载与注册有用到hello_inithello_exit函数,我们前面说的把hello_drv驱动注册到内核就是在hello_init函数里做,如:

左右滑动查看全部代码>>>

staticint __init hello_init(void)
{
  int err;

  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /* 注册hello驱动 */
  major = register_chrdev(0, 	/* 主设备号,为0则系统自动分配 */
                    "hello", 	/* 设备名称 */
                    &hello_drv); /* 驱动程序 */

  /* 下面操作是为了在/dev目录中生成一个hello设备节点 */
    /* 创建一个类 */
  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__);
    unregister_chrdev(major, "hello");
    return-1;
  }

    /* 创建设备,该设备创建在hello_class类下面 */
  device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
	
	return0;
}

这里这个驱动程序入口函数hello_init中注册完驱动程序之后,同时通过下面连个创建操作来创建设备节点,即在/dev目录下生成设备文件。

据我了解,在之前版本的Linux内核中,设备节点需要手动创建,即通过创建节点命令mknod 在/dev目录下自己手动创建设备文件。既然已经有新的方式创建节点了,这里就不抠之前的内容了。

以上就是分享关于驱动一些内容,通过以上分析,我们知道,其是有套路(就是常说的驱动框架)可寻的,比如:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 其她头文件...... */

/* 一些驱动函数 */
static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{

}

static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{

}

static int xxx_open (struct inode *node, struct file *file)
{

}

static int xxx_close (struct inode *node, struct file *file)
{

}
/* 其它驱动函数...... */

/* 定义自己的驱动结构体 */
staticstruct file_operations xxx_drv = {
  .owner	 = THIS_MODULE,
  .open    = xxx_open,
  .read    = xxx_read,
  .write   = xxx_write,
  .release = xxx_close,
  /* 其它程序......... */
};

/* 驱动入口函数 */
staticint __init xxx_init(void)
{

}

/* 驱动出口函数 */
staticvoid __exit hello_exit(void)
{

}

/* 模块注册与卸载函数 */
module_init(xxx_init);
module_exit(xxx_exit);

/* 模块许可证(必选项) */
MODULE_LICENSE("GPL");

按照这样的套路来开发驱动程序的,有套路可寻那就比较好学习了,至少不会想着怎么起函数名而烦恼,哈哈,按套路来就好。

关于驱动的知识,这篇笔记中还可以展开很多内容,限于篇幅就不展开了。我们之后再进行学习、分享。

下面看一下测试程序/应用程序(hello_drv_test.c中的内容,以下代码来自:百问网):

左右滑动查看全部代码>>>

#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);

  return0;
}

就是一些读写操作,跟我们学习文件操作是一样的。学单片机的有些朋友可能不太熟悉main函数的这种写法:

int main(int argc, char **argv)

main函数在C中有好几种写法(可查看往期笔记:main()函数有哪几种形式?),在Linux中常用这种写法。

argc与argv这两个值可以从终端(命令行)输入,因此这两个参数也被称为命令行参数。argc为命令行参数的个数,argv为字符串命令行参数的首地址。

最后,我们把编译生成的驱动模块hello_drv.ko与应用程序hello_drv_test放到共享目录录nfs_share中,同时在开发板终端挂载共享目录:

mount -t nfs -o nolock,vers=4192.168.1.104:/home/book/nfs_share /mnt

关于NFS网络文件系统的使用可查看往期笔记。

然后我们通过insmod 命令装载驱动,但是出现了如下错误:

图片

这是因为我们的驱动的编译依赖与内核版本,编译用的内核版本与当前开发板运行的内核的版本不一致所以会产生该错误。

重新编译内核,并把编译生成的Linux内核zImage映像文件与设备树文件*.dts文件拷贝到开发板根文件系统的/boot目录下,然后进行同步操作:

#mount -t nfs -o nolock,vers=4 192.168.1.114:/home/book/nfs_share /mnt
#cp /mnt/zImage /boot
#cp /mnt/.dtb /boot
#sync

图片

下面是完整的hello驱动程序(来源:百问网):

左右滑动查看全部代码>>>

#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>

/* 1. 确定主设备号                                                                 */
staticint major = 0;
staticchar kernel_buf[1024];
staticstruct class *hello_class;


#define MIN(a, b) (a < b ? a : b)

/* 3. 实现对应的open/read/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_to_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__);
  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__);
  return0;
}

static int hello_drv_close (struct inode *node, struct file *file)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  return0;
}

/* 2. 定义自己的file_operations结构体                                              */
staticstruct file_operations hello_drv =
{
  .owner	 = THIS_MODULE,
  .open    = hello_drv_open,
  .read    = hello_drv_read,
  .write   = hello_drv_write,
  .release = hello_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
staticint __init hello_init(void)
{
  int err;

  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */


  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__);
    unregister_chrdev(major, "hello");
    return-1;
  }

  device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */

  return0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
staticvoid __exit hello_exit(void)
{
  printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
  device_destroy(hello_class, MKDEV(major, 0));
  class_destroy(hello_class);
  unregister_chrdev(major, "hello");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1106822.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数值分析】1 - 误差及有关概念

文章目录 一、误差的背景介绍1.1 误差的来源与分类1.2 误差的传播与积累1.3 例题1.3.1 公式一1.3.2 公式二1.3.3 总结 二、误差与有效数字2.1 绝对误差与绝对误差限2.2 相对误差和相对误差限 三、有效数字3.1 有效数字的定义和标准浮点式3.1.1 例题 3.2 有效数字与相对误差的关…

【一文清晰】单元测试到底是什么?应该怎么做?

我是java程序员出身&#xff0c;后来因为工作原因转到到了测试开发岗位。测试开发工作很多年后&#xff0c;现在是一名自由职业者 1、什么是单元测试 2、该怎么做单元测试 一、什么是单元测试&#xff1f; 单元测试&#xff08;unit testing&#xff09;&#xff0c;是指对软件…

4款文件恢复工具推荐,恢复数据就靠它们!

“想问问大家在使用电脑时有什么好的文件恢复工具推荐吗&#xff1f;最近好像有点粗心&#xff0c;经常误删文件&#xff0c;非常需要一个有用的恢复工具&#xff0c;希望大家给些意见&#xff01;” 在日常工作或学习中&#xff0c;由于各种原因造成的文件丢失是很正常的情况。…

什么是模拟芯片,模拟芯片都有哪些测试指标?

模拟芯片又称处理模拟信号的集成电路 模拟集成电路主要是指由电容、电阻、晶体管等组成的模拟电路集成在一起用来处理模拟信号的集成电路。有许多的模拟集成电路&#xff0c;如运算放大器、模拟乘法器、锁相环、电源管理芯片等。 模拟集成电路的主要构成电路有&#xff1a;放…

打造属于自己的vue图标库

hfex-icon图标库 Install npm i -D hfex-icon主要提供2种使用方式 方式一 通过svg图标资源&#xff0c;借助unplugin-icons库将svg图标文件生成vue组件&#xff0c;然后通过vue组件的引入方式在vue中使用 unplugin-icons 兼容vue2和vue3 在vue.config.js的plugins中配置…

“轻松实现文件夹批量重命名:使用顺序编号批量改名“

你是否曾经需要大量修改文件夹名称&#xff1f;或者需要为文件夹添加有序编号以便于管理&#xff1f;下面就教你一个简单的方法&#xff0c;轻松实现文件夹批量重命名&#xff0c;使用顺序编号批量改名。 首先我们要进入文件批量改名高手主页面&#xff0c;并在板块栏里选择“文…

java springboot VUE粮食经销系统开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 springboot VUE 粮食经销系统是一套完善的完整信息管理类型系统&#xff0c;结合springboot框架和VUE完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09; &#xff0c;系统具有完整的源代码和数…

kafka、zookeeper、flink测试环境、docker

1、kafka环境单点 根据官网版本说明(3.6.0)发布&#xff0c;zookeeper依旧在使用状态&#xff0c;预期在4.0.0大版本的时候彻底抛弃zookeeper使用KRaft(Apache Kafka)官方并给出了zk迁移KR的文档 2、使用docker启动单点kafka 1、首先将kafka启动命令&#xff0c;存储为.servi…

LLC 三相移相PWM产生原理分析

LLC 三相移相PWM产生原理分析 void MX_PWM_Stop(void) {//----------------------使用停止函数后会导致移相角度为60度---------------------------------------------------------------- #if 1 //------Tim1 PWM定时器初始化------------------ HAL_TIM_OC_Stop(&htim1…

最新Google play开发者账号注册要求、单位账号邓白氏编码问题及身份验证解决思路!

随着Google play商店的蓬勃发展&#xff0c;越来越多的开发者在上面上传应用&#xff0c;但部分开发者为了获得更多的收益&#xff0c;试图通过多种方式绕过谷歌限制&#xff0c;无视谷歌规则。 为了维持Google play的生态环境&#xff0c;谷歌采取了多种方式和方法去应对这些…

决策树oo

决策树学习的算法通常是一个递归地选择最优特征(选择方法的不同&#xff0c;对应着不同的算法)&#xff0c;并根据该特征对训练数据进行分割&#xff0c;使得各个子数据集有一个最好的分类的过程。这一过程对应着对特征空间的划分&#xff0c;也对应着决策树的构建 步骤&#…

git切换远程仓库源步骤

git切换远程仓库源步骤&#xff1a; 第一步&#xff1a;git remote -v 查看当前远程仓库源&#xff1a; 第二步&#xff1a;git remote rm origin删除远程仓库源&#xff1b; 第三步&#xff1a;git remote add origin http://newURL.git 添加新的远程仓库源地址&#xff1b…

Operator 开发实践 四 (WebHook)

1. WebHook介绍 我们知道访问Kubernetes API有好几种方式&#xff0c;比如使用kubectl命令、使用client-go之类的开发库、直接通过REST请求等。不管是一个使用kubectl的真人用户&#xff0c;还是一个Service Account&#xff0c;都可以通过API访问认证&#xff0c;这个过程官网…

AI机器视觉多场景应用迸发检测活力,引领食品及包装行业新发展

随着食品安全意识的广泛传播&#xff0c;人们对食品质量和安全的要求越来越高&#xff0c;众多食品包装厂商加速产线数智化转型&#xff0c;迫切需要高效、准确且智能化的检测技术。 在现代食品及包装行业的自动化生产中&#xff0c;涉及到各种各样的识别、检测、测量等环节&a…

LabVIEW中管理大型数据

LabVIEW中管理大数据 LabVIEW的最大优势之一是自动内存管理。这种内存管理允许用户轻松创建字符串、数组和集群&#xff0c;而无需C/C用户经常担心。但是&#xff0c;这种内存管理设计为绝对安全&#xff0c;因此数据被非常频繁地复制。这通常不会造成任何问题&#xff0c;但是…

Linux下使用openssl制作证书

openssl是一个功能丰富且自包含的开源安全工具箱。它提供的主要功能有&#xff1a;SSL协议实现(包括SSLv2、SSLv3和TLSv1)、大量软算法(对称/非对称/摘要)、大数运算、非对称算法密钥生成、ASN.1编解码库、证书请求(PKCS10)编解码、数字证书编解码、CRL编解码、OCSP协议、数字证…

Go语言和Python语言哪个比较好?

目录 1、性能 2、开发效率和易用性 3、社区支持 4、语法 5、其他因素 总结 Go语言和Python语言都是非常优秀的编程语言&#xff0c;它们各自具有不同的优势和适用场景。在选择哪种语言更适合您的项目时&#xff0c;需要考虑多个方面&#xff0c;包括性能、开发效率、可读…

软文为什么成为企业降本增效的营销利器?

如今企业营销面临的重大课题之一就是如何降低成本&#xff0c;增强宣传效果&#xff0c;传统营销模式集中于线下&#xff0c;不仅要花费大量的时间、金钱成本&#xff0c;还需要花费大量的人力成本。而网络技术的发展&#xff0c;使企业营销的方式更加多样化&#xff0c;其中软…

python tk展示图片

from tkinter import * from PIL import ImageTk, Imageroot Tk() root.title("展示图片")# 打开图片 image Image.open("DSC_2040.jpg") # 调整图片大小 image image.resize((300, 300), Image.ANTIALIAS) # 创建图像对象 img ImageTk.PhotoImage(ima…

P2181 对角线

#include<bits/stdc.h> using namespace std; unsigned long long n,ans; int main() {scanf("%lld",&n);ansn * (n-1) / 2 * (n-2) / 3 * (n-3) / 4;printf("%lld\n",ans);return 0; }