linux驱动开发—— 2、字符设备驱动基础知识

来自朱有鹏老师的课堂笔记

  • 模块和驱动的区别:

1、模块是驱动的雏形,驱动:模块+硬件。
2、模块是一个机制,驱动调用了这个机制。
3、就像是汽车和发动机的区别。

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

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

2、file_operations 结构体(内核源代码当中被定义)

file_operations : 文件操作,+s 代表有很多操作
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
	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 *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, int datasync);
	int (*aio_fsync) (struct kiocb *, 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 **);
};

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

(3)设备驱动向内核注册时,提供该结构体类型的变量。

3、注册字符设备驱动
(1)为何要注册驱动?

我们写了一个内核驱动,内核本身并不知道,所以需要我们去进行注册,来让内核知道。

(2)内核注册函数?

驱动向内核注册函数 register_chrdev() 在( #include <linux/fs.h> )中
在这里插入图片描述

(3)注册函数从哪里来?

从内核里面来

(4)注册前怎样?注册后怎样?注册产生什么结果?

1、注册前,内核也不知道,应用程序也不可以调用。
2、注册后,内核给分配一个编号,应用程序利用这个编号可以调用驱动。

1、register_chrdev 详解

路径:/kernel/linux/fs.h ,所以我们要包含的头文件

#include <linux/fs.h>

函数实体:

static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}

返回值:int 返回0表示注册成功,返回一个负整数表示注册失败(整个Linux内核都是这么设计)
参数:
unsigned int major :				 主设备号,可人为向内核申请,不能与已有的设备号重复。
const char *name :					 输入型参数,表示驱动设备的名字
const struct file_operations *fops :输入型参数,用于内核注册的结构体指针

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

(1)内核中有一个数组用来存储注册的字符设备驱动。

1、数组大小是确定的。
2、所以我们内核最多可以挂载 255 个字符设备驱动。(平时我们电脑当中最多有10个字符设备驱动)

(2)register_chrdev 内部将我们要注册的驱动的信息(主要是 )存储在数组中相应的位置

(3)查看内核中已经注册过的字符设备驱动(和块设备驱动)

cat /proc/devices 

好好理解主设备号(major)的概念

1、是我们驱动的编号
2、管理我们驱动的数组下标。

在这里插入图片描述
注:

1、我们之前有很多字符设备驱动,这个编号默认是固定的


二、字符设备驱动代码实践(1)

1、思路和框架

目的:给我们之前写的空模块,来添加驱动壳子

核心工作量:

1、file_operations 及其元素填充
2、注册驱动

(1)脑海里先有框架,知道自己要干嘛

(2)细节代码不需要一个字一个字敲,可以到内核中去寻找参考代码复制过来改

(3)写下的所有代码必须心里清楚明白,不能似懂非懂

2、自己动手开始写

基础流程:
makefile :不需要修改,因为我们的内核源码树的路径并没有发生改变。

modele test.c

1、自定义一个 file_operations 结构体变量,并且去填充。

2、在module _init 宏调用的函数中去注册字符设备驱动,因为这个函数会被 insmod 的时候调用,所以我们顺便去注册。
3、// 在module _exit 宏调用的函数中去注销字符设备驱动,与上同理

#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>

// 注册驱动的时候使用
#define MYMAJOR		200  			// 我们的驱动编号
#define MYNAME		"testchar"		// 我们驱动的编号
// 接收我们注册函数返回值
int mymajor;  // 注册函数成功的话,返回主设备号

// 自定义一个file_operations结构体变量,并且去填充
// test_fops : 这里需要修改,我们是什么驱动,下划线 前面就写什么名字

static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// .release 就是对应着我们的 close 函数
};


// 模块安装函数
static int __init chrdev_init(void)
{	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 在module_init宏调用的函数中去注册字符设备驱动
	// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
	// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
	mymajor = register_chrdev(MYMAJOR, MYNAME, &test_fops);
	if (mymajor < 0)
	{
		// KERN_ERR : 宏定义,内核当中的错误类型
		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");			// 描述模块的别名信息

注:注册函数

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)

1、major :
(1)传入 0 :表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
(2)传入 200: 表示要让内核,帮我指定使用200 这个下标
2、返回值:
(1)注册失败:返回负数。
(2)注册成功:返回我们内核分配的主设备号

根据:结构体当中的函数 open、 release函数指针,来写他们对应的函数。

注:在内核当中的,open 、release、 函数的参数和返回值都是固定的格式
所以我们必须模仿它来写这些函数。

.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
.release	= test_chrdev_release,		// .release 就是对应着我们的 close 函数

static int test_chrdev_open(struct inode *inode, struct file *file)
{
	// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
	// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
	printk(KERN_INFO "test_chrdev_open\n");
	
	return 0;
}

static int test_chrdev_release(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "test_chrdev_release\n");
	
	return 0;
}


