linux驱动开发—— 3、字符设备驱动高级

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

一、更换注册设备函数

1、知识铺垫

1、新接口与老接口
(1)老接口:register_chrdev

缺点:只能返回主设备号

(2)新接口:register_chrdev_region/alloc_chrdev_region + cdev(结构体)

1、register_chrdev_region :自己指定注册设备号:告诉内核这个设备号,要被占用
2、alloc_chrdev_region :内核分配设备号:内核自己分配一个可以用的设备号。

1、现在单纯注册了一设备号,还没有注册设备驱动
2、注册设备驱动,由 cdev(结构体) 的相关函数来完成

2、cdev介绍

(1)结构体

在内核当中查:cdev.h

关键内容:

   const struct file_operations *ops;   // 操纵这个字符设备文件的方法
   dev_t dev;                   // 字符设备的设备号,由主设备号和次设备号构成
struct cdev {
   struct kobject kobj;          // 内嵌的内核对象,每个 cdev 都是一个 kobject
   struct module *owner;       // 指向实现驱动的模块
   const struct file_operations *ops;   // 操纵这个字符设备文件的方法
   struct list_head list;       // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头,用来将已经向内核注册的所有字符设备形成链表
   dev_t dev;                   // 字符设备的设备号,由主设备号和次设备号构成
   unsigned int count;       // 隶属于同一主设备号的次设备号的个数.
};

(2)相关函数:cdev_alloccdev_initcdev_addcdev_del

1、cdev_alloc :给结构体分配内存
2、cdev_add :向内核里面添加一个驱动

3、设备号
(1)主设备号和次设备号

同时操作 100个 led:比如第一个闪烁,第二个呼吸灯。每个led 需要单独控制。
1、所以我们需要给每个led写驱动。
2、共用1个主设备号,各自使用自己的次设备号。

(2)dev_t 类型

包含:主设备号 + 次设备号
可能,前16位是主设备号,后16位是次设备号。也有可能是其他,不同内核的分配不同。
由下面的3个宏来操作

(3)MKDEV、MAJOR、MINOR三个宏

MKDEV :make device ,使用 主设备号+次设备号, 算出一个主次设备号
MAJOR: major:利用主次设备号,提取出主设备号
MINOR: minor:利用主次设备号,提取出次设备号

2、自己指定设备号,实践编程

  • 驱动层

(1)注册一个主设备号


 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 
int register_chrdev_region(dev_t from, unsigned count, const char *name)

分析:region :范围

前两个参数的分析:假如我们的 4 个led,主设备号为 200 ,次设备号分别为: 0,1,2,3
dev_t fromMKDEV(200,0) :从第一个设备开始的注册设备号
unsigned count4 接下来还有 4 个这样的设备

(2)注册一个设备驱动 cdev_init + cdev_add

cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		return -EINVAL;
	}
printk(KERN_INFO "cdev_add success\n");

1、cdev 结构体的定义:全局变量。(因为我们不仅要在安装函数中使用,还得在卸载函数中使用)
2、cdev_init :绑定 test_cdevtest_fops 这两个结构体。
3、cdev_add :注册一个设备驱动

模块安装函数:

#define MYMAJOR		200
#define MYCNT		1
#define MYNAME		"testchar"

// 这3个变量是公用的,在模块注册和卸载当中
int mymajor;
static dev_t mydev;
static struct cdev test_cdev;

// 模块安装函数
static int __init chrdev_init(void)
{	
	int retval;
	
	printk(KERN_INFO "chrdev_init helloworld init\n");
	// 使用新的cdev接口来注册字符设备驱动
	// 新的接口注册字符设备驱动需要2步
	
	// 第1步:注册/分配主次设备号
	mydev = MKDEV(MYMAJOR, 0);
	retval = register_chrdev_region(mydev, MYCNT, MYNAME);
	if (retval) {
		printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
		return -EINVAL;
	}
	printk(KERN_INFO "register_chrdev_region success\n");
	// 第2步:注册字符设备驱动
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		return -EINVAL;
	}
	printk(KERN_INFO "cdev_add success\n");

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

	// 使用新的接口来注销字符设备驱动
	// 注销分2步:
	// 第一步真正注销字符设备驱动用cdev_del
	cdev_del(&test_cdev);
	// 第二步去注销申请的主次设备号
	unregister_chrdev_region(mydev, MYCNT);
}

