当前位置: 首页 > news >正文

Linux字符设备驱动开发

旧模板在2.3小节。

新模版在5.3小节。

应用程序和驱动的交互原理

驱动就是获取外设或者传感器数据,控制外设。数据会提交给应用程序。Linux驱动编写既要编写一个驱动,还要编写一个简单的测试应用程序,APP。

单片机下驱动和应用都是放在一个文件里面,杂糅到一起。Linux下驱动和应用是完全分开的。

用户空间和内核空间

Linux操作系统内核和驱动程序运行在内核空间,应用程序运行在用户空间。

应用程序想要访问内核资源:系统调用、异常、陷入。

应用程序使用open函数打开一个设备文件。

应用程序通过API函数来间接的调用系统调用。每一个系统调用都有一个系统调用号。

一、字符设备驱动简介

字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。

  • Linux里面一切皆文件,驱动设备表现就是一个/dev/下的文件,如/dev/led,应用程序调用open函数打开一个设备的时候,比如led。应用程序通过write函数向/dev/led写数据。关闭设备close。
  • 编写驱动的时候,需要编写驱动的open、close、write函数。字符设备驱动struct file_operations。

  • 写驱动的时候要考虑应用开发的便利性。驱动分框架,要按照驱动框架来编写。

二、字符设备驱动开发步骤

2.1 驱动模块的加载和卸载

Linux驱动运行方式:

  • 编译进Linux内核,内核启动自动运行
  • 编译成模块(.ko),通过modprobe或insmod加载

 注:

  • insmod不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。
  • modprobe会自动分析模块的依赖关系,默认回去/lib/modules/<kernel-version>查找模块。

卸载命令:rmmod drv.ko;modeprobe -r drv 

在编写驱动的时候需要注册加载和卸载函数:

//字符设备驱动模块加载和卸载函数模板
/* 驱动入口函数 */
static int __init xxx_init(void)
{/* 入口函数具体内容 */return 0;
}/* 驱动出口函数 */
static void __exit xxx_exit(void)
{/* 出口函数具体内容 */
}module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数

2.2 字符设备注册与注销

static inline int register_chrdev(unsigned int major,//主设备号const char *name,//设备名字const struct file_operations *fops//指向设备的操作函数集合变量
)static inline void unregister_chrdev(unsigned int major,//主设备号const char *name//设备名
)//加入字符设备注册和注销static struct file_operations test_fops;/* 驱动入口函数 */
static int __init xxx_init(void)
{/* 入口函数具体内容 */int retvalue = 0;/* 注册字符设备驱动 */retvalue = register_chrdev(200, "chrtest", &test_fops);if(retvalue < 0){/* 字符设备注册失败,自行处理 */}return 0;
}/* 驱动出口函数 */
static void __exit xxx_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(200, "chrtest");
}/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

2.3实现设备的具体操作函数

旧API模板

//加入设备操作函数
#define XXX_MAJOR		200		/* 主设备号 */
#define XXX_NAME		"xxx" 	/* 设备名字 *//* 打开设备 */
static int xxx_open(struct inode *inode, struct file *filp)
{/* 用户实现具体功能 */return 0;
}/* 从设备读取 */
static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{/* 用户实现具体功能 */return 0;
}/* 向设备写数据 */
static ssize_t xxx_write(struct file *filp,const char __user *buf,size_t cnt, loff_t *offt)
{/* 用户实现具体功能 */return 0;
}/* 关闭/释放设备 */
static int xxx_release(struct inode *inode, struct file *filp)
{/* 用户实现具体功能 */return 0;
}static struct file_operations xxx_fops = {.owner = THIS_MODULE,.open = xxx_open,.read = xxx_read,.write = xxx_write,.release = xxx_release,
};/* 驱动入口函数 */
static int __init xxx_init(void)
{/* 入口函数具体内容 */int retvalue = 0;/* 注册字符设备驱动 */retvalue = register_chrdev(XXX_MAJOR, XXX_NAME, &xxx_fops);if(retvalue < 0){/* 字符设备注册失败,自行处理 */}return 0;
}/* 驱动出口函数 */
static void __exit xxx_exit(void)
{/* 注销字符设备驱动 */unregister_chrdev(XXX_MAJOR, XXX_NAME);
}/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);MODULE_LICENSE() //添加模块 LICENSE 信息。“GPL”,采用GPL协议
MODULE_AUTHOR() //添加模块作者信息
MODULE_INFO(intree, "Y");