三、应用程序如何调用驱动

1、应用程序只能调用 API,不能直接调用我们的驱动。
2、所以引出设备文件,
3、API-----> 设备文件 -------> 驱动
4、应用程序通过 open、write、read、操作设备文件,从而调用驱动。
5、驱动通过将自己收集到的信息,打包到设备文件当中,从而实现交互。

1、驱动设备文件的创建

(1)设备文件的关键信息是:设备号 = 主设备号 + 次设备号

举例:我们有5块 lcd 屏幕。
1、这 5 个 lcd 屏幕公用一个主设备号
2、但是每个屏幕的子设备号不同

在这里插入图片描述
(2)使用mknod创建设备文件:mknod /dev/xxx c 主设备号 次设备号

1、通过 cat /proc/devices ,来查看我们驱动分配的主设备号
在这里插入图片描述

注:

1、这个设备文件使用 vi 来打开是没有任何意义的,因为它是一个设备文件。
2、一般查看它的信息,我们通过使用 ls -l 来查看这个文件的主次设备号。


2、写应用来进行测试

(1)重新写一个应用程序:app.c

#include <stdio.h>

// 刚刚创建的设备文件
#define FILE  "/dev/test"

int main(void)
{
	int fd = -1;
	// 打开文件
	fd = open(FILE,O_RDWR);
	if(fd < 0)
	{
		printf("open %s error", FILE);
		return -1;
	}
		printf("open %s success",FILE);

	//读写文件

	//关闭文件
	close(fd);
	return 0;
}

(2)makefile 的修改

在默认目标当中添加,使用交叉编译工具链的编译。

all:
	arm-linux-gcc app.c -o app

(3)分析实验现象

1、API 当中的函数,去读写设备文件的时候,内核会自动调用 file_operations 当中对应的函数。
例如:open ——> .open 等等
2、我们 printf 和 printk 打印的消息可能是同时打印。

3、添加读写接口

1、之前我们只添加了 open 和 close 的接口。
2、接下来我们要使用 write 、read 的函数

(1)在驱动层当中添加
研发流程:

1、先装载驱动,得到主设备号
2、根据主设备号,来创建设备文件
3、测试应用程序。
4、清理:卸载驱动模块,删除设备文件。

1、我们 read 函数 和 write 函数都可以从内核当中仿照
2、如果我们驱动修改了,我们必须从新 ismod,并且重新创建设备文件。
原因:
(1)mvmod ,ismod, 重新安装驱动之后,内核分配的设备号会发生改变
(2)mknod /dev/test c 250 0 :这个原来的设备文件的主设备号,可能和我们重新分配的不对应。

char kbuf[100];			// 内核空间的buf

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个不在一个地址空间中
	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");

	// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
	// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
	
	return 0;
}

// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
	.write 		= test_chrdev_write,
	.read		= test_chrdev_read,
};

(1)test_chrdev_write 函数当中的细节:

  • 写函数的本质:

应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。

  • 内核和应用程序之间数据的传递是一个很麻烦的事情:

1、内核和应用程序不在一个地址空间。(并且每一个应用程序都以为自己独占整个空间)
2、不在一个地址空间所以我们不能直接访问,所以内核提供了两个函数:
(1)copy_from_user:使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
(2)copy_to_user :将数据从内核空间,赋值到应用空间

  • copy_from_user 函数的返回值定义,和常规有点不同。

1、返回值如果成功复制则返回0,
2、如果 不成功复制则返回尚未成功复制剩下的字节数

  • 在copy 完成之后,我们就将应用层的数据,拷贝到了驱动层。

1、根据拿到的数据,我们要对硬件进行真正的操控。
2、我们以后再进行添加

// kernel bufeer
char kbuf[100];			// 内核空间的buf

// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
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个不在一个地址空间中
	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");

	// 真正的驱动中,数据从应用层复制到驱动中后,我们就要根据这个数据
	// 去写硬件完成硬件的操作。所以这下面就应该是操作硬件的代码
	
	return 0;
}

(2)test_chrdev_read 函数当中的细节:

添加回环测试:
1、应用层:write —————> 驱动层:ubuf ——> kbuf
2、驱动层:kbuf ——>ubuf ——————> 应用层:用户自定义 buf

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;
}

2、应用程添加代码:

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


#define FILE	"/dev/test"			// 刚才mknod创建的设备文件名

char buf[100];

int main(void)
{
	int fd = -1;
	
	fd = open(FILE, O_RDWR);
	if (fd < 0)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	printf("open %s success..\n", FILE);
	
	// 读写文件
	write(fd, "helloworld2222", 14);
	read(fd, buf, 100);
	printf("读出来的内容是:%s.\n", buf);
	
	// 关闭文件
	close(fd);
	
	return 0;
}

四、驱动如何操控硬件

1、还是那个硬件

(1)硬件物理原理不变
(2)硬件操作接口(寄存器)不变
(3)硬件操作代码不变

2、哪里不同了?

(1)寄存器地址不同

1、原来是直接用物理地址,现在需要用该物理地址,在内核虚拟地址空间,相对应的虚拟地址。
2、现在我们在内核当中去操控,内核当中使用的是虚拟地址
3、寄存器的物理地址是CPU设计时决定的,从 datasheet 中查找到的。

(2)编程习惯不同

1、裸机中习惯直接用函数指针操作寄存器地址
2、而kernel中习惯用封装好的io,读写函数来操作寄存器,以实现最大程度可移植性。

3、内核的虚拟地址映射方法

  • 为什么需要虚拟地址映射

1、有了内核之后,我们就会打开 mmu 。
2、mmu 并不会区分那个是内核,哪一个是应用程序:
(1)应用程序肯定是虚拟地址。
(2)内核为了应用程序,也被迫使用虚拟地址

  • 内核中有2套虚拟地址映射方法:动态映射 和 静态映射

  • 静态映射方法的特点:硬编码

1、在映射之前,我们就已经想好了,什么物理地址对应什么虚拟地址
2、然后建立一个映射表。内核启动的时候开启,内核关机的时候关闭。
3、如果我们想要修改映射地址:我们就必须重新修改代码,并且重新编译内核和烧录。

  • 动态映射方法的特点:

1、我们在内核启动的时候,并没有进行地址映射。
2、而是驱动程序,根据应用程序需要,随时动态的建立映射、使用、销毁映射
3、映射是短期临时的

  • 如何选择虚拟地址映射方法

(1)2种映射并不排他,可以同时使用

(2)静态映射类似于C语言中全局变量,动态方式类似于C语言中 malloc堆内存

(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;
动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)

五、不同映射操作硬件

1、静态映射操作LED1

(1)关于静态映射要说的

(1)不同版本内核中,静态映射表位置、文件名可能不同

(2)不同SoC的,静态映射表位置、文件名可能不同

(3)所谓映射表其实就是:头文件中的宏定义

(2)三星版本内核中的静态映射表

(1)虚拟地址基地址定义在:arch/arm/plat-samsung/include/plat/map-base.h

#define S3C_ADDR_BASE	(0xFD000000)	

1、三星移植时确定的静态映射表的基地址
2、表中的所有虚拟地址都是以这个 地址+偏移量 来指定的

(2)主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h

#define S5P_VA_CHIPID		S3C_ADDR(0x00700000)
#define S5P_VA_GPIO			S3C_ADDR(0x00500000)  // S5P_VA_GPIO = 0xFD000000 + 0x00500000

1、CPU在安排寄存器地址时不是随意乱序分布的,而是按照模块去区分的。每一个模块内部的很多个寄存器的地址是连续的。
2、所以内核在定义寄存器地址时都是先找到基地址,然后再用 基地址+偏移量 来寻找具体的一个寄存器。
3、map-s5p.h中定义的就是要用到的几个模块的寄存器基地址
4、map-s5p.h中定义的是模块的寄存器基地址的虚拟地址

(3)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h
表中是GPIO的各个端口的基地址的定义

