正点原子——字符设备驱动开发详细笔记

------------------------------------------------------------------------------------------------驱动部分------------------------------------------------------------------------------------------------

驱动模块的加载与卸载

----------------------------------------代码部分----------------------------------------

①驱动.c的编写

②配置中头文件的包含路径

③makefile的编写

①驱动.c的编写

#头文件包含

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	printk("chrdevbase init!\r\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{

);

/* 
 * 将上面两个函数指定为驱动的入口和出口函数 
 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

        这一部分一共要写四个函数。

       模块注册函数module_init( )。和模块卸载函数module_exit()。这两个函数的作用从名字就可以看出来,就是注册模块和卸载模块。他们的参数分别是驱动的入口函数static int __init chrdevbase_init(void)和出口函数static void __exit chrdevbase_exit(void)

        疑问:如果换一个驱动,入口函数和出口函数应该怎么写?

        答:可以从Linux内核代码中看其他的函数的写法,直接对着抄就行。假设有个驱动名为pg3,那它的入口函数名就是static int __init pg3_init(void)。

       疑问:为什么这里的入口函数出口函数那么简单?

        答:这里只是初步的实验模块的加载与卸载,所以函数里什么也没写,后面会添加上字符串设备注册函数。

②配置中头文件的包含路径

        因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux 源码中的头文件路径。打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入 “C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件,如下图所示:

        打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件。

        在includePath中添加好的 Linux头文件路径。分别是开发板所使用的Linux 源码下的 include、 arch/arm/include 和 arch/arm/include/generated 这三个目录的路径,注意,这里使用了绝对路径。

        出现问题:对于这一行"compilerPath": "/usr/bin/clang"出错,说找不到clang。

        解决:把clang改成gcc就可以解决。

③makefile的编写

        

KERNELDIR :=/home/alientek/lllllllllll/linux-test-NXP/linux-imx-rel_imx_4.1.15_2.1.0_ga/

CURRENT_PATH := $(shell pwd)  #直接shell pwd 命令,获取当前驱动目录路径

obj-m := chrdevbase.o

build: kernel_modules #表示我们编译的是模块

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

 代码分析(阅读时注意分变量部分和伪目标部分阅读):

        KERNELDIR :=/home/alientek/lllllllllll/linux-test-NXP/linux-imx-rel_imx_4.1.15_2.1.0_ga/

定义一个变量KERNELDIR,表示Linux内核源代码的路径。

        CURRENT_PATH := $(shell pwd)

定义一个变量CURRENT_PATH,通过shell pwd命令获取当前Makefile文件所在的目录的路径。

        obj-m := chrdevbase.o

定义变量obj-m,指定要编译的内核模块的名称为chrdevbase.o

        build: kernel_modules

定义一个build目标,它的依赖是kernel_modules

        kernel_modules:

                $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

定义一个伪目标kernel_modules,它的命令是使用$(MAKE)命令在$(KERNELDIR)目录下编译当前目录下的模块。

     clean:

             $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

定义一个clean目标,它的命令是使用$(MAKE)命令在$(KERNELDIR)目录下清除当前目录下的模块。

出现问题:

解决方案:原来是没保存文档,保存之后makefile内容才会更改,才能编译

疑惑:-C 和 M= 这两个是什么作用?

回答:

-C参数告诉make命令要切换到指定的目录中执行编译操作,而M参数告诉内核源代码Makefile在哪里可以找到要编译的模块源代码。

-C $(KERNELDIR):该参数指定内核源代码目录,告诉 make 命令进入指定的目录进行编译。在编译内核模块时,需要访问内核源代码目录下的头文件和其他代码,因此需要指定内核源代码目录。

M=$(CURRENT_PATH):该参数告诉 make 命令编译当前目录下的驱动程序源代码,其中 $(CURRENT_PATH) 变量表示当前驱动程序的源代码目录路径。

clean规则中,同样使用了-CM参数,告诉内核源代码Makefile在哪里可以找到要清除的模块对象文件。

----------------------------------------实验部分----------------------------------------

步骤:

①将编译出来的.ko文件放到根文件系统里面

②使用insmod或modprove驱动

③驱动加载成功后使用lsmod查看

④使用rmmod卸载驱动

①将编译出来的.ko文件放到根文件系统里面

        疑惑:放到根文件系统的哪?

        回答:modprobe 命令默认会去 /lib/modules/<内核版本>目录中查找模块,比如本书使用的 Linux kernel 的版本号为 4.1.15, 因此 modprobe 命令默认会到/lib/modules/4.1.15 这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。

                  所以.ko文件要放到跟文件系统的/lib/modules/4.1.15文件夹内。

②使用insmod或modprove驱动

        加载驱动会用到加载命令:insmod,modprobe。

        insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。但是 modprobe 就不会存在这 个问题,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod 要智能一些。modprobe 命令主要智能在提供了模块的依赖性分析、 错误检查、错误报告等功能,推荐使用 modprobe 命令来加载驱动。

        出现问题:我用modprove加载驱动,会提示无法打开“modules.dep”这个文件,因为找不到这个文件。

        解决方案:首次加载驱动时,要用depmod命令,会自动生成modules.dep以及其他文件。

字符设备的注册与注销

----------------------------------------代码部分----------------------------------------

①register_chrdev函数与unregister_chrdev函数的编写

②注册注销函数参数的编写:a.设备号、b.设备名字、c. file_operations 结构体变量

③file_operations 结构体中具体操作函数的编写

④添加 LICENSE 和作者信息

①register_chrdev函数与unregister_chrdev函数的编写

        register_chrdev(major,name,fops)函数和 unregister_chrdev(major,name)函数的介绍

                major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。               

                name:设备名字,指向一串字符串。

                fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。

                返回变量:0 成功;其他失败。

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
	if(retvalue < 0){
		printk("chrdevbase driver register failed\r\n");
	}
	printk("chrdevbase init!\r\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
	printk("chrdevbase exit!\r\n");
}

②注册注销函数参数的编写:

a.设备号、b.设备名字

#define CHRDEVBASE_MAJOR	200				/* 主设备号 */
#define CHRDEVBASE_NAME		"chrdevbase" 	/* 设备名     */

        在宏定义中直接写设备号和设备名字,然后把宏填入函数中。

        例:register_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME,fops)

        疑惑:怎么才能知道使用那哪个设备号?

        解答:使用cat /proc/devices可以查看当前系统中所有已经使用的设备号。

                   但是,这种查看再去使用未使用的设备号方法太麻烦了,所有就有了动态分配设备号的方法。不过,暂时不学,这里先设定一个未使用的设备号当参数就行。