2.4 添加LICENSE和作者信息

MODULE_LICENSE() //添加模块 LICENSE 信息。“GPL”,采用GPL协议
MODULE_AUTHOR() //添加模块作者信息

三、Linux设备号

3.1 设备号的组成

typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;高 12 位为主设备号,低 20 位为次设备号include/linux/kdev_t.h:#define MINORBITS 20#define MINORMASK ((1U << MINORBITS) - 1)#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

3.2 设备好的分配

  • 静态分配设备号:驱动开发者静态指定的设备号(查看文档 Documentation/devices.txt)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • 动态分配设备号:避免设备号冲突。
int alloc_chrdev_region(dev_t *dev, //保存申请到的设备号unsigned baseminor, //次设备号起始地址unsigned count, //要申请的设备号数量const char *name //设备名字
)void unregister_chrdev_region(dev_t from, //要释放的设备号unsigned count //表示从from开始,要释放的设备号数量
)

四、chrdevbase 字符设备驱动开发实验

4.1 实验程序编写

  1. 新建Linux_Drivers文件夹
  2. 创建01_chrdevbase的VSCode工程
  3. 添加头文件路径(Ctrl+Shift+P,Edit configurations,在includePath添加头文件路径)
  4. 编写实验程序

4.2 编写测试APP

open/close/write/read参见Linux-应用编程学习笔记(二、文件I/O、标准I/O)

通过调用open/close/write/read来控制设备。

捋顺一下思路:驱动文件编译完成之后是.KO文件,通过modeprobe加载到/dev里面,比如设备叫chrdevbase,在APP中open(/dev/chrdevbase),进而调用驱动里面的open函数。

4.3 编译驱动程序和测试APP

编译驱动程序

KERNELDIR := /home/zuozhongkai/linux/RV1126/alientek_sdk/kernel #KERNELDIR 表示开发板所使用的 Linux 内核源码目录,使用绝对路径
CURRENT_PATH := $(shell pwd) #通过运行“pwd”命令来获取当前所处路径
obj-m := chrdevbase.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules # modules 表示编译模块, -C 表示将当前的工作目录切换到指定目录中,也就是 KERNERLDIR 目录。 M 表示模块源码目录,“make modules”命令中加入 M=dir 以后程序会自动到指定的 dir 目录中读取模块的源码并将其编译为.ko 文件。
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean//Makefile编写好后,编译驱动模块
make ARCH=arm //ARCH=arm 必须指定,否则编译会失败

编译测试APP

/opt/atk-dlrv1126-toolchain/bin/arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

4.4 运行测试

1、使用 ADB 将驱动模块和测试 APP 发送到开发板

adb push chrdevbase.ko chrdevbaseApp /lib/modules/4.19.111

2、加载驱动模块

modprobe chrdevbase//报错找不到“modules.dep”文件
depmod 
//没有depmod命令的重新配置busybox,使能此命令,重新编译busybox

3、创建设备节点文件

mknod /dev/chrdevbase c 200 0

4、chrdevbase 设备操作测试

./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2

5、卸载驱动模块

rmmod chrdevbase

五、新字符设备驱动

5.1 分配和释放设备号

如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)unregister_chrdev_region(devid, 1); /* 注销设备号 */

5.2 新字符设备注册方法

struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
} __randomize_layout;void cdev_init(struct cdev *cdev, const struct file_operations *fops)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_del(struct cdev *p)

5.3 自动创建设备节点

为了省去modprobe加载驱动后,手动mknod创建设备节点。

