博客
关于我
【安卓开发系列 -- 系统开发】字符设备驱动基础
阅读量:367 次
发布时间:2019-03-04

本文共 18317 字,大约阅读时间需要 61 分钟。

【安卓开发系列 -- 系统开发】字符设备驱动基础

【1】简单的字符设备驱动示例代码

驱动代码

#include 
// module_init module_exit#include
// __init __exit// 模块安装函数static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); return 0;}// 模块下载函数static void __exit chrdev_exit(void){ printk(KERN_INFO "chrdev_exit helloworld 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"); // 描述模块的别名信息

ubuntu 系统中的 Makefile

#ubuntu 的内核源码树KERN_VER = $(shell uname -r)KERN_DIR = /lib/modules/$(KERN_VER)/buildobj-m += module_test.oall:	make -C $(KERN_DIR) M=`pwd` modules.PHONY: clean	clean:	make -C $(KERN_DIR) M=`pwd` modules clean

交叉编译的 Makefile (基于 AIO-3399C 六核 AI 开发板)

ifneq ($(KERNELRELEASE),) obj-m += module_test.oelse	# 指定内核源码树	KERNELDIR := /home/shallysun/proj/firefly-rk3399-Industry/kernel	# 获取代码所在的当前目录	PWD := $(shell pwd)	# 指定交叉编译链	ARCH = arm64	CROSS_COMPILE=/home/shallysun/proj/firefly-rk3399-Industry/prebuilts/gcc/        linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-	.PHONY: modules cleanmodules:	# 编译内核驱动模块	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) modulesclean:	# 清除中间文件	@rm -rf *.o *.order *.symvers *.mod.* .*.o.cmd .*.mod.o.cmd .*.ko.cmd .tmp_versions *.ko	endif

【2】常用的模块操作命令

1. lsmod     (list module,将模块列表显示),功能是打印出当前内核中已经安装的模块列表2. insmod    (install module,安装模块),功能是向当前内核中去安装一个模块,用法是insmod xxx.ko3. modinfo   (module information,模块信息),功能是打印出一个内核模块的自带信息,             用法是modinfo xxx.ko4. rmmod     (remove module,卸载模块),功能是从当前内核中卸载一个已经安装了的模块,             用法是rmmod xxx(注意卸载模块时只需要输入模块名即可,不能加.ko后缀)

【3】模块的安装

(1) 先 lsmod 再 insmod 看安装前后系统内模块记录;    实践测试表明内核会将最新安装的模块放在 lsmod 显示的最前面;(2) insmod 与 module_init 宏;    模块源代码中用 module_init 宏声明了一个函数,本实例中即指定 chrdev_init 函数和 insmod 命令绑定起来,即 insmod module_test.ko 时,insmod 命令内部实际执行的操作就是调用 chrdev_init 函数;照此分析,那 insmod 时就应该能看到 chrdev_init 中使用 printk 打印出来的一个 chrdev_init 字符串,但是实际没看到。原因是 ubuntu 中拦截了,要怎么才能看到呢?在 ubuntu 中使用 dmesg 命令就可以看到了。

【4】模块的版本信息

1. 使用 modinfo 查看模块的版本信息;2. 内核 zImage 中也有一个确定的版本信息;3. insmod 时模块的 vermagic 必须和内核的相同,否则不能安装;   报错信息为:   insmod: ERROR: could not insert module module_test.ko: Invalid module format;4. 模块的版本信息是为了保证模块和内核的兼容性,是一种安全措施;5. 如何保证模块的 vermagic 和内核的 vermagic 一致;   保证编译模块的内核源码树为正在运行的内核的内核源码树即可;

【5】模块卸载

1. module_exit 和 rmmod 绑定2. lsmod 可以查看 rmmod 前后系统的模块记录变化

【6】模块中常用宏

1. MODULE_LICENSE,模块的许可证;一般声明为 GPL 许可证,而且最好不要少,   否则可能会出现莫名其妙的错误(譬如一些明显存在的函数提示找不到);2. MODULE_AUTHOR,模块的作者3. MODULE_DESCRIPTION,模块的介绍信息4. MODULE_ALIAS,模块的别名信息

【7】函数修饰符