3、内核自动分配设备号,实践编程

1、使用 alloc_chrdev_region 自动分配设备号

(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;

1、要先查看 cat /proc/devices 去查看没有使用的。 比较麻烦

(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号。

1、使用 alloc_chrdev_region 就可以自动分配了。

(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去 mknod 创建他对应的设备文件

  • 函数原型:

 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

参数分析:
dev_t *dev : 目的从此函数得到相应的,主次设备号
unsigned baseminor :开始的次设备号
unsigned count :几个次设备
const char *name :主设备名称

	int retval;
	
	printk(KERN_INFO "chrdev_init helloworld init\n");

	// 使用新的cdev接口来注册字符设备驱动
	// 新的接口注册字符设备驱动需要2步
	
	// 第1步:分配主次设备号
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}
			// 使用宏来得到相应的主次设备号
	printk(KERN_INFO "alloc_chrdev_region success\n");
	printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
	

2、得到分配的主设备号和次设备号
(1)使用MAJOR宏和MINOR宏从dev_t得到major和minor

// 输入型参数得到:主次设备号
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
// 利用宏来得到:主设备号 + 次设备号
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));

(2)反过来使用MKDEV宏从major和minor得到dev_t。

 // 自己指定主设备号 + 次设备号
#define MYMAJOR		200
// 利用宏来的到:主次设备号
mydev = MKDEV(MYMAJOR, 0);

(3)使用这些宏的代码具有可移植性

4、中途出错的倒影式错误处理方法

(1)内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了

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

	// 使用新的cdev接口来注册字符设备驱动
	// 新的接口注册字符设备驱动需要2步
	
	// 第1步:分配主次设备号
	retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
	if (retval < 0) 
	{
		printk(KERN_ERR "Unable to alloc minors for %s\n", MYNAME);
		goto flag1;
	}
			// 使用宏来得到相应的主次设备号
	printk(KERN_INFO "alloc_chrdev_region success\n");
	printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
	
	
	// 第2步:注册字符设备驱动
	cdev_init(&test_cdev, &test_fops);
	retval = cdev_add(&test_cdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		goto flag2;
	}
	printk(KERN_INFO "cdev_add success\n");

	
	// 使用动态映射的方式来操作寄存器
	if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
//		return -EINVAL;
		goto flag3;
	if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
//		return -EINVAL;
		goto flag3;
	
	pGPJ0CON = ioremap(GPJ0CON_PA, 4);
	pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
	
	*pGPJ0CON = 0x11111111;
	*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));		// 亮
	
	//goto flag0:
	return 0;
	
// 如果第4步才出错跳转到这里来	
flag4:
	release_mem_region(GPJ0CON_PA, 4);
	release_mem_region(GPJ0DAT_PA, 4);

// 如果第3步才出错跳转到这里来
flag3:
	cdev_del(&test_cdev);

// 如果第2步才出错跳转到这里来
flag2:
	// 在这里把第1步做成功的东西给注销掉
	unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:	
	return -EINVAL;
//flag0:	
//	return 0;
}

分析:
(1)如果第一步出错:分配主次设备号失败:

直接返回错误

goto flag1;

// 如果第1步才出错跳转到这里来
flag1:	
	return -EINVAL;

(2)如果第二步出错:注册设备驱动出错,

1、注册设备成功,我们先取消注册设备号。(把第一步的屁股擦完)
2、返回错误
3、注意:flag:标号是依次往下执行的

		goto flag2;
// 如果第2步才出错跳转到这里来
flag2:
	// 在这里把第1步做成功的东西给注销掉
	unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:	
	return -EINVAL;

(3)如果第三步出错:

1、擦第一步屁股
2、擦第二步屁股
3、返回错误

		goto flag3;
		// 如果第3步才出错跳转到这里来