udev机制:udev使用用户程序,开发板启动会启动udev,实现设备节点文件的创建与删除。

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面
加自动创建设备节点相关代码。

struct class * class_create(owner, name);
void class_destroy(struct class *cls);

创建设备

struct device *device_create(struct class *class, //设备创建到哪个类struct device *parent, //父设备,没有NULLdev_t devt, //设备号void *drvdata, //数据,NULLconst char *fmt, //设备名字,xxx->/dev/xxx ...
)void device_destroy(struct class *cls, dev_t devt)//创建/删除类/设备参考代码
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 *//* 驱动入口函数 */
static int __init xxx_init(void)
{/* 创建类 */class = class_create(THIS_MODULE, "xxx");/* 创建设备 */device = device_create(class, NULL, devid, NULL, "xxx");return 0;
}/* 驱动出口函数 */
static void __exit led_exit(void)
{/* 删除设备 */device_destroy(newchrled.class, newchrled.devid);/* 删除类 */class_destroy(newchrled.class);
}module_init(led_init);
module_exit(led_exit);

新API模板

#define NEWCHRXXX_CNT			1		  	/* 设备号个数 */
#define NEWCHRXXX_NAME			"newchrxxx"	/* 名字 *//* newchrled设备结构体 */
struct newchrxxx_dev{dev_t devid;			/* 设备号 	 */struct cdev cdev;		/* cdev 	*/struct class *class;	/* 类 		*/struct device *device;	/* 设备 	 */int major;				/* 主设备号	  */int minor;				/* 次设备号   */
};struct newchrxxx_dev newchrxxx;	/* xxx设备 *//** @description		: 打开设备* @param - inode 	: 传递给驱动的inode* @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量* 					  一般在open的时候将private_data指向设备结构体。* @return 			: 0 成功;其他 失败*/
static int xxx_open(struct inode *inode, struct file *filp)
{filp->private_data = &newchrxxx; /* 设置私有数据 */return 0;
}/** @description		: 从设备读取数据 * @param - filp 	: 要打开的设备文件(文件描述符)* @param - buf 	: 返回给用户空间的数据缓冲区* @param - cnt 	: 要读取的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 读取的字节数,如果为负值,表示读取失败*/
static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{......return 0;
}/** @description		: 向设备写数据 * @param - filp 	: 设备文件,表示打开的文件描述符* @param - buf 	: 要写给设备写入的数据* @param - cnt 	: 要写入的数据长度* @param - offt 	: 相对于文件首地址的偏移* @return 			: 写入的字节数,如果为负值,表示写入失败*/
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{......return 0;
}/** @description		: 关闭/释放设备* @param - filp 	: 要关闭的设备文件(文件描述符)* @return 			: 0 成功;其他 失败*/
static int xxx_release(struct inode *inode, struct file *filp)
{......return 0;
}/* 设备操作函数 */
static struct file_operations newchrxxx_fops = {.owner = THIS_MODULE,.open = xxx_open,.read = xxx_read,.write = xxx_write,.release = 	xxx_release,
};/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static int __init xxx_init(void)
{u32 val = 0;int ret;/* 注册字符设备驱动 *//* 1、创建设备号 */if (newchrxxx.major) {		/*  定义了设备号 */newchrxxx.devid = MKDEV(newchrxxx.major, 0);ret = register_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT, NEWCHRXXX_NAME);if(ret < 0) {pr_err("cannot register %s char driver [ret=%d]\n",NEWCHRXXX_NAME, NEWCHRXXX_CNT);goto fail_map;}} else {						/* 没有定义设备号 */ret = alloc_chrdev_region(&newchrxxx.devid, 0, NEWCHRXXX_CNT, NEWCHRXXX_NAME);	/* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", NEWCHRXXX_NAME, ret);goto fail_map;}newchrxxx.major = MAJOR(newchrxxx.devid);	/* 获取分配号的主设备号 */newchrxxx.minor = MINOR(newchrxxx.devid);	/* 获取分配号的次设备号 */}printk("newchrxxx major=%d,minor=%d\r\n",newchrxxx.major, newchrxxx.minor);	/* 2、初始化cdev */newchrxxx.cdev.owner = THIS_MODULE;cdev_init(&newchrxxx.cdev, &newchrxxx_fops);/* 3、添加一个cdev */ret = cdev_add(&newchrxxx.cdev, newchrxxx.devid, NEWCHRXXX_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */newchrxxx.class = class_create(THIS_MODULE, NEWCHRXXX_NAME);if (IS_ERR(newchrxxx.class)) {goto del_cdev;}/* 5、创建设备 */newchrxxx.device = device_create(newchrxxx.class, NULL, newchrxxx.devid, NULL, NEWCHRXXX_NAME);if (IS_ERR(newchrxxx.device)) {goto destroy_class;}return 0;destroy_class:class_destroy(newchrxxx.class);
del_cdev:cdev_del(&newchrxxx.cdev);
del_unregister:unregister_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT);
//fail_map://xxx_unmap();//return -EIO;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit led_exit(void)
{...../* 注销字符设备驱动 */cdev_del(&newchrxxx.cdev);/*  删除cdev */unregister_chrdev_region(newchrxxx.devid, NEWCHRXXX_CNT); /* 注销设备号 */device_destroy(newchrxxx.class, newchrxxx.devid);class_destroy(newchrxxx.class);
}module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • SpringBoot3无法注入RocketMQTemplate Bean
  • TabLayout使用以及自定义tab标签
  • MySQL和Redis的数据一致性
  • UE C++ FUdpSender和FUdpReveiver
  • 需要全面学习LangChain?您看这篇就够了
  • .net SqlSugarHelper
  • C# 判断电脑是否联网
  • 保研考研机试攻略:第二章——入门经典(1)
  • Android笔试面试题AI答之Kotlin(4)
  • 高级java每日一道面试题-2024年8月07日-网络篇-你对TCP的三次握手了解多少?
  • OOP经典设计模式
  • Docker 入门全攻略:安装、操作与常用命令指南
  • Java 实现括号匹配:栈的应用与优化
  • zabbix的主/动模式自定义监控项
  • LCM红外小目标检测
  • 230. Kth Smallest Element in a BST
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • es的写入过程
  • JavaWeb(学习笔记二)
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • Nodejs和JavaWeb协助开发
  • PHP 小技巧
  • React 快速上手 - 06 容器组件、展示组件、操作组件
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • tab.js分享及浏览器兼容性问题汇总
  • Twitter赢在开放,三年创造奇迹
  • yii2权限控制rbac之rule详细讲解
  • 多线程事务回滚
  • 翻译:Hystrix - How To Use
  • 规范化安全开发 KOA 手脚架
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 如何在GitHub上创建个人博客
  • 入手阿里云新服务器的部署NODE
  • 我看到的前端
  • 数据库巡检项
  • 微龛半导体获数千万Pre-A轮融资,投资方为国中创投 ...
  • ​HTTP与HTTPS:网络通信的安全卫士
  • ​学习笔记——动态路由——IS-IS中间系统到中间系统(报文/TLV)​
  • ​学习一下,什么是预包装食品?​
  • ​一文看懂数据清洗:缺失值、异常值和重复值的处理
  • # 利刃出鞘_Tomcat 核心原理解析(二)
  • #laravel部署安装报错loadFactoriesFrom是undefined method #
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (pycharm)安装python库函数Matplotlib步骤
  • (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
  • (Ruby)Ubuntu12.04安装Rails环境
  • (六)vue-router+UI组件库
  • (六)激光线扫描-三维重建
  • (三)Kafka 监控之 Streams 监控(Streams Monitoring)和其他
  • (数据大屏)(Hadoop)基于SSM框架的学院校友管理系统的设计与实现+文档
  • (四)Linux Shell编程——输入输出重定向
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (转)拼包函数及网络封包的异常处理(含代码)
  • (自用)仿写程序