c. file_operations 结构体变量

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};


        file_operations 结构体变量照着内核源码里抄就可以,将需要的功能留下,不需要的删除。

        例如: .open=chrdevbase_open。左边要与应用层函数中的open同名!右边则是驱动函数里的open函数。

                把三个参数都搞定后,就可以填号字符设备组注册函数了。

                register_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME,&chrdevbase_fops)

③file_operations 结构体中具体操作函数的编写:

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));
	retvalue = copy_to_user(buf, readbuf, cnt);
	if(retvalue == 0){
		printk("kernel senddata ok!\r\n");
	}else{
		printk("kernel senddata failed!\r\n");
	}
	
	//printk("chrdevbase read!\r\n");
	return 0;
}

         一共有四个操作函数,分别对应file_operations结构体中的四项,这里只粘贴了两项。

        具体的写法,照着内核源码或者网上抄就可以,现在操作函数这一块相当完善,做个CV大师即可。

        这里要介绍几个在操作函数里常用的函数,分别是printk()、copy_to_user()、copy_from_user()。

  • printk()函数:Linux内核中没有printf 这个函数,printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级 别。(具体的不做详细介绍了,网上查),这里我们直接使用printk()就行,不用关心消息级别。
  • copy_to_user(to,from,n)函数:参数 to 表示目的,参数 from 表示源,参数 n 表示要复制的数据长度。如果复制成功,返 回值为 0,如果复制失败则返回负数。

        疑惑1:为什么要特地使用一个copy_to_user()函数,不能直接write函数写吗,为何多此一举?

                回答:因为内核空间不能直接操作用户空间的内存,因此需要借 助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。

        疑惑2:copy_to_user()函数的第三个参数cnt是哪来的,并没有看见定义阿?

        回答:cnt来源于chrdevbase_read的参数,为将要读取的字节数,可以直接使用。正如copy_to_user()的第一个参数buf一样,就是要填写chrdevbase_read的buf参数,然后应用程序中的read函数就可以从chrdevbase_read的buf中读取数据。

  • copy_from_user(from,to,n)函数:参考copy_to_user()函数。

        