1. __init,本质上是个宏定义,在内核源代码中存在 #define __init xxxx;   __init 的作用就是将其修饰的函数放入 .init.text 段中去(默认情况下函数是被放入.text段中);整个内核中的所有的这类函数都会被链接器链接放入 .init.text 段中,因此所有的内核模块的 __init 修饰的函数其实是被统一放在一起的;内核启动时统一会加载 .init.text 段中的模块安装函数,加载完后就会把这个段给释放掉以节省内存;2. __exit,本质上是个宏定义,与 __init 类似,在现在驱动模块时使用;3. static 修饰的函数叫做静态函数;   静态函数有两种,根据其出现的地方来分类:   若该静态函数出现在类里,那么它是一个静态成员函数;         静态成员函数的作用在于:调用这个函数不会访问或者修改任何对象(非static)数据成员;         其实很好理解,       类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;       非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,            然后通过类的对象(实例)去访问;      若该静态函数是一个普通的全局的静态函数;         这样的 static 函数与普通函数的区别是:       用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用;       而普通的函数,默认是 extern 的,也就是说它可以被其它代码文件调用;    在函数的返回类型前加上关键字 static,函数就被定义成为静态函数;   普通函数的定义和声明默认情况下是 extern 的,        但静态函数只是在声明他的文件当中可见,不能被其他文件所用;   因此定义静态函数有以下好处:  <1> 其他文件中可以定义相同名字的函数,不会发生冲突;  <2> 静态函数不能被其他文件所用;

【8】printk 函数详解

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

【9】驱动模块中的头文件

1. 驱动源代码中包含的头文件和原来应用编程程序中包含的头文件不是一回事;   应用编程中包含的头文件是应用层的头文件,是应用程序的编译器带来的,和操作系统无关;   驱动源码属于内核源码的一部分,驱动源码中的头文件其实就是内核源代码目录下的 include 目录下的头文件;

【10】驱动编译的 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 需要动;

【11】字符设备驱动工作原理

11.1 系统整体工作原理

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

11.2 file_operations 结构体