#define S5PV210_GPJ0_BASE		(S5P_VA_GPIO + 0x240) 

(4)GPIO的具体寄存器定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h

#define S5PV210_GPJ1CON			(S5PV210_GPJ1_BASE + 0x00)
#define S5PV210_GPJ1DAT			(S5PV210_GPJ1_BASE + 0x04)
#define S5PV210_GPJ1PUD			(S5PV210_GPJ1_BASE + 0x08)
#define S5PV210_GPJ1DRV			(S5PV210_GPJ1_BASE + 0x0c)
#define S5PV210_GPJ1CONPDN		(S5PV210_GPJ1_BASE + 0x10)
#define S5PV210_GPJ1PUDPDN		(S5PV210_GPJ1_BASE + 0x14)

(3)编写代码:

静态映射操作和我们的裸机操作差不多:
1、只是操作的内存地址发生了改变。
2、在裸机当中我们从数据手册当中,找到对应寄存器的地址。
3、在内核当中,我们找到对应的虚拟地址, 然后内核会帮助我们自动切换到物理地址。

  • 驱动层代码:

1、我们借用了内核当中的宏定义 S5PV210_GPJ0CON ,所以我们需要包含对应的头文件。

#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>		// arch/arm/mach-s5pv210/include/mach/gpio-bank.h

2、GPIO的操作代码:
rGPJ0CON : 设置 GPIO 的工作模式
rGPJ0DAT: 设置其输出值

	rGPJ0CON = 0x11111111; 
	rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮

3、我们驱动中的判断尽量简洁,提高内核整体性能。
这个判断只使用了一个字符。

	if (kbuf[0] == '1')
	{
		rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
	}
	else if (kbuf[0] == '0')
	{
		rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
	}
		
#include <linux/module.h>		// module_init  module_exit
#include <linux/init.h>			// __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>		// arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>

#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];			// 内核空间的buf

static 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));
	}
		
	return 0;
}


// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
	.owner		= THIS_MODULE,				// 惯例,直接写即可
	
	.open		= test_chrdev_open,			// 将来应用open打开这个设备时实际调用的
	.release	= test_chrdev_release,		// 就是这个.open对应的函数
	.write 		= test_chrdev_write,
	.read		= test_chrdev_read,
};


// 模块安装函数
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");			// 描述模块的别名信息
  • 应用层代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE	"/dev/test"			// 刚才mknod创建的设备文件名

char buf[100];

int main(void)
{
	int fd = -1;
	int i = 0;
	
	fd = open(FILE, O_RDWR);
	if (fd < 0)
	{
		printf("open %s error.\n", FILE);
		return -1;
	}
	printf("open %s success..\n", FILE);

	while (1)
	{
		memset(buf, 0 , sizeof(buf));
		printf("请输入 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, "flash"))
		{
			for (i=0; i<3; i++)
			{
				write(fd, "1", 1);
				sleep(1);
				write(fd, "0", 1);
				sleep(1);
			}
		}	
		else if (!strcmp(buf, "quit"))
		{
			break;
		}
	}
	
	// 关闭文件
	close(fd);
	
	return 0;
}
  • makefile (不用发生改变)

2、动态映射操作LED

(1)如何建立动态映射

(1)request_mem_region,向内核申请(报告)需要映射的内存资源。

(2)ioremap,真正用来实现映射,传给他物理地址他给你映射返回一个虚拟地址

分两步走:1、先申请 2、在映射

(2)如何销毁动态映射

(1)iounmap ,解除映射

(2)release_mem_region,申请释放

分两步走:1、先解除映射 2、申请释放

(3)代码实践

  • 驱动层更改的代码
#include <linux/io.h>
#include <linux/ioport.h>


// 模块安装函数
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);
	
//	rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
}

分析动态映射函数:

(1)注册:

request_mem_region:申请注册
(1)参数:要映射的物理地址、长度、自己起的名字
(2)返回值:注册失败返回 0 .
ioremap:真正映射
(1)参数:要映射的物理地址、长度
(2)返回值:对应得虚拟地址

// 使用动态映射的方式来操作寄存器
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);

(2) 解除映射

iounmap:解除真正映射
参数:注册时候,自己起的名字
release_mem_region :取消注册申请
参数:要取消的物理地址、长度

// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想文艺一点的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值