④添加 LICENSE 和作者信息

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

        所谓的LICENSE,就是许可证的意思。这个信息是必须添加的,否则的话编译会报错。

        作者信息可添加可不添加,无所谓。

至此,字符设备驱动框架已经搭建好,接下来我们就要去向应用层面,写应用APP进行测试。

------------------------------------------------------------------------------------------------应用部分------------------------------------------------------------------------------------------------

编写测试 APP 程序

----------------------------------------代码部分----------------------------------------

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名		: chrdevbaseApp.c
作者	  	: 左忠凯
版本	   	: V1.0
描述	   	: chrdevbase驱测试APP。
其他	   	: 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
  			 argv[2] 1:读文件
  			 argv[2] 2:写文件		
论坛 	   	: www.openedv.com
日志	   	: 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/

static char usrdata[] = {"usr data!"};

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	char readbuf[100], writebuf[100];

	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开驱动文件 */
	fd  = open(filename, O_RDWR);
	if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}


    /* 从驱动文件读取数据 */
	if(atoi(argv[2]) == 1){ 
		retvalue = read(fd, readbuf, 50);
		if(retvalue < 0){
			printf("read file %s failed!\r\n", filename);
		}else{
			/*  读取成功,打印出读取成功的数据 */
			printf("read data:%s\r\n",readbuf);
		}
	}

    /* 向设备驱动写数据 */
	if(atoi(argv[2]) == 2){
 	
		memcpy(writebuf, usrdata, sizeof(usrdata));
		retvalue = write(fd, writebuf, 50);
		if(retvalue < 0){
			printf("write file %s failed!\r\n", filename);
		}
	}

	/* 关闭设备 */
	retvalue = close(fd);
	if(retvalue < 0){
		printf("Can't close file %s\r\n", filename);
		return -1;
	}

	return 0;
}

①几个常用函数的介绍

②疑惑的解答

①几个常用函数的介绍

  • open(pathname,flags)函数

                参数pathname:要打开的设备或者文件名。

                参数flags:文件打开模式,以下三种模式必选其一。O_RDONLY 只读模式 O_WRONLY 只写模式 O_RDWR 读写模式。

                返回值:文件的描述符

  • close(fd)函数

                参数fd:要关闭的文件描述符。

                返回值:0 成功  ;负值 失败。

  • read(fd,buf,count)函数

                参数fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成 功以后会得到文件描述符。

                参数buf:数据读取到此 buf 中。

                参数count:要读取的数据长度,也就是字节数。

                返回值:读取的字节数 成功; 0 读取到了文件末尾 ;负值 失败

  • write(fd,buf,count)函数

                参数fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文 件成功以后会得到文件描述符。

                参数buf:要写入的数据。

                参数count:要写入的数据长度,也就是字节数。

                返回值:写入的字节数 成功; 0 没有写入任何数据 ;负值 失败