flag3:
	cdev_del(&test_cdev);

// 如果第2步才出错跳转到这里来
flag2:
	// 在这里把第1步做成功的东西给注销掉
	unregister_chrdev_region(mydev, MYCNT);
// 如果第1步才出错跳转到这里来
flag1:	
	return -EINVAL;

总结:依次类推,我们要控制我们的资源。前面使用了,后面要及时释放

5、使用cdev_alloc

(1)cdev_alloc的编程实践

  • 原来:直接定义一个结构体的变量
static struct cdev test_cdev;
  • 现在:我们定义一个结构体指针,这时候还没有生成对应的结构体内存空间
static struct cdev *pcdev = NULL;

驱动层代码修改:

  • 指针实例化
	// 第2步:注册字符设备驱动
	pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化
	cdev_init(pcdev, &test_fops);
  • 内存释放(del 的时候,同时相当于我们进行了 free)
cdev_del(pcdev);

(2)从内存角度体会 cdev_alloc 用与不用的差别

  • 变量的内存从哪里来:

1、全局变量:.data 数据段, bss段 ——> 程序在启动的时候被加载,结束的时候被释放,(所以在整个程序运行的时候都存在,没有被释放)
2、局部变量:栈 ——> 子函数执行时被申请,子函数结束的时候被释放
3、malloc :堆 ——> malloc 时候申请,free 的时候释放。

  • 不用 cdev_alloc ,使用全局变量的缺点:

1、会一直占用内存资源,不够灵活,我们不想用的时候,不能释放。

  • 使用 cdev_alloc 的优点:

1、我们可以按照自己的需求,来分配内存资源。

(3)这就是非面向对象的语言和面向对象的代码

6、cdev_init 的替代

  • 使用 cdev_init 的情况(之前)
	pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化
	cdev_init(pcdev, &test_fops);
  • 不使用 cdev_init 的情况
	pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化	
	pcdev->owner = THIS_MODULE;
	pcdev->ops = &test_fops;

(1)cdev_init源码分析

 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}
struct cdev {
   struct kobject kobj;          // 内嵌的内核对象,每个 cdev 都是一个 kobject
   struct module *owner;       // 指向实现驱动的模块
   const struct file_operations *ops;   // 操纵这个字符设备文件的方法
   struct list_head list;       // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头,用来将已经向内核注册的所有字符设备形成链表
   dev_t dev;                   // 字符设备的设备号,由主设备号和次设备号构成
   unsigned int count;       // 隶属于同一主设备号的次设备号的个数.
};
// 自定义一个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,
};

最关键的代码:(cdev 结构体当中的 ops 指针 和 我们的 file_operations 来绑定)

cdev->ops = fops;

二、分析内核源代码

1、之前我们的内核源码是自己写的。
2、接下来我们,分析内核当中的内核源码

1、老接口分析

1、register_chrdev
	__register_chrdev
		__register_chrdev_region (返回设备号)
		cdev_alloc (分配 cdev 的空间)
		cdev_add (添加驱动,注册模块)

(1)register_chrdev 当中只调用了 __register_chrdev 函数

1、 __register_chrdev 函数 :多了一个 0 ,这个 0 就是次设备号
2、所以这里就将次设备号写死了

在这里插入图片描述
(2)__register_chrdev 函数调用了 __register_chrdev_regioncdev_alloccdev_add

1、因为它同时调用了这么多函数。所以同时完成了设备号的返回 + 模块的注册

在这里插入图片描述
注意:
我们分析注册函数,注销函数类似,我们省略分析

2、新接口分析

register_chrdev_region
	__register_chrdev_region

alloc_chrdev_region
	__register_chrdev_region

cdev_alloc (分配 cdev 的空间)
cdev_add (添加驱动,注册模块)

(1)register_chrdev_region 当中调用了 __register_chrdev_region

申请注册设备号
在这里插入图片描述

(2)alloc_chrdev_region__register_chrdev_region

注册模块驱动
在这里插入图片描述

三、自动创建字符设备驱动的设备文件

