【linux驱动之字符设备驱动基础】

linux驱动之字符设备驱动基础



前言

内容学习来自朱有鹏老师
前面学习断断续续的将朱老师嵌入式linux核心课程 c语言 linux应用编程 arm裸机学习完了,arm裸机部分差不多看了两遍还是有一些不能理解的知识点 ,嗯怎么说呢 还是先学会应用吧 后面接触多了我相信对于每个知识点会有自己新的理解和认识的。现在也开始学习认为神秘的Linux驱动学习,在学习中遇到遗漏的知识点,再去查找相关的资料进行学习,在学习中我认为学习到的不仅仅是对于一些知识点的概念理解更重要的是一种学习方法,遇到不同的问题知道举一反三 这也是朱老师在视频中经常听到的,只有自己敢于去犯错,不断的产生bug不断去网络上搜寻相关资料解决Bug,不断的向自己优秀的人学习 这才是真正的进步。
加油吧 每天进步一点点就是对自己最大的回报


一、开启驱动学习之路

在这里插入图片描述

驱动学习从简单字符设备开启 加油

二、驱动预备知识

1.c语言基础知识
对于嵌入式学习学好c语言是非常重要,熟练掌握c语言中的数组、指针、数组指针、指针数组、函数指针、结构体、链表等等对于我们后面的学习是非常有帮助的,因为在驱动学习中我们需要基于前人大佬们写的驱动进行学习,而在驱动编写过程中就会用到很多结构体调用,各种指针指向,如果前面的知识点没有学好,可能在学习驱动这一部分会比较吃力,而且这样的学习效果是非常不好的。
2.相关硬件操作方面
在最开始编写好简单驱动后,难免会用相应的硬件进行测试,那么在我们进行硬件的使用过程中就需要我们掌握硬件的基础知识,懂得看相关的数据手册,懂的如何去操作寄存器等等
3.应用层API
在前面linux应用层学习中,已经学习了各种不同API接口的简单使用,在linux驱动学习中同样也会使用到,驱动供我们应用层调用进而实现不同的功能。所以在学习驱动之前一定要掌握这些必备的知识点。
4.驱动学习方式
注重实践,一步一步写驱动
框架思维,多考虑整体和上下层
先通过简单的设备学习linux驱动框架
学会总结、记录对于后面的理解会很有帮助

三、什么是驱动?

3.1 驱动概念

设备驱动程序(Device Driver),简称驱动程序(Driver)。它是一个允许计算机软件与硬件交互的程序。这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面。CPU经由主板上的总线(Bus)或其他沟通子系统(Subsystem)与硬件形成连接,这样的连接使得硬件设备之间的数据交换成为可能。

驱动可以说有物理驱动、硬件驱动、linux内核驱动

)linux内核驱动。软件层面的驱动广义上就是指:这一段代码操作了硬件去动,所以这一段代码就叫硬件的驱动程序。(本质上是电力提供了动力,而驱动程序提供了操作逻辑方法)狭义上驱动程序就是专指操作系统中用来操控硬件的逻辑方法部分代码。

3.2 linux 体系架构

1》分层思想
2》驱动的上面是系统调用API
3》驱动的下面是硬件
4》驱动本身也是分层的

3.3 模块化设计

微内核和宏内核
(1)宏内核(又称为单内核):将内核从整体上作为一个大过程实现,并同时运行在一个单独的地址空间。所有的内核服务都在一个地址空间运行,相互之间直接调用函数,简单高效。
(2)微内核:功能被划分成独立的过程,过程间通过IPC进行通信。模块化程度高,一个服务失效不会影响另外一个服务。典型如windows
(3)linux:本质上是宏内核,但是又吸收了微内核的模块化特性,提现在2个层面
静态模块化:在编译时实现可裁剪,特征是想要功能裁剪改变必须重新编译
动态模块化:zImage可以不重新编译烧录,甚至可以不关机重启就实现模块的安装和卸载。

3.4 linux设备驱动分类

linux设备驱动可以分为:字符设备驱动、块设备驱动、网络设备驱动
1》字符设备驱动
字符设备是指那些能一个字节一个字节读取数据的设备,如LED灯、键盘、鼠标等。字符设备一般需要在驱动层实现open()、close()、read()、write()、ioctl()等函数。这些函数最终将被文件系统中的相关函数调用。内核为字符设备对应一个文件,/dev/console。对字符设备的操作可以用个字符设备文件/dev/console来进行。
2》块设备驱动
块设备是相对于字符设备定义的,块设备被软件操作时是以块(多个字节构成的一个单位)为单位的。设备的块大小是设备本身设计时定义好的,软件是不能去更改的,不同设备的块大小可以不一样。常见的块设备都是存储类设备,如:硬盘、NandFlash、iNand、SD····
(3)网络设备,网络设备是专为网卡设计的驱动模型,linux中网络设备驱动主要目的是为了支持API中socket相关的那些函数工作。

在这里插入图片描述

3.5 驱动程序的安全性要求