②疑惑的解答

        疑惑1:main函数的参数argc是从哪来的?或者说,我要怎么给main函数的参数argc赋值??

                回答:参数argc是从操作系统传递给main函数的,表示程序运行时命令行输入的参数数量,包括程序名称在内。例如,命令行输入"./chrdevbaseApp arg1 arg2",则argc的值为3。

        疑惑2:在函数中有一个语句if(atoi(argv[2]) == 1),但是前面都没有对argv[2]的赋值,这里怎么可以直接判断?

                回答:在运行程序时,需要在命令行输入参数,例如"./chrdevbaseApp   /dev/chrdevbase   1  ",则argc为3,argv[0]为"./chrdevbaseApp",argv[1]为"/dev/chrdevbase",argv[2]为"1"。如果参数数量不符合要求,程序会输出"Error Usage!"并返回-1。

        疑惑3:char *[]类型是如何存储字符串的?以及如何使用?

                回答:char *[]是字符指针,使用的时候看输出什么格式。如果是%c,那么肯定是输出一个字符的。如果是%s,那么就输出直到碰到‘\0’才结束的。

        疑惑4:if(atoi(argv[2]) == 1)中的atoi是什么函数?有什么作用?

                回答:判断 argv[2]参数的值是 1 还是 2,因为输入命令的时候其参数都是字符串格式 的,因此需要借助 atoi 函数将字符串格式的数字转换为真实的数字。

        疑惑5:应用层面的open函数怎么就与驱动里的open函数对上了?就连驱动里open函数的参数都对不上,这之间发生了什么?

                回答:驱动程序的作用是让内核能够访问设备,字符设备成功驱动之后,内核就将会使用驱动程序提供的open函数,来打开字符设备,这里的open函数所做的工作主要是将表示字符设备的结构传给file结构(传给了private_data指针),而file结构刚好就是一个被打开了的 文件描述符。file结构中,不仅包含了 *private_data指针,还还包含了file_operations 结构,而file_operations结构中,又恰好的包含了一组驱动程序提供的接口函数(open read write llseek ...)。
        此外,file结构中还包含了系统调用时的参数,如文件模式 mode_t,表示文件可读,可写等; 文件标志 f_flags等。这些
参数就是系统调用open函数中的参数。
       因此,当用open实现系统调用时,系统调用的open函数会将所调用的文件路径传给内核,内核识别该字符设备文件后,
会去调用该字符设备的驱动程序,驱动程序中的open函数将会返回包含字符设备信息的结构dev给file结构,file结构将系统调用open函数传来的参数一并打包,返回的文件描述符包含了这些所有信息。

----------------------------------------实验部分----------------------------------------

①编译驱动程序

②编译测试 APP

③加载驱动模块

④创建设备节点文件

⑤chrdevbase 设备操作测试

⑥卸载驱动模块

①编译驱动程序

        本来这里是要编写makefile文件的,但由于之前编写了,所以直接make,生成chrdevbaes.ko,然后拷贝到根文件系统对应的文件夹里就行。

②编译测试 APP

        测试 APP 比较简单,只有一个文件,因此就不需要编写 Makefile 了,直接输入命令编译。 因为测试 APP 是要在 ARM 开发板上运行的,所以需要使用 arm-linux-gnueabihf-gcc 来编译。

        然后将生成的chrdevbaseApp拷贝到根文件系统对应的文件夹里。

③加载驱动模块

        这里的知识点前面加载模块也已经提及了,就是insmod和modprobe的使用,不再赘述。

④创建设备节点文件

        驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操 作这个设备节点文件来完成对具体设备的操作。

        输入命令:mknod    /dev/chrdevbase   c     200   0

        其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个 字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在 /dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看。

        如果 chrdevbaseAPP 想要读写 chrdevbase 设备,直接对/dev/chrdevbase 进行读写操作即可。 相当于/dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。

⑤chrdevbase 设备操作测试

        

⑥卸载驱动模块

        使用命令rmmod chrdevbase.ko卸载。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值