之前我们需要在操作当中使用 mknod 命令来创建,字符设备驱动的设备文件

mknod /dev/test c 250 0

现在:我们添加代码,以至于他自己自动创建 字符设备驱动的设备文件

1、问题描述:

(1)整体流程回顾:应用层通过读写设备文件/dev/test),来和我们的驱动层进行交互。
在这里插入图片描述

(2)使用mknod创建设备文件的缺点

(3)我们希望:能否自动生成和删除设备文件

insmod 的时候:自动生成 字符设备驱动的设备文件
rmmod 的时候:自动删除 字符设备驱动的设备文件

2、解决方案:udev(嵌入式中用的是mdev)

(1)什么是udev?应用层的一个应用程序

mdev:是 busybox 当中的一个命令
为什么它要在应用层来创建这个文件?而不在内核当中创建这个文件?
因为 /dev/test 是一个用户文件,属于应用层,不属于驱动层。

(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
在这里插入图片描述

驱动注册和注销时信息会被传给udev,由 udev应用层进行设备文件的创建和删除
1、当我们 insmod 的时候,向我们的内核发送安装信息。
2、内核接受到安装信息之后,向 mdev 发送创建设备驱动文件。

(3)应用层启用udev,内核驱动中使用相应接口

1、我们之前在根文件系统当中,就已经启动了 udev
在这里插入图片描述

2、但是我们内核驱动中,没有使用相应接口

3、内核驱动设备类相关函数

(1)class_create

static struct class *test_class;

test_class = class_create(THIS_MODULE, "aston_class");

参数分析:
THIS_MODULE :内核当中的宏定义
"aston_class" :一个类名
返回值:
test_class :这个内的首地址

  • 模块安装函数

1、在第2 步:注册字符设备驱动完毕之后: 添加设备类的操作,以让内核帮我们发信息

// 第2步:注册字符设备驱动
	pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化
	//cdev_init(pcdev, &test_fops);
	pcdev->owner = THIS_MODULE;
	pcdev->ops = &test_fops;

	retval = cdev_add(pcdev, mydev, MYCNT);
	if (retval) {
		printk(KERN_ERR "Unable to cdev_add\n");
		goto flag2;
	}
	printk(KERN_INFO "cdev_add success\n");
	
	// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
	// 给udev,让udev自动创建和删除设备文件
	test_class = class_create(THIS_MODULE, "aston_class");
	if (IS_ERR(test_class))
		return -EINVAL;

(2)device_create

创建设备文件的函数:
参数分析:
test_classclass_create 返回的类的首地址
NULL :
mydev :我们的主次设备号
"test111" :我们将来要在 /dev 目录下创建的设备文件的名字

// 注册字符设备驱动完成后,添加设备类的操作,以让内核帮我们发信息
	// 给udev,让udev自动创建和删除设备文件
	test_class = class_create(THIS_MODULE, "aston_class");
	if (IS_ERR(test_class))
		return -EINVAL;

	// 最后1个参数字符串,就是我们将来要在/dev目录下创建的设备文件的名字
	// 所以我们这里要的文件名是/dev/test
	device_create(test_class, NULL, mydev, NULL, "test");

四、设备类相关代码分析

1、sys文件系统简介

1、sys文件系统:这是一个虚拟文件系统
2、相当于一个窗户,让我们可以和内核进行交互

(1)sys文件系统的设计思想

1、将我们内核当中的一些数据结构,通过这个虚拟文件系统传入到应用层的文件中
2、应用层我们 read 这个文件,相当于将内核当中的数据结构读出来。
3、应用层我们 write 这个文件,相当于将内核当中的数据结构当中的变量修改。

(2)设备类的概念
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)/sys/class/xxx/ 中的文件的作用

相当于一个窗户,让我们可以通过它,来查看一些内核当中的信息。、

2、class_create 和 device_create分析

(1)class_create

class_create();
	__class_create();
		__class_register();
			kset_register();
				kobject_uevent();