驱动是内核的一部分
(1)驱动已经成为内核中最庞大的组成部分
(2)内核会直接以函数调用的方式调用驱动代码
(3)驱动的动态安装和卸载都会“更改”内核
驱动对内核的影响
(1)驱动程序崩溃甚至会导致内核崩溃
(2)驱动的效率会影响内核的整体效率
(3)驱动的漏洞会造成内核安全漏洞
常见驱动安全性问题
(1)未初始化指针
(2)恶意用户程序
(3)缓冲区溢出
(4)竞争状态

四、环境搭建

4.1 内核源码树构建

使用的内核是九鼎提供的,我们直接进行相应的配置就好了,首先我们将Kernel压缩包移到我们的虚拟机下面,注意在进行解压的时候不要在共享文件夹下面进行 要不然可能会出现一些错误,下面我会记录自己遇到的问题。

make x210ii_qt_defconfig  //make成功后获取源码树和ok 的Zimage
//如果不知道具体的 可以在下面的路径去查看

在这里插入图片描述

error 1

在这里插入图片描述

根据错误提示我们去相应的文件中进行查看 我们将提示该行中的defined去掉就好
在这里插入图片描述
在这里插入图片描述

error 2

在这里插入图片描述

原因
在这里插入图片描述

在进行解压的时候老师说不要的windows下直接解压 我是将其移动到共享文件下解压的,然后就出现了一下问题,然后在网上查了先关错误信息 。才知道老师所说的不要在windows下解压 也是不能再共享文件夹下面解压 然后就将该压缩包进行了移动再次解压后 进行make x210ii_qt_defconfig 就成功了。

解压并成功make

在这里插入图片描述

在这里插入图片描述

4.2 nfs服务器搭建

nfs服务器的搭建看其他博主吧

五、简单的模块源码分析

5.1 常用模块操作命令

(1)lsmod(list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表
(2)insmod(install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko
(3)modinfo(module information,模块信息),功能是打印出一个内核模块的自带信息。用法是modinfo xxx.ko
(4)rmmod(remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀)
(5)剩下的后面再说,暂时用不到(如modprobe、depmod等)

5.2 模块的安装

》安装
(1)先lsmod再insmod看安装前后系统内模块记录。实践测试标明内核会将最新安装的模块放在lsmod显示的最前面。
(2)insmod与module_init宏。模块源代码中用module_init宏声明了一个函数(在我们这个例子里是chrdev_init函数),作用就是指定chrdev_init这个函数和insmod命令绑定起来,也就是说当我们insmod module_test.ko时,insmod命令内部实际执行的操作就是帮我们调用chrdev_init函数。
照此分析,那insmod时就应该能看到chrdev_init中使用printk打印出来的一个chrdev_init字符串,但是实际没看到。原因是ubuntu中拦截了,要怎么才能看到呢?在ubuntu中使用dmesg命令就可以看到了。
(3)模块安装时insmod内部除了帮我们调用module_init宏所声明的函数外,实际还做了一些别的事(譬如lsmod能看到多了一个模块也是insmod帮我们在内部做了记录),但是我们就不用管了。
》模块的版本信息
(1)使用modinfo查看模块的版本信息
(2)内核zImage中也有一个确定的版本信息
(3)insmod时模块的vermagic必须和内核的相同,否则不能安装,报错信息为:insmod: ERROR: could not insert module module_test.ko: Invalid module format
(4)模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施
(5)如何保证模块的vermagic和内核的vermagic一致?编译模块的内核源码树就是我们编译正在运行的这个内核的那个内核源码树即可。说白了就是模块和内核要同出一门。
》模块卸载
(1)module_exit和rmmod的对应关系
(2)lsmod查看rmmod前后系统的模块记录变化
》模块中常用的宏
(1)MODULE_LICENSE,模块的许可证。一般声明为GPL许可证,而且最好不要少,否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提升找不到)。
(2)MODULE_AUTHOR (模块编写作者)
(3)MODULE_DESCRIPTION (模块的描述)
(4)MODULE_ALIAS (模块别名)

5.3 函数修饰符

1)__init,本质上是个宏定义,在内核源代码中就有#define __init xxxx。这个__init的作用就是将被他修饰的函数放入.init.text段中去(本来默认情况下函数是被放入.text段中)。
整个内核中的所有的这类函数都会被链接器链接放入.init.text段中,所以所有的内核模块的__init修饰的函数其实是被统一放在一起的。内核启动时统一会加载.init.text段中的这些模块安装函数,加载完后就会把这个段给释放掉以节省内存。
(2)__exit
注意:一般以__xxx开头的都表示内核中的操作
》关于驱动模块中的头文件
(1)驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事。应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的(譬如gcc的头文件路径在 /usr/include下,这些东西是和操作系统无关的)。驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的include目录下的头文件。

5.4 printk函数解析