1. 元素主要是函数指针,用来挂接实体函数地址2. 每个设备驱动都需要一个该结构体类型的变量3. 设备驱动向内核注册时提供该结构体类型的变量file_operations 结构体struct file_operations {	// 指向拥有该结构的模块的指针	struct module *owner;	// 修改当前文件的读写位置	loff_t (*llseek) (struct file *, loff_t, int);	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);	int (*iterate) (struct file *, struct dir_context *);	int (*iterate_shared) (struct file *, struct dir_context *);	__poll_t (*poll) (struct file *, struct poll_table_struct *);	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);	int (*mmap) (struct file *, struct vm_area_struct *);	unsigned long mmap_supported_flags;	int (*open) (struct inode *, struct file *);	int (*flush) (struct file *, fl_owner_t id);	int (*release) (struct inode *, struct file *);	int (*fsync) (struct file *, loff_t, loff_t, int datasync);	int (*fasync) (int, struct file *, int);	int (*lock) (struct file *, int, struct file_lock *);	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);	int (*check_flags)(int);	int (*flock) (struct file *, int, struct file_lock *);	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,size_t, unsigned int);	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,size_t, unsigned int);	int (*setlease)(struct file *, long, struct file_lock **, void **);	long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);	void (*show_fdinfo)(struct seq_file *m, struct file *f);#ifndef CONFIG_MMU	unsigned (*mmap_capabilities)(struct file *);#endif       ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);} __randomize_layout;

11.3 注册字符设备驱动

register_chrdev 函数 :原型 :static inline int register_chrdev(unsigned int major, 	const char *name, const struct file_operations *fops)	参数 :major,	主设备号,内核给设备定义的设备编号name,	当前设备驱动的名称fops,	将驱动的 file_operations 结构体变量传递给内核static inline 说明 :inline 函数与宏的区别 :	1. 展开的时机不一样,宏是在预编译时展开,inline 函数是在编译时展开;	2. 展开的方式不一样,宏的展开是纯字符串替换;inline 函数由编译器展开,包括实参与形参的对应,重名字段的处理等等;	因此宏的替换需要程序员保证字符串替换后的正确性,inline 函数的替换却是编译器帮程序员做了翻译工作,程序员只要保证函数的正确性即可;static 与 inline 的区别 :1. 从作用域来看二者都只在当前文件可见;2. static 修饰变量或者函数,inline 只修饰函数;3. static 修饰的在符号表产生符号,但是符号是 local 的,不受连接器处理,inline 修饰的在调用点直接把代码展开,不产生符号;4. inline 修饰函数定义,修饰函数声明无意义;extern inline 与 static inline : 1. extern inline 表示该函数是已经声明过,由于函数本身可以声明多次,因此 extern 对函数的影响仅仅是把函数的隐藏属性显式化;    extern 对于非函数的对象是有用的,因为对象声明时会带来内存的分配,因而 extern 就表示该对象已经声明过了,不用再分配内存;2. static 是以前 C 的用法,目的是让该关键字标识的函数只在本地文件可见,同一个程序的其它文件是不可见该函数的;   换句话说,就算你其它文件里包含了同名同参数表的函数定义的话,也是不会引起函数重复定义的错误的;
内核管理字符设备驱动的方式1. 内核中有一个数组用来存储注册的字符设备驱动2. register_chrdev 内部将需要注册的驱动的信息存储在数组中相应的位置3. cat /proc/devices 查看内核中已经注册过的字符设备驱动(和块设备驱动)4. 主设备号即为对应于数组的索引位置下标号

11.4 驱动中添加读写接口

应用和驱动之间的数据交换1. copy_from_user   功能 : 用来将数据从用户空间复制到内核空间;   原型 : static inline unsigned long __must_check copy_from_user(void *to, const void __user *from,           unsigned long n)   返回值 : 成功则返回 0,失败则返回尚未成功复制的剩余字节数;2. copy_to_user   功能 : 用来将数据从内核空间复制到用户空间;   原型 : static inline unsigned long __must_check copy_to_user(void __user *to, const void *from,           unsigned long n)   返回值 : 成功则返回 0,失败则返回尚未成功复制的剩余字节数;

11.5 应用程序调用驱动

驱动设备文件的创建1. 设备文件 : 设备驱动程序的接口2. 设备文件的关键信息 :    设备号 = 主设备号 + 次设备号,使用 ls -l 查看设备文件,就可以得到这个设备文件对应的主次设备号;   主设备号 : 设备类型, 次设备号 : 文件名(简要说明),决定特定设备类型中的设备的特定索引;3. 使用 mknod 创建设备文件 : mknod /dev/xxx c 主设备号 次设备号;

11.6 驱动中操控硬件

1. 与裸机相比相同的点 :(1). 硬件物理原理不变(2). 硬件操作接口(寄存器)不变(3). 硬件操作代码不变2. 与裸机相比不同的点 :(1). 寄存器地址不同;     原来是直接用物理地址,现在需要用该物理地址在内核虚拟地址空间相对应的虚拟地址;     寄存器的物理地址是 CPU 设计时决定的,从 datasheet 中可以查找得到;(2). 编程方法不同;     裸机中习惯直接用函数指针操作寄存器地址,kernel 中习惯用封装好的 io 读写函数来操作寄存器,以实现最大程度可移植性;3. 内核的虚拟地址映射方法  (1). 内核中有 2 套虚拟地址映射方法 : 动态和静态(2). 静态映射方法的特点 :     内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核     在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效     对于移植好的内核,映射表一直存在(3). 动态映射方法的特点 :     驱动程序根据需要随时动态的建立、使用、销毁映射     映射是短期临时的4. 如何选择虚拟地址映射方法(1). 2 种映射并不排他,可以同时使用(2). 静态映射类似于 C 语言中全局变量,动态方式类似于 C 语言中 malloc 堆内存(3). 静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间     动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射

11.7 静态映射操作 LED

示例代码

#include 
// module_init module_exit#include
// __init __exit#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h// gpio-bank.h 定义了 GPIO 对应的地址信息#include
#include
#define MYMAJOR 200#define MYNAME "testchar"#define GPJ0CON S5PV210_GPJ0CON#define GPJ0DAT S5PV210_GPJ0DAT#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)int mymajor;char kbuf[100]; // 内核空间的 bufstatic int test_chrdev_open(struct inode *inode, struct file *file){ // 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分 // 但是现在暂时我们写不了这么多,所以用一个 printk 打印个信息来做代表 printk(KERN_INFO "test_chrdev_open\n"); rGPJ0CON = 0x11111111; rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 return 0;}static int test_chrdev_release(struct inode *inode, struct file *file){ printk(KERN_INFO "test_chrdev_release\n"); rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 灭 return 0;}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_ERR "copy_to_user fail\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 个不在一个地址空间中 memset(kbuf, 0, sizeof(kbuf)); ret = copy_from_user(kbuf, ubuf, count); if (ret) { printk(KERN_ERR "copy_from_user fail\n"); return -EINVAL; } printk(KERN_INFO "copy_from_user success..\n"); // 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据 // 去写硬件完成硬件的操作,所以这下面就应该是操作硬件的代码 if (kbuf[0] == '1') { // LED 灯点亮 rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); } else if (kbuf[0] == '0') { // LED 灯熄灭 rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); } if (!strcmp(kbuf, "on")) { rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); } 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打开这个设备时实际调用的就是这个.open对应的函数 .release = test_chrdev_release, .write = test_chrdev_write, .read = test_chrdev_read,};/** * __init : 本质上是个宏定义,在内核源代码中存在 #define __init xxxx; * __init 的作用就是将其修饰的函数放入 .init.text 段中去(默认情况下函数是被放入.text段中); * 整个内核中的所有的这类函数都会被链接器链接放入 .init.text 段中,因此所有的内核模块的 __init 修饰的 * 函数其实是被统一放在一起的;内核启动时统一会加载 .init.text 段中的模块安装函数,加载完后就会把这个 * 段给释放掉以节省内存; *//** * __exit : 本质上是个宏定义, */ // __init __exit : 前面的 _ 越多表明越接近内核,越具备风险 /** * static 修饰的函数叫做静态函数; * 静态函数有两种,根据其出现的地方来分类: * 若该静态函数出现在类里,那么它是一个静态成员函数; * * 静态成员函数的作用在于:调用这个函数不会访问或者修改任何对象(非static)数据成员; * * 类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问; * 非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问; * * 若该静态函数是一个普通的全局的静态函数; * * 这样的 static 函数与普通函数的区别是: * 用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用; * 而普通的函数,默认是 extern 的,也就是说它可以被其它代码文件调用; * * 在函数的返回类型前加上关键字 static,函数就被定义成为静态函数; * 普通函数的定义和声明默认情况下是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用; * 因此定义静态函数有以下好处: * <1> 其他文件中可以定义相同名字的函数,不会发生冲突; * <2> 静态函数不能被其他文件所用; */// 模块安装函数static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); // 在 module_init 宏调用的函数中去注册字符设备驱动 // major 传 0 进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号 // 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数 mymajor = register_chrdev(0, MYNAME, &test_fops); if (mymajor < 0) { printk(KERN_ERR "register_chrdev fail\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);}module_init(chrdev_init);module_exit(chrdev_exit);// MODULE_xxx 这种宏作用是用来添加模块描述信息MODULE_LICENSE("GPL"); // 描述模块的许可证MODULE_AUTHOR("aston"); // 描述模块的作者MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
关于静态映射要说的1. 不同版本内核中静态映射表位置、文件名可能不同2. 不同 SoC 的静态映射表位置、文件名可能不同3. 所谓映射表其实就是头文件中的宏定义

11.8 动态映射操作 LED

示例代码

#include 
// module_init module_exit#include
// __init __exit#include
#include
#include
// arch/arm/mach-s5pv210/include/mach/gpio-bank.h// gpio-bank.h 定义了 GPIO 对应的地址信息#include
#include
#include
#include
#define MYMAJOR 200#define MYNAME "testchar"#define GPJ0CON S5PV210_GPJ0CON#define GPJ0DAT S5PV210_GPJ0DAT#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)// GPIO 寄存器的基地址#define GPJ0CON_PA 0xe0200240 // 4 个字节#define GPJ0DAT_PA 0xe0200244 // 4 个字节// 动态内存映射之后得到的地址unsigned int *pGPJ0CON;unsigned int *pGPJ0DAT;int mymajor;char kbuf[100]; // 内核空间的 bufstatic int test_chrdev_open(struct inode *inode, struct file *file){ // 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分 // 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表 printk(KERN_INFO "test_chrdev_open\n"); rGPJ0CON = 0x11111111; rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 return 0;}static int test_chrdev_release(struct inode *inode, struct file *file){ printk(KERN_INFO "test_chrdev_release\n"); rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 灭 return 0;}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_ERR "copy_to_user fail\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 个不在一个地址空间中 memset(kbuf, 0, sizeof(kbuf)); ret = copy_from_user(kbuf, ubuf, count); if (ret) { printk(KERN_ERR "copy_from_user fail\n"); return -EINVAL; } printk(KERN_INFO "copy_from_user success..\n"); // 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据 // 去写硬件完成硬件的操作,所以这下面就应该是操作硬件的代码 if (kbuf[0] == '1') { rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); } else if (kbuf[0] == '0') { rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); } if (!strcmp(kbuf, "on")) { rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); } 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打开这个设备时实际调用的就是这个.open对应的函数 .release = test_chrdev_release, .write = test_chrdev_write, .read = test_chrdev_read,};/** * __init : 本质上是个宏定义,在内核源代码中存在 #define __init xxxx; * __init 的作用就是将其修饰的函数放入 .init.text 段中去(默认情况下函数是被放入.text段中); * 整个内核中的所有的这类函数都会被链接器链接放入 .init.text 段中,因此所有的内核模块的 __init 修饰的 * 函数其实是被统一放在一起的;内核启动时统一会加载 .init.text 段中的模块安装函数,加载完后就会把这个 * 段给释放掉以节省内存; *//** * __exit : 本质上是个宏定义, */ // __init __exit : 前面的 _ 越多表明越接近内核,越具备风险 /** * static 修饰的函数叫做静态函数; * 静态函数有两种,根据其出现的地方来分类: * 若该静态函数出现在类里,那么它是一个静态成员函数; * * 静态成员函数的作用在于:调用这个函数不会访问或者修改任何对象(非static)数据成员; * * 类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问; * 非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问; * * 若该静态函数是一个普通的全局的静态函数; * * 这样的 static 函数与普通函数的区别是: * 用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用; * 而普通的函数,默认是 extern 的,也就是说它可以被其它代码文件调用; * * 在函数的返回类型前加上关键字 static,函数就被定义成为静态函数; * 普通函数的定义和声明默认情况下是 extern 的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用; * 因此定义静态函数有以下好处: * <1> 其他文件中可以定义相同名字的函数,不会发生冲突; * <2> 静态函数不能被其他文件所用; */// 模块安装函数static int __init chrdev_init(void){ printk(KERN_INFO "chrdev_init helloworld init\n"); // 在 module_init 宏调用的函数中去注册字符设备驱动 // major 传 0 进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号 // 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数 mymajor = register_chrdev(0, MYNAME, &test_fops); if (mymajor < 0) { printk(KERN_ERR "register_chrdev fail\n"); return -EINVAL; } printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor); // 使用动态映射的方式来操作寄存器 if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON")) return -EINVAL; if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON")) return -EINVAL; pGPJ0CON = ioremap(GPJ0CON_PA, 4); pGPJ0DAT = ioremap(GPJ0DAT_PA, 4); *pGPJ0CON = 0x11111111; *pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮 return 0;}// 模块卸载函数static void __exit chrdev_exit(void){ printk(KERN_INFO "chrdev_exit helloworld exit\n"); *pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 解除映射 iounmap(pGPJ0CON); iounmap(pGPJ0DAT); release_mem_region(GPJ0CON_PA, 4); release_mem_region(GPJ0DAT_PA, 4); // 在 module_exit 宏调用的函数中去注销字符设备驱动 unregister_chrdev(mymajor, MYNAME);}module_init(chrdev_init);module_exit(chrdev_exit);// MODULE_xxx 这种宏作用是用来添加模块描述信息MODULE_LICENSE("GPL"); // 描述模块的许可证MODULE_AUTHOR("aston"); // 描述模块的作者MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息MODULE_ALIAS("alias xxx"); // 描述模块的别名信息

参考致谢

本博客为博主学习笔记,同时参考了网上众博主的博文以及相关专业书籍,在此表示感谢,本文若存在不足之处,请批评指正。

转载地址:http://msmr.baihongyu.com/

你可能感兴趣的文章
MYSQL 幻读(Phantom Problem)不可重复读
查看>>
mysql 往字段后面加字符串
查看>>
mysql 快照读 幻读_innodb当前读 与 快照读 and rr级别是否真正避免了幻读
查看>>
MySQL 快速创建千万级测试数据
查看>>
mysql 快速自增假数据, 新增假数据,mysql自增假数据
查看>>
MySql 手动执行主从备份
查看>>
Mysql 批量修改四种方式效率对比(一)
查看>>
Mysql 报错 Field 'id' doesn't have a default value
查看>>
MySQL 报错:Duplicate entry 'xxx' for key 'UNIQ_XXXX'
查看>>
Mysql 拼接多个字段作为查询条件查询方法
查看>>
mysql 排序id_mysql如何按特定id排序
查看>>
Mysql 提示:Communication link failure
查看>>
mysql 插入是否成功_PDO mysql:如何知道插入是否成功
查看>>
Mysql 数据库InnoDB存储引擎中主要组件的刷新清理条件:脏页、RedoLog重做日志、Insert Buffer或ChangeBuffer、Undo Log
查看>>
mysql 数据库中 count(*),count(1),count(列名)区别和效率问题
查看>>
mysql 数据库备份及ibdata1的瘦身
查看>>
MySQL 数据库备份种类以及常用备份工具汇总
查看>>
mysql 数据库存储引擎怎么选择?快来看看性能测试吧
查看>>
MySQL 数据库操作指南:学习如何使用 Python 进行增删改查操作
查看>>
MySQL 数据库的高可用性分析
查看>>