(2)device_create

	device_create_vargs();
		kobject_set_name_vargs();
		device_register();
			device_add();
				kobject_add();
					device_create_file();
					device_create_sys_dev_entry();
					devtmpfs_create_node();
					device_add_class_symlinks();
					device_add_attrs();
					device_pm_add();
					kobject_uevent();

五、静态映射表建立过程分析

1、建立映射表的三个关键部分

(1)映射表具体物理地址,和虚拟地址的值相关的宏定义 (具体参考上篇博客)
在这里插入图片描述

(2)映射表建立函数。smdkc110_map_io

该函数负责由(1)中的映射表,来建立linux内核的页表映射关系
页表:由虚拟地址查表,然后得到对应的物理地址。

kernel/arch/arm/mach-s5pv210/mach-smdkc110.c 中的 smdkc110_map_io 函数

smdkc110_map_io()
	s5p_init_io()
		iotable_init()

在这里插入图片描述

结论:经过分析,真正的内核移植时,给定的静态映射表arch/arm/plat-s5p/cpu.c中的s5p_iodesc

1、本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。
2、这个结构体数组所记录的几个映射关系被 iotable_init 所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。

总结:将宏定义,转化为 结构体数组

(3)开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io 怎么被调用的?

start_kernel()
	setup_arch()
		paging_init()
			devicemaps_init()
			
if (mdesc->map_io)
		mdesc->map_io();

六、动态映射结构体方式操作寄存器

1、问题描述

(1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。来代替我们上篇博客中讲的多次映射。

在上篇博客最后,我们是以,一个寄存器一个寄存器的方式,来进行内存映射

2、实践编码

typedef struct GPJ0REG
{
	volatile unsigned int gpj0con;
	volatile unsigned int gpj0dat;
}gpj0_reg_t;

#define GPJ0_REGBASE	0xe0200240    //物理地址
gpj0_reg_t *pGPJ0REG;

// 使用动态映射的方式来操作寄存器
	if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))
		return -EINVAL;
	pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
	// 映射之后用指向结构体的指针来进行操作
	// 指针使用->结构体内元素的方式来操作各个寄存器
	pGPJ0REG->gpj0con = 0x11111111;
	pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));		// 亮
	
模块卸载函数
// 解除映射
	iounmap(pGPJ0REG);
	release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));

1、ioremap 返回一个 void * 类型的指针。是我们映射后的虚拟地址的首地址
2、pGPJ0REG :通过结构体指针来接收它。
3、最后我们就可以通过这个指针来调用结构体当中的成员。

	pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));
	// 映射之后用指向结构体的指针来进行操作
	// 指针使用->结构体内元素的方式来操作各个寄存器
	pGPJ0REG->gpj0con = 0x11111111;
	pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));		// 亮

七、内核提供的读写寄存器接口

1、之前访问寄存器

通过指针,直接操作内存,从而实现对寄存器的读写

缺点:

1、不好进行移植

2、内核提供的寄存器读写接口
在这里插入图片描述

1、readb:读 1 个字节
2、readw:读 2 个字节

(1)writelreadl

(2)iowrite32ioread32

代码实践

用静态映射的虚拟地址来操作
#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT

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

定义物理地址,然后动态映射的时候使用
#define GPJ0CON_PA	0xe0200240
#define GPJ0DAT_PA 	0xe0200244

#define S5P_GPJ0REG(x)		(x)
#define S5P_GPJ0CON			S5P_GPJ0REG(0)
#define S5P_GPJ0DAT			S5P_GPJ0REG(4)

unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;

static void __iomem *baseaddr;			// 寄存器的虚拟地址的基地址


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

/***********/ 使用内部读写接口的方法  /***********/

 测试1:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
writel(0x11111111, pGPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), pGPJ0DAT);
	
测试2:用静态映射的虚拟地址来操作,测试成功
writel(0x11111111, GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), GPJ0DAT);
	
测试3:用1次ioremap映射多个寄存器得到虚拟地址,测试成功
if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
		return -EINVAL;
baseaddr = ioremap(GPJ0CON_PA, 8);
	
writel(0x11111111, baseaddr + S5P_GPJ0CON);
writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

想文艺一点的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值