(1)printk在内核源码中用来打印信息的函数,用法和printf非常相似。
(2)printk和printf最大的差别:printf是C库函数,是在应用层编程中使用的,不能在linux内核源代码中使用;printk是linux内核源代码中自己封装出来的一个打印函数,是内核源码中的一个普通函数,只能在内核源码范围内使用,不能在应用编程中使用。
(3)printk相比printf来说还多了个:打印级别的设置。printk的打印级别是用来控制printk打印的这条信息是否在终端上显示的。应用程序中的调试信息要么全部打开要么全部关闭,一般用条件编译来实现(DEBUG宏),但是在内核中,因为内核非常庞大,打印信息非常多,有时候整体调试内核时打印信息要么太多找不到想要的要么一个没有没法调试。所以才有了打印级别这个概念。
(4)操作系统的命令行中也有一个打印信息级别属性,值为0-7。当前操作系统中执行printk的时候会去对比printk中的打印级别和我的命令行中设置的打印级别,小于我的命令行设置级别的信息会被放行打印出来,大于的就被拦截的。譬如我的ubuntu中的打印级别默认是4,那么printk中设置的级别比4小的就能打印出来,比4大的就不能打印出来。
(5)ubuntu中这个printk的打印级别控制没法实践,ubuntu中不管你把级别怎么设置都不能直接打印出来,必须dmesg命令去查看。

5.5 Makefile 分析

(1)KERN_DIR,变量的值就是我们用来编译这个模块的内核源码树的目录
(2)obj-m += module_test.o,这一行就表示我们要将module_test.c文件编译成一个模块
(3)make -C $(KERN_DIR) M=pwd modules 这个命令用来实际编译模块,工作原理就是:利用make -C进入到我们指定的内核源码树目录下,然后在源码目录树下借用内核源码中定义的模块编译规则去编译这个模块,编译完成后把生成的文件还拷贝到当前目录下,完成编译。
(4)make clean ,用来清除编译痕迹
总结:模块的makefile非常简单,本身并不能完成模块的编译,而是通过make -C进入到内核源码树下借用内核源码的体系来完成模块的编译链接的。这个Makefile本身是非常模式化的,3和4部分是永远不用动的,只有1和2需要动。1是内核源码树的目录,你必须根据自己的编译环境

#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build	

		
# 开发板的linux内核的源码树目录
KERN_DIR = /home/yhy/driver/kernel

obj-m	+= module_test.o

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

cp:
	cp *.ko  /root/porting_x210/rootfs/rootfs/driver_test

.PHONY: clean	
clean:
	make -C $(KERN_DIR) M=`pwd` modules clean\

六、字符设备驱动工作原理

学任何东西我们都要知其所以然,所以在进行字符驱动学习中,它所工作的原理我们也得大致知道

6.1 系统整体工作原理

(1)应用层->API->设备驱动->硬件
(2)API:open、read、write、close等
(3)驱动源码中提供真正的open、read、write、close等函数实体

6.2 file_operations这个重要结构体

(1)元素主要是函数指针,用来挂接实体函数地址
(2)每个设备驱动都需要一个该结构体类型的变量
(3)设备驱动向内核注册时提供该结构体类型的变量

6.3 注册字符设备驱动

在进行驱动注册的时候我们要搞清楚这几个问题
(1)为何要注册驱动
(2)谁去负责注册
(3)向谁注册
(4)注册函数从哪里来
(5)注册前怎样?注册后怎样?注册产生什么结果?

6.4 register_chrdev详解(#include <linux/fs.h>)

在这里插入图片描述

参数1:major需要申请的主设备号(major>=0)。若为0,则动态随机分配主设备号,若与已存在设备重复,则申请失败。
参数2:name设备名称(某类设备的类型名),与/dev下的设备文件名无任何关系。
参数3:fops与此设备相关的驱动操作。

6.5 内核如何管理字符设备驱动

(1)内核中有一个数组用来存储注册的字符设备驱动
(2)register_chrdev内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置
(3)cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动)

七、驱动编写实践

写驱动重要方式

