首页 > 系统相关 >Linux字符设备驱动开发

Linux字符设备驱动开发

时间:2024-08-09 16:52:06浏览次数:13  
标签:字符 struct xxx dev newchrxxx 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.o

build: kernel_modules

kernel_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, //父设备,没有NULL
    dev_t devt, //设备号
    void *drvdata, //数据,NULL
    const 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");

标签:字符,struct,xxx,dev,newchrxxx,Linux,驱动,设备
From: https://blog.csdn.net/chinalihuanyu/article/details/133842199

相关文章

  • UBOOT以太网驱动程序指南, uboot网卡驱动指南,u-boot网卡驱动编译教程
    以太网驱动程序指南DasU-Boot中的网络堆栈设计允许在运行时轻松添加和控制多个网络设备。本指南适用于希望回顾网络驱动堆栈并实现自己以太网设备驱动程序的人员。在这里,我们将描述一个新的伪“APE”驱动程序。大多数现有驱动程序已经(并且新的网络驱动程序必须)使用U-Boot核......
  • Linux
    一、安装Centex和ubantu1.安装Centex:a:下载安装vm虚拟机平台。b:从开源网站下载Centex镜像文件,网易开源镜像网站http://mirrors.163.com/c:在vm上创建虚拟机。(Centex难用,命令行练习机)2.安装ubantu子系统:a:ubantu官网https://cn.ubuntu.com/download安装镜像文件。b:安装......
  • Linux 进程调度(二)之进程的上下文切换
    目录一、概述二、上下文切换的实现1、context_switch2、switch_mm3、switch_to三、观测进程上下文切换一、概述进程的上下文切换是指在多任务操作系统中,当操作系统决定要切换当前运行的进程时,将当前进程的状态保存起来,并恢复下一个要运行的进程的状态。上下文切换......
  • 代码随想录算法训练营day08|344.反转字符串,541.反转字符串II,卡码网:54.替换数字
    344.反转字符串题目链接:https://leetcode.cn/problems/reverse-string/description/我的代码:classSolution{public:voidreverseString(vector<char>&s){for(inti=0;i<s.size()/2;i++){chartemp=s[i];s[i]=......
  • 【迅为电子】IMX6ULL开发板嵌入式linux开发指南第七章 Linux 常用命令第一部分
        物联网时代,各种传感器的采集和处理技术是需要我们掌握的,迅为IMX6ULL开发板标配了各种传感器设备,包括陀螺仪、重力加速度计和光传感器、红外接收、EEPROM存储,也可以选配温湿度传感器,其他如摄像头(含CMOS和USB两种)、VGA显示、GPS定位功能、RFID门禁、继电器输出、步进电......
  • 创新驱动,中国EL冷光片市场蓬勃发展
    一、行业简述(一)行业概念EL冷光片,全称为电激发光冷光片,是一种利用电场激发荧光物质发光的物理现象来实现发光的器件。EL冷光片通过施加交流电压,在电极之间产生强电场,从而激活发光层中的荧光材料,使其发出均匀、柔和的冷光源。由于其低功耗、颜色多样、寿命长等特点,EL冷光片在......
  • 三防平板定制化:驱动产业高效化发展的新动能
    在数字化转型的浪潮中,三防平板作为一种坚固耐用、功能强大的移动设备,正逐渐成为各行各业提升效率、优化管理的关键工具。通过硬件和软件的定制化服务,三防平板不仅能满足特定行业的需求,更能在复杂的工作环境中展现出卓越的性能,助力产业实现高效化发展。硬件定制化:打造专属设备......
  • Linux的一些常用指令
    记录了一些Linux常用的指令,包括基础指令,网络故障排查时的指令和系统故障排查时的指令。1.基础指令1.ls用于显示当前目录下的文件和目录。-l:以长格式显示,包括文件的详细信息,如权限、所有者、大小、修改时间等;-a:显示所有文件,包括隐藏文件(以.开头的文件);-h:以更易读的方式显示......
  • Linux内核及补丁编译
    Linux内核及补丁编译一、源码下载1、查看当前linux内核版本uname-r2、获取对应版本的linux源码方式1:源方式下载sudoaptsearchlinux-source##找到对应版本的linux-sourcesudoaptinstalllinux-source-5.4.0##以5.4.0举例mkdir~/Projectscd~/Projectssudocp......
  • 字符产part02
    今天学习了字符串的第二部分。翻转字符串里的单词,先整体翻转,再局部翻转。注意移除空格和前头数组中移除元素类似。右旋转,也是先整体再局部翻转。4.翻转字符串里的单词题目:给定一个字符串,逐个翻转字符串中的每个单词。示例1:输入:"theskyisblue"输出:"blueisskyt......