》思路和框架
(1)目的:给空模块添加驱动壳子
(2)核心工作量:file_operations及其元素填充、注册驱动
》如何动手写驱动代码
(1)脑海里先有框架,知道自己要干嘛
(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改
(3)写下的所有代码必须心里清楚明白,不能似懂非懂

由于自己的开发板没在身边 下面的驱动测试都是在乌班图中进行的

下面代码学习来自朱老师

7.1 demo1 驱动代码初次了解注册驱动

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>    //包含file_operations头文件

#define MYMAJOR 200
#define MYNAME  "testchar"


static int test_chrdev_open(struct inode *inode, struct file *file)
{
    
    
	//这个函数中真正应该放置的打开设备的硬件操作代码部分
	printk(KERN_INFO "chrdev_init test_chrdev_open init\n");
	return 0;

}


static int test_chrdev_release(struct inode *inode, struct file *file)
{
    
    

	printk(KERN_INFO "chrdev_exit test_chrdev_release exit\n");
	return 0;
}

//自定义一个file_operations结构体变量 并且去填充
static const struct file_operations test_fops={
    
    

	.owner    =  THIS_MODULE,   		//惯例 直接写即可
	.open     =  test_chrdev_open,		//将来应用open打开这个设备时实际调用的
	.release  =  test_chrdev_release, 	//就是这个.opne对应的函数

};

// 模块安装函数
static int __init chrdev_init(void)
{
    
    	
	int ret = -1;
	printk(KERN_INFO "chrdev_init helloworld init\n");
	//printk("<7>" "chrdev_init helloworld init\n");
	//printk("<7> chrdev_init helloworld init\n");
	//在Module_init 宏调用的函数中去注册字符设备区别
	ret = register_chrdev(MYMAJOR,MYNAME,&test_fops); //成功返回0
	if(ret)
		{
    
    
			printk(KERN_INFO "register_chrdev error\n");
			return -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "register_chrdev success\n");
	

	return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    
    
	//printk(KERN_INFO "chrdev_exit helloworld exit\n");
	//在Module_exit 宏调用的函数中去注销字符设备区别
	unregister_chrdev(MYMAJOR,MYNAME);
	printk(KERN_INFO "unregister_chrdev exit\n");
	
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

make 编译
在这里插入图片描述
在这里插入图片描述

make完成后可以明显的看出多了很多文件 module_test.ko就是我们的驱动文件,在进行驱动安装的时候应该使用以下步骤:
1.我们先使用lsmod进行已有驱动设备查看
2. 在使用insmodj进行安装
3.在使用lsmod查看使用安装成功
4.使用dmesg 查看打印信息查看

7.1.1 lsmod

不同的环境上面所有的设备驱动都不一样 这是我乌班图上面已有的

在这里插入图片描述

7.1.2 insmod module_test.ko  安装  lsmod 再次查看

在这里插入图片描述

从上面我们可以看出刚才我们module_test.ko这个驱动已经成功安装成功了,那么我们驱动Printk所打印的信息为什么我们看不到呢 在开发板调试中安装成功后就会看见相应的打印信息 在虚拟机上面我们通过dmesg打印查看 打印出来的和我们驱动里面写的是一致的。

在这里插入图片描述

7.1.3 rmmod 卸载模块

在这里插入图片描述

执行该命令后再次查看 安装的驱动就已经没有了
注意:
在进行卸载的时候我们不需要加后缀直接rmmod + 设备名称
而进行安装是需要加后缀譬如 insmd module_test.ko

7.2 应用程序如何调用驱动 demo测试

》驱动设备文件的创建
(1)何为设备文件
(2)设备文件的关键信息是:设备号 = 主设备号 + 次设备号,使用ls -l去查看设备文件,就可以得到这个设备文件对应的主次设备号。
(3)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号
》写应用来测试驱动
(1)还是原来的应用
(2)open、write、read、close等
(3)实验现象预测和验证

7.2.1 驱动中添加读写接口


#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>    //包含file_operations头文件
#include <linux/uaccess.h>  //copy_to_user /copy_from_user 头文件包含

#define MYMAJOR 200
#define MYNAME  "testchar"
int mymajor;
char kbuf[1024];





static int test_chrdev_open(struct inode *inode, struct file *file)
{
    
    
	//这个函数中真正应该放置的打开设备的硬件操作代码部分
	printk(KERN_INFO "chrdev_init test_chrdev_open init\n");
	return 0;

}


static int test_chrdev_release(struct inode *inode, struct file *file)
{
    
    

	printk(KERN_INFO " test_chrdev_release exit\n");
	return 0;
}


 static ssize_t  test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    
    
	int ret = -1;
	//printk(KERN_INFO "test_chrdev_read \n");
	ret = copy_to_user(ubuf,kbuf,count);
	if(ret)
		{
    
    
			printk(KERN_INFO "copy_to_user error\n");
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_to_user success\n");
	
	return 0;
}


//写函数的本质就是将应用层传递过来的数据线复制到内核中,然后将之以正确的方式写入硬件完成操作
static ssize_t test_chrdev_write(struct file *file,const  char __user *ubuf, size_t count, loff_t *ppos)
{
    
    

	int ret = -1;
	//printk(KERN_INFO "test_chrdev_write \n");
	//使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间的一个buf中
	ret = copy_from_user(kbuf,ubuf,count);
	if(ret) //如果不为0  则没有复制完
		{
    
    
			printk(KERN_INFO "copy_from_user error \n");	
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_from_user success \n");	
	//真正的驱动中,数据从应用层复制到驱动后,我们就要根据这个数据区写硬件完成硬件操作
	//下面就应该是操作硬件的代码了
	return 0;
}

//自定义一个file_operations结构体变量 并且去填充
static const struct file_operations test_fops={
    
    

	.owner    =  THIS_MODULE,   		//惯例 直接写即可
	.open     =  test_chrdev_open,		//将来应用open打开这个设备时实际调用的
	.release  =  test_chrdev_release, 	//就是这个.opne对应的函数
	.write    =  test_chrdev_write,
	.read     =  test_chrdev_read,

};

// 模块安装函数
static int __init chrdev_init(void)
{
    
    	
	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	//printk("<7>" "chrdev_init helloworld init\n");
	//printk("<7> chrdev_init helloworld init\n");
	//在Module_init 宏调用的函数中去注册字符设备区别
	//major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	//内核如果成功分配就会返回分配的主设备号,如果分配失败就会返回一个错误值
	mymajor = register_chrdev(0,MYNAME,&test_fops); //成功返回0
	if(mymajor < 0)
		{
    
    
			printk(KERN_INFO "register_chrdev error\n");
			return -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "register_chrdev success mymajor = %d\n",mymajor);	

	
		return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    
    
	//printk(KERN_INFO "chrdev_exit helloworld exit\n");
	//在Module_exit 宏调用的函数中去注销字符设备区别
	unregister_chrdev(mymajor,MYNAME);
	printk(KERN_INFO "unregister_chrdev exit\n");
	
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

上面我们说到了应用程序调用驱动 需要先创建一个驱动设备文件具体如下 在修改驱动后我们还是先进行make编译然后insmod 安装 lsmod 进行查看 然后我们使用mknod 来创建设备文件mknod
使用cat /proc/devices 进行相关查看 (尽量写没有的数字) 不过我们在上面代码了写了自动分配设备号 通过该指令就可以查看具有的设备号

在这里插入图片描述

mknod /dev/test c 240 0 
这样我们就把设备文件创建好了 在下面的应用程序中就可以使用了

在这里插入图片描述

应用程序添加读写 app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


#define FILE "/dev/test"    //mknod 创建的文件名


int main(void)
{
    
    
	int fd = -1;
	char writebuf[24]="hello";
	char readbuf[24]= {
    
    '\0'};
	fd = open(FILE,O_RDWR);
	if(fd  < 0)
	{
    
    
		printf("open %s error\n",FILE);
		return -1;
	}
	printf("open %s success\n",FILE);

	//读写文件
	write(fd,writebuf,sizeof(writebuf));
	
	read(fd,readbuf,sizeof(readbuf));
	printf("read data:%s\n",readbuf);
	//关闭文件
	close(fd);
	return 0;
}

make 执行应用程序./app 就可以看到相应的数据
注意:如果只进行应用程序的修改 驱动是不用更改的,如果驱动更改了 就要重新安装相应的驱动

在这里插入图片描述

dmesg 再次查看驱动打印的信息

在这里插入图片描述

7.3 字符设备驱动再次点LED 静态映射操作

1.在init和exit函数中分别点亮和熄灭LED片
2.insmod和rmmod时观察LED亮灭变化
3.将代码移动到open和close函数中去

把上面的学习完成后,对驱动有了一个大致印象 同样我们也可以通过在驱动里面添加相关的点灯程序 将我们在裸机学习时的程序移值过来 相应寄存器地址的定义就可以进行相应的操作了
当然在进行驱动io 操作的时候也是要包含相应的头文件的 ,具体要包含哪些头文件,我认为老师在视频里面教的很好,我们可以去源码里面进行搜索 ,搜索相关使用操作时他们包含了那些头文件 我们按照那个来就好了但是有些还是会有区别
譬如:
在使用copy_from_user 和copy_to_user 函数时 视频里面写的是asm/uaccess.h没有报错 但是自己实验的时候就不行 ,然后就去网上查看相关解答 最后的结果是包含 linux/uaccess.h 这样就解决了。
所以说在遇到不同bug 或者错误的时候 不要慌 自己慢慢去解决 。



#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>    //包含file_operations头文件
#include <linux/uaccess.h>  //copy_to_user /copy_from_user 头文件包含
#include <mach/regs-gpio.h>
#include<mach/gpio-bank.h>


#define MYMAJOR 200
#define MYNAME  "testchar"
int mymajor;
char kbuf[1024];



#define GPJ0CON		S5PV210_GPJ0CON  //虚拟地址
#define GPJ0DAT		S5PV210_GPJ0DAT  //虚拟地址

#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)


static int test_chrdev_open(struct inode *inode, struct file *file)
{
    
    
	//这个函数中真正应该放置的打开设备的硬件操作代码部分
	printk(KERN_INFO "chrdev_init test_chrdev_open init\n");

	rGPJ0CON = 0x11111111;
	rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	printk(KERN_INFO "GPJ0CON = %p\n",GPJ0CON);
	printk(KERN_INFO "GPJ0DAT = %p\n",GPJ0DAT);
	return 0;

}


static int test_chrdev_release(struct inode *inode, struct file *file)
{
    
    

	printk(KERN_INFO " test_chrdev_release exit\n");
	rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	return 0;
}


 static ssize_t  test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    
    
	int ret = -1;
	//printk(KERN_INFO "test_chrdev_read \n");
	ret = copy_to_user(ubuf,kbuf,count);
	if(ret)
		{
    
    
			printk(KERN_INFO "copy_to_user error\n");
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_to_user success\n");
	
	return 0;
}


//写函数的本质就是将应用层传递过来的数据线复制到内核中,然后将之以正确的方式写入硬件完成操作
static ssize_t test_chrdev_write(struct file *file,const  char __user *ubuf, size_t count, loff_t *ppos)
{
    
    

	int ret = -1;
	//printk(KERN_INFO "test_chrdev_write \n");
	//使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间的一个buf中
	ret = copy_from_user(kbuf,ubuf,count);
	if(ret) //如果不为0  则没有复制完
		{
    
    
			printk(KERN_INFO "copy_from_user error \n");	
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_from_user success \n");	
	//真正的驱动中,数据从应用层复制到驱动后,我们就要根据这个数据区写硬件完成硬件操作
	//下面就应该是操作硬件的代码了
	return 0;
}

//自定义一个file_operations结构体变量 并且去填充
static const struct file_operations test_fops={
    
    

	.owner    =  THIS_MODULE,   		//惯例 直接写即可
	.open     =  test_chrdev_open,		//将来应用open打开这个设备时实际调用的
	.release  =  test_chrdev_release, 	//就是这个.opne对应的函数
	.write    =  test_chrdev_write,
	.read     =  test_chrdev_read,

};

// 模块安装函数
static int __init chrdev_init(void)
{
    
    	
	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	//printk("<7>" "chrdev_init helloworld init\n");
	//printk("<7> chrdev_init helloworld init\n");
	//在Module_init 宏调用的函数中去注册字符设备区别
	//major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	//内核如果成功分配就会返回分配的主设备号,如果分配失败就会返回一个错误值
	mymajor = register_chrdev(0,MYNAME,&test_fops); //成功返回0
	if(mymajor < 0)
		{
    
    
			printk(KERN_INFO "register_chrdev error\n");
			return -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "register_chrdev success mymajor = %d\n",mymajor);	
		//insmod时执行的硬件操作
		//rGPJ0CON = 0x11111111;
		//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
		return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    
    
	//printk(KERN_INFO "chrdev_exit helloworld exit\n");
	//在Module_exit 宏调用的函数中去注销字符设备区别
	unregister_chrdev(mymajor,MYNAME);
	printk(KERN_INFO "unregister_chrdev exit\n");
	//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

编写好了我们进行Make 编译 然后出现了一下错误 ,报错的是头文件错误,不知道大家知不知道那里错了。记得最开始的时候说过 每个驱动程序都要有自己的源码树 然而我们写的这个头文件所包含的文件件的路径是我们开发板的那个源码树路径 所以这个我们要在Makefile中进行相应的修改。

在这里插入图片描述
在这里插入图片描述

再次make 就解决了刚才的问题 然后将相应驱动移到开发板上安装就可以实现点灯操作了(由于开发板没有在身边就不进行演示了)此时在乌班图虚拟机上就不能就行驱动安装后的演示了,因为不同的源码树对应着不同的平台 我们刚才在makefile中使用的是开发板上的源码树 就只能在开发板上进行安装了。

7.3 字符设备驱动再次点LED 静态映射操作2

驱动代码



#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>    //包含file_operations头文件
#include <linux/uaccess.h>  //copy_to_user /copy_from_user 头文件包含
#include <mach/regs-gpio.h>
#include<mach/gpio-bank.h>
#include <linux/string.h>


#define MYMAJOR 200
#define MYNAME  "testchar"
int mymajor;
char kbuf[1024];



#define GPJ0CON		S5PV210_GPJ0CON  //虚拟地址
#define GPJ0DAT		S5PV210_GPJ0DAT  //虚拟地址

#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)


static int test_chrdev_open(struct inode *inode, struct file *file)
{
    
    
	//这个函数中真正应该放置的打开设备的硬件操作代码部分
	printk(KERN_INFO "chrdev_init test_chrdev_open init\n");

	rGPJ0CON = 0x11111111;
	//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	//printk(KERN_INFO "GPJ0CON = %p\n",GPJ0CON);
	//printk(KERN_INFO "GPJ0DAT = %p\n",GPJ0DAT);
	return 0;

}


static int test_chrdev_release(struct inode *inode, struct file *file)
{
    
    

	printk(KERN_INFO " test_chrdev_release exit\n");
	//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	return 0;
}


 static ssize_t  test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    
    
	int ret = -1;
	//printk(KERN_INFO "test_chrdev_read \n");
	ret = copy_to_user(ubuf,kbuf,count);
	if(ret)
		{
    
    
			printk(KERN_INFO "copy_to_user error\n");
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_to_user success\n");
	
	return 0;
}


//写函数的本质就是将应用层传递过来的数据线复制到内核中,然后将之以正确的方式写入硬件完成操作
static ssize_t test_chrdev_write(struct file *file,const  char __user *ubuf, size_t count, loff_t *ppos)
{
    
    

	int ret = -1;
	//printk(KERN_INFO "test_chrdev_write \n");
	//使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间的一个buf中
	//memcpy(kbuf,ubuf)  不行 因为2个不在一个地址空间中
	//先清除一下Kbuf
	memset(kbuf,0,sizeof(kbuf));
	ret = copy_from_user(kbuf,ubuf,count);
	if(ret) //如果不为0  则没有复制完
		{
    
    
			printk(KERN_INFO "copy_from_user error \n");	
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_from_user success \n");	
		
	if(kbuf[0] == '1')
	{
    
    
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	}
	else if(kbuf[0] == '0')
	{
    
    
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	}
/*		
		
	//真正的驱动中,数据从应用层复制到驱动后,我们就要根据这个数据区写硬件完成硬件操作
	//下面就应该是操作硬件的代码了
	if(!strcmp(kbuf,"on"))
	{
		rGPJ0CON = 0x11111111;
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	}
	else if(!strcmp(kbuf,"off")){
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	}
*/
	return 0;
}

//自定义一个file_operations结构体变量 并且去填充
static const struct file_operations test_fops={
    
    

	.owner    =  THIS_MODULE,   		//惯例 直接写即可
	.open     =  test_chrdev_open,		//将来应用open打开这个设备时实际调用的
	.release  =  test_chrdev_release, 	//就是这个.opne对应的函数
	.write    =  test_chrdev_write,
	.read     =  test_chrdev_read,

};

// 模块安装函数
static int __init chrdev_init(void)
{
    
    	
	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	//printk("<7>" "chrdev_init helloworld init\n");
	//printk("<7> chrdev_init helloworld init\n");
	//在Module_init 宏调用的函数中去注册字符设备区别
	//major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	//内核如果成功分配就会返回分配的主设备号,如果分配失败就会返回一个错误值
	mymajor = register_chrdev(0,MYNAME,&test_fops); //成功返回0
	if(mymajor < 0)
		{
    
    
			printk(KERN_INFO "register_chrdev error\n");
			return -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "register_chrdev success mymajor = %d\n",mymajor);	
		//insmod时执行的硬件操作
		//rGPJ0CON = 0x11111111;
		//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
		return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    
    
	//printk(KERN_INFO "chrdev_exit helloworld exit\n");
	//在Module_exit 宏调用的函数中去注销字符设备区别
	unregister_chrdev(mymajor,MYNAME);
	printk(KERN_INFO "unregister_chrdev exit\n");
	//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
}


module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名

应用程序代码


#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>


#define FILE "/dev/test"    //mknod 创建的文件名


int main(void)
{
    
    
	int fd = -1;
	char buf[12]={
    
    '\0'};
	//char writebuf[24]="hello";
	//char readbuf[24]= {'\0'};
	fd = open(FILE,O_RDWR);
	if(fd  < 0)
	{
    
    
		printf("open %s error\n",FILE);
		return -1;
	}
	printf("open %s success\n",FILE);
/*
	//读写文件
	write(fd,"on",2);
	sleep(2);
	write(fd,"off",3);
	sleep(2);
*/

/*
	write(fd,"1",1);
	sleep(2);
	write(fd,"0",1);
	sleep(2);
*/	
	//read(fd,readbuf,sizeof(readbuf));
	//printf("read data:%s\n",readbuf);
	//关闭文件
	while(1)
	{
    
    
		memset(buf,0,sizeof(buf));
		printf("please input on | off\n");
		scanf("%s",buf);
		if(!strcmp(buf,"on"))
		{
    
    
			write(fd,"1",1);
		}
		else if(!strcmp(buf,"off"))
		{
    
    
			write(fd,"0",1);
		}
		else if(!strcmp(buf,"quit"))
		{
    
    
			break;
		}
	}
	
	close(fd);
	return 0;
}

7.4 字符设备驱动再次点LED 动态映射操作

》如何建立动态映射
(1)request_mem_region,向内核申请(报告)需要映射的内存资源。
(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址
》如何销毁动态映射
(1)iounmap
(2)release_mem_region
注意:映射建立时,是要先申请再映射;然后使用;使用完要解除映射时要先解除映射再释放申请。

驱动代码 应用代码和上面是一样的



#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>    //包含file_operations头文件
#include <linux/uaccess.h>  //copy_to_user /copy_from_user 头文件包含
#include <mach/regs-gpio.h>
#include<mach/gpio-bank.h>
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>



#define MYMAJOR 200
#define MYNAME  "testchar"
int mymajor;
char kbuf[1024];



//#define GPJ0CON		S5PV210_GPJ0CON  //虚拟地址
//#define GPJ0DAT		S5PV210_GPJ0DAT  //虚拟地址

//#define rGPJ0CON	*((volatile unsigned int *)GPJ0CON)
//#define rGPJ0DAT	*((volatile unsigned int *)GPJ0DAT)

#define GPJ0CON  0xe0200240
#define GPJ0DAT  0xe0200244
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;

static int test_chrdev_open(struct inode *inode, struct file *file)
{
    
    
	//这个函数中真正应该放置的打开设备的硬件操作代码部分
	printk(KERN_INFO "chrdev_init test_chrdev_open init\n");

	//rGPJ0CON = 0x11111111;
	//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	//printk(KERN_INFO "GPJ0CON = %p\n",GPJ0CON);
	//printk(KERN_INFO "GPJ0DAT = %p\n",GPJ0DAT);
	return 0;

}


static int test_chrdev_release(struct inode *inode, struct file *file)
{
    
    

	printk(KERN_INFO " test_chrdev_release exit\n");
	//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	return 0;
}


 static ssize_t  test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    
    
	int ret = -1;
	//printk(KERN_INFO "test_chrdev_read \n");
	ret = copy_to_user(ubuf,kbuf,count);
	if(ret)
		{
    
    
			printk(KERN_INFO "copy_to_user error\n");
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_to_user success\n");
	
	return 0;
}


//写函数的本质就是将应用层传递过来的数据线复制到内核中,然后将之以正确的方式写入硬件完成操作
static ssize_t test_chrdev_write(struct file *file,const  char __user *ubuf, size_t count, loff_t *ppos)
{
    
    

	int ret = -1;
	//printk(KERN_INFO "test_chrdev_write \n");
	//使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间的一个buf中
	//memcpy(kbuf,ubuf)  不行 因为2个不在一个地址空间中
	//先清除一下Kbuf
	memset(kbuf,0,sizeof(kbuf));
	ret = copy_from_user(kbuf,ubuf,count);
	if(ret) //如果不为0  则没有复制完
		{
    
    
			printk(KERN_INFO "copy_from_user error \n");	
			return  -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "copy_from_user success \n");	
	
/*	
	if(kbuf[0] == '1')
	{
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	}
	else if(kbuf[0] == '0')
	{
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	}
*/
/*		
		
	//真正的驱动中,数据从应用层复制到驱动后,我们就要根据这个数据区写硬件完成硬件操作
	//下面就应该是操作硬件的代码了
	if(!strcmp(kbuf,"on"))
	{
		rGPJ0CON = 0x11111111;
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
	}
	else if(!strcmp(kbuf,"off")){
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	}
*/
	return 0;
}

//自定义一个file_operations结构体变量 并且去填充
static const struct file_operations test_fops={
    
    

	.owner    =  THIS_MODULE,   		//惯例 直接写即可
	.open     =  test_chrdev_open,		//将来应用open打开这个设备时实际调用的
	.release  =  test_chrdev_release, 	//就是这个.opne对应的函数
	.write    =  test_chrdev_write,
	.read     =  test_chrdev_read,

};

// 模块安装函数
static int __init chrdev_init(void)
{
    
    	
	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	//printk("<7>" "chrdev_init helloworld init\n");
	//printk("<7> chrdev_init helloworld init\n");
	//在Module_init 宏调用的函数中去注册字符设备区别
	//major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	//内核如果成功分配就会返回分配的主设备号,如果分配失败就会返回一个错误值
	mymajor = register_chrdev(0,MYNAME,&test_fops); //成功返回0
	if(mymajor < 0)
		{
    
    
			printk(KERN_INFO "register_chrdev error\n");
			return -EINVAL; //错误返回  非法参数
		}
		printk(KERN_INFO "register_chrdev success mymajor = %d\n",mymajor);	
		
		//使用动态映射方式来操作寄存器
		if(!request_mem_region(GPJ0CON,4,"GPJ0CON"))
		{
    
    
			return -EINVAL;
		}
		
		if(!request_mem_region(GPJ0DAT,4,"GPJ0DAT"))
		{
    
    
			return -EINVAL;
		}
		//建立映射
		pGPJ0CON = ioremap(GPJ0CON,4);
		pGPJ0DAT = ioremap(GPJ0DAT,4);
		
		*pGPJ0CON = 0x11111111;
		*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); 
		//insmod时执行的硬件操作
		//rGPJ0CON = 0x11111111;
		//rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  //点亮led
		return 0;
}

// 模块下载函数
static void __exit chrdev_exit(void)
{
    
    
	//printk(KERN_INFO "chrdev_exit helloworld exit\n");
	//在Module_exit 宏调用的函数中去注销字符设备区别
	unregister_chrdev(mymajor,MYNAME);
	printk(KERN_INFO "unregister_chrdev exit\n");
	//rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));  //熄灭
	
	//解除映射
	iounmap(pGPJ0CON);
	iounmap(pGPJ0DAT);
	release_mem_region(GPJ0CON,4);
	release_mem_region(GPJ0DAT,4);
}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");				// 描述模块的许可证
MODULE_AUTHOR("aston");				// 描述模块的作者
MODULE_DESCRIPTION("module test");	// 描述模块的介绍信息
MODULE_ALIAS("alias xxx");			// 描述模块的别名信息

上面几种的结果都是一样的,只是使用的方式不同,在学习中这些都是我们应该掌握的。

总结

以上就是自己初学驱动字符设备的一些小小记录。
学会总结真的是一种很好的学习方式虽然上面所记录的内容很多都和老师讲的一致,但至少自己还是花了时间来进行整理,这也是对自己这阶段学习的一个检验吧。对一些知识点遗忘的时候 至少可以快速去查找自己以前学习的记录,很多当时没有明白的问题,可能再次看自己写的笔记时就恍然大悟了。通过初次的学习对驱动有了一定的理解,嗯还是那句话慢慢来吧 至少自己一直在进步。加油

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/boybs/article/details/123588993